@jskit-ai/http-web 0.1.1
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/fixtures/crud-list-date-filters/App.vue +107 -0
- package/fixtures/crud-list-date-filters/index.html +12 -0
- package/fixtures/crud-list-date-filters/main.js +24 -0
- package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
- package/package.json +150 -0
- package/src/client/bulkActions.js +47 -0
- package/src/client/components/CrudAddEditScreen.vue +186 -0
- package/src/client/components/CrudDeleteAction.vue +93 -0
- package/src/client/components/CrudListBulkActionSurface.vue +126 -0
- package/src/client/components/CrudListDateFilterControl.vue +188 -0
- package/src/client/components/CrudListFilterSurface.vue +388 -0
- package/src/client/components/CrudListRecordActionMenu.vue +120 -0
- package/src/client/components/CrudListScreen.vue +501 -0
- package/src/client/components/CrudViewScreen.vue +197 -0
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
- package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
- package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
- package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
- package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
- package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
- package/src/client/composables/internal/useOperationScope.js +144 -0
- package/src/client/composables/records/useAddEdit.js +224 -0
- package/src/client/composables/records/useCrudAddEdit.js +227 -0
- package/src/client/composables/records/useCrudList.js +101 -0
- package/src/client/composables/records/useCrudView.js +47 -0
- package/src/client/composables/records/useList.js +500 -0
- package/src/client/composables/records/useView.js +164 -0
- package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
- package/src/client/composables/runtime/listUiRuntime.js +123 -0
- package/src/client/composables/runtime/modelStateHelpers.js +90 -0
- package/src/client/composables/runtime/operationAdapters.js +34 -0
- package/src/client/composables/runtime/operationUiHelpers.js +125 -0
- package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
- package/src/client/composables/runtime/useAddEditCore.js +112 -0
- package/src/client/composables/runtime/useCommandCore.js +123 -0
- package/src/client/composables/runtime/useEndpointResource.js +199 -0
- package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
- package/src/client/composables/runtime/useListCore.js +112 -0
- package/src/client/composables/runtime/useUiFeedback.js +98 -0
- package/src/client/composables/runtime/useViewCore.js +93 -0
- package/src/client/composables/runtime/viewUiRuntime.js +96 -0
- package/src/client/composables/support/errorMessageHelpers.js +66 -0
- package/src/client/composables/support/listQueryParamSupport.js +473 -0
- package/src/client/composables/support/listSearchSupport.js +70 -0
- package/src/client/composables/support/refValueHelpers.js +19 -0
- package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
- package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
- package/src/client/composables/support/routeTemplateHelpers.js +285 -0
- package/src/client/composables/support/scopeHelpers.js +145 -0
- package/src/client/composables/useAccess.js +109 -0
- package/src/client/composables/useCommand.js +118 -0
- package/src/client/composables/useCrudAddEditScreen.js +88 -0
- package/src/client/composables/useCrudDeleteAction.js +160 -0
- package/src/client/composables/useCrudListBulkActions.js +147 -0
- package/src/client/composables/useCrudListFilterLookups.js +146 -0
- package/src/client/composables/useCrudListFilters.js +339 -0
- package/src/client/composables/useCrudListParentTitle.js +151 -0
- package/src/client/composables/useCrudListRowActions.js +144 -0
- package/src/client/composables/useCrudListScreen.js +237 -0
- package/src/client/composables/useCrudViewScreen.js +77 -0
- package/src/client/composables/usePagedCollection.js +181 -0
- package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
- package/src/client/composables/useScopeRuntime.js +135 -0
- package/src/client/filters.js +15 -0
- package/src/client/index.js +24 -0
- package/src/client/lib/httpClient.js +47 -0
- package/src/client/lib/permissions.js +34 -0
- package/src/client/rowActions.js +47 -0
- package/src/client/support/contractGuards.js +34 -0
- package/src/client/support/crudListDateFilterSupport.js +91 -0
- package/test/addEditUiRuntime.test.js +110 -0
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudHttpClientSupport.test.js +77 -0
- package/test/crudJsonApiTransportSupport.test.js +179 -0
- package/test/crudListBulkActionSurface.test.js +27 -0
- package/test/crudListDateFilterSupport.test.js +92 -0
- package/test/crudListDateFilterSurface.browser.test.js +182 -0
- package/test/crudListFilterSurface.test.js +90 -0
- package/test/crudLookupFieldRuntime.test.js +264 -0
- package/test/crudScreenComponents.test.js +110 -0
- package/test/errorIntentContract.test.js +31 -0
- package/test/errorMessageHelpers.test.js +28 -0
- package/test/exportsContract.test.js +68 -0
- package/test/listQueryParamSupport.test.js +223 -0
- package/test/listUiRuntime.test.js +102 -0
- package/test/operationValidationHelpers.test.js +64 -0
- package/test/packageBoundary.test.js +31 -0
- package/test/permissions.test.js +35 -0
- package/test/refValueHelpers.test.js +14 -0
- package/test/requestTransportOptions.test.js +457 -0
- package/test/resourceLoadStateHelpers.test.js +189 -0
- package/test/routeTemplateHelpers.test.js +85 -0
- package/test/scopeHelpers.test.js +57 -0
- package/test/useAddEditCore.test.js +124 -0
- package/test/useAddEditRequestQueryParams.test.js +190 -0
- package/test/useCrudAddEdit.test.js +415 -0
- package/test/useCrudDeleteAction.test.js +139 -0
- package/test/useCrudListBulkActions.test.js +65 -0
- package/test/useCrudListFilterLookups.test.js +114 -0
- package/test/useCrudListFilters.test.js +293 -0
- package/test/useCrudListParentTitle.test.js +262 -0
- package/test/useCrudListRowActions.test.js +77 -0
- package/test/useListRouteInitialization.test.js +337 -0
- package/test/useListSearchSupport.test.js +61 -0
- package/test/usePagedCollection.test.js +53 -0
- package/test/useViewRequestQueryParams.test.js +20 -0
- package/test/viewCoreLoading.test.js +44 -0
- package/test/viewUiRuntime.test.js +95 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeRouteParamName,
|
|
5
|
+
resolveRouteParamsSource,
|
|
6
|
+
resolveScopedRoutePathname,
|
|
7
|
+
resolveRouteTemplateLocation,
|
|
8
|
+
toRouteParamValue
|
|
9
|
+
} from "../support/routeTemplateHelpers.js";
|
|
10
|
+
|
|
11
|
+
function resolveRecordId({ routeParams, recordIdParam, routeRecordId }) {
|
|
12
|
+
const explicitRecordId = toRouteParamValue(
|
|
13
|
+
typeof routeRecordId === "function" ? routeRecordId() : unref(routeRecordId)
|
|
14
|
+
);
|
|
15
|
+
if (explicitRecordId) {
|
|
16
|
+
return explicitRecordId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return toRouteParamValue(routeParams[recordIdParam]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createViewUiRuntime({
|
|
23
|
+
recordIdParam = "recordId",
|
|
24
|
+
routeParams = null,
|
|
25
|
+
routeParamNames = null,
|
|
26
|
+
routePath = "",
|
|
27
|
+
routeRecordId = null,
|
|
28
|
+
apiUrlTemplate = "",
|
|
29
|
+
listUrlTemplate = "",
|
|
30
|
+
editUrlTemplate = ""
|
|
31
|
+
} = {}) {
|
|
32
|
+
const normalizedRecordIdParam = normalizeRouteParamName(recordIdParam, {
|
|
33
|
+
context: "useView recordIdParam"
|
|
34
|
+
});
|
|
35
|
+
const normalizedApiUrlTemplate = String(apiUrlTemplate || "").trim();
|
|
36
|
+
const normalizedListUrlTemplate = String(listUrlTemplate || "").trim();
|
|
37
|
+
const normalizedEditUrlTemplate = String(editUrlTemplate || "").trim();
|
|
38
|
+
|
|
39
|
+
function resolveTemplatePath(urlTemplate = "", extraParams = {}) {
|
|
40
|
+
const normalizedTemplate = String(urlTemplate || "").trim();
|
|
41
|
+
if (!normalizedTemplate) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const currentRouteParams = resolveRouteParamsSource(routeParams);
|
|
46
|
+
const sourceParams = {
|
|
47
|
+
...currentRouteParams,
|
|
48
|
+
...asPlainObject(extraParams)
|
|
49
|
+
};
|
|
50
|
+
const resolvedRecordId = toRouteParamValue(sourceParams[normalizedRecordIdParam]) ||
|
|
51
|
+
resolveRecordId({
|
|
52
|
+
routeParams: currentRouteParams,
|
|
53
|
+
recordIdParam: normalizedRecordIdParam,
|
|
54
|
+
routeRecordId
|
|
55
|
+
});
|
|
56
|
+
sourceParams[normalizedRecordIdParam] = resolvedRecordId;
|
|
57
|
+
const currentPathname = resolveScopedRoutePathname({
|
|
58
|
+
currentPathname: routePath,
|
|
59
|
+
params: currentRouteParams,
|
|
60
|
+
orderedParamNames: routeParamNames,
|
|
61
|
+
anchorParamName: normalizedRecordIdParam,
|
|
62
|
+
anchorParamValue: resolvedRecordId,
|
|
63
|
+
anchorMode: "at"
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return resolveRouteTemplateLocation(normalizedTemplate, {
|
|
67
|
+
params: sourceParams,
|
|
68
|
+
currentPathname
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const recordId = computed(() =>
|
|
73
|
+
resolveRecordId({
|
|
74
|
+
routeParams: resolveRouteParamsSource(routeParams),
|
|
75
|
+
recordIdParam: normalizedRecordIdParam,
|
|
76
|
+
routeRecordId
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
const apiSuffix = computed(() => resolveTemplatePath(normalizedApiUrlTemplate));
|
|
80
|
+
const listUrl = computed(() => resolveTemplatePath(normalizedListUrlTemplate));
|
|
81
|
+
const editUrl = computed(() => resolveTemplatePath(normalizedEditUrlTemplate));
|
|
82
|
+
|
|
83
|
+
function resolveParams(urlTemplate = "", extraParams = {}) {
|
|
84
|
+
return resolveTemplatePath(urlTemplate, extraParams);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
recordId,
|
|
89
|
+
apiSuffix,
|
|
90
|
+
listUrl,
|
|
91
|
+
editUrl,
|
|
92
|
+
resolveParams
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { createViewUiRuntime };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function normalizeErrorStatus(error) {
|
|
2
|
+
if (error == null || typeof error !== "object") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const hasStatus = Object.prototype.hasOwnProperty.call(error, "status");
|
|
7
|
+
const hasStatusCode = Object.prototype.hasOwnProperty.call(error, "statusCode");
|
|
8
|
+
if (!hasStatus && !hasStatusCode) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const status = Number(hasStatus ? error.status : error.statusCode);
|
|
13
|
+
return Number.isInteger(status) ? status : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isGenericTransportMessage(error) {
|
|
17
|
+
const normalizedMessage = String(error?.message || "").trim();
|
|
18
|
+
if (!normalizedMessage) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const status = normalizeErrorStatus(error);
|
|
23
|
+
if (status === 0) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return status != null && status >= 400 && normalizedMessage === `Request failed with status ${status}.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toQueryErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
|
|
31
|
+
if (!error) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const normalizedFallback = String(fallbackMessage || "").trim();
|
|
36
|
+
const normalizedMessage = String(error?.message || "").trim();
|
|
37
|
+
if (normalizedMessage && !isGenericTransportMessage(error)) {
|
|
38
|
+
return normalizedMessage;
|
|
39
|
+
}
|
|
40
|
+
if (normalizedFallback) {
|
|
41
|
+
return normalizedFallback;
|
|
42
|
+
}
|
|
43
|
+
if (normalizedMessage) {
|
|
44
|
+
return normalizedMessage;
|
|
45
|
+
}
|
|
46
|
+
return String(defaultMessage || "Request failed.").trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toUiErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
|
|
50
|
+
const normalizedFallback = String(fallbackMessage || "").trim();
|
|
51
|
+
if (normalizedFallback) {
|
|
52
|
+
return normalizedFallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const normalizedMessage = String(error?.message || "").trim();
|
|
56
|
+
if (normalizedMessage) {
|
|
57
|
+
return normalizedMessage;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return String(defaultMessage || "Request failed.").trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
toQueryErrorMessage,
|
|
65
|
+
toUiErrorMessage
|
|
66
|
+
};
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { isRef, unref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
normalizeBoolean,
|
|
4
|
+
normalizeText,
|
|
5
|
+
normalizeUniqueTextList
|
|
6
|
+
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
8
|
+
|
|
9
|
+
const QUERY_PARAM_BINDING_TYPE_TEXT = "text";
|
|
10
|
+
const QUERY_PARAM_BINDING_TYPE_BOOLEAN = "boolean";
|
|
11
|
+
const QUERY_PARAM_BINDING_TYPE_NUMBER = "number";
|
|
12
|
+
const QUERY_PARAM_BINDING_TYPE_ARRAY = "array";
|
|
13
|
+
const QUERY_PARAM_BINDING_TYPE_DATE = "date";
|
|
14
|
+
|
|
15
|
+
function normalizeListSyncToRouteConfig(syncToRoute = false, { defaultSearchParam = "q" } = {}) {
|
|
16
|
+
const source = syncToRoute === true ? {} : asPlainObject(syncToRoute);
|
|
17
|
+
const requested = syncToRoute === true || Object.keys(source).length > 0;
|
|
18
|
+
const queryParamBlacklist = Object.freeze(normalizeUniqueTextList(source.queryParamBlacklist));
|
|
19
|
+
if (!requested || source.enabled === false) {
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
enabled: false,
|
|
22
|
+
mode: "replace",
|
|
23
|
+
syncSearch: false,
|
|
24
|
+
syncQueryParams: false,
|
|
25
|
+
hydrateFromRoute: false,
|
|
26
|
+
searchParam: normalizeText(defaultSearchParam) || "q",
|
|
27
|
+
queryParamBlacklist
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mode = normalizeText(source.mode).toLowerCase() === "push" ? "push" : "replace";
|
|
32
|
+
const searchParam = normalizeText(source.searchParam) || normalizeText(defaultSearchParam) || "q";
|
|
33
|
+
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
enabled: true,
|
|
36
|
+
mode,
|
|
37
|
+
syncSearch: source.search !== false,
|
|
38
|
+
syncQueryParams: source.queryParams !== false,
|
|
39
|
+
hydrateFromRoute: source.hydrateFromRoute !== false,
|
|
40
|
+
searchParam,
|
|
41
|
+
queryParamBlacklist
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeQueryParamKey(value) {
|
|
46
|
+
return normalizeText(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeQueryParamValues(value) {
|
|
50
|
+
const list = Array.isArray(value) ? value : [value];
|
|
51
|
+
const normalizedValues = [];
|
|
52
|
+
|
|
53
|
+
for (const entry of list) {
|
|
54
|
+
const resolvedEntry = unref(entry);
|
|
55
|
+
if (resolvedEntry == null) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof resolvedEntry === "boolean") {
|
|
60
|
+
if (resolvedEntry) {
|
|
61
|
+
normalizedValues.push("1");
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof resolvedEntry === "number") {
|
|
67
|
+
if (Number.isFinite(resolvedEntry)) {
|
|
68
|
+
normalizedValues.push(String(resolvedEntry));
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (resolvedEntry instanceof Date) {
|
|
74
|
+
if (!Number.isNaN(resolvedEntry.getTime())) {
|
|
75
|
+
normalizedValues.push(resolvedEntry.toISOString());
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const normalizedText = normalizeText(resolvedEntry);
|
|
81
|
+
if (normalizedText) {
|
|
82
|
+
normalizedValues.push(normalizedText);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return normalizedValues;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveQueryParamsInput(queryParams, context = {}) {
|
|
90
|
+
if (typeof queryParams === "function") {
|
|
91
|
+
return asPlainObject(queryParams(context));
|
|
92
|
+
}
|
|
93
|
+
return asPlainObject(unref(queryParams));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveQueryParamBindingType(value) {
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
return QUERY_PARAM_BINDING_TYPE_ARRAY;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === "boolean") {
|
|
101
|
+
return QUERY_PARAM_BINDING_TYPE_BOOLEAN;
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === "number") {
|
|
104
|
+
return QUERY_PARAM_BINDING_TYPE_NUMBER;
|
|
105
|
+
}
|
|
106
|
+
if (value instanceof Date) {
|
|
107
|
+
return QUERY_PARAM_BINDING_TYPE_DATE;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return QUERY_PARAM_BINDING_TYPE_TEXT;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveArrayQueryParamItemType(values = []) {
|
|
114
|
+
const list = Array.isArray(values) ? values : [];
|
|
115
|
+
for (const entry of list) {
|
|
116
|
+
if (entry == null) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof entry === "boolean") {
|
|
121
|
+
return QUERY_PARAM_BINDING_TYPE_BOOLEAN;
|
|
122
|
+
}
|
|
123
|
+
if (typeof entry === "number") {
|
|
124
|
+
return QUERY_PARAM_BINDING_TYPE_NUMBER;
|
|
125
|
+
}
|
|
126
|
+
if (entry instanceof Date) {
|
|
127
|
+
return QUERY_PARAM_BINDING_TYPE_DATE;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return QUERY_PARAM_BINDING_TYPE_TEXT;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return QUERY_PARAM_BINDING_TYPE_TEXT;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createWritableQueryParamBinding({
|
|
137
|
+
source = {},
|
|
138
|
+
rawKey = "",
|
|
139
|
+
rawValue = null,
|
|
140
|
+
key = "",
|
|
141
|
+
resolveRouteValue = null
|
|
142
|
+
} = {}) {
|
|
143
|
+
const valueSourceIsRef = isRef(rawValue);
|
|
144
|
+
const read = valueSourceIsRef
|
|
145
|
+
? () => rawValue.value
|
|
146
|
+
: () => source[rawKey];
|
|
147
|
+
const write = valueSourceIsRef
|
|
148
|
+
? (nextValue) => {
|
|
149
|
+
rawValue.value = nextValue;
|
|
150
|
+
}
|
|
151
|
+
: (nextValue) => {
|
|
152
|
+
source[rawKey] = nextValue;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const currentValue = read();
|
|
156
|
+
const valueType = resolveQueryParamBindingType(currentValue);
|
|
157
|
+
return {
|
|
158
|
+
key,
|
|
159
|
+
valueType,
|
|
160
|
+
arrayItemType: valueType === QUERY_PARAM_BINDING_TYPE_ARRAY
|
|
161
|
+
? resolveArrayQueryParamItemType(currentValue)
|
|
162
|
+
: QUERY_PARAM_BINDING_TYPE_TEXT,
|
|
163
|
+
get: read,
|
|
164
|
+
set: write,
|
|
165
|
+
resolveRouteValue
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveQueryParamDescriptors(
|
|
170
|
+
queryParams,
|
|
171
|
+
context = {},
|
|
172
|
+
{ routeValueResolvers = null } = {}
|
|
173
|
+
) {
|
|
174
|
+
const source = resolveQueryParamsInput(queryParams, context);
|
|
175
|
+
const resolvers = asPlainObject(unref(routeValueResolvers));
|
|
176
|
+
const descriptorsByKey = new Map();
|
|
177
|
+
const canWriteToSource = typeof queryParams !== "function";
|
|
178
|
+
|
|
179
|
+
for (const [rawKey, rawValue] of Object.entries(source)) {
|
|
180
|
+
const key = normalizeQueryParamKey(rawKey);
|
|
181
|
+
if (!key) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const values = normalizeQueryParamValues(rawValue);
|
|
186
|
+
const current = descriptorsByKey.get(key) || {
|
|
187
|
+
key,
|
|
188
|
+
values: [],
|
|
189
|
+
binding: null
|
|
190
|
+
};
|
|
191
|
+
if (values.length > 0) {
|
|
192
|
+
current.values.push(...values);
|
|
193
|
+
}
|
|
194
|
+
if (!current.binding && canWriteToSource) {
|
|
195
|
+
current.binding = createWritableQueryParamBinding({
|
|
196
|
+
source,
|
|
197
|
+
rawKey,
|
|
198
|
+
rawValue,
|
|
199
|
+
key,
|
|
200
|
+
resolveRouteValue: typeof resolvers[key] === "function"
|
|
201
|
+
? resolvers[key]
|
|
202
|
+
: null
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
descriptorsByKey.set(key, current);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return [...descriptorsByKey.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveActiveQueryParamEntries(descriptors = []) {
|
|
213
|
+
const source = Array.isArray(descriptors) ? descriptors : [];
|
|
214
|
+
return source
|
|
215
|
+
.filter((descriptor) => Array.isArray(descriptor?.values) && descriptor.values.length > 0)
|
|
216
|
+
.map((descriptor) => ({
|
|
217
|
+
key: descriptor.key,
|
|
218
|
+
values: [...descriptor.values]
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resolveWritableQueryParamBindings(descriptors = []) {
|
|
223
|
+
const source = Array.isArray(descriptors) ? descriptors : [];
|
|
224
|
+
return source
|
|
225
|
+
.map((descriptor) => descriptor?.binding || null)
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildQueryParamEntriesToken(entries = []) {
|
|
230
|
+
const normalizedEntries = Array.isArray(entries) ? entries : [];
|
|
231
|
+
if (normalizedEntries.length < 1) {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return normalizedEntries
|
|
236
|
+
.map((entry) => {
|
|
237
|
+
const key = normalizeQueryParamKey(entry?.key);
|
|
238
|
+
const values = Array.isArray(entry?.values)
|
|
239
|
+
? entry.values.map((value) => normalizeText(value)).filter(Boolean)
|
|
240
|
+
: [];
|
|
241
|
+
if (!key || values.length < 1) {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
return `${key}=${values.join(",")}`;
|
|
245
|
+
})
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.join("&");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function firstRouteQueryValue(value) {
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
return value[0];
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeRouteQueryValues(value) {
|
|
258
|
+
const values = Array.isArray(value) ? value : [value];
|
|
259
|
+
return values
|
|
260
|
+
.map((entry) => normalizeText(entry))
|
|
261
|
+
.filter(Boolean);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseRouteBooleanValue(value, fallback = false) {
|
|
265
|
+
if (value === undefined) {
|
|
266
|
+
return fallback;
|
|
267
|
+
}
|
|
268
|
+
if (value === null || value === "") {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
return normalizeBoolean(firstRouteQueryValue(value));
|
|
274
|
+
} catch {
|
|
275
|
+
return fallback;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseRouteNumberValue(value, fallback = null) {
|
|
280
|
+
if (value === undefined) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const normalized = normalizeText(firstRouteQueryValue(value));
|
|
285
|
+
if (!normalized) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const parsed = Number(normalized);
|
|
290
|
+
if (!Number.isFinite(parsed)) {
|
|
291
|
+
return fallback;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return parsed;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseRouteDateValue(value, fallback = null) {
|
|
298
|
+
if (value === undefined) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const normalized = normalizeText(firstRouteQueryValue(value));
|
|
303
|
+
if (!normalized) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const parsed = new Date(normalized);
|
|
308
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
309
|
+
return fallback;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return parsed;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseRouteQueryItemValue(value, itemType = QUERY_PARAM_BINDING_TYPE_TEXT, fallback = null) {
|
|
316
|
+
if (itemType === QUERY_PARAM_BINDING_TYPE_BOOLEAN) {
|
|
317
|
+
try {
|
|
318
|
+
return normalizeBoolean(value);
|
|
319
|
+
} catch {
|
|
320
|
+
return fallback;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (itemType === QUERY_PARAM_BINDING_TYPE_NUMBER) {
|
|
324
|
+
const numeric = Number(value);
|
|
325
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
326
|
+
}
|
|
327
|
+
if (itemType === QUERY_PARAM_BINDING_TYPE_DATE) {
|
|
328
|
+
const date = new Date(value);
|
|
329
|
+
return Number.isNaN(date.getTime()) ? fallback : date;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const normalized = normalizeText(value);
|
|
333
|
+
return normalized || fallback;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function parseRouteBindingValue(binding, routeQueryValue, context = {}) {
|
|
337
|
+
if (typeof binding?.resolveRouteValue === "function") {
|
|
338
|
+
return binding.resolveRouteValue(routeQueryValue, context);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const valueType = normalizeText(binding?.valueType).toLowerCase();
|
|
342
|
+
if (valueType === QUERY_PARAM_BINDING_TYPE_BOOLEAN) {
|
|
343
|
+
return parseRouteBooleanValue(routeQueryValue, false);
|
|
344
|
+
}
|
|
345
|
+
if (valueType === QUERY_PARAM_BINDING_TYPE_NUMBER) {
|
|
346
|
+
const fallback = typeof binding?.get === "function" ? binding.get() : null;
|
|
347
|
+
return parseRouteNumberValue(routeQueryValue, fallback);
|
|
348
|
+
}
|
|
349
|
+
if (valueType === QUERY_PARAM_BINDING_TYPE_DATE) {
|
|
350
|
+
const fallback = typeof binding?.get === "function" ? binding.get() : null;
|
|
351
|
+
return parseRouteDateValue(routeQueryValue, fallback);
|
|
352
|
+
}
|
|
353
|
+
if (valueType === QUERY_PARAM_BINDING_TYPE_ARRAY) {
|
|
354
|
+
const itemType = normalizeText(binding?.arrayItemType).toLowerCase() || QUERY_PARAM_BINDING_TYPE_TEXT;
|
|
355
|
+
return normalizeRouteQueryValues(routeQueryValue)
|
|
356
|
+
.map((value) => parseRouteQueryItemValue(value, itemType, null))
|
|
357
|
+
.filter((value) => value != null && !(typeof value === "string" && !value));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return normalizeText(firstRouteQueryValue(routeQueryValue));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function areQueryParamBindingItemsEqual(left, right) {
|
|
364
|
+
if (left === right) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (left instanceof Date && right instanceof Date) {
|
|
368
|
+
return left.getTime() === right.getTime();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return Object.is(left, right);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function areQueryParamBindingValuesEqual(left, right) {
|
|
375
|
+
if (left === right) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (left instanceof Date && right instanceof Date) {
|
|
379
|
+
return left.getTime() === right.getTime();
|
|
380
|
+
}
|
|
381
|
+
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
382
|
+
return Object.is(left, right);
|
|
383
|
+
}
|
|
384
|
+
if (left.length !== right.length) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
389
|
+
if (!areQueryParamBindingItemsEqual(left[index], right[index])) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildRouteQueryCompareToken(query = {}) {
|
|
398
|
+
const source = asPlainObject(query);
|
|
399
|
+
const keys = Object.keys(source).sort();
|
|
400
|
+
const parts = [];
|
|
401
|
+
for (const key of keys) {
|
|
402
|
+
const normalizedKey = normalizeQueryParamKey(key);
|
|
403
|
+
if (!normalizedKey) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const normalizedValues = normalizeRouteQueryValues(source[key]);
|
|
408
|
+
if (normalizedValues.length < 1) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
parts.push(`${normalizedKey}=${normalizedValues.join(",")}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return parts.join("&");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function mergeManagedQueryParamKeyHistory(history = [], keys = []) {
|
|
419
|
+
const merged = new Set();
|
|
420
|
+
for (const key of Array.isArray(history) ? history : []) {
|
|
421
|
+
const normalized = normalizeQueryParamKey(key);
|
|
422
|
+
if (normalized) {
|
|
423
|
+
merged.add(normalized);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const key of Array.isArray(keys) ? keys : []) {
|
|
427
|
+
const normalized = normalizeQueryParamKey(key);
|
|
428
|
+
if (normalized) {
|
|
429
|
+
merged.add(normalized);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return [...merged].sort((left, right) => left.localeCompare(right));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function resolveRouteSyncManagedKeys({
|
|
437
|
+
searchEnabled = false,
|
|
438
|
+
searchParam = "q",
|
|
439
|
+
syncSearch = false,
|
|
440
|
+
syncQueryParams = false,
|
|
441
|
+
declaredKeys = [],
|
|
442
|
+
keyHistory = []
|
|
443
|
+
} = {}) {
|
|
444
|
+
const managed = new Set();
|
|
445
|
+
|
|
446
|
+
if (syncSearch === true && searchEnabled === true) {
|
|
447
|
+
const normalizedSearchParam = normalizeQueryParamKey(searchParam);
|
|
448
|
+
if (normalizedSearchParam) {
|
|
449
|
+
managed.add(normalizedSearchParam);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (syncQueryParams === true) {
|
|
454
|
+
for (const key of mergeManagedQueryParamKeyHistory(keyHistory, declaredKeys)) {
|
|
455
|
+
managed.add(key);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return [...managed].sort((left, right) => left.localeCompare(right));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export {
|
|
463
|
+
normalizeListSyncToRouteConfig,
|
|
464
|
+
resolveQueryParamDescriptors,
|
|
465
|
+
resolveActiveQueryParamEntries,
|
|
466
|
+
resolveWritableQueryParamBindings,
|
|
467
|
+
buildQueryParamEntriesToken,
|
|
468
|
+
parseRouteBindingValue,
|
|
469
|
+
areQueryParamBindingValuesEqual,
|
|
470
|
+
buildRouteQueryCompareToken,
|
|
471
|
+
mergeManagedQueryParamKeyHistory,
|
|
472
|
+
resolveRouteSyncManagedKeys
|
|
473
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeInteger,
|
|
3
|
+
normalizeText,
|
|
4
|
+
normalizeUniqueTextList
|
|
5
|
+
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LIST_SEARCH_DEBOUNCE_MS = 250;
|
|
9
|
+
const DEFAULT_LIST_SEARCH_MIN_LENGTH = 1;
|
|
10
|
+
const LIST_SEARCH_MODE_LOCAL = "local";
|
|
11
|
+
const LIST_SEARCH_MODE_QUERY = "query";
|
|
12
|
+
|
|
13
|
+
function normalizeListSearchConfig(value = {}) {
|
|
14
|
+
const source = asPlainObject(value);
|
|
15
|
+
const modeRaw = normalizeText(source.mode).toLowerCase();
|
|
16
|
+
const mode = modeRaw === LIST_SEARCH_MODE_LOCAL
|
|
17
|
+
? LIST_SEARCH_MODE_LOCAL
|
|
18
|
+
: LIST_SEARCH_MODE_QUERY;
|
|
19
|
+
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
enabled: source.enabled === true,
|
|
22
|
+
mode,
|
|
23
|
+
queryParam: normalizeText(source.queryParam) || "q",
|
|
24
|
+
label: normalizeText(source.label) || "Search",
|
|
25
|
+
placeholder: normalizeText(source.placeholder),
|
|
26
|
+
initialQuery: normalizeText(source.initialQuery),
|
|
27
|
+
debounceMs: normalizeInteger(source.debounceMs, {
|
|
28
|
+
fallback: DEFAULT_LIST_SEARCH_DEBOUNCE_MS,
|
|
29
|
+
min: 0
|
|
30
|
+
}),
|
|
31
|
+
minLength: normalizeInteger(source.minLength, {
|
|
32
|
+
fallback: DEFAULT_LIST_SEARCH_MIN_LENGTH,
|
|
33
|
+
min: DEFAULT_LIST_SEARCH_MIN_LENGTH
|
|
34
|
+
}),
|
|
35
|
+
fields: Object.freeze(normalizeUniqueTextList(source.fields))
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeSearchableValue(value) {
|
|
40
|
+
if (value == null) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
return normalizeText(value).toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
47
|
+
return String(value).toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function matchesLocalSearch(item = {}, query = "", searchFields = []) {
|
|
53
|
+
const source = asPlainObject(item);
|
|
54
|
+
const normalizedQuery = normalizeText(query).toLowerCase();
|
|
55
|
+
if (!normalizedQuery) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fields = Array.isArray(searchFields) ? searchFields : [];
|
|
60
|
+
if (fields.length > 0) {
|
|
61
|
+
return fields.some((field) => normalizeSearchableValue(source[field]).includes(normalizedQuery));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Object.values(source).some((value) => normalizeSearchableValue(value).includes(normalizedQuery));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
normalizeListSearchConfig,
|
|
69
|
+
matchesLocalSearch
|
|
70
|
+
};
|