@izumisy-tailor/tailor-data-viewer 0.1.21 → 0.1.22
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 +67 -138
- package/src/component/data-viewer.tsx +11 -11
- package/src/component/hooks/use-column-state.ts +2 -2
- package/src/component/index.ts +33 -1
- package/src/component/refresh-button.tsx +20 -0
- package/src/component/search-filter.tsx +19 -24
- package/src/component/single-record-tab-content.test.tsx +10 -10
- package/src/component/single-record-tab-content.tsx +62 -21
- package/src/component/view-save-load.tsx +13 -17
- package/src/generator/metadata-generator.ts +100 -67
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { render, screen, within, waitFor } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import { ColumnSelector } from "./column-selector";
|
|
@@ -6,7 +6,11 @@ import type {
|
|
|
6
6
|
FieldMetadata,
|
|
7
7
|
RelationMetadata,
|
|
8
8
|
TableMetadataMap,
|
|
9
|
+
TableMetadata,
|
|
9
10
|
} from "../generator/metadata-generator";
|
|
11
|
+
import { DataViewerProvider } from "./contexts";
|
|
12
|
+
import { ToolbarProvider } from "./contexts";
|
|
13
|
+
import type { ReactNode } from "react";
|
|
10
14
|
|
|
11
15
|
const mockFields: FieldMetadata[] = [
|
|
12
16
|
{ name: "id", type: "uuid", required: true, description: "ID" },
|
|
@@ -15,17 +19,55 @@ const mockFields: FieldMetadata[] = [
|
|
|
15
19
|
{ name: "createdAt", type: "datetime", required: false },
|
|
16
20
|
];
|
|
17
21
|
|
|
22
|
+
const mockTableMetadata: TableMetadata = {
|
|
23
|
+
name: "TestTable",
|
|
24
|
+
pluralForm: "TestTables",
|
|
25
|
+
readAllowedRoles: [],
|
|
26
|
+
fields: mockFields,
|
|
27
|
+
relations: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mockTableMetadataMap: TableMetadataMap = {
|
|
31
|
+
TestTable: mockTableMetadata,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface WrapperProps {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
tableMetadata?: TableMetadata;
|
|
37
|
+
metadata?: TableMetadataMap;
|
|
38
|
+
initialSelectedFields?: string[];
|
|
39
|
+
initialSelectedRelations?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TestWrapper({
|
|
43
|
+
children,
|
|
44
|
+
tableMetadata = mockTableMetadata,
|
|
45
|
+
metadata = mockTableMetadataMap,
|
|
46
|
+
initialSelectedFields,
|
|
47
|
+
initialSelectedRelations,
|
|
48
|
+
}: WrapperProps) {
|
|
49
|
+
return (
|
|
50
|
+
<DataViewerProvider
|
|
51
|
+
appUri="https://example.com"
|
|
52
|
+
tableName={tableMetadata.name}
|
|
53
|
+
metadata={metadata}
|
|
54
|
+
initialData={{
|
|
55
|
+
selectedFields: initialSelectedFields,
|
|
56
|
+
selectedRelations: initialSelectedRelations,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<ToolbarProvider>{children}</ToolbarProvider>
|
|
60
|
+
</DataViewerProvider>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
18
64
|
describe("ColumnSelector", () => {
|
|
19
65
|
describe("基本的な表示", () => {
|
|
20
66
|
it("カラム選択ボタンが表示される", () => {
|
|
21
67
|
render(
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
onToggle={vi.fn()}
|
|
26
|
-
onSelectAll={vi.fn()}
|
|
27
|
-
onDeselectAll={vi.fn()}
|
|
28
|
-
/>,
|
|
68
|
+
<TestWrapper initialSelectedFields={["id", "name"]}>
|
|
69
|
+
<ColumnSelector />
|
|
70
|
+
</TestWrapper>,
|
|
29
71
|
);
|
|
30
72
|
|
|
31
73
|
expect(screen.getByRole("button")).toHaveTextContent("カラム選択");
|
|
@@ -33,13 +75,9 @@ describe("ColumnSelector", () => {
|
|
|
33
75
|
|
|
34
76
|
it("選択数/総数が正しく表示される", () => {
|
|
35
77
|
render(
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
onToggle={vi.fn()}
|
|
40
|
-
onSelectAll={vi.fn()}
|
|
41
|
-
onDeselectAll={vi.fn()}
|
|
42
|
-
/>,
|
|
78
|
+
<TestWrapper initialSelectedFields={["id", "name"]}>
|
|
79
|
+
<ColumnSelector />
|
|
80
|
+
</TestWrapper>,
|
|
43
81
|
);
|
|
44
82
|
|
|
45
83
|
expect(screen.getByRole("button")).toHaveTextContent("(2/4)");
|
|
@@ -50,13 +88,9 @@ describe("ColumnSelector", () => {
|
|
|
50
88
|
it("ボタンクリックでドロップダウンが開く", async () => {
|
|
51
89
|
const user = userEvent.setup();
|
|
52
90
|
render(
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
onToggle={vi.fn()}
|
|
57
|
-
onSelectAll={vi.fn()}
|
|
58
|
-
onDeselectAll={vi.fn()}
|
|
59
|
-
/>,
|
|
91
|
+
<TestWrapper initialSelectedFields={["id", "name"]}>
|
|
92
|
+
<ColumnSelector />
|
|
93
|
+
</TestWrapper>,
|
|
60
94
|
);
|
|
61
95
|
|
|
62
96
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -71,13 +105,9 @@ describe("ColumnSelector", () => {
|
|
|
71
105
|
it("フィールド一覧が表示される", async () => {
|
|
72
106
|
const user = userEvent.setup();
|
|
73
107
|
render(
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
onToggle={vi.fn()}
|
|
78
|
-
onSelectAll={vi.fn()}
|
|
79
|
-
onDeselectAll={vi.fn()}
|
|
80
|
-
/>,
|
|
108
|
+
<TestWrapper initialSelectedFields={["id", "name"]}>
|
|
109
|
+
<ColumnSelector />
|
|
110
|
+
</TestWrapper>,
|
|
81
111
|
);
|
|
82
112
|
|
|
83
113
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -93,17 +123,12 @@ describe("ColumnSelector", () => {
|
|
|
93
123
|
});
|
|
94
124
|
|
|
95
125
|
describe("フィールド選択", () => {
|
|
96
|
-
it("
|
|
126
|
+
it("フィールドクリックで選択が切り替わる", async () => {
|
|
97
127
|
const user = userEvent.setup();
|
|
98
|
-
const onToggle = vi.fn();
|
|
99
128
|
render(
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
onToggle={onToggle}
|
|
104
|
-
onSelectAll={vi.fn()}
|
|
105
|
-
onDeselectAll={vi.fn()}
|
|
106
|
-
/>,
|
|
129
|
+
<TestWrapper initialSelectedFields={["id"]}>
|
|
130
|
+
<ColumnSelector />
|
|
131
|
+
</TestWrapper>,
|
|
107
132
|
);
|
|
108
133
|
|
|
109
134
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -117,20 +142,19 @@ describe("ColumnSelector", () => {
|
|
|
117
142
|
const nameLabel = body.getByText("name").closest("label")!;
|
|
118
143
|
await user.click(nameLabel);
|
|
119
144
|
|
|
120
|
-
|
|
145
|
+
// After clicking, should have 2 selected (id + name)
|
|
146
|
+
// The button is aria-hidden when dropdown is open, so we need to use hidden: true
|
|
147
|
+
expect(
|
|
148
|
+
screen.getByRole("button", { name: /カラム選択/, hidden: true }),
|
|
149
|
+
).toHaveTextContent("(2/4)");
|
|
121
150
|
});
|
|
122
151
|
|
|
123
|
-
it("
|
|
152
|
+
it("全選択ボタンで全フィールドが選択される", async () => {
|
|
124
153
|
const user = userEvent.setup();
|
|
125
|
-
const onSelectAll = vi.fn();
|
|
126
154
|
render(
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
onToggle={vi.fn()}
|
|
131
|
-
onSelectAll={onSelectAll}
|
|
132
|
-
onDeselectAll={vi.fn()}
|
|
133
|
-
/>,
|
|
155
|
+
<TestWrapper initialSelectedFields={[]}>
|
|
156
|
+
<ColumnSelector />
|
|
157
|
+
</TestWrapper>,
|
|
134
158
|
);
|
|
135
159
|
|
|
136
160
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -143,20 +167,18 @@ describe("ColumnSelector", () => {
|
|
|
143
167
|
const body = within(document.body);
|
|
144
168
|
await user.click(body.getByText("全選択"));
|
|
145
169
|
|
|
146
|
-
|
|
170
|
+
// The button is aria-hidden when dropdown is open, so we need to use hidden: true
|
|
171
|
+
expect(
|
|
172
|
+
screen.getByRole("button", { name: /カラム選択/, hidden: true }),
|
|
173
|
+
).toHaveTextContent("(4/4)");
|
|
147
174
|
});
|
|
148
175
|
|
|
149
|
-
it("
|
|
176
|
+
it("全解除ボタンで全フィールドが解除される", async () => {
|
|
150
177
|
const user = userEvent.setup();
|
|
151
|
-
const onDeselectAll = vi.fn();
|
|
152
178
|
render(
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
onToggle={vi.fn()}
|
|
157
|
-
onSelectAll={vi.fn()}
|
|
158
|
-
onDeselectAll={onDeselectAll}
|
|
159
|
-
/>,
|
|
179
|
+
<TestWrapper initialSelectedFields={["id", "name"]}>
|
|
180
|
+
<ColumnSelector />
|
|
181
|
+
</TestWrapper>,
|
|
160
182
|
);
|
|
161
183
|
|
|
162
184
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -169,7 +191,10 @@ describe("ColumnSelector", () => {
|
|
|
169
191
|
const body = within(document.body);
|
|
170
192
|
await user.click(body.getByText("全解除"));
|
|
171
193
|
|
|
172
|
-
|
|
194
|
+
// The button is aria-hidden when dropdown is open, so we need to use hidden: true
|
|
195
|
+
expect(
|
|
196
|
+
screen.getByRole("button", { name: /カラム選択/, hidden: true }),
|
|
197
|
+
).toHaveTextContent("(0/4)");
|
|
173
198
|
});
|
|
174
199
|
});
|
|
175
200
|
|
|
@@ -189,7 +214,14 @@ describe("ColumnSelector", () => {
|
|
|
189
214
|
},
|
|
190
215
|
];
|
|
191
216
|
|
|
192
|
-
const
|
|
217
|
+
const mockRelationTableMetadataMap: TableMetadataMap = {
|
|
218
|
+
TestTable: {
|
|
219
|
+
name: "TestTable",
|
|
220
|
+
pluralForm: "TestTables",
|
|
221
|
+
readAllowedRoles: [],
|
|
222
|
+
fields: mockFields,
|
|
223
|
+
relations: mockRelations,
|
|
224
|
+
},
|
|
193
225
|
User: {
|
|
194
226
|
name: "User",
|
|
195
227
|
pluralForm: "Users",
|
|
@@ -213,20 +245,24 @@ describe("ColumnSelector", () => {
|
|
|
213
245
|
},
|
|
214
246
|
};
|
|
215
247
|
|
|
248
|
+
const mockTableMetadataWithRelations: TableMetadata = {
|
|
249
|
+
name: "TestTable",
|
|
250
|
+
pluralForm: "TestTables",
|
|
251
|
+
readAllowedRoles: [],
|
|
252
|
+
fields: mockFields,
|
|
253
|
+
relations: mockRelations,
|
|
254
|
+
};
|
|
255
|
+
|
|
216
256
|
it("リレーションセクションが表示される", async () => {
|
|
217
257
|
const user = userEvent.setup();
|
|
218
258
|
render(
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
selectedRelations={[]}
|
|
227
|
-
onToggleRelation={vi.fn()}
|
|
228
|
-
tableMetadataMap={mockTableMetadataMap}
|
|
229
|
-
/>,
|
|
259
|
+
<TestWrapper
|
|
260
|
+
tableMetadata={mockTableMetadataWithRelations}
|
|
261
|
+
metadata={mockRelationTableMetadataMap}
|
|
262
|
+
initialSelectedFields={["id"]}
|
|
263
|
+
>
|
|
264
|
+
<ColumnSelector />
|
|
265
|
+
</TestWrapper>,
|
|
230
266
|
);
|
|
231
267
|
|
|
232
268
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -241,17 +277,13 @@ describe("ColumnSelector", () => {
|
|
|
241
277
|
it("リレーションフィールドが表示される", async () => {
|
|
242
278
|
const user = userEvent.setup();
|
|
243
279
|
render(
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
selectedRelations={[]}
|
|
252
|
-
onToggleRelation={vi.fn()}
|
|
253
|
-
tableMetadataMap={mockTableMetadataMap}
|
|
254
|
-
/>,
|
|
280
|
+
<TestWrapper
|
|
281
|
+
tableMetadata={mockTableMetadataWithRelations}
|
|
282
|
+
metadata={mockRelationTableMetadataMap}
|
|
283
|
+
initialSelectedFields={["id"]}
|
|
284
|
+
>
|
|
285
|
+
<ColumnSelector />
|
|
286
|
+
</TestWrapper>,
|
|
255
287
|
);
|
|
256
288
|
|
|
257
289
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -263,21 +295,17 @@ describe("ColumnSelector", () => {
|
|
|
263
295
|
});
|
|
264
296
|
});
|
|
265
297
|
|
|
266
|
-
it("
|
|
298
|
+
it("リレーションの切り替えで選択が変わる", async () => {
|
|
267
299
|
const user = userEvent.setup();
|
|
268
|
-
const onToggleRelation = vi.fn();
|
|
269
300
|
render(
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
onToggleRelation={onToggleRelation}
|
|
279
|
-
tableMetadataMap={mockTableMetadataMap}
|
|
280
|
-
/>,
|
|
301
|
+
<TestWrapper
|
|
302
|
+
tableMetadata={mockTableMetadataWithRelations}
|
|
303
|
+
metadata={mockRelationTableMetadataMap}
|
|
304
|
+
initialSelectedFields={["id"]}
|
|
305
|
+
initialSelectedRelations={[]}
|
|
306
|
+
>
|
|
307
|
+
<ColumnSelector />
|
|
308
|
+
</TestWrapper>,
|
|
281
309
|
);
|
|
282
310
|
|
|
283
311
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|
|
@@ -291,7 +319,11 @@ describe("ColumnSelector", () => {
|
|
|
291
319
|
const authorLabel = body.getByText("author").closest("label")!;
|
|
292
320
|
await user.click(authorLabel);
|
|
293
321
|
|
|
294
|
-
|
|
322
|
+
// The button should still show, indicating the state was updated
|
|
323
|
+
// The button is aria-hidden when dropdown is open, so we need to use hidden: true
|
|
324
|
+
expect(
|
|
325
|
+
screen.getByRole("button", { name: /カラム選択/, hidden: true }),
|
|
326
|
+
).toBeInTheDocument();
|
|
295
327
|
});
|
|
296
328
|
});
|
|
297
329
|
|
|
@@ -303,14 +335,22 @@ describe("ColumnSelector", () => {
|
|
|
303
335
|
{ name: "metadata", type: "nested", required: false },
|
|
304
336
|
];
|
|
305
337
|
|
|
338
|
+
const tableMetadataWithNested: TableMetadata = {
|
|
339
|
+
name: "TestTable",
|
|
340
|
+
pluralForm: "TestTables",
|
|
341
|
+
readAllowedRoles: [],
|
|
342
|
+
fields: fieldsWithNested,
|
|
343
|
+
relations: [],
|
|
344
|
+
};
|
|
345
|
+
|
|
306
346
|
render(
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
347
|
+
<TestWrapper
|
|
348
|
+
tableMetadata={tableMetadataWithNested}
|
|
349
|
+
metadata={{ TestTable: tableMetadataWithNested }}
|
|
350
|
+
initialSelectedFields={["id"]}
|
|
351
|
+
>
|
|
352
|
+
<ColumnSelector />
|
|
353
|
+
</TestWrapper>,
|
|
314
354
|
);
|
|
315
355
|
|
|
316
356
|
await user.click(screen.getByRole("button", { name: /カラム選択/ }));
|