@jskit-ai/users-web 0.1.70 → 0.1.71
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 +7 -7
- package/package.json +11 -8
- package/src/client/bootstrap/user-bootstrap-handler.js +53 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -22
- package/src/client/composables/internal/crudListParentTitleSupport.js +14 -15
- package/src/client/composables/records/useAddEdit.js +23 -4
- package/src/client/composables/records/useCrudAddEdit.js +5 -20
- package/src/client/composables/records/useList.js +22 -22
- package/src/client/composables/records/useView.js +13 -27
- package/src/client/composables/runtime/operationValidationHelpers.js +18 -19
- package/src/client/composables/runtime/useAddEditCore.js +6 -10
- package/src/client/composables/runtime/useCommandCore.js +3 -10
- package/src/client/composables/runtime/useEndpointResource.js +75 -14
- package/src/client/composables/runtime/useListCore.js +45 -17
- package/src/client/composables/support/requestQueryRuntimeSupport.js +100 -0
- package/src/client/composables/useAccountSettingsRuntime.js +26 -19
- package/src/client/composables/useCommand.js +4 -2
- package/src/client/composables/useCrudListFilters.js +58 -255
- package/src/client/providers/UsersWebClientProvider.js +4 -2
- package/test/bootstrap.test.js +130 -0
- package/test/operationValidationHelpers.test.js +64 -0
- package/test/requestTransportOptions.test.js +107 -0
- package/test/useAddEditCore.test.js +124 -0
- package/test/useAddEditRequestQueryParams.test.js +162 -0
- package/test/useCrudAddEdit.test.js +1 -51
- package/test/useCrudListFilters.test.js +33 -4
- package/test/useCrudListParentTitle.test.js +40 -27
- package/test/useViewRequestQueryParams.test.js +10 -10
- package/src/client/composables/support/requestQueryPathSupport.js +0 -31
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildEndpointReadRequestOptions,
|
|
6
|
+
buildEndpointWriteRequestOptions
|
|
7
|
+
} from "../src/client/composables/runtime/useEndpointResource.js";
|
|
8
|
+
import { buildListRequestOptions } from "../src/client/composables/runtime/useListCore.js";
|
|
9
|
+
|
|
10
|
+
test("endpoint read request options include transport only when provided", () => {
|
|
11
|
+
assert.deepEqual(
|
|
12
|
+
buildEndpointReadRequestOptions({
|
|
13
|
+
method: "get"
|
|
14
|
+
}),
|
|
15
|
+
{
|
|
16
|
+
method: "GET"
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
buildEndpointReadRequestOptions({
|
|
22
|
+
method: "get",
|
|
23
|
+
transport: {
|
|
24
|
+
kind: "jsonapi-resource",
|
|
25
|
+
responseType: "user-settings",
|
|
26
|
+
responseKind: "record"
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
{
|
|
30
|
+
method: "GET",
|
|
31
|
+
transport: {
|
|
32
|
+
kind: "jsonapi-resource",
|
|
33
|
+
responseType: "user-settings",
|
|
34
|
+
responseKind: "record"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("endpoint write request options preserve body/options and append transport", () => {
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
buildEndpointWriteRequestOptions({
|
|
43
|
+
method: "patch",
|
|
44
|
+
body: {
|
|
45
|
+
theme: "dark"
|
|
46
|
+
},
|
|
47
|
+
options: {
|
|
48
|
+
headers: {
|
|
49
|
+
"x-demo": "1"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
transport: {
|
|
53
|
+
kind: "jsonapi-resource",
|
|
54
|
+
requestType: "user-preferences",
|
|
55
|
+
responseType: "user-settings",
|
|
56
|
+
responseKind: "record"
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
{
|
|
60
|
+
method: "PATCH",
|
|
61
|
+
headers: {
|
|
62
|
+
"x-demo": "1"
|
|
63
|
+
},
|
|
64
|
+
body: {
|
|
65
|
+
theme: "dark"
|
|
66
|
+
},
|
|
67
|
+
transport: {
|
|
68
|
+
kind: "jsonapi-resource",
|
|
69
|
+
requestType: "user-preferences",
|
|
70
|
+
responseType: "user-settings",
|
|
71
|
+
responseKind: "record"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("list request options stay GET and carry transport when provided", () => {
|
|
78
|
+
assert.deepEqual(
|
|
79
|
+
buildListRequestOptions({
|
|
80
|
+
requestOptions: {
|
|
81
|
+
headers: {
|
|
82
|
+
"x-demo": "1"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
pageParam: "cursor_2",
|
|
86
|
+
transport: {
|
|
87
|
+
kind: "jsonapi-resource",
|
|
88
|
+
responseType: "contacts",
|
|
89
|
+
responseKind: "collection"
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
{
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers: {
|
|
95
|
+
"x-demo": "1"
|
|
96
|
+
},
|
|
97
|
+
query: {
|
|
98
|
+
cursor: "cursor_2"
|
|
99
|
+
},
|
|
100
|
+
transport: {
|
|
101
|
+
kind: "jsonapi-resource",
|
|
102
|
+
responseType: "contacts",
|
|
103
|
+
responseKind: "collection"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
|
|
4
|
+
import { computed, createSSRApp, effectScope, h, reactive, ref } from "vue";
|
|
5
|
+
import { useAddEditCore } from "../src/client/composables/runtime/useAddEditCore.js";
|
|
6
|
+
|
|
7
|
+
test("useAddEditCore writes saved payloads to the submit-time query key", async () => {
|
|
8
|
+
const queryClient = new QueryClient();
|
|
9
|
+
const model = reactive({
|
|
10
|
+
status: "draft"
|
|
11
|
+
});
|
|
12
|
+
const payload = {
|
|
13
|
+
id: 42,
|
|
14
|
+
status: "published"
|
|
15
|
+
};
|
|
16
|
+
const resource = {
|
|
17
|
+
isSaving: ref(false),
|
|
18
|
+
async save() {
|
|
19
|
+
return payload;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
let runtime = null;
|
|
23
|
+
const app = createSSRApp({
|
|
24
|
+
render() {
|
|
25
|
+
return h("div");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
app.use(VueQueryPlugin, {
|
|
29
|
+
queryClient
|
|
30
|
+
});
|
|
31
|
+
const scope = effectScope();
|
|
32
|
+
|
|
33
|
+
app.runWithContext(() => {
|
|
34
|
+
scope.run(() => {
|
|
35
|
+
runtime = useAddEditCore({
|
|
36
|
+
model,
|
|
37
|
+
resource,
|
|
38
|
+
queryKey: computed(() => ["products", model.status]),
|
|
39
|
+
canSave: ref(true),
|
|
40
|
+
fieldBag: {
|
|
41
|
+
clear() {},
|
|
42
|
+
apply() {}
|
|
43
|
+
},
|
|
44
|
+
feedback: {
|
|
45
|
+
clear() {},
|
|
46
|
+
success() {},
|
|
47
|
+
error() {}
|
|
48
|
+
},
|
|
49
|
+
mapLoadedToModel(target = {}, loaded = {}) {
|
|
50
|
+
target.status = loaded.status;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await runtime.submit();
|
|
57
|
+
scope.stop();
|
|
58
|
+
|
|
59
|
+
assert.equal(model.status, "published");
|
|
60
|
+
assert.deepEqual(queryClient.getQueryData(["products", "draft"]), payload);
|
|
61
|
+
assert.equal(queryClient.getQueryData(["products", "published"]), undefined);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("useAddEditCore snapshots the cache key after payload normalization", async () => {
|
|
65
|
+
const queryClient = new QueryClient();
|
|
66
|
+
const model = reactive({
|
|
67
|
+
status: "draft"
|
|
68
|
+
});
|
|
69
|
+
const payload = {
|
|
70
|
+
id: 42,
|
|
71
|
+
status: "published"
|
|
72
|
+
};
|
|
73
|
+
const resource = {
|
|
74
|
+
isSaving: ref(false),
|
|
75
|
+
async save() {
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
let runtime = null;
|
|
80
|
+
const app = createSSRApp({
|
|
81
|
+
render() {
|
|
82
|
+
return h("div");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
app.use(VueQueryPlugin, {
|
|
86
|
+
queryClient
|
|
87
|
+
});
|
|
88
|
+
const scope = effectScope();
|
|
89
|
+
|
|
90
|
+
app.runWithContext(() => {
|
|
91
|
+
scope.run(() => {
|
|
92
|
+
runtime = useAddEditCore({
|
|
93
|
+
model,
|
|
94
|
+
resource,
|
|
95
|
+
queryKey: computed(() => ["products", model.status]),
|
|
96
|
+
canSave: ref(true),
|
|
97
|
+
fieldBag: {
|
|
98
|
+
clear() {},
|
|
99
|
+
apply() {}
|
|
100
|
+
},
|
|
101
|
+
feedback: {
|
|
102
|
+
clear() {},
|
|
103
|
+
success() {},
|
|
104
|
+
error() {}
|
|
105
|
+
},
|
|
106
|
+
buildRawPayload() {
|
|
107
|
+
model.status = "normalized";
|
|
108
|
+
return {};
|
|
109
|
+
},
|
|
110
|
+
mapLoadedToModel(target = {}, loaded = {}) {
|
|
111
|
+
target.status = loaded.status;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await runtime.submit();
|
|
118
|
+
scope.stop();
|
|
119
|
+
|
|
120
|
+
assert.equal(model.status, "published");
|
|
121
|
+
assert.equal(queryClient.getQueryData(["products", "draft"]), undefined);
|
|
122
|
+
assert.deepEqual(queryClient.getQueryData(["products", "normalized"]), payload);
|
|
123
|
+
assert.equal(queryClient.getQueryData(["products", "published"]), undefined);
|
|
124
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { computed, reactive, ref, shallowRef } from "vue";
|
|
4
|
+
import { createRequestQueryRuntime } from "../src/client/composables/support/requestQueryRuntimeSupport.js";
|
|
5
|
+
|
|
6
|
+
test("add/edit request query params resolve stable cache tokens and request query objects", () => {
|
|
7
|
+
const sourceQueryKey = ref(["products", "dev-admin"]);
|
|
8
|
+
const context = ref(Object.freeze({
|
|
9
|
+
surfaceId: "admin",
|
|
10
|
+
scopeParamValue: "dev-admin",
|
|
11
|
+
ownershipFilter: "workspace",
|
|
12
|
+
recordId: "product-42",
|
|
13
|
+
model: {
|
|
14
|
+
status: "draft"
|
|
15
|
+
}
|
|
16
|
+
}));
|
|
17
|
+
const runtime = createRequestQueryRuntime({
|
|
18
|
+
requestQueryParams(callbackContext = {}) {
|
|
19
|
+
assert.deepEqual(callbackContext, context.value);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
include: "serviceId,bookingSteps,bookingSteps.requiredRoleId",
|
|
23
|
+
preview: callbackContext.model.status === "draft"
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
context,
|
|
27
|
+
sourceQueryKey
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.deepEqual(runtime.activeRequestQueryParamEntries.value, [
|
|
31
|
+
{
|
|
32
|
+
key: "include",
|
|
33
|
+
values: ["serviceId,bookingSteps,bookingSteps.requiredRoleId"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: "preview",
|
|
37
|
+
values: ["1"]
|
|
38
|
+
}
|
|
39
|
+
]);
|
|
40
|
+
assert.equal(
|
|
41
|
+
runtime.activeRequestQueryParamsToken.value,
|
|
42
|
+
"include=serviceId,bookingSteps,bookingSteps.requiredRoleId&preview=1"
|
|
43
|
+
);
|
|
44
|
+
assert.deepEqual(
|
|
45
|
+
runtime.queryKey.value,
|
|
46
|
+
[
|
|
47
|
+
"products",
|
|
48
|
+
"dev-admin",
|
|
49
|
+
"__request_query__",
|
|
50
|
+
"include=serviceId,bookingSteps,bookingSteps.requiredRoleId&preview=1"
|
|
51
|
+
]
|
|
52
|
+
);
|
|
53
|
+
assert.deepEqual(runtime.requestQuery.value, {
|
|
54
|
+
include: "serviceId,bookingSteps,bookingSteps.requiredRoleId",
|
|
55
|
+
preview: "1"
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("add/edit request query params react to callback context changes", () => {
|
|
60
|
+
const recordId = ref("product-42");
|
|
61
|
+
const model = reactive({
|
|
62
|
+
status: "draft"
|
|
63
|
+
});
|
|
64
|
+
const context = computed(() => Object.freeze({
|
|
65
|
+
surfaceId: "admin",
|
|
66
|
+
scopeParamValue: "dev-admin",
|
|
67
|
+
ownershipFilter: "workspace",
|
|
68
|
+
recordId: recordId.value,
|
|
69
|
+
model
|
|
70
|
+
}));
|
|
71
|
+
const runtime = createRequestQueryRuntime({
|
|
72
|
+
requestQueryParams(callbackContext = {}) {
|
|
73
|
+
return {
|
|
74
|
+
include: "bookingSteps",
|
|
75
|
+
preview: callbackContext.model.status === "draft",
|
|
76
|
+
recordId: callbackContext.recordId
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
context,
|
|
80
|
+
sourceQueryKey: computed(() => ["products", context.value.scopeParamValue])
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
runtime.queryKey.value,
|
|
85
|
+
[
|
|
86
|
+
"products",
|
|
87
|
+
"dev-admin",
|
|
88
|
+
"__request_query__",
|
|
89
|
+
"include=bookingSteps&preview=1&recordId=product-42"
|
|
90
|
+
]
|
|
91
|
+
);
|
|
92
|
+
assert.deepEqual(runtime.requestQuery.value, {
|
|
93
|
+
include: "bookingSteps",
|
|
94
|
+
preview: "1",
|
|
95
|
+
recordId: "product-42"
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
model.status = "published";
|
|
99
|
+
recordId.value = "product-99";
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
runtime.queryKey.value,
|
|
103
|
+
[
|
|
104
|
+
"products",
|
|
105
|
+
"dev-admin",
|
|
106
|
+
"__request_query__",
|
|
107
|
+
"include=bookingSteps&recordId=product-99"
|
|
108
|
+
]
|
|
109
|
+
);
|
|
110
|
+
assert.deepEqual(runtime.requestQuery.value, {
|
|
111
|
+
include: "bookingSteps",
|
|
112
|
+
recordId: "product-99"
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("request query runtime preserves scalar base query keys", () => {
|
|
117
|
+
const runtime = createRequestQueryRuntime({
|
|
118
|
+
requestQueryParams: {
|
|
119
|
+
include: "bookingSteps"
|
|
120
|
+
},
|
|
121
|
+
sourceQueryKey: ref("products"),
|
|
122
|
+
sourcePath: "/api/products/42"
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
runtime.queryKey.value,
|
|
127
|
+
[
|
|
128
|
+
"products",
|
|
129
|
+
"__request_query__",
|
|
130
|
+
"include=bookingSteps"
|
|
131
|
+
]
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("request query runtime leaves keys and paths unchanged without active request params", () => {
|
|
136
|
+
const sourceQueryKey = ["products"];
|
|
137
|
+
const runtime = createRequestQueryRuntime({
|
|
138
|
+
requestQueryParams: {
|
|
139
|
+
include: "",
|
|
140
|
+
archived: false
|
|
141
|
+
},
|
|
142
|
+
sourceQueryKey: shallowRef(sourceQueryKey)
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.deepEqual(runtime.activeRequestQueryParamEntries.value, []);
|
|
146
|
+
assert.equal(runtime.activeRequestQueryParamsToken.value, "");
|
|
147
|
+
assert.deepEqual(runtime.queryKey.value, ["products"]);
|
|
148
|
+
assert.equal(runtime.queryKey.value, sourceQueryKey);
|
|
149
|
+
assert.equal(runtime.requestQuery.value, null);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("request query runtime preserves inactive scalar query keys", () => {
|
|
153
|
+
const runtime = createRequestQueryRuntime({
|
|
154
|
+
requestQueryParams: {
|
|
155
|
+
include: ""
|
|
156
|
+
},
|
|
157
|
+
sourceQueryKey: ref("products")
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
assert.equal(runtime.queryKey.value, "products");
|
|
161
|
+
assert.equal(runtime.requestQuery.value, null);
|
|
162
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { reactive } from "vue";
|
|
4
|
-
import { Type } from "typebox";
|
|
5
4
|
import {
|
|
6
5
|
normalizeCrudFormFields,
|
|
7
6
|
createCrudFormModel,
|
|
@@ -9,8 +8,7 @@ import {
|
|
|
9
8
|
applyCrudPayloadToForm,
|
|
10
9
|
resolveCrudRouteBoundFieldValues,
|
|
11
10
|
applyCrudRouteBoundFieldValues,
|
|
12
|
-
resolveCrudFieldErrors
|
|
13
|
-
parseCrudResourceOperationInput
|
|
11
|
+
resolveCrudFieldErrors
|
|
14
12
|
} from "../src/client/composables/crud/crudSchemaFormHelpers.js";
|
|
15
13
|
|
|
16
14
|
test("normalizeCrudFormFields trims keys, removes invalid entries, and deduplicates", () => {
|
|
@@ -290,51 +288,3 @@ test("resolveCrudFieldErrors returns Vuetify-compatible error arrays", () => {
|
|
|
290
288
|
assert.deepEqual(resolveCrudFieldErrors({ name: "Name is required." }, "name"), ["Name is required."]);
|
|
291
289
|
assert.deepEqual(resolveCrudFieldErrors({ name: "Name is required." }, "email"), []);
|
|
292
290
|
});
|
|
293
|
-
|
|
294
|
-
test("parseCrudResourceOperationInput validates and normalizes operation body payloads", () => {
|
|
295
|
-
const resource = {
|
|
296
|
-
operations: {
|
|
297
|
-
create: {
|
|
298
|
-
bodyValidator: {
|
|
299
|
-
schema: Type.Object(
|
|
300
|
-
{
|
|
301
|
-
name: Type.String({ minLength: 1 }),
|
|
302
|
-
age: Type.Integer({ minimum: 1 })
|
|
303
|
-
},
|
|
304
|
-
{ additionalProperties: false }
|
|
305
|
-
),
|
|
306
|
-
normalize(payload = {}) {
|
|
307
|
-
return {
|
|
308
|
-
name: String(payload.name || "").trim(),
|
|
309
|
-
age: Number(payload.age)
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const validResult = parseCrudResourceOperationInput({
|
|
318
|
-
resource,
|
|
319
|
-
operationName: "create",
|
|
320
|
-
rawPayload: {
|
|
321
|
-
name: " Ada ",
|
|
322
|
-
age: "2"
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
assert.equal(validResult.ok, true);
|
|
326
|
-
assert.deepEqual(validResult.value, {
|
|
327
|
-
name: "Ada",
|
|
328
|
-
age: 2
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const invalidResult = parseCrudResourceOperationInput({
|
|
332
|
-
resource,
|
|
333
|
-
operationName: "create",
|
|
334
|
-
rawPayload: {
|
|
335
|
-
name: " ",
|
|
336
|
-
age: "0"
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
assert.equal(invalidResult.ok, false);
|
|
340
|
-
});
|
|
@@ -53,7 +53,7 @@ test("useCrudListFilters manages values, query params, chips, and presets", asyn
|
|
|
53
53
|
|
|
54
54
|
assert.equal(filters.queryParams.onlyStaff.value, true);
|
|
55
55
|
assert.deepEqual(filters.queryParams.status.value, ["active"]);
|
|
56
|
-
assert.equal(filters.queryParams.
|
|
56
|
+
assert.equal(filters.queryParams.arrivalDate.value, "2026-04-01..");
|
|
57
57
|
assert.equal(filters.hasActiveFilters.value, true);
|
|
58
58
|
assert.deepEqual(
|
|
59
59
|
filters.activeChips.value.map((chip) => chip.label),
|
|
@@ -131,6 +131,7 @@ test("useCrudListFilters supports dynamic presets and preset matching", async ()
|
|
|
131
131
|
|
|
132
132
|
assert.equal(filters.values.arrivalDate.from, "2026-04-18");
|
|
133
133
|
assert.equal(filters.values.arrivalDate.to, "2026-04-18");
|
|
134
|
+
assert.equal(filters.queryParams.arrivalDate.value, "2026-04-18");
|
|
134
135
|
assert.deepEqual(filters.values.status, ["archived"]);
|
|
135
136
|
assert.equal(filters.matchesPreset("today"), true);
|
|
136
137
|
assert.equal(filters.matchesPreset("all-dates"), false);
|
|
@@ -145,7 +146,7 @@ test("useCrudListFilters supports dynamic presets and preset matching", async ()
|
|
|
145
146
|
assert.equal(filters.matchesPreset("all-dates"), true);
|
|
146
147
|
});
|
|
147
148
|
|
|
148
|
-
test("useCrudListFilters preset matching
|
|
149
|
+
test("useCrudListFilters preset matching ignores invalid extra enumMany values outside the shared contract", async () => {
|
|
149
150
|
const { useCrudListFilters } = await import("@jskit-ai/users-web/client/composables/useCrudListFilters");
|
|
150
151
|
|
|
151
152
|
const filters = useCrudListFilters(
|
|
@@ -174,9 +175,37 @@ test("useCrudListFilters preset matching rejects extra enumMany values present i
|
|
|
174
175
|
|
|
175
176
|
filters.values.status = ["archived", "bogus"];
|
|
176
177
|
|
|
177
|
-
assert.equal(filters.matchesPreset("archived-only"),
|
|
178
|
+
assert.equal(filters.matchesPreset("archived-only"), true);
|
|
179
|
+
assert.deepEqual(filters.queryParams.status.value, ["archived"]);
|
|
178
180
|
assert.deepEqual(
|
|
179
181
|
filters.activeChips.value.map((chip) => chip.label),
|
|
180
|
-
["Status: Archived"
|
|
182
|
+
["Status: Archived"]
|
|
181
183
|
);
|
|
182
184
|
});
|
|
185
|
+
|
|
186
|
+
test("useCrudListFilters hydrates single-key range query params into structured values", async () => {
|
|
187
|
+
const { useCrudListFilters } = await import("@jskit-ai/users-web/client/composables/useCrudListFilters");
|
|
188
|
+
|
|
189
|
+
const filters = useCrudListFilters({
|
|
190
|
+
arrivalDate: {
|
|
191
|
+
type: "dateRange",
|
|
192
|
+
label: "Arrival Date"
|
|
193
|
+
},
|
|
194
|
+
weight: {
|
|
195
|
+
type: "numberRange",
|
|
196
|
+
label: "Weight"
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
filters.queryParams.arrivalDate.value = "..2026-04-30";
|
|
201
|
+
filters.queryParams.weight.value = "12.5..18";
|
|
202
|
+
|
|
203
|
+
assert.deepEqual(filters.values.arrivalDate, {
|
|
204
|
+
from: "",
|
|
205
|
+
to: "2026-04-30"
|
|
206
|
+
});
|
|
207
|
+
assert.deepEqual(filters.values.weight, {
|
|
208
|
+
min: "12.5",
|
|
209
|
+
max: "18"
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
3
4
|
import { resolveCrudListParentDescriptor, resolveCrudListParentRecordTitle, resolveCrudListParentTitleFromItems } from "../src/client/composables/internal/crudListParentTitleSupport.js";
|
|
4
5
|
|
|
5
6
|
const contactChildResource = Object.freeze({
|
|
@@ -8,24 +9,30 @@ const contactChildResource = Object.freeze({
|
|
|
8
9
|
containerKey: "lookups"
|
|
9
10
|
}
|
|
10
11
|
},
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
operations: {
|
|
13
|
+
view: {
|
|
14
|
+
output: {
|
|
15
|
+
schema: createSchema({
|
|
16
|
+
contactId: {
|
|
17
|
+
type: "integer",
|
|
18
|
+
relation: {
|
|
19
|
+
kind: "lookup",
|
|
20
|
+
namespace: "contacts",
|
|
21
|
+
valueKey: "id"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
serviceId: {
|
|
25
|
+
type: "integer",
|
|
26
|
+
relation: {
|
|
27
|
+
kind: "lookup",
|
|
28
|
+
namespace: "services",
|
|
29
|
+
valueKey: "id"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
29
36
|
});
|
|
30
37
|
|
|
31
38
|
test("resolveCrudListParentDescriptor selects the nearest lookup route parent", () => {
|
|
@@ -116,17 +123,23 @@ test("resolveCrudListParentRecordTitle falls back to entity label plus id", () =
|
|
|
116
123
|
test("resolveCrudListParentDescriptor supports parentRouteParamKey aliases", () => {
|
|
117
124
|
const descriptor = resolveCrudListParentDescriptor({
|
|
118
125
|
resource: {
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
operations: {
|
|
127
|
+
view: {
|
|
128
|
+
output: {
|
|
129
|
+
schema: createSchema({
|
|
130
|
+
staffContactId: {
|
|
131
|
+
type: "integer",
|
|
132
|
+
parentRouteParamKey: "contactId",
|
|
133
|
+
relation: {
|
|
134
|
+
kind: "lookup",
|
|
135
|
+
namespace: "contacts",
|
|
136
|
+
valueKey: "id"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
|
-
|
|
142
|
+
}
|
|
130
143
|
},
|
|
131
144
|
route: {
|
|
132
145
|
matched: [{ path: "/w/:workspaceSlug/admin/contacts/:contactId/availabilities" }],
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
3
|
+
import { buildRequestQueryObject } from "../src/client/composables/support/requestQueryRuntimeSupport.js";
|
|
4
4
|
|
|
5
|
-
test("
|
|
6
|
-
assert.
|
|
7
|
-
|
|
5
|
+
test("buildRequestQueryObject preserves repeated keys as arrays deterministically", () => {
|
|
6
|
+
assert.deepEqual(
|
|
7
|
+
buildRequestQueryObject([
|
|
8
8
|
{ key: "include", values: ["vetId", "linkedUserId", "pets", "pets.breedId"] },
|
|
9
9
|
{ key: "limit", values: ["10"] }
|
|
10
10
|
]),
|
|
11
|
-
|
|
11
|
+
{
|
|
12
|
+
include: ["vetId", "linkedUserId", "pets", "pets.breedId"],
|
|
13
|
+
limit: "10"
|
|
14
|
+
}
|
|
12
15
|
);
|
|
13
16
|
});
|
|
14
17
|
|
|
15
|
-
test("
|
|
16
|
-
assert.
|
|
17
|
-
appendRequestQueryEntriesToPath("/api/w/dev-admin/contacts/1", []),
|
|
18
|
-
"/api/w/dev-admin/contacts/1"
|
|
19
|
-
);
|
|
18
|
+
test("buildRequestQueryObject returns an empty object when no request params are active", () => {
|
|
19
|
+
assert.deepEqual(buildRequestQueryObject([]), {});
|
|
20
20
|
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
2
|
-
|
|
3
|
-
function appendRequestQueryEntriesToPath(path = "", entries = []) {
|
|
4
|
-
const normalizedPath = String(path || "").trim();
|
|
5
|
-
if (!normalizedPath) {
|
|
6
|
-
return "";
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const sourceEntries = Array.isArray(entries) ? entries : [];
|
|
10
|
-
const searchParams = new URLSearchParams();
|
|
11
|
-
for (const entry of sourceEntries) {
|
|
12
|
-
const key = String(entry?.key || "").trim();
|
|
13
|
-
const values = Array.isArray(entry?.values) ? entry.values : [];
|
|
14
|
-
if (!key || values.length < 1) {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
for (const value of values) {
|
|
19
|
-
searchParams.append(key, value);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const serializedSearch = searchParams.toString();
|
|
24
|
-
if (!serializedSearch) {
|
|
25
|
-
return normalizedPath;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return appendQueryString(normalizedPath, serializedSearch);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export { appendRequestQueryEntriesToPath };
|