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