@izumisy-tailor/tailor-data-viewer 0.1.25 → 0.1.27
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
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
} from "../../generator/metadata-generator";
|
|
14
14
|
import { useColumnState } from "../hooks/use-column-state";
|
|
15
15
|
import type { SearchFilters } from "../types";
|
|
16
|
+
import type { SortState } from "../hooks/use-table-data";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Helper type to extract field names from a table in metadata
|
|
@@ -94,6 +95,9 @@ export interface DataViewerContextValue {
|
|
|
94
95
|
filters: SearchFilters;
|
|
95
96
|
setFilters: (filters: SearchFilters) => void;
|
|
96
97
|
|
|
98
|
+
// Sort (initial value, managed by TableDataProvider)
|
|
99
|
+
initialSort: SortState[];
|
|
100
|
+
|
|
97
101
|
// Actions (set by TableDataProvider)
|
|
98
102
|
refetch: () => Promise<void>;
|
|
99
103
|
|
|
@@ -119,6 +123,16 @@ export interface DataViewerInitialData<
|
|
|
119
123
|
selectedRelations?: ExtractRelationNames<TMetadata, TTableName>[];
|
|
120
124
|
/** Initial expanded relation fields (type-safe when metadata is `as const`) */
|
|
121
125
|
expandedRelationFields?: ExtractExpandedRelationFields<TMetadata, TTableName>;
|
|
126
|
+
/** Initial sort state (supports multiple sort fields) */
|
|
127
|
+
sort?:
|
|
128
|
+
| {
|
|
129
|
+
field: ExtractFieldNames<TMetadata, TTableName>;
|
|
130
|
+
direction: "Asc" | "Desc";
|
|
131
|
+
}
|
|
132
|
+
| {
|
|
133
|
+
field: ExtractFieldNames<TMetadata, TTableName>;
|
|
134
|
+
direction: "Asc" | "Desc";
|
|
135
|
+
}[];
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
/**
|
|
@@ -162,6 +176,21 @@ export function DataViewerProvider<
|
|
|
162
176
|
initialData?.filters ?? [],
|
|
163
177
|
);
|
|
164
178
|
|
|
179
|
+
// Initial sort state (passed to TableDataProvider)
|
|
180
|
+
const initialSort: SortState[] = initialData?.sort
|
|
181
|
+
? Array.isArray(initialData.sort)
|
|
182
|
+
? initialData.sort.map((s) => ({
|
|
183
|
+
field: s.field as string,
|
|
184
|
+
direction: s.direction,
|
|
185
|
+
}))
|
|
186
|
+
: [
|
|
187
|
+
{
|
|
188
|
+
field: initialData.sort.field as string,
|
|
189
|
+
direction: initialData.sort.direction,
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
: [];
|
|
193
|
+
|
|
165
194
|
// Column state
|
|
166
195
|
const columnState = useColumnState(
|
|
167
196
|
tableMetadata?.fields ?? [],
|
|
@@ -198,6 +227,7 @@ export function DataViewerProvider<
|
|
|
198
227
|
setColumns: columnState.setColumns,
|
|
199
228
|
filters,
|
|
200
229
|
setFilters,
|
|
230
|
+
initialSort,
|
|
201
231
|
refetch,
|
|
202
232
|
_setRefetch,
|
|
203
233
|
}),
|
|
@@ -216,6 +246,7 @@ export function DataViewerProvider<
|
|
|
216
246
|
columnState.isExpandedRelationFieldSelected,
|
|
217
247
|
columnState.setColumns,
|
|
218
248
|
filters,
|
|
249
|
+
initialSort,
|
|
219
250
|
refetch,
|
|
220
251
|
_setRefetch,
|
|
221
252
|
],
|
|
@@ -20,7 +20,10 @@ export interface TableDataContextValue {
|
|
|
20
20
|
data: Record<string, unknown>[];
|
|
21
21
|
loading: boolean;
|
|
22
22
|
error: Error | null;
|
|
23
|
+
/** Current sort state (first sort field for UI display) */
|
|
23
24
|
sortState: SortState | null;
|
|
25
|
+
/** All sort states (for multi-field sorting) */
|
|
26
|
+
sortStates: SortState[];
|
|
24
27
|
setSort: (field: string) => void;
|
|
25
28
|
pagination: PaginationState;
|
|
26
29
|
hasPreviousPage: boolean;
|
|
@@ -47,6 +50,7 @@ export function TableDataProvider({ children }: TableDataProviderProps) {
|
|
|
47
50
|
selectedRelations,
|
|
48
51
|
expandedRelationFields,
|
|
49
52
|
filters,
|
|
53
|
+
initialSort,
|
|
50
54
|
_setRefetch,
|
|
51
55
|
} = useDataViewer();
|
|
52
56
|
|
|
@@ -59,6 +63,7 @@ export function TableDataProvider({ children }: TableDataProviderProps) {
|
|
|
59
63
|
filters,
|
|
60
64
|
metadata,
|
|
61
65
|
expandedRelationFields,
|
|
66
|
+
initialSort,
|
|
62
67
|
);
|
|
63
68
|
|
|
64
69
|
// Register refetch action in DataViewerContext
|
|
@@ -72,6 +77,7 @@ export function TableDataProvider({ children }: TableDataProviderProps) {
|
|
|
72
77
|
loading: tableData.loading,
|
|
73
78
|
error: tableData.error,
|
|
74
79
|
sortState: tableData.sortState,
|
|
80
|
+
sortStates: tableData.sortStates,
|
|
75
81
|
setSort: tableData.setSort,
|
|
76
82
|
pagination: tableData.pagination,
|
|
77
83
|
hasPreviousPage: tableData.hasPreviousPage,
|
|
@@ -83,6 +89,7 @@ export function TableDataProvider({ children }: TableDataProviderProps) {
|
|
|
83
89
|
tableData.loading,
|
|
84
90
|
tableData.error,
|
|
85
91
|
tableData.sortState,
|
|
92
|
+
tableData.sortStates,
|
|
86
93
|
tableData.setSort,
|
|
87
94
|
tableData.pagination,
|
|
88
95
|
tableData.hasPreviousPage,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { buildQueryInput } from "./use-table-data";
|
|
2
|
+
import { buildQueryInput, buildOrderInput } from "./use-table-data";
|
|
3
3
|
import type { SearchFilters } from "../types";
|
|
4
4
|
|
|
5
5
|
describe("buildQueryInput", () => {
|
|
@@ -397,3 +397,41 @@ describe("buildQueryInput", () => {
|
|
|
397
397
|
});
|
|
398
398
|
});
|
|
399
399
|
});
|
|
400
|
+
|
|
401
|
+
describe("buildOrderInput", () => {
|
|
402
|
+
it("空のソート配列では undefined を返す", () => {
|
|
403
|
+
const result = buildOrderInput([]);
|
|
404
|
+
expect(result).toBeUndefined();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("単一のソート条件でorder配列を生成する", () => {
|
|
408
|
+
const sortStates = [{ field: "status", direction: "Desc" as const }];
|
|
409
|
+
const result = buildOrderInput(sortStates);
|
|
410
|
+
expect(result).toEqual([{ field: "status", direction: "Desc" }]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("複数のソート条件でorder配列を生成する", () => {
|
|
414
|
+
const sortStates = [
|
|
415
|
+
{ field: "status", direction: "Desc" as const },
|
|
416
|
+
{ field: "createdAt", direction: "Asc" as const },
|
|
417
|
+
];
|
|
418
|
+
const result = buildOrderInput(sortStates);
|
|
419
|
+
expect(result).toEqual([
|
|
420
|
+
{ field: "status", direction: "Desc" },
|
|
421
|
+
{ field: "createdAt", direction: "Asc" },
|
|
422
|
+
]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("ソート条件の順序が保持される", () => {
|
|
426
|
+
const sortStates = [
|
|
427
|
+
{ field: "priority", direction: "Asc" as const },
|
|
428
|
+
{ field: "name", direction: "Desc" as const },
|
|
429
|
+
{ field: "updatedAt", direction: "Asc" as const },
|
|
430
|
+
];
|
|
431
|
+
const result = buildOrderInput(sortStates);
|
|
432
|
+
expect(result).toHaveLength(3);
|
|
433
|
+
expect(result![0].field).toBe("priority");
|
|
434
|
+
expect(result![1].field).toBe("name");
|
|
435
|
+
expect(result![2].field).toBe("updatedAt");
|
|
436
|
+
});
|
|
437
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import type {
|
|
3
3
|
TableMetadata,
|
|
4
4
|
TableMetadataMap,
|
|
@@ -31,7 +31,10 @@ export interface TableDataState {
|
|
|
31
31
|
data: Record<string, unknown>[];
|
|
32
32
|
loading: boolean;
|
|
33
33
|
error: Error | null;
|
|
34
|
+
/** Current sort state (first sort field for UI display) */
|
|
34
35
|
sortState: SortState | null;
|
|
36
|
+
/** All sort states (for multi-field sorting) */
|
|
37
|
+
sortStates: SortState[];
|
|
35
38
|
pagination: PaginationState;
|
|
36
39
|
hasPreviousPage: boolean;
|
|
37
40
|
refetch: () => Promise<void>;
|
|
@@ -101,6 +104,17 @@ export function buildQueryInput(
|
|
|
101
104
|
return Object.keys(queryInput).length > 0 ? queryInput : undefined;
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Build order input from sort states for GraphQL query
|
|
109
|
+
* @internal Exported for testing purposes
|
|
110
|
+
*/
|
|
111
|
+
export function buildOrderInput(
|
|
112
|
+
sortStates: SortState[],
|
|
113
|
+
): { field: string; direction: "Asc" | "Desc" }[] | undefined {
|
|
114
|
+
if (sortStates.length === 0) return undefined;
|
|
115
|
+
return sortStates.map((s) => ({ field: s.field, direction: s.direction }));
|
|
116
|
+
}
|
|
117
|
+
|
|
104
118
|
/**
|
|
105
119
|
* Fetch and manage table data with sorting and pagination
|
|
106
120
|
*/
|
|
@@ -112,11 +126,12 @@ export function useTableData(
|
|
|
112
126
|
searchFilters?: SearchFilters,
|
|
113
127
|
tableMetadataMap?: TableMetadataMap,
|
|
114
128
|
expandedRelationFields?: ExpandedRelationFields,
|
|
129
|
+
initialSort?: SortState[],
|
|
115
130
|
): TableDataState {
|
|
116
131
|
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
|
117
132
|
const [loading, setLoading] = useState(false);
|
|
118
133
|
const [error, setError] = useState<Error | null>(null);
|
|
119
|
-
const [
|
|
134
|
+
const [sortStates, setSortStates] = useState<SortState[]>(initialSort ?? []);
|
|
120
135
|
const [pagination, setPagination] = useState<PaginationState>({
|
|
121
136
|
first: DEFAULT_PAGE_SIZE,
|
|
122
137
|
after: null,
|
|
@@ -125,13 +140,22 @@ export function useTableData(
|
|
|
125
140
|
});
|
|
126
141
|
const [cursorHistory, setCursorHistory] = useState<(string | null)[]>([]);
|
|
127
142
|
|
|
143
|
+
// Track previous table name to detect actual table changes (not initial mount)
|
|
144
|
+
const prevTableNameRef = useRef<string | undefined>(table?.name);
|
|
145
|
+
|
|
128
146
|
const client = useMemo(() => createGraphQLClient(appUri), [appUri]);
|
|
129
147
|
|
|
130
|
-
// Clear data
|
|
148
|
+
// Clear data when table actually changes (not on initial mount)
|
|
131
149
|
useEffect(() => {
|
|
150
|
+
// Skip on initial mount - only reset when table changes from one to another
|
|
151
|
+
if (prevTableNameRef.current === table?.name) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
prevTableNameRef.current = table?.name;
|
|
155
|
+
|
|
132
156
|
setData([]);
|
|
133
157
|
setError(null);
|
|
134
|
-
|
|
158
|
+
setSortStates(initialSort ?? []);
|
|
135
159
|
setPagination({
|
|
136
160
|
first: DEFAULT_PAGE_SIZE,
|
|
137
161
|
after: null,
|
|
@@ -139,6 +163,10 @@ export function useTableData(
|
|
|
139
163
|
endCursor: null,
|
|
140
164
|
});
|
|
141
165
|
setCursorHistory([]);
|
|
166
|
+
// Note: initialSort is intentionally excluded from deps to prevent infinite loops.
|
|
167
|
+
// This effect should only run when table changes, using the initialSort value
|
|
168
|
+
// that was current at mount time.
|
|
169
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
142
170
|
}, [table?.name]);
|
|
143
171
|
|
|
144
172
|
const fetchData = useCallback(async () => {
|
|
@@ -213,7 +241,7 @@ export function useTableData(
|
|
|
213
241
|
tableName: table.name,
|
|
214
242
|
pluralForm: table.pluralForm,
|
|
215
243
|
selectedFields: allFieldsToFetch,
|
|
216
|
-
orderBy:
|
|
244
|
+
orderBy: sortStates.length > 0 ? sortStates[0] : undefined,
|
|
217
245
|
first: pagination.first,
|
|
218
246
|
after: pagination.after ?? undefined,
|
|
219
247
|
oneToManyRelationTotals:
|
|
@@ -233,11 +261,10 @@ export function useTableData(
|
|
|
233
261
|
queryVariables.query = queryInput;
|
|
234
262
|
}
|
|
235
263
|
|
|
236
|
-
// Add sort order variable
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
];
|
|
264
|
+
// Add sort order variable (supports multiple sort fields)
|
|
265
|
+
const orderInput = buildOrderInput(sortStates);
|
|
266
|
+
if (orderInput) {
|
|
267
|
+
queryVariables.order = orderInput;
|
|
241
268
|
}
|
|
242
269
|
|
|
243
270
|
// Add pagination variables
|
|
@@ -271,7 +298,7 @@ export function useTableData(
|
|
|
271
298
|
table,
|
|
272
299
|
selectedFields,
|
|
273
300
|
selectedRelations,
|
|
274
|
-
|
|
301
|
+
sortStates,
|
|
275
302
|
pagination.first,
|
|
276
303
|
pagination.after,
|
|
277
304
|
client,
|
|
@@ -286,15 +313,18 @@ export function useTableData(
|
|
|
286
313
|
}, [fetchData]);
|
|
287
314
|
|
|
288
315
|
const setSort = useCallback((field: string, direction?: "Asc" | "Desc") => {
|
|
289
|
-
|
|
316
|
+
setSortStates((prev) => {
|
|
317
|
+
const currentSort = prev[0];
|
|
290
318
|
// If clicking the same field, toggle direction
|
|
291
|
-
if (
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
319
|
+
if (currentSort?.field === field && !direction) {
|
|
320
|
+
return [
|
|
321
|
+
{
|
|
322
|
+
field,
|
|
323
|
+
direction: currentSort.direction === "Asc" ? "Desc" : "Asc",
|
|
324
|
+
},
|
|
325
|
+
];
|
|
296
326
|
}
|
|
297
|
-
return { field, direction: direction ?? "Asc" };
|
|
327
|
+
return [{ field, direction: direction ?? "Asc" }];
|
|
298
328
|
});
|
|
299
329
|
// Reset pagination when sorting changes
|
|
300
330
|
setPagination((prev) => ({ ...prev, after: null }));
|
|
@@ -339,7 +369,8 @@ export function useTableData(
|
|
|
339
369
|
data,
|
|
340
370
|
loading,
|
|
341
371
|
error,
|
|
342
|
-
sortState,
|
|
372
|
+
sortState: sortStates[0] ?? null,
|
|
373
|
+
sortStates,
|
|
343
374
|
pagination,
|
|
344
375
|
hasPreviousPage: cursorHistory.length > 0 || pagination.after !== null,
|
|
345
376
|
refetch: fetchData,
|