@jskit-ai/users-web 0.1.37 → 0.1.40
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 +13 -244
- package/package.json +17 -13
- package/src/client/components/ConsoleSettingsClientElement.vue +1 -1
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
- package/src/client/components/WorkspaceMembersClientElement.vue +3 -3
- package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
- package/src/client/components/WorkspacesClientElement.vue +2 -2
- package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
- package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
- package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +1 -1
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +1 -1
- package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +6 -2
- package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +155 -2
- package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
- package/src/client/composables/internal/useOperationScope.js +1 -1
- package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +9 -9
- package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
- package/src/client/composables/records/useCrudList.js +83 -0
- package/src/client/composables/records/useCrudView.js +35 -0
- package/src/client/composables/{useList.js → records/useList.js} +31 -57
- package/src/client/composables/{useView.js → records/useView.js} +6 -9
- package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +2 -2
- package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +2 -2
- package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
- package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
- package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
- package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
- package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +2 -2
- package/src/client/composables/useAccess.js +2 -2
- package/src/client/composables/useAccountSettingsRuntime.js +6 -6
- package/src/client/composables/useBootstrapQuery.js +1 -1
- package/src/client/composables/useCommand.js +5 -5
- package/src/client/composables/useCrudListParentTitle.js +131 -0
- package/src/client/composables/usePagedCollection.js +3 -3
- package/src/client/composables/useScopeRuntime.js +1 -1
- package/src/client/index.js +1 -0
- package/src/client/providers/UsersWebClientProvider.js +0 -12
- package/src/client/providers/UsersWorkspacesClientProvider.js +26 -0
- package/src/client/support/menuLinkTarget.js +93 -0
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +16 -9
- package/templates/src/pages/console/settings/index.vue +1 -0
- package/test/addEditUiRuntime.test.js +1 -1
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudLookupFieldRuntime.test.js +1 -1
- package/test/errorMessageHelpers.test.js +1 -1
- package/test/exportsContract.test.js +10 -1
- package/test/listQueryParamSupport.test.js +1 -1
- package/test/listUiRuntime.test.js +1 -1
- package/test/menuLinkTarget.test.js +116 -0
- package/test/permissions.test.js +2 -2
- package/test/refValueHelpers.test.js +1 -1
- package/test/resourceLoadStateHelpers.test.js +1 -1
- package/test/routeTemplateHelpers.test.js +1 -1
- package/test/scopeHelpers.test.js +1 -1
- package/test/settingsPlacementContract.test.js +44 -0
- package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
- package/test/useCrudListParentTitle.test.js +143 -0
- package/test/useListSearchSupport.test.js +1 -1
- package/test/viewCoreLoading.test.js +1 -1
- package/test/viewUiRuntime.test.js +1 -1
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -95
- package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +0 -162
- package/templates/src/components/WorkspaceNotFoundCard.vue +0 -34
- package/templates/src/composables/useWorkspaceNotFoundState.js +0 -41
- package/templates/src/pages/admin/members/index.vue +0 -7
- package/templates/src/pages/admin/workspace/settings/index.vue +0 -16
- package/templates/src/surfaces/admin/index.vue +0 -29
- package/templates/src/surfaces/admin/root.vue +0 -20
- package/templates/src/surfaces/app/index.vue +0 -27
- package/templates/src/surfaces/app/root.vue +0 -20
- /package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -0
- /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
- /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
- /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
- /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
- /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
- /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
- /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
- /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
- /package/src/client/composables/{listQueryParamSupport.js → support/listQueryParamSupport.js} +0 -0
- /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
- /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
- /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
- /package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +0 -0
- /package/src/client/composables/{scopeHelpers.js → support/scopeHelpers.js} +0 -0
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import UsersWorkspaceSelector from "../components/UsersWorkspaceSelector.vue";
|
|
2
|
-
import UsersWorkspaceToolsWidget from "../components/UsersWorkspaceToolsWidget.vue";
|
|
3
1
|
import UsersShellMenuLinkItem from "../components/UsersShellMenuLinkItem.vue";
|
|
4
2
|
import UsersSurfaceAwareMenuLinkItem from "../components/UsersSurfaceAwareMenuLinkItem.vue";
|
|
5
3
|
import UsersProfileSurfaceSwitchMenuItem from "../components/UsersProfileSurfaceSwitchMenuItem.vue";
|
|
6
|
-
import UsersWorkspaceSettingsMenuItem from "../components/UsersWorkspaceSettingsMenuItem.vue";
|
|
7
|
-
import UsersWorkspaceMembersMenuItem from "../components/UsersWorkspaceMembersMenuItem.vue";
|
|
8
4
|
import ProfileClientElement from "../components/ProfileClientElement.vue";
|
|
9
|
-
import MembersAdminClientElement from "../components/MembersAdminClientElement.vue";
|
|
10
|
-
import WorkspaceSettingsClientElement from "../components/WorkspaceSettingsClientElement.vue";
|
|
11
5
|
import {
|
|
12
6
|
createBootstrapPlacementRuntime
|
|
13
7
|
} from "../runtime/bootstrapPlacementRuntime.js";
|
|
@@ -21,16 +15,10 @@ class UsersWebClientProvider {
|
|
|
21
15
|
throw new Error("UsersWebClientProvider requires application singleton().");
|
|
22
16
|
}
|
|
23
17
|
|
|
24
|
-
app.singleton("users.web.workspace.selector", () => UsersWorkspaceSelector);
|
|
25
|
-
app.singleton("users.web.workspace.tools.widget", () => UsersWorkspaceToolsWidget);
|
|
26
18
|
app.singleton("users.web.shell.menu-link-item", () => UsersShellMenuLinkItem);
|
|
27
19
|
app.singleton("users.web.shell.surface-aware-menu-link-item", () => UsersSurfaceAwareMenuLinkItem);
|
|
28
20
|
app.singleton("users.web.profile.menu.surface-switch-item", () => UsersProfileSurfaceSwitchMenuItem);
|
|
29
|
-
app.singleton("users.web.workspace-settings.menu-item", () => UsersWorkspaceSettingsMenuItem);
|
|
30
|
-
app.singleton("users.web.workspace-members.menu-item", () => UsersWorkspaceMembersMenuItem);
|
|
31
21
|
app.singleton("users.web.profile.element", () => ProfileClientElement);
|
|
32
|
-
app.singleton("users.web.members-admin.element", () => MembersAdminClientElement);
|
|
33
|
-
app.singleton("users.web.workspace-settings.element", () => WorkspaceSettingsClientElement);
|
|
34
22
|
app.singleton("users.web.bootstrap-placement.runtime", (scope) => createBootstrapPlacementRuntime({ app: scope }));
|
|
35
23
|
}
|
|
36
24
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import UsersWorkspaceSelector from "../components/UsersWorkspaceSelector.vue";
|
|
2
|
+
import UsersWorkspaceToolsWidget from "../components/UsersWorkspaceToolsWidget.vue";
|
|
3
|
+
import UsersWorkspaceSettingsMenuItem from "../components/UsersWorkspaceSettingsMenuItem.vue";
|
|
4
|
+
import UsersWorkspaceMembersMenuItem from "../components/UsersWorkspaceMembersMenuItem.vue";
|
|
5
|
+
import MembersAdminClientElement from "../components/MembersAdminClientElement.vue";
|
|
6
|
+
import WorkspaceSettingsClientElement from "../components/WorkspaceSettingsClientElement.vue";
|
|
7
|
+
|
|
8
|
+
class UsersWorkspacesClientProvider {
|
|
9
|
+
static id = "workspaces.web.client";
|
|
10
|
+
static dependsOn = ["users.web.client"];
|
|
11
|
+
|
|
12
|
+
register(app) {
|
|
13
|
+
if (!app || typeof app.singleton !== "function") {
|
|
14
|
+
throw new Error("UsersWorkspacesClientProvider requires application singleton().");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
app.singleton("users.web.workspace.selector", () => UsersWorkspaceSelector);
|
|
18
|
+
app.singleton("users.web.workspace.tools.widget", () => UsersWorkspaceToolsWidget);
|
|
19
|
+
app.singleton("users.web.workspace-settings.menu-item", () => UsersWorkspaceSettingsMenuItem);
|
|
20
|
+
app.singleton("users.web.workspace-members.menu-item", () => UsersWorkspaceMembersMenuItem);
|
|
21
|
+
app.singleton("users.web.members-admin.element", () => MembersAdminClientElement);
|
|
22
|
+
app.singleton("users.web.workspace-settings.element", () => WorkspaceSettingsClientElement);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { UsersWorkspacesClientProvider };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { surfaceRequiresWorkspaceFromPlacementContext } from "../lib/workspaceSurfaceContext.js";
|
|
3
|
+
|
|
4
|
+
function normalizeMenuLinkPathname(pathname = "") {
|
|
5
|
+
const source = String(pathname || "").trim();
|
|
6
|
+
if (!source) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const queryIndex = source.indexOf("?");
|
|
11
|
+
const hashIndex = source.indexOf("#");
|
|
12
|
+
const cutoff =
|
|
13
|
+
queryIndex < 0
|
|
14
|
+
? hashIndex
|
|
15
|
+
: hashIndex < 0
|
|
16
|
+
? queryIndex
|
|
17
|
+
: Math.min(queryIndex, hashIndex);
|
|
18
|
+
|
|
19
|
+
return cutoff < 0 ? source : source.slice(0, cutoff);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveMenuLinkSurfaceId(surface = "", fallbackSurfaceId = "") {
|
|
23
|
+
const explicitSurface = normalizeText(surface).toLowerCase();
|
|
24
|
+
if (explicitSurface && explicitSurface !== "*") {
|
|
25
|
+
return explicitSurface;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalizeText(fallbackSurfaceId).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function interpolateBracketParams(pathTemplate = "", params = {}) {
|
|
32
|
+
const source = String(pathTemplate || "").trim();
|
|
33
|
+
if (!source) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return source.replace(/\[([^\]]+)\]/g, (_match, rawKey) => {
|
|
38
|
+
const key = String(rawKey || "").trim();
|
|
39
|
+
if (!key) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const value = params?.[key];
|
|
44
|
+
return value == null ? `[${key}]` : encodeURIComponent(String(value));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRelativeMenuLinkTarget(target = "") {
|
|
49
|
+
const normalizedTarget = normalizeText(target);
|
|
50
|
+
return normalizedTarget.startsWith("./") || normalizedTarget.startsWith("../");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveMenuLinkTarget({
|
|
54
|
+
to = "",
|
|
55
|
+
surface = "",
|
|
56
|
+
currentSurfaceId = "",
|
|
57
|
+
placementContext = null,
|
|
58
|
+
workspaceSuffix = "/",
|
|
59
|
+
nonWorkspaceSuffix = "/",
|
|
60
|
+
routeParams = {},
|
|
61
|
+
resolvePagePath = null
|
|
62
|
+
} = {}) {
|
|
63
|
+
const explicitTarget = normalizeText(to);
|
|
64
|
+
const targetSurfaceId = resolveMenuLinkSurfaceId(surface, currentSurfaceId);
|
|
65
|
+
const workspaceRequired = surfaceRequiresWorkspaceFromPlacementContext(placementContext, targetSurfaceId);
|
|
66
|
+
const suffixTemplate = normalizeText(workspaceRequired ? workspaceSuffix : nonWorkspaceSuffix) || "/";
|
|
67
|
+
const interpolatedSuffix = interpolateBracketParams(suffixTemplate, routeParams);
|
|
68
|
+
const resolvedSuffixTarget =
|
|
69
|
+
typeof resolvePagePath === "function" &&
|
|
70
|
+
targetSurfaceId &&
|
|
71
|
+
interpolatedSuffix &&
|
|
72
|
+
!interpolatedSuffix.includes("[")
|
|
73
|
+
? normalizeText(resolvePagePath(interpolatedSuffix, {
|
|
74
|
+
surface: targetSurfaceId,
|
|
75
|
+
mode: "auto"
|
|
76
|
+
}))
|
|
77
|
+
: "";
|
|
78
|
+
|
|
79
|
+
if (!explicitTarget) {
|
|
80
|
+
return resolvedSuffixTarget;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isRelativeMenuLinkTarget(explicitTarget)) {
|
|
84
|
+
return resolvedSuffixTarget;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return explicitTarget;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
normalizeMenuLinkPathname,
|
|
92
|
+
resolveMenuLinkTarget
|
|
93
|
+
};
|
|
@@ -12,17 +12,24 @@ const runtime = useAccountSettingsRuntime();
|
|
|
12
12
|
const route = useRoute();
|
|
13
13
|
const router = useRouter();
|
|
14
14
|
|
|
15
|
-
const sections =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
]
|
|
21
|
-
|
|
15
|
+
const sections = computed(() => {
|
|
16
|
+
const nextSections = [
|
|
17
|
+
{ title: "Profile", value: "profile" },
|
|
18
|
+
{ title: "Preferences", value: "preferences" },
|
|
19
|
+
{ title: "Notifications", value: "notifications" }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
if (runtime.invites.isAvailable.value) {
|
|
23
|
+
nextSections.push({ title: "Invites", value: "invites" });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Object.freeze(nextSections);
|
|
27
|
+
});
|
|
28
|
+
const sectionValues = computed(() => Object.freeze(sections.value.map((section) => section.value)));
|
|
22
29
|
|
|
23
30
|
function normalizeSection(value) {
|
|
24
31
|
const source = Array.isArray(value) ? value[0] : value;
|
|
25
|
-
return normalizeOneOf(source, sectionValues, "profile");
|
|
32
|
+
return normalizeOneOf(source, sectionValues.value, "profile");
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
function readRouteSection() {
|
|
@@ -61,7 +68,7 @@ const activeTab = computed({
|
|
|
61
68
|
<v-card class="panel-card" rounded="lg" elevation="1" border>
|
|
62
69
|
<v-card-item>
|
|
63
70
|
<v-card-title class="panel-title">Account settings</v-card-title>
|
|
64
|
-
<v-card-subtitle>Global profile, preferences, notifications, and
|
|
71
|
+
<v-card-subtitle>Global profile, preferences, notifications, and account controls.</v-card-subtitle>
|
|
65
72
|
<template #append>
|
|
66
73
|
<v-btn
|
|
67
74
|
variant="text"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ref } from "vue";
|
|
4
|
-
import { createAddEditUiRuntime } from "../src/client/composables/addEditUiRuntime.js";
|
|
4
|
+
import { createAddEditUiRuntime } from "../src/client/composables/runtime/addEditUiRuntime.js";
|
|
5
5
|
|
|
6
6
|
test("createAddEditUiRuntime resolves api/list/cancel paths from route params", () => {
|
|
7
7
|
const runtime = createAddEditUiRuntime({
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
CRUD_BINDING_MODE_ROUTE,
|
|
6
|
+
CRUD_BINDING_MODE_MERGE,
|
|
7
|
+
CRUD_BINDING_MODE_EXPLICIT,
|
|
8
|
+
CRUD_BINDING_MODE_NONE,
|
|
9
|
+
normalizeCrudBindingMode,
|
|
10
|
+
resolveCrudBoundValues
|
|
11
|
+
} from "../src/client/composables/crud/crudBindingSupport.js";
|
|
12
|
+
|
|
13
|
+
test("normalizeCrudBindingMode defaults invalid values to route", () => {
|
|
14
|
+
assert.equal(normalizeCrudBindingMode(""), CRUD_BINDING_MODE_ROUTE);
|
|
15
|
+
assert.equal(normalizeCrudBindingMode("unknown"), CRUD_BINDING_MODE_ROUTE);
|
|
16
|
+
assert.equal(normalizeCrudBindingMode("merge"), CRUD_BINDING_MODE_MERGE);
|
|
17
|
+
assert.equal(normalizeCrudBindingMode("explicit"), CRUD_BINDING_MODE_EXPLICIT);
|
|
18
|
+
assert.equal(normalizeCrudBindingMode("none"), CRUD_BINDING_MODE_NONE);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("resolveCrudBoundValues returns route values in route mode", () => {
|
|
22
|
+
const values = resolveCrudBoundValues({
|
|
23
|
+
binding: {
|
|
24
|
+
mode: "route",
|
|
25
|
+
values: {
|
|
26
|
+
contactId: "22"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
routeValues: {
|
|
30
|
+
contactId: "11"
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.deepEqual(values, {
|
|
35
|
+
contactId: "11"
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("resolveCrudBoundValues merges explicit values over route values in merge mode", () => {
|
|
40
|
+
const values = resolveCrudBoundValues({
|
|
41
|
+
binding: {
|
|
42
|
+
mode: "merge",
|
|
43
|
+
values: {
|
|
44
|
+
contactId: "22",
|
|
45
|
+
serviceId: "4"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
routeValues: {
|
|
49
|
+
contactId: "11"
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(values, {
|
|
54
|
+
contactId: "22",
|
|
55
|
+
serviceId: "4"
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("resolveCrudBoundValues uses only explicit values in explicit mode", () => {
|
|
60
|
+
const values = resolveCrudBoundValues({
|
|
61
|
+
binding: {
|
|
62
|
+
mode: "explicit",
|
|
63
|
+
values: {
|
|
64
|
+
serviceId: "4"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
routeValues: {
|
|
68
|
+
contactId: "11"
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
assert.deepEqual(values, {
|
|
73
|
+
serviceId: "4"
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("resolveCrudBoundValues disables automatic binding in none mode", () => {
|
|
78
|
+
const values = resolveCrudBoundValues({
|
|
79
|
+
binding: {
|
|
80
|
+
mode: "none",
|
|
81
|
+
values: {
|
|
82
|
+
serviceId: "4"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
routeValues: {
|
|
86
|
+
contactId: "11"
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
assert.deepEqual(values, {});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("resolveCrudBoundValues unwraps reactive binding config and values", () => {
|
|
94
|
+
const values = resolveCrudBoundValues({
|
|
95
|
+
binding: ref({
|
|
96
|
+
mode: "merge",
|
|
97
|
+
values: ref({
|
|
98
|
+
serviceId: "4"
|
|
99
|
+
})
|
|
100
|
+
}),
|
|
101
|
+
routeValues: {
|
|
102
|
+
contactId: "11"
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(values, {
|
|
107
|
+
contactId: "11",
|
|
108
|
+
serviceId: "4"
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
resolveLookupItemLabel,
|
|
5
5
|
resolveLookupFieldDisplayValue,
|
|
6
6
|
resolveRecordTitle
|
|
7
|
-
} from "../src/client/composables/crudLookupFieldLabelSupport.js";
|
|
7
|
+
} from "../src/client/composables/crud/crudLookupFieldLabelSupport.js";
|
|
8
8
|
|
|
9
9
|
test("resolveLookupItemLabel composes name + surname", () => {
|
|
10
10
|
assert.equal(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { toQueryErrorMessage, toUiErrorMessage } from "../src/client/composables/errorMessageHelpers.js";
|
|
3
|
+
import { toQueryErrorMessage, toUiErrorMessage } from "../src/client/composables/support/errorMessageHelpers.js";
|
|
4
4
|
|
|
5
5
|
test("toQueryErrorMessage returns empty when query has no error", () => {
|
|
6
6
|
assert.equal(toQueryErrorMessage(null, "Unable to load list."), "");
|
|
@@ -13,7 +13,16 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
13
13
|
repoRoot: REPO_ROOT,
|
|
14
14
|
packageDir: PACKAGE_DIR,
|
|
15
15
|
packageId: "@jskit-ai/users-web",
|
|
16
|
-
requiredExports: [
|
|
16
|
+
requiredExports: [
|
|
17
|
+
"./client",
|
|
18
|
+
"./client/composables/useAddEdit",
|
|
19
|
+
"./client/composables/useList",
|
|
20
|
+
"./client/composables/useView",
|
|
21
|
+
"./client/composables/useCrudAddEdit",
|
|
22
|
+
"./client/composables/useCrudList",
|
|
23
|
+
"./client/composables/useCrudView",
|
|
24
|
+
"./client/support/menuLinkTarget"
|
|
25
|
+
]
|
|
17
26
|
});
|
|
18
27
|
|
|
19
28
|
assert.deepEqual(
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
buildRouteQueryCompareToken,
|
|
13
13
|
mergeManagedQueryParamKeyHistory,
|
|
14
14
|
resolveRouteSyncManagedKeys
|
|
15
|
-
} from "../src/client/composables/listQueryParamSupport.js";
|
|
15
|
+
} from "../src/client/composables/support/listQueryParamSupport.js";
|
|
16
16
|
|
|
17
17
|
test("normalizeListSyncToRouteConfig defaults", () => {
|
|
18
18
|
assert.deepEqual(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ref } from "vue";
|
|
4
|
-
import { createListUiRuntime } from "../src/client/composables/listUiRuntime.js";
|
|
4
|
+
import { createListUiRuntime } from "../src/client/composables/runtime/listUiRuntime.js";
|
|
5
5
|
|
|
6
6
|
test("createListUiRuntime resolves row keys and relative route templates from string record ids", () => {
|
|
7
7
|
const items = ref([{ uuid: "abc 123" }]);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
normalizeMenuLinkPathname,
|
|
5
|
+
resolveMenuLinkTarget
|
|
6
|
+
} from "../src/client/support/menuLinkTarget.js";
|
|
7
|
+
|
|
8
|
+
const WORKSPACE_PLACEMENT_CONTEXT = Object.freeze({
|
|
9
|
+
surfaceConfig: {
|
|
10
|
+
enabledSurfaceIds: ["admin"],
|
|
11
|
+
surfacesById: {
|
|
12
|
+
admin: {
|
|
13
|
+
id: "admin",
|
|
14
|
+
requiresWorkspace: true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const MIXED_PLACEMENT_CONTEXT = Object.freeze({
|
|
21
|
+
surfaceConfig: {
|
|
22
|
+
enabledSurfaceIds: ["app", "admin"],
|
|
23
|
+
surfacesById: {
|
|
24
|
+
app: {
|
|
25
|
+
id: "app",
|
|
26
|
+
requiresWorkspace: false
|
|
27
|
+
},
|
|
28
|
+
admin: {
|
|
29
|
+
id: "admin",
|
|
30
|
+
requiresWorkspace: true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function createPageResolver() {
|
|
37
|
+
return function resolvePagePath(relativePath = "", options = {}) {
|
|
38
|
+
return `page:${String(options.surface || "")}:${String(relativePath || "")}`;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("resolveMenuLinkTarget resolves suffix targets when no explicit to is provided", () => {
|
|
43
|
+
assert.equal(
|
|
44
|
+
resolveMenuLinkTarget({
|
|
45
|
+
surface: "admin",
|
|
46
|
+
placementContext: WORKSPACE_PLACEMENT_CONTEXT,
|
|
47
|
+
workspaceSuffix: "/practice/vets",
|
|
48
|
+
nonWorkspaceSuffix: "/practice/vets",
|
|
49
|
+
resolvePagePath: createPageResolver()
|
|
50
|
+
}),
|
|
51
|
+
"page:admin:/practice/vets"
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("resolveMenuLinkTarget resolves relative targets through suffix templates", () => {
|
|
56
|
+
assert.equal(
|
|
57
|
+
resolveMenuLinkTarget({
|
|
58
|
+
to: "./notes",
|
|
59
|
+
surface: "admin",
|
|
60
|
+
placementContext: WORKSPACE_PLACEMENT_CONTEXT,
|
|
61
|
+
workspaceSuffix: "/contacts/[contactId]/notes",
|
|
62
|
+
nonWorkspaceSuffix: "/contacts/[contactId]/notes",
|
|
63
|
+
routeParams: {
|
|
64
|
+
contactId: 42
|
|
65
|
+
},
|
|
66
|
+
resolvePagePath: createPageResolver()
|
|
67
|
+
}),
|
|
68
|
+
"page:admin:/contacts/42/notes"
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("resolveMenuLinkTarget returns empty string for unresolved relative targets", () => {
|
|
73
|
+
assert.equal(
|
|
74
|
+
resolveMenuLinkTarget({
|
|
75
|
+
to: "./notes",
|
|
76
|
+
surface: "admin",
|
|
77
|
+
placementContext: WORKSPACE_PLACEMENT_CONTEXT,
|
|
78
|
+
workspaceSuffix: "/contacts/[contactId]/notes",
|
|
79
|
+
nonWorkspaceSuffix: "/contacts/[contactId]/notes",
|
|
80
|
+
resolvePagePath: createPageResolver()
|
|
81
|
+
}),
|
|
82
|
+
""
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("resolveMenuLinkTarget keeps absolute targets unchanged", () => {
|
|
87
|
+
assert.equal(
|
|
88
|
+
resolveMenuLinkTarget({
|
|
89
|
+
to: "/practice/vets",
|
|
90
|
+
surface: "admin",
|
|
91
|
+
placementContext: WORKSPACE_PLACEMENT_CONTEXT,
|
|
92
|
+
workspaceSuffix: "/ignored",
|
|
93
|
+
nonWorkspaceSuffix: "/ignored",
|
|
94
|
+
resolvePagePath: createPageResolver()
|
|
95
|
+
}),
|
|
96
|
+
"/practice/vets"
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("resolveMenuLinkTarget uses non-workspace suffix for non-workspace surfaces", () => {
|
|
101
|
+
assert.equal(
|
|
102
|
+
resolveMenuLinkTarget({
|
|
103
|
+
surface: "app",
|
|
104
|
+
currentSurfaceId: "admin",
|
|
105
|
+
placementContext: MIXED_PLACEMENT_CONTEXT,
|
|
106
|
+
workspaceSuffix: "/workspace-only",
|
|
107
|
+
nonWorkspaceSuffix: "/public-page",
|
|
108
|
+
resolvePagePath: createPageResolver()
|
|
109
|
+
}),
|
|
110
|
+
"page:app:/public-page"
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("normalizeMenuLinkPathname removes query strings and hashes", () => {
|
|
115
|
+
assert.equal(normalizeMenuLinkPathname("/practice/vets?tab=all#section"), "/practice/vets");
|
|
116
|
+
});
|
package/test/permissions.test.js
CHANGED
|
@@ -30,6 +30,6 @@ test("arePermissionListsEqual returns false for different permission sets", () =
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test("hasPermission supports namespace wildcard matches", () => {
|
|
33
|
-
assert.equal(hasPermission(["
|
|
34
|
-
assert.equal(hasPermission(["
|
|
33
|
+
assert.equal(hasPermission(["crud.contacts.*"], "crud.contacts.update"), true);
|
|
34
|
+
assert.equal(hasPermission(["crud.contacts.*"], "crud.projects.update"), false);
|
|
35
35
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { computed, ref } from "vue";
|
|
4
|
-
import { resolveEnabledRef } from "../src/client/composables/refValueHelpers.js";
|
|
4
|
+
import { resolveEnabledRef } from "../src/client/composables/support/refValueHelpers.js";
|
|
5
5
|
|
|
6
6
|
test("resolveEnabledRef unwraps refs", () => {
|
|
7
7
|
assert.equal(resolveEnabledRef(ref(true)), true);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ref } from "vue";
|
|
4
|
-
import { hasResolvedQueryData } from "../src/client/composables/resourceLoadStateHelpers.js";
|
|
4
|
+
import { hasResolvedQueryData } from "../src/client/composables/support/resourceLoadStateHelpers.js";
|
|
5
5
|
|
|
6
6
|
test("hasResolvedQueryData returns true when the query succeeded", () => {
|
|
7
7
|
const query = {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
extractRouteParamNames,
|
|
5
5
|
resolveScopedRoutePathname,
|
|
6
6
|
resolveRouteParamNamesInOrder
|
|
7
|
-
} from "../src/client/composables/routeTemplateHelpers.js";
|
|
7
|
+
} from "../src/client/composables/support/routeTemplateHelpers.js";
|
|
8
8
|
|
|
9
9
|
test("extractRouteParamNames reads dynamic params from route templates", () => {
|
|
10
10
|
assert.deepEqual(
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
normalizeOwnershipFilter,
|
|
7
7
|
resolveApiSuffix,
|
|
8
8
|
resolveResourceMessages
|
|
9
|
-
} from "../src/client/composables/scopeHelpers.js";
|
|
9
|
+
} from "../src/client/composables/support/scopeHelpers.js";
|
|
10
10
|
|
|
11
11
|
test("resolveResourceMessages merges defaults with resource messages", () => {
|
|
12
12
|
const messages = resolveResourceMessages(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import descriptor from "../package.descriptor.mjs";
|
|
7
|
+
|
|
8
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
10
|
+
|
|
11
|
+
function readSettingsOutlets() {
|
|
12
|
+
const outlets = descriptor?.metadata?.ui?.placements?.outlets;
|
|
13
|
+
return Array.isArray(outlets)
|
|
14
|
+
? outlets.filter((entry) => String(entry?.host || "").trim() === "console-settings")
|
|
15
|
+
: [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("users-web console settings template exposes surface-derived settings outlets", async () => {
|
|
19
|
+
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "console", "settings", "index.vue"), "utf8");
|
|
20
|
+
|
|
21
|
+
assert.match(source, /<ShellOutlet host="console-settings" position="primary-menu" \/>/);
|
|
22
|
+
assert.match(source, /<ShellOutlet host="console-settings" position="forms" \/>/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("users-web descriptor metadata advertises console settings outlets with standard positions", () => {
|
|
26
|
+
const outlets = readSettingsOutlets();
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
outlets,
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
host: "console-settings",
|
|
32
|
+
position: "primary-menu",
|
|
33
|
+
surfaces: ["console"],
|
|
34
|
+
source: "templates/src/pages/console/settings/index.vue"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
host: "console-settings",
|
|
38
|
+
position: "forms",
|
|
39
|
+
surfaces: ["console"],
|
|
40
|
+
source: "templates/src/pages/console/settings/index.vue"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
);
|
|
44
|
+
});
|