@jskit-ai/users-web 0.1.59 → 0.1.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +19 -7
- package/package.json +11 -7
- package/src/client/composables/crud/crudSchemaFormHelpers.js +11 -3
- package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
- package/src/client/composables/useCrudListFilterLookups.js +141 -0
- package/src/client/composables/useCrudListFilters.js +503 -0
- package/test/exportsContract.test.js +7 -1
- package/test/useCrudAddEdit.test.js +80 -0
- package/test/useCrudListFilterLookups.test.js +114 -0
- package/test/useCrudListFilters.test.js +182 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { computed, reactive, toRef } from "vue";
|
|
2
|
+
import { normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import {
|
|
4
|
+
defineCrudListFilters,
|
|
5
|
+
resolveCrudListFilterOptionLabel,
|
|
6
|
+
CRUD_LIST_FILTER_TYPE_FLAG,
|
|
7
|
+
CRUD_LIST_FILTER_TYPE_ENUM,
|
|
8
|
+
CRUD_LIST_FILTER_TYPE_ENUM_MANY,
|
|
9
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID,
|
|
10
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY,
|
|
11
|
+
CRUD_LIST_FILTER_TYPE_DATE,
|
|
12
|
+
CRUD_LIST_FILTER_TYPE_DATE_RANGE,
|
|
13
|
+
CRUD_LIST_FILTER_TYPE_NUMBER_RANGE,
|
|
14
|
+
CRUD_LIST_FILTER_TYPE_PRESENCE
|
|
15
|
+
} from "@jskit-ai/kernel/shared/support/crudListFilters";
|
|
16
|
+
|
|
17
|
+
function normalizeFunctionMap(value = {}) {
|
|
18
|
+
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
19
|
+
? value
|
|
20
|
+
: {};
|
|
21
|
+
const normalized = {};
|
|
22
|
+
|
|
23
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
24
|
+
const normalizedKey = normalizeText(key);
|
|
25
|
+
if (!normalizedKey || typeof entry !== "function") {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
normalized[normalizedKey] = entry;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Object.freeze(normalized);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createInitialFilterValue(filter = {}) {
|
|
36
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
43
|
+
return reactive({
|
|
44
|
+
from: "",
|
|
45
|
+
to: ""
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
49
|
+
return reactive({
|
|
50
|
+
min: "",
|
|
51
|
+
max: ""
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizePresetEntries(presets = []) {
|
|
59
|
+
const source = Array.isArray(presets) ? presets : [];
|
|
60
|
+
const normalized = [];
|
|
61
|
+
const seenKeys = new Set();
|
|
62
|
+
|
|
63
|
+
for (const rawPreset of source) {
|
|
64
|
+
if (!rawPreset || typeof rawPreset !== "object" || Array.isArray(rawPreset)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const key = normalizeText(rawPreset.key);
|
|
69
|
+
const label = normalizeText(rawPreset.label);
|
|
70
|
+
if (!key || !label || seenKeys.has(key)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
seenKeys.add(key);
|
|
74
|
+
|
|
75
|
+
const values = rawPreset.values && typeof rawPreset.values === "object" && !Array.isArray(rawPreset.values)
|
|
76
|
+
? rawPreset.values
|
|
77
|
+
: {};
|
|
78
|
+
const resolveValues = typeof rawPreset.resolveValues === "function"
|
|
79
|
+
? rawPreset.resolveValues
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
normalized.push(Object.freeze({
|
|
83
|
+
key,
|
|
84
|
+
label,
|
|
85
|
+
values,
|
|
86
|
+
resolveValues
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Object.freeze(normalized);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolvePresetValues(preset = {}, { values = {}, filters = {} } = {}) {
|
|
94
|
+
const rawValues = typeof preset.resolveValues === "function"
|
|
95
|
+
? preset.resolveValues({
|
|
96
|
+
values,
|
|
97
|
+
filters,
|
|
98
|
+
presetKey: preset.key,
|
|
99
|
+
preset
|
|
100
|
+
})
|
|
101
|
+
: preset.values;
|
|
102
|
+
|
|
103
|
+
return rawValues && typeof rawValues === "object" && !Array.isArray(rawValues)
|
|
104
|
+
? rawValues
|
|
105
|
+
: {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizePresetFilterValue(filter = {}, rawValue) {
|
|
109
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
110
|
+
return rawValue === true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
114
|
+
const allowedValues = filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY
|
|
115
|
+
? new Set((filter.options || []).map((entry) => entry.value))
|
|
116
|
+
: null;
|
|
117
|
+
const normalizedList = normalizeUniqueTextList(rawValue, {
|
|
118
|
+
acceptSingle: true
|
|
119
|
+
});
|
|
120
|
+
if (!allowedValues) {
|
|
121
|
+
return normalizedList;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return normalizedList.filter((entry) => allowedValues.has(entry));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
128
|
+
const source = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)
|
|
129
|
+
? rawValue
|
|
130
|
+
: {};
|
|
131
|
+
return {
|
|
132
|
+
from: normalizeText(source.from),
|
|
133
|
+
to: normalizeText(source.to)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
138
|
+
const source = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)
|
|
139
|
+
? rawValue
|
|
140
|
+
: {};
|
|
141
|
+
return {
|
|
142
|
+
min: normalizeText(source.min),
|
|
143
|
+
max: normalizeText(source.max)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const normalized = normalizeText(rawValue);
|
|
148
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM || filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE) {
|
|
149
|
+
const allowedValues = new Set((filter.options || []).map((entry) => entry.value));
|
|
150
|
+
return allowedValues.has(normalized) ? normalized : "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeCurrentManyFilterValues(value) {
|
|
157
|
+
const source = Array.isArray(value) ? value : [value];
|
|
158
|
+
return source
|
|
159
|
+
.map((entry) => normalizeText(entry))
|
|
160
|
+
.filter(Boolean);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function matchArrayValues(currentValue = [], expectedValue = []) {
|
|
164
|
+
const currentList = Array.isArray(currentValue) ? [...currentValue].sort() : [];
|
|
165
|
+
const expectedList = Array.isArray(expectedValue) ? [...expectedValue].sort() : [];
|
|
166
|
+
if (currentList.length !== expectedList.length) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return currentList.every((entry, index) => entry === expectedList[index]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function matchesPresetFilterValue(filter = {}, currentValue, rawExpectedValue) {
|
|
174
|
+
const expectedValue = normalizePresetFilterValue(filter, rawExpectedValue);
|
|
175
|
+
|
|
176
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
177
|
+
return matchArrayValues(normalizeCurrentManyFilterValues(currentValue), expectedValue);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const normalizedCurrentValue = normalizePresetFilterValue(filter, currentValue);
|
|
181
|
+
|
|
182
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
183
|
+
return (
|
|
184
|
+
normalizeText(normalizedCurrentValue?.from) === normalizeText(expectedValue?.from) &&
|
|
185
|
+
normalizeText(normalizedCurrentValue?.to) === normalizeText(expectedValue?.to)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
190
|
+
return (
|
|
191
|
+
normalizeText(normalizedCurrentValue?.min) === normalizeText(expectedValue?.min) &&
|
|
192
|
+
normalizeText(normalizedCurrentValue?.max) === normalizeText(expectedValue?.max)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return normalizedCurrentValue === expectedValue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resetFilterValue(values, filter = {}) {
|
|
200
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
201
|
+
values[filter.key] = false;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
206
|
+
values[filter.key] = [];
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
211
|
+
values[filter.key].from = "";
|
|
212
|
+
values[filter.key].to = "";
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
217
|
+
values[filter.key].min = "";
|
|
218
|
+
values[filter.key].max = "";
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
values[filter.key] = "";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function applyPresetFilterValue(values, filter = {}, rawValue) {
|
|
226
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE || filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
227
|
+
const nextValue = normalizePresetFilterValue(filter, rawValue);
|
|
228
|
+
Object.assign(values[filter.key], nextValue);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
values[filter.key] = normalizePresetFilterValue(filter, rawValue);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function createQueryParams(values, filterEntries = []) {
|
|
236
|
+
const queryParams = {};
|
|
237
|
+
|
|
238
|
+
for (const filter of filterEntries) {
|
|
239
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
240
|
+
queryParams[filter.fromKey] = toRef(values[filter.key], "from");
|
|
241
|
+
queryParams[filter.toKey] = toRef(values[filter.key], "to");
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
246
|
+
queryParams[filter.minKey] = toRef(values[filter.key], "min");
|
|
247
|
+
queryParams[filter.maxKey] = toRef(values[filter.key], "max");
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
queryParams[filter.queryKey] = toRef(values, filter.key);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return Object.freeze(queryParams);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function resolveAtomicValueLabel(filter = {}, value = "", labelResolvers = {}) {
|
|
258
|
+
const customResolver = labelResolvers[filter.key];
|
|
259
|
+
if (typeof customResolver === "function") {
|
|
260
|
+
const customLabel = normalizeText(customResolver(value, filter));
|
|
261
|
+
if (customLabel) {
|
|
262
|
+
return customLabel;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return resolveCrudListFilterOptionLabel(filter, value, {
|
|
267
|
+
fallback: String(value || "")
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function defaultChipLabel(filter = {}, value, labelResolvers = {}) {
|
|
272
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
273
|
+
return filter.label;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
filter.type === CRUD_LIST_FILTER_TYPE_ENUM ||
|
|
278
|
+
filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE ||
|
|
279
|
+
filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID
|
|
280
|
+
) {
|
|
281
|
+
return `${filter.label}: ${resolveAtomicValueLabel(filter, value, labelResolvers)}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
|
|
285
|
+
return `${filter.label}: ${value}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
289
|
+
if (value?.from && value?.to) {
|
|
290
|
+
return `${filter.label}: ${value.from} to ${value.to}`;
|
|
291
|
+
}
|
|
292
|
+
if (value?.from) {
|
|
293
|
+
return `${filter.label}: from ${value.from}`;
|
|
294
|
+
}
|
|
295
|
+
return `${filter.label}: to ${value?.to || ""}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
299
|
+
if (value?.min && value?.max) {
|
|
300
|
+
return `${filter.label}: ${value.min} to ${value.max}`;
|
|
301
|
+
}
|
|
302
|
+
if (value?.min) {
|
|
303
|
+
return `${filter.label}: min ${value.min}`;
|
|
304
|
+
}
|
|
305
|
+
return `${filter.label}: max ${value?.max || ""}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return filter.label;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function useCrudListFilters(definitions = {}, { labelResolvers = {}, chipLabels = {}, presets = [] } = {}) {
|
|
312
|
+
const filters = defineCrudListFilters(definitions);
|
|
313
|
+
const filterEntries = Object.values(filters);
|
|
314
|
+
const normalizedLabelResolvers = normalizeFunctionMap(labelResolvers);
|
|
315
|
+
const normalizedChipLabels = normalizeFunctionMap(chipLabels);
|
|
316
|
+
const normalizedPresets = normalizePresetEntries(presets);
|
|
317
|
+
const values = reactive({});
|
|
318
|
+
const options = {};
|
|
319
|
+
|
|
320
|
+
for (const filter of filterEntries) {
|
|
321
|
+
values[filter.key] = createInitialFilterValue(filter);
|
|
322
|
+
if (Array.isArray(filter.options) && filter.options.length > 0) {
|
|
323
|
+
options[filter.key] = filter.options;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const queryParams = createQueryParams(values, filterEntries);
|
|
328
|
+
|
|
329
|
+
const activeChips = computed(() => {
|
|
330
|
+
const chips = [];
|
|
331
|
+
|
|
332
|
+
for (const filter of filterEntries) {
|
|
333
|
+
const customChipLabel = normalizedChipLabels[filter.key];
|
|
334
|
+
const rawValue = values[filter.key];
|
|
335
|
+
|
|
336
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
337
|
+
if (rawValue === true) {
|
|
338
|
+
chips.push({
|
|
339
|
+
id: filter.key,
|
|
340
|
+
filterKey: filter.key,
|
|
341
|
+
label: normalizeText(customChipLabel?.(rawValue, filter, values))
|
|
342
|
+
|| normalizeText(filter.chipLabel?.(rawValue, filter, values))
|
|
343
|
+
|| defaultChipLabel(filter, rawValue, normalizedLabelResolvers)
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
350
|
+
for (const value of Array.isArray(rawValue) ? rawValue : []) {
|
|
351
|
+
chips.push({
|
|
352
|
+
id: `${filter.key}:${value}`,
|
|
353
|
+
filterKey: filter.key,
|
|
354
|
+
value,
|
|
355
|
+
label: normalizeText(customChipLabel?.(value, filter, values))
|
|
356
|
+
|| normalizeText(filter.chipLabel?.(value, filter, values))
|
|
357
|
+
|| `${filter.label}: ${resolveAtomicValueLabel(filter, value, normalizedLabelResolvers)}`
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE || filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
364
|
+
const hasValue = Boolean(rawValue?.from || rawValue?.to || rawValue?.min || rawValue?.max);
|
|
365
|
+
if (!hasValue) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
chips.push({
|
|
370
|
+
id: filter.key,
|
|
371
|
+
filterKey: filter.key,
|
|
372
|
+
label: normalizeText(customChipLabel?.(rawValue, filter, values))
|
|
373
|
+
|| normalizeText(filter.chipLabel?.(rawValue, filter, values))
|
|
374
|
+
|| defaultChipLabel(filter, rawValue, normalizedLabelResolvers)
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!normalizeText(rawValue)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
chips.push({
|
|
384
|
+
id: filter.key,
|
|
385
|
+
filterKey: filter.key,
|
|
386
|
+
label: normalizeText(customChipLabel?.(rawValue, filter, values))
|
|
387
|
+
|| normalizeText(filter.chipLabel?.(rawValue, filter, values))
|
|
388
|
+
|| defaultChipLabel(filter, rawValue, normalizedLabelResolvers)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return chips;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const hasActiveFilters = computed(() => activeChips.value.length > 0);
|
|
396
|
+
|
|
397
|
+
function clearFilter(filterKey = "") {
|
|
398
|
+
const filter = filters[normalizeText(filterKey)];
|
|
399
|
+
if (!filter) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
resetFilterValue(values, filter);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function clearFilters() {
|
|
407
|
+
for (const filter of filterEntries) {
|
|
408
|
+
resetFilterValue(values, filter);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function clearChip(chip = {}) {
|
|
413
|
+
const filter = filters[normalizeText(chip.filterKey)];
|
|
414
|
+
if (!filter) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
419
|
+
values[filter.key] = (Array.isArray(values[filter.key]) ? values[filter.key] : [])
|
|
420
|
+
.filter((entry) => entry !== chip.value);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
resetFilterValue(values, filter);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function toggle(filterKey = "") {
|
|
428
|
+
const filter = filters[normalizeText(filterKey)];
|
|
429
|
+
if (!filter || filter.type !== CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
values[filter.key] = !values[filter.key];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function applyPreset(presetKey = "", { mode = "replace" } = {}) {
|
|
437
|
+
const preset = normalizedPresets.find((entry) => entry.key === normalizeText(presetKey));
|
|
438
|
+
if (!preset) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const presetValues = resolvePresetValues(preset, {
|
|
443
|
+
values,
|
|
444
|
+
filters
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (mode !== "merge") {
|
|
448
|
+
clearFilters();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (const filter of filterEntries) {
|
|
452
|
+
if (!Object.hasOwn(presetValues, filter.key)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
applyPresetFilterValue(values, filter, presetValues[filter.key]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function matchesPreset(presetKey = "") {
|
|
461
|
+
const preset = normalizedPresets.find((entry) => entry.key === normalizeText(presetKey));
|
|
462
|
+
if (!preset) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const presetValues = resolvePresetValues(preset, {
|
|
467
|
+
values,
|
|
468
|
+
filters
|
|
469
|
+
});
|
|
470
|
+
let matchedFilter = false;
|
|
471
|
+
|
|
472
|
+
for (const filter of filterEntries) {
|
|
473
|
+
if (!Object.hasOwn(presetValues, filter.key)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
matchedFilter = true;
|
|
478
|
+
if (!matchesPresetFilterValue(filter, values[filter.key], presetValues[filter.key])) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return matchedFilter;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return Object.freeze({
|
|
487
|
+
filters,
|
|
488
|
+
values,
|
|
489
|
+
queryParams,
|
|
490
|
+
options: Object.freeze(options),
|
|
491
|
+
presets: normalizedPresets,
|
|
492
|
+
activeChips,
|
|
493
|
+
hasActiveFilters,
|
|
494
|
+
clearFilter,
|
|
495
|
+
clearFilters,
|
|
496
|
+
clearChip,
|
|
497
|
+
toggle,
|
|
498
|
+
applyPreset,
|
|
499
|
+
matchesPreset
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export { useCrudListFilters };
|
|
@@ -17,11 +17,17 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
17
17
|
"./client",
|
|
18
18
|
"./client/account-settings/sections",
|
|
19
19
|
"./client/composables/useAddEdit",
|
|
20
|
+
"./client/composables/useCommand",
|
|
21
|
+
"./client/composables/useEndpointResource",
|
|
20
22
|
"./client/composables/useList",
|
|
23
|
+
"./client/composables/usePaths",
|
|
21
24
|
"./client/composables/useView",
|
|
22
25
|
"./client/composables/useCrudAddEdit",
|
|
26
|
+
"./client/composables/useCrudListFilterLookups",
|
|
27
|
+
"./client/composables/useCrudListFilters",
|
|
23
28
|
"./client/composables/useCrudList",
|
|
24
|
-
"./client/composables/useCrudView"
|
|
29
|
+
"./client/composables/useCrudView",
|
|
30
|
+
"./client/lib/httpClient"
|
|
25
31
|
]
|
|
26
32
|
});
|
|
27
33
|
|
|
@@ -29,16 +29,30 @@ test("createCrudFormModel resolves defaults and supports explicit initial values
|
|
|
29
29
|
const model = createCrudFormModel([
|
|
30
30
|
{ key: "name", type: "string" },
|
|
31
31
|
{ key: "active", type: "boolean" },
|
|
32
|
+
{ key: "reviewed", type: "boolean", nullable: true },
|
|
32
33
|
{ key: "role", type: "string", initialValue: "member" }
|
|
33
34
|
]);
|
|
34
35
|
|
|
35
36
|
assert.deepEqual(model, {
|
|
36
37
|
name: "",
|
|
37
38
|
active: false,
|
|
39
|
+
reviewed: null,
|
|
38
40
|
role: "member"
|
|
39
41
|
});
|
|
40
42
|
});
|
|
41
43
|
|
|
44
|
+
test("createCrudFormModel preserves explicit boolean defaults for nullable fields", () => {
|
|
45
|
+
const model = createCrudFormModel([
|
|
46
|
+
{ key: "reviewed", type: "boolean", nullable: true, initialValue: false },
|
|
47
|
+
{ key: "approved", type: "boolean", nullable: true, defaultValue: true }
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
assert.deepEqual(model, {
|
|
51
|
+
reviewed: false,
|
|
52
|
+
approved: true
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
42
56
|
test("buildCrudFormPayload normalizes booleans and numbers while skipping empty numeric values", () => {
|
|
43
57
|
const payload = buildCrudFormPayload(
|
|
44
58
|
[
|
|
@@ -100,12 +114,14 @@ test("buildCrudFormPayload normalizes time fields to canonical HH:MM", () => {
|
|
|
100
114
|
test("buildCrudFormPayload serializes cleared nullable typed fields as null", () => {
|
|
101
115
|
const payload = buildCrudFormPayload(
|
|
102
116
|
[
|
|
117
|
+
{ key: "reviewed", type: "boolean", nullable: true },
|
|
103
118
|
{ key: "serviceId", type: "integer", nullable: true },
|
|
104
119
|
{ key: "fromDate", type: "string", format: "date", nullable: true },
|
|
105
120
|
{ key: "scheduledAt", type: "string", format: "date-time", nullable: true },
|
|
106
121
|
{ key: "fromTime", type: "string", format: "time", nullable: true }
|
|
107
122
|
],
|
|
108
123
|
{
|
|
124
|
+
reviewed: null,
|
|
109
125
|
serviceId: null,
|
|
110
126
|
fromDate: "",
|
|
111
127
|
scheduledAt: "",
|
|
@@ -114,6 +130,7 @@ test("buildCrudFormPayload serializes cleared nullable typed fields as null", ()
|
|
|
114
130
|
);
|
|
115
131
|
|
|
116
132
|
assert.deepEqual(payload, {
|
|
133
|
+
reviewed: null,
|
|
117
134
|
serviceId: null,
|
|
118
135
|
fromDate: null,
|
|
119
136
|
scheduledAt: null,
|
|
@@ -121,6 +138,40 @@ test("buildCrudFormPayload serializes cleared nullable typed fields as null", ()
|
|
|
121
138
|
});
|
|
122
139
|
});
|
|
123
140
|
|
|
141
|
+
test("buildCrudFormPayload preserves nullable booleans while keeping non-nullable booleans binary", () => {
|
|
142
|
+
const fields = [
|
|
143
|
+
{ key: "active", type: "boolean" },
|
|
144
|
+
{ key: "reviewed", type: "boolean", nullable: true },
|
|
145
|
+
{ key: "approved", type: "boolean", nullable: true }
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(
|
|
149
|
+
buildCrudFormPayload(fields, {
|
|
150
|
+
active: null,
|
|
151
|
+
reviewed: null,
|
|
152
|
+
approved: true
|
|
153
|
+
}),
|
|
154
|
+
{
|
|
155
|
+
active: false,
|
|
156
|
+
reviewed: null,
|
|
157
|
+
approved: true
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(
|
|
162
|
+
buildCrudFormPayload(fields, {
|
|
163
|
+
active: 1,
|
|
164
|
+
reviewed: 0,
|
|
165
|
+
approved: false
|
|
166
|
+
}),
|
|
167
|
+
{
|
|
168
|
+
active: true,
|
|
169
|
+
reviewed: false,
|
|
170
|
+
approved: false
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
124
175
|
test("applyCrudPayloadToForm normalizes time fields for form inputs", () => {
|
|
125
176
|
const fields = [
|
|
126
177
|
{ key: "fromTime", type: "string", format: "time" },
|
|
@@ -146,18 +197,21 @@ test("applyCrudPayloadToForm maps payload values into reactive form model", () =
|
|
|
146
197
|
const form = reactive({
|
|
147
198
|
name: "",
|
|
148
199
|
active: false,
|
|
200
|
+
reviewed: null,
|
|
149
201
|
age: ""
|
|
150
202
|
});
|
|
151
203
|
applyCrudPayloadToForm(
|
|
152
204
|
[
|
|
153
205
|
{ key: "name", type: "string" },
|
|
154
206
|
{ key: "active", type: "boolean" },
|
|
207
|
+
{ key: "reviewed", type: "boolean", nullable: true },
|
|
155
208
|
{ key: "age", type: "integer" }
|
|
156
209
|
],
|
|
157
210
|
form,
|
|
158
211
|
{
|
|
159
212
|
name: "Grace",
|
|
160
213
|
active: 1,
|
|
214
|
+
reviewed: null,
|
|
161
215
|
age: 33
|
|
162
216
|
}
|
|
163
217
|
);
|
|
@@ -165,10 +219,36 @@ test("applyCrudPayloadToForm maps payload values into reactive form model", () =
|
|
|
165
219
|
assert.deepEqual(form, {
|
|
166
220
|
name: "Grace",
|
|
167
221
|
active: true,
|
|
222
|
+
reviewed: null,
|
|
168
223
|
age: "33"
|
|
169
224
|
});
|
|
170
225
|
});
|
|
171
226
|
|
|
227
|
+
test("applyCrudPayloadToForm preserves nullable boolean payload values", () => {
|
|
228
|
+
const fields = [
|
|
229
|
+
{ key: "active", type: "boolean" },
|
|
230
|
+
{ key: "reviewed", type: "boolean", nullable: true },
|
|
231
|
+
{ key: "approved", type: "boolean", nullable: true }
|
|
232
|
+
];
|
|
233
|
+
const form = reactive({
|
|
234
|
+
active: false,
|
|
235
|
+
reviewed: null,
|
|
236
|
+
approved: null
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
applyCrudPayloadToForm(fields, form, {
|
|
240
|
+
active: null,
|
|
241
|
+
reviewed: null,
|
|
242
|
+
approved: true
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
assert.deepEqual(form, {
|
|
246
|
+
active: false,
|
|
247
|
+
reviewed: null,
|
|
248
|
+
approved: true
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
172
252
|
test("resolveCrudRouteBoundFieldValues maps route params for route-bound form fields", () => {
|
|
173
253
|
const values = resolveCrudRouteBoundFieldValues(
|
|
174
254
|
[
|