@jskit-ai/users-web 0.1.31 → 0.1.33
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 -6
- package/package.json +7 -7
- package/src/client/components/WorkspaceMembersClientElement.vue +3 -8
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -2
- package/src/client/components/WorkspacesClientElement.vue +3 -8
- package/src/client/composables/addEditUiRuntime.js +120 -0
- package/src/client/composables/crudSchemaFormHelpers.js +152 -0
- package/src/client/composables/listUiRuntime.js +111 -0
- package/src/client/composables/operationAdapters.js +34 -0
- package/src/client/composables/routeTemplateHelpers.js +110 -0
- package/src/client/composables/useAccountSettingsRuntime.js +10 -14
- package/src/client/composables/useAddEdit.js +37 -5
- package/src/client/composables/useCrudSchemaForm.js +177 -0
- package/src/client/composables/useList.js +31 -4
- package/src/client/composables/useView.js +43 -5
- package/src/client/composables/viewUiRuntime.js +87 -0
- package/src/client/lib/theme.js +2 -3
- package/src/client/providers/UsersWebClientProvider.js +14 -39
- package/src/client/runtime/bootstrapPlacementRuntime.js +14 -24
- package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +0 -4
- package/test/addEditUiRuntime.test.js +75 -0
- package/test/bootstrapPlacementRuntime.test.js +45 -52
- package/test/listUiRuntime.test.js +81 -0
- package/test/useCrudSchemaForm.test.js +141 -0
- package/test/viewUiRuntime.test.js +60 -0
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CLIENT_MODULE_ROUTER_TOKEN,
|
|
3
|
-
CLIENT_MODULE_VUE_APP_TOKEN
|
|
4
|
-
} from "@jskit-ai/kernel/client/moduleBootstrap";
|
|
5
|
-
import { WEB_PLACEMENT_RUNTIME_CLIENT_TOKEN } from "@jskit-ai/shell-web/client/placement";
|
|
6
|
-
import { REALTIME_SOCKET_CLIENT_TOKEN } from "@jskit-ai/realtime/client/tokens";
|
|
7
|
-
import { USERS_BOOTSTRAP_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
|
|
8
1
|
import {
|
|
9
2
|
findWorkspaceBySlug,
|
|
10
3
|
normalizeWorkspaceList,
|
|
@@ -20,8 +13,6 @@ import {
|
|
|
20
13
|
} from "../lib/theme.js";
|
|
21
14
|
import { createBootstrapPlacementRouteGuards } from "./bootstrapPlacementRouteGuards.js";
|
|
22
15
|
import {
|
|
23
|
-
BOOTSTRAP_PLACEMENT_SOURCE,
|
|
24
|
-
USERS_WEB_BOOTSTRAP_PLACEMENT_RUNTIME_TOKEN,
|
|
25
16
|
WORKSPACE_BOOTSTRAP_STATUS_ERROR,
|
|
26
17
|
WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN,
|
|
27
18
|
WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND,
|
|
@@ -44,7 +35,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
44
35
|
if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
|
|
45
36
|
throw new Error("createBootstrapPlacementRuntime requires application has()/make().");
|
|
46
37
|
}
|
|
47
|
-
if (!app.has(
|
|
38
|
+
if (!app.has("runtime.web-placement.client")) {
|
|
48
39
|
throw new Error("createBootstrapPlacementRuntime requires shell-web placement runtime.");
|
|
49
40
|
}
|
|
50
41
|
if (typeof fetchBootstrap !== "function") {
|
|
@@ -52,12 +43,12 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
const runtimeLogger = logger || createProviderLogger(app);
|
|
55
|
-
const placementRuntime = app.make(
|
|
56
|
-
const router = app.has(
|
|
46
|
+
const placementRuntime = app.make("runtime.web-placement.client");
|
|
47
|
+
const router = app.has("jskit.client.router") ? app.make("jskit.client.router") : null;
|
|
57
48
|
let vuetifyThemeController = resolveVuetifyThemeController(
|
|
58
|
-
app.has(
|
|
49
|
+
app.has("jskit.client.vue.app") ? app.make("jskit.client.vue.app") : null
|
|
59
50
|
);
|
|
60
|
-
const socket = app.has(
|
|
51
|
+
const socket = app.has("runtime.realtime.client.socket") ? app.make("runtime.realtime.client.socket") : null;
|
|
61
52
|
const cleanup = [];
|
|
62
53
|
let refreshQueue = Promise.resolve();
|
|
63
54
|
let shutdownRequested = false;
|
|
@@ -78,7 +69,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
78
69
|
routeGuards.shutdown();
|
|
79
70
|
});
|
|
80
71
|
|
|
81
|
-
function setWorkspaceBootstrapStatus(workspaceSlug = "", status = "", source =
|
|
72
|
+
function setWorkspaceBootstrapStatus(workspaceSlug = "", status = "", source = "users-web.bootstrap-placement") {
|
|
82
73
|
const workspaceSlugKey = normalizeWorkspaceSlugKey(workspaceSlug);
|
|
83
74
|
const normalizedStatus = normalizeWorkspaceBootstrapStatus(status);
|
|
84
75
|
if (!workspaceSlugKey || !normalizedStatus) {
|
|
@@ -103,7 +94,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
103
94
|
const payload = Object.freeze({
|
|
104
95
|
workspaceSlug: workspaceSlugKey,
|
|
105
96
|
status: normalizedStatus,
|
|
106
|
-
source: String(source ||
|
|
97
|
+
source: String(source || "users-web.bootstrap-placement").trim() || "users-web.bootstrap-placement"
|
|
107
98
|
});
|
|
108
99
|
for (const listener of workspaceBootstrapStatusListeners) {
|
|
109
100
|
try {
|
|
@@ -131,7 +122,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
131
122
|
};
|
|
132
123
|
}
|
|
133
124
|
|
|
134
|
-
function writePlacementContext(payload = {}, state = {}, source =
|
|
125
|
+
function writePlacementContext(payload = {}, state = {}, source = "users-web.bootstrap-placement") {
|
|
135
126
|
const availableWorkspaces = normalizeWorkspaceList(payload?.workspaces);
|
|
136
127
|
const currentWorkspace = findWorkspaceBySlug(availableWorkspaces, state.workspaceSlug);
|
|
137
128
|
const workspaceSettings =
|
|
@@ -162,7 +153,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
162
153
|
applyWorkspaceColorFromPlacementContext("write");
|
|
163
154
|
}
|
|
164
155
|
|
|
165
|
-
function clearPlacementContext(source =
|
|
156
|
+
function clearPlacementContext(source = "users-web.bootstrap-placement") {
|
|
166
157
|
placementRuntime.setContext(
|
|
167
158
|
{
|
|
168
159
|
workspace: null,
|
|
@@ -186,11 +177,11 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
186
177
|
if (vuetifyThemeController) {
|
|
187
178
|
return vuetifyThemeController;
|
|
188
179
|
}
|
|
189
|
-
if (!app.has(
|
|
180
|
+
if (!app.has("jskit.client.vue.app")) {
|
|
190
181
|
return null;
|
|
191
182
|
}
|
|
192
183
|
|
|
193
|
-
vuetifyThemeController = resolveVuetifyThemeController(app.make(
|
|
184
|
+
vuetifyThemeController = resolveVuetifyThemeController(app.make("jskit.client.vue.app"));
|
|
194
185
|
return vuetifyThemeController;
|
|
195
186
|
}
|
|
196
187
|
|
|
@@ -267,7 +258,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
267
258
|
}
|
|
268
259
|
|
|
269
260
|
const stateAtStart = resolveRouteState(placementRuntime, router);
|
|
270
|
-
const source =
|
|
261
|
+
const source = `users-web.bootstrap-placement.${String(reason || "manual").trim() || "manual"}`;
|
|
271
262
|
try {
|
|
272
263
|
const payload = await fetchBootstrap(stateAtStart.workspaceSlug);
|
|
273
264
|
const stateAtApply = resolveRouteState(placementRuntime, router);
|
|
@@ -433,10 +424,10 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
433
424
|
const handleBootstrapChanged = () => {
|
|
434
425
|
void queueRefresh("realtime");
|
|
435
426
|
};
|
|
436
|
-
socket.on(
|
|
427
|
+
socket.on("users.bootstrap.changed", handleBootstrapChanged);
|
|
437
428
|
cleanup.push(() => {
|
|
438
429
|
if (typeof socket.off === "function") {
|
|
439
|
-
socket.off(
|
|
430
|
+
socket.off("users.bootstrap.changed", handleBootstrapChanged);
|
|
440
431
|
}
|
|
441
432
|
});
|
|
442
433
|
}
|
|
@@ -463,7 +454,6 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
463
454
|
}
|
|
464
455
|
|
|
465
456
|
export {
|
|
466
|
-
USERS_WEB_BOOTSTRAP_PLACEMENT_RUNTIME_TOKEN,
|
|
467
457
|
WORKSPACE_BOOTSTRAP_STATUS_RESOLVED,
|
|
468
458
|
WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND,
|
|
469
459
|
WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const USERS_WEB_BOOTSTRAP_PLACEMENT_RUNTIME_TOKEN = "users.web.bootstrap-placement.runtime";
|
|
2
|
-
const BOOTSTRAP_PLACEMENT_SOURCE = "users-web.bootstrap-placement";
|
|
3
1
|
const WORKSPACE_BOOTSTRAP_STATUS_RESOLVED = "resolved";
|
|
4
2
|
const WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND = "not_found";
|
|
5
3
|
const WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN = "forbidden";
|
|
@@ -18,9 +16,7 @@ const WORKSPACE_BOOTSTRAP_STATUSES = new Set([
|
|
|
18
16
|
]);
|
|
19
17
|
|
|
20
18
|
export {
|
|
21
|
-
BOOTSTRAP_PLACEMENT_SOURCE,
|
|
22
19
|
SHELL_GUARD_EVALUATOR_KEY,
|
|
23
|
-
USERS_WEB_BOOTSTRAP_PLACEMENT_RUNTIME_TOKEN,
|
|
24
20
|
WORKSPACE_BOOTSTRAP_STATUSES,
|
|
25
21
|
WORKSPACE_BOOTSTRAP_STATUS_ERROR,
|
|
26
22
|
WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { createAddEditUiRuntime } from "../src/client/composables/addEditUiRuntime.js";
|
|
5
|
+
|
|
6
|
+
test("createAddEditUiRuntime resolves api/list/cancel paths from route params", () => {
|
|
7
|
+
const runtime = createAddEditUiRuntime({
|
|
8
|
+
recordIdParam: "addressId",
|
|
9
|
+
routeParams: ref({
|
|
10
|
+
contactId: "7",
|
|
11
|
+
addressId: "42"
|
|
12
|
+
}),
|
|
13
|
+
routePath: ref("/contacts/7/addresses/new"),
|
|
14
|
+
apiUrlTemplate: "/crud/contacts/:contactId/addresses/:addressId",
|
|
15
|
+
viewUrlTemplate: "../:addressId",
|
|
16
|
+
listUrlTemplate: ".."
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
assert.equal(runtime.recordId.value, "42");
|
|
20
|
+
assert.equal(runtime.apiSuffix.value, "/crud/contacts/7/addresses/42");
|
|
21
|
+
assert.equal(runtime.listUrl.value, "/contacts/7/addresses");
|
|
22
|
+
assert.equal(runtime.cancelUrl.value, "/contacts/7/addresses/42");
|
|
23
|
+
assert.equal(runtime.resolveParams("../:addressId"), "/contacts/7/addresses/42");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("createAddEditUiRuntime resolves view urls for saved payload ids with nested params", () => {
|
|
27
|
+
const runtime = createAddEditUiRuntime({
|
|
28
|
+
recordIdParam: "addressId",
|
|
29
|
+
routeParams: ref({
|
|
30
|
+
contactId: "7"
|
|
31
|
+
}),
|
|
32
|
+
viewUrlTemplate: "/contacts/:contactId/addresses/:addressId"
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(runtime.resolveSavedViewUrl({ id: 99 }), "/contacts/7/addresses/99");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("createAddEditUiRuntime resolves edit-page relative list and cancel links", () => {
|
|
39
|
+
const runtime = createAddEditUiRuntime({
|
|
40
|
+
recordIdParam: "addressId",
|
|
41
|
+
routeParams: ref({
|
|
42
|
+
contactId: "7",
|
|
43
|
+
addressId: "42"
|
|
44
|
+
}),
|
|
45
|
+
routePath: ref("/contacts/7/addresses/42/edit"),
|
|
46
|
+
viewUrlTemplate: "..",
|
|
47
|
+
listUrlTemplate: "../.."
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.equal(runtime.listUrl.value, "/contacts/7/addresses");
|
|
51
|
+
assert.equal(runtime.cancelUrl.value, "/contacts/7/addresses/42");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("createAddEditUiRuntime supports custom saved-record selector", () => {
|
|
55
|
+
const runtime = createAddEditUiRuntime({
|
|
56
|
+
recordIdParam: "addressId",
|
|
57
|
+
routeParams: ref({
|
|
58
|
+
contactId: "7"
|
|
59
|
+
}),
|
|
60
|
+
viewUrlTemplate: "/contacts/:contactId/addresses/:addressId",
|
|
61
|
+
saveRecordIdSelector: (payload = {}) => payload.uuid
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.equal(runtime.resolveSavedViewUrl({ uuid: "abc-123" }), "/contacts/7/addresses/abc-123");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("createAddEditUiRuntime validates route parameter names", () => {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() =>
|
|
70
|
+
createAddEditUiRuntime({
|
|
71
|
+
recordIdParam: "record-id"
|
|
72
|
+
}),
|
|
73
|
+
/recordIdParam "record-id" is invalid/
|
|
74
|
+
);
|
|
75
|
+
});
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
CLIENT_MODULE_ROUTER_TOKEN,
|
|
5
|
-
CLIENT_MODULE_VUE_APP_TOKEN
|
|
6
|
-
} from "@jskit-ai/kernel/client/moduleBootstrap";
|
|
7
|
-
import { WEB_PLACEMENT_RUNTIME_CLIENT_TOKEN } from "@jskit-ai/shell-web/client/placement";
|
|
8
|
-
import { REALTIME_SOCKET_CLIENT_TOKEN } from "@jskit-ai/realtime/client/tokens";
|
|
9
|
-
import { USERS_BOOTSTRAP_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
|
|
10
3
|
import { resolveWorkspaceThemePalette } from "@jskit-ai/users-core/shared/settings";
|
|
11
4
|
import { ThemeSymbol } from "vuetify/lib/composables/theme.js";
|
|
12
5
|
import {
|
|
@@ -244,8 +237,8 @@ test("bootstrap placement runtime writes user/workspace/permissions into placeme
|
|
|
244
237
|
const fetchCalls = [];
|
|
245
238
|
const runtime = createBootstrapPlacementRuntime({
|
|
246
239
|
app: createAppStub({
|
|
247
|
-
[
|
|
248
|
-
[
|
|
240
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
241
|
+
["jskit.client.router"]: router
|
|
249
242
|
}),
|
|
250
243
|
fetchBootstrap: async (workspaceSlug) => {
|
|
251
244
|
fetchCalls.push(workspaceSlug);
|
|
@@ -304,8 +297,8 @@ test("bootstrap placement runtime resolves workspace slug from pathname when sur
|
|
|
304
297
|
const fetchCalls = [];
|
|
305
298
|
const runtime = createBootstrapPlacementRuntime({
|
|
306
299
|
app: createAppStub({
|
|
307
|
-
[
|
|
308
|
-
[
|
|
300
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
301
|
+
["jskit.client.router"]: router
|
|
309
302
|
}),
|
|
310
303
|
fetchBootstrap: async (workspaceSlug) => {
|
|
311
304
|
fetchCalls.push(workspaceSlug);
|
|
@@ -348,8 +341,8 @@ test("bootstrap placement runtime does not mutate placement auth context", async
|
|
|
348
341
|
const router = createRouterStub("/w/acme/dashboard");
|
|
349
342
|
const runtime = createBootstrapPlacementRuntime({
|
|
350
343
|
app: createAppStub({
|
|
351
|
-
[
|
|
352
|
-
[
|
|
344
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
345
|
+
["jskit.client.router"]: router
|
|
353
346
|
}),
|
|
354
347
|
fetchBootstrap: async () => {
|
|
355
348
|
return {
|
|
@@ -385,9 +378,9 @@ test("bootstrap placement runtime refetches on route changes and users.bootstrap
|
|
|
385
378
|
const fetchCalls = [];
|
|
386
379
|
const runtime = createBootstrapPlacementRuntime({
|
|
387
380
|
app: createAppStub({
|
|
388
|
-
[
|
|
389
|
-
[
|
|
390
|
-
[
|
|
381
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
382
|
+
["jskit.client.router"]: router,
|
|
383
|
+
["runtime.realtime.client.socket"]: socket
|
|
391
384
|
}),
|
|
392
385
|
fetchBootstrap: async (workspaceSlug) => {
|
|
393
386
|
fetchCalls.push(workspaceSlug);
|
|
@@ -424,7 +417,7 @@ test("bootstrap placement runtime refetches on route changes and users.bootstrap
|
|
|
424
417
|
await flushTasks();
|
|
425
418
|
assert.deepEqual(fetchCalls, ["acme", "zen"]);
|
|
426
419
|
|
|
427
|
-
socket.emit(
|
|
420
|
+
socket.emit("users.bootstrap.changed", {});
|
|
428
421
|
await flushTasks();
|
|
429
422
|
assert.deepEqual(fetchCalls, ["acme", "zen", "zen"]);
|
|
430
423
|
});
|
|
@@ -435,8 +428,8 @@ test("bootstrap placement runtime refetches when auth context changes", async ()
|
|
|
435
428
|
const fetchCalls = [];
|
|
436
429
|
const runtime = createBootstrapPlacementRuntime({
|
|
437
430
|
app: createAppStub({
|
|
438
|
-
[
|
|
439
|
-
[
|
|
431
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
432
|
+
["jskit.client.router"]: router
|
|
440
433
|
}),
|
|
441
434
|
fetchBootstrap: async (workspaceSlug) => {
|
|
442
435
|
fetchCalls.push(workspaceSlug);
|
|
@@ -497,9 +490,9 @@ test("bootstrap placement runtime applies persisted theme preference for unauthe
|
|
|
497
490
|
};
|
|
498
491
|
const runtime = createBootstrapPlacementRuntime({
|
|
499
492
|
app: createAppStub({
|
|
500
|
-
[
|
|
501
|
-
[
|
|
502
|
-
[
|
|
493
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
494
|
+
["jskit.client.router"]: router,
|
|
495
|
+
["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
|
|
503
496
|
}),
|
|
504
497
|
fetchBootstrap: async () => {
|
|
505
498
|
return {
|
|
@@ -532,10 +525,10 @@ test("bootstrap placement runtime reapplies theme when bootstrap payload changes
|
|
|
532
525
|
let fetchCount = 0;
|
|
533
526
|
const runtime = createBootstrapPlacementRuntime({
|
|
534
527
|
app: createAppStub({
|
|
535
|
-
[
|
|
536
|
-
[
|
|
537
|
-
[
|
|
538
|
-
[
|
|
528
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
529
|
+
["jskit.client.router"]: router,
|
|
530
|
+
["runtime.realtime.client.socket"]: socket,
|
|
531
|
+
["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
|
|
539
532
|
}),
|
|
540
533
|
fetchBootstrap: async (workspaceSlug) => {
|
|
541
534
|
fetchCount += 1;
|
|
@@ -563,7 +556,7 @@ test("bootstrap placement runtime reapplies theme when bootstrap payload changes
|
|
|
563
556
|
await runtime.initialize();
|
|
564
557
|
assert.equal(themeController.global.name.value, "workspace-dark");
|
|
565
558
|
|
|
566
|
-
socket.emit(
|
|
559
|
+
socket.emit("users.bootstrap.changed", {});
|
|
567
560
|
await flushTasks();
|
|
568
561
|
assert.equal(themeController.global.name.value, "workspace-light");
|
|
569
562
|
});
|
|
@@ -574,9 +567,9 @@ test("bootstrap placement runtime applies workspace palette via Vuetify workspac
|
|
|
574
567
|
const themeController = createVuetifyThemeController("light");
|
|
575
568
|
const runtime = createBootstrapPlacementRuntime({
|
|
576
569
|
app: createAppStub({
|
|
577
|
-
[
|
|
578
|
-
[
|
|
579
|
-
[
|
|
570
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
571
|
+
["jskit.client.router"]: router,
|
|
572
|
+
["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
|
|
580
573
|
}),
|
|
581
574
|
fetchBootstrap: async (workspaceSlug) => {
|
|
582
575
|
return {
|
|
@@ -646,8 +639,8 @@ test("bootstrap placement runtime marks workspace slug as not_found and clears w
|
|
|
646
639
|
|
|
647
640
|
const runtime = createBootstrapPlacementRuntime({
|
|
648
641
|
app: createAppStub({
|
|
649
|
-
[
|
|
650
|
-
[
|
|
642
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
643
|
+
["jskit.client.router"]: createRouterStub("/w/acme/dashboard")
|
|
651
644
|
}),
|
|
652
645
|
fetchBootstrap: async () => {
|
|
653
646
|
const error = new Error("Workspace not found.");
|
|
@@ -674,8 +667,8 @@ test("bootstrap placement runtime updates status per workspace slug across route
|
|
|
674
667
|
const router = createRouterStub("/w/acme/dashboard");
|
|
675
668
|
const runtime = createBootstrapPlacementRuntime({
|
|
676
669
|
app: createAppStub({
|
|
677
|
-
[
|
|
678
|
-
[
|
|
670
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
671
|
+
["jskit.client.router"]: router
|
|
679
672
|
}),
|
|
680
673
|
fetchBootstrap: async (workspaceSlug) => {
|
|
681
674
|
if (workspaceSlug === "zen") {
|
|
@@ -721,8 +714,8 @@ test("bootstrap placement runtime uses requestedWorkspace status and keeps globa
|
|
|
721
714
|
const router = createRouterStub("/w/tonymobily");
|
|
722
715
|
const runtime = createBootstrapPlacementRuntime({
|
|
723
716
|
app: createAppStub({
|
|
724
|
-
[
|
|
725
|
-
[
|
|
717
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
718
|
+
["jskit.client.router"]: router
|
|
726
719
|
}),
|
|
727
720
|
fetchBootstrap: async () => {
|
|
728
721
|
return {
|
|
@@ -763,8 +756,8 @@ test("bootstrap placement runtime uses requestedWorkspace=not_found without forc
|
|
|
763
756
|
const router = createRouterStub("/w/missing");
|
|
764
757
|
const runtime = createBootstrapPlacementRuntime({
|
|
765
758
|
app: createAppStub({
|
|
766
|
-
[
|
|
767
|
-
[
|
|
759
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
760
|
+
["jskit.client.router"]: router
|
|
768
761
|
}),
|
|
769
762
|
fetchBootstrap: async () => {
|
|
770
763
|
return {
|
|
@@ -811,8 +804,8 @@ test("bootstrap placement runtime guard wrapper preserves delegated deny outcome
|
|
|
811
804
|
|
|
812
805
|
const runtime = createBootstrapPlacementRuntime({
|
|
813
806
|
app: createAppStub({
|
|
814
|
-
[
|
|
815
|
-
[
|
|
807
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
808
|
+
["jskit.client.router"]: router
|
|
816
809
|
}),
|
|
817
810
|
fetchBootstrap: async () => {
|
|
818
811
|
return {
|
|
@@ -854,8 +847,8 @@ test("bootstrap placement runtime guard wrapper blocks forbidden workspace route
|
|
|
854
847
|
|
|
855
848
|
const runtime = createBootstrapPlacementRuntime({
|
|
856
849
|
app: createAppStub({
|
|
857
|
-
[
|
|
858
|
-
[
|
|
850
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
851
|
+
["jskit.client.router"]: router
|
|
859
852
|
}),
|
|
860
853
|
fetchBootstrap: async () => {
|
|
861
854
|
return {
|
|
@@ -899,8 +892,8 @@ test("bootstrap placement runtime guard wrapper redirects nested not_found route
|
|
|
899
892
|
const router = createRouterStub("/w/acme/dashboard");
|
|
900
893
|
const runtime = createBootstrapPlacementRuntime({
|
|
901
894
|
app: createAppStub({
|
|
902
|
-
[
|
|
903
|
-
[
|
|
895
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
896
|
+
["jskit.client.router"]: router
|
|
904
897
|
}),
|
|
905
898
|
fetchBootstrap: async () => {
|
|
906
899
|
const error = new Error("Not found");
|
|
@@ -957,8 +950,8 @@ test("bootstrap placement runtime redirects admin nested route to admin root whe
|
|
|
957
950
|
const router = createRouterStub("/w/acme/admin/workspace/settings");
|
|
958
951
|
const runtime = createBootstrapPlacementRuntime({
|
|
959
952
|
app: createAppStub({
|
|
960
|
-
[
|
|
961
|
-
[
|
|
953
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
954
|
+
["jskit.client.router"]: router
|
|
962
955
|
}),
|
|
963
956
|
fetchBootstrap: async () => {
|
|
964
957
|
const error = new Error("Not found");
|
|
@@ -977,8 +970,8 @@ test("bootstrap placement runtime redirects forbidden workspace route to workspa
|
|
|
977
970
|
const router = createRouterStub("/w/acme/admin/workspace/settings?tab=general");
|
|
978
971
|
const runtime = createBootstrapPlacementRuntime({
|
|
979
972
|
app: createAppStub({
|
|
980
|
-
[
|
|
981
|
-
[
|
|
973
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
974
|
+
["jskit.client.router"]: router
|
|
982
975
|
}),
|
|
983
976
|
fetchBootstrap: async () => {
|
|
984
977
|
const error = new Error("Forbidden");
|
|
@@ -1032,8 +1025,8 @@ test("bootstrap placement runtime enforces surface access policies after bootstr
|
|
|
1032
1025
|
const router = createRouterStub("/console");
|
|
1033
1026
|
const runtime = createBootstrapPlacementRuntime({
|
|
1034
1027
|
app: createAppStub({
|
|
1035
|
-
[
|
|
1036
|
-
[
|
|
1028
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
1029
|
+
["jskit.client.router"]: router
|
|
1037
1030
|
}),
|
|
1038
1031
|
fetchBootstrap: async () => {
|
|
1039
1032
|
return {
|
|
@@ -1059,8 +1052,8 @@ test("bootstrap placement runtime captures guard evaluator assignments after ini
|
|
|
1059
1052
|
const router = createRouterStub("/w/acme/dashboard");
|
|
1060
1053
|
const runtime = createBootstrapPlacementRuntime({
|
|
1061
1054
|
app: createAppStub({
|
|
1062
|
-
[
|
|
1063
|
-
[
|
|
1055
|
+
["runtime.web-placement.client"]: placementRuntime,
|
|
1056
|
+
["jskit.client.router"]: router
|
|
1064
1057
|
}),
|
|
1065
1058
|
fetchBootstrap: async () => {
|
|
1066
1059
|
return {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { createListUiRuntime } from "../src/client/composables/listUiRuntime.js";
|
|
5
|
+
|
|
6
|
+
test("createListUiRuntime resolves row keys and relative route templates from string record ids", () => {
|
|
7
|
+
const items = ref([{ uuid: "abc 123" }]);
|
|
8
|
+
const runtime = createListUiRuntime({
|
|
9
|
+
items,
|
|
10
|
+
isInitialLoading: ref(false),
|
|
11
|
+
recordIdParam: "contactId",
|
|
12
|
+
recordIdSelector: (item) => item.uuid,
|
|
13
|
+
routePath: ref("/w/acme/admin/contacts"),
|
|
14
|
+
viewUrlTemplate: "./:contactId",
|
|
15
|
+
editUrlTemplate: "./:contactId/edit"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
assert.equal(runtime.actionColumnCount, 2);
|
|
19
|
+
assert.equal(runtime.resolveRowKey(items.value[0], 0), "abc 123");
|
|
20
|
+
assert.equal(runtime.resolveParams("./new"), "/w/acme/admin/contacts/new");
|
|
21
|
+
assert.equal(runtime.resolveViewUrl(items.value[0]), "/w/acme/admin/contacts/abc%20123");
|
|
22
|
+
assert.equal(runtime.resolveEditUrl(items.value[0]), "/w/acme/admin/contacts/abc%20123/edit");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("createListUiRuntime resolves templates that depend on existing route params", () => {
|
|
26
|
+
const runtime = createListUiRuntime({
|
|
27
|
+
items: ref([{ id: 42 }]),
|
|
28
|
+
isInitialLoading: ref(false),
|
|
29
|
+
recordIdParam: "addressId",
|
|
30
|
+
recordIdSelector: (item) => item.id,
|
|
31
|
+
routeParams: ref({ contactId: "7" }),
|
|
32
|
+
viewUrlTemplate: "/contacts/:contactId/addresses/:addressId",
|
|
33
|
+
editUrlTemplate: "/contacts/:contactId/addresses/:addressId/edit"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.equal(runtime.resolveViewUrl({ id: 42 }), "/contacts/7/addresses/42");
|
|
37
|
+
assert.equal(runtime.resolveEditUrl({ id: 42 }), "/contacts/7/addresses/42/edit");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("createListUiRuntime returns empty urls and fallback row keys when record id is missing", () => {
|
|
41
|
+
const runtime = createListUiRuntime({
|
|
42
|
+
items: ref([]),
|
|
43
|
+
isInitialLoading: ref(false),
|
|
44
|
+
recordIdParam: "recordId",
|
|
45
|
+
recordIdSelector: () => "",
|
|
46
|
+
viewUrlTemplate: "./:recordId",
|
|
47
|
+
editUrlTemplate: "./:recordId/edit"
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.equal(runtime.resolveRowKey({}, 5), "row-5");
|
|
51
|
+
assert.equal(runtime.resolveViewUrl({}), "");
|
|
52
|
+
assert.equal(runtime.resolveEditUrl({}), "");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("createListUiRuntime computes list skeleton state from loading and item count", () => {
|
|
56
|
+
const items = ref([]);
|
|
57
|
+
const isInitialLoading = ref(true);
|
|
58
|
+
const runtime = createListUiRuntime({
|
|
59
|
+
items,
|
|
60
|
+
isInitialLoading
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.equal(runtime.showListSkeleton.value, true);
|
|
64
|
+
items.value = [{}];
|
|
65
|
+
assert.equal(runtime.showListSkeleton.value, false);
|
|
66
|
+
items.value = [];
|
|
67
|
+
isInitialLoading.value = false;
|
|
68
|
+
assert.equal(runtime.showListSkeleton.value, false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("createListUiRuntime validates route parameter names", () => {
|
|
72
|
+
assert.throws(
|
|
73
|
+
() =>
|
|
74
|
+
createListUiRuntime({
|
|
75
|
+
items: ref([]),
|
|
76
|
+
isInitialLoading: ref(false),
|
|
77
|
+
recordIdParam: "record-id"
|
|
78
|
+
}),
|
|
79
|
+
/recordIdParam "record-id" is invalid/
|
|
80
|
+
);
|
|
81
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { reactive } from "vue";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import {
|
|
6
|
+
normalizeCrudFormFields,
|
|
7
|
+
createCrudFormModel,
|
|
8
|
+
buildCrudFormPayload,
|
|
9
|
+
applyCrudPayloadToForm,
|
|
10
|
+
resolveCrudFieldErrors,
|
|
11
|
+
parseCrudResourceOperationInput
|
|
12
|
+
} from "../src/client/composables/crudSchemaFormHelpers.js";
|
|
13
|
+
|
|
14
|
+
test("normalizeCrudFormFields trims keys, removes invalid entries, and deduplicates", () => {
|
|
15
|
+
const fields = normalizeCrudFormFields([
|
|
16
|
+
{ key: " name ", type: "string" },
|
|
17
|
+
{ key: "name", type: "string" },
|
|
18
|
+
{ key: "", type: "string" },
|
|
19
|
+
null,
|
|
20
|
+
{ key: "active", type: "boolean" }
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(fields.map((field) => field.key), ["name", "active"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("createCrudFormModel resolves defaults and supports explicit initial values", () => {
|
|
27
|
+
const model = createCrudFormModel([
|
|
28
|
+
{ key: "name", type: "string" },
|
|
29
|
+
{ key: "active", type: "boolean" },
|
|
30
|
+
{ key: "role", type: "string", initialValue: "member" }
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(model, {
|
|
34
|
+
name: "",
|
|
35
|
+
active: false,
|
|
36
|
+
role: "member"
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("buildCrudFormPayload normalizes booleans and numbers while skipping empty numeric values", () => {
|
|
41
|
+
const payload = buildCrudFormPayload(
|
|
42
|
+
[
|
|
43
|
+
{ key: "name", type: "string" },
|
|
44
|
+
{ key: "active", type: "boolean" },
|
|
45
|
+
{ key: "age", type: "integer" },
|
|
46
|
+
{ key: "score", type: "number" }
|
|
47
|
+
],
|
|
48
|
+
{
|
|
49
|
+
name: "Ada",
|
|
50
|
+
active: 1,
|
|
51
|
+
age: "42",
|
|
52
|
+
score: ""
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(payload, {
|
|
57
|
+
name: "Ada",
|
|
58
|
+
active: true,
|
|
59
|
+
age: 42
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("applyCrudPayloadToForm maps payload values into reactive form model", () => {
|
|
64
|
+
const form = reactive({
|
|
65
|
+
name: "",
|
|
66
|
+
active: false,
|
|
67
|
+
age: ""
|
|
68
|
+
});
|
|
69
|
+
applyCrudPayloadToForm(
|
|
70
|
+
[
|
|
71
|
+
{ key: "name", type: "string" },
|
|
72
|
+
{ key: "active", type: "boolean" },
|
|
73
|
+
{ key: "age", type: "integer" }
|
|
74
|
+
],
|
|
75
|
+
form,
|
|
76
|
+
{
|
|
77
|
+
name: "Grace",
|
|
78
|
+
active: 1,
|
|
79
|
+
age: 33
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
assert.deepEqual(form, {
|
|
84
|
+
name: "Grace",
|
|
85
|
+
active: true,
|
|
86
|
+
age: "33"
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("resolveCrudFieldErrors returns Vuetify-compatible error arrays", () => {
|
|
91
|
+
assert.deepEqual(resolveCrudFieldErrors({ name: "Name is required." }, "name"), ["Name is required."]);
|
|
92
|
+
assert.deepEqual(resolveCrudFieldErrors({ name: "Name is required." }, "email"), []);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("parseCrudResourceOperationInput validates and normalizes operation body payloads", () => {
|
|
96
|
+
const resource = {
|
|
97
|
+
operations: {
|
|
98
|
+
create: {
|
|
99
|
+
bodyValidator: {
|
|
100
|
+
schema: Type.Object(
|
|
101
|
+
{
|
|
102
|
+
name: Type.String({ minLength: 1 }),
|
|
103
|
+
age: Type.Integer({ minimum: 1 })
|
|
104
|
+
},
|
|
105
|
+
{ additionalProperties: false }
|
|
106
|
+
),
|
|
107
|
+
normalize(payload = {}) {
|
|
108
|
+
return {
|
|
109
|
+
name: String(payload.name || "").trim(),
|
|
110
|
+
age: Number(payload.age)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const validResult = parseCrudResourceOperationInput({
|
|
119
|
+
resource,
|
|
120
|
+
operationName: "create",
|
|
121
|
+
rawPayload: {
|
|
122
|
+
name: " Ada ",
|
|
123
|
+
age: "2"
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
assert.equal(validResult.ok, true);
|
|
127
|
+
assert.deepEqual(validResult.value, {
|
|
128
|
+
name: "Ada",
|
|
129
|
+
age: 2
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const invalidResult = parseCrudResourceOperationInput({
|
|
133
|
+
resource,
|
|
134
|
+
operationName: "create",
|
|
135
|
+
rawPayload: {
|
|
136
|
+
name: " ",
|
|
137
|
+
age: "0"
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
assert.equal(invalidResult.ok, false);
|
|
141
|
+
});
|