@izumisy-tailor/tailor-data-viewer 0.1.42 → 0.1.44

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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.1.42",
4
+ "version": "0.1.44",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -71,13 +71,25 @@ describe("ColumnSelector", () => {
71
71
  expect(screen.getByRole("button")).toHaveTextContent("カラム選択");
72
72
  });
73
73
 
74
- it("選択数/総数が正しく表示される", () => {
74
+ it("選択数/総数が正しく表示される(columns定義がある場合はその数で表示)", () => {
75
75
  render(
76
76
  <TestWrapper columns={["id", "name"]}>
77
77
  <ColumnSelector />
78
78
  </TestWrapper>,
79
79
  );
80
80
 
81
+ // columns が ["id", "name"] なので、2/2 となる
82
+ expect(screen.getByRole("button")).toHaveTextContent("(2/2)");
83
+ });
84
+
85
+ it("ignoreColumns=true の場合、全フィールド数が表示される", () => {
86
+ render(
87
+ <TestWrapper columns={["id", "name"]}>
88
+ <ColumnSelector ignoreColumns />
89
+ </TestWrapper>,
90
+ );
91
+
92
+ // ignoreColumns=true なので、テーブルの全フィールド 4 つが対象
81
93
  expect(screen.getByRole("button")).toHaveTextContent("(2/4)");
82
94
  });
83
95
  });
@@ -100,7 +112,7 @@ describe("ColumnSelector", () => {
100
112
  });
101
113
  });
102
114
 
103
- it("フィールド一覧が表示される", async () => {
115
+ it("フィールド一覧がcolumns定義に基づいて表示される", async () => {
104
116
  const user = userEvent.setup();
105
117
  render(
106
118
  <TestWrapper columns={["id", "name"]}>
@@ -110,6 +122,27 @@ describe("ColumnSelector", () => {
110
122
 
111
123
  await user.click(screen.getByRole("button", { name: /カラム選択/ }));
112
124
 
125
+ await waitFor(() => {
126
+ const body = within(document.body);
127
+ // columns定義に含まれるフィールドのみ表示される
128
+ expect(body.getByText("id")).toBeInTheDocument();
129
+ expect(body.getByText("name")).toBeInTheDocument();
130
+ // columns定義に含まれないフィールドは表示されない
131
+ expect(body.queryByText("email")).not.toBeInTheDocument();
132
+ expect(body.queryByText("createdAt")).not.toBeInTheDocument();
133
+ });
134
+ });
135
+
136
+ it("ignoreColumns=true の場合、全フィールドが表示される", async () => {
137
+ const user = userEvent.setup();
138
+ render(
139
+ <TestWrapper columns={["id", "name"]}>
140
+ <ColumnSelector ignoreColumns />
141
+ </TestWrapper>,
142
+ );
143
+
144
+ await user.click(screen.getByRole("button", { name: /カラム選択/ }));
145
+
113
146
  await waitFor(() => {
114
147
  const body = within(document.body);
115
148
  expect(body.getByText("id")).toBeInTheDocument();
@@ -123,9 +156,10 @@ describe("ColumnSelector", () => {
123
156
  describe("フィールド選択", () => {
124
157
  it("フィールドクリックで選択が切り替わる", async () => {
125
158
  const user = userEvent.setup();
159
+ // ignoreColumns=true で全フィールドを表示してテスト
126
160
  render(
127
161
  <TestWrapper columns={["id"]}>
128
- <ColumnSelector />
162
+ <ColumnSelector ignoreColumns />
129
163
  </TestWrapper>,
130
164
  );
131
165
 
@@ -149,9 +183,10 @@ describe("ColumnSelector", () => {
149
183
 
150
184
  it("全選択ボタンで全フィールドが選択される", async () => {
151
185
  const user = userEvent.setup();
186
+ // ignoreColumns=true で全フィールドを表示してテスト
152
187
  render(
153
188
  <TestWrapper columns={[]}>
154
- <ColumnSelector />
189
+ <ColumnSelector ignoreColumns />
155
190
  </TestWrapper>,
156
191
  );
157
192
 
@@ -173,9 +208,10 @@ describe("ColumnSelector", () => {
173
208
 
174
209
  it("全解除ボタンで全フィールドが解除される", async () => {
175
210
  const user = userEvent.setup();
211
+ // ignoreColumns=true で全フィールドを表示してテスト
176
212
  render(
177
213
  <TestWrapper columns={["id", "name"]}>
178
- <ColumnSelector />
214
+ <ColumnSelector ignoreColumns />
179
215
  </TestWrapper>,
180
216
  );
181
217
 
@@ -251,7 +287,7 @@ describe("ColumnSelector", () => {
251
287
  relations: mockRelations,
252
288
  };
253
289
 
254
- it("リレーションセクションが表示される", async () => {
290
+ it("リレーションセクションが表示される(ignoreColumns=trueの場合)", async () => {
255
291
  const user = userEvent.setup();
256
292
  render(
257
293
  <TestWrapper
@@ -259,7 +295,7 @@ describe("ColumnSelector", () => {
259
295
  metadata={mockRelationTableMetadataMap}
260
296
  columns={["id"]}
261
297
  >
262
- <ColumnSelector />
298
+ <ColumnSelector ignoreColumns />
263
299
  </TestWrapper>,
264
300
  );
265
301
 
@@ -272,13 +308,13 @@ describe("ColumnSelector", () => {
272
308
  });
273
309
  });
274
310
 
275
- it("リレーションフィールドが表示される", async () => {
311
+ it("columnsにリレーションが含まれている場合、そのリレーションセクションが表示される", async () => {
276
312
  const user = userEvent.setup();
277
313
  render(
278
314
  <TestWrapper
279
315
  tableMetadata={mockTableMetadataWithRelations}
280
316
  metadata={mockRelationTableMetadataMap}
281
- columns={["id"]}
317
+ columns={["id", "author"]}
282
318
  >
283
319
  <ColumnSelector />
284
320
  </TestWrapper>,
@@ -286,6 +322,30 @@ describe("ColumnSelector", () => {
286
322
 
287
323
  await user.click(screen.getByRole("button", { name: /カラム選択/ }));
288
324
 
325
+ await waitFor(() => {
326
+ const body = within(document.body);
327
+ // author はリレーションなのでリレーションセクションが表示される
328
+ expect(body.getByText("リレーション (1対1)")).toBeInTheDocument();
329
+ expect(body.getByText("author")).toBeInTheDocument();
330
+ // comments は columns に含まれていないので表示されない
331
+ expect(body.queryByText("comments")).not.toBeInTheDocument();
332
+ });
333
+ });
334
+
335
+ it("リレーションフィールドが表示される(ignoreColumns=trueの場合)", async () => {
336
+ const user = userEvent.setup();
337
+ render(
338
+ <TestWrapper
339
+ tableMetadata={mockTableMetadataWithRelations}
340
+ metadata={mockRelationTableMetadataMap}
341
+ columns={["id"]}
342
+ >
343
+ <ColumnSelector ignoreColumns />
344
+ </TestWrapper>,
345
+ );
346
+
347
+ await user.click(screen.getByRole("button", { name: /カラム選択/ }));
348
+
289
349
  await waitFor(() => {
290
350
  const body = within(document.body);
291
351
  expect(body.getByText("author")).toBeInTheDocument();
@@ -301,7 +361,7 @@ describe("ColumnSelector", () => {
301
361
  metadata={mockRelationTableMetadataMap}
302
362
  columns={["id"]}
303
363
  >
304
- <ColumnSelector />
364
+ <ColumnSelector ignoreColumns />
305
365
  </TestWrapper>,
306
366
  );
307
367
 
@@ -1,3 +1,4 @@
1
+ import { useMemo } from "react";
1
2
  import { Columns3 } from "lucide-react";
2
3
  import { Checkbox } from "./ui/checkbox";
3
4
  import { Button } from "./ui/button";
@@ -15,14 +16,26 @@ import { useDataViewer } from "./contexts";
15
16
  import { useToolbar } from "./contexts";
16
17
  import { useLabels } from "./hooks/use-labels";
17
18
 
19
+ /**
20
+ * Props for ColumnSelector component
21
+ */
22
+ export interface ColumnSelectorProps {
23
+ /**
24
+ * If true, ignore the columns prop definition and show all fields in the table.
25
+ * If false (default), only show fields that are defined in the columns prop.
26
+ */
27
+ ignoreColumns?: boolean;
28
+ }
29
+
18
30
  /**
19
31
  * Column visibility selector with checkboxes
20
32
  * Must be used within DataViewer.Root and DataViewer.Toolbar context
21
33
  */
22
- export function ColumnSelector() {
34
+ export function ColumnSelector({ ignoreColumns = false }: ColumnSelectorProps) {
23
35
  const {
24
36
  tableMetadata,
25
37
  metadata,
38
+ columnDefinitions,
26
39
  selectedFields,
27
40
  toggleField,
28
41
  selectAllFields,
@@ -43,27 +56,76 @@ export function ColumnSelector() {
43
56
  const onOpenChange = (isOpen: boolean) =>
44
57
  setActivePanel(isOpen ? "column" : null);
45
58
 
59
+ // Get allowed field names from column definitions
60
+ const allowedFieldNames = useMemo(() => {
61
+ if (ignoreColumns || !columnDefinitions) {
62
+ return null; // null means all fields are allowed
63
+ }
64
+ return new Set(columnDefinitions.map((col) => col.baseField));
65
+ }, [ignoreColumns, columnDefinitions]);
66
+
46
67
  // Filter out nested fields as they're not directly selectable
47
- const selectableFields = fields.filter((field) => field.type !== "nested");
68
+ // Also filter by allowedFieldNames if columns are defined
69
+ const selectableFields = fields.filter(
70
+ (field) =>
71
+ field.type !== "nested" &&
72
+ (allowedFieldNames === null || allowedFieldNames.has(field.name)),
73
+ );
48
74
 
49
- // Separate manyToOne and oneToMany relations
75
+ // Separate manyToOne and oneToMany relations, filtered by allowedFieldNames
50
76
  const manyToOneRelations = relations.filter(
51
- (r) => r.relationType === "manyToOne",
77
+ (r) =>
78
+ r.relationType === "manyToOne" &&
79
+ (allowedFieldNames === null || allowedFieldNames.has(r.fieldName)),
52
80
  );
53
81
  const oneToManyRelations = relations.filter(
54
- (r) => r.relationType === "oneToMany",
82
+ (r) =>
83
+ r.relationType === "oneToMany" &&
84
+ (allowedFieldNames === null || allowedFieldNames.has(r.fieldName)),
55
85
  );
56
86
 
57
- // Count expanded relation fields
58
- const expandedFieldCount = Object.values(expandedRelationFields).reduce(
59
- (sum, fields) => sum + fields.length,
60
- 0,
61
- );
87
+ // Count expanded relation fields (only for allowed relations)
88
+ const filteredExpandedRelationFields = useMemo(() => {
89
+ if (allowedFieldNames === null) {
90
+ return expandedRelationFields;
91
+ }
92
+ const filtered: typeof expandedRelationFields = {};
93
+ for (const [key, value] of Object.entries(expandedRelationFields)) {
94
+ if (allowedFieldNames.has(key)) {
95
+ filtered[key] = value;
96
+ }
97
+ }
98
+ return filtered;
99
+ }, [expandedRelationFields, allowedFieldNames]);
100
+
101
+ const expandedFieldCount = Object.values(
102
+ filteredExpandedRelationFields,
103
+ ).reduce((sum, fields) => sum + fields.length, 0);
104
+
105
+ const filteredRelationsCount =
106
+ manyToOneRelations.length + oneToManyRelations.length;
107
+
108
+ // Filter selected fields and relations to only count those in the allowed list
109
+ const filteredSelectedFields = useMemo(() => {
110
+ if (allowedFieldNames === null) {
111
+ return selectedFields;
112
+ }
113
+ return selectedFields.filter((f) => allowedFieldNames.has(f));
114
+ }, [selectedFields, allowedFieldNames]);
115
+
116
+ const filteredSelectedRelations = useMemo(() => {
117
+ if (allowedFieldNames === null) {
118
+ return selectedRelations;
119
+ }
120
+ return selectedRelations.filter((r) => allowedFieldNames.has(r));
121
+ }, [selectedRelations, allowedFieldNames]);
62
122
 
63
123
  const totalSelectable =
64
- selectableFields.length + relations.length + expandedFieldCount;
124
+ selectableFields.length + filteredRelationsCount + expandedFieldCount;
65
125
  const totalSelected =
66
- selectedFields.length + selectedRelations.length + expandedFieldCount;
126
+ filteredSelectedFields.length +
127
+ filteredSelectedRelations.length +
128
+ expandedFieldCount;
67
129
 
68
130
  return (
69
131
  <DropdownMenu open={open} onOpenChange={onOpenChange}>
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
  import type { TableMetadataMap } from "../generator/metadata-generator";
3
3
  import type { GraphQLFetcher } from "../graphql/fetcher";
4
4
  import type { SavedViewStore, SavedViewStoreFactory } from "../store/types";
5
- import type { Renderers, Labels, UntypedColumnDef } from "./types";
5
+ import type { Renderers, Labels, ColumnDef } from "./types";
6
6
  import {
7
7
  DataViewerProvider,
8
8
  type DataViewerInitialData,
@@ -47,7 +47,7 @@ export interface DataViewerTableDataProviderProps<
47
47
  * If not specified, all fields will be selected by default.
48
48
  * The order of columns determines the display order.
49
49
  */
50
- columns?: UntypedColumnDef[];
50
+ columns?: ColumnDef<TMetadata, TTableName>[];
51
51
  /** Initial data for filters and sort (type-safe when metadata is `as const`) */
52
52
  initialData?: DataViewerInitialData<TMetadata, TTableName>;
53
53
  }
@@ -38,6 +38,7 @@ export type { ToolbarContextValue, ToolbarActivePanel } from "./contexts";
38
38
 
39
39
  // Individual UI components (must be used within DataViewer.TableDataProvider)
40
40
  export { ColumnSelector } from "./column-selector";
41
+ export type { ColumnSelectorProps } from "./column-selector";
41
42
  export { SearchFilterForm } from "./search-filter";
42
43
  export type { SearchFilterFormProps } from "./search-filter";
43
44
  export { VariationFilterForm } from "./variation-filter";
@@ -641,10 +641,10 @@ describe("SearchFilterForm", () => {
641
641
  });
642
642
  });
643
643
 
644
- describe("selectedFieldsによるフィルタリング", () => {
644
+ describe("columnsによるフィルタリング", () => {
645
645
  /**
646
646
  * AllFieldsWrapper: columnsをすべてのフィルター可能なフィールドに設定する
647
- * これにより、ignoreSelectedFieldsの効果をテストできる
647
+ * これにより、ignoreColumnsの効果をテストできる
648
648
  */
649
649
  function AllFieldsWrapper({ children, initialFilters = [] }: WrapperProps) {
650
650
  return (
@@ -671,7 +671,7 @@ describe("SearchFilterForm", () => {
671
671
  );
672
672
  }
673
673
 
674
- it("デフォルトではselectedFieldsに含まれるフィールドのみがフィルター対象", async () => {
674
+ it("デフォルトではcolumns定義に含まれるフィールドのみがフィルター対象", async () => {
675
675
  // TestWrapperはcolumns: ["id", "name"]を設定
676
676
  // この場合、emailやageは選択可能フィールドに表示されないはず
677
677
  const user = userEvent.setup();
@@ -689,17 +689,17 @@ describe("SearchFilterForm", () => {
689
689
  expect(body.getByText("フィルターを追加")).toBeInTheDocument();
690
690
  });
691
691
 
692
- // フィールドセレクトが存在することを確認(デフォルトのselectedFieldsにはidとnameのみ)
692
+ // フィールドセレクトが存在することを確認(デフォルトのcolumnsにはidとnameのみ)
693
693
  const selectTrigger = screen.getByRole("combobox");
694
694
  expect(selectTrigger).toBeInTheDocument();
695
695
  });
696
696
 
697
- it("ignoreSelectedFields=trueの場合、selectedFieldsに関係なく全フィルター可能フィールドが表示", async () => {
698
- // 制限されたselectedFieldsでもignoreSelectedFields=trueなら全フィールド表示
697
+ it("ignoreColumns=trueの場合、columns定義に関係なく全フィルター可能フィールドが表示", async () => {
698
+ // 制限されたcolumnsでもignoreColumns=trueなら全フィールド表示
699
699
  const user = userEvent.setup();
700
700
  render(
701
701
  <TestWrapper>
702
- <SearchFilterForm ignoreSelectedFields />
702
+ <SearchFilterForm ignoreColumns />
703
703
  </TestWrapper>,
704
704
  );
705
705
 
@@ -715,7 +715,7 @@ describe("SearchFilterForm", () => {
715
715
  expect(selectTrigger).toBeInTheDocument();
716
716
  });
717
717
 
718
- it("selectedFieldsが全フィールドの場合、ignoreSelectedFieldsなしでも全フィールドが表示される", async () => {
718
+ it("columnsが全フィールドの場合、ignoreColumnsなしでも全フィールドが表示される", async () => {
719
719
  const user = userEvent.setup();
720
720
  render(
721
721
  <AllFieldsWrapper>
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useEffect } from "react";
1
+ import { useState, useCallback, useEffect, useMemo } from "react";
2
2
  import { Search, Plus, X, Filter } from "lucide-react";
3
3
  import { Button } from "./ui/button";
4
4
  import { Input } from "./ui/input";
@@ -47,10 +47,10 @@ function isFilterableField(field: FieldMetadata): boolean {
47
47
  */
48
48
  export interface SearchFilterFormProps {
49
49
  /**
50
- * If true, ignore selectedFields and show all filterable fields.
51
- * If false (default), only show fields that are in selectedFields.
50
+ * If true, ignore the columns prop definition and show all filterable fields in the table.
51
+ * If false (default), only show fields that are defined in the columns prop.
52
52
  */
53
- ignoreSelectedFields?: boolean;
53
+ ignoreColumns?: boolean;
54
54
  }
55
55
 
56
56
  /**
@@ -59,9 +59,9 @@ export interface SearchFilterFormProps {
59
59
  * Must be used within DataViewer.Root and DataViewer.Toolbar context
60
60
  */
61
61
  export function SearchFilterForm({
62
- ignoreSelectedFields = false,
62
+ ignoreColumns = false,
63
63
  }: SearchFilterFormProps = {}) {
64
- const { tableMetadata, filters, setFilters, selectedFields } =
64
+ const { tableMetadata, filters, setFilters, columnDefinitions } =
65
65
  useDataViewer();
66
66
  const { activePanel, setActivePanel } = useToolbar();
67
67
  const { getLabel } = useLabels();
@@ -80,13 +80,21 @@ export function SearchFilterForm({
80
80
  const [inputValue, setInputValue] = useState<string>("");
81
81
  const [booleanValue, setBooleanValue] = useState<boolean>(false);
82
82
 
83
+ // Get allowed field names from column definitions
84
+ const allowedFieldNames = useMemo(() => {
85
+ if (ignoreColumns || !columnDefinitions) {
86
+ return null; // null means all fields are allowed
87
+ }
88
+ return new Set(columnDefinitions.map((col) => col.baseField));
89
+ }, [ignoreColumns, columnDefinitions]);
90
+
83
91
  // Get filterable fields excluding already-filtered fields
84
- // If ignoreSelectedFields is false, also filter to only include selected fields
92
+ // If ignoreColumns is false and columnDefinitions exist, only include fields from column definitions
85
93
  const filterableFields = fields.filter(
86
94
  (f) =>
87
95
  isFilterableField(f) &&
88
96
  !searchFilters.some((filter) => filter.field === f.name) &&
89
- (ignoreSelectedFields || selectedFields.includes(f.name)),
97
+ (allowedFieldNames === null || allowedFieldNames.has(f.name)),
90
98
  );
91
99
 
92
100
  const selectedFieldMetadata = fields.find((f) => f.name === selectedField);