@douglasneuroinformatics/libui 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZIAKQCCQ.js → chunk-LSGD4EQY.js} +2 -2
- package/dist/chunk-LSGD4EQY.js.map +1 -0
- package/dist/{chunk-ARKHRGTL.js → chunk-VFVO337W.js} +17 -1
- package/dist/chunk-VFVO337W.js.map +1 -0
- package/dist/components.d.ts +4 -18
- package/dist/components.js +404 -486
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +2 -2
- package/dist/i18n.d.ts +2 -2
- package/dist/i18n.js +1 -1
- package/dist/tailwind/globals.css +5 -0
- package/dist/{types-DHTtLrqP.d.ts → types-DDyMlEuL.d.ts} +19 -1
- package/package.json +2 -1
- package/src/components/DataTable/DataTable.stories.tsx +76 -0
- package/src/components/DataTable/DataTable.tsx +289 -0
- package/src/components/DataTable/DestructiveActionDialog.tsx +71 -0
- package/src/components/DataTable/RowActionsDropdown.tsx +64 -0
- package/src/components/DataTable/index.ts +1 -0
- package/src/components/Form/ErrorMessage.tsx +1 -1
- package/src/components/Form/Form.stories.tsx +3 -0
- package/src/components/Form/Form.tsx +27 -21
- package/src/components/Form/NumberRecordField.tsx +2 -2
- package/src/components/Form/RecordArrayField.tsx +3 -3
- package/src/components/OneTimePasswordInput/OneTimePasswordInput.tsx +1 -1
- package/src/components/index.ts +0 -1
- package/src/hooks/useOnClickOutside/useOnClickOutside.ts +1 -1
- package/src/i18n/translations/libui.json +16 -0
- package/src/tailwind/globals.css +5 -0
- package/dist/chunk-ARKHRGTL.js.map +0 -1
- package/dist/chunk-ZIAKQCCQ.js.map +0 -1
package/dist/hooks.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { D as DEFAULT_THEME, a as SYS_DARK_MEDIA_QUERY, S as StorageName, b as T
|
|
|
3
3
|
import { Promisable } from 'type-fest';
|
|
4
4
|
import { RefObject, useEffect, Dispatch, SetStateAction } from 'react';
|
|
5
5
|
import * as zustand from 'zustand';
|
|
6
|
-
import { T as TranslationNamespace, L as Language, a as TranslateFunction } from './types-
|
|
6
|
+
import { T as TranslationNamespace, L as Language, a as TranslateFunction } from './types-DDyMlEuL.js';
|
|
7
7
|
|
|
8
8
|
declare function useChart(): {
|
|
9
9
|
config: ChartConfig;
|
|
@@ -64,7 +64,7 @@ type NotificationsStore = {
|
|
|
64
64
|
declare const useNotificationsStore: zustand.UseBoundStore<zustand.StoreApi<NotificationsStore>>;
|
|
65
65
|
|
|
66
66
|
type Handler = (event: MouseEvent) => void;
|
|
67
|
-
declare function useOnClickOutside<T extends HTMLElement = HTMLElement>(ref: RefObject<T>, handler: Handler, mouseEvent?: 'mousedown' | 'mouseup'): void;
|
|
67
|
+
declare function useOnClickOutside<T extends HTMLElement | null = HTMLElement>(ref: RefObject<T>, handler: Handler, mouseEvent?: 'mousedown' | 'mouseup'): void;
|
|
68
68
|
|
|
69
69
|
/** Custom hook that uses local storage to persist state across page reloads */
|
|
70
70
|
declare function useLocalStorage<T>(key: string, initialValue: (() => T) | T, options?: UseStorageOptions<T>): [T, Dispatch<SetStateAction<T>>];
|
package/dist/hooks.js
CHANGED
package/dist/i18n.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as zustand from 'zustand';
|
|
2
2
|
import { SetOptional } from 'type-fest';
|
|
3
|
-
import { L as Language, b as Translations, a as TranslateFunction } from './types-
|
|
4
|
-
export { E as ExtractTranslationKey, c as LanguageOptions, d as TranslationKey, T as TranslationNamespace, U as UserConfig } from './types-
|
|
3
|
+
import { L as Language, b as Translations, a as TranslateFunction } from './types-DDyMlEuL.js';
|
|
4
|
+
export { E as ExtractTranslationKey, c as LanguageOptions, d as TranslationKey, T as TranslationNamespace, U as UserConfig } from './types-DDyMlEuL.js';
|
|
5
5
|
|
|
6
6
|
type InitOptions = {
|
|
7
7
|
defaultLanguage?: Language;
|
package/dist/i18n.js
CHANGED
|
@@ -143,6 +143,10 @@ var pagination = {
|
|
|
143
143
|
en: "<< First",
|
|
144
144
|
fr: "<< Première"
|
|
145
145
|
},
|
|
146
|
+
first: {
|
|
147
|
+
en: "First",
|
|
148
|
+
fr: "Première"
|
|
149
|
+
},
|
|
146
150
|
info: {
|
|
147
151
|
en: "Showing {{first}} to {{last}} of {{total}} results",
|
|
148
152
|
fr: "Affichage de {{first}} à {{last}} sur {{total}} résultats"
|
|
@@ -151,6 +155,10 @@ var pagination = {
|
|
|
151
155
|
en: "Last >>",
|
|
152
156
|
fr: "Dernière >>"
|
|
153
157
|
},
|
|
158
|
+
last: {
|
|
159
|
+
en: "Last",
|
|
160
|
+
fr: "Dernière"
|
|
161
|
+
},
|
|
154
162
|
next: {
|
|
155
163
|
en: "Next",
|
|
156
164
|
fr: "Suivant"
|
|
@@ -166,6 +174,14 @@ var searchBar = {
|
|
|
166
174
|
fr: "Rechercher..."
|
|
167
175
|
}
|
|
168
176
|
};
|
|
177
|
+
var yes = {
|
|
178
|
+
en: "Yes",
|
|
179
|
+
fr: "Oui"
|
|
180
|
+
};
|
|
181
|
+
var no = {
|
|
182
|
+
en: "No",
|
|
183
|
+
fr: "Non"
|
|
184
|
+
};
|
|
169
185
|
var libuiTranslations = {
|
|
170
186
|
days: days,
|
|
171
187
|
form: form,
|
|
@@ -173,7 +189,9 @@ var libuiTranslations = {
|
|
|
173
189
|
notifications: notifications,
|
|
174
190
|
oneTimePasswordInput: oneTimePasswordInput,
|
|
175
191
|
pagination: pagination,
|
|
176
|
-
searchBar: searchBar
|
|
192
|
+
searchBar: searchBar,
|
|
193
|
+
yes: yes,
|
|
194
|
+
no: no
|
|
177
195
|
};
|
|
178
196
|
|
|
179
197
|
declare namespace UserConfig {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@douglasneuroinformatics/libui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.3.0",
|
|
5
5
|
"packageManager": "pnpm@10.7.1",
|
|
6
6
|
"description": "Generic UI components for DNP projects, built using React and Tailwind CSS",
|
|
7
7
|
"author": "Joshua Unrau",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"@radix-ui/react-switch": "^1.1.3",
|
|
93
93
|
"@radix-ui/react-tabs": "^1.1.3",
|
|
94
94
|
"@radix-ui/react-tooltip": "^1.1.8",
|
|
95
|
+
"@tanstack/react-table": "^8.21.3",
|
|
95
96
|
"class-variance-authority": "^0.7.1",
|
|
96
97
|
"clsx": "^2.1.1",
|
|
97
98
|
"cmdk": "^1.1.1",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { range, toBasicISOString } from '@douglasneuroinformatics/libjs';
|
|
2
|
+
import { faker } from '@faker-js/faker';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
|
|
5
|
+
import { DataTable } from './DataTable';
|
|
6
|
+
|
|
7
|
+
import type { DataTableColumn } from './DataTable';
|
|
8
|
+
|
|
9
|
+
type User = {
|
|
10
|
+
birthday: Date;
|
|
11
|
+
email: string;
|
|
12
|
+
firstName: string;
|
|
13
|
+
lastName: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof DataTable<User>>;
|
|
17
|
+
|
|
18
|
+
const columns: DataTableColumn<User>[] = [
|
|
19
|
+
{
|
|
20
|
+
format: (value) => {
|
|
21
|
+
return toBasicISOString(value);
|
|
22
|
+
},
|
|
23
|
+
key: 'birthday',
|
|
24
|
+
label: 'Birthday'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
format: 'email',
|
|
28
|
+
key: 'email',
|
|
29
|
+
label: 'Email',
|
|
30
|
+
sortable: true
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const data: User[] = range(60)
|
|
35
|
+
.unwrap()
|
|
36
|
+
.map(() => ({
|
|
37
|
+
birthday: faker.date.birthdate(),
|
|
38
|
+
email: faker.internet.email(),
|
|
39
|
+
firstName: faker.person.firstName(),
|
|
40
|
+
lastName: faker.person.lastName()
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
export default { component: DataTable } as Meta<typeof DataTable<User>>;
|
|
44
|
+
|
|
45
|
+
export const Default: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
columns,
|
|
48
|
+
data,
|
|
49
|
+
headerAction: {
|
|
50
|
+
label: 'Do Something',
|
|
51
|
+
onClick: () => {
|
|
52
|
+
alert('Something!');
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
rowActions: [
|
|
56
|
+
{
|
|
57
|
+
destructive: true,
|
|
58
|
+
label: 'Delete',
|
|
59
|
+
onSelect: (row) => {
|
|
60
|
+
alert(`Delete User: ${row.firstName} ${row.lastName}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
search: {
|
|
65
|
+
key: 'email',
|
|
66
|
+
placeholder: 'Filter emails...'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Empty: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
columns,
|
|
74
|
+
data: []
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Fragment, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
flexRender,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getFilteredRowModel,
|
|
7
|
+
getPaginationRowModel,
|
|
8
|
+
getSortedRowModel,
|
|
9
|
+
useReactTable
|
|
10
|
+
} from '@tanstack/react-table';
|
|
11
|
+
import type { ColumnDef, ColumnFiltersState, SortingState } from '@tanstack/react-table';
|
|
12
|
+
import { range } from 'lodash-es';
|
|
13
|
+
import { ArrowUpDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon } from 'lucide-react';
|
|
14
|
+
import type { Promisable } from 'type-fest';
|
|
15
|
+
|
|
16
|
+
import { useTranslation } from '@/hooks';
|
|
17
|
+
|
|
18
|
+
import { Button } from '../Button';
|
|
19
|
+
import { SearchBar } from '../SearchBar';
|
|
20
|
+
import { Table } from '../Table';
|
|
21
|
+
import { DestructiveActionDialog } from './DestructiveActionDialog';
|
|
22
|
+
import { RowActionsDropdown } from './RowActionsDropdown';
|
|
23
|
+
|
|
24
|
+
import type { RowAction } from './RowActionsDropdown';
|
|
25
|
+
|
|
26
|
+
type StaticDataTableColumn<TData extends { [key: string]: unknown }> = {
|
|
27
|
+
[K in Extract<keyof TData, string>]: {
|
|
28
|
+
format?: 'email' | ((val: TData[K]) => unknown);
|
|
29
|
+
key: K;
|
|
30
|
+
label: string;
|
|
31
|
+
sortable?: boolean;
|
|
32
|
+
};
|
|
33
|
+
}[Extract<keyof TData, string>];
|
|
34
|
+
|
|
35
|
+
type DynamicDataTableColumn<TData extends { [key: string]: unknown }> = {
|
|
36
|
+
compute: (row: TData) => unknown;
|
|
37
|
+
key?: never;
|
|
38
|
+
label: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type DataTableColumn<TData extends { [key: string]: unknown }> =
|
|
42
|
+
| DynamicDataTableColumn<TData>
|
|
43
|
+
| StaticDataTableColumn<TData>;
|
|
44
|
+
|
|
45
|
+
type DataTableProps<TData extends { [key: string]: unknown }> = {
|
|
46
|
+
columns: DataTableColumn<TData>[];
|
|
47
|
+
data: TData[];
|
|
48
|
+
headerAction?: {
|
|
49
|
+
label: string;
|
|
50
|
+
onClick: () => void;
|
|
51
|
+
};
|
|
52
|
+
rowActions?: RowAction<TData>[];
|
|
53
|
+
search?: {
|
|
54
|
+
key: Extract<keyof TData, string>;
|
|
55
|
+
placeholder?: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function isStaticColumn<TData extends { [key: string]: unknown }>(
|
|
60
|
+
column: DataTableColumn<TData>
|
|
61
|
+
): column is StaticDataTableColumn<TData> {
|
|
62
|
+
return typeof column.key === 'string';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const DataTable = <TData extends { [key: string]: unknown }>({
|
|
66
|
+
columns,
|
|
67
|
+
data,
|
|
68
|
+
headerAction,
|
|
69
|
+
rowActions,
|
|
70
|
+
search
|
|
71
|
+
}: DataTableProps<TData>) => {
|
|
72
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
73
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
74
|
+
const [searchValue, setSearchValue] = useState('');
|
|
75
|
+
const [destructiveActionPending, setDestructiveActionPending] = useState<(() => Promisable<void>) | null>(null);
|
|
76
|
+
const { t } = useTranslation('libui');
|
|
77
|
+
const [pagination, setPagination] = useState({
|
|
78
|
+
pageIndex: 0,
|
|
79
|
+
pageSize: 10
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const columnDefs = useMemo<ColumnDef<TData, unknown>[]>(() => {
|
|
83
|
+
const result: ColumnDef<TData, unknown>[] = columns.map((col) => {
|
|
84
|
+
let def: ColumnDef<TData, unknown>;
|
|
85
|
+
if (isStaticColumn(col)) {
|
|
86
|
+
def = {
|
|
87
|
+
accessorKey: col.key,
|
|
88
|
+
header: col.sortable
|
|
89
|
+
? ({ column }) => (
|
|
90
|
+
<button
|
|
91
|
+
className="flex items-center justify-between gap-2"
|
|
92
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
93
|
+
>
|
|
94
|
+
{col.label}
|
|
95
|
+
<ArrowUpDownIcon className="h-4 w-4" />
|
|
96
|
+
</button>
|
|
97
|
+
)
|
|
98
|
+
: col.label
|
|
99
|
+
};
|
|
100
|
+
if (col.format) {
|
|
101
|
+
def.cell = ({ getValue }) => {
|
|
102
|
+
const value = getValue() as TData[Extract<keyof TData, string>];
|
|
103
|
+
if (typeof col.format === 'function') {
|
|
104
|
+
return col.format(value);
|
|
105
|
+
} else if (col.format === 'email') {
|
|
106
|
+
return (
|
|
107
|
+
<a className="hover:underline" href={`mailto:${value as string}`}>
|
|
108
|
+
{value as string}
|
|
109
|
+
</a>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
def = {
|
|
117
|
+
accessorFn: col.compute,
|
|
118
|
+
header: col.label
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return def;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (rowActions) {
|
|
125
|
+
result.push({
|
|
126
|
+
cell: ({ row }) => {
|
|
127
|
+
return (
|
|
128
|
+
<RowActionsDropdown
|
|
129
|
+
row={row}
|
|
130
|
+
rowActions={rowActions}
|
|
131
|
+
setDestructiveActionPending={setDestructiveActionPending}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
id: '__actions'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}, [columns]);
|
|
140
|
+
|
|
141
|
+
const table = useReactTable({
|
|
142
|
+
columns: columnDefs,
|
|
143
|
+
data,
|
|
144
|
+
getCoreRowModel: getCoreRowModel(),
|
|
145
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
146
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
147
|
+
getSortedRowModel: getSortedRowModel(),
|
|
148
|
+
onColumnFiltersChange: setColumnFilters,
|
|
149
|
+
onPaginationChange: setPagination,
|
|
150
|
+
onSortingChange: setSorting,
|
|
151
|
+
state: {
|
|
152
|
+
columnFilters,
|
|
153
|
+
pagination,
|
|
154
|
+
sorting
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (search) {
|
|
160
|
+
table.getColumn(search.key)?.setFilterValue(searchValue);
|
|
161
|
+
}
|
|
162
|
+
}, [table, searchValue]);
|
|
163
|
+
|
|
164
|
+
const headerGroups = table.getHeaderGroups();
|
|
165
|
+
const { rows } = table.getRowModel();
|
|
166
|
+
|
|
167
|
+
const pageCount = table.getPageCount();
|
|
168
|
+
const currentPage = pagination.pageIndex;
|
|
169
|
+
|
|
170
|
+
const start = Math.max(0, Math.min(currentPage - 1, pageCount - 3));
|
|
171
|
+
const end = Math.min(start + 3, pageCount);
|
|
172
|
+
|
|
173
|
+
const pageIndexOptions = range(start, end);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Fragment>
|
|
177
|
+
<DestructiveActionDialog
|
|
178
|
+
destructiveActionPending={destructiveActionPending}
|
|
179
|
+
setDestructiveActionPending={setDestructiveActionPending}
|
|
180
|
+
/>
|
|
181
|
+
{search && (
|
|
182
|
+
<div className="flex items-center gap-4 py-4">
|
|
183
|
+
<SearchBar
|
|
184
|
+
className="grow"
|
|
185
|
+
placeholder={search.placeholder}
|
|
186
|
+
value={searchValue}
|
|
187
|
+
onValueChange={setSearchValue}
|
|
188
|
+
/>
|
|
189
|
+
{headerAction && (
|
|
190
|
+
<Button type="button" variant="outline" onClick={headerAction.onClick}>
|
|
191
|
+
{headerAction.label}
|
|
192
|
+
</Button>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
<div className="rounded-md border">
|
|
197
|
+
<Table>
|
|
198
|
+
<Table.Header>
|
|
199
|
+
{headerGroups.map((headerGroup) => (
|
|
200
|
+
<Table.Row key={headerGroup.id}>
|
|
201
|
+
{headerGroup.headers.map((header) => {
|
|
202
|
+
return (
|
|
203
|
+
<Table.Head key={header.id}>
|
|
204
|
+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
205
|
+
</Table.Head>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</Table.Row>
|
|
209
|
+
))}
|
|
210
|
+
</Table.Header>
|
|
211
|
+
<Table.Body>
|
|
212
|
+
{rows?.length ? (
|
|
213
|
+
rows.map((row) => (
|
|
214
|
+
<Table.Row data-state={row.getIsSelected() && 'selected'} key={row.id}>
|
|
215
|
+
{row.getVisibleCells().map((cell) => (
|
|
216
|
+
<Table.Cell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Table.Cell>
|
|
217
|
+
))}
|
|
218
|
+
</Table.Row>
|
|
219
|
+
))
|
|
220
|
+
) : (
|
|
221
|
+
<Table.Row>
|
|
222
|
+
<Table.Cell className="h-24 text-center" colSpan={rowActions ? columns.length + 1 : columns.length}>
|
|
223
|
+
{t({
|
|
224
|
+
en: 'No Results',
|
|
225
|
+
fr: 'Aucun résultat'
|
|
226
|
+
})}
|
|
227
|
+
</Table.Cell>
|
|
228
|
+
</Table.Row>
|
|
229
|
+
)}
|
|
230
|
+
</Table.Body>
|
|
231
|
+
</Table>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="mx-auto flex w-min pt-6 pb-4">
|
|
234
|
+
<Button
|
|
235
|
+
className="flex gap-1"
|
|
236
|
+
disabled={!table.getCanPreviousPage()}
|
|
237
|
+
type="button"
|
|
238
|
+
variant="ghost"
|
|
239
|
+
onClick={() => table.firstPage()}
|
|
240
|
+
>
|
|
241
|
+
<ChevronsLeftIcon className="-ml-1 h-4 w-4" />
|
|
242
|
+
<span>{t('pagination.first')}</span>
|
|
243
|
+
</Button>
|
|
244
|
+
<Button
|
|
245
|
+
className="mr-1 flex gap-1"
|
|
246
|
+
disabled={!table.getCanPreviousPage()}
|
|
247
|
+
type="button"
|
|
248
|
+
variant="ghost"
|
|
249
|
+
onClick={() => table.previousPage()}
|
|
250
|
+
>
|
|
251
|
+
<ChevronLeftIcon className="-ml-1 h-4 w-4" />
|
|
252
|
+
<span>{t('pagination.previous')}</span>
|
|
253
|
+
</Button>
|
|
254
|
+
{pageIndexOptions.map((index) => (
|
|
255
|
+
<Button
|
|
256
|
+
key={index}
|
|
257
|
+
type="button"
|
|
258
|
+
variant={index === pagination.pageIndex ? 'outline' : 'ghost'}
|
|
259
|
+
onClick={() => table.setPageIndex(index)}
|
|
260
|
+
>
|
|
261
|
+
{index + 1}
|
|
262
|
+
</Button>
|
|
263
|
+
))}
|
|
264
|
+
<Button
|
|
265
|
+
className="ml-1 flex gap-1"
|
|
266
|
+
disabled={!table.getCanNextPage()}
|
|
267
|
+
type="button"
|
|
268
|
+
variant="ghost"
|
|
269
|
+
onClick={() => table.nextPage()}
|
|
270
|
+
>
|
|
271
|
+
<span>{t('pagination.next')}</span>
|
|
272
|
+
<ChevronRightIcon className="-mr-1 h-4 w-4" />
|
|
273
|
+
</Button>
|
|
274
|
+
<Button
|
|
275
|
+
className="flex gap-1"
|
|
276
|
+
disabled={!table.getCanNextPage()}
|
|
277
|
+
type="button"
|
|
278
|
+
variant="ghost"
|
|
279
|
+
onClick={() => table.lastPage()}
|
|
280
|
+
>
|
|
281
|
+
<span>{t('pagination.last')}</span>
|
|
282
|
+
<ChevronsRightIcon className="-mr-1 h-4 w-4" />
|
|
283
|
+
</Button>
|
|
284
|
+
</div>
|
|
285
|
+
</Fragment>
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export type { DataTableColumn };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
2
|
+
/* eslint-disable jsx-a11y/no-autofocus */
|
|
3
|
+
|
|
4
|
+
import type React from 'react';
|
|
5
|
+
|
|
6
|
+
import type { Promisable } from 'type-fest';
|
|
7
|
+
|
|
8
|
+
import { useTranslation } from '@/hooks';
|
|
9
|
+
|
|
10
|
+
import { Button } from '../Button';
|
|
11
|
+
import { Dialog } from '../Dialog';
|
|
12
|
+
|
|
13
|
+
export type DestructiveActionPending = (() => Promisable<void>) | null;
|
|
14
|
+
|
|
15
|
+
export const DestructiveActionDialog: React.FC<{
|
|
16
|
+
destructiveActionPending: DestructiveActionPending;
|
|
17
|
+
setDestructiveActionPending: React.Dispatch<React.SetStateAction<DestructiveActionPending>>;
|
|
18
|
+
}> = ({ destructiveActionPending, setDestructiveActionPending }) => {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
return (
|
|
21
|
+
<Dialog
|
|
22
|
+
open={destructiveActionPending !== null}
|
|
23
|
+
onOpenChange={(open) => {
|
|
24
|
+
if (!open) {
|
|
25
|
+
setDestructiveActionPending(null);
|
|
26
|
+
}
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<Dialog.Content>
|
|
30
|
+
<Dialog.Header>
|
|
31
|
+
<Dialog.Title>
|
|
32
|
+
{t({
|
|
33
|
+
en: 'Confirm Action',
|
|
34
|
+
fr: "Confirmer l'action"
|
|
35
|
+
})}
|
|
36
|
+
</Dialog.Title>
|
|
37
|
+
<Dialog.Description>
|
|
38
|
+
{t({
|
|
39
|
+
en: 'This action cannot be reversed. Please confirm that you would like to continue.',
|
|
40
|
+
fr: 'Cette action ne peut être inversée. Veuillez confirmer que vous souhaitez poursuivre.'
|
|
41
|
+
})}
|
|
42
|
+
</Dialog.Description>
|
|
43
|
+
</Dialog.Header>
|
|
44
|
+
<Dialog.Footer>
|
|
45
|
+
<Button
|
|
46
|
+
className="min-w-16"
|
|
47
|
+
size="sm"
|
|
48
|
+
type="button"
|
|
49
|
+
variant="outline"
|
|
50
|
+
onClick={async () => {
|
|
51
|
+
await destructiveActionPending?.();
|
|
52
|
+
setDestructiveActionPending(null);
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{t('libui.yes')}
|
|
56
|
+
</Button>
|
|
57
|
+
<Button
|
|
58
|
+
autoFocus={true}
|
|
59
|
+
className="min-w-16"
|
|
60
|
+
size="sm"
|
|
61
|
+
type="button"
|
|
62
|
+
variant="primary"
|
|
63
|
+
onClick={() => setDestructiveActionPending(null)}
|
|
64
|
+
>
|
|
65
|
+
{t('libui.no')}
|
|
66
|
+
</Button>
|
|
67
|
+
</Dialog.Footer>
|
|
68
|
+
</Dialog.Content>
|
|
69
|
+
</Dialog>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Row } from '@tanstack/react-table';
|
|
4
|
+
import { MoreHorizontalIcon } from 'lucide-react';
|
|
5
|
+
import type { Promisable } from 'type-fest';
|
|
6
|
+
|
|
7
|
+
import { useTranslation } from '@/hooks';
|
|
8
|
+
|
|
9
|
+
import { Button } from '../Button';
|
|
10
|
+
import { DropdownMenu } from '../DropdownMenu';
|
|
11
|
+
|
|
12
|
+
import type { DestructiveActionPending } from './DestructiveActionDialog';
|
|
13
|
+
|
|
14
|
+
export type RowAction<TData extends { [key: string]: unknown }> = {
|
|
15
|
+
destructive?: boolean;
|
|
16
|
+
label: string;
|
|
17
|
+
onSelect: (row: TData) => Promisable<void>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const RowActionsDropdown = <TData extends { [key: string]: unknown }>({
|
|
21
|
+
row,
|
|
22
|
+
rowActions,
|
|
23
|
+
setDestructiveActionPending
|
|
24
|
+
}: {
|
|
25
|
+
row: Row<TData>;
|
|
26
|
+
rowActions: RowAction<TData>[];
|
|
27
|
+
setDestructiveActionPending: React.Dispatch<React.SetStateAction<DestructiveActionPending>>;
|
|
28
|
+
}) => {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex w-full justify-end">
|
|
32
|
+
<DropdownMenu>
|
|
33
|
+
<DropdownMenu.Trigger asChild>
|
|
34
|
+
<Button className="-m-1.5" size="icon" variant="ghost">
|
|
35
|
+
<MoreHorizontalIcon className="h-4 w-4" />
|
|
36
|
+
</Button>
|
|
37
|
+
</DropdownMenu.Trigger>
|
|
38
|
+
<DropdownMenu.Content align="end">
|
|
39
|
+
<DropdownMenu.Label>
|
|
40
|
+
{t({
|
|
41
|
+
en: 'Actions',
|
|
42
|
+
fr: 'Actions'
|
|
43
|
+
})}
|
|
44
|
+
</DropdownMenu.Label>
|
|
45
|
+
{rowActions.map(({ destructive, label, onSelect }, i) => (
|
|
46
|
+
<DropdownMenu.Item
|
|
47
|
+
className={destructive ? 'text-destructive' : undefined}
|
|
48
|
+
key={i}
|
|
49
|
+
onSelect={() => {
|
|
50
|
+
if (destructive) {
|
|
51
|
+
setDestructiveActionPending(() => () => void onSelect(row.original));
|
|
52
|
+
} else {
|
|
53
|
+
void onSelect(row.original);
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{label}
|
|
58
|
+
</DropdownMenu.Item>
|
|
59
|
+
))}
|
|
60
|
+
</DropdownMenu.Content>
|
|
61
|
+
</DropdownMenu>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './DataTable';
|
|
@@ -6,7 +6,7 @@ import { cn } from '@/utils';
|
|
|
6
6
|
|
|
7
7
|
export const ErrorMessage: React.FC<{ className?: string; error?: null | string[] }> = ({ className, error }) => {
|
|
8
8
|
return error ? (
|
|
9
|
-
<div className="
|
|
9
|
+
<div className="flex flex-col gap-1.5">
|
|
10
10
|
{error.map((message) => (
|
|
11
11
|
<div className={cn('text-destructive flex w-full items-center text-sm font-medium', className)} key={message}>
|
|
12
12
|
<CircleAlertIcon className="mr-1" style={{ strokeWidth: '2px' }} />
|