@douglasneuroinformatics/libui 4.2.1 → 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-6C6CJESI.js → chunk-LSGD4EQY.js} +2 -2
- package/dist/{chunk-ARKHRGTL.js → chunk-VFVO337W.js} +17 -1
- package/dist/chunk-VFVO337W.js.map +1 -0
- package/dist/components.d.ts +2 -16
- package/dist/components.js +71 -156
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +2 -2
- package/dist/i18n.d.ts +2 -2
- package/dist/i18n.js +1 -1
- 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/index.ts +0 -1
- package/src/i18n/translations/libui.json +16 -0
- package/dist/chunk-ARKHRGTL.js.map +0 -1
- /package/dist/{chunk-6C6CJESI.js.map → chunk-LSGD4EQY.js.map} +0 -0
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;
|
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';
|
package/src/components/index.ts
CHANGED
|
@@ -33,7 +33,6 @@ export * from './ListboxDropdown';
|
|
|
33
33
|
export * from './MenuBar';
|
|
34
34
|
export * from './NotificationHub';
|
|
35
35
|
export * from './OneTimePasswordInput';
|
|
36
|
-
export * from './Pagination';
|
|
37
36
|
export * from './Popover';
|
|
38
37
|
export * from './Progress';
|
|
39
38
|
export * from './RadioGroup';
|
|
@@ -142,6 +142,10 @@
|
|
|
142
142
|
"en": "<< First",
|
|
143
143
|
"fr": "<< Première"
|
|
144
144
|
},
|
|
145
|
+
"first": {
|
|
146
|
+
"en": "First",
|
|
147
|
+
"fr": "Première"
|
|
148
|
+
},
|
|
145
149
|
"info": {
|
|
146
150
|
"en": "Showing {{first}} to {{last}} of {{total}} results",
|
|
147
151
|
"fr": "Affichage de {{first}} à {{last}} sur {{total}} résultats"
|
|
@@ -150,6 +154,10 @@
|
|
|
150
154
|
"en": "Last >>",
|
|
151
155
|
"fr": "Dernière >>"
|
|
152
156
|
},
|
|
157
|
+
"last": {
|
|
158
|
+
"en": "Last",
|
|
159
|
+
"fr": "Dernière"
|
|
160
|
+
},
|
|
153
161
|
"next": {
|
|
154
162
|
"en": "Next",
|
|
155
163
|
"fr": "Suivant"
|
|
@@ -164,5 +172,13 @@
|
|
|
164
172
|
"en": "Search...",
|
|
165
173
|
"fr": "Rechercher..."
|
|
166
174
|
}
|
|
175
|
+
},
|
|
176
|
+
"yes": {
|
|
177
|
+
"en": "Yes",
|
|
178
|
+
"fr": "Oui"
|
|
179
|
+
},
|
|
180
|
+
"no": {
|
|
181
|
+
"en": "No",
|
|
182
|
+
"fr": "Non"
|
|
167
183
|
}
|
|
168
184
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/i18n/store.ts","../src/i18n/translations/libui.json","../src/i18n/internal.ts"],"sourcesContent":["import type { SetOptional } from 'type-fest';\nimport { subscribeWithSelector } from 'zustand/middleware';\nimport { createStore } from 'zustand/vanilla';\n\nimport libui from '@/i18n/translations/libui.json';\n\nimport { getTranslation } from './internal';\n\nimport type { Language, TranslateFunction, Translations } from './types';\n\ntype InitOptions = {\n defaultLanguage?: Language;\n fallbackLanguage?: Language;\n translations?: SetOptional<Translations, 'libui'>;\n};\n\ntype I18N = {\n init: (options?: InitOptions) => void;\n t: TranslateFunction;\n};\n\nexport type TranslationStore = {\n changeLanguage: (language: Language) => void;\n fallbackLanguage: Language;\n isInitialized: boolean;\n resolvedLanguage: Language;\n translations: Translations;\n};\n\nexport const translationStore = createStore(\n subscribeWithSelector<TranslationStore>((set) => ({\n changeLanguage(language) {\n set({ resolvedLanguage: language });\n },\n fallbackLanguage: 'en',\n isInitialized: false,\n resolvedLanguage: 'en',\n translations: { libui }\n }))\n);\n\nexport const i18n: I18N = {\n init: ({ defaultLanguage, fallbackLanguage, translations }: InitOptions = {}) => {\n const state = translationStore.getState();\n if (state.isInitialized) {\n console.error('Cannot reinitialize translations store');\n return;\n }\n translationStore.subscribe(\n (state) => state.resolvedLanguage,\n (resolvedLanguage) => {\n document.documentElement.lang = resolvedLanguage;\n }\n );\n translationStore.setState({\n fallbackLanguage: fallbackLanguage ?? state.fallbackLanguage,\n isInitialized: true,\n resolvedLanguage: defaultLanguage ?? state.resolvedLanguage,\n translations: {\n ...state.translations,\n ...translations\n }\n });\n },\n t: (target, ...args) => {\n const state = translationStore.getState();\n return getTranslation(target, state, ...args);\n }\n};\n","{\n \"days\": {\n \"friday\": {\n \"en\": \"Friday\",\n \"fr\": \"Vendredi\"\n },\n \"monday\": {\n \"en\": \"Monday\",\n \"fr\": \"Lundi\"\n },\n \"saturday\": {\n \"en\": \"Saturday\",\n \"fr\": \"Samedi\"\n },\n \"sunday\": {\n \"en\": \"Sunday\",\n \"fr\": \"Dimanche\"\n },\n \"thursday\": {\n \"en\": \"Thursday\",\n \"fr\": \"Jeudi\"\n },\n \"tuesday\": {\n \"en\": \"Tuesday\",\n \"fr\": \"Mardi\"\n },\n \"wednesday\": {\n \"en\": \"Wednesday\",\n \"fr\": \"Mercredi\"\n }\n },\n \"form\": {\n \"append\": {\n \"en\": \"Append\",\n \"fr\": \"Ajouter\"\n },\n \"radioLabels\": {\n \"false\": {\n \"en\": \"False\",\n \"fr\": \"Faux\"\n },\n \"true\": {\n \"en\": \"True\",\n \"fr\": \"Vrai\"\n }\n },\n \"remove\": {\n \"en\": \"Remove\",\n \"fr\": \"Supprimer\"\n },\n \"required\": {\n \"en\": \"This field is required\",\n \"fr\": \"Ce champ est obligatoire\"\n },\n \"reset\": {\n \"en\": \"Reset\",\n \"fr\": \"Réinitialiser\"\n },\n \"submit\": {\n \"en\": \"Submit\",\n \"fr\": \"Soumettre\"\n }\n },\n \"months\": {\n \"april\": {\n \"en\": \"April\",\n \"fr\": \"Avril\"\n },\n \"august\": {\n \"en\": \"August\",\n \"fr\": \"Août\"\n },\n \"december\": {\n \"en\": \"December\",\n \"fr\": \"Décembre\"\n },\n \"february\": {\n \"en\": \"February\",\n \"fr\": \"Février\"\n },\n \"january\": {\n \"en\": \"January\",\n \"fr\": \"Janvier\"\n },\n \"july\": {\n \"en\": \"July\",\n \"fr\": \"Juillet\"\n },\n \"june\": {\n \"en\": \"June\",\n \"fr\": \"Juin\"\n },\n \"march\": {\n \"en\": \"March\",\n \"fr\": \"Mars\"\n },\n \"may\": {\n \"en\": \"May\",\n \"fr\": \"Mai\"\n },\n \"november\": {\n \"en\": \"November\",\n \"fr\": \"Novembre\"\n },\n \"october\": {\n \"en\": \"October\",\n \"fr\": \"Octobre\"\n },\n \"september\": {\n \"en\": \"September\",\n \"fr\": \"Septembre\"\n }\n },\n \"notifications\": {\n \"types\": {\n \"error\": {\n \"en\": \"Error\",\n \"fr\": \"Erreur\"\n },\n \"info\": {\n \"en\": \"Info\",\n \"fr\": \"Attention\"\n },\n \"success\": {\n \"en\": \"Success\",\n \"fr\": \"Succès\"\n },\n \"warning\": {\n \"en\": \"Warning\",\n \"fr\": \"Avertissement\"\n }\n }\n },\n \"oneTimePasswordInput\": {\n \"invalidCodeFormat\": {\n \"en\": \"Invalid code format\",\n \"fr\": \"Format de code invalide\"\n }\n },\n \"pagination\": {\n \"firstPage\": {\n \"en\": \"<< First\",\n \"fr\": \"<< Première\"\n },\n \"info\": {\n \"en\": \"Showing {{first}} to {{last}} of {{total}} results\",\n \"fr\": \"Affichage de {{first}} à {{last}} sur {{total}} résultats\"\n },\n \"lastPage\": {\n \"en\": \"Last >>\",\n \"fr\": \"Dernière >>\"\n },\n \"next\": {\n \"en\": \"Next\",\n \"fr\": \"Suivant\"\n },\n \"previous\": {\n \"en\": \"Previous\",\n \"fr\": \"Précédent\"\n }\n },\n \"searchBar\": {\n \"placeholder\": {\n \"en\": \"Search...\",\n \"fr\": \"Rechercher...\"\n }\n }\n}\n","import { format } from '@douglasneuroinformatics/libjs';\nimport { get } from 'lodash-es';\nimport type { Primitive } from 'type-fest';\n\nimport type { Language } from './types';\n\nexport function getTranslation(\n target: string | { [L in Language]?: string },\n state: {\n fallbackLanguage: Language;\n resolvedLanguage: Language;\n translations: { [key: string]: any };\n },\n ...args: Exclude<Primitive, symbol>[]\n) {\n let value: { [key: string]: string };\n if (typeof target === 'string') {\n value = get(state.translations, target) as { [key: string]: string };\n } else {\n value = target;\n }\n return format((value[state.resolvedLanguage] ?? value[state.fallbackLanguage])!, ...args);\n}\n"],"mappings":"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","names":["state"]}
|
|
File without changes
|