@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 +1 -1
- package/src/component/column-selector.test.tsx +70 -10
- package/src/component/column-selector.tsx +74 -12
- package/src/component/create-data-viewer.tsx +2 -2
- package/src/component/index.ts +1 -0
- package/src/component/search-filter.test.tsx +8 -8
- package/src/component/search-filter.tsx +16 -8
package/package.json
CHANGED
|
@@ -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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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) =>
|
|
77
|
+
(r) =>
|
|
78
|
+
r.relationType === "manyToOne" &&
|
|
79
|
+
(allowedFieldNames === null || allowedFieldNames.has(r.fieldName)),
|
|
52
80
|
);
|
|
53
81
|
const oneToManyRelations = relations.filter(
|
|
54
|
-
(r) =>
|
|
82
|
+
(r) =>
|
|
83
|
+
r.relationType === "oneToMany" &&
|
|
84
|
+
(allowedFieldNames === null || allowedFieldNames.has(r.fieldName)),
|
|
55
85
|
);
|
|
56
86
|
|
|
57
|
-
// Count expanded relation fields
|
|
58
|
-
const
|
|
59
|
-
(
|
|
60
|
-
|
|
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 +
|
|
124
|
+
selectableFields.length + filteredRelationsCount + expandedFieldCount;
|
|
65
125
|
const totalSelected =
|
|
66
|
-
|
|
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,
|
|
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?:
|
|
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
|
}
|
package/src/component/index.ts
CHANGED
|
@@ -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("
|
|
644
|
+
describe("columnsによるフィルタリング", () => {
|
|
645
645
|
/**
|
|
646
646
|
* AllFieldsWrapper: columnsをすべてのフィルター可能なフィールドに設定する
|
|
647
|
-
* これにより、
|
|
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("デフォルトでは
|
|
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
|
-
// フィールドセレクトが存在することを確認(デフォルトの
|
|
692
|
+
// フィールドセレクトが存在することを確認(デフォルトのcolumnsにはidとnameのみ)
|
|
693
693
|
const selectTrigger = screen.getByRole("combobox");
|
|
694
694
|
expect(selectTrigger).toBeInTheDocument();
|
|
695
695
|
});
|
|
696
696
|
|
|
697
|
-
it("
|
|
698
|
-
// 制限された
|
|
697
|
+
it("ignoreColumns=trueの場合、columns定義に関係なく全フィルター可能フィールドが表示", async () => {
|
|
698
|
+
// 制限されたcolumnsでもignoreColumns=trueなら全フィールド表示
|
|
699
699
|
const user = userEvent.setup();
|
|
700
700
|
render(
|
|
701
701
|
<TestWrapper>
|
|
702
|
-
<SearchFilterForm
|
|
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("
|
|
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
|
|
51
|
-
* If false (default), only show fields that are in
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
ignoreColumns = false,
|
|
63
63
|
}: SearchFilterFormProps = {}) {
|
|
64
|
-
const { tableMetadata, filters, setFilters,
|
|
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
|
|
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
|
-
(
|
|
97
|
+
(allowedFieldNames === null || allowedFieldNames.has(f.name)),
|
|
90
98
|
);
|
|
91
99
|
|
|
92
100
|
const selectedFieldMetadata = fields.find((f) => f.name === selectedField);
|