@izumisy-tailor/tailor-data-viewer 0.2.6 → 0.2.7
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/collection-params/use-collection-params.test.ts +0 -79
- package/src/component/collection-params/use-collection-params.ts +7 -23
- package/src/component/index.ts +1 -0
- package/src/component/search-filter-form.tsx +3 -1
- package/src/component/types.ts +87 -26
package/package.json
CHANGED
|
@@ -44,7 +44,6 @@ describe("useCollectionParams", () => {
|
|
|
44
44
|
initialFilters: [
|
|
45
45
|
{
|
|
46
46
|
field: "status",
|
|
47
|
-
fieldType: "enum",
|
|
48
47
|
operator: "eq",
|
|
49
48
|
value: "ACTIVE",
|
|
50
49
|
},
|
|
@@ -101,13 +100,11 @@ describe("useCollectionParams", () => {
|
|
|
101
100
|
result.current.setFilters([
|
|
102
101
|
{
|
|
103
102
|
field: "status",
|
|
104
|
-
fieldType: "enum",
|
|
105
103
|
operator: "eq",
|
|
106
104
|
value: "ACTIVE",
|
|
107
105
|
},
|
|
108
106
|
{
|
|
109
107
|
field: "amount",
|
|
110
|
-
fieldType: "number",
|
|
111
108
|
operator: "gte",
|
|
112
109
|
value: 1000,
|
|
113
110
|
},
|
|
@@ -308,7 +305,6 @@ describe("useCollectionParams", () => {
|
|
|
308
305
|
initialFilters: [
|
|
309
306
|
{
|
|
310
307
|
field: "status",
|
|
311
|
-
fieldType: "enum",
|
|
312
308
|
operator: "eq",
|
|
313
309
|
value: "ACTIVE",
|
|
314
310
|
},
|
|
@@ -374,81 +370,6 @@ describe("useCollectionParams", () => {
|
|
|
374
370
|
expect(result.current.variables).toEqual({ first: 10 });
|
|
375
371
|
});
|
|
376
372
|
|
|
377
|
-
it("auto-detects fieldType from metadata for string field", () => {
|
|
378
|
-
const { result } = renderHook(() =>
|
|
379
|
-
useCollectionParams({
|
|
380
|
-
metadata: testMetadata,
|
|
381
|
-
tableName: "task",
|
|
382
|
-
}),
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
act(() => {
|
|
386
|
-
result.current.addFilter("title", "foo");
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
expect(result.current.filters[0].fieldType).toBe("string");
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("auto-detects fieldType from metadata for enum field", () => {
|
|
393
|
-
const { result } = renderHook(() =>
|
|
394
|
-
useCollectionParams({
|
|
395
|
-
metadata: testMetadata,
|
|
396
|
-
tableName: "task",
|
|
397
|
-
}),
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
act(() => {
|
|
401
|
-
result.current.addFilter("status", "todo");
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
expect(result.current.filters[0].fieldType).toBe("enum");
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it("auto-detects fieldType from metadata for date field", () => {
|
|
408
|
-
const { result } = renderHook(() =>
|
|
409
|
-
useCollectionParams({
|
|
410
|
-
metadata: testMetadata,
|
|
411
|
-
tableName: "task",
|
|
412
|
-
}),
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
act(() => {
|
|
416
|
-
result.current.addFilter("dueDate", "2026-01-01");
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
expect(result.current.filters[0].fieldType).toBe("date");
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it("auto-detects fieldType from metadata for uuid field", () => {
|
|
423
|
-
const { result } = renderHook(() =>
|
|
424
|
-
useCollectionParams({
|
|
425
|
-
metadata: testMetadata,
|
|
426
|
-
tableName: "task",
|
|
427
|
-
}),
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
act(() => {
|
|
431
|
-
result.current.addFilter("id", "some-uuid");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
expect(result.current.filters[0].fieldType).toBe("uuid");
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("auto-detects fieldType from metadata for number field", () => {
|
|
438
|
-
const { result } = renderHook(() =>
|
|
439
|
-
useCollectionParams({
|
|
440
|
-
metadata: testMetadata,
|
|
441
|
-
tableName: "task",
|
|
442
|
-
}),
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
act(() => {
|
|
446
|
-
result.current.addFilter("count", 42);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
expect(result.current.filters[0].fieldType).toBe("number");
|
|
450
|
-
});
|
|
451
|
-
|
|
452
373
|
it("applies typed initialSort", () => {
|
|
453
374
|
const { result } = renderHook(() =>
|
|
454
375
|
useCollectionParams({
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import { useCallback, useMemo,
|
|
2
|
-
import type {
|
|
3
|
-
FieldType,
|
|
4
|
-
TableMetadataMap,
|
|
5
|
-
} from "../../generator/metadata-generator";
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import type { TableMetadataMap } from "../../generator/metadata-generator";
|
|
6
3
|
import type {
|
|
7
4
|
Filter,
|
|
8
5
|
FilterOperator,
|
|
6
|
+
MetadataFilter,
|
|
9
7
|
QueryVariables,
|
|
10
8
|
SortState,
|
|
11
9
|
UseCollectionParamsOptions,
|
|
12
10
|
UseCollectionParamsReturn,
|
|
13
11
|
ExtractQueryVariables,
|
|
14
12
|
} from "../types";
|
|
15
|
-
import { fieldTypeToFilterConfig } from "../types";
|
|
16
13
|
import type { FieldName } from "../types";
|
|
17
14
|
|
|
18
15
|
// -----------------------------------------------------------------------------
|
|
@@ -52,7 +49,10 @@ export function useCollectionParams<
|
|
|
52
49
|
TTableName extends string & keyof TMetadata,
|
|
53
50
|
TQuery = never,
|
|
54
51
|
>(
|
|
55
|
-
options: UseCollectionParamsOptions<
|
|
52
|
+
options: UseCollectionParamsOptions<
|
|
53
|
+
FieldName<TMetadata, TTableName>,
|
|
54
|
+
MetadataFilter<TMetadata, TTableName>
|
|
55
|
+
> & {
|
|
56
56
|
metadata: TMetadata;
|
|
57
57
|
tableName: TTableName;
|
|
58
58
|
query?: TQuery;
|
|
@@ -106,15 +106,8 @@ export function useCollectionParams(
|
|
|
106
106
|
initialFilters = [],
|
|
107
107
|
initialSort = [],
|
|
108
108
|
pageSize: initialPageSize = 20,
|
|
109
|
-
metadata,
|
|
110
|
-
tableName,
|
|
111
109
|
} = options;
|
|
112
110
|
|
|
113
|
-
// Keep a ref to the resolved fields list so callbacks can look up fieldType.
|
|
114
|
-
const fieldsRef = useRef(
|
|
115
|
-
metadata && tableName ? metadata[tableName]?.fields : undefined,
|
|
116
|
-
);
|
|
117
|
-
|
|
118
111
|
// ---------------------------------------------------------------------------
|
|
119
112
|
// State
|
|
120
113
|
// ---------------------------------------------------------------------------
|
|
@@ -131,17 +124,8 @@ export function useCollectionParams(
|
|
|
131
124
|
(field: string, value: unknown, operator: FilterOperator = "eq") => {
|
|
132
125
|
setFiltersState((prev) => {
|
|
133
126
|
const existing = prev.findIndex((f) => f.field === field);
|
|
134
|
-
// Auto-detect fieldType from metadata when available
|
|
135
|
-
const fieldMeta = fieldsRef.current?.find((f) => f.name === field);
|
|
136
|
-
const detectedType: Filter["fieldType"] = fieldMeta
|
|
137
|
-
? (fieldTypeToFilterConfig(
|
|
138
|
-
fieldMeta.type as FieldType,
|
|
139
|
-
fieldMeta.enumValues as readonly string[] | undefined,
|
|
140
|
-
)?.type ?? "string")
|
|
141
|
-
: "string";
|
|
142
127
|
const newFilter: Filter = {
|
|
143
128
|
field,
|
|
144
|
-
fieldType: detectedType,
|
|
145
129
|
operator,
|
|
146
130
|
value,
|
|
147
131
|
};
|
package/src/component/index.ts
CHANGED
|
@@ -116,7 +116,9 @@ export function SearchFilterForm<TRow extends Record<string, unknown>>({
|
|
|
116
116
|
// Reset operator when field changes
|
|
117
117
|
useEffect(() => {
|
|
118
118
|
if (selectedFilterConfig) {
|
|
119
|
-
const ops = OPERATORS_BY_FILTER_TYPE[
|
|
119
|
+
const ops = OPERATORS_BY_FILTER_TYPE[
|
|
120
|
+
selectedFilterConfig.type
|
|
121
|
+
] as readonly FilterOperator[];
|
|
120
122
|
if (ops.length > 0 && !ops.includes(selectedOperator)) {
|
|
121
123
|
setSelectedOperator(ops[0]);
|
|
122
124
|
}
|
package/src/component/types.ts
CHANGED
|
@@ -39,39 +39,60 @@ export type FilterConfig =
|
|
|
39
39
|
| { type: "uuid" };
|
|
40
40
|
|
|
41
41
|
// =============================================================================
|
|
42
|
-
// Filter Operators
|
|
42
|
+
// Filter Operators (Single Source of Truth)
|
|
43
43
|
// =============================================================================
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Available filter operators (Tailor Platform compliant).
|
|
47
|
-
*/
|
|
48
|
-
export type FilterOperator =
|
|
49
|
-
| "eq"
|
|
50
|
-
| "ne"
|
|
51
|
-
| "contains"
|
|
52
|
-
| "startsWith"
|
|
53
|
-
| "endsWith"
|
|
54
|
-
| "gt"
|
|
55
|
-
| "gte"
|
|
56
|
-
| "lt"
|
|
57
|
-
| "lte"
|
|
58
|
-
| "between"
|
|
59
|
-
| "in"
|
|
60
|
-
| "notIn";
|
|
61
|
-
|
|
62
45
|
/**
|
|
63
46
|
* Operators available per filter type (Tailor Platform schema).
|
|
47
|
+
*
|
|
48
|
+
* This `as const satisfies` definition is the **single source of truth**.
|
|
49
|
+
* Both the `FilterOperator` union and the `OperatorForFilterType` mapping
|
|
50
|
+
* are derived from it, eliminating duplication.
|
|
64
51
|
*/
|
|
65
|
-
export const OPERATORS_BY_FILTER_TYPE
|
|
66
|
-
FilterConfig["type"],
|
|
67
|
-
FilterOperator[]
|
|
68
|
-
> = {
|
|
52
|
+
export const OPERATORS_BY_FILTER_TYPE = {
|
|
69
53
|
string: ["eq", "ne", "contains", "startsWith", "endsWith"],
|
|
70
54
|
number: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
|
|
71
55
|
date: ["eq", "ne", "gt", "gte", "lt", "lte", "between"],
|
|
72
56
|
enum: ["eq", "ne", "in", "notIn"],
|
|
73
57
|
boolean: ["eq", "ne"],
|
|
74
58
|
uuid: ["eq", "ne", "in", "notIn"],
|
|
59
|
+
} as const satisfies Record<FilterConfig["type"], readonly string[]>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Maps each filter type to the union of operators it supports.
|
|
63
|
+
* Derived from `OPERATORS_BY_FILTER_TYPE`.
|
|
64
|
+
*/
|
|
65
|
+
export type OperatorForFilterType = {
|
|
66
|
+
[T in FilterConfig["type"]]: (typeof OPERATORS_BY_FILTER_TYPE)[T][number];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Union of all available filter operators.
|
|
71
|
+
* Derived from `OPERATORS_BY_FILTER_TYPE`.
|
|
72
|
+
*/
|
|
73
|
+
export type FilterOperator = OperatorForFilterType[FilterConfig["type"]];
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Filter Type → FieldType Mapping (type-level)
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Type-level mapping from `FieldType` (metadata) to `FilterConfig["type"]`.
|
|
81
|
+
* Mirrors the runtime `fieldTypeToFilterConfig` function.
|
|
82
|
+
* Types that don't support filtering map to `never`.
|
|
83
|
+
*/
|
|
84
|
+
export type FieldTypeToFilterConfigType = {
|
|
85
|
+
string: "string";
|
|
86
|
+
number: "number";
|
|
87
|
+
boolean: "boolean";
|
|
88
|
+
uuid: "uuid";
|
|
89
|
+
datetime: "date";
|
|
90
|
+
date: "date";
|
|
91
|
+
time: "date";
|
|
92
|
+
enum: "enum";
|
|
93
|
+
array: never;
|
|
94
|
+
nested: never;
|
|
95
|
+
file: never;
|
|
75
96
|
};
|
|
76
97
|
|
|
77
98
|
// =============================================================================
|
|
@@ -80,14 +101,53 @@ export const OPERATORS_BY_FILTER_TYPE: Record<
|
|
|
80
101
|
|
|
81
102
|
/**
|
|
82
103
|
* Active filter state.
|
|
104
|
+
*
|
|
105
|
+
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
83
106
|
*/
|
|
84
|
-
export interface Filter {
|
|
85
|
-
field:
|
|
86
|
-
fieldType: FilterConfig["type"];
|
|
107
|
+
export interface Filter<TFieldName extends string = string> {
|
|
108
|
+
field: TFieldName;
|
|
87
109
|
operator: FilterOperator;
|
|
88
110
|
value: unknown;
|
|
89
111
|
}
|
|
90
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Metadata-aware filter type.
|
|
115
|
+
*
|
|
116
|
+
* Given table metadata, produces a discriminated union over each field
|
|
117
|
+
* where the `operator` is narrowed to only those valid for that field's type.
|
|
118
|
+
* Fields whose `FieldType` doesn't support filtering (array, nested, file) are excluded.
|
|
119
|
+
*
|
|
120
|
+
* @typeParam TMetadata - The `tableMetadata` object type (as const).
|
|
121
|
+
* @typeParam TTableName - The table name key in `TMetadata`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* // If table "task" has fields: name (string), priority (number), status (enum)
|
|
126
|
+
* type F = MetadataFilter<typeof tableMetadata, "task">;
|
|
127
|
+
* // | { field: "name"; operator: "eq" | "ne" | "contains" | ...; value: unknown }
|
|
128
|
+
* // | { field: "priority"; operator: "eq" | "ne" | "gt" | ...; value: unknown }
|
|
129
|
+
* // | { field: "status"; operator: "eq" | "ne" | "in" | "notIn"; value: unknown }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export type MetadataFilter<
|
|
133
|
+
TMetadata extends TableMetadataMap,
|
|
134
|
+
TTableName extends string & keyof TMetadata,
|
|
135
|
+
> = TMetadata[TTableName]["fields"][number] extends infer F
|
|
136
|
+
? F extends { readonly name: infer N; readonly type: infer T }
|
|
137
|
+
? N extends string
|
|
138
|
+
? T extends keyof FieldTypeToFilterConfigType
|
|
139
|
+
? FieldTypeToFilterConfigType[T] extends never
|
|
140
|
+
? never
|
|
141
|
+
: {
|
|
142
|
+
field: N;
|
|
143
|
+
operator: OperatorForFilterType[FieldTypeToFilterConfigType[T]];
|
|
144
|
+
value: unknown;
|
|
145
|
+
}
|
|
146
|
+
: never
|
|
147
|
+
: never
|
|
148
|
+
: never
|
|
149
|
+
: never;
|
|
150
|
+
|
|
91
151
|
/**
|
|
92
152
|
* Active sort state for a single field.
|
|
93
153
|
*/
|
|
@@ -270,9 +330,10 @@ export type ColumnDefinition<TRow extends Record<string, unknown>> =
|
|
|
270
330
|
*/
|
|
271
331
|
export interface UseCollectionParamsOptions<
|
|
272
332
|
TFieldName extends string = string,
|
|
333
|
+
TFilter extends Filter<TFieldName> = Filter<TFieldName>,
|
|
273
334
|
> {
|
|
274
335
|
/** Initial filters to apply */
|
|
275
|
-
initialFilters?:
|
|
336
|
+
initialFilters?: TFilter[];
|
|
276
337
|
/** Initial sort states */
|
|
277
338
|
initialSort?: { field: TFieldName; direction: "Asc" | "Desc" }[];
|
|
278
339
|
/** Number of items per page (default: 20) */
|