@izumisy-tailor/tailor-data-viewer 0.1.21 → 0.1.23
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/README.md +45 -1
- package/dist/generator/index.d.mts +54 -31
- package/dist/generator/index.mjs +20 -13
- package/docs/compositional-api.md +366 -0
- package/package.json +1 -1
- package/src/app-shell/create-data-view-module.tsx +1 -1
- package/src/component/column-selector.test.tsx +143 -103
- package/src/component/column-selector.tsx +121 -156
- package/src/component/contexts/data-viewer-context.test.tsx +191 -0
- package/src/component/contexts/data-viewer-context.tsx +244 -0
- package/src/component/contexts/index.ts +19 -0
- package/src/component/contexts/table-data-context.tsx +114 -0
- package/src/component/contexts/toolbar-context.tsx +62 -0
- package/src/component/csv-button.tsx +79 -0
- package/src/component/data-table-toolbar.test.tsx +127 -72
- package/src/component/data-table-toolbar.tsx +14 -151
- package/src/component/data-table.tsx +255 -225
- package/src/component/data-view-tab-content.tsx +68 -138
- package/src/component/data-viewer.tsx +11 -11
- package/src/component/hooks/use-column-state.ts +2 -2
- package/src/component/hooks/use-table-data.test.ts +399 -0
- package/src/component/hooks/use-table-data.ts +24 -7
- package/src/component/index.ts +43 -1
- package/src/component/refresh-button.tsx +20 -0
- package/src/component/saved-view-context.tsx +31 -2
- package/src/component/search-filter.test.tsx +612 -0
- package/src/component/search-filter.tsx +168 -33
- package/src/component/single-record-tab-content.test.tsx +10 -10
- package/src/component/single-record-tab-content.tsx +62 -21
- package/src/component/types.ts +78 -0
- package/src/component/view-save-load.tsx +13 -17
- package/src/generator/metadata-generator.ts +100 -67
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { buildQueryInput } from "./use-table-data";
|
|
3
|
+
import type { SearchFilters } from "../types";
|
|
4
|
+
|
|
5
|
+
describe("buildQueryInput", () => {
|
|
6
|
+
describe("基本的な動作", () => {
|
|
7
|
+
it("空のフィルター配列では undefined を返す", () => {
|
|
8
|
+
const result = buildQueryInput([]);
|
|
9
|
+
expect(result).toBeUndefined();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("単一のフィルターでクエリオブジェクトを生成する", () => {
|
|
13
|
+
const filters: SearchFilters = [
|
|
14
|
+
{ field: "name", fieldType: "string", operator: "eq", value: "test" },
|
|
15
|
+
];
|
|
16
|
+
const result = buildQueryInput(filters);
|
|
17
|
+
expect(result).toEqual({
|
|
18
|
+
name: { eq: "test" },
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("複数のフィルターでクエリオブジェクトを生成する", () => {
|
|
23
|
+
const filters: SearchFilters = [
|
|
24
|
+
{ field: "name", fieldType: "string", operator: "eq", value: "test" },
|
|
25
|
+
{ field: "age", fieldType: "number", operator: "gt", value: "25" },
|
|
26
|
+
];
|
|
27
|
+
const result = buildQueryInput(filters);
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
name: { eq: "test" },
|
|
30
|
+
age: { gt: 25 },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("文字列フィールド", () => {
|
|
36
|
+
it("eq 演算子で等価検索クエリを生成する", () => {
|
|
37
|
+
const filters: SearchFilters = [
|
|
38
|
+
{ field: "name", fieldType: "string", operator: "eq", value: "test" },
|
|
39
|
+
];
|
|
40
|
+
const result = buildQueryInput(filters);
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
name: { eq: "test" },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("contains 演算子で部分一致検索クエリを生成する", () => {
|
|
47
|
+
const filters: SearchFilters = [
|
|
48
|
+
{
|
|
49
|
+
field: "name",
|
|
50
|
+
fieldType: "string",
|
|
51
|
+
operator: "contains",
|
|
52
|
+
value: "test",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
const result = buildQueryInput(filters);
|
|
56
|
+
expect(result).toEqual({
|
|
57
|
+
name: { contains: "test" },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("hasPrefix 演算子で前方一致検索クエリを生成する", () => {
|
|
62
|
+
const filters: SearchFilters = [
|
|
63
|
+
{
|
|
64
|
+
field: "name",
|
|
65
|
+
fieldType: "string",
|
|
66
|
+
operator: "hasPrefix",
|
|
67
|
+
value: "pre",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
const result = buildQueryInput(filters);
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
name: { hasPrefix: "pre" },
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("hasSuffix 演算子で後方一致検索クエリを生成する", () => {
|
|
77
|
+
const filters: SearchFilters = [
|
|
78
|
+
{
|
|
79
|
+
field: "email",
|
|
80
|
+
fieldType: "string",
|
|
81
|
+
operator: "hasSuffix",
|
|
82
|
+
value: ".com",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const result = buildQueryInput(filters);
|
|
86
|
+
expect(result).toEqual({
|
|
87
|
+
email: { hasSuffix: ".com" },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("数値フィールド", () => {
|
|
93
|
+
it("eq 演算子で等価検索クエリを生成する", () => {
|
|
94
|
+
const filters: SearchFilters = [
|
|
95
|
+
{ field: "age", fieldType: "number", operator: "eq", value: "25" },
|
|
96
|
+
];
|
|
97
|
+
const result = buildQueryInput(filters);
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
age: { eq: 25 },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("gt 演算子で大なり検索クエリを生成する", () => {
|
|
104
|
+
const filters: SearchFilters = [
|
|
105
|
+
{ field: "age", fieldType: "number", operator: "gt", value: "18" },
|
|
106
|
+
];
|
|
107
|
+
const result = buildQueryInput(filters);
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
age: { gt: 18 },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("lt 演算子で小なり検索クエリを生成する", () => {
|
|
114
|
+
const filters: SearchFilters = [
|
|
115
|
+
{ field: "age", fieldType: "number", operator: "lt", value: "65" },
|
|
116
|
+
];
|
|
117
|
+
const result = buildQueryInput(filters);
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
age: { lt: 65 },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("小数点を含む数値を正しく変換する", () => {
|
|
124
|
+
const filters: SearchFilters = [
|
|
125
|
+
{ field: "price", fieldType: "number", operator: "gt", value: "99.99" },
|
|
126
|
+
];
|
|
127
|
+
const result = buildQueryInput(filters);
|
|
128
|
+
expect(result).toEqual({
|
|
129
|
+
price: { gt: 99.99 },
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("負の数値を正しく変換する", () => {
|
|
134
|
+
const filters: SearchFilters = [
|
|
135
|
+
{
|
|
136
|
+
field: "temperature",
|
|
137
|
+
fieldType: "number",
|
|
138
|
+
operator: "lt",
|
|
139
|
+
value: "-10",
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
const result = buildQueryInput(filters);
|
|
143
|
+
expect(result).toEqual({
|
|
144
|
+
temperature: { lt: -10 },
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("無効な数値はフィルターに含まれない", () => {
|
|
149
|
+
const filters: SearchFilters = [
|
|
150
|
+
{ field: "age", fieldType: "number", operator: "eq", value: "abc" },
|
|
151
|
+
];
|
|
152
|
+
const result = buildQueryInput(filters);
|
|
153
|
+
expect(result).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("booleanフィールド", () => {
|
|
158
|
+
it("true 値で検索クエリを生成する", () => {
|
|
159
|
+
const filters: SearchFilters = [
|
|
160
|
+
{
|
|
161
|
+
field: "isActive",
|
|
162
|
+
fieldType: "boolean",
|
|
163
|
+
operator: "eq",
|
|
164
|
+
value: true,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
const result = buildQueryInput(filters);
|
|
168
|
+
expect(result).toEqual({
|
|
169
|
+
isActive: { eq: true },
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("false 値で検索クエリを生成する", () => {
|
|
174
|
+
const filters: SearchFilters = [
|
|
175
|
+
{
|
|
176
|
+
field: "isActive",
|
|
177
|
+
fieldType: "boolean",
|
|
178
|
+
operator: "eq",
|
|
179
|
+
value: false,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
const result = buildQueryInput(filters);
|
|
183
|
+
expect(result).toEqual({
|
|
184
|
+
isActive: { eq: false },
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("enumフィールド", () => {
|
|
190
|
+
it("enum 値で検索クエリを生成する", () => {
|
|
191
|
+
const filters: SearchFilters = [
|
|
192
|
+
{
|
|
193
|
+
field: "status",
|
|
194
|
+
fieldType: "enum",
|
|
195
|
+
operator: "eq",
|
|
196
|
+
value: "ACTIVE",
|
|
197
|
+
enumValues: ["ACTIVE", "INACTIVE", "PENDING"],
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
const result = buildQueryInput(filters);
|
|
201
|
+
expect(result).toEqual({
|
|
202
|
+
status: { eq: "ACTIVE" },
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("UUIDフィールド", () => {
|
|
208
|
+
it("UUID 値で検索クエリを生成する", () => {
|
|
209
|
+
const filters: SearchFilters = [
|
|
210
|
+
{
|
|
211
|
+
field: "id",
|
|
212
|
+
fieldType: "uuid",
|
|
213
|
+
operator: "eq",
|
|
214
|
+
value: "123e4567-e89b-12d3-a456-426614174000",
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
const result = buildQueryInput(filters);
|
|
218
|
+
expect(result).toEqual({
|
|
219
|
+
id: { eq: "123e4567-e89b-12d3-a456-426614174000" },
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("日付フィールド", () => {
|
|
225
|
+
it("date フィールドで eq 演算子の検索クエリを生成する", () => {
|
|
226
|
+
const filters: SearchFilters = [
|
|
227
|
+
{
|
|
228
|
+
field: "birthDate",
|
|
229
|
+
fieldType: "date",
|
|
230
|
+
operator: "eq",
|
|
231
|
+
value: "1990-01-15",
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
const result = buildQueryInput(filters);
|
|
235
|
+
expect(result).toEqual({
|
|
236
|
+
birthDate: { eq: "1990-01-15" },
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("date フィールドで gt 演算子の検索クエリを生成する", () => {
|
|
241
|
+
const filters: SearchFilters = [
|
|
242
|
+
{
|
|
243
|
+
field: "birthDate",
|
|
244
|
+
fieldType: "date",
|
|
245
|
+
operator: "gt",
|
|
246
|
+
value: "2000-01-01",
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
const result = buildQueryInput(filters);
|
|
250
|
+
expect(result).toEqual({
|
|
251
|
+
birthDate: { gt: "2000-01-01" },
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("date フィールドで lt 演算子の検索クエリを生成する", () => {
|
|
256
|
+
const filters: SearchFilters = [
|
|
257
|
+
{
|
|
258
|
+
field: "birthDate",
|
|
259
|
+
fieldType: "date",
|
|
260
|
+
operator: "lt",
|
|
261
|
+
value: "1980-12-31",
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
const result = buildQueryInput(filters);
|
|
265
|
+
expect(result).toEqual({
|
|
266
|
+
birthDate: { lt: "1980-12-31" },
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("空の日付値はフィルターに含まれない", () => {
|
|
271
|
+
const filters: SearchFilters = [
|
|
272
|
+
{ field: "birthDate", fieldType: "date", operator: "eq", value: "" },
|
|
273
|
+
];
|
|
274
|
+
const result = buildQueryInput(filters);
|
|
275
|
+
expect(result).toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("日時フィールド", () => {
|
|
280
|
+
// Mock Date to ensure consistent ISO string output
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
vi.useFakeTimers();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
afterEach(() => {
|
|
286
|
+
vi.useRealTimers();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("datetime フィールドで eq 演算子の検索クエリを生成する", () => {
|
|
290
|
+
// datetime-local format: "YYYY-MM-DDTHH:mm"
|
|
291
|
+
const filters: SearchFilters = [
|
|
292
|
+
{
|
|
293
|
+
field: "createdAt",
|
|
294
|
+
fieldType: "datetime",
|
|
295
|
+
operator: "eq",
|
|
296
|
+
value: "2024-01-15T10:30",
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
const result = buildQueryInput(filters);
|
|
300
|
+
|
|
301
|
+
// The result should contain an ISO string
|
|
302
|
+
expect(result).toBeDefined();
|
|
303
|
+
expect(result!.createdAt).toBeDefined();
|
|
304
|
+
expect((result!.createdAt as { eq: string }).eq).toMatch(
|
|
305
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("datetime フィールドで gt 演算子の検索クエリを生成する", () => {
|
|
310
|
+
const filters: SearchFilters = [
|
|
311
|
+
{
|
|
312
|
+
field: "createdAt",
|
|
313
|
+
fieldType: "datetime",
|
|
314
|
+
operator: "gt",
|
|
315
|
+
value: "2024-01-01T00:00",
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
const result = buildQueryInput(filters);
|
|
319
|
+
|
|
320
|
+
expect(result).toBeDefined();
|
|
321
|
+
expect(result!.createdAt).toBeDefined();
|
|
322
|
+
expect((result!.createdAt as { gt: string }).gt).toMatch(
|
|
323
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("datetime フィールドで lt 演算子の検索クエリを生成する", () => {
|
|
328
|
+
const filters: SearchFilters = [
|
|
329
|
+
{
|
|
330
|
+
field: "updatedAt",
|
|
331
|
+
fieldType: "datetime",
|
|
332
|
+
operator: "lt",
|
|
333
|
+
value: "2024-12-31T23:59",
|
|
334
|
+
},
|
|
335
|
+
];
|
|
336
|
+
const result = buildQueryInput(filters);
|
|
337
|
+
|
|
338
|
+
expect(result).toBeDefined();
|
|
339
|
+
expect(result!.updatedAt).toBeDefined();
|
|
340
|
+
expect((result!.updatedAt as { lt: string }).lt).toMatch(
|
|
341
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("空の日時値はフィルターに含まれない", () => {
|
|
346
|
+
const filters: SearchFilters = [
|
|
347
|
+
{
|
|
348
|
+
field: "createdAt",
|
|
349
|
+
fieldType: "datetime",
|
|
350
|
+
operator: "eq",
|
|
351
|
+
value: "",
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
const result = buildQueryInput(filters);
|
|
355
|
+
expect(result).toBeUndefined();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("複合フィルター", () => {
|
|
360
|
+
it("異なる型のフィルターを組み合わせてクエリを生成する", () => {
|
|
361
|
+
const filters: SearchFilters = [
|
|
362
|
+
{
|
|
363
|
+
field: "name",
|
|
364
|
+
fieldType: "string",
|
|
365
|
+
operator: "contains",
|
|
366
|
+
value: "test",
|
|
367
|
+
},
|
|
368
|
+
{ field: "age", fieldType: "number", operator: "gt", value: "18" },
|
|
369
|
+
{
|
|
370
|
+
field: "isActive",
|
|
371
|
+
fieldType: "boolean",
|
|
372
|
+
operator: "eq",
|
|
373
|
+
value: true,
|
|
374
|
+
},
|
|
375
|
+
{ field: "status", fieldType: "enum", operator: "eq", value: "ACTIVE" },
|
|
376
|
+
];
|
|
377
|
+
const result = buildQueryInput(filters);
|
|
378
|
+
expect(result).toEqual({
|
|
379
|
+
name: { contains: "test" },
|
|
380
|
+
age: { gt: 18 },
|
|
381
|
+
isActive: { eq: true },
|
|
382
|
+
status: { eq: "ACTIVE" },
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("同じフィールドタイプで異なる演算子のフィルターを処理する", () => {
|
|
387
|
+
// Note: In actual use, same field shouldn't appear twice, but testing the function behavior
|
|
388
|
+
const filters: SearchFilters = [
|
|
389
|
+
{ field: "minAge", fieldType: "number", operator: "gt", value: "18" },
|
|
390
|
+
{ field: "maxAge", fieldType: "number", operator: "lt", value: "65" },
|
|
391
|
+
];
|
|
392
|
+
const result = buildQueryInput(filters);
|
|
393
|
+
expect(result).toEqual({
|
|
394
|
+
minAge: { gt: 18 },
|
|
395
|
+
maxAge: { lt: 65 },
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -56,8 +56,9 @@ const DEFAULT_PAGE_SIZE = 20;
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Build query input from search filters (AND logic)
|
|
59
|
+
* @internal Exported for testing purposes
|
|
59
60
|
*/
|
|
60
|
-
function buildQueryInput(
|
|
61
|
+
export function buildQueryInput(
|
|
61
62
|
filters: SearchFilters,
|
|
62
63
|
): Record<string, unknown> | undefined {
|
|
63
64
|
if (filters.length === 0) return undefined;
|
|
@@ -65,19 +66,35 @@ function buildQueryInput(
|
|
|
65
66
|
const queryInput: Record<string, unknown> = {};
|
|
66
67
|
|
|
67
68
|
for (const filter of filters) {
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
const operator = filter.operator;
|
|
70
|
+
|
|
70
71
|
if (filter.fieldType === "boolean") {
|
|
72
|
+
// Boolean always uses eq
|
|
71
73
|
queryInput[filter.field] = { eq: filter.value };
|
|
72
74
|
} else if (filter.fieldType === "number") {
|
|
73
|
-
// Parse number value
|
|
75
|
+
// Parse number value and apply operator
|
|
74
76
|
const numValue = parseFloat(filter.value as string);
|
|
75
77
|
if (!isNaN(numValue)) {
|
|
76
|
-
queryInput[filter.field] = {
|
|
78
|
+
queryInput[filter.field] = { [operator]: numValue };
|
|
79
|
+
}
|
|
80
|
+
} else if (filter.fieldType === "datetime") {
|
|
81
|
+
// DateTime: convert local datetime string to ISO format
|
|
82
|
+
const dateValue = filter.value as string;
|
|
83
|
+
if (dateValue) {
|
|
84
|
+
// datetime-local format is "YYYY-MM-DDTHH:mm"
|
|
85
|
+
// Need to convert to ISO 8601 format for GraphQL
|
|
86
|
+
const isoDate = new Date(dateValue).toISOString();
|
|
87
|
+
queryInput[filter.field] = { [operator]: isoDate };
|
|
88
|
+
}
|
|
89
|
+
} else if (filter.fieldType === "date") {
|
|
90
|
+
// Date: use as-is (YYYY-MM-DD format)
|
|
91
|
+
const dateValue = filter.value as string;
|
|
92
|
+
if (dateValue) {
|
|
93
|
+
queryInput[filter.field] = { [operator]: dateValue };
|
|
77
94
|
}
|
|
78
95
|
} else {
|
|
79
|
-
// string, enum, uuid - use string value
|
|
80
|
-
queryInput[filter.field] = {
|
|
96
|
+
// string, enum, uuid - use string value with operator
|
|
97
|
+
queryInput[filter.field] = { [operator]: filter.value };
|
|
81
98
|
}
|
|
82
99
|
}
|
|
83
100
|
|
package/src/component/index.ts
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
|
+
// Main DataViewer component
|
|
1
2
|
export { DataViewer } from "./data-viewer";
|
|
2
3
|
export type { InitialQuery } from "./data-viewer";
|
|
4
|
+
|
|
5
|
+
// Toolbar component (children-based)
|
|
3
6
|
export { DataTableToolbar } from "./data-table-toolbar";
|
|
4
|
-
|
|
7
|
+
|
|
8
|
+
// Saved view context
|
|
5
9
|
export { useSavedViews, SavedViewProvider } from "./saved-view-context";
|
|
6
10
|
export type { SavedView, SaveViewInput } from "./saved-view-context";
|
|
11
|
+
|
|
12
|
+
// Context providers and hooks
|
|
13
|
+
export { DataViewerProvider, useDataViewer } from "./contexts";
|
|
14
|
+
export type {
|
|
15
|
+
DataViewerContextValue,
|
|
16
|
+
DataViewerProviderProps,
|
|
17
|
+
DataViewerInitialData,
|
|
18
|
+
} from "./contexts";
|
|
19
|
+
export { TableDataProvider, useTableDataContext } from "./contexts";
|
|
20
|
+
export type { TableDataContextValue, TableDataProviderProps } from "./contexts";
|
|
21
|
+
export { ToolbarProvider, useToolbar } from "./contexts";
|
|
22
|
+
export type {
|
|
23
|
+
ToolbarContextValue,
|
|
24
|
+
ToolbarProviderProps,
|
|
25
|
+
ToolbarActivePanel,
|
|
26
|
+
} from "./contexts";
|
|
27
|
+
|
|
28
|
+
// Individual components (context-only, must be used within DataViewerProvider)
|
|
29
|
+
export { ColumnSelector } from "./column-selector";
|
|
30
|
+
export { SearchFilterForm } from "./search-filter";
|
|
31
|
+
export { ViewSave } from "./view-save-load";
|
|
32
|
+
export { DataTable } from "./data-table";
|
|
33
|
+
export type { DataTableProps } from "./data-table";
|
|
34
|
+
export { CsvButton } from "./csv-button";
|
|
35
|
+
export { RefreshButton } from "./refresh-button";
|
|
36
|
+
|
|
37
|
+
// Types
|
|
38
|
+
export type {
|
|
39
|
+
SearchFilter,
|
|
40
|
+
SearchFilters,
|
|
41
|
+
FilterOperator,
|
|
42
|
+
StringFilterOperator,
|
|
43
|
+
NumberFilterOperator,
|
|
44
|
+
DateFilterOperator,
|
|
45
|
+
BooleanFilterOperator,
|
|
46
|
+
EnumFilterOperator,
|
|
47
|
+
} from "./types";
|
|
48
|
+
export { OPERATOR_LABELS, OPERATORS_BY_FIELD_TYPE } from "./types";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RefreshCw } from "lucide-react";
|
|
2
|
+
import { Button } from "./ui/button";
|
|
3
|
+
import { useDataViewer } from "./contexts";
|
|
4
|
+
import { useTableDataContext } from "./contexts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Refresh button
|
|
8
|
+
* Must be used within DataViewer.Root and TableDataProvider context.
|
|
9
|
+
*/
|
|
10
|
+
export function RefreshButton() {
|
|
11
|
+
const { refetch } = useDataViewer();
|
|
12
|
+
const { loading } = useTableDataContext();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Button variant="outline" size="sm" onClick={refetch} disabled={loading}>
|
|
16
|
+
<RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />
|
|
17
|
+
更新
|
|
18
|
+
</Button>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from "react";
|
|
9
9
|
import type { ExpandedRelationFields } from "../generator/metadata-generator";
|
|
10
|
-
import type { SearchFilters } from "./types";
|
|
10
|
+
import type { SearchFilters, SearchFilter } from "./types";
|
|
11
11
|
import type {
|
|
12
12
|
SavedViewStore,
|
|
13
13
|
SavedView as StoreSavedView,
|
|
@@ -66,6 +66,35 @@ interface SavedViewProviderProps {
|
|
|
66
66
|
store?: SavedViewStore;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Migrate a filter from old format (without operator) to new format (with operator)
|
|
71
|
+
* Adds default "eq" operator for backward compatibility
|
|
72
|
+
*/
|
|
73
|
+
function migrateFilter(
|
|
74
|
+
filter:
|
|
75
|
+
| SearchFilter
|
|
76
|
+
| (Omit<SearchFilter, "operator"> & { operator?: never }),
|
|
77
|
+
): SearchFilter {
|
|
78
|
+
// If operator is already present, return as-is
|
|
79
|
+
if ("operator" in filter && filter.operator !== undefined) {
|
|
80
|
+
return filter as SearchFilter;
|
|
81
|
+
}
|
|
82
|
+
// Add default "eq" operator for backward compatibility
|
|
83
|
+
return {
|
|
84
|
+
...filter,
|
|
85
|
+
operator: "eq",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Migrate filters array from old format to new format
|
|
91
|
+
*/
|
|
92
|
+
function migrateFilters(
|
|
93
|
+
filters: SearchFilters | Array<Omit<SearchFilter, "operator">>,
|
|
94
|
+
): SearchFilters {
|
|
95
|
+
return filters.map(migrateFilter);
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
/**
|
|
70
99
|
* Convert store format to UI format
|
|
71
100
|
*/
|
|
@@ -74,7 +103,7 @@ function fromStoreView(storeView: StoreSavedView): SavedView {
|
|
|
74
103
|
id: storeView.id,
|
|
75
104
|
name: storeView.name,
|
|
76
105
|
tableName: storeView.tableName,
|
|
77
|
-
filters: storeView.filters,
|
|
106
|
+
filters: migrateFilters(storeView.filters),
|
|
78
107
|
selectedFields: storeView.columns,
|
|
79
108
|
selectedRelations: storeView.selectedRelations,
|
|
80
109
|
expandedRelationFields: storeView.expandedRelationFields,
|