filtercn 0.1.2 → 0.2.0
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/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/templates/index.d.ts +1 -10
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +877 -713
- package/dist/templates/index.js.map +1 -1
- package/dist/utils/dependencies.d.ts.map +1 -1
- package/dist/utils/dependencies.js +4 -12
- package/dist/utils/dependencies.js.map +1 -1
- package/dist/utils/detect-project.js +1 -1
- package/dist/utils/detect-project.js.map +1 -1
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +1 -1
- package/dist/utils/file-writer.js.map +1 -1
- package/package.json +8 -2
- package/src/commands/init.ts +9 -26
- package/src/index.ts +1 -5
- package/src/templates/index.ts +871 -707
- package/src/utils/dependencies.ts +6 -18
- package/src/utils/detect-project.ts +1 -1
- package/src/utils/file-writer.ts +2 -5
package/src/templates/index.ts
CHANGED
|
@@ -1,110 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Each key is the relative path within the component directory,
|
|
4
|
-
* and each value is the file content.
|
|
5
|
-
*
|
|
6
|
-
* The `ALIAS` placeholder is replaced with the user's actual import alias.
|
|
2
|
+
* Auto-generated templates for the FilterCN CLI
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
const ALIAS = "__ALIAS__";
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
const TYPES = `// ===== FIELD DEFINITION =====
|
|
13
|
-
export type FieldType = "text" | "number" | "select" | "multiselect"
|
|
14
|
-
| "date" | "datetime" | "boolean" | "combobox";
|
|
15
|
-
|
|
16
|
-
export interface FilterFieldDefinition {
|
|
17
|
-
/** Unique key, also used as the query param name (e.g. "status", "category_id") */
|
|
18
|
-
name: string;
|
|
19
|
-
/** Label displayed in the UI */
|
|
20
|
-
label: string;
|
|
21
|
-
/** Input type to render */
|
|
22
|
-
type: FieldType;
|
|
23
|
-
/** Allowed operators (defaults based on type if not provided) */
|
|
24
|
-
operators?: OperatorType[];
|
|
25
|
-
/** Static options for select/multiselect */
|
|
26
|
-
options?: SelectOption[];
|
|
27
|
-
/** Async function to fetch options from a REST API (for combobox/dynamic multiselect) */
|
|
28
|
-
fetchOptions?: (search: string) => Promise<SelectOption[]>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ===== OPERATORS =====
|
|
32
|
-
export type OperatorType =
|
|
33
|
-
| "is" // =
|
|
34
|
-
| "is_not" // !=
|
|
35
|
-
| "contains" // *value*
|
|
36
|
-
| "not_contains" // NOT *value*
|
|
37
|
-
| "gt" // >
|
|
38
|
-
| "gte" // >=
|
|
39
|
-
| "lt" // <
|
|
40
|
-
| "lte" // <=
|
|
41
|
-
| "between" // range [from, to]
|
|
42
|
-
| "in" // in list
|
|
43
|
-
| "not_in" // not in list
|
|
44
|
-
| "is_empty" // NULL/empty check
|
|
45
|
-
| "is_not_empty";
|
|
46
|
-
|
|
47
|
-
// ===== SELECT OPTION =====
|
|
48
|
-
export interface SelectOption {
|
|
49
|
-
label: string;
|
|
50
|
-
value: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ===== FILTER ROW (runtime state) =====
|
|
54
|
-
export interface FilterRow {
|
|
55
|
-
id: string; // unique row id
|
|
56
|
-
field: FilterFieldDefinition | null; // selected field
|
|
57
|
-
operator: OperatorType | null; // selected operator
|
|
58
|
-
value: FilterValue; // entered value
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export type FilterValue =
|
|
62
|
-
| string
|
|
63
|
-
| string[]
|
|
64
|
-
| number
|
|
65
|
-
| [number, number] // range
|
|
66
|
-
| [string, string] // date range
|
|
67
|
-
| boolean
|
|
68
|
-
| null;
|
|
69
|
-
|
|
70
|
-
// ===== FILTER STATE =====
|
|
71
|
-
export interface FilterState {
|
|
72
|
-
rows: FilterRow[];
|
|
73
|
-
conjunction: "and" | "or";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ===== REST QUERY OUTPUT =====
|
|
77
|
-
export type RestQueryParams = Record<string, string | string[]>;
|
|
78
|
-
|
|
79
|
-
// ===== FILTER CONFIG (passed to Provider) =====
|
|
80
|
-
export interface FilterConfig {
|
|
81
|
-
/** List of fields available for filtering */
|
|
82
|
-
fields: FilterFieldDefinition[];
|
|
83
|
-
/** Allow toggling AND/OR. Default: false */
|
|
84
|
-
allowConjunctionToggle?: boolean;
|
|
85
|
-
/** Max filter rows. Default: 10 */
|
|
86
|
-
maxRows?: number;
|
|
87
|
-
/** Param style. Default: underscore */
|
|
88
|
-
paramStyle?: "underscore" | "bracket" | "custom";
|
|
89
|
-
/** Custom builder */
|
|
90
|
-
customParamBuilder?: (field: string, operator: OperatorType, value: FilterValue) => Record<string, string>;
|
|
91
|
-
/** Locale */
|
|
92
|
-
locale?: FilterLocale;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface FilterLocale {
|
|
96
|
-
addFilter: string;
|
|
97
|
-
reset: string;
|
|
98
|
-
apply: string;
|
|
99
|
-
placeholder: string;
|
|
100
|
-
and: string;
|
|
101
|
-
or: string;
|
|
102
|
-
noFilters: string;
|
|
103
|
-
}
|
|
104
|
-
`;
|
|
105
|
-
|
|
106
|
-
// ===================== constants.ts =====================
|
|
107
|
-
const CONSTANTS = `import type { FilterLocale, OperatorType, FieldType } from "./types";
|
|
7
|
+
const TEMPLATE_CONSTANTS_TS = `import type { FieldType, FilterLocale, OperatorType } from "./types";
|
|
108
8
|
|
|
109
9
|
export const DEFAULT_LOCALE: FilterLocale = {
|
|
110
10
|
addFilter: "+ Add filter",
|
|
@@ -128,12 +28,11 @@ export const DEFAULT_OPERATORS: Record<FieldType, OperatorType[]> = {
|
|
|
128
28
|
};
|
|
129
29
|
`;
|
|
130
30
|
|
|
131
|
-
|
|
132
|
-
const OPERATORS = `import type { OperatorType } from "../types";
|
|
31
|
+
const TEMPLATE_HELPERS_OPERATORS_TS = `import type { OperatorType } from "../types";
|
|
133
32
|
|
|
134
33
|
export const getOperatorSuffix = (operator: OperatorType, paramStyle: "underscore" | "bracket" | "custom"): string => {
|
|
135
34
|
if (paramStyle === "custom") return "";
|
|
136
|
-
|
|
35
|
+
|
|
137
36
|
const mapping: Record<OperatorType, string> = {
|
|
138
37
|
is: "", // usually no suffix for exact match
|
|
139
38
|
is_not: "not",
|
|
@@ -149,7 +48,7 @@ export const getOperatorSuffix = (operator: OperatorType, paramStyle: "underscor
|
|
|
149
48
|
is_empty: "isnull",
|
|
150
49
|
is_not_empty: "isnull",
|
|
151
50
|
};
|
|
152
|
-
|
|
51
|
+
|
|
153
52
|
const suffix = mapping[operator];
|
|
154
53
|
if (!suffix) return "";
|
|
155
54
|
|
|
@@ -159,48 +58,11 @@ export const getOperatorSuffix = (operator: OperatorType, paramStyle: "underscor
|
|
|
159
58
|
};
|
|
160
59
|
`;
|
|
161
60
|
|
|
162
|
-
|
|
163
|
-
const VALIDATORS = `import type { FilterRow } from "../types";
|
|
164
|
-
|
|
165
|
-
export const isValidFilterRow = (row: FilterRow): boolean => {
|
|
166
|
-
if (!row.field || !row.operator) return false;
|
|
167
|
-
|
|
168
|
-
if (row.operator === "is_empty" || row.operator === "is_not_empty") {
|
|
169
|
-
return true; // value doesn't matter for empty checks
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (row.operator === "between") {
|
|
173
|
-
if (!Array.isArray(row.value) || row.value.length !== 2) return false;
|
|
174
|
-
if (row.value[0] === null || row.value[0] === "" || row.value[1] === null || row.value[1] === "") return false;
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (row.operator === "in" || row.operator === "not_in") {
|
|
179
|
-
if (!Array.isArray(row.value) || row.value.length === 0) return false;
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (row.value === null || row.value === "" || (Array.isArray(row.value) && row.value.length === 0)) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return true;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
export const getValidFilterRows = (rows: FilterRow[]): FilterRow[] => {
|
|
191
|
-
return rows.filter(isValidFilterRow);
|
|
192
|
-
};
|
|
193
|
-
`;
|
|
194
|
-
|
|
195
|
-
// ===================== helpers/query-builder.ts =====================
|
|
196
|
-
const QUERY_BUILDER = `import type { FilterConfig, FilterRow, RestQueryParams } from "../types";
|
|
61
|
+
const TEMPLATE_HELPERS_QUERY_BUILDER_TS = `import type { FilterConfig, FilterRow, RestQueryParams } from "../types";
|
|
197
62
|
import { getOperatorSuffix } from "./operators";
|
|
198
63
|
import { getValidFilterRows } from "./validators";
|
|
199
64
|
|
|
200
|
-
export const buildRestQuery = (
|
|
201
|
-
rows: FilterRow[],
|
|
202
|
-
config: FilterConfig
|
|
203
|
-
): RestQueryParams => {
|
|
65
|
+
export const buildRestQuery = (rows: FilterRow[], config: FilterConfig): RestQueryParams => {
|
|
204
66
|
const validRows = getValidFilterRows(rows);
|
|
205
67
|
const params: RestQueryParams = {};
|
|
206
68
|
|
|
@@ -215,7 +77,8 @@ export const buildRestQuery = (
|
|
|
215
77
|
|
|
216
78
|
const style = config.paramStyle || "underscore";
|
|
217
79
|
const suffix = getOperatorSuffix(row.operator, style);
|
|
218
|
-
|
|
80
|
+
const prefix = config.paramPrefix || "";
|
|
81
|
+
|
|
219
82
|
let paramKey = row.field.name;
|
|
220
83
|
if (style === "underscore" && suffix) {
|
|
221
84
|
paramKey = \`\${row.field.name}\${suffix}\`;
|
|
@@ -225,6 +88,10 @@ export const buildRestQuery = (
|
|
|
225
88
|
paramKey = \`filter[\${row.field.name}]\`;
|
|
226
89
|
}
|
|
227
90
|
|
|
91
|
+
if (prefix) {
|
|
92
|
+
paramKey = \`\${prefix}\${paramKey}\`;
|
|
93
|
+
}
|
|
94
|
+
|
|
228
95
|
let paramValue: string | string[];
|
|
229
96
|
|
|
230
97
|
if (row.operator === "is_empty") {
|
|
@@ -233,15 +100,15 @@ export const buildRestQuery = (
|
|
|
233
100
|
paramValue = "false";
|
|
234
101
|
} else if (row.operator === "between" && Array.isArray(row.value)) {
|
|
235
102
|
if (style === "underscore") {
|
|
236
|
-
|
|
103
|
+
paramValue = \`\${row.value[0]},\${row.value[1]}\`;
|
|
237
104
|
} else {
|
|
238
|
-
|
|
105
|
+
paramValue = [row.value[0].toString(), row.value[1].toString()];
|
|
239
106
|
}
|
|
240
107
|
} else if ((row.operator === "in" || row.operator === "not_in") && Array.isArray(row.value)) {
|
|
241
108
|
if (style === "underscore") {
|
|
242
109
|
paramValue = row.value.join(",");
|
|
243
110
|
} else {
|
|
244
|
-
paramValue = row.value.map(v => v.toString());
|
|
111
|
+
paramValue = row.value.map((v) => v.toString());
|
|
245
112
|
}
|
|
246
113
|
} else {
|
|
247
114
|
paramValue = String(row.value);
|
|
@@ -254,23 +121,22 @@ export const buildRestQuery = (
|
|
|
254
121
|
};
|
|
255
122
|
`;
|
|
256
123
|
|
|
257
|
-
|
|
258
|
-
const SERIALIZER = `import type { FilterConfig, FilterRow, FilterState, OperatorType } from "../types";
|
|
124
|
+
const TEMPLATE_HELPERS_SERIALIZER_TS = `import type { FilterConfig, FilterRow, FilterState, FilterValue, OperatorType } from "../types";
|
|
259
125
|
import { buildRestQuery } from "./query-builder";
|
|
260
126
|
|
|
261
127
|
export const serializeFiltersToUrl = (state: FilterState, config: FilterConfig): URLSearchParams => {
|
|
262
128
|
const params = new URLSearchParams();
|
|
263
|
-
const
|
|
129
|
+
const restQuery = buildRestQuery(state.rows, config);
|
|
264
130
|
|
|
265
|
-
Object.entries(
|
|
131
|
+
Object.entries(restQuery).forEach(([key, value]) => {
|
|
266
132
|
if (Array.isArray(value)) {
|
|
267
|
-
|
|
133
|
+
params.append(key, value.join(","));
|
|
268
134
|
} else {
|
|
269
|
-
params.
|
|
135
|
+
params.append(key, value);
|
|
270
136
|
}
|
|
271
137
|
});
|
|
272
138
|
|
|
273
|
-
if (state.conjunction === "or"
|
|
139
|
+
if (state.conjunction === "or") {
|
|
274
140
|
params.set("conjunction", "or");
|
|
275
141
|
}
|
|
276
142
|
|
|
@@ -280,49 +146,59 @@ export const serializeFiltersToUrl = (state: FilterState, config: FilterConfig):
|
|
|
280
146
|
export const deserializeUrlToFilters = (params: URLSearchParams, config: FilterConfig): FilterState => {
|
|
281
147
|
const rows: FilterRow[] = [];
|
|
282
148
|
const style = config.paramStyle || "underscore";
|
|
149
|
+
const prefix = config.paramPrefix || "";
|
|
283
150
|
|
|
284
151
|
Array.from(params.entries()).forEach(([key, value]) => {
|
|
285
152
|
if (key === "conjunction") return;
|
|
286
153
|
|
|
287
|
-
let
|
|
154
|
+
let processKey = key;
|
|
155
|
+
if (prefix && processKey.startsWith(prefix)) {
|
|
156
|
+
processKey = processKey.slice(prefix.length);
|
|
157
|
+
} else if (prefix && !processKey.startsWith(prefix)) {
|
|
158
|
+
// If a prefix is configured but this key doesn't have it, it's not a filter param
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let fieldName = processKey;
|
|
288
163
|
let operatorStr = "";
|
|
289
164
|
|
|
290
|
-
if (style === "underscore" &&
|
|
291
|
-
const parts =
|
|
165
|
+
if (style === "underscore" && processKey.includes("__")) {
|
|
166
|
+
const parts = processKey.split("__");
|
|
292
167
|
operatorStr = parts.pop() || "";
|
|
293
168
|
fieldName = parts.join("__");
|
|
294
|
-
} else if (style === "bracket" &&
|
|
295
|
-
|
|
296
|
-
|
|
169
|
+
} else if (style === "bracket" && processKey.startsWith("filter[")) {
|
|
170
|
+
// Very basic bracket parse assumption
|
|
171
|
+
const match = processKey.match(/filter\\[(.*?)\\](?:\\[(.*?)\\])?/);
|
|
172
|
+
if (match?.[1]) {
|
|
297
173
|
fieldName = match[1];
|
|
298
174
|
operatorStr = match[2] || "";
|
|
299
175
|
}
|
|
300
176
|
}
|
|
301
177
|
|
|
302
|
-
const fieldDef = config.fields.find(f => f.name === fieldName);
|
|
178
|
+
const fieldDef = config.fields.find((f) => f.name === fieldName);
|
|
303
179
|
if (!fieldDef) return;
|
|
304
180
|
|
|
305
181
|
const suffixToOp: Record<string, OperatorType> = {
|
|
306
182
|
"": "is",
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
183
|
+
not: "is_not",
|
|
184
|
+
icontains: "contains",
|
|
185
|
+
not_icontains: "not_contains",
|
|
186
|
+
gt: "gt",
|
|
187
|
+
gte: "gte",
|
|
188
|
+
lt: "lt",
|
|
189
|
+
lte: "lte",
|
|
190
|
+
range: "between",
|
|
191
|
+
in: "in",
|
|
192
|
+
not_in: "not_in",
|
|
193
|
+
isnull: value === "true" ? "is_empty" : "is_not_empty",
|
|
318
194
|
};
|
|
319
195
|
|
|
320
196
|
const operator = suffixToOp[operatorStr] || "is";
|
|
321
|
-
let parsedValue:
|
|
197
|
+
let parsedValue: FilterValue = value;
|
|
322
198
|
|
|
323
|
-
if (operator === "between" && typeof value ===
|
|
199
|
+
if (operator === "between" && typeof value === "string" && value.includes(",")) {
|
|
324
200
|
parsedValue = value.split(",");
|
|
325
|
-
} else if ((operator === "in" || operator === "not_in") && typeof value ===
|
|
201
|
+
} else if ((operator === "in" || operator === "not_in") && typeof value === "string" && value.includes(",")) {
|
|
326
202
|
parsedValue = value.split(",");
|
|
327
203
|
}
|
|
328
204
|
|
|
@@ -330,7 +206,7 @@ export const deserializeUrlToFilters = (params: URLSearchParams, config: FilterC
|
|
|
330
206
|
id: crypto.randomUUID(),
|
|
331
207
|
field: fieldDef,
|
|
332
208
|
operator,
|
|
333
|
-
value: parsedValue
|
|
209
|
+
value: parsedValue,
|
|
334
210
|
});
|
|
335
211
|
});
|
|
336
212
|
|
|
@@ -341,11 +217,109 @@ export const deserializeUrlToFilters = (params: URLSearchParams, config: FilterC
|
|
|
341
217
|
};
|
|
342
218
|
`;
|
|
343
219
|
|
|
344
|
-
|
|
345
|
-
|
|
220
|
+
const TEMPLATE_HELPERS_VALIDATORS_TS = `import type { FilterRow } from "../types";
|
|
221
|
+
|
|
222
|
+
export const isValidFilterRow = (row: FilterRow): boolean => {
|
|
223
|
+
if (!row.field || !row.operator) return false;
|
|
224
|
+
|
|
225
|
+
if (row.operator === "is_empty" || row.operator === "is_not_empty") {
|
|
226
|
+
return true; // value doesn't matter for empty checks
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (row.operator === "between") {
|
|
230
|
+
if (!Array.isArray(row.value) || row.value.length !== 2) return false;
|
|
231
|
+
if (row.value[0] === null || row.value[0] === "" || row.value[1] === null || row.value[1] === "") return false;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (row.operator === "in" || row.operator === "not_in") {
|
|
236
|
+
if (!Array.isArray(row.value) || row.value.length === 0) return false;
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (row.value === null || row.value === "" || (Array.isArray(row.value) && row.value.length === 0)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return true;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const getValidFilterRows = (rows: FilterRow[]): FilterRow[] => {
|
|
248
|
+
return rows.filter(isValidFilterRow);
|
|
249
|
+
};
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
const TEMPLATE_HOOKS_USE_FILTER_OPTIONS_TS = `"use client";
|
|
253
|
+
|
|
254
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
255
|
+
import type { SelectOption } from "../types";
|
|
256
|
+
|
|
257
|
+
export const useFilterOptions = (fetchFn?: (search: string) => Promise<SelectOption[]>) => {
|
|
258
|
+
const [options, setOptions] = useState<SelectOption[]>([]);
|
|
259
|
+
const [loading, setLoading] = useState(false);
|
|
260
|
+
const timeoutRef = useRef<NodeJS.Timeout>(null);
|
|
261
|
+
|
|
262
|
+
const search = useCallback(
|
|
263
|
+
(query: string) => {
|
|
264
|
+
if (!fetchFn) return;
|
|
265
|
+
|
|
266
|
+
if (timeoutRef.current) {
|
|
267
|
+
clearTimeout(timeoutRef.current);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
setLoading(true);
|
|
271
|
+
|
|
272
|
+
timeoutRef.current = setTimeout(async () => {
|
|
273
|
+
try {
|
|
274
|
+
const result = await fetchFn(query);
|
|
275
|
+
setOptions(result);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error("Failed to fetch filter options", err);
|
|
278
|
+
} finally {
|
|
279
|
+
setLoading(false);
|
|
280
|
+
}
|
|
281
|
+
}, 300);
|
|
282
|
+
},
|
|
283
|
+
[fetchFn],
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Initial fetch
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (fetchFn) {
|
|
289
|
+
search("");
|
|
290
|
+
}
|
|
291
|
+
}, [fetchFn, search]);
|
|
292
|
+
|
|
293
|
+
return { options, loading, search };
|
|
294
|
+
};
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
const TEMPLATE_HOOKS_USE_FILTER_QUERY_TS = `"use client";
|
|
298
|
+
|
|
299
|
+
import { useMemo } from "react";
|
|
300
|
+
import { buildRestQuery } from "../helpers/query-builder";
|
|
301
|
+
import { useFilterContext } from "../provider/filter-context";
|
|
302
|
+
|
|
303
|
+
export function useFilterQuery() {
|
|
304
|
+
const { state, config, activeCount, isValid } = useFilterContext();
|
|
305
|
+
|
|
306
|
+
const queryParams = useMemo(() => {
|
|
307
|
+
return buildRestQuery(state.rows, config);
|
|
308
|
+
}, [state.rows, config]);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
queryParams,
|
|
312
|
+
activeCount,
|
|
313
|
+
isValid,
|
|
314
|
+
conjunction: state.conjunction,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
const TEMPLATE_HOOKS_USE_FILTER_STATE_TS = `"use client";
|
|
346
320
|
|
|
347
|
-
import {
|
|
348
|
-
import type { FilterState,
|
|
321
|
+
import { useCallback, useState } from "react";
|
|
322
|
+
import type { FilterFieldDefinition, FilterState, FilterValue, OperatorType } from "../types";
|
|
349
323
|
|
|
350
324
|
export const useFilterState = (initialState?: FilterState) => {
|
|
351
325
|
const [state, setState] = useState<FilterState>(initialState || { rows: [], conjunction: "and" });
|
|
@@ -353,10 +327,7 @@ export const useFilterState = (initialState?: FilterState) => {
|
|
|
353
327
|
const addRow = useCallback(() => {
|
|
354
328
|
setState((prev) => ({
|
|
355
329
|
...prev,
|
|
356
|
-
rows: [
|
|
357
|
-
...prev.rows,
|
|
358
|
-
{ id: crypto.randomUUID(), field: null, operator: null, value: null },
|
|
359
|
-
],
|
|
330
|
+
rows: [...prev.rows, { id: crypto.randomUUID(), field: null, operator: null, value: null }],
|
|
360
331
|
}));
|
|
361
332
|
}, []);
|
|
362
333
|
|
|
@@ -370,18 +341,14 @@ export const useFilterState = (initialState?: FilterState) => {
|
|
|
370
341
|
const updateField = useCallback((id: string, field: FilterFieldDefinition) => {
|
|
371
342
|
setState((prev) => ({
|
|
372
343
|
...prev,
|
|
373
|
-
rows: prev.rows.map((row) =>
|
|
374
|
-
row.id === id ? { ...row, field, operator: null, value: null } : row
|
|
375
|
-
),
|
|
344
|
+
rows: prev.rows.map((row) => (row.id === id ? { ...row, field, operator: null, value: null } : row)),
|
|
376
345
|
}));
|
|
377
346
|
}, []);
|
|
378
347
|
|
|
379
348
|
const updateOperator = useCallback((id: string, operator: OperatorType) => {
|
|
380
349
|
setState((prev) => ({
|
|
381
350
|
...prev,
|
|
382
|
-
rows: prev.rows.map((row) =>
|
|
383
|
-
row.id === id ? { ...row, operator, value: null } : row
|
|
384
|
-
),
|
|
351
|
+
rows: prev.rows.map((row) => (row.id === id ? { ...row, operator, value: null } : row)),
|
|
385
352
|
}));
|
|
386
353
|
}, []);
|
|
387
354
|
|
|
@@ -400,6 +367,8 @@ export const useFilterState = (initialState?: FilterState) => {
|
|
|
400
367
|
setState({ rows: [], conjunction: "and" });
|
|
401
368
|
}, []);
|
|
402
369
|
|
|
370
|
+
// Sync internal state with external state if external state changes intentionally
|
|
371
|
+
// Often useful when URL changes
|
|
403
372
|
const overrideState = useCallback((newState: FilterState) => {
|
|
404
373
|
setState(newState);
|
|
405
374
|
}, []);
|
|
@@ -413,33 +382,25 @@ export const useFilterState = (initialState?: FilterState) => {
|
|
|
413
382
|
updateValue,
|
|
414
383
|
setConjunction,
|
|
415
384
|
reset,
|
|
416
|
-
overrideState
|
|
385
|
+
overrideState,
|
|
417
386
|
};
|
|
418
387
|
};
|
|
419
388
|
`;
|
|
420
389
|
|
|
421
|
-
|
|
422
|
-
const USE_FILTER_URL_SYNC = `"use client";
|
|
390
|
+
const TEMPLATE_HOOKS_USE_FILTER_URL_SYNC_TS = `"use client";
|
|
423
391
|
|
|
424
|
-
import {
|
|
425
|
-
import {
|
|
426
|
-
import
|
|
427
|
-
import { serializeFiltersToUrl, deserializeUrlToFilters } from "../helpers/serializer";
|
|
392
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
393
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
394
|
+
import { deserializeUrlToFilters, serializeFiltersToUrl } from "../helpers/serializer";
|
|
428
395
|
import { getValidFilterRows } from "../helpers/validators";
|
|
396
|
+
import type { FilterConfig, FilterState } from "../types";
|
|
429
397
|
|
|
430
|
-
|
|
431
|
-
syncMode: "immediate" | "on-apply";
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export const useFilterUrlSync = (
|
|
435
|
-
config: FilterConfig,
|
|
436
|
-
_options: UseFilterUrlSyncOptions
|
|
437
|
-
) => {
|
|
398
|
+
export const useFilterUrlSync = (config: FilterConfig) => {
|
|
438
399
|
const router = useRouter();
|
|
439
400
|
const searchParams = useSearchParams();
|
|
440
401
|
const pathname = usePathname();
|
|
441
402
|
const isInitializing = useRef(true);
|
|
442
|
-
|
|
403
|
+
|
|
443
404
|
const [syncedState, setSyncedState] = useState<FilterState>(() => {
|
|
444
405
|
const params = new URLSearchParams(searchParams.toString());
|
|
445
406
|
return deserializeUrlToFilters(params, config);
|
|
@@ -454,13 +415,39 @@ export const useFilterUrlSync = (
|
|
|
454
415
|
setSyncedState(deserializeUrlToFilters(params, config));
|
|
455
416
|
}, [searchParams, config]);
|
|
456
417
|
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
418
|
+
const handleSync = useCallback(
|
|
419
|
+
(currentState: FilterState) => {
|
|
420
|
+
// preserve current non-filter params + search param
|
|
421
|
+
const currentParams = new URLSearchParams(searchParams.toString());
|
|
422
|
+
const newParams = serializeFiltersToUrl(currentState, config);
|
|
423
|
+
|
|
424
|
+
// Keep searchParamName if it exists
|
|
425
|
+
const searchParamName = config.searchParamName || "q";
|
|
426
|
+
const currentSearchParam = currentParams.get(searchParamName);
|
|
427
|
+
|
|
428
|
+
// We clear all existing filter params (by trusting what serialize gives us)
|
|
429
|
+
// Then we append any non-filter params back (except what's newly generated)
|
|
430
|
+
// Simplest approach: create a fresh URLSearchParams, add all from \`newParams\`,
|
|
431
|
+
// then add anything from \`currentParams\` that isn't managed by the filter.
|
|
432
|
+
// For simplicity, we just keep the search param if provided.
|
|
433
|
+
if (currentSearchParam) {
|
|
434
|
+
newParams.set(searchParamName, currentSearchParam);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const queryStr = newParams.toString();
|
|
438
|
+
const newPath = queryStr ? \`\${pathname}?\${queryStr}\` : pathname;
|
|
439
|
+
|
|
440
|
+
router.push(newPath, { scroll: false });
|
|
441
|
+
},
|
|
442
|
+
[config, pathname, router, searchParams],
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const applyChanges = useCallback(
|
|
446
|
+
(newState: FilterState) => {
|
|
447
|
+
handleSync({ ...newState, rows: getValidFilterRows(newState.rows) });
|
|
448
|
+
},
|
|
449
|
+
[handleSync],
|
|
450
|
+
);
|
|
464
451
|
|
|
465
452
|
return {
|
|
466
453
|
initialState: syncedState,
|
|
@@ -469,50 +456,36 @@ export const useFilterUrlSync = (
|
|
|
469
456
|
};
|
|
470
457
|
`;
|
|
471
458
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
export
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
setLoading(false);
|
|
492
|
-
}
|
|
493
|
-
}, [fetchFn]);
|
|
494
|
-
|
|
495
|
-
// Initial fetch
|
|
496
|
-
useEffect(() => {
|
|
497
|
-
if (fetchFn) {
|
|
498
|
-
search("");
|
|
499
|
-
}
|
|
500
|
-
}, [fetchFn, search]);
|
|
501
|
-
|
|
502
|
-
return { options, loading, search };
|
|
503
|
-
};
|
|
459
|
+
const TEMPLATE_INDEX_TS = `export * from "./constants";
|
|
460
|
+
export { buildRestQuery } from "./helpers/query-builder";
|
|
461
|
+
export { deserializeUrlToFilters, serializeFiltersToUrl } from "./helpers/serializer";
|
|
462
|
+
export { getValidFilterRows, isValidFilterRow } from "./helpers/validators";
|
|
463
|
+
export * from "./hooks/use-filter-options";
|
|
464
|
+
export * from "./hooks/use-filter-query";
|
|
465
|
+
export * from "./hooks/use-filter-state";
|
|
466
|
+
export * from "./hooks/use-filter-url-sync";
|
|
467
|
+
export * from "./provider/filter-context";
|
|
468
|
+
export * from "./provider/filter-provider";
|
|
469
|
+
export * from "./types";
|
|
470
|
+
export * from "./ui/field-select";
|
|
471
|
+
export * from "./ui/filter-badge";
|
|
472
|
+
export * from "./ui/filter-bar";
|
|
473
|
+
export * from "./ui/filter-footer";
|
|
474
|
+
export * from "./ui/filter-root";
|
|
475
|
+
export * from "./ui/filter-row";
|
|
476
|
+
export * from "./ui/operator-select";
|
|
477
|
+
export * from "./ui/value-input";
|
|
504
478
|
`;
|
|
505
479
|
|
|
506
|
-
|
|
507
|
-
const FILTER_CONTEXT = `"use client";
|
|
480
|
+
const TEMPLATE_PROVIDER_FILTER_CONTEXT_TS = `"use client";
|
|
508
481
|
|
|
509
482
|
import { createContext, useContext } from "react";
|
|
510
|
-
import type { FilterConfig, FilterState,
|
|
483
|
+
import type { FilterConfig, FilterFieldDefinition, FilterState, FilterValue, OperatorType } from "../types";
|
|
511
484
|
|
|
512
485
|
export interface FilterContextValue {
|
|
513
486
|
config: FilterConfig;
|
|
514
487
|
state: FilterState;
|
|
515
|
-
|
|
488
|
+
|
|
516
489
|
// Mutations
|
|
517
490
|
addRow: () => void;
|
|
518
491
|
removeRow: (id: string) => void;
|
|
@@ -541,16 +514,16 @@ export const useFilterContext = () => {
|
|
|
541
514
|
};
|
|
542
515
|
`;
|
|
543
516
|
|
|
544
|
-
|
|
545
|
-
const FILTER_PROVIDER = `"use client";
|
|
517
|
+
const TEMPLATE_PROVIDER_FILTER_PROVIDER_TSX = `"use client";
|
|
546
518
|
|
|
547
|
-
import React
|
|
548
|
-
import
|
|
549
|
-
import { FilterContext } from "./filter-context";
|
|
550
|
-
import { useFilterState } from "../hooks/use-filter-state";
|
|
551
|
-
import { useFilterUrlSync } from "../hooks/use-filter-url-sync";
|
|
519
|
+
import type React from "react";
|
|
520
|
+
import { useCallback, useMemo } from "react";
|
|
552
521
|
import { DEFAULT_LOCALE } from "../constants";
|
|
553
522
|
import { getValidFilterRows, isValidFilterRow } from "../helpers/validators";
|
|
523
|
+
import { useFilterState } from "../hooks/use-filter-state";
|
|
524
|
+
import { useFilterUrlSync } from "../hooks/use-filter-url-sync";
|
|
525
|
+
import type { FilterConfig } from "../types";
|
|
526
|
+
import { FilterContext } from "./filter-context";
|
|
554
527
|
|
|
555
528
|
export interface FilterProviderProps {
|
|
556
529
|
config: FilterConfig;
|
|
@@ -565,9 +538,7 @@ export const FilterProvider: React.FC<FilterProviderProps> = ({ config, children
|
|
|
565
538
|
};
|
|
566
539
|
}, [config]);
|
|
567
540
|
|
|
568
|
-
const { initialState, applyChanges } = useFilterUrlSync(currentConfig
|
|
569
|
-
syncMode: "on-apply",
|
|
570
|
-
});
|
|
541
|
+
const { initialState, applyChanges } = useFilterUrlSync(currentConfig);
|
|
571
542
|
|
|
572
543
|
const {
|
|
573
544
|
state,
|
|
@@ -581,7 +552,7 @@ export const FilterProvider: React.FC<FilterProviderProps> = ({ config, children
|
|
|
581
552
|
} = useFilterState(initialState);
|
|
582
553
|
|
|
583
554
|
const isValid = useMemo(() => state.rows.every(isValidFilterRow), [state.rows]);
|
|
584
|
-
|
|
555
|
+
|
|
585
556
|
const activeCount = useMemo(() => getValidFilterRows(state.rows).length, [state.rows]);
|
|
586
557
|
|
|
587
558
|
const apply = useCallback(() => {
|
|
@@ -621,113 +592,150 @@ export const FilterProvider: React.FC<FilterProviderProps> = ({ config, children
|
|
|
621
592
|
isValid,
|
|
622
593
|
activeCount,
|
|
623
594
|
apply,
|
|
624
|
-
]
|
|
595
|
+
],
|
|
625
596
|
);
|
|
626
597
|
|
|
627
|
-
return
|
|
628
|
-
<FilterContext.Provider value={value}>
|
|
629
|
-
{children}
|
|
630
|
-
</FilterContext.Provider>
|
|
631
|
-
);
|
|
598
|
+
return <FilterContext.Provider value={value}>{children}</FilterContext.Provider>;
|
|
632
599
|
};
|
|
633
600
|
`;
|
|
634
601
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
export * from "./constants";
|
|
638
|
-
export * from "./hooks/use-filter-state";
|
|
639
|
-
export * from "./hooks/use-filter-url-sync";
|
|
640
|
-
export * from "./hooks/use-filter-options";
|
|
641
|
-
export * from "./provider/filter-context";
|
|
642
|
-
export * from "./provider/filter-provider";
|
|
643
|
-
|
|
644
|
-
export * from "./ui/field-select";
|
|
645
|
-
export * from "./ui/operator-select";
|
|
646
|
-
export * from "./ui/value-input";
|
|
647
|
-
export * from "./ui/filter-row";
|
|
648
|
-
export * from "./ui/filter-footer";
|
|
649
|
-
export * from "./ui/filter-badge";
|
|
650
|
-
export * from "./ui/filter-root";
|
|
602
|
+
const TEMPLATE_TYPES_TS = `// ===== FIELD DEFINITION =====
|
|
603
|
+
export type FieldType = "text" | "number" | "select" | "multiselect" | "date" | "datetime" | "boolean" | "combobox";
|
|
651
604
|
|
|
652
|
-
export
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
605
|
+
export interface FilterFieldDefinition {
|
|
606
|
+
/** Unique key, cũng là tên query param (e.g. "status", "category_id") */
|
|
607
|
+
name: string;
|
|
608
|
+
/** Label hiển thị trên UI */
|
|
609
|
+
label: string;
|
|
610
|
+
/** Loại input sẽ render */
|
|
611
|
+
type: FieldType;
|
|
612
|
+
/**
|
|
613
|
+
* Danh sách operators được phép (nếu không truyền, lấy default)
|
|
614
|
+
*/
|
|
615
|
+
operators?: OperatorType[];
|
|
616
|
+
/**
|
|
617
|
+
* Options tĩnh cho select/multiselect
|
|
618
|
+
*/
|
|
619
|
+
options?: SelectOption[];
|
|
620
|
+
/**
|
|
621
|
+
* Hàm fetch options từ REST API (cho combobox/multiselect động)
|
|
622
|
+
*/
|
|
623
|
+
fetchOptions?: (search: string) => Promise<SelectOption[]>;
|
|
624
|
+
}
|
|
656
625
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
626
|
+
// ===== OPERATORS =====
|
|
627
|
+
export type OperatorType =
|
|
628
|
+
| "is" // =
|
|
629
|
+
| "is_not" // !=
|
|
630
|
+
| "contains" // *value*
|
|
631
|
+
| "not_contains" // NOT *value*
|
|
632
|
+
| "gt" // >
|
|
633
|
+
| "gte" // >=
|
|
634
|
+
| "lt" // <
|
|
635
|
+
| "lte" // <=
|
|
636
|
+
| "between" // range [from, to]
|
|
637
|
+
| "in" // in list
|
|
638
|
+
| "not_in" // not in list
|
|
639
|
+
| "is_empty" // NULL/empty check
|
|
640
|
+
| "is_not_empty";
|
|
665
641
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
"helpers/operators.ts": replaceAlias(OPERATORS),
|
|
671
|
-
"helpers/validators.ts": replaceAlias(VALIDATORS),
|
|
672
|
-
"helpers/query-builder.ts": replaceAlias(QUERY_BUILDER),
|
|
673
|
-
"helpers/serializer.ts": replaceAlias(SERIALIZER),
|
|
674
|
-
"hooks/use-filter-state.ts": replaceAlias(USE_FILTER_STATE),
|
|
675
|
-
"hooks/use-filter-url-sync.ts": replaceAlias(USE_FILTER_URL_SYNC),
|
|
676
|
-
"hooks/use-filter-options.ts": replaceAlias(USE_FILTER_OPTIONS),
|
|
677
|
-
"provider/filter-context.ts": replaceAlias(FILTER_CONTEXT),
|
|
678
|
-
"provider/filter-provider.tsx": replaceAlias(FILTER_PROVIDER),
|
|
679
|
-
...getUITemplates(alias),
|
|
680
|
-
};
|
|
642
|
+
// ===== SELECT OPTION =====
|
|
643
|
+
export interface SelectOption {
|
|
644
|
+
label: string;
|
|
645
|
+
value: string;
|
|
681
646
|
}
|
|
682
647
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
648
|
+
// ===== FILTER ROW (runtime state) =====
|
|
649
|
+
export interface FilterRow {
|
|
650
|
+
id: string; // unique row id
|
|
651
|
+
field: FilterFieldDefinition | null; // field đã chọn
|
|
652
|
+
operator: OperatorType | null; // operator đã chọn
|
|
653
|
+
value: FilterValue; // giá trị đã nhập
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export type FilterValue =
|
|
657
|
+
| string
|
|
658
|
+
| string[]
|
|
659
|
+
| number
|
|
660
|
+
| [number, number] // range
|
|
661
|
+
| [string, string] // date range
|
|
662
|
+
| boolean
|
|
663
|
+
| null;
|
|
664
|
+
|
|
665
|
+
// ===== FILTER STATE =====
|
|
666
|
+
export interface FilterState {
|
|
667
|
+
rows: FilterRow[];
|
|
668
|
+
conjunction: "and" | "or";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ===== REST QUERY OUTPUT =====
|
|
672
|
+
export type RestQueryParams = Record<string, string | string[]>;
|
|
673
|
+
|
|
674
|
+
// ===== FILTER CONFIG (truyền vào Provider) =====
|
|
675
|
+
export interface FilterConfig {
|
|
676
|
+
/** Danh sách fields có thể filter */
|
|
677
|
+
fields: FilterFieldDefinition[];
|
|
678
|
+
/** Cho phép toggle AND/OR. Default: false */
|
|
679
|
+
allowConjunctionToggle?: boolean;
|
|
680
|
+
/** Max filter rows. Default: 10 */
|
|
681
|
+
maxRows?: number;
|
|
682
|
+
/** Param style. Default: underscore */
|
|
683
|
+
paramStyle?: "underscore" | "bracket" | "custom";
|
|
684
|
+
/** Prefix appended to all query params. e.g "filter_" */
|
|
685
|
+
paramPrefix?: string;
|
|
686
|
+
/** Global search query param name. Default: q */
|
|
687
|
+
searchParamName?: string;
|
|
688
|
+
/** Custom builder */
|
|
689
|
+
customParamBuilder?: (field: string, operator: OperatorType, value: FilterValue) => Record<string, string>;
|
|
690
|
+
/** Locale */
|
|
691
|
+
locale?: FilterLocale;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export interface FilterLocale {
|
|
695
|
+
addFilter: string;
|
|
696
|
+
reset: string;
|
|
697
|
+
apply: string;
|
|
698
|
+
placeholder: string;
|
|
699
|
+
and: string;
|
|
700
|
+
or: string;
|
|
701
|
+
noFilters: string;
|
|
702
|
+
}
|
|
703
|
+
`;
|
|
704
|
+
|
|
705
|
+
const TEMPLATE_UI_FIELD_SELECT_TSX = `"use client";
|
|
688
706
|
|
|
689
|
-
import { useState } from "react";
|
|
690
707
|
import { Check, ChevronsUpDown } from "lucide-react";
|
|
691
|
-
import {
|
|
692
|
-
import {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
CommandInput,
|
|
697
|
-
CommandItem,
|
|
698
|
-
CommandList,
|
|
699
|
-
} from "${alias}components/ui/command";
|
|
700
|
-
import {
|
|
701
|
-
Popover,
|
|
702
|
-
PopoverContent,
|
|
703
|
-
PopoverTrigger,
|
|
704
|
-
} from "${alias}components/ui/popover";
|
|
708
|
+
import { useState } from "react";
|
|
709
|
+
import { Button } from "__ALIAS__components/ui/button";
|
|
710
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "__ALIAS__components/ui/command";
|
|
711
|
+
import { Popover, PopoverContent, PopoverTrigger } from "__ALIAS__components/ui/popover";
|
|
712
|
+
import { cn } from "__ALIAS__lib/utils";
|
|
705
713
|
import { useFilterContext } from "../provider/filter-context";
|
|
706
714
|
import type { FilterFieldDefinition } from "../types";
|
|
707
715
|
|
|
708
|
-
interface FieldSelectProps {
|
|
716
|
+
export interface FieldSelectProps {
|
|
709
717
|
rowId: string;
|
|
718
|
+
selectedField: FilterFieldDefinition | null;
|
|
710
719
|
}
|
|
711
720
|
|
|
712
|
-
export function FieldSelect({ rowId }: FieldSelectProps) {
|
|
721
|
+
export function FieldSelect({ rowId, selectedField }: FieldSelectProps) {
|
|
722
|
+
const { config, updateField } = useFilterContext();
|
|
713
723
|
const [open, setOpen] = useState(false);
|
|
714
|
-
const { config, state, updateField } = useFilterContext();
|
|
715
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
716
|
-
|
|
717
|
-
const handleSelect = (field: FilterFieldDefinition) => {
|
|
718
|
-
updateField(rowId, field);
|
|
719
|
-
setOpen(false);
|
|
720
|
-
};
|
|
721
724
|
|
|
722
725
|
return (
|
|
723
726
|
<Popover open={open} onOpenChange={setOpen}>
|
|
724
727
|
<PopoverTrigger asChild>
|
|
725
|
-
<Button
|
|
726
|
-
|
|
728
|
+
<Button
|
|
729
|
+
variant="outline"
|
|
730
|
+
role="combobox"
|
|
731
|
+
aria-expanded={open}
|
|
732
|
+
className="w-[200px] justify-between font-normal"
|
|
733
|
+
>
|
|
734
|
+
{selectedField ? selectedField.label : "Select field..."}
|
|
727
735
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
728
736
|
</Button>
|
|
729
737
|
</PopoverTrigger>
|
|
730
|
-
<PopoverContent className="w-[200px] p-0">
|
|
738
|
+
<PopoverContent className="w-[200px] p-0" align="start">
|
|
731
739
|
<Command>
|
|
732
740
|
<CommandInput placeholder="Search field..." />
|
|
733
741
|
<CommandList>
|
|
@@ -737,10 +745,13 @@ export function FieldSelect({ rowId }: FieldSelectProps) {
|
|
|
737
745
|
<CommandItem
|
|
738
746
|
key={field.name}
|
|
739
747
|
value={field.name}
|
|
740
|
-
onSelect={() =>
|
|
748
|
+
onSelect={() => {
|
|
749
|
+
updateField(rowId, field);
|
|
750
|
+
setOpen(false);
|
|
751
|
+
}}
|
|
741
752
|
>
|
|
742
753
|
<Check
|
|
743
|
-
className={
|
|
754
|
+
className={cn("mr-2 h-4 w-4", selectedField?.name === field.name ? "opacity-100" : "opacity-0")}
|
|
744
755
|
/>
|
|
745
756
|
{field.label}
|
|
746
757
|
</CommandItem>
|
|
@@ -754,322 +765,238 @@ export function FieldSelect({ rowId }: FieldSelectProps) {
|
|
|
754
765
|
}
|
|
755
766
|
`;
|
|
756
767
|
|
|
757
|
-
|
|
768
|
+
const TEMPLATE_UI_FILTER_BADGE_TSX = `"use client";
|
|
758
769
|
|
|
759
|
-
import {
|
|
760
|
-
Select,
|
|
761
|
-
SelectContent,
|
|
762
|
-
SelectItem,
|
|
763
|
-
SelectTrigger,
|
|
764
|
-
SelectValue,
|
|
765
|
-
} from "${alias}components/ui/select";
|
|
770
|
+
import { Badge } from "__ALIAS__components/ui/badge";
|
|
766
771
|
import { useFilterContext } from "../provider/filter-context";
|
|
767
|
-
import { DEFAULT_OPERATORS } from "../constants";
|
|
768
|
-
import type { OperatorType } from "../types";
|
|
769
|
-
|
|
770
|
-
interface OperatorSelectProps {
|
|
771
|
-
rowId: string;
|
|
772
|
-
}
|
|
773
772
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
is_not: "is not",
|
|
777
|
-
contains: "contains",
|
|
778
|
-
not_contains: "not contains",
|
|
779
|
-
gt: "greater than",
|
|
780
|
-
gte: "greater or equal",
|
|
781
|
-
lt: "less than",
|
|
782
|
-
lte: "less or equal",
|
|
783
|
-
between: "between",
|
|
784
|
-
in: "in",
|
|
785
|
-
not_in: "not in",
|
|
786
|
-
is_empty: "is empty",
|
|
787
|
-
is_not_empty: "is not empty",
|
|
788
|
-
};
|
|
789
|
-
|
|
790
|
-
export function OperatorSelect({ rowId }: OperatorSelectProps) {
|
|
791
|
-
const { state, updateOperator } = useFilterContext();
|
|
792
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
793
|
-
|
|
794
|
-
if (!row?.field) return null;
|
|
773
|
+
export function FilterBadge() {
|
|
774
|
+
const { activeCount } = useFilterContext();
|
|
795
775
|
|
|
796
|
-
|
|
776
|
+
if (activeCount === 0) return null;
|
|
797
777
|
|
|
798
778
|
return (
|
|
799
|
-
<
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
>
|
|
803
|
-
<SelectTrigger className="w-[160px] text-sm">
|
|
804
|
-
<SelectValue placeholder="Operator..." />
|
|
805
|
-
</SelectTrigger>
|
|
806
|
-
<SelectContent>
|
|
807
|
-
{operators.map((op) => (
|
|
808
|
-
<SelectItem key={op} value={op}>
|
|
809
|
-
{OPERATOR_LABELS[op] || op}
|
|
810
|
-
</SelectItem>
|
|
811
|
-
))}
|
|
812
|
-
</SelectContent>
|
|
813
|
-
</Select>
|
|
779
|
+
<Badge variant="secondary" className="ml-2 rounded-full px-2">
|
|
780
|
+
{activeCount}
|
|
781
|
+
</Badge>
|
|
814
782
|
);
|
|
815
783
|
}
|
|
816
784
|
`;
|
|
817
785
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
import {
|
|
821
|
-
import {
|
|
822
|
-
import
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
SelectItem,
|
|
826
|
-
SelectTrigger,
|
|
827
|
-
SelectValue,
|
|
828
|
-
} from "${alias}components/ui/select";
|
|
829
|
-
import {
|
|
830
|
-
Popover,
|
|
831
|
-
PopoverContent,
|
|
832
|
-
PopoverTrigger,
|
|
833
|
-
} from "${alias}components/ui/popover";
|
|
834
|
-
import { Button } from "${alias}components/ui/button";
|
|
835
|
-
import { Calendar } from "${alias}components/ui/calendar";
|
|
836
|
-
import { CalendarIcon } from "lucide-react";
|
|
837
|
-
import { format } from "date-fns";
|
|
786
|
+
const TEMPLATE_UI_FILTER_BAR_TSX = `"use client";
|
|
787
|
+
|
|
788
|
+
import { Search } from "lucide-react";
|
|
789
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
790
|
+
import * as React from "react";
|
|
791
|
+
import { Input } from "__ALIAS__components/ui/input";
|
|
792
|
+
import { cn } from "__ALIAS__lib/utils";
|
|
838
793
|
import { useFilterContext } from "../provider/filter-context";
|
|
839
|
-
import {
|
|
840
|
-
import { Badge } from "${alias}components/ui/badge";
|
|
841
|
-
import type { SelectOption } from "../types";
|
|
794
|
+
import { FilterRoot } from "./filter-root";
|
|
842
795
|
|
|
843
|
-
interface
|
|
844
|
-
|
|
796
|
+
export interface FilterBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
797
|
+
searchPlaceholder?: string;
|
|
798
|
+
searchValue?: string;
|
|
799
|
+
onSearchChange?: (value: string) => void;
|
|
800
|
+
hideSearch?: boolean;
|
|
845
801
|
}
|
|
846
802
|
|
|
847
|
-
export
|
|
848
|
-
|
|
849
|
-
|
|
803
|
+
export const FilterBar = React.forwardRef<HTMLDivElement, FilterBarProps>(
|
|
804
|
+
(
|
|
805
|
+
{
|
|
806
|
+
className,
|
|
807
|
+
searchPlaceholder = "Search...",
|
|
808
|
+
searchValue: externalSearchValue,
|
|
809
|
+
onSearchChange,
|
|
810
|
+
hideSearch = false,
|
|
811
|
+
...props
|
|
812
|
+
},
|
|
813
|
+
ref,
|
|
814
|
+
) => {
|
|
815
|
+
const router = useRouter();
|
|
816
|
+
const pathname = usePathname();
|
|
817
|
+
const searchParams = useSearchParams();
|
|
818
|
+
const { config } = useFilterContext();
|
|
819
|
+
|
|
820
|
+
const searchParamName = config.searchParamName || "q";
|
|
821
|
+
const internalSearchValue = searchParams.get(searchParamName) || "";
|
|
822
|
+
|
|
823
|
+
const [localValue, setLocalValue] = React.useState(
|
|
824
|
+
externalSearchValue !== undefined ? externalSearchValue : internalSearchValue,
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
const [, startTransition] = React.useTransition();
|
|
828
|
+
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
|
829
|
+
|
|
830
|
+
// Sync external changes (e.g. from clear filters) back into local state
|
|
831
|
+
React.useEffect(() => {
|
|
832
|
+
if (externalSearchValue !== undefined) {
|
|
833
|
+
setLocalValue(externalSearchValue);
|
|
834
|
+
} else {
|
|
835
|
+
setLocalValue(internalSearchValue);
|
|
836
|
+
}
|
|
837
|
+
}, [externalSearchValue, internalSearchValue]);
|
|
850
838
|
|
|
851
|
-
|
|
839
|
+
const handleSearchChange = (value: string) => {
|
|
840
|
+
if (onSearchChange) {
|
|
841
|
+
onSearchChange(value);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
852
844
|
|
|
853
|
-
|
|
854
|
-
if (row.operator === "is_empty" || row.operator === "is_not_empty") {
|
|
855
|
-
return null;
|
|
856
|
-
}
|
|
845
|
+
setLocalValue(value);
|
|
857
846
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
case "text":
|
|
862
|
-
return <TextInput rowId={rowId} />;
|
|
863
|
-
case "number":
|
|
864
|
-
return row.operator === "between"
|
|
865
|
-
? <RangeInput rowId={rowId} />
|
|
866
|
-
: <NumberInput rowId={rowId} />;
|
|
867
|
-
case "select":
|
|
868
|
-
case "combobox":
|
|
869
|
-
return <SelectInput rowId={rowId} />;
|
|
870
|
-
case "multiselect":
|
|
871
|
-
return <MultiSelectInput rowId={rowId} />;
|
|
872
|
-
case "date":
|
|
873
|
-
case "datetime":
|
|
874
|
-
return row.operator === "between"
|
|
875
|
-
? <DateRangeInput rowId={rowId} />
|
|
876
|
-
: <DateInput rowId={rowId} />;
|
|
877
|
-
case "boolean":
|
|
878
|
-
return <BooleanInput rowId={rowId} />;
|
|
879
|
-
default:
|
|
880
|
-
return <TextInput rowId={rowId} />;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
847
|
+
if (timeoutRef.current) {
|
|
848
|
+
clearTimeout(timeoutRef.current);
|
|
849
|
+
}
|
|
883
850
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
851
|
+
timeoutRef.current = setTimeout(() => {
|
|
852
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
853
|
+
if (value) {
|
|
854
|
+
params.set(searchParamName, value);
|
|
855
|
+
} else {
|
|
856
|
+
params.delete(searchParamName);
|
|
857
|
+
}
|
|
858
|
+
const queryStr = params.toString();
|
|
859
|
+
const newPath = queryStr ? \`\${pathname}?\${queryStr}\` : pathname;
|
|
860
|
+
|
|
861
|
+
startTransition(() => {
|
|
862
|
+
router.replace(newPath, { scroll: false });
|
|
863
|
+
});
|
|
864
|
+
}, 300);
|
|
865
|
+
};
|
|
887
866
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
867
|
+
return (
|
|
868
|
+
<div ref={ref} className={cn("flex items-center justify-between gap-4", className)} {...props}>
|
|
869
|
+
<div className="flex-1 max-w-sm">
|
|
870
|
+
{!hideSearch && (
|
|
871
|
+
<div className="relative">
|
|
872
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
873
|
+
<Input
|
|
874
|
+
type="search"
|
|
875
|
+
placeholder={searchPlaceholder}
|
|
876
|
+
className="pl-8"
|
|
877
|
+
value={localValue}
|
|
878
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
879
|
+
/>
|
|
880
|
+
</div>
|
|
881
|
+
)}
|
|
882
|
+
</div>
|
|
883
|
+
<div className="flex items-center gap-2">
|
|
884
|
+
<FilterRoot />
|
|
885
|
+
</div>
|
|
886
|
+
</div>
|
|
887
|
+
);
|
|
888
|
+
},
|
|
889
|
+
);
|
|
890
|
+
FilterBar.displayName = "FilterBar";
|
|
891
|
+
`;
|
|
898
892
|
|
|
899
|
-
|
|
900
|
-
const { state, updateValue } = useFilterContext();
|
|
901
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
893
|
+
const TEMPLATE_UI_FILTER_FOOTER_TSX = `"use client";
|
|
902
894
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
className="w-[200px] text-sm"
|
|
908
|
-
value={(row?.value as string) || ""}
|
|
909
|
-
onChange={(e) => updateValue(rowId, e.target.value)}
|
|
910
|
-
/>
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function RangeInput({ rowId }: { rowId: string }) {
|
|
915
|
-
const { state, updateValue } = useFilterContext();
|
|
916
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
917
|
-
const values = Array.isArray(row?.value) ? row.value : ["", ""];
|
|
895
|
+
import { Button } from "__ALIAS__components/ui/button";
|
|
896
|
+
import { PopoverClose } from "__ALIAS__components/ui/popover";
|
|
897
|
+
import { isValidFilterRow } from "../helpers/validators";
|
|
898
|
+
import { useFilterContext } from "../provider/filter-context";
|
|
918
899
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
placeholder="From"
|
|
924
|
-
className="w-[100px] text-sm"
|
|
925
|
-
value={values[0]?.toString() || ""}
|
|
926
|
-
onChange={(e) => updateValue(rowId, [e.target.value, values[1]?.toString() || ""])}
|
|
927
|
-
/>
|
|
928
|
-
<span className="text-xs text-muted-foreground">to</span>
|
|
929
|
-
<Input
|
|
930
|
-
type="number"
|
|
931
|
-
placeholder="To"
|
|
932
|
-
className="w-[100px] text-sm"
|
|
933
|
-
value={values[1]?.toString() || ""}
|
|
934
|
-
onChange={(e) => updateValue(rowId, [values[0]?.toString() || "", e.target.value])}
|
|
935
|
-
/>
|
|
936
|
-
</div>
|
|
937
|
-
);
|
|
938
|
-
}
|
|
900
|
+
export function FilterFooter() {
|
|
901
|
+
const { config, addRow, reset, apply, state, setConjunction } = useFilterContext();
|
|
902
|
+
const maxRows = config.maxRows || 10;
|
|
903
|
+
const canAddMore = state.rows.length < maxRows;
|
|
939
904
|
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
943
|
-
const { options: fetchedOptions } = useFilterOptions(row?.field?.fetchOptions);
|
|
944
|
-
const options: SelectOption[] = row?.field?.options || fetchedOptions || [];
|
|
905
|
+
// Determine if there is at least one valid filter row
|
|
906
|
+
const hasValidFilters = state.rows.some(isValidFilterRow);
|
|
945
907
|
|
|
946
908
|
return (
|
|
947
|
-
<
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
909
|
+
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
|
910
|
+
<div className="flex items-center space-x-2">
|
|
911
|
+
<Button variant="ghost" onClick={addRow} disabled={!canAddMore} className="text-sm font-medium">
|
|
912
|
+
{config.locale?.addFilter || "+ Add filter"}
|
|
913
|
+
</Button>
|
|
914
|
+
{config.allowConjunctionToggle && state.rows.length > 1 && (
|
|
915
|
+
<div className="flex items-center bg-zinc-100 dark:bg-zinc-800 p-0.5 rounded-md">
|
|
916
|
+
<Button
|
|
917
|
+
variant={state.conjunction === "and" ? "default" : "ghost"}
|
|
918
|
+
size="sm"
|
|
919
|
+
onClick={() => setConjunction("and")}
|
|
920
|
+
className="h-7 text-xs px-3"
|
|
921
|
+
>
|
|
922
|
+
{config.locale?.and || "AND"}
|
|
923
|
+
</Button>
|
|
924
|
+
<Button
|
|
925
|
+
variant={state.conjunction === "or" ? "default" : "ghost"}
|
|
926
|
+
size="sm"
|
|
927
|
+
onClick={() => setConjunction("or")}
|
|
928
|
+
className="h-7 text-xs px-3"
|
|
929
|
+
>
|
|
930
|
+
{config.locale?.or || "OR"}
|
|
931
|
+
</Button>
|
|
932
|
+
</div>
|
|
933
|
+
)}
|
|
934
|
+
</div>
|
|
935
|
+
<div className="flex space-x-2">
|
|
936
|
+
<PopoverClose asChild>
|
|
937
|
+
<Button variant="outline" onClick={reset}>
|
|
938
|
+
{config.locale?.reset || "Reset"}
|
|
939
|
+
</Button>
|
|
940
|
+
</PopoverClose>
|
|
941
|
+
{hasValidFilters ? (
|
|
942
|
+
<PopoverClose asChild>
|
|
943
|
+
<Button onClick={apply}>{config.locale?.apply || "Apply"}</Button>
|
|
944
|
+
</PopoverClose>
|
|
945
|
+
) : (
|
|
946
|
+
<Button disabled>{config.locale?.apply || "Apply"}</Button>
|
|
947
|
+
)}
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
962
950
|
);
|
|
963
951
|
}
|
|
952
|
+
`;
|
|
964
953
|
|
|
965
|
-
|
|
966
|
-
const { state, updateValue } = useFilterContext();
|
|
967
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
968
|
-
const selectedValues = Array.isArray(row?.value) ? (row.value as string[]) : [];
|
|
969
|
-
const options: SelectOption[] = row?.field?.options || [];
|
|
970
|
-
|
|
971
|
-
const toggleValue = (val: string) => {
|
|
972
|
-
const newValues = selectedValues.includes(val)
|
|
973
|
-
? selectedValues.filter((v) => v !== val)
|
|
974
|
-
: [...selectedValues, val];
|
|
975
|
-
updateValue(rowId, newValues);
|
|
976
|
-
};
|
|
954
|
+
const TEMPLATE_UI_FILTER_ROOT_TSX = `"use client";
|
|
977
955
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
>
|
|
987
|
-
{opt.label}
|
|
988
|
-
</Badge>
|
|
989
|
-
))}
|
|
990
|
-
</div>
|
|
991
|
-
);
|
|
992
|
-
}
|
|
956
|
+
import { Filter as FilterIcon } from "lucide-react";
|
|
957
|
+
import { Button } from "__ALIAS__components/ui/button";
|
|
958
|
+
import { Popover, PopoverContent, PopoverTrigger } from "__ALIAS__components/ui/popover";
|
|
959
|
+
import { ScrollArea } from "__ALIAS__components/ui/scroll-area";
|
|
960
|
+
import { useFilterContext } from "../provider/filter-context";
|
|
961
|
+
import { FilterBadge } from "./filter-badge";
|
|
962
|
+
import { FilterFooter } from "./filter-footer";
|
|
963
|
+
import { FilterRowComponent } from "./filter-row";
|
|
993
964
|
|
|
994
|
-
function
|
|
995
|
-
const { state,
|
|
996
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
997
|
-
const [open, setOpen] = useState(false);
|
|
998
|
-
const dateValue = row?.value ? new Date(row.value as string) : undefined;
|
|
965
|
+
export function FilterRoot() {
|
|
966
|
+
const { state, config } = useFilterContext();
|
|
999
967
|
|
|
1000
968
|
return (
|
|
1001
|
-
<Popover
|
|
969
|
+
<Popover>
|
|
1002
970
|
<PopoverTrigger asChild>
|
|
1003
|
-
<Button variant="outline" className="
|
|
1004
|
-
<
|
|
1005
|
-
|
|
971
|
+
<Button variant="outline" className="flex items-center gap-2">
|
|
972
|
+
<FilterIcon className="h-4 w-4" />
|
|
973
|
+
<span>Filters</span>
|
|
974
|
+
<FilterBadge />
|
|
1006
975
|
</Button>
|
|
1007
976
|
</PopoverTrigger>
|
|
1008
|
-
<PopoverContent className="w-
|
|
1009
|
-
<
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
977
|
+
<PopoverContent className="w-[850px] p-4" align="start">
|
|
978
|
+
<ScrollArea className="max-h-[400px] pr-4">
|
|
979
|
+
<div className="flex flex-col gap-2">
|
|
980
|
+
{state.rows.length === 0 ? (
|
|
981
|
+
<div className="text-center py-4 text-muted-foreground">
|
|
982
|
+
{config.locale?.noFilters || "No filters active"}
|
|
983
|
+
</div>
|
|
984
|
+
) : (
|
|
985
|
+
state.rows.map((row) => <FilterRowComponent key={row.id} rowId={row.id} />)
|
|
986
|
+
)}
|
|
987
|
+
</div>
|
|
988
|
+
</ScrollArea>
|
|
989
|
+
<FilterFooter />
|
|
1019
990
|
</PopoverContent>
|
|
1020
991
|
</Popover>
|
|
1021
992
|
);
|
|
1022
993
|
}
|
|
1023
|
-
|
|
1024
|
-
function DateRangeInput({ rowId }: { rowId: string }) {
|
|
1025
|
-
const { state, updateValue } = useFilterContext();
|
|
1026
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
1027
|
-
const values = Array.isArray(row?.value) ? row.value : ["", ""];
|
|
1028
|
-
|
|
1029
|
-
return (
|
|
1030
|
-
<div className="flex items-center gap-2">
|
|
1031
|
-
<Input
|
|
1032
|
-
type="date"
|
|
1033
|
-
className="w-[150px] text-sm"
|
|
1034
|
-
value={(values[0] as string) || ""}
|
|
1035
|
-
onChange={(e) => updateValue(rowId, [e.target.value, values[1] as string || ""])}
|
|
1036
|
-
/>
|
|
1037
|
-
<span className="text-xs text-muted-foreground">to</span>
|
|
1038
|
-
<Input
|
|
1039
|
-
type="date"
|
|
1040
|
-
className="w-[150px] text-sm"
|
|
1041
|
-
value={(values[1] as string) || ""}
|
|
1042
|
-
onChange={(e) => updateValue(rowId, [values[0] as string || "", e.target.value])}
|
|
1043
|
-
/>
|
|
1044
|
-
</div>
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function BooleanInput({ rowId }: { rowId: string }) {
|
|
1049
|
-
const { state, updateValue } = useFilterContext();
|
|
1050
|
-
const row = state.rows.find((r) => r.id === rowId);
|
|
1051
|
-
|
|
1052
|
-
return (
|
|
1053
|
-
<Select
|
|
1054
|
-
value={row?.value?.toString() || ""}
|
|
1055
|
-
onValueChange={(value) => updateValue(rowId, value === "true")}
|
|
1056
|
-
>
|
|
1057
|
-
<SelectTrigger className="w-[120px] text-sm">
|
|
1058
|
-
<SelectValue placeholder="Select..." />
|
|
1059
|
-
</SelectTrigger>
|
|
1060
|
-
<SelectContent>
|
|
1061
|
-
<SelectItem value="true">True</SelectItem>
|
|
1062
|
-
<SelectItem value="false">False</SelectItem>
|
|
1063
|
-
</SelectContent>
|
|
1064
|
-
</Select>
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
994
|
`;
|
|
1068
995
|
|
|
1069
|
-
|
|
996
|
+
const TEMPLATE_UI_FILTER_ROW_TSX = `"use client";
|
|
1070
997
|
|
|
1071
998
|
import { Trash2 } from "lucide-react";
|
|
1072
|
-
import { Button } from "
|
|
999
|
+
import { Button } from "__ALIAS__components/ui/button";
|
|
1073
1000
|
import { useFilterContext } from "../provider/filter-context";
|
|
1074
1001
|
import { FieldSelect } from "./field-select";
|
|
1075
1002
|
import { OperatorSelect } from "./operator-select";
|
|
@@ -1082,131 +1009,368 @@ interface FilterRowProps {
|
|
|
1082
1009
|
export function FilterRowComponent({ rowId }: FilterRowProps) {
|
|
1083
1010
|
const { state, removeRow } = useFilterContext();
|
|
1084
1011
|
const row = state.rows.find((r) => r.id === rowId);
|
|
1012
|
+
|
|
1085
1013
|
if (!row) return null;
|
|
1086
1014
|
|
|
1087
1015
|
return (
|
|
1088
|
-
<div className="flex items-center
|
|
1089
|
-
<FieldSelect rowId={
|
|
1090
|
-
|
|
1091
|
-
{row.field && row.
|
|
1016
|
+
<div className="flex items-center space-x-2 w-full py-2">
|
|
1017
|
+
<FieldSelect rowId={row.id} selectedField={row.field} />
|
|
1018
|
+
|
|
1019
|
+
{row.field && <OperatorSelect rowId={row.id} selectedField={row.field.name} selectedOperator={row.operator} />}
|
|
1020
|
+
|
|
1021
|
+
{row.field && row.operator && (
|
|
1022
|
+
<div className="flex-1 flex max-w-[280px]">
|
|
1023
|
+
<ValueInput rowId={row.id} field={row.field} operator={row.operator} value={row.value} />
|
|
1024
|
+
</div>
|
|
1025
|
+
)}
|
|
1026
|
+
|
|
1092
1027
|
<Button
|
|
1093
1028
|
variant="ghost"
|
|
1094
1029
|
size="icon"
|
|
1095
|
-
|
|
1096
|
-
|
|
1030
|
+
onClick={() => removeRow(row.id)}
|
|
1031
|
+
className="ml-auto flex-shrink-0"
|
|
1032
|
+
aria-label="Remove filter"
|
|
1097
1033
|
>
|
|
1098
|
-
<Trash2 className="h-4 w-4" />
|
|
1034
|
+
<Trash2 className="h-4 w-4 text-muted-foreground" />
|
|
1099
1035
|
</Button>
|
|
1100
1036
|
</div>
|
|
1101
1037
|
);
|
|
1102
1038
|
}
|
|
1103
1039
|
`;
|
|
1104
1040
|
|
|
1105
|
-
|
|
1041
|
+
const TEMPLATE_UI_OPERATOR_SELECT_TSX = `"use client";
|
|
1106
1042
|
|
|
1107
|
-
import {
|
|
1108
|
-
import {
|
|
1043
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "__ALIAS__components/ui/select";
|
|
1044
|
+
import { DEFAULT_OPERATORS } from "../constants";
|
|
1109
1045
|
import { useFilterContext } from "../provider/filter-context";
|
|
1110
|
-
import {
|
|
1111
|
-
import { isValidFilterRow } from "../helpers/validators";
|
|
1046
|
+
import type { OperatorType } from "../types";
|
|
1112
1047
|
|
|
1113
|
-
export
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1048
|
+
export interface OperatorSelectProps {
|
|
1049
|
+
rowId: string;
|
|
1050
|
+
selectedField: string | null;
|
|
1051
|
+
selectedOperator: OperatorType | null;
|
|
1052
|
+
}
|
|
1117
1053
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1054
|
+
const OPERATOR_LABELS: Record<OperatorType, string> = {
|
|
1055
|
+
is: "is",
|
|
1056
|
+
is_not: "is not",
|
|
1057
|
+
contains: "contains",
|
|
1058
|
+
not_contains: "does not contain",
|
|
1059
|
+
gt: "greater than",
|
|
1060
|
+
gte: "greater or equal",
|
|
1061
|
+
lt: "less than",
|
|
1062
|
+
lte: "less or equal",
|
|
1063
|
+
between: "between",
|
|
1064
|
+
in: "in list",
|
|
1065
|
+
not_in: "not in list",
|
|
1066
|
+
is_empty: "is empty",
|
|
1067
|
+
is_not_empty: "is not empty",
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
export function OperatorSelect({ rowId, selectedField, selectedOperator }: OperatorSelectProps) {
|
|
1071
|
+
const { config, updateOperator } = useFilterContext();
|
|
1072
|
+
|
|
1073
|
+
const fieldDef = config.fields.find((f) => f.name === selectedField);
|
|
1074
|
+
|
|
1075
|
+
if (!fieldDef) {
|
|
1076
|
+
return (
|
|
1077
|
+
<Select disabled>
|
|
1078
|
+
<SelectTrigger className="w-[180px]">
|
|
1079
|
+
<SelectValue placeholder="Operator" />
|
|
1080
|
+
</SelectTrigger>
|
|
1081
|
+
</Select>
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const allowedOperators = fieldDef.operators || DEFAULT_OPERATORS[fieldDef.type] || ["is"];
|
|
1120
1086
|
|
|
1121
1087
|
return (
|
|
1122
|
-
<
|
|
1123
|
-
<
|
|
1124
|
-
<
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
</
|
|
1133
|
-
|
|
1134
|
-
<PopoverClose asChild>
|
|
1135
|
-
<Button size="sm" onClick={apply}>
|
|
1136
|
-
<Check className="mr-1 h-4 w-4" />
|
|
1137
|
-
{config.locale?.apply || "Apply"}
|
|
1138
|
-
</Button>
|
|
1139
|
-
</PopoverClose>
|
|
1140
|
-
) : (
|
|
1141
|
-
<Button size="sm" disabled>
|
|
1142
|
-
<Check className="mr-1 h-4 w-4" />
|
|
1143
|
-
{config.locale?.apply || "Apply"}
|
|
1144
|
-
</Button>
|
|
1145
|
-
)}
|
|
1146
|
-
</div>
|
|
1088
|
+
<Select value={selectedOperator || ""} onValueChange={(val) => updateOperator(rowId, val as OperatorType)}>
|
|
1089
|
+
<SelectTrigger className="w-[180px]">
|
|
1090
|
+
<SelectValue placeholder="Select operator..." />
|
|
1091
|
+
</SelectTrigger>
|
|
1092
|
+
<SelectContent>
|
|
1093
|
+
{allowedOperators.map((op) => (
|
|
1094
|
+
<SelectItem key={op} value={op}>
|
|
1095
|
+
{OPERATOR_LABELS[op] || op}
|
|
1096
|
+
</SelectItem>
|
|
1097
|
+
))}
|
|
1098
|
+
</SelectContent>
|
|
1099
|
+
</Select>
|
|
1147
1100
|
);
|
|
1148
1101
|
}
|
|
1149
1102
|
`;
|
|
1150
1103
|
|
|
1151
|
-
|
|
1104
|
+
const TEMPLATE_UI_VALUE_INPUT_TSX = `"use client";
|
|
1152
1105
|
|
|
1106
|
+
import { format } from "date-fns";
|
|
1107
|
+
import { CalendarIcon, Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
|
1108
|
+
import React from "react";
|
|
1109
|
+
import { Button } from "__ALIAS__components/ui/button";
|
|
1110
|
+
import { Calendar } from "__ALIAS__components/ui/calendar";
|
|
1111
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "__ALIAS__components/ui/command";
|
|
1112
|
+
import { Input } from "__ALIAS__components/ui/input";
|
|
1113
|
+
import { Popover, PopoverContent, PopoverTrigger } from "__ALIAS__components/ui/popover";
|
|
1114
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "__ALIAS__components/ui/select";
|
|
1115
|
+
import { cn } from "__ALIAS__lib/utils";
|
|
1116
|
+
import { useFilterOptions } from "../hooks/use-filter-options";
|
|
1153
1117
|
import { useFilterContext } from "../provider/filter-context";
|
|
1154
|
-
import {
|
|
1118
|
+
import type { FilterFieldDefinition, FilterValue, OperatorType } from "../types";
|
|
1155
1119
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1120
|
+
interface ValueInputProps {
|
|
1121
|
+
rowId: string;
|
|
1122
|
+
field: FilterFieldDefinition;
|
|
1123
|
+
operator: OperatorType;
|
|
1124
|
+
value: FilterValue;
|
|
1160
1125
|
}
|
|
1161
|
-
`;
|
|
1162
1126
|
|
|
1163
|
-
|
|
1127
|
+
export function ValueInput({ rowId, field, operator, value }: ValueInputProps) {
|
|
1128
|
+
const { updateValue } = useFilterContext();
|
|
1164
1129
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1130
|
+
if (operator === "is_empty" || operator === "is_not_empty") {
|
|
1131
|
+
return null; // value doesn't matter
|
|
1132
|
+
}
|
|
1168
1133
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1134
|
+
const handleChange = (val: FilterValue) => {
|
|
1135
|
+
updateValue(rowId, val);
|
|
1136
|
+
};
|
|
1171
1137
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
<FilterRowComponent rowId={row.id} />
|
|
1193
|
-
</div>
|
|
1194
|
-
))}
|
|
1138
|
+
const isDateField = field.type === "date" || field.type === "datetime";
|
|
1139
|
+
|
|
1140
|
+
// Range
|
|
1141
|
+
if (operator === "between") {
|
|
1142
|
+
const valArr = Array.isArray(value) ? value : ["", ""];
|
|
1143
|
+
|
|
1144
|
+
if (isDateField) {
|
|
1145
|
+
return (
|
|
1146
|
+
<div className="flex items-center flex-1 space-x-2">
|
|
1147
|
+
<DatePickerInput
|
|
1148
|
+
value={valArr[0] as string}
|
|
1149
|
+
onChange={(dateStr) => handleChange([dateStr, String(valArr[1])] as FilterValue)}
|
|
1150
|
+
placeholder="Start date"
|
|
1151
|
+
/>
|
|
1152
|
+
<span className="text-muted-foreground">-</span>
|
|
1153
|
+
<DatePickerInput
|
|
1154
|
+
value={valArr[1] as string}
|
|
1155
|
+
onChange={(dateStr) => handleChange([String(valArr[0]), dateStr] as FilterValue)}
|
|
1156
|
+
placeholder="End date"
|
|
1157
|
+
/>
|
|
1195
1158
|
</div>
|
|
1196
|
-
)
|
|
1197
|
-
|
|
1198
|
-
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return (
|
|
1163
|
+
<div className="flex items-center flex-1 space-x-2">
|
|
1164
|
+
<Input
|
|
1165
|
+
type={field.type === "number" ? "number" : "text"}
|
|
1166
|
+
placeholder="Min"
|
|
1167
|
+
value={(valArr[0] as string | number) || ""}
|
|
1168
|
+
onChange={(e) => handleChange([e.target.value, String(valArr[1])] as FilterValue)}
|
|
1169
|
+
className="flex-1 min-w-[80px]"
|
|
1170
|
+
/>
|
|
1171
|
+
<span className="text-muted-foreground">-</span>
|
|
1172
|
+
<Input
|
|
1173
|
+
type={field.type === "number" ? "number" : "text"}
|
|
1174
|
+
placeholder="Max"
|
|
1175
|
+
value={(valArr[1] as string | number) || ""}
|
|
1176
|
+
onChange={(e) => handleChange([String(valArr[0]), e.target.value] as FilterValue)}
|
|
1177
|
+
className="flex-1 min-w-[80px]"
|
|
1178
|
+
/>
|
|
1179
|
+
</div>
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Boolean
|
|
1184
|
+
if (field.type === "boolean") {
|
|
1185
|
+
return (
|
|
1186
|
+
<Select
|
|
1187
|
+
value={value === true ? "true" : value === false ? "false" : ""}
|
|
1188
|
+
onValueChange={(val) => handleChange(val === "true")}
|
|
1189
|
+
>
|
|
1190
|
+
<SelectTrigger className="flex-1 min-w-[120px]">
|
|
1191
|
+
<SelectValue placeholder="Select..." />
|
|
1192
|
+
</SelectTrigger>
|
|
1193
|
+
<SelectContent>
|
|
1194
|
+
<SelectItem value="true">Yes</SelectItem>
|
|
1195
|
+
<SelectItem value="false">No</SelectItem>
|
|
1196
|
+
</SelectContent>
|
|
1197
|
+
</Select>
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Select (Static options)
|
|
1202
|
+
if (field.type === "select" && field.options) {
|
|
1203
|
+
return (
|
|
1204
|
+
<Select value={(value as string) || ""} onValueChange={handleChange}>
|
|
1205
|
+
<SelectTrigger className="flex-1 min-w-[120px]">
|
|
1206
|
+
<SelectValue placeholder="Select option..." />
|
|
1207
|
+
</SelectTrigger>
|
|
1208
|
+
<SelectContent>
|
|
1209
|
+
{field.options.map((opt) => (
|
|
1210
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
1211
|
+
{opt.label}
|
|
1212
|
+
</SelectItem>
|
|
1213
|
+
))}
|
|
1214
|
+
</SelectContent>
|
|
1215
|
+
</Select>
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Combobox (Dynamic options)
|
|
1220
|
+
if (field.type === "combobox" && field.fetchOptions) {
|
|
1221
|
+
return <ComboboxValueInput field={field} value={value} onChange={handleChange} />;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Date Picker
|
|
1225
|
+
if (isDateField) {
|
|
1226
|
+
return (
|
|
1227
|
+
<DatePickerInput
|
|
1228
|
+
value={value as string}
|
|
1229
|
+
onChange={handleChange}
|
|
1230
|
+
placeholder="Pick a date"
|
|
1231
|
+
className="flex-1 min-w-[120px]"
|
|
1232
|
+
/>
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Default Input (Text, Number)
|
|
1237
|
+
return (
|
|
1238
|
+
<Input
|
|
1239
|
+
type={field.type === "number" ? "number" : "text"}
|
|
1240
|
+
placeholder="Value..."
|
|
1241
|
+
value={(value as string | number) || ""}
|
|
1242
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
1243
|
+
className="flex-1 min-w-[120px]"
|
|
1244
|
+
/>
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function DatePickerInput({
|
|
1249
|
+
value,
|
|
1250
|
+
onChange,
|
|
1251
|
+
placeholder,
|
|
1252
|
+
className,
|
|
1253
|
+
}: {
|
|
1254
|
+
value?: string;
|
|
1255
|
+
onChange: (val: string) => void;
|
|
1256
|
+
placeholder?: string;
|
|
1257
|
+
className?: string;
|
|
1258
|
+
}) {
|
|
1259
|
+
const date = value ? new Date(value) : undefined;
|
|
1260
|
+
|
|
1261
|
+
return (
|
|
1262
|
+
<Popover>
|
|
1263
|
+
<PopoverTrigger asChild>
|
|
1264
|
+
<Button
|
|
1265
|
+
variant={"outline"}
|
|
1266
|
+
className={cn("justify-start text-left font-normal flex-1", !date && "text-muted-foreground", className)}
|
|
1267
|
+
>
|
|
1268
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
1269
|
+
{date ? format(date, "PPP") : <span>{placeholder}</span>}
|
|
1270
|
+
</Button>
|
|
1271
|
+
</PopoverTrigger>
|
|
1272
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
1273
|
+
<Calendar
|
|
1274
|
+
mode="single"
|
|
1275
|
+
selected={date}
|
|
1276
|
+
onSelect={(d) => onChange(d ? format(d, "yyyy-MM-dd") : "")}
|
|
1277
|
+
initialFocus
|
|
1278
|
+
/>
|
|
1279
|
+
</PopoverContent>
|
|
1280
|
+
</Popover>
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function ComboboxValueInput({
|
|
1285
|
+
field,
|
|
1286
|
+
value,
|
|
1287
|
+
onChange,
|
|
1288
|
+
}: {
|
|
1289
|
+
field: FilterFieldDefinition;
|
|
1290
|
+
value: FilterValue;
|
|
1291
|
+
onChange: (val: FilterValue) => void;
|
|
1292
|
+
}) {
|
|
1293
|
+
const [open, setOpen] = React.useState(false);
|
|
1294
|
+
const { options, loading, search } = useFilterOptions(field.fetchOptions);
|
|
1295
|
+
|
|
1296
|
+
const selectedLabel = React.useMemo(() => {
|
|
1297
|
+
if (!value) return "Select...";
|
|
1298
|
+
const opt = options.find((o) => o.value === value);
|
|
1299
|
+
return opt ? opt.label : value;
|
|
1300
|
+
}, [value, options]);
|
|
1301
|
+
|
|
1302
|
+
return (
|
|
1303
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
1304
|
+
<PopoverTrigger asChild>
|
|
1305
|
+
<Button
|
|
1306
|
+
variant="outline"
|
|
1307
|
+
role="combobox"
|
|
1308
|
+
aria-expanded={open}
|
|
1309
|
+
className="flex-1 min-w-[120px] justify-between font-normal"
|
|
1310
|
+
>
|
|
1311
|
+
{selectedLabel}
|
|
1312
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
1313
|
+
</Button>
|
|
1314
|
+
</PopoverTrigger>
|
|
1315
|
+
<PopoverContent className="w-[200px] p-0" align="start">
|
|
1316
|
+
<Command shouldFilter={false}>
|
|
1317
|
+
<CommandInput placeholder="Search..." onValueChange={search} />
|
|
1318
|
+
<CommandList>
|
|
1319
|
+
{loading ? (
|
|
1320
|
+
<div className="p-4 text-center text-sm text-muted-foreground flex items-center justify-center">
|
|
1321
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1322
|
+
Loading...
|
|
1323
|
+
</div>
|
|
1324
|
+
) : options.length === 0 ? (
|
|
1325
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
1326
|
+
) : (
|
|
1327
|
+
<CommandGroup>
|
|
1328
|
+
{options.map((opt) => (
|
|
1329
|
+
<CommandItem
|
|
1330
|
+
key={opt.value}
|
|
1331
|
+
value={opt.value}
|
|
1332
|
+
onSelect={() => {
|
|
1333
|
+
onChange(opt.value);
|
|
1334
|
+
setOpen(false);
|
|
1335
|
+
}}
|
|
1336
|
+
>
|
|
1337
|
+
<Check className={cn("mr-2 h-4 w-4", value === opt.value ? "opacity-100" : "opacity-0")} />
|
|
1338
|
+
{opt.label}
|
|
1339
|
+
</CommandItem>
|
|
1340
|
+
))}
|
|
1341
|
+
</CommandGroup>
|
|
1342
|
+
)}
|
|
1343
|
+
</CommandList>
|
|
1344
|
+
</Command>
|
|
1345
|
+
</PopoverContent>
|
|
1346
|
+
</Popover>
|
|
1199
1347
|
);
|
|
1200
1348
|
}
|
|
1201
1349
|
`;
|
|
1202
1350
|
|
|
1351
|
+
export function getTemplateFiles(alias: string): Record<string, string> {
|
|
1352
|
+
const replaceAlias = (content: string) => content.replaceAll(ALIAS, alias);
|
|
1203
1353
|
return {
|
|
1204
|
-
"
|
|
1205
|
-
"
|
|
1206
|
-
"
|
|
1207
|
-
"
|
|
1208
|
-
"
|
|
1209
|
-
"
|
|
1210
|
-
"
|
|
1354
|
+
"constants.ts": replaceAlias(TEMPLATE_CONSTANTS_TS),
|
|
1355
|
+
"helpers/operators.ts": replaceAlias(TEMPLATE_HELPERS_OPERATORS_TS),
|
|
1356
|
+
"helpers/query-builder.ts": replaceAlias(TEMPLATE_HELPERS_QUERY_BUILDER_TS),
|
|
1357
|
+
"helpers/serializer.ts": replaceAlias(TEMPLATE_HELPERS_SERIALIZER_TS),
|
|
1358
|
+
"helpers/validators.ts": replaceAlias(TEMPLATE_HELPERS_VALIDATORS_TS),
|
|
1359
|
+
"hooks/use-filter-options.ts": replaceAlias(TEMPLATE_HOOKS_USE_FILTER_OPTIONS_TS),
|
|
1360
|
+
"hooks/use-filter-query.ts": replaceAlias(TEMPLATE_HOOKS_USE_FILTER_QUERY_TS),
|
|
1361
|
+
"hooks/use-filter-state.ts": replaceAlias(TEMPLATE_HOOKS_USE_FILTER_STATE_TS),
|
|
1362
|
+
"hooks/use-filter-url-sync.ts": replaceAlias(TEMPLATE_HOOKS_USE_FILTER_URL_SYNC_TS),
|
|
1363
|
+
"index.ts": replaceAlias(TEMPLATE_INDEX_TS),
|
|
1364
|
+
"provider/filter-context.ts": replaceAlias(TEMPLATE_PROVIDER_FILTER_CONTEXT_TS),
|
|
1365
|
+
"provider/filter-provider.tsx": replaceAlias(TEMPLATE_PROVIDER_FILTER_PROVIDER_TSX),
|
|
1366
|
+
"types.ts": replaceAlias(TEMPLATE_TYPES_TS),
|
|
1367
|
+
"ui/field-select.tsx": replaceAlias(TEMPLATE_UI_FIELD_SELECT_TSX),
|
|
1368
|
+
"ui/filter-badge.tsx": replaceAlias(TEMPLATE_UI_FILTER_BADGE_TSX),
|
|
1369
|
+
"ui/filter-bar.tsx": replaceAlias(TEMPLATE_UI_FILTER_BAR_TSX),
|
|
1370
|
+
"ui/filter-footer.tsx": replaceAlias(TEMPLATE_UI_FILTER_FOOTER_TSX),
|
|
1371
|
+
"ui/filter-root.tsx": replaceAlias(TEMPLATE_UI_FILTER_ROOT_TSX),
|
|
1372
|
+
"ui/filter-row.tsx": replaceAlias(TEMPLATE_UI_FILTER_ROW_TSX),
|
|
1373
|
+
"ui/operator-select.tsx": replaceAlias(TEMPLATE_UI_OPERATOR_SELECT_TSX),
|
|
1374
|
+
"ui/value-input.tsx": replaceAlias(TEMPLATE_UI_VALUE_INPUT_TSX),
|
|
1211
1375
|
};
|
|
1212
1376
|
}
|