@izumisy-tailor/tailor-data-viewer 0.2.19 → 0.2.21
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/collection/collection-provider.tsx +1 -1
- package/src/component/components.test.tsx +88 -36
- package/src/component/data-table/data-table-context.tsx +14 -6
- package/src/component/data-table/{index.tsx → data-table.tsx} +194 -157
- package/src/component/data-table/use-data-table.test.ts +6 -29
- package/src/component/data-table/use-data-table.ts +28 -56
- package/src/component/index.ts +2 -4
- package/src/component/types.ts +44 -48
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ const CollectionContext = createContext<UseCollectionReturn<
|
|
|
15
15
|
*
|
|
16
16
|
* <CollectionProvider value={collection}>
|
|
17
17
|
* <FilterPanel />
|
|
18
|
-
* <DataTable.Root
|
|
18
|
+
* <DataTable.Root>...</DataTable.Root>
|
|
19
19
|
* <Pagination {...table} />
|
|
20
20
|
* </CollectionProvider>
|
|
21
21
|
* ```
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
3
|
import { Table } from "./table";
|
|
4
|
-
import { DataTable } from "./data-table/
|
|
4
|
+
import { DataTable } from "./data-table/data-table";
|
|
5
|
+
import { DataTableContext } from "./data-table/data-table-context";
|
|
6
|
+
import type { DataTableContextValue } from "./data-table/data-table-context";
|
|
5
7
|
import type { Column } from "./types";
|
|
6
8
|
|
|
7
9
|
describe("Table (static)", () => {
|
|
@@ -65,13 +67,51 @@ const testRows: TestRow[] = [
|
|
|
65
67
|
{ id: "2", name: "Bob", status: "Inactive" },
|
|
66
68
|
];
|
|
67
69
|
|
|
70
|
+
const noopRowOps = {
|
|
71
|
+
updateRow: () => ({ rollback: () => {} }),
|
|
72
|
+
deleteRow: () => ({
|
|
73
|
+
rollback: () => {},
|
|
74
|
+
deletedRow: {} as TestRow,
|
|
75
|
+
}),
|
|
76
|
+
insertRow: () => ({ rollback: () => {} }),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const defaultPageInfo = {
|
|
80
|
+
hasNextPage: false,
|
|
81
|
+
endCursor: null,
|
|
82
|
+
hasPreviousPage: false,
|
|
83
|
+
startCursor: null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function createCtx(
|
|
87
|
+
overrides: Partial<DataTableContextValue<TestRow>> = {},
|
|
88
|
+
): DataTableContextValue<TestRow> {
|
|
89
|
+
return {
|
|
90
|
+
columns: testColumns,
|
|
91
|
+
rows: testRows,
|
|
92
|
+
loading: false,
|
|
93
|
+
error: null,
|
|
94
|
+
sortStates: [],
|
|
95
|
+
visibleColumns: testColumns,
|
|
96
|
+
isColumnVisible: () => true,
|
|
97
|
+
toggleColumn: () => {},
|
|
98
|
+
showAllColumns: () => {},
|
|
99
|
+
hideAllColumns: () => {},
|
|
100
|
+
pageInfo: defaultPageInfo,
|
|
101
|
+
...noopRowOps,
|
|
102
|
+
...overrides,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
describe("DataTable", () => {
|
|
69
107
|
it("renders data-bound table with auto-generated rows", () => {
|
|
70
108
|
render(
|
|
71
|
-
<
|
|
72
|
-
<DataTable.
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
<DataTableContext.Provider value={createCtx()}>
|
|
110
|
+
<DataTable.Root>
|
|
111
|
+
<DataTable.Headers />
|
|
112
|
+
<DataTable.Body />
|
|
113
|
+
</DataTable.Root>
|
|
114
|
+
</DataTableContext.Provider>,
|
|
75
115
|
);
|
|
76
116
|
|
|
77
117
|
expect(screen.getByText("Name")).toBeInTheDocument();
|
|
@@ -83,10 +123,12 @@ describe("DataTable", () => {
|
|
|
83
123
|
|
|
84
124
|
it("renders display columns via render function", () => {
|
|
85
125
|
render(
|
|
86
|
-
<
|
|
87
|
-
<DataTable.
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
<DataTableContext.Provider value={createCtx()}>
|
|
127
|
+
<DataTable.Root>
|
|
128
|
+
<DataTable.Headers />
|
|
129
|
+
<DataTable.Body />
|
|
130
|
+
</DataTable.Root>
|
|
131
|
+
</DataTableContext.Provider>,
|
|
90
132
|
);
|
|
91
133
|
|
|
92
134
|
expect(screen.getByText("Edit Alice")).toBeInTheDocument();
|
|
@@ -95,10 +137,12 @@ describe("DataTable", () => {
|
|
|
95
137
|
|
|
96
138
|
it("shows loading state", () => {
|
|
97
139
|
render(
|
|
98
|
-
<
|
|
99
|
-
<DataTable.
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
<DataTableContext.Provider value={createCtx({ rows: [], loading: true })}>
|
|
141
|
+
<DataTable.Root>
|
|
142
|
+
<DataTable.Headers />
|
|
143
|
+
<DataTable.Body />
|
|
144
|
+
</DataTable.Root>
|
|
145
|
+
</DataTableContext.Provider>,
|
|
102
146
|
);
|
|
103
147
|
|
|
104
148
|
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
|
@@ -107,10 +151,12 @@ describe("DataTable", () => {
|
|
|
107
151
|
it("shows error state", () => {
|
|
108
152
|
const err = new Error("Something went wrong");
|
|
109
153
|
render(
|
|
110
|
-
<
|
|
111
|
-
<DataTable.
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
<DataTableContext.Provider value={createCtx({ rows: [], error: err })}>
|
|
155
|
+
<DataTable.Root>
|
|
156
|
+
<DataTable.Headers />
|
|
157
|
+
<DataTable.Body />
|
|
158
|
+
</DataTable.Root>
|
|
159
|
+
</DataTableContext.Provider>,
|
|
114
160
|
);
|
|
115
161
|
|
|
116
162
|
expect(screen.getByText("Error: Something went wrong")).toBeInTheDocument();
|
|
@@ -118,10 +164,12 @@ describe("DataTable", () => {
|
|
|
118
164
|
|
|
119
165
|
it("shows empty state", () => {
|
|
120
166
|
render(
|
|
121
|
-
<
|
|
122
|
-
<DataTable.
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
<DataTableContext.Provider value={createCtx({ rows: [] })}>
|
|
168
|
+
<DataTable.Root>
|
|
169
|
+
<DataTable.Headers />
|
|
170
|
+
<DataTable.Body />
|
|
171
|
+
</DataTable.Root>
|
|
172
|
+
</DataTableContext.Provider>,
|
|
125
173
|
);
|
|
126
174
|
|
|
127
175
|
expect(screen.getByText("No data")).toBeInTheDocument();
|
|
@@ -129,14 +177,16 @@ describe("DataTable", () => {
|
|
|
129
177
|
|
|
130
178
|
it("renders sort indicator on sorted column", () => {
|
|
131
179
|
render(
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
180
|
+
<DataTableContext.Provider
|
|
181
|
+
value={createCtx({
|
|
182
|
+
sortStates: [{ field: "name", direction: "Asc" }],
|
|
183
|
+
})}
|
|
136
184
|
>
|
|
137
|
-
<DataTable.
|
|
138
|
-
|
|
139
|
-
|
|
185
|
+
<DataTable.Root>
|
|
186
|
+
<DataTable.Headers />
|
|
187
|
+
<DataTable.Body />
|
|
188
|
+
</DataTable.Root>
|
|
189
|
+
</DataTableContext.Provider>,
|
|
140
190
|
);
|
|
141
191
|
|
|
142
192
|
expect(screen.getByText("▲")).toBeInTheDocument();
|
|
@@ -144,14 +194,16 @@ describe("DataTable", () => {
|
|
|
144
194
|
|
|
145
195
|
it("supports custom rendering with children", () => {
|
|
146
196
|
render(
|
|
147
|
-
<
|
|
148
|
-
<DataTable.
|
|
149
|
-
|
|
150
|
-
<DataTable.
|
|
151
|
-
<DataTable.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
197
|
+
<DataTableContext.Provider value={createCtx()}>
|
|
198
|
+
<DataTable.Root>
|
|
199
|
+
<DataTable.Headers />
|
|
200
|
+
<DataTable.Body>
|
|
201
|
+
<DataTable.Row>
|
|
202
|
+
<DataTable.Cell>Custom Cell</DataTable.Cell>
|
|
203
|
+
</DataTable.Row>
|
|
204
|
+
</DataTable.Body>
|
|
205
|
+
</DataTable.Root>
|
|
206
|
+
</DataTableContext.Provider>,
|
|
155
207
|
);
|
|
156
208
|
|
|
157
209
|
expect(screen.getByText("Custom Cell")).toBeInTheDocument();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import type { Column, PageInfo, RowOperations, SortState } from "../types";
|
|
2
|
+
import type { Column, PageInfo, RowAction, RowOperations, SortState } from "../types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Context value provided by `DataTable.
|
|
5
|
+
* Context value provided by `DataTable.Provider`.
|
|
6
6
|
*
|
|
7
7
|
* Exposes row operations for optimistic updates, table state, column
|
|
8
8
|
* visibility, and page info so that `DataTable.Headers` / `DataTable.Body`
|
|
@@ -14,7 +14,7 @@ export interface DataTableContextValue<TRow extends Record<string, unknown>> {
|
|
|
14
14
|
deleteRow: RowOperations<TRow>["deleteRow"];
|
|
15
15
|
insertRow: RowOperations<TRow>["insertRow"];
|
|
16
16
|
|
|
17
|
-
// Table state propagated from DataTable.
|
|
17
|
+
// Table state propagated from DataTable.Provider
|
|
18
18
|
columns: Column<TRow>[];
|
|
19
19
|
rows: TRow[];
|
|
20
20
|
loading: boolean;
|
|
@@ -31,6 +31,12 @@ export interface DataTableContextValue<TRow extends Record<string, unknown>> {
|
|
|
31
31
|
|
|
32
32
|
// Page info from GraphQL response (populated by DataTable.Provider)
|
|
33
33
|
pageInfo: PageInfo;
|
|
34
|
+
|
|
35
|
+
// Row interaction (populated by DataTable.Root)
|
|
36
|
+
/** Handler called when a row is clicked */
|
|
37
|
+
onClickRow?: (row: TRow) => void;
|
|
38
|
+
/** Row action definitions for the actions column */
|
|
39
|
+
rowActions?: RowAction<TRow>[];
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
// Using `any` for the context default since generic contexts need a base type.
|
|
@@ -40,9 +46,9 @@ const DataTableContext = createContext<DataTableContextValue<any> | null>(null);
|
|
|
40
46
|
export { DataTableContext };
|
|
41
47
|
|
|
42
48
|
/**
|
|
43
|
-
* Hook to access row operations from the nearest `DataTable.
|
|
49
|
+
* Hook to access row operations from the nearest `DataTable.Provider`.
|
|
44
50
|
*
|
|
45
|
-
* @throws Error if used outside of `DataTable.
|
|
51
|
+
* @throws Error if used outside of `DataTable.Provider`.
|
|
46
52
|
*
|
|
47
53
|
* @example
|
|
48
54
|
* ```tsx
|
|
@@ -57,7 +63,9 @@ export function useDataTableContext<
|
|
|
57
63
|
>(): DataTableContextValue<TRow> {
|
|
58
64
|
const ctx = useContext(DataTableContext);
|
|
59
65
|
if (!ctx) {
|
|
60
|
-
throw new Error(
|
|
66
|
+
throw new Error(
|
|
67
|
+
"useDataTableContext must be used within <DataTable.Provider>",
|
|
68
|
+
);
|
|
61
69
|
}
|
|
62
70
|
return ctx as DataTableContextValue<TRow>;
|
|
63
71
|
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createElement,
|
|
3
3
|
useContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
4
8
|
type ComponentProps,
|
|
5
9
|
type ReactNode,
|
|
6
10
|
} from "react";
|
|
7
11
|
import { cn } from "../lib/utils";
|
|
8
12
|
import { CollectionProvider } from "../collection/collection-provider";
|
|
9
13
|
import { Table } from "../table";
|
|
10
|
-
import type {
|
|
11
|
-
Column,
|
|
12
|
-
DataTableRootProps,
|
|
13
|
-
PageInfo,
|
|
14
|
-
UseDataTableReturn,
|
|
15
|
-
} from "../types";
|
|
14
|
+
import type { Column, RowAction, UseDataTableReturn } from "../types";
|
|
16
15
|
import {
|
|
17
16
|
DataTableContext,
|
|
18
17
|
type DataTableContextValue,
|
|
@@ -23,66 +22,28 @@ import {
|
|
|
23
22
|
// =============================================================================
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
25
|
+
* Root container for the DataTable compound component.
|
|
26
|
+
*
|
|
27
|
+
* Must be used within `DataTable.Provider`.
|
|
28
|
+
* Renders a `<Table.Root>` wrapper; all data (including `onClickRow` and
|
|
29
|
+
* `rowActions`) is read from context via `useDataTable()` options.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <DataTable.Root>
|
|
34
|
+
* <DataTable.Headers />
|
|
35
|
+
* <DataTable.Body />
|
|
36
|
+
* </DataTable.Root>
|
|
37
|
+
* ```
|
|
27
38
|
*/
|
|
28
|
-
|
|
29
|
-
updateRow: () => ({ rollback: () => {} }),
|
|
30
|
-
deleteRow: () => ({
|
|
31
|
-
rollback: () => {},
|
|
32
|
-
deletedRow: {} as Record<string, unknown>,
|
|
33
|
-
}),
|
|
34
|
-
insertRow: () => ({ rollback: () => {} }),
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const defaultPageInfo: PageInfo = {
|
|
38
|
-
hasNextPage: false,
|
|
39
|
-
endCursor: null,
|
|
40
|
-
hasPreviousPage: false,
|
|
41
|
-
startCursor: null,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function DataTableRoot<TRow extends Record<string, unknown>>({
|
|
45
|
-
columns = [] as Column<TRow>[],
|
|
46
|
-
rows = [] as TRow[],
|
|
47
|
-
loading = false,
|
|
48
|
-
error = null,
|
|
49
|
-
sortStates = [],
|
|
50
|
-
onSort,
|
|
51
|
-
rowOperations,
|
|
39
|
+
function DataTableRoot({
|
|
52
40
|
children,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const contextValue: DataTableContextValue<TRow> = {
|
|
60
|
-
updateRow: (rowOperations?.updateRow ??
|
|
61
|
-
noopRowOps.updateRow) as DataTableContextValue<TRow>["updateRow"],
|
|
62
|
-
deleteRow: (rowOperations?.deleteRow ??
|
|
63
|
-
noopRowOps.deleteRow) as DataTableContextValue<TRow>["deleteRow"],
|
|
64
|
-
insertRow: (rowOperations?.insertRow ??
|
|
65
|
-
noopRowOps.insertRow) as DataTableContextValue<TRow>["insertRow"],
|
|
66
|
-
columns,
|
|
67
|
-
rows,
|
|
68
|
-
loading,
|
|
69
|
-
error,
|
|
70
|
-
sortStates,
|
|
71
|
-
onSort,
|
|
72
|
-
// Inherit column visibility & pageInfo from parent context (DataTable.Provider)
|
|
73
|
-
visibleColumns: parentCtx?.visibleColumns ?? columns,
|
|
74
|
-
isColumnVisible: parentCtx?.isColumnVisible ?? (() => true),
|
|
75
|
-
toggleColumn: parentCtx?.toggleColumn ?? (() => {}),
|
|
76
|
-
showAllColumns: parentCtx?.showAllColumns ?? (() => {}),
|
|
77
|
-
hideAllColumns: parentCtx?.hideAllColumns ?? (() => {}),
|
|
78
|
-
pageInfo: parentCtx?.pageInfo ?? defaultPageInfo,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<DataTableContext.Provider value={contextValue}>
|
|
83
|
-
<Table.Root>{children}</Table.Root>
|
|
84
|
-
</DataTableContext.Provider>
|
|
85
|
-
);
|
|
41
|
+
className,
|
|
42
|
+
}: {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
className?: string;
|
|
45
|
+
}) {
|
|
46
|
+
return <Table.Root className={className}>{children}</Table.Root>;
|
|
86
47
|
}
|
|
87
48
|
|
|
88
49
|
// =============================================================================
|
|
@@ -125,8 +86,8 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
|
125
86
|
rows: value.rows,
|
|
126
87
|
loading: value.loading,
|
|
127
88
|
error: value.error,
|
|
128
|
-
sortStates: value.
|
|
129
|
-
onSort: value.
|
|
89
|
+
sortStates: value.sortStates ?? [],
|
|
90
|
+
onSort: value.onSort,
|
|
130
91
|
updateRow: value.updateRow,
|
|
131
92
|
deleteRow: value.deleteRow,
|
|
132
93
|
insertRow: value.insertRow,
|
|
@@ -136,6 +97,8 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
|
136
97
|
showAllColumns: value.showAllColumns,
|
|
137
98
|
hideAllColumns: value.hideAllColumns,
|
|
138
99
|
pageInfo: value.pageInfo,
|
|
100
|
+
onClickRow: value.onClickRow,
|
|
101
|
+
rowActions: value.rowActions,
|
|
139
102
|
};
|
|
140
103
|
|
|
141
104
|
const collectionValue = value.collection ?? null;
|
|
@@ -160,16 +123,14 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
|
160
123
|
// DataTable.Headers
|
|
161
124
|
// =============================================================================
|
|
162
125
|
|
|
163
|
-
|
|
164
|
-
className?: string;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function DataTableHeaders({ className }: DataTableHeadersProps) {
|
|
126
|
+
function DataTableHeaders({ className }: { className?: string }) {
|
|
168
127
|
const ctx = useContext(DataTableContext);
|
|
169
128
|
if (!ctx) {
|
|
170
|
-
throw new Error(
|
|
129
|
+
throw new Error(
|
|
130
|
+
"<DataTable.Headers> must be used within <DataTable.Provider>",
|
|
131
|
+
);
|
|
171
132
|
}
|
|
172
|
-
const { columns, sortStates, onSort } = ctx;
|
|
133
|
+
const { columns, sortStates, onSort, rowActions } = ctx;
|
|
173
134
|
|
|
174
135
|
return (
|
|
175
136
|
<Table.Headers className={className}>
|
|
@@ -208,6 +169,11 @@ function DataTableHeaders({ className }: DataTableHeadersProps) {
|
|
|
208
169
|
</Table.HeaderCell>
|
|
209
170
|
);
|
|
210
171
|
})}
|
|
172
|
+
{rowActions && rowActions.length > 0 && (
|
|
173
|
+
<Table.HeaderCell style={{ width: 50 }}>
|
|
174
|
+
<span className="sr-only">操作</span>
|
|
175
|
+
</Table.HeaderCell>
|
|
176
|
+
)}
|
|
211
177
|
</Table.HeaderRow>
|
|
212
178
|
</Table.Headers>
|
|
213
179
|
);
|
|
@@ -225,17 +191,22 @@ function SortIndicator({ direction }: { direction: "Asc" | "Desc" }) {
|
|
|
225
191
|
// DataTable.Body
|
|
226
192
|
// =============================================================================
|
|
227
193
|
|
|
228
|
-
|
|
194
|
+
function DataTableBody({
|
|
195
|
+
children,
|
|
196
|
+
className,
|
|
197
|
+
}: {
|
|
229
198
|
children?: ReactNode;
|
|
230
199
|
className?: string;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
200
|
+
}) {
|
|
234
201
|
const ctx = useContext(DataTableContext);
|
|
235
202
|
if (!ctx) {
|
|
236
|
-
throw new Error(
|
|
203
|
+
throw new Error(
|
|
204
|
+
"<DataTable.Body> must be used within <DataTable.Provider>",
|
|
205
|
+
);
|
|
237
206
|
}
|
|
238
|
-
const { columns, rows, loading, error } = ctx;
|
|
207
|
+
const { columns, rows, loading, error, onClickRow, rowActions } = ctx;
|
|
208
|
+
const hasRowActions = rowActions && rowActions.length > 0;
|
|
209
|
+
const totalColSpan = (columns?.length ?? 1) + (hasRowActions ? 1 : 0);
|
|
239
210
|
|
|
240
211
|
// If children are provided, render them directly (custom rendering)
|
|
241
212
|
if (children) {
|
|
@@ -248,7 +219,7 @@ function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
|
248
219
|
{loading && (!rows || rows.length === 0) && (
|
|
249
220
|
<Table.Row>
|
|
250
221
|
<Table.Cell
|
|
251
|
-
colSpan={
|
|
222
|
+
colSpan={totalColSpan}
|
|
252
223
|
className="h-24 text-center"
|
|
253
224
|
>
|
|
254
225
|
<span className="text-muted-foreground">Loading...</span>
|
|
@@ -258,7 +229,7 @@ function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
|
258
229
|
{error && (
|
|
259
230
|
<Table.Row>
|
|
260
231
|
<Table.Cell
|
|
261
|
-
colSpan={
|
|
232
|
+
colSpan={totalColSpan}
|
|
262
233
|
className="h-24 text-center"
|
|
263
234
|
>
|
|
264
235
|
<span className="text-destructive">Error: {error.message}</span>
|
|
@@ -268,7 +239,7 @@ function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
|
268
239
|
{!loading && !error && (!rows || rows.length === 0) && (
|
|
269
240
|
<Table.Row>
|
|
270
241
|
<Table.Cell
|
|
271
|
-
colSpan={
|
|
242
|
+
colSpan={totalColSpan}
|
|
272
243
|
className="h-24 text-center"
|
|
273
244
|
>
|
|
274
245
|
<span className="text-muted-foreground">No data</span>
|
|
@@ -276,19 +247,31 @@ function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
|
276
247
|
</Table.Row>
|
|
277
248
|
)}
|
|
278
249
|
{rows?.map((row, rowIndex) => (
|
|
279
|
-
<
|
|
250
|
+
<Table.Row
|
|
251
|
+
key={rowIndex}
|
|
252
|
+
className={cn(onClickRow && "cursor-pointer")}
|
|
253
|
+
onClick={onClickRow ? () => onClickRow(row) : undefined}
|
|
254
|
+
>
|
|
280
255
|
{columns?.map((col) => {
|
|
281
256
|
const key = col.kind === "field" ? col.dataKey : col.id;
|
|
282
257
|
return (
|
|
283
|
-
<
|
|
258
|
+
<Table.Cell
|
|
284
259
|
key={key}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
260
|
+
style={col.width ? { width: col.width } : undefined}
|
|
261
|
+
>
|
|
262
|
+
{resolveContent(row, col, rowIndex)}
|
|
263
|
+
</Table.Cell>
|
|
289
264
|
);
|
|
290
265
|
})}
|
|
291
|
-
|
|
266
|
+
{hasRowActions && (
|
|
267
|
+
<Table.Cell
|
|
268
|
+
style={{ width: 50 }}
|
|
269
|
+
onClick={(e) => e.stopPropagation()}
|
|
270
|
+
>
|
|
271
|
+
<RowActionsMenu actions={rowActions} row={row} />
|
|
272
|
+
</Table.Cell>
|
|
273
|
+
)}
|
|
274
|
+
</Table.Row>
|
|
292
275
|
))}
|
|
293
276
|
</Table.Body>
|
|
294
277
|
);
|
|
@@ -298,82 +281,54 @@ function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
|
298
281
|
// DataTable.Row
|
|
299
282
|
// =============================================================================
|
|
300
283
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function DataTableRow<TRow extends Record<string, unknown>>({
|
|
309
|
-
children,
|
|
310
|
-
...restProps
|
|
311
|
-
}: DataTableRowComponentProps<TRow>) {
|
|
312
|
-
return <Table.Row {...restProps}>{children}</Table.Row>;
|
|
284
|
+
/**
|
|
285
|
+
* Thin wrapper around `Table.Row` for use in custom rendering within
|
|
286
|
+
* `DataTable.Body`.
|
|
287
|
+
*/
|
|
288
|
+
function DataTableRow(props: ComponentProps<"tr">) {
|
|
289
|
+
return <Table.Row {...props} />;
|
|
313
290
|
}
|
|
314
291
|
|
|
315
292
|
// =============================================================================
|
|
316
293
|
// DataTable.Cell
|
|
317
294
|
// =============================================================================
|
|
318
295
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Thin wrapper around `Table.Cell` for use in custom rendering within
|
|
298
|
+
* `DataTable.Body`.
|
|
299
|
+
*/
|
|
300
|
+
function DataTableCell(props: ComponentProps<"td">) {
|
|
301
|
+
return <Table.Cell {...props} />;
|
|
325
302
|
}
|
|
326
303
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
rowIndex = 0,
|
|
331
|
-
children,
|
|
332
|
-
className,
|
|
333
|
-
...restProps
|
|
334
|
-
}: DataTableCellComponentProps<TRow>) {
|
|
335
|
-
const content = resolveContent(children, row, column, rowIndex);
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<Table.Cell
|
|
339
|
-
className={className}
|
|
340
|
-
style={column?.width ? { width: column.width } : undefined}
|
|
341
|
-
{...restProps}
|
|
342
|
-
>
|
|
343
|
-
{content}
|
|
344
|
-
</Table.Cell>
|
|
345
|
-
);
|
|
346
|
-
}
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// Helpers
|
|
306
|
+
// =============================================================================
|
|
347
307
|
|
|
348
308
|
/**
|
|
349
|
-
* Resolve cell content from
|
|
309
|
+
* Resolve cell content from row data and column definition.
|
|
350
310
|
*/
|
|
351
311
|
function resolveContent<TRow extends Record<string, unknown>>(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
column: Column<TRow> | undefined,
|
|
312
|
+
row: TRow,
|
|
313
|
+
column: Column<TRow>,
|
|
355
314
|
rowIndex: number,
|
|
356
315
|
): ReactNode {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
return formatValue(value);
|
|
316
|
+
switch (column.kind) {
|
|
317
|
+
case "field": {
|
|
318
|
+
const value = row[column.dataKey];
|
|
319
|
+
if (column.renderer) {
|
|
320
|
+
return createElement(column.renderer, {
|
|
321
|
+
value,
|
|
322
|
+
row,
|
|
323
|
+
rowIndex,
|
|
324
|
+
column,
|
|
325
|
+
});
|
|
370
326
|
}
|
|
371
|
-
|
|
372
|
-
return column.render(row);
|
|
327
|
+
return formatValue(value);
|
|
373
328
|
}
|
|
329
|
+
case "display":
|
|
330
|
+
return column.render(row);
|
|
374
331
|
}
|
|
375
|
-
|
|
376
|
-
return children;
|
|
377
332
|
}
|
|
378
333
|
|
|
379
334
|
/**
|
|
@@ -387,6 +342,95 @@ function formatValue(value: unknown): ReactNode {
|
|
|
387
342
|
return String(value);
|
|
388
343
|
}
|
|
389
344
|
|
|
345
|
+
// =============================================================================
|
|
346
|
+
// RowActionsMenu
|
|
347
|
+
// =============================================================================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Dropdown menu for row-level actions.
|
|
351
|
+
*
|
|
352
|
+
* Uses a simple toggle-based dropdown without external dependencies.
|
|
353
|
+
*/
|
|
354
|
+
function RowActionsMenu<TRow extends Record<string, unknown>>({
|
|
355
|
+
actions,
|
|
356
|
+
row,
|
|
357
|
+
}: {
|
|
358
|
+
actions: RowAction<TRow>[];
|
|
359
|
+
row: TRow;
|
|
360
|
+
}) {
|
|
361
|
+
const [open, setOpen] = useState(false);
|
|
362
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
363
|
+
|
|
364
|
+
// Close on outside click
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (!open) return;
|
|
367
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
368
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
369
|
+
setOpen(false);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
373
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
374
|
+
}, [open]);
|
|
375
|
+
|
|
376
|
+
// Close on Escape
|
|
377
|
+
const handleKeyDown = useCallback(
|
|
378
|
+
(e: React.KeyboardEvent) => {
|
|
379
|
+
if (e.key === "Escape") setOpen(false);
|
|
380
|
+
},
|
|
381
|
+
[],
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div className="relative inline-block" ref={menuRef} onKeyDown={handleKeyDown}>
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm hover:bg-accent"
|
|
389
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
390
|
+
aria-label="Row actions"
|
|
391
|
+
aria-haspopup="true"
|
|
392
|
+
aria-expanded={open}
|
|
393
|
+
>
|
|
394
|
+
<span aria-hidden>⋯</span>
|
|
395
|
+
</button>
|
|
396
|
+
{open && (
|
|
397
|
+
<div
|
|
398
|
+
className="absolute right-0 z-50 mt-1 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
399
|
+
role="menu"
|
|
400
|
+
>
|
|
401
|
+
{actions.map((action) => {
|
|
402
|
+
const disabled = action.isDisabled?.(row) ?? false;
|
|
403
|
+
return (
|
|
404
|
+
<button
|
|
405
|
+
key={action.id}
|
|
406
|
+
type="button"
|
|
407
|
+
role="menuitem"
|
|
408
|
+
disabled={disabled}
|
|
409
|
+
className={cn(
|
|
410
|
+
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm",
|
|
411
|
+
disabled
|
|
412
|
+
? "cursor-not-allowed opacity-50"
|
|
413
|
+
: "cursor-pointer hover:bg-accent",
|
|
414
|
+
action.variant === "destructive" && "text-destructive",
|
|
415
|
+
)}
|
|
416
|
+
onClick={() => {
|
|
417
|
+
if (!disabled) {
|
|
418
|
+
action.onClick(row);
|
|
419
|
+
setOpen(false);
|
|
420
|
+
}
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
{action.icon}
|
|
424
|
+
{action.label}
|
|
425
|
+
</button>
|
|
426
|
+
);
|
|
427
|
+
})}
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
390
434
|
// =============================================================================
|
|
391
435
|
// DataTable namespace
|
|
392
436
|
// =============================================================================
|
|
@@ -394,12 +438,11 @@ function formatValue(value: unknown): ReactNode {
|
|
|
394
438
|
/**
|
|
395
439
|
* Data-bound table compound component.
|
|
396
440
|
*
|
|
397
|
-
* Use with `useDataTable()` hook
|
|
398
|
-
*
|
|
441
|
+
* Use with `useDataTable()` hook and wrap with `DataTable.Provider`
|
|
442
|
+
* for context-based data flow.
|
|
399
443
|
*
|
|
400
444
|
* @example
|
|
401
445
|
* ```tsx
|
|
402
|
-
* // Context-based (recommended)
|
|
403
446
|
* const table = useDataTable({ columns, data, loading, collection });
|
|
404
447
|
*
|
|
405
448
|
* <DataTable.Provider value={table}>
|
|
@@ -411,12 +454,6 @@ function formatValue(value: unknown): ReactNode {
|
|
|
411
454
|
* </DataTable.Root>
|
|
412
455
|
* <Pagination />
|
|
413
456
|
* </DataTable.Provider>
|
|
414
|
-
*
|
|
415
|
-
* // Props-based (still supported)
|
|
416
|
-
* <DataTable.Root {...table.rootProps}>
|
|
417
|
-
* <DataTable.Headers />
|
|
418
|
-
* <DataTable.Body />
|
|
419
|
-
* </DataTable.Root>
|
|
420
457
|
* ```
|
|
421
458
|
*/
|
|
422
459
|
export const DataTable = {
|
|
@@ -234,46 +234,23 @@ describe("useDataTable", () => {
|
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
// ---------------------------------------------------------------------------
|
|
237
|
-
//
|
|
237
|
+
// Sort state
|
|
238
238
|
// ---------------------------------------------------------------------------
|
|
239
|
-
describe("
|
|
240
|
-
it("
|
|
241
|
-
const { result } = renderHook(() =>
|
|
242
|
-
useDataTable<TestRow>({
|
|
243
|
-
columns: testColumns,
|
|
244
|
-
data: testData,
|
|
245
|
-
loading: true,
|
|
246
|
-
}),
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const { rootProps } = result.current;
|
|
250
|
-
expect(rootProps.columns).toHaveLength(4);
|
|
251
|
-
expect(rootProps.rows).toHaveLength(3);
|
|
252
|
-
expect(rootProps.loading).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("getRowProps returns row", () => {
|
|
239
|
+
describe("sort state", () => {
|
|
240
|
+
it("sortStates is empty when no collection is provided", () => {
|
|
256
241
|
const { result } = renderHook(() =>
|
|
257
242
|
useDataTable<TestRow>({ columns: testColumns, data: testData }),
|
|
258
243
|
);
|
|
259
244
|
|
|
260
|
-
|
|
261
|
-
expect(rowProps.row.name).toBe("Alice");
|
|
245
|
+
expect(result.current.sortStates).toEqual([]);
|
|
262
246
|
});
|
|
263
247
|
|
|
264
|
-
it("
|
|
248
|
+
it("onSort is undefined when no collection is provided", () => {
|
|
265
249
|
const { result } = renderHook(() =>
|
|
266
250
|
useDataTable<TestRow>({ columns: testColumns, data: testData }),
|
|
267
251
|
);
|
|
268
252
|
|
|
269
|
-
|
|
270
|
-
result.current.rows[0],
|
|
271
|
-
testColumns[0],
|
|
272
|
-
0,
|
|
273
|
-
);
|
|
274
|
-
expect(cellProps.row.name).toBe("Alice");
|
|
275
|
-
expect(cellProps.column).toBe(testColumns[0]);
|
|
276
|
-
expect(cellProps.rowIndex).toBe(0);
|
|
253
|
+
expect(result.current.onSort).toBeUndefined();
|
|
277
254
|
});
|
|
278
255
|
});
|
|
279
256
|
});
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import type {
|
|
3
3
|
Column,
|
|
4
|
-
DataTableCellProps,
|
|
5
|
-
DataTableRootProps,
|
|
6
|
-
DataTableRowProps,
|
|
7
4
|
PageInfo,
|
|
5
|
+
SortState,
|
|
8
6
|
UseDataTableOptions,
|
|
9
7
|
UseDataTableReturn,
|
|
10
8
|
} from "../types";
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* Hook that integrates data management, column visibility, row operations, and
|
|
14
|
-
*
|
|
12
|
+
* sort/pagination state for the `DataTable.*` compound component.
|
|
15
13
|
*
|
|
16
14
|
* @example
|
|
17
15
|
* ```tsx
|
|
@@ -25,10 +23,12 @@ import type {
|
|
|
25
23
|
* collection,
|
|
26
24
|
* });
|
|
27
25
|
*
|
|
28
|
-
* <DataTable.
|
|
29
|
-
* <DataTable.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* <DataTable.Provider value={table}>
|
|
27
|
+
* <DataTable.Root>
|
|
28
|
+
* <DataTable.Headers />
|
|
29
|
+
* <DataTable.Body />
|
|
30
|
+
* </DataTable.Root>
|
|
31
|
+
* </DataTable.Provider>
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
export function useDataTable<TRow extends Record<string, unknown>>(
|
|
@@ -40,6 +40,8 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
40
40
|
loading = false,
|
|
41
41
|
error = null,
|
|
42
42
|
collection,
|
|
43
|
+
onClickRow,
|
|
44
|
+
rowActions,
|
|
43
45
|
} = options;
|
|
44
46
|
|
|
45
47
|
// ---------------------------------------------------------------------------
|
|
@@ -194,64 +196,30 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
194
196
|
);
|
|
195
197
|
|
|
196
198
|
// ---------------------------------------------------------------------------
|
|
197
|
-
//
|
|
199
|
+
// Sort (delegated from collection)
|
|
198
200
|
// ---------------------------------------------------------------------------
|
|
199
|
-
const
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
rowOperations: { updateRow, deleteRow, insertRow },
|
|
211
|
-
children: null,
|
|
212
|
-
};
|
|
213
|
-
}, [
|
|
214
|
-
visibleColumns,
|
|
215
|
-
rows,
|
|
216
|
-
loading,
|
|
217
|
-
error,
|
|
218
|
-
collection,
|
|
219
|
-
updateRow,
|
|
220
|
-
deleteRow,
|
|
221
|
-
insertRow,
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
const getRowProps = useCallback(
|
|
225
|
-
(row: TRow): DataTableRowProps<TRow> => ({ row }),
|
|
226
|
-
[],
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const getCellProps = useCallback(
|
|
230
|
-
(
|
|
231
|
-
row: TRow,
|
|
232
|
-
column: Column<TRow>,
|
|
233
|
-
rowIndex: number,
|
|
234
|
-
): DataTableCellProps<TRow> => ({
|
|
235
|
-
row,
|
|
236
|
-
column,
|
|
237
|
-
rowIndex,
|
|
238
|
-
}),
|
|
239
|
-
[],
|
|
240
|
-
);
|
|
201
|
+
const sortStates = useMemo<SortState[]>(() => {
|
|
202
|
+
return collection?.sortStates ?? [];
|
|
203
|
+
}, [collection?.sortStates]);
|
|
204
|
+
|
|
205
|
+
const onSort = useMemo<
|
|
206
|
+
((field: string, direction?: "Asc" | "Desc") => void) | undefined
|
|
207
|
+
>(() => {
|
|
208
|
+
if (!collection) return undefined;
|
|
209
|
+
return (field: string, direction?: "Asc" | "Desc") =>
|
|
210
|
+
collection.setSort(field, direction);
|
|
211
|
+
}, [collection]);
|
|
241
212
|
|
|
242
213
|
// ---------------------------------------------------------------------------
|
|
243
214
|
// Return
|
|
244
215
|
// ---------------------------------------------------------------------------
|
|
245
216
|
return {
|
|
246
|
-
// Props generators
|
|
247
|
-
rootProps,
|
|
248
|
-
getRowProps,
|
|
249
|
-
getCellProps,
|
|
250
|
-
|
|
251
217
|
// Data
|
|
252
218
|
rows,
|
|
253
219
|
loading,
|
|
254
220
|
error,
|
|
221
|
+
sortStates,
|
|
222
|
+
onSort,
|
|
255
223
|
|
|
256
224
|
// Pagination
|
|
257
225
|
pageInfo,
|
|
@@ -275,5 +243,9 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
275
243
|
|
|
276
244
|
// Collection (passthrough for DataTable.Provider)
|
|
277
245
|
collection,
|
|
246
|
+
|
|
247
|
+
// Row interaction (passthrough for DataTable.Provider)
|
|
248
|
+
onClickRow,
|
|
249
|
+
rowActions,
|
|
278
250
|
};
|
|
279
251
|
}
|
package/src/component/index.ts
CHANGED
|
@@ -22,10 +22,8 @@ export type {
|
|
|
22
22
|
UseCollectionReturn,
|
|
23
23
|
UseDataTableOptions,
|
|
24
24
|
UseDataTableReturn,
|
|
25
|
+
RowAction,
|
|
25
26
|
RowOperations,
|
|
26
|
-
DataTableRootProps,
|
|
27
|
-
DataTableRowProps,
|
|
28
|
-
DataTableCellProps,
|
|
29
27
|
FieldName,
|
|
30
28
|
TableFieldName,
|
|
31
29
|
OrderableFieldName,
|
|
@@ -62,7 +60,7 @@ export {
|
|
|
62
60
|
export { Table } from "./table";
|
|
63
61
|
|
|
64
62
|
// DataTable (data-bound)
|
|
65
|
-
export { DataTable } from "./data-table/
|
|
63
|
+
export { DataTable } from "./data-table/data-table";
|
|
66
64
|
export { useDataTable } from "./data-table/use-data-table";
|
|
67
65
|
export { useDataTableContext } from "./data-table/data-table-context";
|
|
68
66
|
|
package/src/component/types.ts
CHANGED
|
@@ -497,6 +497,10 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
|
|
|
497
497
|
error?: Error | null;
|
|
498
498
|
/** Collection state for sort/pagination integration */
|
|
499
499
|
collection?: UseCollectionReturn<string, unknown>;
|
|
500
|
+
/** Handler called when a row is clicked */
|
|
501
|
+
onClickRow?: (row: TRow) => void;
|
|
502
|
+
/** Row action definitions for the actions column */
|
|
503
|
+
rowActions?: RowAction<TRow>[];
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
/**
|
|
@@ -511,63 +515,45 @@ export interface RowOperations<TRow extends Record<string, unknown>> {
|
|
|
511
515
|
insertRow: (row: TRow) => { rollback: () => void };
|
|
512
516
|
}
|
|
513
517
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
export interface DataTableRootProps<TRow extends Record<string, unknown>> {
|
|
518
|
-
/** Visible column definitions */
|
|
519
|
-
columns: Column<TRow>[];
|
|
520
|
-
/** Row data */
|
|
521
|
-
rows: TRow[];
|
|
522
|
-
/** Loading state */
|
|
523
|
-
loading?: boolean;
|
|
524
|
-
/** Error */
|
|
525
|
-
error?: Error | null;
|
|
526
|
-
/** Sort handler (connected to collection) */
|
|
527
|
-
onSort?: (field: string, direction?: "Asc" | "Desc") => void;
|
|
528
|
-
/** Current sort states */
|
|
529
|
-
sortStates?: SortState[];
|
|
530
|
-
/** Row operations (provided to children via Context) */
|
|
531
|
-
rowOperations?: RowOperations<TRow>;
|
|
532
|
-
children: ReactNode;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Props for `DataTable.Row` component.
|
|
537
|
-
*/
|
|
538
|
-
export interface DataTableRowProps<TRow extends Record<string, unknown>> {
|
|
539
|
-
/** The row data */
|
|
540
|
-
row: TRow;
|
|
541
|
-
}
|
|
518
|
+
// =============================================================================
|
|
519
|
+
// Row Actions
|
|
520
|
+
// =============================================================================
|
|
542
521
|
|
|
543
522
|
/**
|
|
544
|
-
*
|
|
523
|
+
* A single row action definition for the actions column.
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```tsx
|
|
527
|
+
* const actions: RowAction<Order>[] = [
|
|
528
|
+
* {
|
|
529
|
+
* id: "delete",
|
|
530
|
+
* label: "削除",
|
|
531
|
+
* icon: <Trash2 className="h-4 w-4" />,
|
|
532
|
+
* variant: "destructive",
|
|
533
|
+
* onClick: (row) => handleDelete(row.id),
|
|
534
|
+
* },
|
|
535
|
+
* ];
|
|
536
|
+
* ```
|
|
545
537
|
*/
|
|
546
|
-
export interface
|
|
547
|
-
/**
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
|
|
538
|
+
export interface RowAction<TRow extends Record<string, unknown>> {
|
|
539
|
+
/** Unique action identifier */
|
|
540
|
+
id: string;
|
|
541
|
+
/** Action label text */
|
|
542
|
+
label: string;
|
|
543
|
+
/** Optional icon element */
|
|
544
|
+
icon?: ReactNode;
|
|
545
|
+
/** Visual variant */
|
|
546
|
+
variant?: "default" | "destructive";
|
|
547
|
+
/** Whether the action is disabled for a given row */
|
|
548
|
+
isDisabled?: (row: TRow) => boolean;
|
|
549
|
+
/** Click handler receiving the row data */
|
|
550
|
+
onClick: (row: TRow) => void;
|
|
553
551
|
}
|
|
554
552
|
|
|
555
553
|
/**
|
|
556
554
|
* Return type of `useDataTable` hook.
|
|
557
555
|
*/
|
|
558
556
|
export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
559
|
-
// Props generators (for spreading)
|
|
560
|
-
/** Props for DataTable.Root */
|
|
561
|
-
rootProps: DataTableRootProps<TRow>;
|
|
562
|
-
/** Get props for a DataTable.Row */
|
|
563
|
-
getRowProps: (row: TRow) => DataTableRowProps<TRow>;
|
|
564
|
-
/** Get props for a DataTable.Cell */
|
|
565
|
-
getCellProps: (
|
|
566
|
-
row: TRow,
|
|
567
|
-
column: Column<TRow>,
|
|
568
|
-
rowIndex: number,
|
|
569
|
-
) => DataTableCellProps<TRow>;
|
|
570
|
-
|
|
571
557
|
// Data
|
|
572
558
|
/** Row data extracted from collection result */
|
|
573
559
|
rows: TRow[];
|
|
@@ -575,6 +561,10 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
|
575
561
|
loading: boolean;
|
|
576
562
|
/** Error */
|
|
577
563
|
error: Error | null;
|
|
564
|
+
/** Current sort states */
|
|
565
|
+
sortStates: SortState[];
|
|
566
|
+
/** Sort handler (connected to collection) */
|
|
567
|
+
onSort?: (field: string, direction?: "Asc" | "Desc") => void;
|
|
578
568
|
|
|
579
569
|
// Pagination (delegated from collection)
|
|
580
570
|
/** Page info from GraphQL response */
|
|
@@ -613,6 +603,12 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
|
613
603
|
// Collection (passthrough for DataTable.Provider)
|
|
614
604
|
/** Collection state passed through from options */
|
|
615
605
|
collection: UseCollectionReturn<string, unknown> | undefined;
|
|
606
|
+
|
|
607
|
+
// Row interaction (passthrough for DataTable.Provider)
|
|
608
|
+
/** Handler called when a row is clicked */
|
|
609
|
+
onClickRow?: (row: TRow) => void;
|
|
610
|
+
/** Row action definitions for the actions column */
|
|
611
|
+
rowActions?: RowAction<TRow>[];
|
|
616
612
|
}
|
|
617
613
|
|
|
618
614
|
// =============================================================================
|