@jskit-ai/users-web 0.1.37 → 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 +7 -7
- package/package.json +16 -12
- 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/support/menuLinkTarget.js +93 -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/{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/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,10 +1,10 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useMutation, useQuery } from "@tanstack/vue-query";
|
|
3
|
-
import { usersWebHttpClient } from "
|
|
4
|
-
import { asPlainObject } from "
|
|
5
|
-
import { resolveEnabledRef, resolveTextRef } from "
|
|
6
|
-
import { toQueryErrorMessage } from "
|
|
7
|
-
import { hasResolvedQueryData } from "
|
|
3
|
+
import { usersWebHttpClient } from "../../lib/httpClient.js";
|
|
4
|
+
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
5
|
+
import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js";
|
|
6
|
+
import { toQueryErrorMessage } from "../support/errorMessageHelpers.js";
|
|
7
|
+
import { hasResolvedQueryData } from "../support/resourceLoadStateHelpers.js";
|
|
8
8
|
|
|
9
9
|
function useEndpointResource({
|
|
10
10
|
queryKey,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
3
|
-
import { usersWebHttpClient } from "
|
|
4
|
-
import { asPlainObject } from "
|
|
5
|
-
import { resolveEnabledRef, resolveTextRef } from "
|
|
6
|
-
import { usePagedCollection } from "
|
|
3
|
+
import { usersWebHttpClient } from "../../lib/httpClient.js";
|
|
4
|
+
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
5
|
+
import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js";
|
|
6
|
+
import { usePagedCollection } from "../usePagedCollection.js";
|
|
7
7
|
|
|
8
8
|
function appendPageParam(path, pageParam) {
|
|
9
9
|
const normalizedPath = String(path || "").trim();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ref } from "vue";
|
|
2
2
|
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
3
|
-
import { toUiErrorMessage } from "
|
|
3
|
+
import { toUiErrorMessage } from "../support/errorMessageHelpers.js";
|
|
4
4
|
|
|
5
5
|
function useUiFeedback({
|
|
6
6
|
initialType = "success",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { computed, unref } from "vue";
|
|
2
|
-
import { asPlainObject } from "
|
|
2
|
+
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
3
3
|
import {
|
|
4
4
|
normalizeRouteParamName,
|
|
5
5
|
resolveRouteParamsSource,
|
|
6
6
|
resolveScopedRoutePathname,
|
|
7
7
|
resolveRouteTemplateLocation,
|
|
8
8
|
toRouteParamValue
|
|
9
|
-
} from "
|
|
9
|
+
} from "../support/routeTemplateHelpers.js";
|
|
10
10
|
|
|
11
11
|
function resolveRecordId({ routeParams, recordIdParam, routeRecordId }) {
|
|
12
12
|
const explicitRecordId = toRouteParamValue(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { hasPermission, normalizePermissionList } from "../lib/permissions.js";
|
|
3
|
-
import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
|
|
3
|
+
import { resolveEnabledRef, resolveTextRef } from "./support/refValueHelpers.js";
|
|
4
4
|
import {
|
|
5
5
|
normalizeAccessMode,
|
|
6
6
|
resolveAccessModeEnabled
|
|
7
|
-
} from "./scopeHelpers.js";
|
|
7
|
+
} from "./support/scopeHelpers.js";
|
|
8
8
|
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
9
9
|
|
|
10
10
|
function asPermissionList(value) {
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
useWorkspaceSurfaceId
|
|
21
21
|
} from "./useWorkspaceSurfaceId.js";
|
|
22
|
-
import { useAddEdit } from "./useAddEdit.js";
|
|
22
|
+
import { useAddEdit } from "./records/useAddEdit.js";
|
|
23
23
|
import { useCommand } from "./useCommand.js";
|
|
24
|
-
import { useView } from "./useView.js";
|
|
24
|
+
import { useView } from "./records/useView.js";
|
|
25
25
|
import { usePaths } from "./usePaths.js";
|
|
26
26
|
import { resolveAccountSettingsPathFromPlacementContext } from "../lib/workspaceSurfacePaths.js";
|
|
27
27
|
import {
|
|
@@ -34,16 +34,16 @@ import {
|
|
|
34
34
|
NUMBER_FORMAT_OPTIONS,
|
|
35
35
|
THEME_OPTIONS,
|
|
36
36
|
TIME_ZONE_OPTIONS
|
|
37
|
-
} from "./accountSettingsRuntimeConstants.js";
|
|
37
|
+
} from "./account-settings/accountSettingsRuntimeConstants.js";
|
|
38
38
|
import {
|
|
39
39
|
normalizeAvatarSize,
|
|
40
40
|
normalizePendingInvite,
|
|
41
41
|
normalizeReturnToPath,
|
|
42
42
|
normalizeSettingsPayload,
|
|
43
43
|
resolveAllowedReturnToOrigins
|
|
44
|
-
} from "./accountSettingsRuntimeHelpers.js";
|
|
45
|
-
import { createAccountSettingsAvatarUploadRuntime } from "./accountSettingsAvatarUploadRuntime.js";
|
|
46
|
-
import { createAccountSettingsInvitesRuntime } from "./accountSettingsInvitesRuntime.js";
|
|
44
|
+
} from "./account-settings/accountSettingsRuntimeHelpers.js";
|
|
45
|
+
import { createAccountSettingsAvatarUploadRuntime } from "./account-settings/accountSettingsAvatarUploadRuntime.js";
|
|
46
|
+
import { createAccountSettingsInvitesRuntime } from "./account-settings/accountSettingsInvitesRuntime.js";
|
|
47
47
|
|
|
48
48
|
function useAccountSettingsRuntime() {
|
|
49
49
|
const route = useRoute();
|
|
@@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/vue-query";
|
|
|
5
5
|
import { normalizeQueryToken } from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
6
|
import { usersWebHttpClient } from "../lib/httpClient.js";
|
|
7
7
|
import { buildBootstrapApiPath } from "../lib/bootstrap.js";
|
|
8
|
-
import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
|
|
8
|
+
import { resolveEnabledRef, resolveTextRef } from "./support/refValueHelpers.js";
|
|
9
9
|
|
|
10
10
|
const DEFAULT_BOOTSTRAP_STALE_TIME_MS = 60_000;
|
|
11
11
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { proxyRefs } from "vue";
|
|
2
2
|
import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
3
|
-
import { useCommandCore } from "./useCommandCore.js";
|
|
4
|
-
import { useEndpointResource } from "./useEndpointResource.js";
|
|
3
|
+
import { useCommandCore } from "./runtime/useCommandCore.js";
|
|
4
|
+
import { useEndpointResource } from "./runtime/useEndpointResource.js";
|
|
5
5
|
import { useOperationScope } from "./internal/useOperationScope.js";
|
|
6
|
-
import { useUiFeedback } from "./useUiFeedback.js";
|
|
7
|
-
import { useFieldErrorBag } from "./useFieldErrorBag.js";
|
|
6
|
+
import { useUiFeedback } from "./runtime/useUiFeedback.js";
|
|
7
|
+
import { useFieldErrorBag } from "./runtime/useFieldErrorBag.js";
|
|
8
8
|
import {
|
|
9
9
|
setupRouteChangeCleanup,
|
|
10
10
|
setupOperationErrorReporting
|
|
11
|
-
} from "./operationUiHelpers.js";
|
|
11
|
+
} from "./runtime/operationUiHelpers.js";
|
|
12
12
|
|
|
13
13
|
function useCommand({
|
|
14
14
|
ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { computed, proxyRefs } from "vue";
|
|
2
|
+
import { useRoute } from "vue-router";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import {
|
|
5
|
+
resolveRouteParamsSource,
|
|
6
|
+
toRouteParamValue
|
|
7
|
+
} from "./support/routeTemplateHelpers.js";
|
|
8
|
+
import { useView } from "./records/useView.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveCrudListParentDescriptor,
|
|
11
|
+
resolveCrudListParentRecordTitle,
|
|
12
|
+
resolveCrudListParentTitleFromItems
|
|
13
|
+
} from "./internal/crudListParentTitleSupport.js";
|
|
14
|
+
|
|
15
|
+
function normalizeQueryKeyPrefix(value = []) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value
|
|
18
|
+
.map((entry) => normalizeText(entry))
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const normalizedValue = normalizeText(value);
|
|
23
|
+
return normalizedValue ? [normalizedValue] : [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function useCrudListParentTitle({
|
|
27
|
+
listRuntime = null,
|
|
28
|
+
resource = {},
|
|
29
|
+
adapter = null,
|
|
30
|
+
recordIdParam = "recordId",
|
|
31
|
+
queryKeyPrefix = ["users-web", "crud-list-parent-title"],
|
|
32
|
+
placementSource = "users-web.crud-list-parent-title",
|
|
33
|
+
fallbackLoadError = "Unable to load parent record.",
|
|
34
|
+
notFoundMessage = "Parent record not found.",
|
|
35
|
+
route = null,
|
|
36
|
+
viewRuntimeFactory = useView
|
|
37
|
+
} = {}) {
|
|
38
|
+
const sourceRoute = route && typeof route === "object" ? route : useRoute();
|
|
39
|
+
const parentDescriptor = computed(() => {
|
|
40
|
+
const descriptor = resolveCrudListParentDescriptor({
|
|
41
|
+
resource,
|
|
42
|
+
route: sourceRoute,
|
|
43
|
+
recordIdParam
|
|
44
|
+
});
|
|
45
|
+
if (!descriptor) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const routeParams = resolveRouteParamsSource(sourceRoute?.params || {});
|
|
50
|
+
const routeParamValue = toRouteParamValue(routeParams[descriptor.routeParamKey]);
|
|
51
|
+
if (!routeParamValue) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
...descriptor,
|
|
57
|
+
routeParamValue
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const initialParentDescriptor = parentDescriptor.value || {};
|
|
62
|
+
const normalizedQueryKeyPrefix = normalizeQueryKeyPrefix(queryKeyPrefix);
|
|
63
|
+
const shouldLoadParentRecord = computed(() => {
|
|
64
|
+
const descriptor = parentDescriptor.value;
|
|
65
|
+
if (!descriptor?.apiUrlTemplate) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (Boolean(listRuntime?.isInitialLoading) || normalizeText(listRuntime?.loadError)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const items = Array.isArray(listRuntime?.items) ? listRuntime.items : [];
|
|
73
|
+
return items.length < 1;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const parentView = viewRuntimeFactory({
|
|
77
|
+
adapter,
|
|
78
|
+
apiUrlTemplate: normalizeText(initialParentDescriptor.apiUrlTemplate),
|
|
79
|
+
readEnabled: shouldLoadParentRecord,
|
|
80
|
+
recordIdParam: normalizeText(initialParentDescriptor.routeParamKey) || "recordId",
|
|
81
|
+
includeRecordIdInQueryKey: true,
|
|
82
|
+
queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
|
|
83
|
+
...normalizedQueryKeyPrefix,
|
|
84
|
+
normalizeText(initialParentDescriptor.relationNamespace),
|
|
85
|
+
normalizeText(initialParentDescriptor.routeParamKey),
|
|
86
|
+
String(surfaceId || ""),
|
|
87
|
+
String(workspaceSlug || "")
|
|
88
|
+
],
|
|
89
|
+
placementSource,
|
|
90
|
+
fallbackLoadError,
|
|
91
|
+
notFoundMessage
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const title = computed(() => {
|
|
95
|
+
const descriptor = parentDescriptor.value;
|
|
96
|
+
if (!descriptor) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parentRecordTitle = resolveCrudListParentRecordTitle(parentView?.record || {}, descriptor);
|
|
101
|
+
if (parentRecordTitle && shouldLoadParentRecord.value) {
|
|
102
|
+
return parentRecordTitle;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const listTitle = resolveCrudListParentTitleFromItems(listRuntime?.items, descriptor);
|
|
106
|
+
if (listTitle) {
|
|
107
|
+
return listTitle;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parentRecordTitle) {
|
|
111
|
+
return parentRecordTitle;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (descriptor.routeParamValue) {
|
|
115
|
+
return `${descriptor.entityLabel} #${descriptor.routeParamValue}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return descriptor.entityLabel;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return proxyRefs({
|
|
122
|
+
title,
|
|
123
|
+
descriptor: parentDescriptor,
|
|
124
|
+
shouldLoadParentRecord,
|
|
125
|
+
record: computed(() => parentView?.record || null),
|
|
126
|
+
isLoading: computed(() => Boolean(parentView?.isLoading)),
|
|
127
|
+
loadError: computed(() => normalizeText(parentView?.loadError))
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { useCrudListParentTitle };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { computed, unref } from "vue";
|
|
2
2
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/vue-query";
|
|
3
|
-
import { asPlainObject } from "./scopeHelpers.js";
|
|
4
|
-
import { resolveEnabledRef } from "./refValueHelpers.js";
|
|
5
|
-
import { toQueryErrorMessage } from "./errorMessageHelpers.js";
|
|
3
|
+
import { asPlainObject } from "./support/scopeHelpers.js";
|
|
4
|
+
import { resolveEnabledRef } from "./support/refValueHelpers.js";
|
|
5
|
+
import { toQueryErrorMessage } from "./support/errorMessageHelpers.js";
|
|
6
6
|
|
|
7
7
|
function defaultSelectItems(page) {
|
|
8
8
|
return Array.isArray(page?.items) ? page.items : [];
|
|
@@ -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
|
+
};
|
|
@@ -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
|
+
});
|