@izumisy-tailor/tailor-data-viewer 0.2.30 → 0.2.32
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/package.json +1 -1
- package/src/component/{column-selector.test.tsx → data-table/column-selector.test.tsx} +2 -2
- package/src/component/{column-selector.tsx → data-table/column-selector.tsx} +7 -4
- package/src/component/{csv-button.test.tsx → data-table/csv-button.test.tsx} +2 -2
- package/src/component/{csv-button.tsx → data-table/csv-button.tsx} +5 -3
- package/src/component/data-table/data-table-context.tsx +4 -0
- package/src/component/data-table/data-table.test.tsx +10 -3
- package/src/component/data-table/data-table.tsx +20 -8
- package/src/component/data-table/i18n.ts +153 -0
- package/src/component/{pagination.test.tsx → data-table/pagination.test.tsx} +2 -2
- package/src/component/{pagination.tsx → data-table/pagination.tsx} +10 -8
- package/src/component/{search-filter-form.test.tsx → data-table/search-filter-form.test.tsx} +2 -2
- package/src/component/{search-filter-form.tsx → data-table/search-filter-form.tsx} +19 -14
- package/src/component/data-table/use-data-table.ts +4 -0
- package/src/component/index.ts +10 -6
- package/src/component/types.ts +5 -0
- package/src/tests/helpers.tsx +1 -0
- package/src/component/components.test.tsx +0 -349
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
import type { Column } from "
|
|
4
|
-
import { createTestProviders } from "
|
|
3
|
+
import type { Column } from "../types";
|
|
4
|
+
import { createTestProviders } from "../../tests/helpers";
|
|
5
5
|
import { ColumnSelector } from "./column-selector";
|
|
6
6
|
|
|
7
7
|
// =============================================================================
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useDataTableContext } from "./data-table
|
|
1
|
+
import { useDataTableContext } from "./data-table-context";
|
|
2
|
+
import { getLabels } from "./i18n";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Column visibility toggle dropdown.
|
|
@@ -20,12 +21,14 @@ export function ColumnSelector() {
|
|
|
20
21
|
toggleColumn,
|
|
21
22
|
showAllColumns,
|
|
22
23
|
hideAllColumns,
|
|
24
|
+
locale,
|
|
23
25
|
} = useDataTableContext();
|
|
26
|
+
const labels = getLabels(locale);
|
|
24
27
|
return (
|
|
25
28
|
<div className="relative inline-block">
|
|
26
29
|
<details className="group">
|
|
27
30
|
<summary className="inline-flex cursor-pointer items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground select-none">
|
|
28
|
-
|
|
31
|
+
{labels.columnSelector.columns}
|
|
29
32
|
</summary>
|
|
30
33
|
<div className="absolute right-0 z-50 mt-1 min-w-[180px] rounded-md border bg-popover p-2 text-popover-foreground shadow-md">
|
|
31
34
|
<div className="flex gap-2 border-b pb-2 mb-2">
|
|
@@ -34,14 +37,14 @@ export function ColumnSelector() {
|
|
|
34
37
|
className="text-xs text-muted-foreground hover:text-foreground"
|
|
35
38
|
onClick={showAllColumns}
|
|
36
39
|
>
|
|
37
|
-
|
|
40
|
+
{labels.columnSelector.selectAll}
|
|
38
41
|
</button>
|
|
39
42
|
<button
|
|
40
43
|
type="button"
|
|
41
44
|
className="text-xs text-muted-foreground hover:text-foreground"
|
|
42
45
|
onClick={hideAllColumns}
|
|
43
46
|
>
|
|
44
|
-
|
|
47
|
+
{labels.columnSelector.deselectAll}
|
|
45
48
|
</button>
|
|
46
49
|
</div>
|
|
47
50
|
{columns.map((col) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
-
import type { Column } from "
|
|
4
|
-
import { createTestProviders } from "
|
|
3
|
+
import type { Column } from "../types";
|
|
4
|
+
import { createTestProviders } from "../../tests/helpers";
|
|
5
5
|
import { CsvButton } from "./csv-button";
|
|
6
6
|
|
|
7
7
|
// =============================================================================
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useDataTableContext } from "./data-table
|
|
1
|
+
import { useDataTableContext } from "./data-table-context";
|
|
2
|
+
import { getLabels } from "./i18n";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* CSV export button.
|
|
@@ -15,7 +16,8 @@ import { useDataTableContext } from "./data-table/data-table-context";
|
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
18
|
export function CsvButton({ filename = "export" }: { filename?: string }) {
|
|
18
|
-
const { visibleColumns, rows } = useDataTableContext();
|
|
19
|
+
const { visibleColumns, rows, locale } = useDataTableContext();
|
|
20
|
+
const labels = getLabels(locale);
|
|
19
21
|
const handleExport = () => {
|
|
20
22
|
// Build header row
|
|
21
23
|
const headers = visibleColumns.map((col) => {
|
|
@@ -62,7 +64,7 @@ export function CsvButton({ filename = "export" }: { filename?: string }) {
|
|
|
62
64
|
className="inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
63
65
|
onClick={handleExport}
|
|
64
66
|
>
|
|
65
|
-
|
|
67
|
+
{labels.csvExport}
|
|
66
68
|
</button>
|
|
67
69
|
);
|
|
68
70
|
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
RowOperations,
|
|
7
7
|
SortState,
|
|
8
8
|
} from "../types";
|
|
9
|
+
import type { DataTableLocale } from "./i18n";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Context value provided by `DataTable.Provider`.
|
|
@@ -43,6 +44,9 @@ export interface DataTableContextValue<TRow extends Record<string, unknown>> {
|
|
|
43
44
|
onClickRow?: (row: TRow) => void;
|
|
44
45
|
/** Row action definitions for the actions column */
|
|
45
46
|
rowActions?: RowAction<TRow>[];
|
|
47
|
+
|
|
48
|
+
/** Locale for i18n labels */
|
|
49
|
+
locale: DataTableLocale;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
// Using `any` for the context default since generic contexts need a base type.
|
|
@@ -68,6 +68,7 @@ function createCtx(
|
|
|
68
68
|
showAllColumns: () => {},
|
|
69
69
|
hideAllColumns: () => {},
|
|
70
70
|
pageInfo: defaultPageInfo,
|
|
71
|
+
locale: "en",
|
|
71
72
|
...noopRowOps,
|
|
72
73
|
...overrides,
|
|
73
74
|
};
|
|
@@ -115,7 +116,9 @@ describe("DataTable", () => {
|
|
|
115
116
|
</DataTableContext.Provider>,
|
|
116
117
|
);
|
|
117
118
|
|
|
118
|
-
expect(
|
|
119
|
+
expect(
|
|
120
|
+
document.querySelector('[data-datatable-state="loading"]'),
|
|
121
|
+
).toBeInTheDocument();
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
it("shows error state", () => {
|
|
@@ -129,7 +132,9 @@ describe("DataTable", () => {
|
|
|
129
132
|
</DataTableContext.Provider>,
|
|
130
133
|
);
|
|
131
134
|
|
|
132
|
-
expect(
|
|
135
|
+
expect(
|
|
136
|
+
document.querySelector('[data-datatable-state="error"]'),
|
|
137
|
+
).toBeInTheDocument();
|
|
133
138
|
});
|
|
134
139
|
|
|
135
140
|
it("shows empty state", () => {
|
|
@@ -142,7 +147,9 @@ describe("DataTable", () => {
|
|
|
142
147
|
</DataTableContext.Provider>,
|
|
143
148
|
);
|
|
144
149
|
|
|
145
|
-
expect(
|
|
150
|
+
expect(
|
|
151
|
+
document.querySelector('[data-datatable-state="empty"]'),
|
|
152
|
+
).toBeInTheDocument();
|
|
146
153
|
});
|
|
147
154
|
|
|
148
155
|
it("renders sort indicator on sorted column", () => {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
DataTableContext,
|
|
17
17
|
type DataTableContextValue,
|
|
18
18
|
} from "./data-table-context";
|
|
19
|
+
import { getLabels } from "./i18n";
|
|
19
20
|
|
|
20
21
|
// =============================================================================
|
|
21
22
|
// DataTable.Root
|
|
@@ -99,6 +100,7 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
|
99
100
|
pageInfo: value.pageInfo,
|
|
100
101
|
onClickRow: value.onClickRow,
|
|
101
102
|
rowActions: value.rowActions,
|
|
103
|
+
locale: value.locale,
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
const collectionValue = value.collection ?? null;
|
|
@@ -130,7 +132,8 @@ function DataTableHeaders({ className }: { className?: string }) {
|
|
|
130
132
|
"<DataTable.Headers> must be used within <DataTable.Provider>",
|
|
131
133
|
);
|
|
132
134
|
}
|
|
133
|
-
const { columns, sortStates, onSort, rowActions } = ctx;
|
|
135
|
+
const { columns, sortStates, onSort, rowActions, locale } = ctx;
|
|
136
|
+
const labels = getLabels(locale);
|
|
134
137
|
|
|
135
138
|
return (
|
|
136
139
|
<Table.Headers className={className}>
|
|
@@ -176,7 +179,7 @@ function DataTableHeaders({ className }: { className?: string }) {
|
|
|
176
179
|
})}
|
|
177
180
|
{rowActions && rowActions.length > 0 && (
|
|
178
181
|
<Table.HeaderCell style={{ width: 50 }}>
|
|
179
|
-
<span className="sr-only"
|
|
182
|
+
<span className="sr-only">{labels.actionsHeader}</span>
|
|
180
183
|
</Table.HeaderCell>
|
|
181
184
|
)}
|
|
182
185
|
</Table.HeaderRow>
|
|
@@ -209,7 +212,8 @@ function DataTableBody({
|
|
|
209
212
|
"<DataTable.Body> must be used within <DataTable.Provider>",
|
|
210
213
|
);
|
|
211
214
|
}
|
|
212
|
-
const { columns, rows, loading, error, onClickRow, rowActions } = ctx;
|
|
215
|
+
const { columns, rows, loading, error, onClickRow, rowActions, locale } = ctx;
|
|
216
|
+
const labels = getLabels(locale);
|
|
213
217
|
const hasRowActions = rowActions && rowActions.length > 0;
|
|
214
218
|
const totalColSpan = (columns?.length ?? 1) + (hasRowActions ? 1 : 0);
|
|
215
219
|
|
|
@@ -224,21 +228,23 @@ function DataTableBody({
|
|
|
224
228
|
{loading && (!rows || rows.length === 0) && (
|
|
225
229
|
<Table.Row>
|
|
226
230
|
<Table.Cell colSpan={totalColSpan} className="h-24 text-center">
|
|
227
|
-
<span className="text-muted-foreground">
|
|
231
|
+
<span className="text-muted-foreground" data-datatable-state="loading">{labels.loading}</span>
|
|
228
232
|
</Table.Cell>
|
|
229
233
|
</Table.Row>
|
|
230
234
|
)}
|
|
231
235
|
{error && (
|
|
232
236
|
<Table.Row>
|
|
233
237
|
<Table.Cell colSpan={totalColSpan} className="h-24 text-center">
|
|
234
|
-
<span className="text-destructive"
|
|
238
|
+
<span className="text-destructive" data-datatable-state="error">
|
|
239
|
+
{labels.errorPrefix} {error.message}
|
|
240
|
+
</span>
|
|
235
241
|
</Table.Cell>
|
|
236
242
|
</Table.Row>
|
|
237
243
|
)}
|
|
238
244
|
{!loading && !error && (!rows || rows.length === 0) && (
|
|
239
245
|
<Table.Row>
|
|
240
246
|
<Table.Cell colSpan={totalColSpan} className="h-24 text-center">
|
|
241
|
-
<span className="text-muted-foreground"
|
|
247
|
+
<span className="text-muted-foreground" data-datatable-state="empty">{labels.noData}</span>
|
|
242
248
|
</Table.Cell>
|
|
243
249
|
</Table.Row>
|
|
244
250
|
)}
|
|
@@ -264,7 +270,11 @@ function DataTableBody({
|
|
|
264
270
|
style={{ width: 50 }}
|
|
265
271
|
onClick={(e) => e.stopPropagation()}
|
|
266
272
|
>
|
|
267
|
-
<RowActionsMenu
|
|
273
|
+
<RowActionsMenu
|
|
274
|
+
actions={rowActions}
|
|
275
|
+
row={row}
|
|
276
|
+
ariaLabel={labels.rowActions}
|
|
277
|
+
/>
|
|
268
278
|
</Table.Cell>
|
|
269
279
|
)}
|
|
270
280
|
</Table.Row>
|
|
@@ -350,9 +360,11 @@ function formatValue(value: unknown): ReactNode {
|
|
|
350
360
|
function RowActionsMenu<TRow extends Record<string, unknown>>({
|
|
351
361
|
actions,
|
|
352
362
|
row,
|
|
363
|
+
ariaLabel,
|
|
353
364
|
}: {
|
|
354
365
|
actions: RowAction<TRow>[];
|
|
355
366
|
row: TRow;
|
|
367
|
+
ariaLabel: string;
|
|
356
368
|
}) {
|
|
357
369
|
const [open, setOpen] = useState(false);
|
|
358
370
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
@@ -384,7 +396,7 @@ function RowActionsMenu<TRow extends Record<string, unknown>>({
|
|
|
384
396
|
type="button"
|
|
385
397
|
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm hover:bg-accent"
|
|
386
398
|
onClick={() => setOpen((prev) => !prev)}
|
|
387
|
-
aria-label=
|
|
399
|
+
aria-label={ariaLabel}
|
|
388
400
|
aria-haspopup="true"
|
|
389
401
|
aria-expanded={open}
|
|
390
402
|
>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { FilterOperator } from "../types";
|
|
2
|
+
|
|
3
|
+
export type DataTableLocale = "en" | "ja";
|
|
4
|
+
|
|
5
|
+
export interface DataTableLabels {
|
|
6
|
+
// DataTable.Body
|
|
7
|
+
loading: string;
|
|
8
|
+
noData: string;
|
|
9
|
+
errorPrefix: string;
|
|
10
|
+
|
|
11
|
+
// DataTable.Headers (sr-only for actions column)
|
|
12
|
+
actionsHeader: string;
|
|
13
|
+
|
|
14
|
+
// RowActionsMenu aria-label
|
|
15
|
+
rowActions: string;
|
|
16
|
+
|
|
17
|
+
// Pagination
|
|
18
|
+
pagination: {
|
|
19
|
+
first: string;
|
|
20
|
+
previous: string;
|
|
21
|
+
next: string;
|
|
22
|
+
last: string;
|
|
23
|
+
rowsPerPage: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ColumnSelector
|
|
27
|
+
columnSelector: {
|
|
28
|
+
columns: string;
|
|
29
|
+
selectAll: string;
|
|
30
|
+
deselectAll: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// CsvButton
|
|
34
|
+
csvExport: string;
|
|
35
|
+
|
|
36
|
+
// SearchFilterForm
|
|
37
|
+
searchFilter: {
|
|
38
|
+
search: string;
|
|
39
|
+
searchFilter: string;
|
|
40
|
+
clearAll: string;
|
|
41
|
+
activeFilters: string;
|
|
42
|
+
addFilter: string;
|
|
43
|
+
selectField: string;
|
|
44
|
+
enterValue: string;
|
|
45
|
+
selectValue: string;
|
|
46
|
+
add: string;
|
|
47
|
+
noFilterableFields: string;
|
|
48
|
+
allFieldsFiltered: string;
|
|
49
|
+
operatorLabels: Record<FilterOperator, string>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const en: DataTableLabels = {
|
|
54
|
+
loading: "Loading...",
|
|
55
|
+
noData: "No data",
|
|
56
|
+
errorPrefix: "Error:",
|
|
57
|
+
actionsHeader: "Actions",
|
|
58
|
+
rowActions: "Row actions",
|
|
59
|
+
pagination: {
|
|
60
|
+
first: "First page",
|
|
61
|
+
previous: "Previous page",
|
|
62
|
+
next: "Next page",
|
|
63
|
+
last: "Last page",
|
|
64
|
+
rowsPerPage: "Rows per page",
|
|
65
|
+
},
|
|
66
|
+
columnSelector: {
|
|
67
|
+
columns: "Columns",
|
|
68
|
+
selectAll: "Select all",
|
|
69
|
+
deselectAll: "Deselect all",
|
|
70
|
+
},
|
|
71
|
+
csvExport: "Export CSV",
|
|
72
|
+
searchFilter: {
|
|
73
|
+
search: "Search",
|
|
74
|
+
searchFilter: "Search Filter",
|
|
75
|
+
clearAll: "Clear all",
|
|
76
|
+
activeFilters: "Active filters",
|
|
77
|
+
addFilter: "Add filter",
|
|
78
|
+
selectField: "Select field...",
|
|
79
|
+
enterValue: "Enter value...",
|
|
80
|
+
selectValue: "Select...",
|
|
81
|
+
add: "Add",
|
|
82
|
+
noFilterableFields: "No filterable fields",
|
|
83
|
+
allFieldsFiltered: "All fields have filters applied",
|
|
84
|
+
operatorLabels: {
|
|
85
|
+
eq: "=",
|
|
86
|
+
ne: "≠",
|
|
87
|
+
contains: "contains",
|
|
88
|
+
startsWith: "starts with",
|
|
89
|
+
endsWith: "ends with",
|
|
90
|
+
gt: ">",
|
|
91
|
+
gte: "≥",
|
|
92
|
+
lt: "<",
|
|
93
|
+
lte: "≤",
|
|
94
|
+
between: "between",
|
|
95
|
+
in: "in",
|
|
96
|
+
notIn: "not in",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ja: DataTableLabels = {
|
|
102
|
+
loading: "読み込み中...",
|
|
103
|
+
noData: "データがありません",
|
|
104
|
+
errorPrefix: "エラー:",
|
|
105
|
+
actionsHeader: "操作",
|
|
106
|
+
rowActions: "行の操作",
|
|
107
|
+
pagination: {
|
|
108
|
+
first: "最初のページ",
|
|
109
|
+
previous: "前のページ",
|
|
110
|
+
next: "次のページ",
|
|
111
|
+
last: "最後のページ",
|
|
112
|
+
rowsPerPage: "表示件数",
|
|
113
|
+
},
|
|
114
|
+
columnSelector: {
|
|
115
|
+
columns: "列の表示",
|
|
116
|
+
selectAll: "すべて選択",
|
|
117
|
+
deselectAll: "すべて解除",
|
|
118
|
+
},
|
|
119
|
+
csvExport: "CSV出力",
|
|
120
|
+
searchFilter: {
|
|
121
|
+
search: "検索",
|
|
122
|
+
searchFilter: "検索フィルター",
|
|
123
|
+
clearAll: "すべてクリア",
|
|
124
|
+
activeFilters: "適用中のフィルター",
|
|
125
|
+
addFilter: "フィルターを追加",
|
|
126
|
+
selectField: "フィールドを選択...",
|
|
127
|
+
enterValue: "値を入力...",
|
|
128
|
+
selectValue: "選択...",
|
|
129
|
+
add: "追加",
|
|
130
|
+
noFilterableFields: "フィルタ可能なフィールドがありません",
|
|
131
|
+
allFieldsFiltered: "すべてのフィールドにフィルターが適用されています",
|
|
132
|
+
operatorLabels: {
|
|
133
|
+
eq: "等しい",
|
|
134
|
+
ne: "等しくない",
|
|
135
|
+
contains: "含む",
|
|
136
|
+
startsWith: "で始まる",
|
|
137
|
+
endsWith: "で終わる",
|
|
138
|
+
gt: "より大きい",
|
|
139
|
+
gte: "以上",
|
|
140
|
+
lt: "より小さい",
|
|
141
|
+
lte: "以下",
|
|
142
|
+
between: "範囲内",
|
|
143
|
+
in: "含まれる",
|
|
144
|
+
notIn: "含まれない",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const locales: Record<DataTableLocale, DataTableLabels> = { en, ja };
|
|
150
|
+
|
|
151
|
+
export function getLabels(locale: DataTableLocale): DataTableLabels {
|
|
152
|
+
return locales[locale];
|
|
153
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
import type { Column } from "
|
|
4
|
-
import { createTestProviders } from "
|
|
3
|
+
import type { Column } from "../types";
|
|
4
|
+
import { createTestProviders } from "../../tests/helpers";
|
|
5
5
|
import { Pagination } from "./pagination";
|
|
6
6
|
|
|
7
7
|
// =============================================================================
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useDataTableContext } from "./data-table
|
|
2
|
-
import { useCollectionContext } from "
|
|
1
|
+
import { useDataTableContext } from "./data-table-context";
|
|
2
|
+
import { useCollectionContext } from "../collection/collection-provider";
|
|
3
|
+
import { getLabels } from "./i18n";
|
|
3
4
|
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// Inline SVG Icons (lucide-style, no external dependency)
|
|
@@ -126,7 +127,7 @@ const btnClass =
|
|
|
126
127
|
* ```
|
|
127
128
|
*/
|
|
128
129
|
export function Pagination({ labels, pageSizeOptions }: PaginationProps = {}) {
|
|
129
|
-
const { pageInfo } = useDataTableContext();
|
|
130
|
+
const { pageInfo, locale } = useDataTableContext();
|
|
130
131
|
const {
|
|
131
132
|
nextPage,
|
|
132
133
|
prevPage,
|
|
@@ -140,11 +141,12 @@ export function Pagination({ labels, pageSizeOptions }: PaginationProps = {}) {
|
|
|
140
141
|
setPageSize,
|
|
141
142
|
} = useCollectionContext();
|
|
142
143
|
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
144
|
+
const pl = getLabels(locale).pagination;
|
|
145
|
+
const firstLabel = labels?.first ?? pl.first;
|
|
146
|
+
const previousLabel = labels?.previous ?? pl.previous;
|
|
147
|
+
const nextLabel = labels?.next ?? pl.next;
|
|
148
|
+
const lastLabel = labels?.last ?? pl.last;
|
|
149
|
+
const rowsPerPageLabel = labels?.rowsPerPage ?? pl.rowsPerPage;
|
|
148
150
|
|
|
149
151
|
return (
|
|
150
152
|
<div className="flex items-center justify-end gap-2 py-2">
|
package/src/component/{search-filter-form.test.tsx → data-table/search-filter-form.test.tsx}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
import { describe, it, expect, vi } from "vitest";
|
|
4
|
-
import type { Column, Filter } from "
|
|
5
|
-
import { createTestProviders } from "
|
|
4
|
+
import type { Column, Filter } from "../types";
|
|
5
|
+
import { createTestProviders } from "../../tests/helpers";
|
|
6
6
|
import { SearchFilterForm } from "./search-filter-form";
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
|
@@ -6,10 +6,16 @@ import {
|
|
|
6
6
|
type KeyboardEvent,
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from "react";
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import type {
|
|
10
|
+
Filter,
|
|
11
|
+
FilterOperator,
|
|
12
|
+
FilterConfig,
|
|
13
|
+
SearchFilterLabels,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import { OPERATORS_BY_FILTER_TYPE } from "../types";
|
|
16
|
+
import { useDataTableContext } from "./data-table-context";
|
|
17
|
+
import { useCollectionContext } from "../collection/collection-provider";
|
|
18
|
+
import { getLabels } from "./i18n";
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* Composite search filter form.
|
|
@@ -38,9 +44,10 @@ export function SearchFilterForm({
|
|
|
38
44
|
/** Custom trigger element. When provided, replaces the default trigger button content. */
|
|
39
45
|
trigger?: ReactNode;
|
|
40
46
|
} = {}) {
|
|
41
|
-
const { columns } = useDataTableContext();
|
|
47
|
+
const { columns, locale } = useDataTableContext();
|
|
42
48
|
const { filters, addFilter, removeFilter, clearFilters } =
|
|
43
49
|
useCollectionContext();
|
|
50
|
+
const sf = getLabels(locale).searchFilter;
|
|
44
51
|
const filterableColumns = columns.filter(
|
|
45
52
|
(col) => col.kind === "field" && col.filter,
|
|
46
53
|
);
|
|
@@ -70,16 +77,18 @@ export function SearchFilterForm({
|
|
|
70
77
|
|
|
71
78
|
// Label helpers
|
|
72
79
|
const l = (key: string, fallback: string) =>
|
|
73
|
-
(labels as Record<string, string> | undefined)?.[key] ??
|
|
80
|
+
(labels as Record<string, string> | undefined)?.[key] ??
|
|
81
|
+
(sf as unknown as Record<string, string>)[key] ??
|
|
82
|
+
fallback;
|
|
74
83
|
|
|
75
84
|
const getOperatorLabel = useCallback(
|
|
76
85
|
(filterType: FilterConfig["type"], operator: FilterOperator): string => {
|
|
77
86
|
if (labels?.getOperatorLabel) {
|
|
78
87
|
return labels.getOperatorLabel(filterType, operator);
|
|
79
88
|
}
|
|
80
|
-
return
|
|
89
|
+
return sf.operatorLabels[operator] ?? operator;
|
|
81
90
|
},
|
|
82
|
-
[labels],
|
|
91
|
+
[labels, sf],
|
|
83
92
|
);
|
|
84
93
|
|
|
85
94
|
const getFieldLabel = useCallback(
|
|
@@ -100,10 +109,7 @@ export function SearchFilterForm({
|
|
|
100
109
|
}): string => {
|
|
101
110
|
const fieldLabel = getFieldLabel(filter.field);
|
|
102
111
|
if (labels?.formatFilterDisplay) {
|
|
103
|
-
return labels.formatFilterDisplay(
|
|
104
|
-
filter as import("./types").Filter,
|
|
105
|
-
fieldLabel,
|
|
106
|
-
);
|
|
112
|
+
return labels.formatFilterDisplay(filter as Filter, fieldLabel);
|
|
107
113
|
}
|
|
108
114
|
const value =
|
|
109
115
|
typeof filter.value === "boolean"
|
|
@@ -111,8 +117,7 @@ export function SearchFilterForm({
|
|
|
111
117
|
? "true"
|
|
112
118
|
: "false"
|
|
113
119
|
: String(filter.value);
|
|
114
|
-
const opLabel =
|
|
115
|
-
DEFAULT_OPERATOR_LABELS[filter.operator] ?? filter.operator;
|
|
120
|
+
const opLabel = sf.operatorLabels[filter.operator] ?? filter.operator;
|
|
116
121
|
return `${fieldLabel} ${opLabel} ${value}`;
|
|
117
122
|
},
|
|
118
123
|
[getFieldLabel, labels],
|
|
@@ -42,6 +42,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
42
42
|
collection,
|
|
43
43
|
onClickRow,
|
|
44
44
|
rowActions,
|
|
45
|
+
locale = "en",
|
|
45
46
|
} = options;
|
|
46
47
|
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
@@ -255,5 +256,8 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
255
256
|
// Row interaction (passthrough for DataTable.Provider)
|
|
256
257
|
onClickRow,
|
|
257
258
|
rowActions,
|
|
259
|
+
|
|
260
|
+
// Locale
|
|
261
|
+
locale,
|
|
258
262
|
};
|
|
259
263
|
}
|
package/src/component/index.ts
CHANGED
|
@@ -63,13 +63,17 @@ export { Table } from "./table";
|
|
|
63
63
|
export { DataTable } from "./data-table/data-table";
|
|
64
64
|
export { useDataTable } from "./data-table/use-data-table";
|
|
65
65
|
export { useDataTableContext } from "./data-table/data-table-context";
|
|
66
|
+
export type { DataTableLocale } from "./data-table/i18n";
|
|
66
67
|
|
|
67
68
|
// Field helpers
|
|
68
69
|
export { createColumnHelper } from "./field-helpers";
|
|
69
70
|
|
|
70
|
-
// Utility components
|
|
71
|
-
export { Pagination } from "./pagination";
|
|
72
|
-
export type {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
// Utility components (co-located with DataTable)
|
|
72
|
+
export { Pagination } from "./data-table/pagination";
|
|
73
|
+
export type {
|
|
74
|
+
PaginationProps,
|
|
75
|
+
PaginationLabels,
|
|
76
|
+
} from "./data-table/pagination";
|
|
77
|
+
export { ColumnSelector } from "./data-table/column-selector";
|
|
78
|
+
export { CsvButton } from "./data-table/csv-button";
|
|
79
|
+
export { SearchFilterForm } from "./data-table/search-filter-form";
|
package/src/component/types.ts
CHANGED
|
@@ -515,6 +515,8 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
|
|
|
515
515
|
onClickRow?: (row: TRow) => void;
|
|
516
516
|
/** Row action definitions for the actions column */
|
|
517
517
|
rowActions?: RowAction<TRow>[];
|
|
518
|
+
/** Locale for i18n labels (default: "en") */
|
|
519
|
+
locale?: "en" | "ja";
|
|
518
520
|
}
|
|
519
521
|
|
|
520
522
|
/**
|
|
@@ -623,6 +625,9 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
|
623
625
|
onClickRow?: (row: TRow) => void;
|
|
624
626
|
/** Row action definitions for the actions column */
|
|
625
627
|
rowActions?: RowAction<TRow>[];
|
|
628
|
+
|
|
629
|
+
/** Resolved locale */
|
|
630
|
+
locale: "en" | "ja";
|
|
626
631
|
}
|
|
627
632
|
|
|
628
633
|
// =============================================================================
|
package/src/tests/helpers.tsx
CHANGED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { describe, it, expect, vi } from "vitest";
|
|
4
|
-
import { Table } from "./table";
|
|
5
|
-
import { DataTable } from "./data-table/data-table";
|
|
6
|
-
import { DataTableContext } from "./data-table/data-table-context";
|
|
7
|
-
import type { DataTableContextValue } from "./data-table/data-table-context";
|
|
8
|
-
import type { Column } from "./types";
|
|
9
|
-
|
|
10
|
-
describe("Table (static)", () => {
|
|
11
|
-
it("renders a basic static table", () => {
|
|
12
|
-
render(
|
|
13
|
-
<Table.Root>
|
|
14
|
-
<Table.Headers>
|
|
15
|
-
<Table.HeaderRow>
|
|
16
|
-
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
17
|
-
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
18
|
-
</Table.HeaderRow>
|
|
19
|
-
</Table.Headers>
|
|
20
|
-
<Table.Body>
|
|
21
|
-
<Table.Row>
|
|
22
|
-
<Table.Cell>Alice</Table.Cell>
|
|
23
|
-
<Table.Cell>Active</Table.Cell>
|
|
24
|
-
</Table.Row>
|
|
25
|
-
<Table.Row>
|
|
26
|
-
<Table.Cell>Bob</Table.Cell>
|
|
27
|
-
<Table.Cell>Inactive</Table.Cell>
|
|
28
|
-
</Table.Row>
|
|
29
|
-
</Table.Body>
|
|
30
|
-
</Table.Root>,
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
expect(screen.getByText("Name")).toBeInTheDocument();
|
|
34
|
-
expect(screen.getByText("Status")).toBeInTheDocument();
|
|
35
|
-
expect(screen.getByText("Alice")).toBeInTheDocument();
|
|
36
|
-
expect(screen.getByText("Bob")).toBeInTheDocument();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
type TestRow = {
|
|
41
|
-
id: string;
|
|
42
|
-
name: string;
|
|
43
|
-
status: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const testColumns: Column<TestRow>[] = [
|
|
47
|
-
{
|
|
48
|
-
kind: "field",
|
|
49
|
-
dataKey: "name",
|
|
50
|
-
label: "Name",
|
|
51
|
-
sort: { type: "string" },
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
kind: "field",
|
|
55
|
-
dataKey: "status",
|
|
56
|
-
label: "Status",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
kind: "display",
|
|
60
|
-
id: "actions",
|
|
61
|
-
label: "Actions",
|
|
62
|
-
render: (row) => <button>Edit {row.name}</button>,
|
|
63
|
-
},
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
const testRows: TestRow[] = [
|
|
67
|
-
{ id: "1", name: "Alice", status: "Active" },
|
|
68
|
-
{ id: "2", name: "Bob", status: "Inactive" },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
const noopRowOps = {
|
|
72
|
-
updateRow: () => ({ rollback: () => {} }),
|
|
73
|
-
deleteRow: () => ({
|
|
74
|
-
rollback: () => {},
|
|
75
|
-
deletedRow: {} as TestRow,
|
|
76
|
-
}),
|
|
77
|
-
insertRow: () => ({ rollback: () => {} }),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const defaultPageInfo = {
|
|
81
|
-
hasNextPage: false,
|
|
82
|
-
endCursor: null,
|
|
83
|
-
hasPreviousPage: false,
|
|
84
|
-
startCursor: null,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
function createCtx(
|
|
88
|
-
overrides: Partial<DataTableContextValue<TestRow>> = {},
|
|
89
|
-
): DataTableContextValue<TestRow> {
|
|
90
|
-
return {
|
|
91
|
-
columns: testColumns,
|
|
92
|
-
rows: testRows,
|
|
93
|
-
loading: false,
|
|
94
|
-
error: null,
|
|
95
|
-
sortStates: [],
|
|
96
|
-
visibleColumns: testColumns,
|
|
97
|
-
isColumnVisible: () => true,
|
|
98
|
-
toggleColumn: () => {},
|
|
99
|
-
showAllColumns: () => {},
|
|
100
|
-
hideAllColumns: () => {},
|
|
101
|
-
pageInfo: defaultPageInfo,
|
|
102
|
-
...noopRowOps,
|
|
103
|
-
...overrides,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
describe("DataTable", () => {
|
|
108
|
-
it("renders data-bound table with auto-generated rows", () => {
|
|
109
|
-
render(
|
|
110
|
-
<DataTableContext.Provider value={createCtx()}>
|
|
111
|
-
<DataTable.Root>
|
|
112
|
-
<DataTable.Headers />
|
|
113
|
-
<DataTable.Body />
|
|
114
|
-
</DataTable.Root>
|
|
115
|
-
</DataTableContext.Provider>,
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
expect(screen.getByText("Name")).toBeInTheDocument();
|
|
119
|
-
expect(screen.getByText("Status")).toBeInTheDocument();
|
|
120
|
-
expect(screen.getByText("Alice")).toBeInTheDocument();
|
|
121
|
-
expect(screen.getByText("Bob")).toBeInTheDocument();
|
|
122
|
-
expect(screen.getByText("Active")).toBeInTheDocument();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("renders display columns via render function", () => {
|
|
126
|
-
render(
|
|
127
|
-
<DataTableContext.Provider value={createCtx()}>
|
|
128
|
-
<DataTable.Root>
|
|
129
|
-
<DataTable.Headers />
|
|
130
|
-
<DataTable.Body />
|
|
131
|
-
</DataTable.Root>
|
|
132
|
-
</DataTableContext.Provider>,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
expect(screen.getByText("Edit Alice")).toBeInTheDocument();
|
|
136
|
-
expect(screen.getByText("Edit Bob")).toBeInTheDocument();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("shows loading state", () => {
|
|
140
|
-
render(
|
|
141
|
-
<DataTableContext.Provider value={createCtx({ rows: [], loading: true })}>
|
|
142
|
-
<DataTable.Root>
|
|
143
|
-
<DataTable.Headers />
|
|
144
|
-
<DataTable.Body />
|
|
145
|
-
</DataTable.Root>
|
|
146
|
-
</DataTableContext.Provider>,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("shows error state", () => {
|
|
153
|
-
const err = new Error("Something went wrong");
|
|
154
|
-
render(
|
|
155
|
-
<DataTableContext.Provider value={createCtx({ rows: [], error: err })}>
|
|
156
|
-
<DataTable.Root>
|
|
157
|
-
<DataTable.Headers />
|
|
158
|
-
<DataTable.Body />
|
|
159
|
-
</DataTable.Root>
|
|
160
|
-
</DataTableContext.Provider>,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
expect(screen.getByText("Error: Something went wrong")).toBeInTheDocument();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("shows empty state", () => {
|
|
167
|
-
render(
|
|
168
|
-
<DataTableContext.Provider value={createCtx({ rows: [] })}>
|
|
169
|
-
<DataTable.Root>
|
|
170
|
-
<DataTable.Headers />
|
|
171
|
-
<DataTable.Body />
|
|
172
|
-
</DataTable.Root>
|
|
173
|
-
</DataTableContext.Provider>,
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
expect(screen.getByText("No data")).toBeInTheDocument();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("renders sort indicator on sorted column", () => {
|
|
180
|
-
render(
|
|
181
|
-
<DataTableContext.Provider
|
|
182
|
-
value={createCtx({
|
|
183
|
-
sortStates: [{ field: "name", direction: "Asc" }],
|
|
184
|
-
})}
|
|
185
|
-
>
|
|
186
|
-
<DataTable.Root>
|
|
187
|
-
<DataTable.Headers />
|
|
188
|
-
<DataTable.Body />
|
|
189
|
-
</DataTable.Root>
|
|
190
|
-
</DataTableContext.Provider>,
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
expect(screen.getByText("▲")).toBeInTheDocument();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("supports custom rendering with children", () => {
|
|
197
|
-
render(
|
|
198
|
-
<DataTableContext.Provider value={createCtx()}>
|
|
199
|
-
<DataTable.Root>
|
|
200
|
-
<DataTable.Headers />
|
|
201
|
-
<DataTable.Body>
|
|
202
|
-
<DataTable.Row>
|
|
203
|
-
<DataTable.Cell>Custom Cell</DataTable.Cell>
|
|
204
|
-
</DataTable.Row>
|
|
205
|
-
</DataTable.Body>
|
|
206
|
-
</DataTable.Root>
|
|
207
|
-
</DataTableContext.Provider>,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
expect(screen.getByText("Custom Cell")).toBeInTheDocument();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
describe("header sort click", () => {
|
|
214
|
-
const sortableColumns: Column<TestRow>[] = [
|
|
215
|
-
{
|
|
216
|
-
kind: "field",
|
|
217
|
-
dataKey: "name",
|
|
218
|
-
label: "Name",
|
|
219
|
-
sort: { type: "string" },
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
kind: "field",
|
|
223
|
-
dataKey: "status",
|
|
224
|
-
label: "Status",
|
|
225
|
-
sort: { type: "string" },
|
|
226
|
-
},
|
|
227
|
-
];
|
|
228
|
-
|
|
229
|
-
it("calls onSort with Asc when clicking unsorted column", async () => {
|
|
230
|
-
const onSort = vi.fn();
|
|
231
|
-
render(
|
|
232
|
-
<DataTableContext.Provider
|
|
233
|
-
value={createCtx({
|
|
234
|
-
columns: sortableColumns,
|
|
235
|
-
visibleColumns: sortableColumns,
|
|
236
|
-
sortStates: [],
|
|
237
|
-
onSort,
|
|
238
|
-
})}
|
|
239
|
-
>
|
|
240
|
-
<DataTable.Root>
|
|
241
|
-
<DataTable.Headers />
|
|
242
|
-
<DataTable.Body />
|
|
243
|
-
</DataTable.Root>
|
|
244
|
-
</DataTableContext.Provider>,
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
await userEvent.click(screen.getByText("Name"));
|
|
248
|
-
expect(onSort).toHaveBeenCalledWith("name", "Asc");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("cycles Asc → Desc on second click", async () => {
|
|
252
|
-
const onSort = vi.fn();
|
|
253
|
-
render(
|
|
254
|
-
<DataTableContext.Provider
|
|
255
|
-
value={createCtx({
|
|
256
|
-
columns: sortableColumns,
|
|
257
|
-
visibleColumns: sortableColumns,
|
|
258
|
-
sortStates: [{ field: "name", direction: "Asc" }],
|
|
259
|
-
onSort,
|
|
260
|
-
})}
|
|
261
|
-
>
|
|
262
|
-
<DataTable.Root>
|
|
263
|
-
<DataTable.Headers />
|
|
264
|
-
<DataTable.Body />
|
|
265
|
-
</DataTable.Root>
|
|
266
|
-
</DataTableContext.Provider>,
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
await userEvent.click(screen.getByText("Name"));
|
|
270
|
-
expect(onSort).toHaveBeenCalledWith("name", "Desc");
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("cycles Desc → remove (undefined) on third click", async () => {
|
|
274
|
-
const onSort = vi.fn();
|
|
275
|
-
render(
|
|
276
|
-
<DataTableContext.Provider
|
|
277
|
-
value={createCtx({
|
|
278
|
-
columns: sortableColumns,
|
|
279
|
-
visibleColumns: sortableColumns,
|
|
280
|
-
sortStates: [{ field: "name", direction: "Desc" }],
|
|
281
|
-
onSort,
|
|
282
|
-
})}
|
|
283
|
-
>
|
|
284
|
-
<DataTable.Root>
|
|
285
|
-
<DataTable.Headers />
|
|
286
|
-
<DataTable.Body />
|
|
287
|
-
</DataTable.Root>
|
|
288
|
-
</DataTableContext.Provider>,
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
await userEvent.click(screen.getByText("Name"));
|
|
292
|
-
expect(onSort).toHaveBeenCalledWith("name", undefined);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("does not call onSort for non-sortable column", async () => {
|
|
296
|
-
const onSort = vi.fn();
|
|
297
|
-
const mixedColumns: Column<TestRow>[] = [
|
|
298
|
-
{
|
|
299
|
-
kind: "field",
|
|
300
|
-
dataKey: "name",
|
|
301
|
-
label: "Name",
|
|
302
|
-
sort: { type: "string" },
|
|
303
|
-
},
|
|
304
|
-
{ kind: "field", dataKey: "status", label: "Status" },
|
|
305
|
-
];
|
|
306
|
-
render(
|
|
307
|
-
<DataTableContext.Provider
|
|
308
|
-
value={createCtx({
|
|
309
|
-
columns: mixedColumns,
|
|
310
|
-
visibleColumns: mixedColumns,
|
|
311
|
-
sortStates: [],
|
|
312
|
-
onSort,
|
|
313
|
-
})}
|
|
314
|
-
>
|
|
315
|
-
<DataTable.Root>
|
|
316
|
-
<DataTable.Headers />
|
|
317
|
-
<DataTable.Body />
|
|
318
|
-
</DataTable.Root>
|
|
319
|
-
</DataTableContext.Provider>,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
await userEvent.click(screen.getByText("Status"));
|
|
323
|
-
expect(onSort).not.toHaveBeenCalled();
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("shows sort indicators for multiple sorted columns", () => {
|
|
327
|
-
render(
|
|
328
|
-
<DataTableContext.Provider
|
|
329
|
-
value={createCtx({
|
|
330
|
-
columns: sortableColumns,
|
|
331
|
-
visibleColumns: sortableColumns,
|
|
332
|
-
sortStates: [
|
|
333
|
-
{ field: "name", direction: "Asc" },
|
|
334
|
-
{ field: "status", direction: "Desc" },
|
|
335
|
-
],
|
|
336
|
-
})}
|
|
337
|
-
>
|
|
338
|
-
<DataTable.Root>
|
|
339
|
-
<DataTable.Headers />
|
|
340
|
-
<DataTable.Body />
|
|
341
|
-
</DataTable.Root>
|
|
342
|
-
</DataTableContext.Provider>,
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
expect(screen.getByText("▲")).toBeInTheDocument();
|
|
346
|
-
expect(screen.getByText("▼")).toBeInTheDocument();
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
});
|