@jskit-ai/users-web 0.1.33 → 0.1.34

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 (26) hide show
  1. package/package.descriptor.mjs +10 -6
  2. package/package.json +7 -6
  3. package/src/client/components/WorkspaceMembersClientElement.vue +16 -16
  4. package/src/client/components/WorkspacesClientElement.vue +2 -2
  5. package/src/client/composables/crudLookupFieldLabelSupport.js +107 -0
  6. package/src/client/composables/crudLookupFieldRuntime.js +238 -0
  7. package/src/client/composables/crudSchemaFormHelpers.js +35 -0
  8. package/src/client/composables/listSearchSupport.js +70 -0
  9. package/src/client/composables/resourceLoadStateHelpers.js +10 -0
  10. package/src/client/composables/routeTemplateHelpers.js +54 -1
  11. package/src/client/composables/useAccountSettingsRuntime.js +14 -14
  12. package/src/client/composables/useAddEdit.js +2 -1
  13. package/src/client/composables/useCrudSchemaForm.js +37 -11
  14. package/src/client/composables/useEndpointResource.js +6 -1
  15. package/src/client/composables/useList.js +164 -8
  16. package/src/client/composables/useRealtimeQueryInvalidation.js +33 -8
  17. package/src/client/composables/useView.js +4 -2
  18. package/src/client/composables/useViewCore.js +12 -2
  19. package/templates/src/components/account/settings/AccountSettingsClientElement.vue +3 -6
  20. package/templates/src/composables/useWorkspaceNotFoundState.js +8 -15
  21. package/test/crudLookupFieldRuntime.test.js +189 -0
  22. package/test/resourceLoadStateHelpers.test.js +39 -0
  23. package/test/routeTemplateHelpers.test.js +29 -0
  24. package/test/useCrudSchemaForm.test.js +39 -0
  25. package/test/useListSearchSupport.test.js +61 -0
  26. package/test/viewCoreLoading.test.js +44 -0
@@ -100,11 +100,64 @@ function resolveRouteTemplateLocation(routeTemplate = "", { params = {}, current
100
100
  return resolvedPathname;
101
101
  }
102
102
 
103
+ function extractRouteParamNames(pathTemplate = "") {
104
+ const normalizedTemplate = String(pathTemplate || "").trim();
105
+ if (!normalizedTemplate) {
106
+ return [];
107
+ }
108
+
109
+ const names = [];
110
+ const seen = new Set();
111
+ const pattern = /:([A-Za-z][A-Za-z0-9_]*)/g;
112
+ let match = null;
113
+ while ((match = pattern.exec(normalizedTemplate)) != null) {
114
+ const name = String(match[1] || "").trim();
115
+ if (!name || seen.has(name)) {
116
+ continue;
117
+ }
118
+ seen.add(name);
119
+ names.push(name);
120
+ }
121
+
122
+ return names;
123
+ }
124
+
125
+ function resolveRouteParamNamesInOrder(route = null) {
126
+ const sourceRoute = route && typeof route === "object" ? route : {};
127
+ const matched = Array.isArray(sourceRoute.matched) ? sourceRoute.matched : [];
128
+ const names = [];
129
+ const seen = new Set();
130
+
131
+ for (const entry of matched) {
132
+ const entryPath = String(entry?.path || "").trim();
133
+ if (!entryPath) {
134
+ continue;
135
+ }
136
+ for (const name of extractRouteParamNames(entryPath)) {
137
+ if (seen.has(name)) {
138
+ continue;
139
+ }
140
+ seen.add(name);
141
+ names.push(name);
142
+ }
143
+ }
144
+
145
+ if (names.length > 0) {
146
+ return names;
147
+ }
148
+
149
+ return Object.keys(asPlainObject(sourceRoute.params))
150
+ .map((entry) => String(entry || "").trim())
151
+ .filter(Boolean);
152
+ }
153
+
103
154
  export {
104
155
  normalizeRouteParamName,
105
156
  toRouteParamValue,
106
157
  resolveRouteParamsSource,
107
158
  resolveRoutePathnameSource,
108
159
  resolveRouteTemplatePath,
109
- resolveRouteTemplateLocation
160
+ resolveRouteTemplateLocation,
161
+ extractRouteParamNames,
162
+ resolveRouteParamNamesInOrder
110
163
  };
@@ -202,22 +202,22 @@ function useAccountSettingsRuntime() {
202
202
 
203
203
  const settingsView = useView({
204
204
  ownershipFilter: OWNERSHIP_PUBLIC,
205
- apiSuffix: "/settings",
206
- queryKeyFactory: () => accountSettingsQueryKey,
207
- realtime: {
208
- event: "account.settings.changed"
209
- },
205
+ apiSuffix: "/settings",
206
+ queryKeyFactory: () => accountSettingsQueryKey,
207
+ realtime: {
208
+ event: "account.settings.changed"
209
+ },
210
210
  fallbackLoadError: "Unable to load settings.",
211
211
  mapLoadedToModel: mapAccountSettingsPayload
212
212
  });
213
213
 
214
214
  const pendingInvitesView = useView({
215
215
  ownershipFilter: OWNERSHIP_PUBLIC,
216
- apiSuffix: "/bootstrap",
217
- queryKeyFactory: () => pendingInvitesQueryKey,
218
- realtime: {
219
- event: "workspace.invitations.pending.changed"
220
- },
216
+ apiSuffix: "/bootstrap",
217
+ queryKeyFactory: () => pendingInvitesQueryKey,
218
+ realtime: {
219
+ event: "workspace.invitations.pending.changed"
220
+ },
221
221
  fallbackLoadError: "Unable to load invitations.",
222
222
  model: pendingInvitesModel,
223
223
  mapLoadedToModel: (model, payload = {}) => {
@@ -347,11 +347,11 @@ function useAccountSettingsRuntime() {
347
347
  }
348
348
  });
349
349
 
350
- const loadingSettings = computed(() => Boolean(settingsView.isLoading.value));
351
- const refreshingSettings = computed(() => Boolean(settingsView.isRefetching.value));
350
+ const loadingSettings = computed(() => Boolean(settingsView.isLoading));
351
+ const refreshingSettings = computed(() => Boolean(settingsView.isRefetching));
352
352
  const invitesAvailable = computed(() => pendingInvitesModel.workspaceInvitesEnabled === true);
353
- const loadingInvites = computed(() => Boolean(pendingInvitesView.isLoading.value));
354
- const refreshingInvites = computed(() => Boolean(pendingInvitesView.isRefetching.value));
353
+ const loadingInvites = computed(() => Boolean(pendingInvitesView.isLoading));
354
+ const refreshingInvites = computed(() => Boolean(pendingInvitesView.isRefetching));
355
355
  const pendingInvites = computed(() => {
356
356
  return Array.isArray(pendingInvitesModel.pendingInvites) ? pendingInvitesModel.pendingInvites : [];
357
357
  });
@@ -92,11 +92,12 @@ function useAddEdit({
92
92
 
93
93
  const canView = operationScope.permissionGate("view");
94
94
  const canSave = operationScope.permissionGate("save");
95
+ const queryCanRun = operationScope.queryCanRun(canView);
95
96
 
96
97
  const endpointResource = useEndpointResource({
97
98
  queryKey: operationScope.queryKey,
98
99
  path: operationScope.apiPath,
99
- enabled: operationScope.queryCanRun(canView),
100
+ enabled: queryCanRun,
100
101
  readMethod,
101
102
  writeMethod,
102
103
  fallbackLoadError,
@@ -1,5 +1,5 @@
1
- import { computed, reactive } from "vue";
2
- import { useRouter } from "vue-router";
1
+ import { computed, proxyRefs, reactive, watch } from "vue";
2
+ import { useRoute, useRouter } from "vue-router";
3
3
  import { asPlainObject } from "./scopeHelpers.js";
4
4
  import { useAddEdit } from "./useAddEdit.js";
5
5
  import {
@@ -7,9 +7,11 @@ import {
7
7
  createCrudFormModel,
8
8
  buildCrudFormPayload,
9
9
  applyCrudPayloadToForm,
10
+ applyCrudRouteBoundFieldValues,
10
11
  resolveCrudFieldErrors,
11
12
  parseCrudResourceOperationInput
12
13
  } from "./crudSchemaFormHelpers.js";
14
+ import { hasResolvedQueryData } from "./resourceLoadStateHelpers.js";
13
15
 
14
16
  function normalizeFieldErrorKeys(keys = []) {
15
17
  return Array.isArray(keys)
@@ -46,6 +48,7 @@ function useCrudSchemaForm({
46
48
  parseInput = null
47
49
  } = {}) {
48
50
  const router = useRouter();
51
+ const route = useRoute();
49
52
  const normalizedFields = normalizeCrudFormFields(formFields);
50
53
  const normalizedAddEditOptions = asPlainObject(addEditOptions);
51
54
  const saveSuccessOptions = normalizeSaveSuccessOptions(saveSuccess);
@@ -58,6 +61,19 @@ function useCrudSchemaForm({
58
61
  ? asPlainObject(createModel(normalizedFields))
59
62
  : createCrudFormModel(normalizedFields);
60
63
  const form = hasProvidedModel ? providedModel : reactive(defaultModel);
64
+
65
+ function applyRouteBoundValues(target = {}) {
66
+ return applyCrudRouteBoundFieldValues(normalizedFields, target, route?.params || {});
67
+ }
68
+
69
+ applyRouteBoundValues(form);
70
+ watch(
71
+ () => route?.params,
72
+ () => {
73
+ applyRouteBoundValues(form);
74
+ },
75
+ { deep: true }
76
+ );
61
77
  const parseInputOverride = typeof parseInput === "function"
62
78
  ? parseInput
63
79
  : (typeof normalizedAddEditOptions.parseInput === "function" ? normalizedAddEditOptions.parseInput : null);
@@ -87,14 +103,15 @@ function useCrudSchemaForm({
87
103
  }
88
104
 
89
105
  function resolveBuildRawPayload(model = {}, context = {}) {
90
- if (buildPayloadOverride) {
91
- return buildPayloadOverride(model, {
92
- ...context,
93
- fields: normalizedFields
94
- });
95
- }
106
+ const payload = buildPayloadOverride
107
+ ? buildPayloadOverride(model, {
108
+ ...context,
109
+ fields: normalizedFields
110
+ })
111
+ : buildCrudFormPayload(normalizedFields, model);
112
+ applyRouteBoundValues(payload);
96
113
 
97
- return buildCrudFormPayload(normalizedFields, model);
114
+ return payload;
98
115
  }
99
116
 
100
117
  const effectiveMapLoadedToModel = mapPayloadToModelOverride
@@ -103,10 +120,12 @@ function useCrudSchemaForm({
103
120
  ...context,
104
121
  fields: normalizedFields
105
122
  });
123
+ applyRouteBoundValues(model);
106
124
  }
107
125
  : (shouldApplyDefaultMapPayload
108
126
  ? (model = {}, payload = {}) => {
109
127
  applyCrudPayloadToForm(normalizedFields, model, payload);
128
+ applyRouteBoundValues(model);
110
129
  }
111
130
  : undefined);
112
131
 
@@ -162,9 +181,16 @@ function useCrudSchemaForm({
162
181
  return resolveCrudFieldErrors(addEdit.fieldErrors, fieldKey);
163
182
  }
164
183
 
165
- const showFormSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
184
+ const showFormSkeleton = computed(() => {
185
+ const hasResolvedData = hasResolvedQueryData({
186
+ query: addEdit?.resource?.query,
187
+ data: addEdit?.resource?.data
188
+ });
166
189
 
167
- return Object.freeze({
190
+ return Boolean(addEdit.isInitialLoading) && !hasResolvedData;
191
+ });
192
+
193
+ return proxyRefs({
168
194
  formFields: normalizedFields,
169
195
  fieldErrorKeys,
170
196
  form,
@@ -4,6 +4,7 @@ import { usersWebHttpClient } from "../lib/httpClient.js";
4
4
  import { asPlainObject } from "./scopeHelpers.js";
5
5
  import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
6
6
  import { toQueryErrorMessage } from "./errorMessageHelpers.js";
7
+ import { hasResolvedQueryData } from "./resourceLoadStateHelpers.js";
7
8
 
8
9
  function useEndpointResource({
9
10
  queryKey,
@@ -66,7 +67,11 @@ function useEndpointResource({
66
67
  });
67
68
 
68
69
  const data = computed(() => query.data.value);
69
- const isInitialLoading = computed(() => Boolean(queryEnabled.value && query.isPending.value));
70
+ const hasResolvedData = computed(() => hasResolvedQueryData({
71
+ query,
72
+ data
73
+ }));
74
+ const isInitialLoading = computed(() => Boolean(queryEnabled.value && query.isPending.value && !hasResolvedData.value));
70
75
  const isFetching = computed(() => Boolean(queryEnabled.value && query.isFetching.value));
71
76
  const isRefetching = computed(() => Boolean(isFetching.value && !isInitialLoading.value));
72
77
  const isLoading = computed(() => Boolean(isInitialLoading.value || isFetching.value));
@@ -1,9 +1,22 @@
1
- import { computed } from "vue";
1
+ import { computed, onScopeDispose, proxyRefs, ref, watch } from "vue";
2
+ import { appendQueryString } from "@jskit-ai/kernel/shared/support";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { resolveCrudLookupFieldKeys } from "@jskit-ai/kernel/shared/support/crudLookup";
2
5
  import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
3
6
  import { useListCore } from "./useListCore.js";
4
7
  import { resolveOperationAdapter } from "./operationAdapters.js";
5
8
  import { setupOperationErrorReporting } from "./operationUiHelpers.js";
6
9
  import { createListUiRuntime } from "./listUiRuntime.js";
10
+ import {
11
+ normalizeListSearchConfig,
12
+ matchesLocalSearch
13
+ } from "./listSearchSupport.js";
14
+ import { resolveLookupFieldDisplayValue } from "./crudLookupFieldLabelSupport.js";
15
+ import {
16
+ resolveRouteParamNamesInOrder,
17
+ resolveRouteParamsSource,
18
+ toRouteParamValue
19
+ } from "./routeTemplateHelpers.js";
7
20
 
8
21
  function useList({
9
22
  ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
@@ -22,11 +35,45 @@ function useList({
22
35
  queryOptions,
23
36
  realtime = null,
24
37
  adapter = null,
38
+ resource = null,
25
39
  recordIdParam = "recordId",
26
40
  recordIdSelector = null,
27
41
  viewUrlTemplate = "",
28
- editUrlTemplate = ""
42
+ editUrlTemplate = "",
43
+ search = null
29
44
  } = {}) {
45
+ const searchConfig = normalizeListSearchConfig(search);
46
+ const searchQuery = ref(searchConfig.initialQuery);
47
+ const debouncedSearchQuery = ref(searchConfig.initialQuery);
48
+ let searchDebounceTimer = null;
49
+ const isSearchDebouncing = ref(false);
50
+
51
+ watch(searchQuery, (value) => {
52
+ const normalizedValue = normalizeText(value);
53
+ if (searchDebounceTimer) {
54
+ clearTimeout(searchDebounceTimer);
55
+ }
56
+ if (searchConfig.enabled !== true) {
57
+ debouncedSearchQuery.value = normalizedValue;
58
+ isSearchDebouncing.value = false;
59
+ return;
60
+ }
61
+ isSearchDebouncing.value = true;
62
+ searchDebounceTimer = setTimeout(() => {
63
+ debouncedSearchQuery.value = normalizedValue;
64
+ isSearchDebouncing.value = false;
65
+ searchDebounceTimer = null;
66
+ }, searchConfig.debounceMs);
67
+ }, { immediate: true });
68
+
69
+ onScopeDispose(() => {
70
+ if (!searchDebounceTimer) {
71
+ return;
72
+ }
73
+ clearTimeout(searchDebounceTimer);
74
+ searchDebounceTimer = null;
75
+ });
76
+
30
77
  const operationAdapter = resolveOperationAdapter(adapter, {
31
78
  context: "useList adapter"
32
79
  });
@@ -44,10 +91,99 @@ function useList({
44
91
  realtime
45
92
  });
46
93
  const canView = operationScope.permissionGate("view");
94
+ const parentRouteFilter = computed(() => {
95
+ const lookupFieldKeys = resolveCrudLookupFieldKeys(resource);
96
+ if (lookupFieldKeys.length < 1) {
97
+ return null;
98
+ }
99
+
100
+ const lookupFieldKeySet = new Set(lookupFieldKeys);
101
+ const sourceRoute = operationScope.routeContext.route;
102
+ const orderedRouteParamNames = resolveRouteParamNamesInOrder(sourceRoute);
103
+ if (orderedRouteParamNames.length < 1) {
104
+ return null;
105
+ }
106
+
107
+ const normalizedRecordIdParam = normalizeText(recordIdParam) || "recordId";
108
+ const parentParamName = [...orderedRouteParamNames]
109
+ .reverse()
110
+ .find((name) => (
111
+ name !== "workspaceSlug" &&
112
+ name !== normalizedRecordIdParam &&
113
+ lookupFieldKeySet.has(name)
114
+ )) || "";
115
+ if (!parentParamName) {
116
+ return null;
117
+ }
118
+
119
+ const routeParams = resolveRouteParamsSource(sourceRoute?.params || {});
120
+ const parentParamValue = toRouteParamValue(routeParams[parentParamName]);
121
+ if (!parentParamValue) {
122
+ return null;
123
+ }
124
+
125
+ return Object.freeze({
126
+ key: parentParamName,
127
+ value: parentParamValue
128
+ });
129
+ });
130
+ const activeSearchQuery = computed(() => {
131
+ if (searchConfig.enabled !== true) {
132
+ return "";
133
+ }
134
+ const normalized = normalizeText(debouncedSearchQuery.value);
135
+ if (!normalized || normalized.length < searchConfig.minLength) {
136
+ return "";
137
+ }
138
+ return normalized;
139
+ });
140
+ const querySearchEnabled = computed(() => searchConfig.enabled === true && searchConfig.mode === "query");
141
+ const listPath = computed(() => {
142
+ const basePath = normalizeText(operationScope.apiPath.value);
143
+ if (!basePath) {
144
+ return "";
145
+ }
146
+
147
+ const searchParams = new URLSearchParams();
148
+ if (querySearchEnabled.value) {
149
+ const queryValue = activeSearchQuery.value;
150
+ if (queryValue) {
151
+ searchParams.set(searchConfig.queryParam, queryValue);
152
+ }
153
+ }
154
+
155
+ const parentFilter = parentRouteFilter.value;
156
+ if (parentFilter) {
157
+ searchParams.set(parentFilter.key, parentFilter.value);
158
+ }
159
+
160
+ const serializedSearch = searchParams.toString();
161
+ if (!serializedSearch) {
162
+ return basePath;
163
+ }
164
+
165
+ return appendQueryString(basePath, serializedSearch);
166
+ });
167
+ const listQueryKey = computed(() => {
168
+ const sourceQueryKey = operationScope.queryKey.value;
169
+ const baseQueryKey = Array.isArray(sourceQueryKey)
170
+ ? [...sourceQueryKey]
171
+ : sourceQueryKey == null
172
+ ? []
173
+ : [sourceQueryKey];
174
+ const parentFilter = parentRouteFilter.value;
175
+ if (parentFilter) {
176
+ baseQueryKey.push("__parent__", parentFilter.key, parentFilter.value);
177
+ }
178
+ if (querySearchEnabled.value) {
179
+ baseQueryKey.push("__search__", searchConfig.queryParam, activeSearchQuery.value);
180
+ }
181
+ return baseQueryKey;
182
+ });
47
183
 
48
184
  const list = useListCore({
49
- queryKey: operationScope.queryKey,
50
- path: operationScope.apiPath,
185
+ queryKey: listQueryKey,
186
+ path: listPath,
51
187
  enabled: operationScope.queryCanRun(canView),
52
188
  initialPageParam,
53
189
  getNextPageParam,
@@ -56,6 +192,19 @@ function useList({
56
192
  queryOptions,
57
193
  fallbackLoadError
58
194
  });
195
+ const filteredItems = computed(() => {
196
+ const sourceItems = Array.isArray(list.items.value) ? list.items.value : [];
197
+ if (searchConfig.enabled !== true || searchConfig.mode !== "local") {
198
+ return sourceItems;
199
+ }
200
+
201
+ const queryValue = activeSearchQuery.value;
202
+ if (!queryValue) {
203
+ return sourceItems;
204
+ }
205
+
206
+ return sourceItems.filter((item) => matchesLocalSearch(item, queryValue, searchConfig.fields));
207
+ });
59
208
 
60
209
  const isInitialLoading = operationScope.isLoading(list.isInitialLoading);
61
210
  const isFetching = operationScope.isLoading(list.isFetching);
@@ -63,7 +212,7 @@ function useList({
63
212
  const loadError = operationScope.loadError(list.loadError);
64
213
  const isLoading = operationScope.isLoading(list.isLoading);
65
214
  const listUiRuntime = createListUiRuntime({
66
- items: list.items,
215
+ items: filteredItems,
67
216
  isInitialLoading,
68
217
  recordIdParam,
69
218
  recordIdSelector,
@@ -85,7 +234,7 @@ function useList({
85
234
  })
86
235
  });
87
236
 
88
- return Object.freeze({
237
+ return proxyRefs({
89
238
  canView,
90
239
  isInitialLoading,
91
240
  isFetching,
@@ -95,7 +244,7 @@ function useList({
95
244
  hasMore: list.hasMore,
96
245
  loadError,
97
246
  pages: list.pages,
98
- items: list.items,
247
+ items: filteredItems,
99
248
  reload: list.reload,
100
249
  loadMore: list.loadMore,
101
250
  hasViewUrl: listUiRuntime.hasViewUrl,
@@ -105,7 +254,14 @@ function useList({
105
254
  resolveRowKey: listUiRuntime.resolveRowKey,
106
255
  resolveParams: listUiRuntime.resolveParams,
107
256
  resolveViewUrl: listUiRuntime.resolveViewUrl,
108
- resolveEditUrl: listUiRuntime.resolveEditUrl
257
+ resolveEditUrl: listUiRuntime.resolveEditUrl,
258
+ resolveFieldDisplay: resolveLookupFieldDisplayValue,
259
+ searchEnabled: searchConfig.enabled,
260
+ searchMode: searchConfig.mode,
261
+ searchQuery,
262
+ searchLabel: searchConfig.label,
263
+ searchPlaceholder: searchConfig.placeholder,
264
+ isSearchDebouncing
109
265
  });
110
266
  }
111
267
 
@@ -77,11 +77,30 @@ function useOperationRealtime({
77
77
  enabled = true
78
78
  } = {}) {
79
79
  const source = normalizeRealtimeOptions(realtime);
80
- if (!source.event) {
80
+ const eventList = [];
81
+ if (Object.hasOwn(source, "event")) {
82
+ const normalizedEvent = normalizeText(source.event);
83
+ if (normalizedEvent) {
84
+ eventList.push(normalizedEvent);
85
+ }
86
+ }
87
+ if (Object.hasOwn(source, "events")) {
88
+ if (!Array.isArray(source.events)) {
89
+ throw new TypeError("realtime.events must be an array when configured.");
90
+ }
91
+ for (const entry of source.events) {
92
+ const normalizedEvent = normalizeText(entry);
93
+ if (!normalizedEvent || eventList.includes(normalizedEvent)) {
94
+ continue;
95
+ }
96
+ eventList.push(normalizedEvent);
97
+ }
98
+ }
99
+
100
+ if (eventList.length < 1) {
81
101
  return null;
82
102
  }
83
103
 
84
- const event = source.event;
85
104
  const matches = typeof source.matches === "function" ? source.matches : null;
86
105
  const onEvent = typeof source.onEvent === "function" ? source.onEvent : null;
87
106
  const resolvedQueryKey = Object.hasOwn(source, "queryKey") ? source.queryKey : queryKey;
@@ -90,12 +109,18 @@ function useOperationRealtime({
90
109
  return resolveEnabled(enabled) && resolveEnabled(sourceEnabled);
91
110
  });
92
111
 
93
- return useRealtimeQueryInvalidation({
94
- event,
95
- enabled: active,
96
- matches,
97
- queryKey: resolvedQueryKey,
98
- onEvent
112
+ const bindings = eventList.map((event) =>
113
+ useRealtimeQueryInvalidation({
114
+ event,
115
+ enabled: active,
116
+ matches,
117
+ queryKey: resolvedQueryKey,
118
+ onEvent
119
+ })
120
+ );
121
+
122
+ return Object.freeze({
123
+ active: computed(() => bindings.some((binding) => Boolean(binding?.active?.value)))
99
124
  });
100
125
  }
101
126
 
@@ -1,4 +1,4 @@
1
- import { computed } from "vue";
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
4
  import { useViewCore } from "./useViewCore.js";
@@ -6,6 +6,7 @@ import { useEndpointResource } from "./useEndpointResource.js";
6
6
  import { resolveOperationAdapter } from "./operationAdapters.js";
7
7
  import { setupOperationErrorReporting } from "./operationUiHelpers.js";
8
8
  import { createViewUiRuntime } from "./viewUiRuntime.js";
9
+ import { resolveLookupFieldDisplayValue } from "./crudLookupFieldLabelSupport.js";
9
10
 
10
11
  function useView({
11
12
  ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
@@ -106,12 +107,13 @@ function useView({
106
107
  })
107
108
  });
108
109
 
109
- return Object.freeze({
110
+ return proxyRefs({
110
111
  record: view.record,
111
112
  recordId: viewUiRuntime.recordId,
112
113
  listUrl: viewUiRuntime.listUrl,
113
114
  editUrl: viewUiRuntime.editUrl,
114
115
  resolveParams: viewUiRuntime.resolveParams,
116
+ resolveFieldDisplay: resolveLookupFieldDisplayValue,
115
117
  canView,
116
118
  isLoading,
117
119
  isFetching,
@@ -31,8 +31,18 @@ function useViewCore({
31
31
 
32
32
  const data = resource?.data;
33
33
  const record = computed(() => (model !== undefined ? model : data?.value));
34
- const isLoading = computed(() => Boolean(resource?.query?.isPending?.value));
35
- const isFetching = computed(() => Boolean(resource?.query?.isFetching?.value));
34
+ const isLoading = computed(() => {
35
+ if (resource?.isInitialLoading?.value !== undefined) {
36
+ return Boolean(resource.isInitialLoading.value);
37
+ }
38
+ return Boolean(resource?.query?.isPending?.value);
39
+ });
40
+ const isFetching = computed(() => {
41
+ if (resource?.isFetching?.value !== undefined) {
42
+ return Boolean(resource.isFetching.value);
43
+ }
44
+ return Boolean(resource?.query?.isFetching?.value);
45
+ });
36
46
  const error = computed(() => resource?.query?.error?.value || null);
37
47
 
38
48
  const isNotFound = computed(() => {
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
3
  import { useRoute, useRouter } from "vue-router";
4
+ import { normalizeOneOf } from "@jskit-ai/kernel/shared/support/normalize";
4
5
  import { useAccountSettingsRuntime } from "@jskit-ai/users-web/client/composables/useAccountSettingsRuntime";
5
6
  import AccountSettingsProfileSection from "./AccountSettingsProfileSection.vue";
6
7
  import AccountSettingsPreferencesSection from "./AccountSettingsPreferencesSection.vue";
@@ -17,15 +18,11 @@ const sections = Object.freeze([
17
18
  { title: "Notifications", value: "notifications" },
18
19
  { title: "Invites", value: "invites" }
19
20
  ]);
20
- const sectionValues = new Set(sections.map((section) => section.value));
21
+ const sectionValues = Object.freeze(sections.map((section) => section.value));
21
22
 
22
23
  function normalizeSection(value) {
23
24
  const source = Array.isArray(value) ? value[0] : value;
24
- const normalized = String(source || "").trim().toLowerCase();
25
- if (!sectionValues.has(normalized)) {
26
- return "profile";
27
- }
28
- return normalized;
25
+ return normalizeOneOf(source, sectionValues, "profile");
29
26
  }
30
27
 
31
28
  function readRouteSection() {
@@ -1,5 +1,6 @@
1
1
  import { computed } from "vue";
2
2
  import { useRoute } from "vue-router";
3
+ import { normalizeLowerText, normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
3
4
  import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
4
5
 
5
6
  const STATUS_MESSAGES = {
@@ -8,33 +9,25 @@ const STATUS_MESSAGES = {
8
9
  unauthenticated: "You need to sign in to access this workspace.",
9
10
  error: "Workspace data could not be loaded right now."
10
11
  };
12
+ const DEFAULT_WORKSPACE_UNAVAILABLE_MESSAGE = "Workspace is currently unavailable.";
13
+ const RESOLVED_WORKSPACE_STATUS = "resolved";
11
14
 
12
15
  function useWorkspaceNotFoundState() {
13
16
  const route = useRoute();
14
17
  const { context: placementContext } = useWebPlacementContext();
15
18
 
16
- const routeWorkspaceSlug = computed(() =>
17
- String(route?.params?.workspaceSlug || "")
18
- .trim()
19
- .toLowerCase()
20
- );
19
+ const routeWorkspaceSlug = computed(() => normalizeLowerText(route?.params?.workspaceSlug));
21
20
 
22
21
  const workspaceBootstrapStatus = computed(() => {
23
- const statuses =
24
- placementContext.value?.workspaceBootstrapStatuses &&
25
- typeof placementContext.value.workspaceBootstrapStatuses === "object"
26
- ? placementContext.value.workspaceBootstrapStatuses
27
- : {};
28
- return String(statuses[routeWorkspaceSlug.value] || "")
29
- .trim()
30
- .toLowerCase();
22
+ const statuses = normalizeObject(placementContext.value?.workspaceBootstrapStatuses);
23
+ return normalizeLowerText(statuses[routeWorkspaceSlug.value]);
31
24
  });
32
25
 
33
26
  const workspaceUnavailable = computed(
34
- () => Boolean(workspaceBootstrapStatus.value) && workspaceBootstrapStatus.value !== "resolved"
27
+ () => Boolean(workspaceBootstrapStatus.value) && workspaceBootstrapStatus.value !== RESOLVED_WORKSPACE_STATUS
35
28
  );
36
29
  const workspaceUnavailableMessage = computed(
37
- () => STATUS_MESSAGES[workspaceBootstrapStatus.value] || "Workspace is currently unavailable."
30
+ () => STATUS_MESSAGES[workspaceBootstrapStatus.value] || DEFAULT_WORKSPACE_UNAVAILABLE_MESSAGE
38
31
  );
39
32
 
40
33
  return Object.freeze({