@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.
Files changed (79) hide show
  1. package/package.descriptor.mjs +8 -22
  2. package/package.json +16 -11
  3. package/src/client/components/MembersAdminClientElement.vue +5 -5
  4. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
  5. package/src/client/components/WorkspaceMembersClientElement.vue +19 -19
  6. package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
  7. package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
  8. package/src/client/components/WorkspacesClientElement.vue +4 -4
  9. package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
  10. package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
  11. package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -4
  12. package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +2 -2
  13. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  14. package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +37 -5
  15. package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +11 -4
  16. package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +178 -5
  17. package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
  18. package/src/client/composables/internal/useOperationScope.js +1 -1
  19. package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +18 -8
  20. package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
  21. package/src/client/composables/records/useCrudList.js +83 -0
  22. package/src/client/composables/records/useCrudView.js +35 -0
  23. package/src/client/composables/records/useList.js +482 -0
  24. package/src/client/composables/{useView.js → records/useView.js} +7 -7
  25. package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +13 -4
  26. package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +20 -8
  27. package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
  28. package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
  29. package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
  30. package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
  31. package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +13 -4
  32. package/src/client/composables/support/listQueryParamSupport.js +459 -0
  33. package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +122 -0
  34. package/src/client/composables/useAccess.js +2 -2
  35. package/src/client/composables/useAccountSettingsRuntime.js +6 -6
  36. package/src/client/composables/useBootstrapQuery.js +1 -1
  37. package/src/client/composables/useCommand.js +5 -5
  38. package/src/client/composables/useCrudListParentTitle.js +131 -0
  39. package/src/client/composables/usePagedCollection.js +58 -7
  40. package/src/client/composables/useScopeRuntime.js +1 -1
  41. package/src/client/lib/bootstrap.js +1 -1
  42. package/src/client/lib/menuIcons.js +27 -6
  43. package/src/client/support/menuLinkTarget.js +93 -0
  44. package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
  45. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
  46. package/test/addEditUiRuntime.test.js +19 -1
  47. package/test/crudBindingSupport.test.js +110 -0
  48. package/test/crudLookupFieldRuntime.test.js +52 -2
  49. package/test/errorMessageHelpers.test.js +1 -1
  50. package/test/exportsContract.test.js +10 -1
  51. package/test/listQueryParamSupport.test.js +190 -0
  52. package/test/listUiRuntime.test.js +22 -1
  53. package/test/menuIcons.test.js +2 -0
  54. package/test/menuLinkTarget.test.js +116 -0
  55. package/test/permissions.test.js +2 -2
  56. package/test/refValueHelpers.test.js +1 -1
  57. package/test/resourceLoadStateHelpers.test.js +1 -1
  58. package/test/routeTemplateHelpers.test.js +57 -1
  59. package/test/scopeHelpers.test.js +1 -1
  60. package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
  61. package/test/useCrudListParentTitle.test.js +143 -0
  62. package/test/useListSearchSupport.test.js +1 -1
  63. package/test/usePagedCollection.test.js +53 -0
  64. package/test/viewCoreLoading.test.js +1 -1
  65. package/test/viewUiRuntime.test.js +36 -1
  66. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -241
  67. package/src/client/composables/useList.js +0 -268
  68. /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
  69. /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
  70. /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
  71. /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
  72. /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
  73. /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
  74. /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
  75. /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
  76. /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
  77. /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
  78. /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
  79. /package/src/client/composables/{scopeHelpers.js → support/scopeHelpers.js} +0 -0
@@ -40,6 +40,17 @@ function resolveRouteParamsSource(source = null) {
40
40
  return asPlainObject(resolveRouteSourceValue(source));
41
41
  }
42
42
 
43
+ function normalizeRouteParamNameList(value = []) {
44
+ const source = Array.isArray(value) ? value : [];
45
+ return source
46
+ .map((entry) => String(entry || "").trim())
47
+ .filter(Boolean);
48
+ }
49
+
50
+ function resolveRouteParamNamesSource(source = []) {
51
+ return normalizeRouteParamNameList(resolveRouteSourceValue(source));
52
+ }
53
+
43
54
  function normalizeRoutePathname(value = "") {
44
55
  const rawPathname = String(value || "").trim();
45
56
  const sanitizedPathname = rawPathname.split(/[?#]/u, 1)[0] || "";
@@ -54,6 +65,115 @@ function resolveRoutePathnameSource(source = "") {
54
65
  return normalizeRoutePathname(resolveRouteSourceValue(source));
55
66
  }
56
67
 
68
+ function segmentMatchesParamValue(segment = "", paramValue = "") {
69
+ const normalizedSegment = String(segment || "").trim();
70
+ const normalizedParamValue = toRouteParamValue(paramValue);
71
+ if (!normalizedSegment || !normalizedParamValue) {
72
+ return false;
73
+ }
74
+
75
+ const encodedParamValue = encodeURIComponent(normalizedParamValue);
76
+ if (normalizedSegment === encodedParamValue) {
77
+ return true;
78
+ }
79
+
80
+ try {
81
+ return decodeURIComponent(normalizedSegment) === normalizedParamValue;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function findRouteParamSegmentIndex(segments = [], paramValue = "", fromIndex = 0) {
88
+ const source = Array.isArray(segments) ? segments : [];
89
+ const cursor = Number.isInteger(fromIndex) && fromIndex > 0 ? fromIndex : 0;
90
+ for (let index = cursor; index < source.length; index += 1) {
91
+ if (segmentMatchesParamValue(source[index], paramValue)) {
92
+ return index;
93
+ }
94
+ }
95
+ return -1;
96
+ }
97
+
98
+ function normalizePathPrefix(segments = [], endIndex = -1) {
99
+ const source = Array.isArray(segments) ? segments : [];
100
+ if (!Number.isInteger(endIndex) || endIndex < 0) {
101
+ return "/";
102
+ }
103
+ return `/${source.slice(0, endIndex + 1).join("/")}`;
104
+ }
105
+
106
+ function resolveAnchorEndIndex(segmentIndex = -1, totalSegments = 0, anchorMode = "at") {
107
+ const normalizedSegmentIndex = Number.isInteger(segmentIndex) ? segmentIndex : -1;
108
+ if (normalizedSegmentIndex < 0) {
109
+ return -1;
110
+ }
111
+
112
+ const normalizedTotalSegments = Number.isInteger(totalSegments) && totalSegments > 0 ? totalSegments : 0;
113
+ const normalizedMode = String(anchorMode || "at").trim().toLowerCase();
114
+ if (normalizedMode === "before") {
115
+ return normalizedSegmentIndex - 1;
116
+ }
117
+ if (normalizedMode === "after") {
118
+ return normalizedTotalSegments > 0
119
+ ? Math.min(normalizedSegmentIndex + 1, normalizedTotalSegments - 1)
120
+ : normalizedSegmentIndex + 1;
121
+ }
122
+
123
+ return normalizedSegmentIndex;
124
+ }
125
+
126
+ function resolveScopedRoutePathname({
127
+ currentPathname = "/",
128
+ params = {},
129
+ orderedParamNames = [],
130
+ anchorParamName = "",
131
+ anchorParamValue = "",
132
+ anchorMode = "at"
133
+ } = {}) {
134
+ const normalizedCurrentPathname = resolveRoutePathnameSource(currentPathname);
135
+ const normalizedAnchorParamName = String(anchorParamName || "").trim();
136
+ if (!normalizedAnchorParamName) {
137
+ return normalizedCurrentPathname;
138
+ }
139
+
140
+ const sourceParams = asPlainObject(params);
141
+ const segments = normalizedCurrentPathname.split("/").filter(Boolean);
142
+ if (segments.length < 1) {
143
+ return normalizedCurrentPathname;
144
+ }
145
+
146
+ const paramNames = resolveRouteParamNamesSource(orderedParamNames);
147
+ let cursor = 0;
148
+ for (const paramName of paramNames) {
149
+ const segmentIndex = findRouteParamSegmentIndex(segments, sourceParams[paramName], cursor);
150
+ if (segmentIndex < 0) {
151
+ continue;
152
+ }
153
+
154
+ if (paramName === normalizedAnchorParamName) {
155
+ const endIndex = resolveAnchorEndIndex(segmentIndex, segments.length, anchorMode);
156
+ return normalizePathPrefix(segments, endIndex);
157
+ }
158
+
159
+ cursor = segmentIndex + 1;
160
+ }
161
+
162
+ const fallbackAnchorValue = toRouteParamValue(anchorParamValue) ||
163
+ toRouteParamValue(sourceParams[normalizedAnchorParamName]);
164
+ if (!fallbackAnchorValue) {
165
+ return normalizedCurrentPathname;
166
+ }
167
+
168
+ const fallbackSegmentIndex = findRouteParamSegmentIndex(segments, fallbackAnchorValue, 0);
169
+ if (fallbackSegmentIndex < 0) {
170
+ return normalizedCurrentPathname;
171
+ }
172
+
173
+ const fallbackEndIndex = resolveAnchorEndIndex(fallbackSegmentIndex, segments.length, anchorMode);
174
+ return normalizePathPrefix(segments, fallbackEndIndex);
175
+ }
176
+
57
177
  function resolveRouteTemplatePath(routeTemplate = "", params = {}) {
58
178
  const normalizedTemplate = String(routeTemplate || "").trim();
59
179
  if (!normalizedTemplate) {
@@ -155,7 +275,9 @@ export {
155
275
  normalizeRouteParamName,
156
276
  toRouteParamValue,
157
277
  resolveRouteParamsSource,
278
+ resolveRouteParamNamesSource,
158
279
  resolveRoutePathnameSource,
280
+ resolveScopedRoutePathname,
159
281
  resolveRouteTemplatePath,
160
282
  resolveRouteTemplateLocation,
161
283
  extractRouteParamNames,
@@ -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
- import { computed } from "vue";
2
- import { useInfiniteQuery } from "@tanstack/vue-query";
3
- import { asPlainObject } from "./scopeHelpers.js";
4
- import { resolveEnabledRef } from "./refValueHelpers.js";
5
- import { toQueryErrorMessage } from "./errorMessageHelpers.js";
1
+ import { computed, unref } from "vue";
2
+ import { useInfiniteQuery, useQueryClient } from "@tanstack/vue-query";
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 : [];
@@ -12,6 +12,42 @@ function defaultGetNextPageParam(lastPage) {
12
12
  return lastPage?.nextCursor ?? null;
13
13
  }
14
14
 
15
+ function normalizeQueryKeyValue(queryKey = null) {
16
+ const resolved = unref(queryKey);
17
+ if (Array.isArray(resolved)) {
18
+ return resolved;
19
+ }
20
+ if (resolved === null || resolved === undefined || resolved === "") {
21
+ return [];
22
+ }
23
+
24
+ return [resolved];
25
+ }
26
+
27
+ function trimInfinitePagesToFirst(data = null) {
28
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
29
+ return data;
30
+ }
31
+
32
+ const sourcePages = Array.isArray(data.pages) ? data.pages : [];
33
+ const sourcePageParams = Array.isArray(data.pageParams) ? data.pageParams : [];
34
+ if (sourcePages.length < 2 && sourcePageParams.length < 2) {
35
+ return data;
36
+ }
37
+ const pages = sourcePages.slice(0, 1);
38
+ const pageParams = sourcePageParams.length > 0
39
+ ? sourcePageParams.slice(0, 1)
40
+ : pages.length > 0
41
+ ? [null]
42
+ : [];
43
+
44
+ return {
45
+ ...data,
46
+ pages,
47
+ pageParams
48
+ };
49
+ }
50
+
15
51
  function usePagedCollection({
16
52
  queryKey,
17
53
  enabled = true,
@@ -36,7 +72,9 @@ function usePagedCollection({
36
72
  throw new TypeError("usePagedCollection dedupeBy must be a function when provided.");
37
73
  }
38
74
 
75
+ const queryClient = useQueryClient();
39
76
  const queryEnabled = computed(() => resolveEnabledRef(enabled));
77
+ const normalizedQueryKey = computed(() => normalizeQueryKeyValue(queryKey));
40
78
 
41
79
  const query = useInfiniteQuery({
42
80
  queryKey,
@@ -106,6 +144,15 @@ function usePagedCollection({
106
144
  return query.fetchNextPage();
107
145
  }
108
146
 
147
+ function trimToFirstPage() {
148
+ const key = normalizedQueryKey.value;
149
+ if (key.length < 1) {
150
+ return;
151
+ }
152
+
153
+ queryClient.setQueryData(key, (data) => trimInfinitePagesToFirst(data));
154
+ }
155
+
109
156
  return Object.freeze({
110
157
  query,
111
158
  pages,
@@ -118,8 +165,12 @@ function usePagedCollection({
118
165
  hasMore,
119
166
  loadError,
120
167
  reload,
121
- loadMore
168
+ loadMore,
169
+ trimToFirstPage
122
170
  });
123
171
  }
124
172
 
125
- export { usePagedCollection };
173
+ export {
174
+ usePagedCollection,
175
+ trimInfinitePagesToFirst
176
+ };
@@ -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,
@@ -30,7 +30,7 @@ function normalizeWorkspaceEntry(entry) {
30
30
  surfaceColor: String(entry.surfaceColor || "").trim(),
31
31
  surfaceVariantColor: String(entry.surfaceVariantColor || "").trim(),
32
32
  avatarUrl: String(entry.avatarUrl || "").trim(),
33
- roleId: String(entry.roleId || "member").trim().toLowerCase() || "member",
33
+ roleSid: String(entry.roleSid || "member").trim().toLowerCase() || "member",
34
34
  isAccessible: entry.isAccessible !== false
35
35
  });
36
36
  }
@@ -1,3 +1,4 @@
1
+ import * as mdiIcons from "@mdi/js";
1
2
  import {
2
3
  mdiAccountCircleOutline,
3
4
  mdiAccountCogOutline,
@@ -26,6 +27,26 @@ const SURFACE_SWITCH_ICON_BY_ID = Object.freeze({
26
27
  console: mdiConsoleNetworkOutline
27
28
  });
28
29
 
30
+ function resolveExplicitIconValue(explicitIcon = "") {
31
+ const normalizedExplicitIcon = normalizeText(explicitIcon);
32
+ if (!normalizedExplicitIcon) {
33
+ return "";
34
+ }
35
+
36
+ if (!normalizedExplicitIcon.startsWith("mdi-")) {
37
+ return normalizedExplicitIcon;
38
+ }
39
+
40
+ const iconKey = normalizedExplicitIcon
41
+ .slice("mdi-".length)
42
+ .split("-")
43
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
44
+ .join("");
45
+ const exportName = `mdi${iconKey}`;
46
+ const resolvedIcon = mdiIcons[exportName];
47
+ return typeof resolvedIcon === "string" && resolvedIcon ? resolvedIcon : normalizedExplicitIcon;
48
+ }
49
+
29
50
  function normalizePathname(value) {
30
51
  const normalizedValue = normalizeText(value);
31
52
  if (!normalizedValue) {
@@ -70,9 +91,9 @@ function resolveSurfaceSwitchIdFromLabel(label = "") {
70
91
  }
71
92
 
72
93
  function resolveSurfaceSwitchIcon(surfaceId = "", explicitIcon = "") {
73
- const normalizedExplicitIcon = normalizeText(explicitIcon);
74
- if (normalizedExplicitIcon) {
75
- return normalizedExplicitIcon;
94
+ const resolvedExplicitIcon = resolveExplicitIconValue(explicitIcon);
95
+ if (resolvedExplicitIcon) {
96
+ return resolvedExplicitIcon;
76
97
  }
77
98
 
78
99
  const normalizedSurfaceId = normalizeText(surfaceId).toLowerCase();
@@ -80,9 +101,9 @@ function resolveSurfaceSwitchIcon(surfaceId = "", explicitIcon = "") {
80
101
  }
81
102
 
82
103
  function resolveMenuLinkIcon({ icon = "", label = "", to = "" } = {}) {
83
- const normalizedIcon = normalizeText(icon);
84
- if (normalizedIcon) {
85
- return normalizedIcon;
104
+ const resolvedExplicitIcon = resolveExplicitIconValue(icon);
105
+ if (resolvedExplicitIcon) {
106
+ return resolvedExplicitIcon;
86
107
  }
87
108
 
88
109
  const normalizedLabel = normalizeText(label).toLowerCase();
@@ -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,4 +1,5 @@
1
1
  <script setup>
2
+ import { mdiAlertCircleOutline } from "@mdi/js";
2
3
  import { computed } from "vue";
3
4
 
4
5
  const props = defineProps({
@@ -20,7 +21,7 @@ const normalizedMessage = computed(() => String(props.message || "").trim() || "
20
21
  <v-card rounded="lg" elevation="1" border>
21
22
  <v-card-item>
22
23
  <template #prepend>
23
- <v-icon icon="mdi-alert-circle-outline" color="error" />
24
+ <v-icon :icon="mdiAlertCircleOutline" color="error" />
24
25
  </template>
25
26
  <v-card-title class="text-h5">Unavailable</v-card-title>
26
27
  <v-card-subtitle>{{ normalizedSurfaceLabel }} surface.</v-card-subtitle>
@@ -33,7 +33,7 @@ const invites = props.runtime.invites;
33
33
  v-for="invite in invites.items.value"
34
34
  :key="invite.id"
35
35
  :title="invite.workspaceName"
36
- :subtitle="`/${invite.workspaceSlug} • role: ${invite.roleId}`"
36
+ :subtitle="`/${invite.workspaceSlug} • role: ${invite.roleSid}`"
37
37
  class="px-0"
38
38
  >
39
39
  <template #prepend>
@@ -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({
@@ -51,6 +51,24 @@ test("createAddEditUiRuntime resolves edit-page relative list and cancel links",
51
51
  assert.equal(runtime.cancelUrl.value, "/contacts/7/addresses/42");
52
52
  });
53
53
 
54
+ test("createAddEditUiRuntime resolves nested edit links from the record scope", () => {
55
+ const runtime = createAddEditUiRuntime({
56
+ recordIdParam: "petId",
57
+ routeParams: ref({
58
+ workspaceSlug: "dogandgroom",
59
+ contactId: "541841",
60
+ petId: "715528"
61
+ }),
62
+ routeParamNames: ref(["workspaceSlug", "contactId", "petId"]),
63
+ routePath: ref("/w/dogandgroom/admin/contacts/541841/pets/715528/edit/advanced"),
64
+ viewUrlTemplate: "..",
65
+ listUrlTemplate: "../.."
66
+ });
67
+
68
+ assert.equal(runtime.cancelUrl.value, "/w/dogandgroom/admin/contacts/541841/pets/715528");
69
+ assert.equal(runtime.listUrl.value, "/w/dogandgroom/admin/contacts/541841/pets");
70
+ });
71
+
54
72
  test("createAddEditUiRuntime supports custom saved-record selector", () => {
55
73
  const runtime = createAddEditUiRuntime({
56
74
  recordIdParam: "addressId",