@jskit-ai/users-web 0.1.36 → 0.1.38
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 +8 -22
- package/package.json +16 -11
- package/src/client/components/MembersAdminClientElement.vue +5 -5
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
- package/src/client/components/WorkspaceMembersClientElement.vue +19 -19
- package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
- package/src/client/components/WorkspacesClientElement.vue +4 -4
- 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/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -4
- package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +2 -2
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +37 -5
- package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +11 -4
- package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +178 -5
- 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} +18 -8
- 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/records/useList.js +482 -0
- package/src/client/composables/{useView.js → records/useView.js} +7 -7
- package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +13 -4
- package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +20 -8
- 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} +13 -4
- package/src/client/composables/support/listQueryParamSupport.js +459 -0
- package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +122 -0
- 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 +58 -7
- package/src/client/composables/useScopeRuntime.js +1 -1
- package/src/client/lib/bootstrap.js +1 -1
- package/src/client/lib/menuIcons.js +27 -6
- package/src/client/support/menuLinkTarget.js +93 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
- package/test/addEditUiRuntime.test.js +19 -1
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudLookupFieldRuntime.test.js +52 -2
- package/test/errorMessageHelpers.test.js +1 -1
- package/test/exportsContract.test.js +10 -1
- package/test/listQueryParamSupport.test.js +190 -0
- package/test/listUiRuntime.test.js +22 -1
- package/test/menuIcons.test.js +2 -0
- 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 +57 -1
- package/test/scopeHelpers.test.js +1 -1
- 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/usePagedCollection.test.js +53 -0
- package/test/viewCoreLoading.test.js +1 -1
- package/test/viewUiRuntime.test.js +36 -1
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -241
- package/src/client/composables/useList.js +0 -268
- /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/{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/{scopeHelpers.js → support/scopeHelpers.js} +0 -0
|
@@ -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
|
+
});
|
|
@@ -2,8 +2,9 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
resolveLookupItemLabel,
|
|
5
|
-
resolveLookupFieldDisplayValue
|
|
6
|
-
|
|
5
|
+
resolveLookupFieldDisplayValue,
|
|
6
|
+
resolveRecordTitle
|
|
7
|
+
} from "../src/client/composables/crud/crudLookupFieldLabelSupport.js";
|
|
7
8
|
|
|
8
9
|
test("resolveLookupItemLabel composes name + surname", () => {
|
|
9
10
|
assert.equal(
|
|
@@ -31,6 +32,19 @@ test("resolveLookupItemLabel composes firstName + surname", () => {
|
|
|
31
32
|
);
|
|
32
33
|
});
|
|
33
34
|
|
|
35
|
+
test("resolveLookupItemLabel composes firstName + lastName", () => {
|
|
36
|
+
assert.equal(
|
|
37
|
+
resolveLookupItemLabel(
|
|
38
|
+
{
|
|
39
|
+
firstName: "Ana",
|
|
40
|
+
lastName: "Marin"
|
|
41
|
+
},
|
|
42
|
+
"name"
|
|
43
|
+
),
|
|
44
|
+
"Ana Marin"
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
34
48
|
test("resolveLookupItemLabel falls back to explicit labelKey", () => {
|
|
35
49
|
assert.equal(
|
|
36
50
|
resolveLookupItemLabel(
|
|
@@ -59,6 +73,42 @@ test("resolveLookupItemLabel returns empty when no label fields match", () => {
|
|
|
59
73
|
assert.equal(resolveLookupItemLabel({ id: 42 }, "name"), "");
|
|
60
74
|
});
|
|
61
75
|
|
|
76
|
+
test("resolveRecordTitle composes name-like fields before fallback key", () => {
|
|
77
|
+
assert.equal(
|
|
78
|
+
resolveRecordTitle(
|
|
79
|
+
{
|
|
80
|
+
firstName: "Ana",
|
|
81
|
+
lastName: "Marin",
|
|
82
|
+
title: "Ignored"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
fallbackKey: "title",
|
|
86
|
+
defaultValue: "Record"
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
"Ana Marin"
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("resolveRecordTitle falls back to provided field key", () => {
|
|
94
|
+
assert.equal(
|
|
95
|
+
resolveRecordTitle(
|
|
96
|
+
{
|
|
97
|
+
title: "Harbor Visit"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
fallbackKey: "title",
|
|
101
|
+
defaultValue: "Record"
|
|
102
|
+
}
|
|
103
|
+
),
|
|
104
|
+
"Harbor Visit"
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('resolveRecordTitle falls back to "-" when no title data exists', () => {
|
|
109
|
+
assert.equal(resolveRecordTitle({}, { fallbackKey: "title", defaultValue: "" }), "-");
|
|
110
|
+
});
|
|
111
|
+
|
|
62
112
|
test("resolveLookupFieldDisplayValue returns hydrated label for lookup fields", () => {
|
|
63
113
|
assert.equal(
|
|
64
114
|
resolveLookupFieldDisplayValue(
|
|
@@ -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(
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
normalizeListSyncToRouteConfig,
|
|
6
|
+
resolveQueryParamDescriptors,
|
|
7
|
+
resolveActiveQueryParamEntries,
|
|
8
|
+
resolveWritableQueryParamBindings,
|
|
9
|
+
buildQueryParamEntriesToken,
|
|
10
|
+
parseRouteBindingValue,
|
|
11
|
+
areQueryParamBindingValuesEqual,
|
|
12
|
+
buildRouteQueryCompareToken,
|
|
13
|
+
mergeManagedQueryParamKeyHistory,
|
|
14
|
+
resolveRouteSyncManagedKeys
|
|
15
|
+
} from "../src/client/composables/support/listQueryParamSupport.js";
|
|
16
|
+
|
|
17
|
+
test("normalizeListSyncToRouteConfig defaults", () => {
|
|
18
|
+
assert.deepEqual(
|
|
19
|
+
normalizeListSyncToRouteConfig(false, {
|
|
20
|
+
defaultSearchParam: "search"
|
|
21
|
+
}),
|
|
22
|
+
{
|
|
23
|
+
enabled: false,
|
|
24
|
+
mode: "replace",
|
|
25
|
+
syncSearch: false,
|
|
26
|
+
syncQueryParams: false,
|
|
27
|
+
hydrateFromRoute: false,
|
|
28
|
+
searchParam: "search",
|
|
29
|
+
queryParamBlacklist: []
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
normalizeListSyncToRouteConfig(true),
|
|
35
|
+
{
|
|
36
|
+
enabled: true,
|
|
37
|
+
mode: "replace",
|
|
38
|
+
syncSearch: true,
|
|
39
|
+
syncQueryParams: true,
|
|
40
|
+
hydrateFromRoute: true,
|
|
41
|
+
searchParam: "q",
|
|
42
|
+
queryParamBlacklist: []
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
normalizeListSyncToRouteConfig({
|
|
48
|
+
enabled: true,
|
|
49
|
+
queryParamBlacklist: [" include ", "cursor", "include", ""]
|
|
50
|
+
}),
|
|
51
|
+
{
|
|
52
|
+
enabled: true,
|
|
53
|
+
mode: "replace",
|
|
54
|
+
syncSearch: true,
|
|
55
|
+
syncQueryParams: true,
|
|
56
|
+
hydrateFromRoute: true,
|
|
57
|
+
searchParam: "q",
|
|
58
|
+
queryParamBlacklist: ["include", "cursor"]
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("resolveQueryParamDescriptors keeps declared keys and supports writable bindings", () => {
|
|
64
|
+
const source = {
|
|
65
|
+
" status ": ref("open"),
|
|
66
|
+
" count ": 2,
|
|
67
|
+
includeArchived: ref(false),
|
|
68
|
+
empty: ""
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const descriptors = resolveQueryParamDescriptors(source);
|
|
72
|
+
assert.deepEqual(
|
|
73
|
+
descriptors.map((descriptor) => descriptor.key),
|
|
74
|
+
["count", "empty", "includeArchived", "status"]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const activeEntries = resolveActiveQueryParamEntries(descriptors);
|
|
78
|
+
assert.deepEqual(
|
|
79
|
+
activeEntries,
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
key: "count",
|
|
83
|
+
values: ["2"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "status",
|
|
87
|
+
values: ["open"]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const bindings = resolveWritableQueryParamBindings(descriptors);
|
|
93
|
+
const countBinding = bindings.find((binding) => binding.key === "count");
|
|
94
|
+
assert.ok(countBinding);
|
|
95
|
+
countBinding.set(9);
|
|
96
|
+
assert.equal(source[" count "], 9);
|
|
97
|
+
assert.equal(source.count, undefined);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("buildQueryParamEntriesToken and compare token stay deterministic", () => {
|
|
101
|
+
assert.equal(
|
|
102
|
+
buildQueryParamEntriesToken([
|
|
103
|
+
{
|
|
104
|
+
key: "status",
|
|
105
|
+
values: ["open"]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "tags",
|
|
109
|
+
values: ["a", "b"]
|
|
110
|
+
}
|
|
111
|
+
]),
|
|
112
|
+
"status=open&tags=a,b"
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
assert.equal(
|
|
116
|
+
buildRouteQueryCompareToken({
|
|
117
|
+
b: "2",
|
|
118
|
+
a: ["1"]
|
|
119
|
+
}),
|
|
120
|
+
buildRouteQueryCompareToken({
|
|
121
|
+
a: "1",
|
|
122
|
+
b: ["2"]
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("parseRouteBindingValue handles boolean, number and array bindings", () => {
|
|
128
|
+
const booleanBinding = {
|
|
129
|
+
valueType: "boolean",
|
|
130
|
+
get: () => false
|
|
131
|
+
};
|
|
132
|
+
assert.equal(parseRouteBindingValue(booleanBinding, "1"), true);
|
|
133
|
+
assert.equal(parseRouteBindingValue(booleanBinding, undefined), false);
|
|
134
|
+
|
|
135
|
+
const numberBinding = {
|
|
136
|
+
valueType: "number",
|
|
137
|
+
get: () => 7
|
|
138
|
+
};
|
|
139
|
+
assert.equal(parseRouteBindingValue(numberBinding, "12"), 12);
|
|
140
|
+
assert.equal(parseRouteBindingValue(numberBinding, "bad"), 7);
|
|
141
|
+
assert.equal(parseRouteBindingValue(numberBinding, undefined), null);
|
|
142
|
+
|
|
143
|
+
const arrayBinding = {
|
|
144
|
+
valueType: "array",
|
|
145
|
+
arrayItemType: "number",
|
|
146
|
+
get: () => []
|
|
147
|
+
};
|
|
148
|
+
assert.deepEqual(
|
|
149
|
+
parseRouteBindingValue(arrayBinding, ["1", "bad", "3"]),
|
|
150
|
+
[1, 3]
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("route sync key helpers preserve declared key history for cleanup", () => {
|
|
155
|
+
const history = mergeManagedQueryParamKeyHistory(
|
|
156
|
+
["status"],
|
|
157
|
+
["assignee", "status"]
|
|
158
|
+
);
|
|
159
|
+
assert.deepEqual(history, ["assignee", "status"]);
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(
|
|
162
|
+
resolveRouteSyncManagedKeys({
|
|
163
|
+
searchEnabled: true,
|
|
164
|
+
searchParam: "q",
|
|
165
|
+
syncSearch: true,
|
|
166
|
+
syncQueryParams: true,
|
|
167
|
+
declaredKeys: ["status"],
|
|
168
|
+
keyHistory: ["assignee"]
|
|
169
|
+
}),
|
|
170
|
+
["assignee", "q", "status"]
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("areQueryParamBindingValuesEqual handles arrays and dates", () => {
|
|
175
|
+
assert.equal(
|
|
176
|
+
areQueryParamBindingValuesEqual([1, 2], [1, 2]),
|
|
177
|
+
true
|
|
178
|
+
);
|
|
179
|
+
assert.equal(
|
|
180
|
+
areQueryParamBindingValuesEqual([1, 2], [2, 1]),
|
|
181
|
+
false
|
|
182
|
+
);
|
|
183
|
+
assert.equal(
|
|
184
|
+
areQueryParamBindingValuesEqual(
|
|
185
|
+
new Date("2026-01-01T00:00:00.000Z"),
|
|
186
|
+
new Date("2026-01-01T00:00:00.000Z")
|
|
187
|
+
),
|
|
188
|
+
true
|
|
189
|
+
);
|
|
190
|
+
});
|
|
@@ -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" }]);
|
|
@@ -22,6 +22,27 @@ test("createListUiRuntime resolves row keys and relative route templates from st
|
|
|
22
22
|
assert.equal(runtime.resolveEditUrl(items.value[0]), "/w/acme/admin/contacts/abc%20123/edit");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test("createListUiRuntime resolves nested route links from the parent list scope", () => {
|
|
26
|
+
const runtime = createListUiRuntime({
|
|
27
|
+
items: ref([{ id: "901" }]),
|
|
28
|
+
isInitialLoading: ref(false),
|
|
29
|
+
recordIdParam: "petId",
|
|
30
|
+
routeParams: ref({
|
|
31
|
+
workspaceSlug: "dogandgroom",
|
|
32
|
+
contactId: "541841",
|
|
33
|
+
petId: "715528"
|
|
34
|
+
}),
|
|
35
|
+
routeParamNames: ref(["workspaceSlug", "contactId", "petId"]),
|
|
36
|
+
routePath: ref("/w/dogandgroom/admin/contacts/541841/pets/715528"),
|
|
37
|
+
viewUrlTemplate: "./:petId",
|
|
38
|
+
editUrlTemplate: "./:petId/edit"
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.equal(runtime.resolveParams("./new"), "/w/dogandgroom/admin/contacts/541841/pets/new");
|
|
42
|
+
assert.equal(runtime.resolveViewUrl({ id: "901" }), "/w/dogandgroom/admin/contacts/541841/pets/901");
|
|
43
|
+
assert.equal(runtime.resolveEditUrl({ id: "901" }), "/w/dogandgroom/admin/contacts/541841/pets/901/edit");
|
|
44
|
+
});
|
|
45
|
+
|
|
25
46
|
test("createListUiRuntime resolves templates that depend on existing route params", () => {
|
|
26
47
|
const runtime = createListUiRuntime({
|
|
27
48
|
items: ref([{ id: 42 }]),
|
package/test/menuIcons.test.js
CHANGED
|
@@ -19,9 +19,11 @@ test("resolveSurfaceSwitchIcon prefers explicit icon and maps known surfaces", (
|
|
|
19
19
|
assert.equal(resolveSurfaceSwitchIcon("admin"), mdiShieldCrownOutline);
|
|
20
20
|
assert.equal(resolveSurfaceSwitchIcon("console"), mdiConsoleNetworkOutline);
|
|
21
21
|
assert.equal(resolveSurfaceSwitchIcon("admin", "custom-icon"), "custom-icon");
|
|
22
|
+
assert.equal(resolveSurfaceSwitchIcon("admin", "mdi-cog-outline"), mdiCogOutline);
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
test("resolveMenuLinkIcon resolves settings/login fallbacks and generic default", () => {
|
|
26
|
+
assert.equal(resolveMenuLinkIcon({ icon: "mdi-cog-outline" }), mdiCogOutline);
|
|
25
27
|
assert.equal(resolveMenuLinkIcon({ to: "/account" }), mdiAccountCogOutline);
|
|
26
28
|
assert.equal(resolveMenuLinkIcon({ label: "Sign in" }), mdiLogin);
|
|
27
29
|
assert.equal(resolveMenuLinkIcon({ label: "Go to admin" }), mdiShieldCrownOutline);
|
|
@@ -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 = {
|
|
@@ -2,8 +2,9 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
extractRouteParamNames,
|
|
5
|
+
resolveScopedRoutePathname,
|
|
5
6
|
resolveRouteParamNamesInOrder
|
|
6
|
-
} from "../src/client/composables/routeTemplateHelpers.js";
|
|
7
|
+
} from "../src/client/composables/support/routeTemplateHelpers.js";
|
|
7
8
|
|
|
8
9
|
test("extractRouteParamNames reads dynamic params from route templates", () => {
|
|
9
10
|
assert.deepEqual(
|
|
@@ -27,3 +28,58 @@ test("resolveRouteParamNamesInOrder prefers matched route templates", () => {
|
|
|
27
28
|
|
|
28
29
|
assert.deepEqual(resolveRouteParamNamesInOrder(route), ["workspaceSlug", "contactId", "addressId"]);
|
|
29
30
|
});
|
|
31
|
+
|
|
32
|
+
test("resolveScopedRoutePathname supports at/before/after anchors", () => {
|
|
33
|
+
const currentPathname = "/w/dogandgroom/admin/contacts/541841/pets/715528/edit/advanced";
|
|
34
|
+
const params = {
|
|
35
|
+
workspaceSlug: "dogandgroom",
|
|
36
|
+
contactId: "541841",
|
|
37
|
+
petId: "715528"
|
|
38
|
+
};
|
|
39
|
+
const orderedParamNames = ["workspaceSlug", "contactId", "petId"];
|
|
40
|
+
|
|
41
|
+
assert.equal(
|
|
42
|
+
resolveScopedRoutePathname({
|
|
43
|
+
currentPathname,
|
|
44
|
+
params,
|
|
45
|
+
orderedParamNames,
|
|
46
|
+
anchorParamName: "contactId",
|
|
47
|
+
anchorMode: "at"
|
|
48
|
+
}),
|
|
49
|
+
"/w/dogandgroom/admin/contacts/541841"
|
|
50
|
+
);
|
|
51
|
+
assert.equal(
|
|
52
|
+
resolveScopedRoutePathname({
|
|
53
|
+
currentPathname,
|
|
54
|
+
params,
|
|
55
|
+
orderedParamNames,
|
|
56
|
+
anchorParamName: "petId",
|
|
57
|
+
anchorMode: "before"
|
|
58
|
+
}),
|
|
59
|
+
"/w/dogandgroom/admin/contacts/541841/pets"
|
|
60
|
+
);
|
|
61
|
+
assert.equal(
|
|
62
|
+
resolveScopedRoutePathname({
|
|
63
|
+
currentPathname,
|
|
64
|
+
params,
|
|
65
|
+
orderedParamNames,
|
|
66
|
+
anchorParamName: "petId",
|
|
67
|
+
anchorMode: "after"
|
|
68
|
+
}),
|
|
69
|
+
"/w/dogandgroom/admin/contacts/541841/pets/715528/edit"
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("resolveScopedRoutePathname falls back to direct value matching", () => {
|
|
74
|
+
assert.equal(
|
|
75
|
+
resolveScopedRoutePathname({
|
|
76
|
+
currentPathname: "/contacts/abc%20123/notes",
|
|
77
|
+
params: {},
|
|
78
|
+
orderedParamNames: [],
|
|
79
|
+
anchorParamName: "contactId",
|
|
80
|
+
anchorParamValue: "abc 123",
|
|
81
|
+
anchorMode: "at"
|
|
82
|
+
}),
|
|
83
|
+
"/contacts/abc%20123"
|
|
84
|
+
);
|
|
85
|
+
});
|
|
@@ -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(
|