@jskit-ai/users-web 0.1.58 → 0.1.60
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 +11 -7
- package/package.json +9 -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 +2 -0
- package/test/useCrudAddEdit.test.js +80 -0
- package/test/useCrudListFilterLookups.test.js +114 -0
- package/test/useCrudListFilters.test.js +182 -0
package/package.descriptor.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { HOME_TOOLS_OUTLET } from "./src/shared/toolsOutletContracts.js";
|
|
|
3
3
|
export default Object.freeze({
|
|
4
4
|
packageVersion: 1,
|
|
5
5
|
packageId: "@jskit-ai/users-web",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.60",
|
|
7
7
|
kind: "runtime",
|
|
8
8
|
description: "Users web module: account/profile UI plus shared users web widgets.",
|
|
9
9
|
dependsOn: [
|
|
@@ -66,6 +66,10 @@ export default Object.freeze({
|
|
|
66
66
|
subpath: "./client/composables/useCommand",
|
|
67
67
|
summary: "Exports command operation composable."
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
subpath: "./client/composables/useCrudListFilterLookups",
|
|
71
|
+
summary: "Exports lookup-backed CRUD list filter helper for remote autocomplete filters."
|
|
72
|
+
},
|
|
69
73
|
{
|
|
70
74
|
subpath: "./client/composables/useView",
|
|
71
75
|
summary: "Exports read/view operation composable."
|
|
@@ -142,12 +146,12 @@ export default Object.freeze({
|
|
|
142
146
|
runtime: {
|
|
143
147
|
"@tanstack/vue-query": "5.92.12",
|
|
144
148
|
"@mdi/js": "^7.4.47",
|
|
145
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
146
|
-
"@jskit-ai/realtime": "0.1.
|
|
147
|
-
"@jskit-ai/kernel": "0.1.
|
|
148
|
-
"@jskit-ai/shell-web": "0.1.
|
|
149
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
150
|
-
"@jskit-ai/users-core": "0.1.
|
|
149
|
+
"@jskit-ai/http-runtime": "0.1.44",
|
|
150
|
+
"@jskit-ai/realtime": "0.1.44",
|
|
151
|
+
"@jskit-ai/kernel": "0.1.45",
|
|
152
|
+
"@jskit-ai/shell-web": "0.1.44",
|
|
153
|
+
"@jskit-ai/uploads-image-web": "0.1.23",
|
|
154
|
+
"@jskit-ai/users-core": "0.1.55",
|
|
151
155
|
vuetify: "^4.0.0"
|
|
152
156
|
},
|
|
153
157
|
dev: {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.60",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"./client/composables/useCommand": "./src/client/composables/useCommand.js",
|
|
14
14
|
"./client/composables/useCrudAddEdit": "./src/client/composables/records/useCrudAddEdit.js",
|
|
15
15
|
"./client/composables/crudLookupFieldRuntime": "./src/client/composables/crud/crudLookupFieldRuntime.js",
|
|
16
|
+
"./client/composables/useCrudListFilterLookups": "./src/client/composables/useCrudListFilterLookups.js",
|
|
17
|
+
"./client/composables/useCrudListFilters": "./src/client/composables/useCrudListFilters.js",
|
|
16
18
|
"./client/composables/useList": "./src/client/composables/records/useList.js",
|
|
17
19
|
"./client/composables/useCrudList": "./src/client/composables/records/useCrudList.js",
|
|
18
20
|
"./client/composables/useCrudListParentTitle": "./src/client/composables/useCrudListParentTitle.js",
|
|
@@ -31,12 +33,12 @@
|
|
|
31
33
|
"dependencies": {
|
|
32
34
|
"@tanstack/vue-query": "5.92.12",
|
|
33
35
|
"@mdi/js": "^7.4.47",
|
|
34
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
35
|
-
"@jskit-ai/kernel": "0.1.
|
|
36
|
-
"@jskit-ai/realtime": "0.1.
|
|
37
|
-
"@jskit-ai/shell-web": "0.1.
|
|
38
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
39
|
-
"@jskit-ai/users-core": "0.1.
|
|
36
|
+
"@jskit-ai/http-runtime": "0.1.44",
|
|
37
|
+
"@jskit-ai/kernel": "0.1.45",
|
|
38
|
+
"@jskit-ai/realtime": "0.1.44",
|
|
39
|
+
"@jskit-ai/shell-web": "0.1.44",
|
|
40
|
+
"@jskit-ai/uploads-image-web": "0.1.23",
|
|
41
|
+
"@jskit-ai/users-core": "0.1.55",
|
|
40
42
|
"vuetify": "^4.0.0"
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -48,6 +48,10 @@ function resolveFormFieldFormat(field = {}) {
|
|
|
48
48
|
return String(field.format || "").trim().toLowerCase();
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function isNullableFormField(field = {}) {
|
|
52
|
+
return field?.nullable === true;
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
function padDateTimePart(value) {
|
|
52
56
|
return String(value).padStart(2, "0");
|
|
53
57
|
}
|
|
@@ -132,7 +136,7 @@ function resolveFormFieldInitialValue(field = {}) {
|
|
|
132
136
|
|
|
133
137
|
const fieldType = resolveFormFieldType(field);
|
|
134
138
|
if (fieldType === "boolean") {
|
|
135
|
-
return false;
|
|
139
|
+
return isNullableFormField(field) ? null : false;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
return "";
|
|
@@ -176,7 +180,9 @@ function buildCrudFormPayload(fields = [], model = {}) {
|
|
|
176
180
|
const rawValue = sourceModel[fieldKey];
|
|
177
181
|
|
|
178
182
|
if (fieldType === "boolean") {
|
|
179
|
-
payload[fieldKey] =
|
|
183
|
+
payload[fieldKey] = rawValue == null && isNullableFormField(field)
|
|
184
|
+
? null
|
|
185
|
+
: Boolean(rawValue);
|
|
180
186
|
continue;
|
|
181
187
|
}
|
|
182
188
|
|
|
@@ -258,7 +264,9 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
|
|
|
258
264
|
const rawValue = sourcePayload[fieldKey];
|
|
259
265
|
|
|
260
266
|
if (fieldType === "boolean") {
|
|
261
|
-
targetModel[fieldKey] =
|
|
267
|
+
targetModel[fieldKey] = rawValue == null && isNullableFormField(field)
|
|
268
|
+
? null
|
|
269
|
+
: Boolean(rawValue);
|
|
262
270
|
continue;
|
|
263
271
|
}
|
|
264
272
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { resolveLookupItemLabel } from "../crud/crudLookupFieldLabelSupport.js";
|
|
3
|
+
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
4
|
+
|
|
5
|
+
function normalizeLookupQueryKeyPrefix(value = []) {
|
|
6
|
+
const source = Array.isArray(value) ? value : [];
|
|
7
|
+
return Object.freeze(
|
|
8
|
+
source
|
|
9
|
+
.map((entry) => normalizeText(entry))
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeLookupLabelResolverMap(value = {}) {
|
|
15
|
+
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: {};
|
|
18
|
+
const normalized = {};
|
|
19
|
+
|
|
20
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
21
|
+
const normalizedKey = normalizeText(key);
|
|
22
|
+
if (!normalizedKey || typeof entry !== "function") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
normalized[normalizedKey] = entry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Object.freeze(normalized);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeLookupRequestQueryParamsMap(value = {}) {
|
|
33
|
+
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
34
|
+
? value
|
|
35
|
+
: {};
|
|
36
|
+
const normalized = {};
|
|
37
|
+
|
|
38
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
39
|
+
const normalizedKey = normalizeText(key);
|
|
40
|
+
if (!normalizedKey) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (entry == null) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (typeof entry === "function") {
|
|
47
|
+
normalized[normalizedKey] = entry;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (typeof entry !== "object" || Array.isArray(entry)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
normalized[normalizedKey] = asPlainObject(entry);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.freeze(normalized);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveLookupSelectedValues(filter = {}, rawValue = undefined) {
|
|
61
|
+
if (filter?.type === "recordIdMany") {
|
|
62
|
+
return normalizeUniqueTextList(rawValue, {
|
|
63
|
+
acceptSingle: true
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const normalizedValue = normalizeText(rawValue);
|
|
68
|
+
return normalizedValue ? Object.freeze([normalizedValue]) : Object.freeze([]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createLookupOptionFromItem(item = {}, filter = {}, labelResolver = null) {
|
|
72
|
+
const sourceRecord = asPlainObject(item);
|
|
73
|
+
const valueKey = normalizeText(filter?.lookup?.valueKey) || "id";
|
|
74
|
+
const labelKey = normalizeText(filter?.lookup?.labelKey);
|
|
75
|
+
const value = normalizeText(sourceRecord[valueKey]);
|
|
76
|
+
if (!value) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const customLabel = typeof labelResolver === "function"
|
|
81
|
+
? normalizeText(labelResolver(sourceRecord, filter))
|
|
82
|
+
: "";
|
|
83
|
+
const fallbackLabel = resolveLookupItemLabel(sourceRecord, labelKey) || value;
|
|
84
|
+
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
value,
|
|
87
|
+
label: customLabel || String(fallbackLabel || value),
|
|
88
|
+
record: sourceRecord
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createLookupOptionsFromItems(items = [], filter = {}, labelResolver = null) {
|
|
93
|
+
const optionMap = new Map();
|
|
94
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
95
|
+
const option = createLookupOptionFromItem(item, filter, labelResolver);
|
|
96
|
+
if (!option || optionMap.has(option.value)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
optionMap.set(option.value, option);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Object.freeze([...optionMap.values()]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mergeSelectedLookupOptions(options = [], selectedValues = [], cachedOptions = new Map()) {
|
|
107
|
+
const optionMap = new Map(
|
|
108
|
+
(Array.isArray(options) ? options : []).map((option) => [normalizeText(option?.value), option])
|
|
109
|
+
);
|
|
110
|
+
const normalizedCache = cachedOptions instanceof Map ? cachedOptions : new Map();
|
|
111
|
+
|
|
112
|
+
for (const value of Array.isArray(selectedValues) ? selectedValues : []) {
|
|
113
|
+
const normalizedValue = normalizeText(value);
|
|
114
|
+
if (!normalizedValue || optionMap.has(normalizedValue)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cachedOption = normalizedCache.get(normalizedValue);
|
|
119
|
+
if (cachedOption) {
|
|
120
|
+
optionMap.set(normalizedValue, cachedOption);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Object.freeze(
|
|
125
|
+
[...optionMap.values()].sort((left, right) => {
|
|
126
|
+
return String(left?.label || "").localeCompare(String(right?.label || ""));
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveLookupOptionLabel(options = [], cachedOptions = new Map(), value = "", fallback = "Option") {
|
|
132
|
+
const normalizedValue = normalizeText(value);
|
|
133
|
+
if (!normalizedValue) {
|
|
134
|
+
return normalizeText(fallback) || "Option";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const matchingOption = (Array.isArray(options) ? options : [])
|
|
138
|
+
.find((option) => normalizeText(option?.value) === normalizedValue);
|
|
139
|
+
if (matchingOption?.label) {
|
|
140
|
+
return String(matchingOption.label);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const normalizedCache = cachedOptions instanceof Map ? cachedOptions : new Map();
|
|
144
|
+
const cachedOption = normalizedCache.get(normalizedValue);
|
|
145
|
+
if (cachedOption?.label) {
|
|
146
|
+
return String(cachedOption.label);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const normalizedFallback = normalizeText(fallback) || "Option";
|
|
150
|
+
return `${normalizedFallback} ${normalizedValue}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
normalizeLookupQueryKeyPrefix,
|
|
155
|
+
normalizeLookupLabelResolverMap,
|
|
156
|
+
normalizeLookupRequestQueryParamsMap,
|
|
157
|
+
resolveLookupSelectedValues,
|
|
158
|
+
createLookupOptionsFromItems,
|
|
159
|
+
mergeSelectedLookupOptions,
|
|
160
|
+
resolveLookupOptionLabel
|
|
161
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { computed, proxyRefs, ref, watch } from "vue";
|
|
2
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import {
|
|
4
|
+
defineCrudListFilters,
|
|
5
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID,
|
|
6
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY
|
|
7
|
+
} from "@jskit-ai/kernel/shared/support/crudListFilters";
|
|
8
|
+
import { useList } from "./records/useList.js";
|
|
9
|
+
import {
|
|
10
|
+
normalizeLookupQueryKeyPrefix,
|
|
11
|
+
normalizeLookupLabelResolverMap,
|
|
12
|
+
normalizeLookupRequestQueryParamsMap,
|
|
13
|
+
resolveLookupSelectedValues,
|
|
14
|
+
createLookupOptionsFromItems,
|
|
15
|
+
mergeSelectedLookupOptions,
|
|
16
|
+
resolveLookupOptionLabel
|
|
17
|
+
} from "./internal/crudListFilterLookupSupport.js";
|
|
18
|
+
|
|
19
|
+
function useCrudListFilterLookups(
|
|
20
|
+
definitions = {},
|
|
21
|
+
{
|
|
22
|
+
values = {},
|
|
23
|
+
adapter = null,
|
|
24
|
+
recordIdParam = "recordId",
|
|
25
|
+
queryKeyPrefix = [],
|
|
26
|
+
placementSourcePrefix = "",
|
|
27
|
+
requestQueryParams = {},
|
|
28
|
+
labelResolvers = {}
|
|
29
|
+
} = {}
|
|
30
|
+
) {
|
|
31
|
+
const filters = defineCrudListFilters(definitions);
|
|
32
|
+
const filterEntries = Object.values(filters);
|
|
33
|
+
const normalizedQueryKeyPrefix = normalizeLookupQueryKeyPrefix(queryKeyPrefix);
|
|
34
|
+
const normalizedPlacementSourcePrefix = normalizeText(placementSourcePrefix);
|
|
35
|
+
const normalizedLabelResolvers = normalizeLookupLabelResolverMap(labelResolvers);
|
|
36
|
+
const normalizedRequestQueryParams = normalizeLookupRequestQueryParamsMap(requestQueryParams);
|
|
37
|
+
const lookups = {};
|
|
38
|
+
|
|
39
|
+
for (const filter of filterEntries) {
|
|
40
|
+
if (
|
|
41
|
+
filter.type !== CRUD_LIST_FILTER_TYPE_RECORD_ID &&
|
|
42
|
+
filter.type !== CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY
|
|
43
|
+
) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!filter.lookup?.apiSuffix) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const runtime = useList({
|
|
51
|
+
adapter: adapter || undefined,
|
|
52
|
+
apiSuffix: filter.lookup.apiSuffix,
|
|
53
|
+
queryKeyFactory: (surfaceId = "", scopeParamValue = "") => [
|
|
54
|
+
...normalizedQueryKeyPrefix,
|
|
55
|
+
filter.key,
|
|
56
|
+
String(surfaceId || ""),
|
|
57
|
+
String(scopeParamValue || "")
|
|
58
|
+
],
|
|
59
|
+
search: {
|
|
60
|
+
enabled: true,
|
|
61
|
+
mode: "query"
|
|
62
|
+
},
|
|
63
|
+
...(Object.hasOwn(normalizedRequestQueryParams, filter.key)
|
|
64
|
+
? { requestQueryParams: normalizedRequestQueryParams[filter.key] }
|
|
65
|
+
: {}),
|
|
66
|
+
placementSource: normalizedPlacementSourcePrefix
|
|
67
|
+
? `${normalizedPlacementSourcePrefix}.${filter.key}`
|
|
68
|
+
: `crud.list-filter.lookup.${filter.key}`,
|
|
69
|
+
fallbackLoadError: `Unable to load filter options (${filter.lookup.apiSuffix}).`,
|
|
70
|
+
recordIdParam,
|
|
71
|
+
recordIdSelector: (item = {}) => item[filter.lookup.valueKey || "id"],
|
|
72
|
+
viewUrlTemplate: "",
|
|
73
|
+
editUrlTemplate: ""
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const cachedOptions = ref(new Map());
|
|
77
|
+
const selectedValues = computed(() => resolveLookupSelectedValues(filter, values?.[filter.key]));
|
|
78
|
+
const currentOptions = computed(() => createLookupOptionsFromItems(
|
|
79
|
+
Array.isArray(runtime.items) ? runtime.items : [],
|
|
80
|
+
filter,
|
|
81
|
+
normalizedLabelResolvers[filter.key]
|
|
82
|
+
));
|
|
83
|
+
const options = computed(() => {
|
|
84
|
+
return mergeSelectedLookupOptions(
|
|
85
|
+
currentOptions.value,
|
|
86
|
+
selectedValues.value,
|
|
87
|
+
cachedOptions.value
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
watch(currentOptions, (nextOptions) => {
|
|
92
|
+
const nextCache = new Map(cachedOptions.value);
|
|
93
|
+
for (const option of nextOptions) {
|
|
94
|
+
nextCache.set(option.value, option);
|
|
95
|
+
}
|
|
96
|
+
cachedOptions.value = nextCache;
|
|
97
|
+
}, { immediate: true });
|
|
98
|
+
|
|
99
|
+
lookups[filter.key] = proxyRefs({
|
|
100
|
+
filter,
|
|
101
|
+
options,
|
|
102
|
+
searchQuery: computed(() => String(runtime.searchQuery || "")),
|
|
103
|
+
isLoading: computed(() => {
|
|
104
|
+
return Boolean(runtime.isInitialLoading || runtime.isFetching || runtime.isRefetching || runtime.isSearchDebouncing);
|
|
105
|
+
}),
|
|
106
|
+
resolveLabel(value, fallback = filter.label || "Option") {
|
|
107
|
+
return resolveLookupOptionLabel(options.value, cachedOptions.value, value, fallback);
|
|
108
|
+
},
|
|
109
|
+
setSearch(value = "") {
|
|
110
|
+
runtime.searchQuery = normalizeText(value);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveLookup(filterKey = "") {
|
|
116
|
+
return lookups[normalizeText(filterKey)] || null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
lookups: Object.freeze(lookups),
|
|
121
|
+
resolveLookup,
|
|
122
|
+
resolveLookupItems(filterKey = "") {
|
|
123
|
+
return resolveLookup(filterKey)?.options || [];
|
|
124
|
+
},
|
|
125
|
+
resolveLookupLoading(filterKey = "") {
|
|
126
|
+
return Boolean(resolveLookup(filterKey)?.isLoading);
|
|
127
|
+
},
|
|
128
|
+
resolveLookupSearch(filterKey = "") {
|
|
129
|
+
return String(resolveLookup(filterKey)?.searchQuery || "");
|
|
130
|
+
},
|
|
131
|
+
setLookupSearch(filterKey = "", value = "") {
|
|
132
|
+
resolveLookup(filterKey)?.setSearch(value);
|
|
133
|
+
},
|
|
134
|
+
resolveLookupLabel(filterKey = "", value = "", fallback = "Option") {
|
|
135
|
+
return resolveLookup(filterKey)?.resolveLabel(value, fallback)
|
|
136
|
+
|| resolveLookupOptionLabel([], new Map(), value, fallback);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { useCrudListFilterLookups };
|
|
@@ -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 };
|
|
@@ -20,6 +20,8 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
20
20
|
"./client/composables/useList",
|
|
21
21
|
"./client/composables/useView",
|
|
22
22
|
"./client/composables/useCrudAddEdit",
|
|
23
|
+
"./client/composables/useCrudListFilterLookups",
|
|
24
|
+
"./client/composables/useCrudListFilters",
|
|
23
25
|
"./client/composables/useCrudList",
|
|
24
26
|
"./client/composables/useCrudView"
|
|
25
27
|
]
|
|
@@ -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
|
[
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
resolveLookupSelectedValues,
|
|
5
|
+
createLookupOptionsFromItems,
|
|
6
|
+
mergeSelectedLookupOptions,
|
|
7
|
+
resolveLookupOptionLabel
|
|
8
|
+
} from "../src/client/composables/internal/crudListFilterLookupSupport.js";
|
|
9
|
+
|
|
10
|
+
const supplierFilter = Object.freeze({
|
|
11
|
+
key: "supplierContactId",
|
|
12
|
+
type: "recordIdMany",
|
|
13
|
+
label: "Supplier",
|
|
14
|
+
lookup: Object.freeze({
|
|
15
|
+
apiSuffix: "/contacts",
|
|
16
|
+
valueKey: "id",
|
|
17
|
+
labelKey: "name"
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("lookup filter support normalizes selected record ids for single and many filters", () => {
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
resolveLookupSelectedValues(supplierFilter, ["7", "4", "7", ""]),
|
|
24
|
+
["7", "4"]
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
resolveLookupSelectedValues(
|
|
29
|
+
{
|
|
30
|
+
...supplierFilter,
|
|
31
|
+
type: "recordId"
|
|
32
|
+
},
|
|
33
|
+
12
|
|
34
|
+
),
|
|
35
|
+
["12"]
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("lookup filter support builds option labels from custom resolvers and fallback lookup labels", () => {
|
|
40
|
+
const options = createLookupOptionsFromItems(
|
|
41
|
+
[
|
|
42
|
+
{
|
|
43
|
+
id: 7,
|
|
44
|
+
firstName: "Pollen",
|
|
45
|
+
lastName: "Partners"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 4,
|
|
49
|
+
name: "Harbor Storage"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
supplierFilter,
|
|
53
|
+
(item = {}) => {
|
|
54
|
+
if (item.id === 7) {
|
|
55
|
+
return `${item.firstName} ${item.lastName}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
assert.deepEqual(options, [
|
|
63
|
+
{
|
|
64
|
+
value: "7",
|
|
65
|
+
label: "Pollen Partners",
|
|
66
|
+
record: {
|
|
67
|
+
id: 7,
|
|
68
|
+
firstName: "Pollen",
|
|
69
|
+
lastName: "Partners"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
value: "4",
|
|
74
|
+
label: "Harbor Storage",
|
|
75
|
+
record: {
|
|
76
|
+
id: 4,
|
|
77
|
+
name: "Harbor Storage"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("lookup filter support merges cached selected labels and resolves fallback labels", () => {
|
|
84
|
+
const currentOptions = [
|
|
85
|
+
{
|
|
86
|
+
value: "7",
|
|
87
|
+
label: "Pollen Partners",
|
|
88
|
+
record: {
|
|
89
|
+
id: 7,
|
|
90
|
+
name: "Pollen Partners"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
const cachedOptions = new Map([
|
|
95
|
+
["12", {
|
|
96
|
+
value: "12",
|
|
97
|
+
label: "North Shed",
|
|
98
|
+
record: {
|
|
99
|
+
id: 12,
|
|
100
|
+
name: "North Shed"
|
|
101
|
+
}
|
|
102
|
+
}]
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const mergedOptions = mergeSelectedLookupOptions(
|
|
106
|
+
currentOptions,
|
|
107
|
+
["12", "7"],
|
|
108
|
+
cachedOptions
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(mergedOptions.map((option) => option.value), ["12", "7"]);
|
|
112
|
+
assert.equal(resolveLookupOptionLabel(mergedOptions, cachedOptions, "12", "Storage"), "North Shed");
|
|
113
|
+
assert.equal(resolveLookupOptionLabel(mergedOptions, cachedOptions, "55", "Storage"), "Storage 55");
|
|
114
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
test("useCrudListFilters manages values, query params, chips, and presets", async () => {
|
|
5
|
+
const { useCrudListFilters } = await import("@jskit-ai/users-web/client/composables/useCrudListFilters");
|
|
6
|
+
|
|
7
|
+
const filters = useCrudListFilters(
|
|
8
|
+
{
|
|
9
|
+
onlyStaff: {
|
|
10
|
+
type: "flag",
|
|
11
|
+
label: "Staff"
|
|
12
|
+
},
|
|
13
|
+
status: {
|
|
14
|
+
type: "enumMany",
|
|
15
|
+
label: "Status",
|
|
16
|
+
options: [
|
|
17
|
+
{ value: "active", label: "Active" },
|
|
18
|
+
{ value: "archived", label: "Archived" }
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
supplierContactId: {
|
|
22
|
+
type: "recordIdMany",
|
|
23
|
+
label: "Supplier"
|
|
24
|
+
},
|
|
25
|
+
arrivalDate: {
|
|
26
|
+
type: "dateRange",
|
|
27
|
+
label: "Arrival Date"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
labelResolvers: {
|
|
32
|
+
supplierContactId(value) {
|
|
33
|
+
return value === "7" ? "Pollen Partners" : "";
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
presets: [
|
|
37
|
+
{
|
|
38
|
+
key: "needs-staff-review",
|
|
39
|
+
label: "Needs Staff Review",
|
|
40
|
+
values: {
|
|
41
|
+
onlyStaff: true,
|
|
42
|
+
status: ["archived"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
filters.values.onlyStaff = true;
|
|
50
|
+
filters.values.status = ["active"];
|
|
51
|
+
filters.values.supplierContactId = ["7"];
|
|
52
|
+
filters.values.arrivalDate.from = "2026-04-01";
|
|
53
|
+
|
|
54
|
+
assert.equal(filters.queryParams.onlyStaff.value, true);
|
|
55
|
+
assert.deepEqual(filters.queryParams.status.value, ["active"]);
|
|
56
|
+
assert.equal(filters.queryParams.arrivalDateFrom.value, "2026-04-01");
|
|
57
|
+
assert.equal(filters.hasActiveFilters.value, true);
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
filters.activeChips.value.map((chip) => chip.label),
|
|
60
|
+
[
|
|
61
|
+
"Staff",
|
|
62
|
+
"Status: Active",
|
|
63
|
+
"Supplier: Pollen Partners",
|
|
64
|
+
"Arrival Date: from 2026-04-01"
|
|
65
|
+
]
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
filters.clearChip(filters.activeChips.value.find((chip) => chip.filterKey === "supplierContactId"));
|
|
69
|
+
assert.deepEqual(filters.values.supplierContactId, []);
|
|
70
|
+
|
|
71
|
+
filters.applyPreset("needs-staff-review");
|
|
72
|
+
assert.equal(filters.values.onlyStaff, true);
|
|
73
|
+
assert.deepEqual(filters.values.status, ["archived"]);
|
|
74
|
+
assert.equal(filters.values.arrivalDate.from, "");
|
|
75
|
+
|
|
76
|
+
filters.clearFilters();
|
|
77
|
+
assert.equal(filters.values.onlyStaff, false);
|
|
78
|
+
assert.deepEqual(filters.values.status, []);
|
|
79
|
+
assert.equal(filters.hasActiveFilters.value, false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("useCrudListFilters supports dynamic presets and preset matching", async () => {
|
|
83
|
+
const { useCrudListFilters } = await import("@jskit-ai/users-web/client/composables/useCrudListFilters");
|
|
84
|
+
let today = "2026-04-18";
|
|
85
|
+
|
|
86
|
+
const filters = useCrudListFilters(
|
|
87
|
+
{
|
|
88
|
+
status: {
|
|
89
|
+
type: "enumMany",
|
|
90
|
+
label: "Status",
|
|
91
|
+
options: [
|
|
92
|
+
{ value: "active", label: "Active" },
|
|
93
|
+
{ value: "archived", label: "Archived" }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
arrivalDate: {
|
|
97
|
+
type: "dateRange",
|
|
98
|
+
label: "Arrival Date"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
presets: [
|
|
103
|
+
{
|
|
104
|
+
key: "today",
|
|
105
|
+
label: "Today",
|
|
106
|
+
resolveValues() {
|
|
107
|
+
return {
|
|
108
|
+
arrivalDate: {
|
|
109
|
+
from: today,
|
|
110
|
+
to: today
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "all-dates",
|
|
117
|
+
label: "All Dates",
|
|
118
|
+
values: {
|
|
119
|
+
arrivalDate: {
|
|
120
|
+
from: "",
|
|
121
|
+
to: ""
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
filters.values.status = ["archived"];
|
|
130
|
+
filters.applyPreset("today", { mode: "merge" });
|
|
131
|
+
|
|
132
|
+
assert.equal(filters.values.arrivalDate.from, "2026-04-18");
|
|
133
|
+
assert.equal(filters.values.arrivalDate.to, "2026-04-18");
|
|
134
|
+
assert.deepEqual(filters.values.status, ["archived"]);
|
|
135
|
+
assert.equal(filters.matchesPreset("today"), true);
|
|
136
|
+
assert.equal(filters.matchesPreset("all-dates"), false);
|
|
137
|
+
|
|
138
|
+
today = "2026-04-19";
|
|
139
|
+
assert.equal(filters.matchesPreset("today"), false);
|
|
140
|
+
|
|
141
|
+
filters.applyPreset("all-dates", { mode: "merge" });
|
|
142
|
+
assert.equal(filters.values.arrivalDate.from, "");
|
|
143
|
+
assert.equal(filters.values.arrivalDate.to, "");
|
|
144
|
+
assert.deepEqual(filters.values.status, ["archived"]);
|
|
145
|
+
assert.equal(filters.matchesPreset("all-dates"), true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("useCrudListFilters preset matching rejects extra enumMany values present in current state", async () => {
|
|
149
|
+
const { useCrudListFilters } = await import("@jskit-ai/users-web/client/composables/useCrudListFilters");
|
|
150
|
+
|
|
151
|
+
const filters = useCrudListFilters(
|
|
152
|
+
{
|
|
153
|
+
status: {
|
|
154
|
+
type: "enumMany",
|
|
155
|
+
label: "Status",
|
|
156
|
+
options: [
|
|
157
|
+
{ value: "active", label: "Active" },
|
|
158
|
+
{ value: "archived", label: "Archived" }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
presets: [
|
|
164
|
+
{
|
|
165
|
+
key: "archived-only",
|
|
166
|
+
label: "Archived Only",
|
|
167
|
+
values: {
|
|
168
|
+
status: ["archived"]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
filters.values.status = ["archived", "bogus"];
|
|
176
|
+
|
|
177
|
+
assert.equal(filters.matchesPreset("archived-only"), false);
|
|
178
|
+
assert.deepEqual(
|
|
179
|
+
filters.activeChips.value.map((chip) => chip.label),
|
|
180
|
+
["Status: Archived", "Status: bogus"]
|
|
181
|
+
);
|
|
182
|
+
});
|