@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,19 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
|
|
3
|
+
function resolveEnabledRef(value) {
|
|
4
|
+
if (value === undefined) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof value === "function") {
|
|
9
|
+
return Boolean(value());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return Boolean(unref(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveTextRef(value) {
|
|
16
|
+
return String(unref(value) || "").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { resolveEnabledRef, resolveTextRef };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
buildJsonApiFieldsetsToken,
|
|
4
|
+
normalizeJsonApiFieldsets
|
|
5
|
+
} from "@jskit-ai/kernel/shared/support/jsonApiFieldsets";
|
|
6
|
+
import {
|
|
7
|
+
resolveQueryParamDescriptors,
|
|
8
|
+
resolveActiveQueryParamEntries,
|
|
9
|
+
buildQueryParamEntriesToken
|
|
10
|
+
} from "./listQueryParamSupport.js";
|
|
11
|
+
|
|
12
|
+
function resolveRequestQueryContext(context = null) {
|
|
13
|
+
const source = unref(context);
|
|
14
|
+
return source && typeof source === "object" && !Array.isArray(source) ? source : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveRequestQueryBaseKey(sourceQueryKey = null) {
|
|
18
|
+
const source = unref(sourceQueryKey);
|
|
19
|
+
if (Array.isArray(source)) {
|
|
20
|
+
return [...source];
|
|
21
|
+
}
|
|
22
|
+
if (source == null) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return [source];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveRequestFieldsets(requestFieldsets = null, context = {}) {
|
|
29
|
+
const source = typeof requestFieldsets === "function"
|
|
30
|
+
? requestFieldsets(context)
|
|
31
|
+
: unref(requestFieldsets);
|
|
32
|
+
return normalizeJsonApiFieldsets(source);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appendRequestQueryValue(target = {}, key = "", values = []) {
|
|
36
|
+
const normalizedKey = String(key || "").trim();
|
|
37
|
+
const normalizedValues = (Array.isArray(values) ? values : [])
|
|
38
|
+
.map((value) => String(value ?? "").trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
if (!normalizedKey || normalizedValues.length < 1) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const currentValue = target[normalizedKey];
|
|
45
|
+
if (currentValue === undefined) {
|
|
46
|
+
target[normalizedKey] = normalizedValues.length === 1 ? normalizedValues[0] : [...normalizedValues];
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const currentValues = Array.isArray(currentValue) ? [...currentValue] : [currentValue];
|
|
51
|
+
target[normalizedKey] = [...currentValues, ...normalizedValues];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildRequestQueryObject(entries = [], { fieldsets = null } = {}) {
|
|
55
|
+
const sourceEntries = Array.isArray(entries) ? entries : [];
|
|
56
|
+
const query = {};
|
|
57
|
+
|
|
58
|
+
for (const entry of sourceEntries) {
|
|
59
|
+
appendRequestQueryValue(query, entry?.key, entry?.values);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalizedFieldsets = normalizeJsonApiFieldsets(fieldsets);
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
...query,
|
|
65
|
+
...(Object.keys(normalizedFieldsets).length > 0
|
|
66
|
+
? { fields: normalizedFieldsets }
|
|
67
|
+
: {})
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createRequestQueryRuntime({
|
|
72
|
+
requestQueryParams = null,
|
|
73
|
+
requestFieldsets = null,
|
|
74
|
+
context = null,
|
|
75
|
+
sourceQueryKey = null
|
|
76
|
+
} = {}) {
|
|
77
|
+
const requestQueryParamDescriptors = computed(() => {
|
|
78
|
+
return resolveQueryParamDescriptors(requestQueryParams, resolveRequestQueryContext(context));
|
|
79
|
+
});
|
|
80
|
+
const activeRequestQueryParamEntries = computed(() => {
|
|
81
|
+
return resolveActiveQueryParamEntries(requestQueryParamDescriptors.value);
|
|
82
|
+
});
|
|
83
|
+
const activeRequestQueryParamsToken = computed(() => {
|
|
84
|
+
return buildQueryParamEntriesToken(activeRequestQueryParamEntries.value);
|
|
85
|
+
});
|
|
86
|
+
const activeRequestFieldsets = computed(() => {
|
|
87
|
+
return resolveRequestFieldsets(
|
|
88
|
+
requestFieldsets,
|
|
89
|
+
resolveRequestQueryContext(context)
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
const activeRequestFieldsetsToken = computed(() => {
|
|
93
|
+
return buildJsonApiFieldsetsToken(activeRequestFieldsets.value);
|
|
94
|
+
});
|
|
95
|
+
const queryKeyParts = computed(() => {
|
|
96
|
+
const parts = [];
|
|
97
|
+
if (activeRequestQueryParamsToken.value) {
|
|
98
|
+
parts.push("__request_query__", activeRequestQueryParamsToken.value);
|
|
99
|
+
}
|
|
100
|
+
if (activeRequestFieldsetsToken.value) {
|
|
101
|
+
parts.push("__request_fieldsets__", activeRequestFieldsetsToken.value);
|
|
102
|
+
}
|
|
103
|
+
return Object.freeze(parts);
|
|
104
|
+
});
|
|
105
|
+
const queryKey = computed(() => {
|
|
106
|
+
if (queryKeyParts.value.length < 1) {
|
|
107
|
+
return unref(sourceQueryKey);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
...resolveRequestQueryBaseKey(sourceQueryKey),
|
|
112
|
+
...queryKeyParts.value
|
|
113
|
+
];
|
|
114
|
+
});
|
|
115
|
+
const requestQuery = computed(() => {
|
|
116
|
+
const query = buildRequestQueryObject(activeRequestQueryParamEntries.value, {
|
|
117
|
+
fieldsets: activeRequestFieldsets.value
|
|
118
|
+
});
|
|
119
|
+
if (Object.keys(query).length < 1) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return query;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
requestQueryParamDescriptors,
|
|
128
|
+
activeRequestQueryParamEntries,
|
|
129
|
+
activeRequestQueryParamsToken,
|
|
130
|
+
activeRequestFieldsets,
|
|
131
|
+
activeRequestFieldsetsToken,
|
|
132
|
+
queryKeyParts,
|
|
133
|
+
queryKey,
|
|
134
|
+
requestQuery
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
buildRequestQueryObject,
|
|
140
|
+
createRequestQueryRuntime
|
|
141
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
|
|
3
|
+
function hasResolvedQueryData({ query = null, data = null } = {}) {
|
|
4
|
+
const querySucceeded = Boolean(unref(query?.isSuccess));
|
|
5
|
+
const hasDataPayload = unref(data) != null;
|
|
6
|
+
|
|
7
|
+
return querySucceeded || hasDataPayload;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function mergeQueryMeta(queryOptions = null, meta = {}) {
|
|
11
|
+
const sourceOptions =
|
|
12
|
+
queryOptions && typeof queryOptions === "object" && !Array.isArray(queryOptions) ? queryOptions : {};
|
|
13
|
+
const sourceMeta =
|
|
14
|
+
sourceOptions.meta && typeof sourceOptions.meta === "object" && !Array.isArray(sourceOptions.meta)
|
|
15
|
+
? sourceOptions.meta
|
|
16
|
+
: {};
|
|
17
|
+
const sourceJskitMeta =
|
|
18
|
+
sourceMeta.jskit && typeof sourceMeta.jskit === "object" && !Array.isArray(sourceMeta.jskit)
|
|
19
|
+
? sourceMeta.jskit
|
|
20
|
+
: {};
|
|
21
|
+
const nextJskitMeta =
|
|
22
|
+
meta.jskit && typeof meta.jskit === "object" && !Array.isArray(meta.jskit)
|
|
23
|
+
? meta.jskit
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...sourceOptions,
|
|
28
|
+
meta: {
|
|
29
|
+
...sourceMeta,
|
|
30
|
+
...meta,
|
|
31
|
+
jskit: {
|
|
32
|
+
...sourceJskitMeta,
|
|
33
|
+
...nextJskitMeta
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeText(value = "") {
|
|
40
|
+
return String(value || "").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function firstNormalizedText(...values) {
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
const normalized = normalizeText(value);
|
|
46
|
+
if (normalized) {
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function firstNonNegativeFiniteNumber(...values) {
|
|
54
|
+
for (const value of values) {
|
|
55
|
+
const numberValue = Number(value);
|
|
56
|
+
if (Number.isFinite(numberValue)) {
|
|
57
|
+
return Math.max(0, numberValue);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizePlainObject(value = null) {
|
|
64
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeRequestRecoveryInput(requestRecovery = null) {
|
|
68
|
+
if (typeof requestRecovery === "string") {
|
|
69
|
+
return {
|
|
70
|
+
label: requestRecovery
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return normalizePlainObject(requestRecovery);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeRequestRecoveryMethod(value = "") {
|
|
77
|
+
return normalizeText(value).toUpperCase();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isRequestRecoveryMetaDisabled(sourceJskitMeta = {}) {
|
|
81
|
+
return sourceJskitMeta.requestRecovery === false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasUsableMetaValue(source = {}, key = "") {
|
|
85
|
+
return Object.hasOwn(source, key) && source[key] !== undefined && source[key] !== null && source[key] !== "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildRequestRecoveryMeta(requestRecovery = null, defaults = {}) {
|
|
89
|
+
if (requestRecovery === false) {
|
|
90
|
+
return {
|
|
91
|
+
jskit: {
|
|
92
|
+
requestRecovery: false
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fallback = normalizePlainObject(defaults);
|
|
98
|
+
const source = normalizeRequestRecoveryInput(requestRecovery);
|
|
99
|
+
if (source.requestRecovery === false || source.enabled === false) {
|
|
100
|
+
return {
|
|
101
|
+
jskit: {
|
|
102
|
+
requestRecovery: false
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const label = firstNormalizedText(
|
|
108
|
+
source.requestRecoveryLabel,
|
|
109
|
+
source.label,
|
|
110
|
+
fallback.requestRecoveryLabel,
|
|
111
|
+
fallback.label
|
|
112
|
+
);
|
|
113
|
+
const recoverySource = firstNormalizedText(
|
|
114
|
+
source.requestRecoverySource,
|
|
115
|
+
source.source,
|
|
116
|
+
fallback.requestRecoverySource,
|
|
117
|
+
fallback.source
|
|
118
|
+
);
|
|
119
|
+
const dedupeKey = firstNormalizedText(
|
|
120
|
+
source.requestRecoveryDedupeKey,
|
|
121
|
+
source.dedupeKey,
|
|
122
|
+
fallback.requestRecoveryDedupeKey,
|
|
123
|
+
fallback.dedupeKey
|
|
124
|
+
);
|
|
125
|
+
const method = normalizeRequestRecoveryMethod(
|
|
126
|
+
firstNormalizedText(
|
|
127
|
+
source.requestRecoveryMethod,
|
|
128
|
+
source.method,
|
|
129
|
+
fallback.requestRecoveryMethod,
|
|
130
|
+
fallback.method
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
const dedupeWindowMs = firstNonNegativeFiniteNumber(
|
|
134
|
+
source.requestRecoveryDedupeWindowMs,
|
|
135
|
+
source.dedupeWindowMs,
|
|
136
|
+
fallback.requestRecoveryDedupeWindowMs,
|
|
137
|
+
fallback.dedupeWindowMs
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const jskit = {};
|
|
141
|
+
if (label) {
|
|
142
|
+
jskit.requestRecoveryLabel = label;
|
|
143
|
+
}
|
|
144
|
+
if (recoverySource) {
|
|
145
|
+
jskit.requestRecoverySource = recoverySource;
|
|
146
|
+
}
|
|
147
|
+
if (dedupeKey) {
|
|
148
|
+
jskit.requestRecoveryDedupeKey = dedupeKey;
|
|
149
|
+
}
|
|
150
|
+
if (method) {
|
|
151
|
+
jskit.requestRecoveryMethod = method;
|
|
152
|
+
}
|
|
153
|
+
if (dedupeWindowMs !== null) {
|
|
154
|
+
jskit.requestRecoveryDedupeWindowMs = dedupeWindowMs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Object.keys(jskit).length > 0 ? { jskit } : {};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveRequestRecoveryDefaults(queryOptions = null, defaults = {}) {
|
|
161
|
+
const sourceOptions = normalizePlainObject(queryOptions);
|
|
162
|
+
const sourceMeta = normalizePlainObject(sourceOptions.meta);
|
|
163
|
+
const sourceJskitMeta = normalizePlainObject(sourceMeta.jskit);
|
|
164
|
+
const fallback = normalizePlainObject(defaults);
|
|
165
|
+
if (isRequestRecoveryMetaDisabled(sourceJskitMeta)) {
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const hasExplicitLabel = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryLabel");
|
|
170
|
+
const hasExplicitSource = hasUsableMetaValue(sourceJskitMeta, "requestRecoverySource");
|
|
171
|
+
const hasExplicitDedupeKey = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeKey");
|
|
172
|
+
const hasExplicitDedupeWindowMs = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeWindowMs");
|
|
173
|
+
const hasExplicitMethod = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryMethod");
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
...fallback,
|
|
177
|
+
requestRecoveryLabel: hasExplicitLabel ? "" : fallback.requestRecoveryLabel,
|
|
178
|
+
label: hasExplicitLabel ? "" : fallback.label,
|
|
179
|
+
requestRecoverySource: hasExplicitSource ? "" : fallback.requestRecoverySource,
|
|
180
|
+
source: hasExplicitSource ? "" : fallback.source,
|
|
181
|
+
requestRecoveryDedupeKey: hasExplicitDedupeKey ? "" : fallback.requestRecoveryDedupeKey,
|
|
182
|
+
dedupeKey: hasExplicitDedupeKey ? "" : fallback.dedupeKey,
|
|
183
|
+
requestRecoveryDedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.requestRecoveryDedupeWindowMs,
|
|
184
|
+
dedupeWindowMs: hasExplicitDedupeWindowMs ? "" : fallback.dedupeWindowMs,
|
|
185
|
+
requestRecoveryMethod: hasExplicitMethod ? "" : fallback.requestRecoveryMethod,
|
|
186
|
+
method: hasExplicitMethod ? "" : fallback.method
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function mergeRequestRecoveryQueryMeta(queryOptions = null, requestRecovery = null, defaults = {}) {
|
|
191
|
+
const sourceOptions = normalizePlainObject(queryOptions);
|
|
192
|
+
const effectiveDefaults =
|
|
193
|
+
requestRecovery == null
|
|
194
|
+
? resolveRequestRecoveryDefaults(sourceOptions, defaults)
|
|
195
|
+
: normalizePlainObject(defaults);
|
|
196
|
+
const meta = buildRequestRecoveryMeta(requestRecovery, effectiveDefaults);
|
|
197
|
+
if (Object.keys(meta).length < 1) {
|
|
198
|
+
return sourceOptions;
|
|
199
|
+
}
|
|
200
|
+
return mergeQueryMeta(sourceOptions, meta);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
buildRequestRecoveryMeta,
|
|
205
|
+
hasResolvedQueryData,
|
|
206
|
+
mergeQueryMeta,
|
|
207
|
+
mergeRequestRecoveryQueryMeta
|
|
208
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
3
|
+
|
|
4
|
+
const ROUTE_PARAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
5
|
+
|
|
6
|
+
function normalizeRouteParamName(value = "", { context = "http-web route param" } = {}) {
|
|
7
|
+
const normalizedValue = String(value || "").trim();
|
|
8
|
+
if (!normalizedValue) {
|
|
9
|
+
throw new TypeError(`${context} must be a non-empty route parameter name.`);
|
|
10
|
+
}
|
|
11
|
+
if (!ROUTE_PARAM_NAME_PATTERN.test(normalizedValue)) {
|
|
12
|
+
throw new TypeError(
|
|
13
|
+
`${context} "${normalizedValue}" is invalid. Use letters, numbers, and underscores only.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return normalizedValue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toRouteParamValue(value) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return toRouteParamValue(value[0]);
|
|
23
|
+
}
|
|
24
|
+
if (value == null) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return String(value).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveRouteSourceValue(source = null) {
|
|
32
|
+
if (typeof source === "function") {
|
|
33
|
+
return source();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return unref(source);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveRouteParamsSource(source = null) {
|
|
40
|
+
return asPlainObject(resolveRouteSourceValue(source));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeRouteParamNameList(value = []) {
|
|
44
|
+
const source = Array.isArray(value) ? value : [];
|
|
45
|
+
return source
|
|
46
|
+
.map((entry) => String(entry || "").trim())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveRouteParamNamesSource(source = []) {
|
|
51
|
+
return normalizeRouteParamNameList(resolveRouteSourceValue(source));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeRoutePathname(value = "") {
|
|
55
|
+
const rawPathname = String(value || "").trim();
|
|
56
|
+
const sanitizedPathname = rawPathname.split(/[?#]/u, 1)[0] || "";
|
|
57
|
+
if (!sanitizedPathname) {
|
|
58
|
+
return "/";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return sanitizedPathname.startsWith("/") ? sanitizedPathname : `/${sanitizedPathname}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveRoutePathnameSource(source = "") {
|
|
65
|
+
return normalizeRoutePathname(resolveRouteSourceValue(source));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function segmentMatchesParamValue(segment = "", paramValue = "") {
|
|
69
|
+
const normalizedSegment = String(segment || "").trim();
|
|
70
|
+
const normalizedParamValue = toRouteParamValue(paramValue);
|
|
71
|
+
if (!normalizedSegment || !normalizedParamValue) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const encodedParamValue = encodeURIComponent(normalizedParamValue);
|
|
76
|
+
if (normalizedSegment === encodedParamValue) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return decodeURIComponent(normalizedSegment) === normalizedParamValue;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findRouteParamSegmentIndex(segments = [], paramValue = "", fromIndex = 0) {
|
|
88
|
+
const source = Array.isArray(segments) ? segments : [];
|
|
89
|
+
const cursor = Number.isInteger(fromIndex) && fromIndex > 0 ? fromIndex : 0;
|
|
90
|
+
for (let index = cursor; index < source.length; index += 1) {
|
|
91
|
+
if (segmentMatchesParamValue(source[index], paramValue)) {
|
|
92
|
+
return index;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return -1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizePathPrefix(segments = [], endIndex = -1) {
|
|
99
|
+
const source = Array.isArray(segments) ? segments : [];
|
|
100
|
+
if (!Number.isInteger(endIndex) || endIndex < 0) {
|
|
101
|
+
return "/";
|
|
102
|
+
}
|
|
103
|
+
return `/${source.slice(0, endIndex + 1).join("/")}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveAnchorEndIndex(segmentIndex = -1, totalSegments = 0, anchorMode = "at") {
|
|
107
|
+
const normalizedSegmentIndex = Number.isInteger(segmentIndex) ? segmentIndex : -1;
|
|
108
|
+
if (normalizedSegmentIndex < 0) {
|
|
109
|
+
return -1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalizedTotalSegments = Number.isInteger(totalSegments) && totalSegments > 0 ? totalSegments : 0;
|
|
113
|
+
const normalizedMode = String(anchorMode || "at").trim().toLowerCase();
|
|
114
|
+
if (normalizedMode === "before") {
|
|
115
|
+
return normalizedSegmentIndex - 1;
|
|
116
|
+
}
|
|
117
|
+
if (normalizedMode === "after") {
|
|
118
|
+
return normalizedTotalSegments > 0
|
|
119
|
+
? Math.min(normalizedSegmentIndex + 1, normalizedTotalSegments - 1)
|
|
120
|
+
: normalizedSegmentIndex + 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalizedSegmentIndex;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveScopedRoutePathname({
|
|
127
|
+
currentPathname = "/",
|
|
128
|
+
params = {},
|
|
129
|
+
orderedParamNames = [],
|
|
130
|
+
anchorParamName = "",
|
|
131
|
+
anchorParamValue = "",
|
|
132
|
+
anchorMode = "at"
|
|
133
|
+
} = {}) {
|
|
134
|
+
const normalizedCurrentPathname = resolveRoutePathnameSource(currentPathname);
|
|
135
|
+
const normalizedAnchorParamName = String(anchorParamName || "").trim();
|
|
136
|
+
if (!normalizedAnchorParamName) {
|
|
137
|
+
return normalizedCurrentPathname;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sourceParams = asPlainObject(params);
|
|
141
|
+
const segments = normalizedCurrentPathname.split("/").filter(Boolean);
|
|
142
|
+
if (segments.length < 1) {
|
|
143
|
+
return normalizedCurrentPathname;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const paramNames = resolveRouteParamNamesSource(orderedParamNames);
|
|
147
|
+
let cursor = 0;
|
|
148
|
+
for (const paramName of paramNames) {
|
|
149
|
+
const segmentIndex = findRouteParamSegmentIndex(segments, sourceParams[paramName], cursor);
|
|
150
|
+
if (segmentIndex < 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (paramName === normalizedAnchorParamName) {
|
|
155
|
+
const endIndex = resolveAnchorEndIndex(segmentIndex, segments.length, anchorMode);
|
|
156
|
+
return normalizePathPrefix(segments, endIndex);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
cursor = segmentIndex + 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const fallbackAnchorValue = toRouteParamValue(anchorParamValue) ||
|
|
163
|
+
toRouteParamValue(sourceParams[normalizedAnchorParamName]);
|
|
164
|
+
if (!fallbackAnchorValue) {
|
|
165
|
+
return normalizedCurrentPathname;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fallbackSegmentIndex = findRouteParamSegmentIndex(segments, fallbackAnchorValue, 0);
|
|
169
|
+
if (fallbackSegmentIndex < 0) {
|
|
170
|
+
return normalizedCurrentPathname;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fallbackEndIndex = resolveAnchorEndIndex(fallbackSegmentIndex, segments.length, anchorMode);
|
|
174
|
+
return normalizePathPrefix(segments, fallbackEndIndex);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveRouteTemplatePath(routeTemplate = "", params = {}) {
|
|
178
|
+
const normalizedTemplate = String(routeTemplate || "").trim();
|
|
179
|
+
if (!normalizedTemplate) {
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const source = asPlainObject(params);
|
|
184
|
+
const missingParams = [];
|
|
185
|
+
const resolvedPath = normalizedTemplate.replace(/:([A-Za-z][A-Za-z0-9_]*)/g, (_, key) => {
|
|
186
|
+
const value = toRouteParamValue(source[key]);
|
|
187
|
+
if (!value) {
|
|
188
|
+
missingParams.push(key);
|
|
189
|
+
return `:${key}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return encodeURIComponent(value);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (missingParams.length > 0) {
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return resolvedPath;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveRouteTemplateLocation(routeTemplate = "", { params = {}, currentPathname = "/" } = {}) {
|
|
203
|
+
const resolvedTemplatePath = resolveRouteTemplatePath(routeTemplate, params);
|
|
204
|
+
if (!resolvedTemplatePath) {
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
if (resolvedTemplatePath.startsWith("/")) {
|
|
208
|
+
return resolvedTemplatePath;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const normalizedCurrentPathname = resolveRoutePathnameSource(currentPathname);
|
|
212
|
+
const basePathname = normalizedCurrentPathname.endsWith("/")
|
|
213
|
+
? normalizedCurrentPathname
|
|
214
|
+
: `${normalizedCurrentPathname}/`;
|
|
215
|
+
const resolvedPathname = new URL(resolvedTemplatePath, `https://jskit.local${basePathname}`).pathname;
|
|
216
|
+
if (resolvedPathname.length > 1 && resolvedPathname.endsWith("/")) {
|
|
217
|
+
return resolvedPathname.slice(0, -1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return resolvedPathname;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function extractRouteParamNames(pathTemplate = "") {
|
|
224
|
+
const normalizedTemplate = String(pathTemplate || "").trim();
|
|
225
|
+
if (!normalizedTemplate) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const names = [];
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
const pattern = /:([A-Za-z][A-Za-z0-9_]*)/g;
|
|
232
|
+
let match = null;
|
|
233
|
+
while ((match = pattern.exec(normalizedTemplate)) != null) {
|
|
234
|
+
const name = String(match[1] || "").trim();
|
|
235
|
+
if (!name || seen.has(name)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
seen.add(name);
|
|
239
|
+
names.push(name);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return names;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveRouteParamNamesInOrder(route = null) {
|
|
246
|
+
const sourceRoute = route && typeof route === "object" ? route : {};
|
|
247
|
+
const matched = Array.isArray(sourceRoute.matched) ? sourceRoute.matched : [];
|
|
248
|
+
const names = [];
|
|
249
|
+
const seen = new Set();
|
|
250
|
+
|
|
251
|
+
for (const entry of matched) {
|
|
252
|
+
const entryPath = String(entry?.path || "").trim();
|
|
253
|
+
if (!entryPath) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
for (const name of extractRouteParamNames(entryPath)) {
|
|
257
|
+
if (seen.has(name)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
seen.add(name);
|
|
261
|
+
names.push(name);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (names.length > 0) {
|
|
266
|
+
return names;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return Object.keys(asPlainObject(sourceRoute.params))
|
|
270
|
+
.map((entry) => String(entry || "").trim())
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export {
|
|
275
|
+
normalizeRouteParamName,
|
|
276
|
+
toRouteParamValue,
|
|
277
|
+
resolveRouteParamsSource,
|
|
278
|
+
resolveRouteParamNamesSource,
|
|
279
|
+
resolveRoutePathnameSource,
|
|
280
|
+
resolveScopedRoutePathname,
|
|
281
|
+
resolveRouteTemplatePath,
|
|
282
|
+
resolveRouteTemplateLocation,
|
|
283
|
+
extractRouteParamNames,
|
|
284
|
+
resolveRouteParamNamesInOrder
|
|
285
|
+
};
|