@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.
Files changed (71) hide show
  1. package/package.descriptor.mjs +7 -7
  2. package/package.json +16 -12
  3. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
  4. package/src/client/components/WorkspaceMembersClientElement.vue +3 -3
  5. package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
  6. package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
  7. package/src/client/components/WorkspacesClientElement.vue +2 -2
  8. package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
  9. package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
  10. package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +1 -1
  11. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  12. package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +1 -1
  13. package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +6 -2
  14. package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +155 -2
  15. package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
  16. package/src/client/composables/internal/useOperationScope.js +1 -1
  17. package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +9 -9
  18. package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
  19. package/src/client/composables/records/useCrudList.js +83 -0
  20. package/src/client/composables/records/useCrudView.js +35 -0
  21. package/src/client/composables/{useList.js → records/useList.js} +31 -57
  22. package/src/client/composables/{useView.js → records/useView.js} +6 -9
  23. package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +2 -2
  24. package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +2 -2
  25. package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
  26. package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
  27. package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
  28. package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
  29. package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +2 -2
  30. package/src/client/composables/useAccess.js +2 -2
  31. package/src/client/composables/useAccountSettingsRuntime.js +6 -6
  32. package/src/client/composables/useBootstrapQuery.js +1 -1
  33. package/src/client/composables/useCommand.js +5 -5
  34. package/src/client/composables/useCrudListParentTitle.js +131 -0
  35. package/src/client/composables/usePagedCollection.js +3 -3
  36. package/src/client/composables/useScopeRuntime.js +1 -1
  37. package/src/client/support/menuLinkTarget.js +93 -0
  38. package/test/addEditUiRuntime.test.js +1 -1
  39. package/test/crudBindingSupport.test.js +110 -0
  40. package/test/crudLookupFieldRuntime.test.js +1 -1
  41. package/test/errorMessageHelpers.test.js +1 -1
  42. package/test/exportsContract.test.js +10 -1
  43. package/test/listQueryParamSupport.test.js +1 -1
  44. package/test/listUiRuntime.test.js +1 -1
  45. package/test/menuLinkTarget.test.js +116 -0
  46. package/test/permissions.test.js +2 -2
  47. package/test/refValueHelpers.test.js +1 -1
  48. package/test/resourceLoadStateHelpers.test.js +1 -1
  49. package/test/routeTemplateHelpers.test.js +1 -1
  50. package/test/scopeHelpers.test.js +1 -1
  51. package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
  52. package/test/useCrudListParentTitle.test.js +143 -0
  53. package/test/useListSearchSupport.test.js +1 -1
  54. package/test/viewCoreLoading.test.js +1 -1
  55. package/test/viewUiRuntime.test.js +1 -1
  56. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -95
  57. /package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -0
  58. /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
  59. /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
  60. /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
  61. /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
  62. /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
  63. /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
  64. /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
  65. /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
  66. /package/src/client/composables/{listQueryParamSupport.js → support/listQueryParamSupport.js} +0 -0
  67. /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
  68. /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
  69. /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
  70. /package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +0 -0
  71. /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 "../lib/httpClient.js";
4
- import { asPlainObject } from "./scopeHelpers.js";
5
- import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
6
- import { toQueryErrorMessage } from "./errorMessageHelpers.js";
7
- import { hasResolvedQueryData } from "./resourceLoadStateHelpers.js";
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 "../lib/httpClient.js";
4
- import { asPlainObject } from "./scopeHelpers.js";
5
- import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
6
- import { usePagedCollection } from "./usePagedCollection.js";
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 "./errorMessageHelpers.js";
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 "./scopeHelpers.js";
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 "./routeTemplateHelpers.js";
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 : [];
@@ -11,7 +11,7 @@ import {
11
11
  resolveAccessModeEnabled,
12
12
  normalizeOwnershipFilter,
13
13
  resolveApiSuffix
14
- } from "./scopeHelpers.js";
14
+ } from "./support/scopeHelpers.js";
15
15
 
16
16
  function useScopeRuntime({
17
17
  ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
@@ -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: ["./client"]
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
+ });