@izumisy-tailor/tailor-data-viewer 0.1.20 → 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.
@@ -2,7 +2,18 @@ import { describe, it, expect, vi } 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 { DataTableToolbar } from "./data-table-toolbar";
5
- import type { FieldMetadata } from "../generator/metadata-generator";
5
+ import { ColumnSelector } from "./column-selector";
6
+ import { SearchFilterForm } from "./search-filter";
7
+ import { CsvButton } from "./csv-button";
8
+ import { RefreshButton } from "./refresh-button";
9
+ import { DataViewerProvider } from "./contexts";
10
+ import { TableDataProvider } from "./contexts";
11
+ import type {
12
+ FieldMetadata,
13
+ TableMetadata,
14
+ TableMetadataMap,
15
+ } from "../generator/metadata-generator";
16
+ import type { ReactNode } from "react";
6
17
 
7
18
  const mockFields: FieldMetadata[] = [
8
19
  { name: "id", type: "uuid", required: true, description: "ID" },
@@ -11,45 +22,96 @@ const mockFields: FieldMetadata[] = [
11
22
  { name: "age", type: "number", required: false, description: "年齢" },
12
23
  ];
13
24
 
14
- const createDefaultProps = () => ({
15
- columnSelector: {
16
- fields: mockFields,
17
- selectedFields: ["id", "name"],
18
- onToggle: vi.fn(),
19
- onSelectAll: vi.fn(),
20
- onDeselectAll: vi.fn(),
21
- },
22
- searchFilter: {
23
- fields: mockFields,
24
- filters: [],
25
- onFiltersChange: vi.fn(),
26
- },
27
- });
25
+ const mockTableMetadata: TableMetadata = {
26
+ name: "TestTable",
27
+ pluralForm: "TestTables",
28
+ readAllowedRoles: [],
29
+ fields: mockFields,
30
+ relations: [],
31
+ };
32
+
33
+ const mockTableMetadataMap: TableMetadataMap = {
34
+ TestTable: mockTableMetadata,
35
+ };
36
+
37
+ interface WrapperProps {
38
+ children: ReactNode;
39
+ initialSelectedFields?: string[];
40
+ }
41
+
42
+ // Mock the GraphQL client to prevent actual API calls
43
+ vi.mock("../graphql/graphql-client", () => ({
44
+ createGraphQLClient: vi.fn(() => ({
45
+ fetch: vi.fn(() =>
46
+ Promise.resolve({
47
+ data: {
48
+ testTables: { collection: [], pageInfo: { hasNextPage: false } },
49
+ },
50
+ }),
51
+ ),
52
+ })),
53
+ }));
54
+
55
+ function TestWrapper({
56
+ children,
57
+ initialSelectedFields = ["id", "name"],
58
+ }: WrapperProps) {
59
+ return (
60
+ <DataViewerProvider
61
+ appUri="https://example.com"
62
+ tableName={mockTableMetadata.name}
63
+ metadata={mockTableMetadataMap}
64
+ initialData={{ selectedFields: initialSelectedFields }}
65
+ >
66
+ <TableDataProvider>{children}</TableDataProvider>
67
+ </DataViewerProvider>
68
+ );
69
+ }
28
70
 
29
71
  describe("DataTableToolbar", () => {
30
72
  describe("基本的な表示", () => {
31
73
  it("カラム選択ボタンが表示される", () => {
32
- render(<DataTableToolbar {...createDefaultProps()} />);
74
+ render(
75
+ <TestWrapper>
76
+ <DataTableToolbar>
77
+ <ColumnSelector />
78
+ </DataTableToolbar>
79
+ </TestWrapper>,
80
+ );
33
81
  expect(
34
82
  screen.getByRole("button", { name: /カラム選択/ }),
35
83
  ).toBeInTheDocument();
36
84
  });
37
85
 
38
86
  it("検索ボタンが表示される", () => {
39
- render(<DataTableToolbar {...createDefaultProps()} />);
87
+ render(
88
+ <TestWrapper>
89
+ <DataTableToolbar>
90
+ <SearchFilterForm />
91
+ </DataTableToolbar>
92
+ </TestWrapper>,
93
+ );
40
94
  expect(screen.getByRole("button", { name: /検索/ })).toBeInTheDocument();
41
95
  });
42
96
 
43
- it("CSV ボタンが表示される (onDownloadCsv が渡された場合)", () => {
97
+ it("CSV ボタンが表示される", () => {
44
98
  render(
45
- <DataTableToolbar {...createDefaultProps()} onDownloadCsv={vi.fn()} />,
99
+ <TestWrapper>
100
+ <DataTableToolbar>
101
+ <CsvButton />
102
+ </DataTableToolbar>
103
+ </TestWrapper>,
46
104
  );
47
105
  expect(screen.getByRole("button", { name: /CSV/ })).toBeInTheDocument();
48
106
  });
49
107
 
50
- it("更新ボタンが表示される (onRefresh が渡された場合)", () => {
108
+ it("更新ボタンが表示される", () => {
51
109
  render(
52
- <DataTableToolbar {...createDefaultProps()} onRefresh={vi.fn()} />,
110
+ <TestWrapper>
111
+ <DataTableToolbar>
112
+ <RefreshButton />
113
+ </DataTableToolbar>
114
+ </TestWrapper>,
53
115
  );
54
116
  expect(screen.getByRole("button", { name: /更新/ })).toBeInTheDocument();
55
117
  });
@@ -58,7 +120,14 @@ describe("DataTableToolbar", () => {
58
120
  describe("パネルの排他制御", () => {
59
121
  it("カラム選択パネルを開くと検索パネルは開かない", async () => {
60
122
  const user = userEvent.setup();
61
- render(<DataTableToolbar {...createDefaultProps()} />);
123
+ render(
124
+ <TestWrapper>
125
+ <DataTableToolbar>
126
+ <ColumnSelector />
127
+ <SearchFilterForm />
128
+ </DataTableToolbar>
129
+ </TestWrapper>,
130
+ );
62
131
 
63
132
  // カラム選択パネルを開く
64
133
  await user.click(screen.getByRole("button", { name: /カラム選択/ }));
@@ -76,7 +145,14 @@ describe("DataTableToolbar", () => {
76
145
 
77
146
  it("検索パネルを開くとカラム選択パネルは開かない", async () => {
78
147
  const user = userEvent.setup();
79
- render(<DataTableToolbar {...createDefaultProps()} />);
148
+ render(
149
+ <TestWrapper>
150
+ <DataTableToolbar>
151
+ <ColumnSelector />
152
+ <SearchFilterForm />
153
+ </DataTableToolbar>
154
+ </TestWrapper>,
155
+ );
80
156
 
81
157
  // 検索パネルを開く
82
158
  await user.click(screen.getByRole("button", { name: /検索/ }));
@@ -94,7 +170,14 @@ describe("DataTableToolbar", () => {
94
170
 
95
171
  it("カラム選択パネルが開いている状態で検索パネルを開くとカラム選択パネルが閉じる", async () => {
96
172
  const user = userEvent.setup({ pointerEventsCheck: 0 });
97
- render(<DataTableToolbar {...createDefaultProps()} />);
173
+ render(
174
+ <TestWrapper>
175
+ <DataTableToolbar>
176
+ <ColumnSelector />
177
+ <SearchFilterForm />
178
+ </DataTableToolbar>
179
+ </TestWrapper>,
180
+ );
98
181
 
99
182
  // カラム選択パネルを開く
100
183
  await user.click(screen.getByRole("button", { name: /カラム選択/ }));
@@ -128,7 +211,14 @@ describe("DataTableToolbar", () => {
128
211
 
129
212
  it("検索パネルが開いている状態でカラム選択パネルを開くと検索パネルが閉じる", async () => {
130
213
  const user = userEvent.setup({ pointerEventsCheck: 0 });
131
- render(<DataTableToolbar {...createDefaultProps()} />);
214
+ render(
215
+ <TestWrapper>
216
+ <DataTableToolbar>
217
+ <ColumnSelector />
218
+ <SearchFilterForm />
219
+ </DataTableToolbar>
220
+ </TestWrapper>,
221
+ );
132
222
 
133
223
  // 検索パネルを開く
134
224
  await user.click(screen.getByRole("button", { name: /検索/ }));
@@ -161,56 +251,21 @@ describe("DataTableToolbar", () => {
161
251
  });
162
252
  });
163
253
 
164
- describe("アクションボタン", () => {
165
- it("CSV ボタンクリックで onDownloadCsv が呼ばれる", async () => {
166
- const user = userEvent.setup();
167
- const onDownloadCsv = vi.fn();
254
+ describe("Compositional API", () => {
255
+ it("children で任意のコンポーネントを配置できる", () => {
168
256
  render(
169
- <DataTableToolbar
170
- {...createDefaultProps()}
171
- onDownloadCsv={onDownloadCsv}
172
- />,
257
+ <TestWrapper>
258
+ <DataTableToolbar>
259
+ <button data-testid="custom-button">Custom Button</button>
260
+ <ColumnSelector />
261
+ </DataTableToolbar>
262
+ </TestWrapper>,
173
263
  );
174
264
 
175
- await user.click(screen.getByRole("button", { name: /CSV/ }));
176
- expect(onDownloadCsv).toHaveBeenCalled();
177
- });
178
-
179
- it("更新ボタンクリックで onRefresh が呼ばれる", async () => {
180
- const user = userEvent.setup();
181
- const onRefresh = vi.fn();
182
- render(
183
- <DataTableToolbar {...createDefaultProps()} onRefresh={onRefresh} />,
184
- );
185
-
186
- await user.click(screen.getByRole("button", { name: /更新/ }));
187
- expect(onRefresh).toHaveBeenCalled();
188
- });
189
-
190
- it("loading 中は CSV と更新ボタンが無効になる", () => {
191
- render(
192
- <DataTableToolbar
193
- {...createDefaultProps()}
194
- onDownloadCsv={vi.fn()}
195
- onRefresh={vi.fn()}
196
- loading={true}
197
- />,
198
- );
199
-
200
- expect(screen.getByRole("button", { name: /CSV/ })).toBeDisabled();
201
- expect(screen.getByRole("button", { name: /更新/ })).toBeDisabled();
202
- });
203
-
204
- it("csvDisabled が true の場合、CSV ボタンが無効になる", () => {
205
- render(
206
- <DataTableToolbar
207
- {...createDefaultProps()}
208
- onDownloadCsv={vi.fn()}
209
- csvDisabled={true}
210
- />,
211
- );
212
-
213
- expect(screen.getByRole("button", { name: /CSV/ })).toBeDisabled();
265
+ expect(screen.getByTestId("custom-button")).toBeInTheDocument();
266
+ expect(
267
+ screen.getByRole("button", { name: /カラム選択/ }),
268
+ ).toBeInTheDocument();
214
269
  });
215
270
  });
216
271
  });
@@ -1,165 +1,28 @@
1
- import { useState, useCallback } from "react";
2
- import { RefreshCw, Download } from "lucide-react";
3
- import { Button } from "./ui/button";
4
- import type {
5
- FieldMetadata,
6
- RelationMetadata,
7
- TableMetadataMap,
8
- ExpandedRelationFields,
9
- } from "../generator/metadata-generator";
10
- import { ColumnSelector } from "./column-selector";
11
- import { SearchFilterForm } from "./search-filter";
12
- import { ViewSave } from "./view-save-load";
13
- import type { SearchFilters } from "./types";
1
+ import { type ReactNode } from "react";
2
+ import { ToolbarProvider } from "./contexts";
14
3
 
15
4
  /**
16
5
  * Active panel type - only one panel can be open at a time
6
+ * @deprecated Use ToolbarContext instead
17
7
  */
18
8
  export type ActivePanel = "column" | "search" | null;
19
9
 
20
- interface ColumnSelectorConfig {
21
- fields: FieldMetadata[];
22
- selectedFields: string[];
23
- onToggle: (fieldName: string) => void;
24
- onSelectAll: () => void;
25
- onDeselectAll: () => void;
26
- relations?: RelationMetadata[];
27
- selectedRelations?: string[];
28
- onToggleRelation?: (fieldName: string) => void;
29
- tableMetadataMap?: TableMetadataMap;
30
- expandedRelationFields?: ExpandedRelationFields;
31
- onToggleExpandedRelationField?: (
32
- relationFieldName: string,
33
- fieldName: string,
34
- ) => void;
35
- isExpandedRelationFieldSelected?: (
36
- relationFieldName: string,
37
- fieldName: string,
38
- ) => boolean;
39
- }
40
-
41
- interface SearchFilterConfig {
42
- fields: FieldMetadata[];
43
- filters: SearchFilters;
44
- onFiltersChange: (filters: SearchFilters) => void;
45
- }
46
-
47
- interface ViewSaveConfig {
48
- tableName: string;
49
- filters: SearchFilters;
50
- selectedFields: string[];
51
- selectedRelations: string[];
52
- expandedRelationFields: ExpandedRelationFields;
53
- }
54
-
55
- interface DataTableToolbarProps {
56
- /** Column selector configuration */
57
- columnSelector: ColumnSelectorConfig;
58
- /** Search filter configuration */
59
- searchFilter: SearchFilterConfig;
60
- /** View save configuration (optional - hidden if not provided) */
61
- viewSave?: ViewSaveConfig;
62
- /** CSV download handler */
63
- onDownloadCsv?: () => void;
64
- /** Refresh handler */
65
- onRefresh?: () => void;
66
- /** Whether the table is loading */
67
- loading?: boolean;
68
- /** Whether CSV download is disabled */
69
- csvDisabled?: boolean;
10
+ export interface DataTableToolbarProps {
11
+ /** Children components (uses ToolbarContext) */
12
+ children: ReactNode;
70
13
  }
71
14
 
72
15
  /**
73
16
  * Data table toolbar component
74
- * Combines column selector, search filter, view save, and action buttons
75
- * Ensures only one panel can be open at a time
17
+ *
18
+ * Provides ToolbarContext for exclusive panel management.
19
+ * Place ColumnSelector, SearchFilterForm, etc. as children.
20
+ * Must be used within DataViewer.Root context.
76
21
  */
77
- export function DataTableToolbar({
78
- columnSelector,
79
- searchFilter,
80
- viewSave,
81
- onDownloadCsv,
82
- onRefresh,
83
- loading = false,
84
- csvDisabled = false,
85
- }: DataTableToolbarProps) {
86
- // Active panel state - only one panel can be open at a time
87
- const [activePanel, setActivePanel] = useState<ActivePanel>(null);
88
-
89
- const handleColumnPanelChange = useCallback((open: boolean) => {
90
- setActivePanel(open ? "column" : null);
91
- }, []);
92
-
93
- const handleSearchPanelChange = useCallback((open: boolean) => {
94
- setActivePanel(open ? "search" : null);
95
- }, []);
96
-
22
+ export function DataTableToolbar({ children }: DataTableToolbarProps) {
97
23
  return (
98
- <div className="flex items-center gap-4">
99
- <ColumnSelector
100
- fields={columnSelector.fields}
101
- selectedFields={columnSelector.selectedFields}
102
- onToggle={columnSelector.onToggle}
103
- onSelectAll={columnSelector.onSelectAll}
104
- onDeselectAll={columnSelector.onDeselectAll}
105
- relations={columnSelector.relations}
106
- selectedRelations={columnSelector.selectedRelations}
107
- onToggleRelation={columnSelector.onToggleRelation}
108
- tableMetadataMap={columnSelector.tableMetadataMap}
109
- expandedRelationFields={columnSelector.expandedRelationFields}
110
- onToggleExpandedRelationField={
111
- columnSelector.onToggleExpandedRelationField
112
- }
113
- isExpandedRelationFieldSelected={
114
- columnSelector.isExpandedRelationFieldSelected
115
- }
116
- open={activePanel === "column"}
117
- onOpenChange={handleColumnPanelChange}
118
- />
119
-
120
- <SearchFilterForm
121
- fields={searchFilter.fields}
122
- filters={searchFilter.filters}
123
- onFiltersChange={searchFilter.onFiltersChange}
124
- open={activePanel === "search"}
125
- onOpenChange={handleSearchPanelChange}
126
- />
127
-
128
- {viewSave && (
129
- <ViewSave
130
- tableName={viewSave.tableName}
131
- filters={viewSave.filters}
132
- selectedFields={viewSave.selectedFields}
133
- selectedRelations={viewSave.selectedRelations}
134
- expandedRelationFields={viewSave.expandedRelationFields}
135
- />
136
- )}
137
-
138
- <div className="flex-1" />
139
-
140
- {onDownloadCsv && (
141
- <Button
142
- variant="outline"
143
- size="sm"
144
- onClick={onDownloadCsv}
145
- disabled={loading || csvDisabled}
146
- >
147
- <Download className="size-4" />
148
- CSV
149
- </Button>
150
- )}
151
-
152
- {onRefresh && (
153
- <Button
154
- variant="outline"
155
- size="sm"
156
- onClick={onRefresh}
157
- disabled={loading}
158
- >
159
- <RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />
160
- 更新
161
- </Button>
162
- )}
163
- </div>
24
+ <ToolbarProvider>
25
+ <div className="flex items-center gap-4">{children}</div>
26
+ </ToolbarProvider>
164
27
  );
165
28
  }