@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
@@ -0,0 +1,83 @@
1
+ import { computed, unref } from "vue";
2
+ import { useRoute } from "vue-router";
3
+ import { resolveLookupFieldDisplayValue } from "../crud/crudLookupFieldLabelSupport.js";
4
+ import { resolveCrudBoundValues } from "../crud/crudBindingSupport.js";
5
+ import { resolveCrudListParentDescriptor } from "../internal/crudListParentTitleSupport.js";
6
+ import {
7
+ resolveRouteParamsSource,
8
+ toRouteParamValue
9
+ } from "../support/routeTemplateHelpers.js";
10
+ import { asPlainObject } from "../support/scopeHelpers.js";
11
+ import { useList } from "./useList.js";
12
+
13
+ function resolveRequestQueryParamsInput(requestQueryParams, context = {}) {
14
+ if (typeof requestQueryParams === "function") {
15
+ return asPlainObject(requestQueryParams(context));
16
+ }
17
+
18
+ return asPlainObject(unref(requestQueryParams));
19
+ }
20
+
21
+ function resolveCrudParentRequestQueryParams({ resource = {}, route = null, recordIdParam = "recordId" } = {}) {
22
+ const descriptor = resolveCrudListParentDescriptor({
23
+ resource,
24
+ route,
25
+ recordIdParam
26
+ });
27
+ if (!descriptor?.fieldKey || !descriptor?.routeParamKey) {
28
+ return {};
29
+ }
30
+
31
+ const routeParams = resolveRouteParamsSource(route?.params || {});
32
+ const routeParamValue = toRouteParamValue(routeParams[descriptor.routeParamKey]);
33
+ if (!routeParamValue) {
34
+ return {};
35
+ }
36
+
37
+ return {
38
+ [descriptor.fieldKey]: routeParamValue
39
+ };
40
+ }
41
+
42
+ function useCrudList({
43
+ resource = null,
44
+ requestQueryParams = null,
45
+ parentBinding = null,
46
+ recordIdParam = "recordId",
47
+ route = null,
48
+ ...listOptions
49
+ } = {}) {
50
+ const sourceRoute = route && typeof route === "object" ? route : useRoute();
51
+ const boundParentRequestQueryParams = computed(() => {
52
+ return resolveCrudBoundValues({
53
+ binding: parentBinding,
54
+ routeValues: resolveCrudParentRequestQueryParams({
55
+ resource,
56
+ route: sourceRoute,
57
+ recordIdParam
58
+ }),
59
+ context: Object.freeze({
60
+ route: sourceRoute,
61
+ resource,
62
+ recordIdParam
63
+ })
64
+ });
65
+ });
66
+ const records = useList({
67
+ ...listOptions,
68
+ recordIdParam,
69
+ requestQueryParams(context = {}) {
70
+ const baseRequestQueryParams = resolveRequestQueryParamsInput(requestQueryParams, context);
71
+
72
+ return {
73
+ ...baseRequestQueryParams,
74
+ ...boundParentRequestQueryParams.value
75
+ };
76
+ }
77
+ });
78
+
79
+ records.resolveFieldDisplay = resolveLookupFieldDisplayValue;
80
+ return records;
81
+ }
82
+
83
+ export { useCrudList };
@@ -0,0 +1,35 @@
1
+ import { computed } from "vue";
2
+ import { useRoute } from "vue-router";
3
+ import {
4
+ resolveLookupFieldDisplayValue,
5
+ resolveRecordTitle
6
+ } from "../crud/crudLookupFieldLabelSupport.js";
7
+ import { resolveCrudBoundValues } from "../crud/crudBindingSupport.js";
8
+ import { asPlainObject } from "../support/scopeHelpers.js";
9
+ import { useView } from "./useView.js";
10
+
11
+ function useCrudView({
12
+ paramBinding = null,
13
+ route = null,
14
+ ...viewOptions
15
+ } = {}) {
16
+ const sourceRoute = route && typeof route === "object" ? route : useRoute();
17
+ const boundRouteParams = computed(() => {
18
+ return resolveCrudBoundValues({
19
+ binding: paramBinding,
20
+ routeValues: asPlainObject(sourceRoute?.params || {}),
21
+ context: Object.freeze({
22
+ route: sourceRoute
23
+ })
24
+ });
25
+ });
26
+ const view = useView({
27
+ ...viewOptions,
28
+ routeParams: boundRouteParams
29
+ });
30
+ view.resolveFieldDisplay = resolveLookupFieldDisplayValue;
31
+ view.resolveRecordTitle = resolveRecordTitle;
32
+ return view;
33
+ }
34
+
35
+ export { useCrudView };
@@ -0,0 +1,482 @@
1
+ import { computed, onScopeDispose, proxyRefs, ref, watch } from "vue";
2
+ import { useRouter } from "vue-router";
3
+ import { appendQueryString } from "@jskit-ai/kernel/shared/support";
4
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
5
+ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
6
+ import { useListCore } from "../runtime/useListCore.js";
7
+ import { resolveOperationAdapter } from "../runtime/operationAdapters.js";
8
+ import { setupOperationErrorReporting } from "../runtime/operationUiHelpers.js";
9
+ import { createListUiRuntime } from "../runtime/listUiRuntime.js";
10
+ import { asPlainObject } from "../support/scopeHelpers.js";
11
+ import {
12
+ normalizeListSearchConfig,
13
+ matchesLocalSearch
14
+ } from "../support/listSearchSupport.js";
15
+ import {
16
+ normalizeListSyncToRouteConfig,
17
+ resolveQueryParamDescriptors,
18
+ resolveActiveQueryParamEntries,
19
+ resolveWritableQueryParamBindings,
20
+ buildQueryParamEntriesToken,
21
+ parseRouteBindingValue,
22
+ areQueryParamBindingValuesEqual,
23
+ buildRouteQueryCompareToken,
24
+ mergeManagedQueryParamKeyHistory,
25
+ resolveRouteSyncManagedKeys
26
+ } from "../support/listQueryParamSupport.js";
27
+ import {
28
+ resolveRouteParamNamesInOrder,
29
+ } from "../support/routeTemplateHelpers.js";
30
+
31
+ const EMPTY_ROUTE_SYNC_QUERY_PARAM_BLACKLIST = Object.freeze([]);
32
+
33
+ function useList({
34
+ ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
35
+ surfaceId = "",
36
+ access = "auto",
37
+ apiSuffix = "",
38
+ queryKeyFactory = null,
39
+ viewPermissions = [],
40
+ readEnabled = true,
41
+ placementSource = "users-web.list",
42
+ fallbackLoadError = "Unable to load list.",
43
+ initialPageParam = null,
44
+ getNextPageParam,
45
+ selectItems,
46
+ requestOptions,
47
+ queryOptions,
48
+ realtime = null,
49
+ adapter = null,
50
+ recordIdParam = "recordId",
51
+ recordIdSelector = null,
52
+ viewUrlTemplate = "",
53
+ editUrlTemplate = "",
54
+ search = null,
55
+ queryParams = null,
56
+ requestQueryParams = null,
57
+ syncToRoute = false
58
+ } = {}) {
59
+ const searchConfig = normalizeListSearchConfig(search);
60
+ const routeSyncConfig = normalizeListSyncToRouteConfig(syncToRoute, {
61
+ defaultSearchParam: searchConfig.queryParam
62
+ });
63
+ const router = routeSyncConfig.enabled === true ? useRouter() : null;
64
+ const searchQuery = ref(searchConfig.initialQuery);
65
+ const debouncedSearchQuery = ref(searchConfig.initialQuery);
66
+ let searchDebounceTimer = null;
67
+ const isSearchDebouncing = ref(false);
68
+
69
+ watch(searchQuery, (value) => {
70
+ const normalizedValue = normalizeText(value);
71
+ if (searchDebounceTimer) {
72
+ clearTimeout(searchDebounceTimer);
73
+ }
74
+ if (searchConfig.enabled !== true) {
75
+ debouncedSearchQuery.value = normalizedValue;
76
+ isSearchDebouncing.value = false;
77
+ return;
78
+ }
79
+ isSearchDebouncing.value = true;
80
+ searchDebounceTimer = setTimeout(() => {
81
+ debouncedSearchQuery.value = normalizedValue;
82
+ isSearchDebouncing.value = false;
83
+ searchDebounceTimer = null;
84
+ }, searchConfig.debounceMs);
85
+ }, { immediate: true });
86
+
87
+ onScopeDispose(() => {
88
+ if (!searchDebounceTimer) {
89
+ return;
90
+ }
91
+ clearTimeout(searchDebounceTimer);
92
+ searchDebounceTimer = null;
93
+ });
94
+
95
+ const operationAdapter = resolveOperationAdapter(adapter, {
96
+ context: "useList adapter"
97
+ });
98
+ const operationScope = operationAdapter.useOperationScope({
99
+ ownershipFilter,
100
+ surfaceId,
101
+ access,
102
+ placementSource,
103
+ apiSuffix,
104
+ readEnabled,
105
+ queryKeyFactory,
106
+ permissionSets: {
107
+ view: viewPermissions
108
+ },
109
+ realtime
110
+ });
111
+ if (
112
+ routeSyncConfig.enabled === true &&
113
+ routeSyncConfig.hydrateFromRoute === true &&
114
+ routeSyncConfig.syncSearch === true &&
115
+ searchConfig.enabled === true
116
+ ) {
117
+ const routeQuerySource = asPlainObject(operationScope.routeContext.route?.query || {});
118
+ const routeSearchValue = routeQuerySource[routeSyncConfig.searchParam];
119
+ const hydratedSearch = normalizeText(Array.isArray(routeSearchValue) ? routeSearchValue[0] : routeSearchValue);
120
+
121
+ if (searchDebounceTimer) {
122
+ clearTimeout(searchDebounceTimer);
123
+ searchDebounceTimer = null;
124
+ }
125
+
126
+ searchQuery.value = hydratedSearch;
127
+ debouncedSearchQuery.value = hydratedSearch;
128
+ isSearchDebouncing.value = false;
129
+ }
130
+ const canView = operationScope.permissionGate("view");
131
+ const queryParamsContext = computed(() => {
132
+ return Object.freeze({
133
+ surfaceId: operationScope.routeContext.currentSurfaceId.value,
134
+ workspaceSlug: operationScope.workspaceSlugFromRoute.value,
135
+ ownershipFilter: operationScope.normalizedOwnershipFilter
136
+ });
137
+ });
138
+ const queryParamDescriptors = computed(() => {
139
+ return resolveQueryParamDescriptors(queryParams, queryParamsContext.value);
140
+ });
141
+ const requestQueryParamDescriptors = computed(() => {
142
+ return resolveQueryParamDescriptors(requestQueryParams, queryParamsContext.value);
143
+ });
144
+ const declaredQueryParamKeys = computed(() => {
145
+ return queryParamDescriptors.value.map((descriptor) => descriptor.key);
146
+ });
147
+ const activeQueryParamEntries = computed(() => {
148
+ return resolveActiveQueryParamEntries(queryParamDescriptors.value);
149
+ });
150
+ const activeRequestQueryParamEntries = computed(() => {
151
+ return resolveActiveQueryParamEntries(requestQueryParamDescriptors.value);
152
+ });
153
+ const activeQueryParamsToken = computed(() => buildQueryParamEntriesToken(activeQueryParamEntries.value));
154
+ const activeRequestQueryParamsToken = computed(() => {
155
+ return buildQueryParamEntriesToken(activeRequestQueryParamEntries.value);
156
+ });
157
+ const writableQueryParamBindings = computed(() => {
158
+ return resolveWritableQueryParamBindings(queryParamDescriptors.value);
159
+ });
160
+ const activeSearchQuery = computed(() => {
161
+ if (searchConfig.enabled !== true) {
162
+ return "";
163
+ }
164
+ const normalized = normalizeText(debouncedSearchQuery.value);
165
+ if (!normalized || normalized.length < searchConfig.minLength) {
166
+ return "";
167
+ }
168
+ return normalized;
169
+ });
170
+ const querySearchEnabled = computed(() => searchConfig.enabled === true && searchConfig.mode === "query");
171
+ const listPath = computed(() => {
172
+ const basePath = normalizeText(operationScope.apiPath.value);
173
+ if (!basePath) {
174
+ return "";
175
+ }
176
+
177
+ const searchParams = new URLSearchParams();
178
+ if (querySearchEnabled.value) {
179
+ const queryValue = activeSearchQuery.value;
180
+ if (queryValue) {
181
+ searchParams.set(searchConfig.queryParam, queryValue);
182
+ }
183
+ }
184
+
185
+ for (const entry of activeRequestQueryParamEntries.value) {
186
+ for (const value of entry.values) {
187
+ searchParams.append(entry.key, value);
188
+ }
189
+ }
190
+ for (const entry of activeQueryParamEntries.value) {
191
+ for (const value of entry.values) {
192
+ searchParams.append(entry.key, value);
193
+ }
194
+ }
195
+
196
+ const serializedSearch = searchParams.toString();
197
+ if (!serializedSearch) {
198
+ return basePath;
199
+ }
200
+
201
+ return appendQueryString(basePath, serializedSearch);
202
+ });
203
+ const listQueryKey = computed(() => {
204
+ const sourceQueryKey = operationScope.queryKey.value;
205
+ const baseQueryKey = Array.isArray(sourceQueryKey)
206
+ ? [...sourceQueryKey]
207
+ : sourceQueryKey == null
208
+ ? []
209
+ : [sourceQueryKey];
210
+ if (activeRequestQueryParamsToken.value) {
211
+ baseQueryKey.push("__request_query__", activeRequestQueryParamsToken.value);
212
+ }
213
+ if (querySearchEnabled.value) {
214
+ baseQueryKey.push("__search__", searchConfig.queryParam, activeSearchQuery.value);
215
+ }
216
+ if (activeQueryParamsToken.value) {
217
+ baseQueryKey.push("__query__", activeQueryParamsToken.value);
218
+ }
219
+ return baseQueryKey;
220
+ });
221
+
222
+ const list = useListCore({
223
+ queryKey: listQueryKey,
224
+ path: listPath,
225
+ enabled: operationScope.queryCanRun(canView),
226
+ initialPageParam,
227
+ getNextPageParam,
228
+ selectItems,
229
+ requestOptions,
230
+ queryOptions,
231
+ fallbackLoadError
232
+ });
233
+ const routeSyncHydrated = ref(routeSyncConfig.enabled !== true);
234
+ const routeSyncApplying = ref(false);
235
+ const routeSyncManagedKeyHistory = ref([]);
236
+ const routeSyncQueryParamBlacklist = computed(() => {
237
+ if (routeSyncConfig.enabled !== true || routeSyncConfig.syncQueryParams !== true) {
238
+ return EMPTY_ROUTE_SYNC_QUERY_PARAM_BLACKLIST;
239
+ }
240
+ return routeSyncConfig.queryParamBlacklist;
241
+ });
242
+ const routeSyncQueryParamBlacklistSet = computed(() => {
243
+ return new Set(routeSyncQueryParamBlacklist.value);
244
+ });
245
+ if (routeSyncConfig.enabled === true && routeSyncConfig.syncQueryParams === true) {
246
+ watch(declaredQueryParamKeys, (nextKeys) => {
247
+ routeSyncManagedKeyHistory.value = mergeManagedQueryParamKeyHistory(
248
+ routeSyncManagedKeyHistory.value,
249
+ nextKeys
250
+ );
251
+ }, { immediate: true });
252
+ }
253
+ const routeSyncManagedKeys = computed(() => {
254
+ const managedKeys = resolveRouteSyncManagedKeys({
255
+ searchEnabled: searchConfig.enabled,
256
+ searchParam: routeSyncConfig.searchParam,
257
+ syncSearch: routeSyncConfig.enabled === true && routeSyncConfig.syncSearch === true,
258
+ syncQueryParams: routeSyncConfig.enabled === true && routeSyncConfig.syncQueryParams === true,
259
+ declaredKeys: declaredQueryParamKeys.value,
260
+ keyHistory: routeSyncManagedKeyHistory.value
261
+ });
262
+ if (routeSyncConfig.enabled !== true || routeSyncConfig.syncQueryParams !== true) {
263
+ return managedKeys;
264
+ }
265
+
266
+ const output = new Set(managedKeys);
267
+ for (const key of routeSyncQueryParamBlacklist.value) {
268
+ output.add(key);
269
+ }
270
+ return [...output].sort((left, right) => left.localeCompare(right));
271
+ });
272
+ const routeSyncDesiredQuery = computed(() => {
273
+ if (routeSyncConfig.enabled !== true) {
274
+ return {};
275
+ }
276
+
277
+ const desiredQuery = {};
278
+ if (routeSyncConfig.syncSearch === true && searchConfig.enabled === true) {
279
+ const normalizedSearch = normalizeText(searchQuery.value);
280
+ if (normalizedSearch) {
281
+ desiredQuery[routeSyncConfig.searchParam] = normalizedSearch;
282
+ }
283
+ }
284
+ if (routeSyncConfig.syncQueryParams === true) {
285
+ for (const entry of activeQueryParamEntries.value) {
286
+ if (routeSyncQueryParamBlacklistSet.value.has(entry.key)) {
287
+ continue;
288
+ }
289
+ if (entry.values.length === 1) {
290
+ desiredQuery[entry.key] = entry.values[0];
291
+ continue;
292
+ }
293
+ desiredQuery[entry.key] = [...entry.values];
294
+ }
295
+ }
296
+
297
+ return desiredQuery;
298
+ });
299
+ if (routeSyncConfig.enabled === true) {
300
+ watch(
301
+ () => operationScope.routeContext.route?.query || {},
302
+ (routeQuery) => {
303
+ if (routeSyncConfig.hydrateFromRoute !== true || routeSyncApplying.value === true) {
304
+ routeSyncHydrated.value = true;
305
+ return;
306
+ }
307
+
308
+ const routeQuerySource = asPlainObject(routeQuery);
309
+ if (routeSyncConfig.syncSearch === true && searchConfig.enabled === true) {
310
+ const routeSearchValue = routeQuerySource[routeSyncConfig.searchParam];
311
+ const nextSearch = normalizeText(Array.isArray(routeSearchValue) ? routeSearchValue[0] : routeSearchValue);
312
+ if (nextSearch !== searchQuery.value) {
313
+ searchQuery.value = nextSearch;
314
+ }
315
+ }
316
+ if (routeSyncConfig.syncQueryParams === true) {
317
+ for (const binding of writableQueryParamBindings.value) {
318
+ if (routeSyncQueryParamBlacklistSet.value.has(binding.key)) {
319
+ continue;
320
+ }
321
+ const nextValue = parseRouteBindingValue(binding, routeQuerySource[binding.key]);
322
+ const currentValue = typeof binding.get === "function" ? binding.get() : undefined;
323
+ if (areQueryParamBindingValuesEqual(currentValue, nextValue)) {
324
+ continue;
325
+ }
326
+ try {
327
+ binding.set(nextValue);
328
+ } catch {
329
+ // Ignore non-writable query param bindings.
330
+ }
331
+ }
332
+ }
333
+
334
+ routeSyncHydrated.value = true;
335
+ },
336
+ {
337
+ immediate: true
338
+ }
339
+ );
340
+
341
+ watch(
342
+ [routeSyncDesiredQuery, routeSyncManagedKeys],
343
+ async ([desiredQuery, managedKeys]) => {
344
+ if (routeSyncHydrated.value !== true || routeSyncApplying.value === true) {
345
+ return;
346
+ }
347
+
348
+ const managedKeySet = new Set(Array.isArray(managedKeys) ? managedKeys : []);
349
+ const currentQuery = asPlainObject(operationScope.routeContext.route?.query || {});
350
+ const nextQuery = {};
351
+
352
+ for (const [key, value] of Object.entries(currentQuery)) {
353
+ if (managedKeySet.has(key)) {
354
+ continue;
355
+ }
356
+ nextQuery[key] = value;
357
+ }
358
+ for (const [key, value] of Object.entries(asPlainObject(desiredQuery))) {
359
+ nextQuery[key] = value;
360
+ }
361
+
362
+ if (buildRouteQueryCompareToken(currentQuery) === buildRouteQueryCompareToken(nextQuery)) {
363
+ return;
364
+ }
365
+
366
+ routeSyncApplying.value = true;
367
+ try {
368
+ if (routeSyncConfig.mode === "push") {
369
+ await router.push({
370
+ query: nextQuery
371
+ });
372
+ } else {
373
+ await router.replace({
374
+ query: nextQuery
375
+ });
376
+ }
377
+ } finally {
378
+ routeSyncApplying.value = false;
379
+ }
380
+ }
381
+ );
382
+ }
383
+
384
+ watch(activeSearchQuery, (nextValue, previousValue) => {
385
+ if (!querySearchEnabled.value) {
386
+ return;
387
+ }
388
+ if (nextValue === previousValue) {
389
+ return;
390
+ }
391
+
392
+ list.trimToFirstPage();
393
+ });
394
+ watch(activeQueryParamsToken, (nextValue, previousValue) => {
395
+ if (nextValue === previousValue) {
396
+ return;
397
+ }
398
+
399
+ list.trimToFirstPage();
400
+ });
401
+ watch(activeRequestQueryParamsToken, (nextValue, previousValue) => {
402
+ if (nextValue === previousValue) {
403
+ return;
404
+ }
405
+
406
+ list.trimToFirstPage();
407
+ });
408
+ const filteredItems = computed(() => {
409
+ const sourceItems = Array.isArray(list.items.value) ? list.items.value : [];
410
+ if (searchConfig.enabled !== true || searchConfig.mode !== "local") {
411
+ return sourceItems;
412
+ }
413
+
414
+ const queryValue = activeSearchQuery.value;
415
+ if (!queryValue) {
416
+ return sourceItems;
417
+ }
418
+
419
+ return sourceItems.filter((item) => matchesLocalSearch(item, queryValue, searchConfig.fields));
420
+ });
421
+
422
+ const isInitialLoading = operationScope.isLoading(list.isInitialLoading);
423
+ const isFetching = operationScope.isLoading(list.isFetching);
424
+ const isRefetching = computed(() => Boolean(isFetching.value && !isInitialLoading.value));
425
+ const loadError = operationScope.loadError(list.loadError);
426
+ const isLoading = operationScope.isLoading(list.isLoading);
427
+ const listUiRuntime = createListUiRuntime({
428
+ items: filteredItems,
429
+ isInitialLoading,
430
+ recordIdParam,
431
+ recordIdSelector,
432
+ routeParams: computed(() => operationScope.routeContext.route?.params || {}),
433
+ routeParamNames: computed(() => resolveRouteParamNamesInOrder(operationScope.routeContext.route)),
434
+ routePath: computed(() => operationScope.routeContext.route?.path || ""),
435
+ viewUrlTemplate,
436
+ editUrlTemplate
437
+ });
438
+ setupOperationErrorReporting({
439
+ source: `${placementSource}.load`,
440
+ loadError,
441
+ dedupeWindowMs: 0,
442
+ loadActionFactory: () => ({
443
+ label: "Retry",
444
+ dismissOnRun: true,
445
+ handler() {
446
+ void list.reload();
447
+ }
448
+ })
449
+ });
450
+
451
+ return proxyRefs({
452
+ canView,
453
+ isInitialLoading,
454
+ isFetching,
455
+ isRefetching,
456
+ isLoading,
457
+ isLoadingMore: list.isLoadingMore,
458
+ hasMore: list.hasMore,
459
+ loadError,
460
+ pages: list.pages,
461
+ items: filteredItems,
462
+ reload: list.reload,
463
+ loadMore: list.loadMore,
464
+ hasViewUrl: listUiRuntime.hasViewUrl,
465
+ hasEditUrl: listUiRuntime.hasEditUrl,
466
+ actionColumnCount: listUiRuntime.actionColumnCount,
467
+ showListSkeleton: listUiRuntime.showListSkeleton,
468
+ resolveRowKey: listUiRuntime.resolveRowKey,
469
+ resolveParams: listUiRuntime.resolveParams,
470
+ resolveViewUrl: listUiRuntime.resolveViewUrl,
471
+ resolveEditUrl: listUiRuntime.resolveEditUrl,
472
+ searchEnabled: searchConfig.enabled,
473
+ searchMode: searchConfig.mode,
474
+ searchQuery,
475
+ searchLabel: searchConfig.label,
476
+ searchPlaceholder: searchConfig.placeholder,
477
+ isSearchDebouncing,
478
+ activeQueryParamsToken
479
+ });
480
+ }
481
+
482
+ export { useList };
@@ -1,12 +1,12 @@
1
1
  import { computed, proxyRefs } from "vue";
2
2
  import { useRoute } from "vue-router";
3
3
  import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
4
- import { useViewCore } from "./useViewCore.js";
5
- import { useEndpointResource } from "./useEndpointResource.js";
6
- import { resolveOperationAdapter } from "./operationAdapters.js";
7
- import { setupOperationErrorReporting } from "./operationUiHelpers.js";
8
- import { createViewUiRuntime } from "./viewUiRuntime.js";
9
- import { resolveLookupFieldDisplayValue } from "./crudLookupFieldLabelSupport.js";
4
+ import { useViewCore } from "../runtime/useViewCore.js";
5
+ import { useEndpointResource } from "../runtime/useEndpointResource.js";
6
+ import { resolveOperationAdapter } from "../runtime/operationAdapters.js";
7
+ import { setupOperationErrorReporting } from "../runtime/operationUiHelpers.js";
8
+ import { createViewUiRuntime } from "../runtime/viewUiRuntime.js";
9
+ import { resolveRouteParamNamesInOrder } from "../support/routeTemplateHelpers.js";
10
10
 
11
11
  function useView({
12
12
  ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
@@ -37,6 +37,7 @@ function useView({
37
37
  const viewUiRuntime = createViewUiRuntime({
38
38
  recordIdParam,
39
39
  routeParams: routeParams ?? computed(() => route?.params || {}),
40
+ routeParamNames: computed(() => resolveRouteParamNamesInOrder(route)),
40
41
  routePath: computed(() => route?.path || ""),
41
42
  routeRecordId,
42
43
  apiUrlTemplate,
@@ -113,7 +114,6 @@ function useView({
113
114
  listUrl: viewUiRuntime.listUrl,
114
115
  editUrl: viewUiRuntime.editUrl,
115
116
  resolveParams: viewUiRuntime.resolveParams,
116
- resolveFieldDisplay: resolveLookupFieldDisplayValue,
117
117
  canView,
118
118
  isLoading,
119
119
  isFetching,
@@ -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
- resolveRoutePathnameSource,
6
+ resolveScopedRoutePathname,
7
7
  resolveRouteTemplateLocation,
8
8
  toRouteParamValue
9
- } from "./routeTemplateHelpers.js";
9
+ } from "../support/routeTemplateHelpers.js";
10
10
 
11
11
  function toResolvedRecordId({ routeParams, recordIdParam, routeRecordId }) {
12
12
  const explicitRecordId = toRouteParamValue(
@@ -31,6 +31,7 @@ function resolveSavedRecordId(payload, saveRecordIdSelector) {
31
31
  function createAddEditUiRuntime({
32
32
  recordIdParam = "recordId",
33
33
  routeParams = null,
34
+ routeParamNames = null,
34
35
  routePath = "",
35
36
  routeRecordId = null,
36
37
  apiUrlTemplate = "",
@@ -63,10 +64,18 @@ function createAddEditUiRuntime({
63
64
  routeRecordId
64
65
  });
65
66
  sourceParams[normalizedRecordIdParam] = resolvedRecordId;
67
+ const currentPathname = resolveScopedRoutePathname({
68
+ currentPathname: routePath,
69
+ params: currentRouteParams,
70
+ orderedParamNames: routeParamNames,
71
+ anchorParamName: normalizedRecordIdParam,
72
+ anchorParamValue: resolvedRecordId,
73
+ anchorMode: "after"
74
+ });
66
75
 
67
76
  return resolveRouteTemplateLocation(normalizedTemplate, {
68
77
  params: sourceParams,
69
- currentPathname: resolveRoutePathnameSource(routePath)
78
+ currentPathname
70
79
  });
71
80
  }
72
81