@jskit-ai/users-web 0.1.36 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +8 -22
- package/package.json +7 -6
- package/src/client/components/MembersAdminClientElement.vue +5 -5
- package/src/client/components/WorkspaceMembersClientElement.vue +16 -16
- package/src/client/components/WorkspacesClientElement.vue +2 -2
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +26 -172
- package/src/client/composables/accountSettingsRuntimeConstants.js +0 -4
- package/src/client/composables/accountSettingsRuntimeHelpers.js +1 -1
- package/src/client/composables/addEditUiRuntime.js +11 -2
- package/src/client/composables/crudLookupFieldLabelSupport.js +36 -4
- package/src/client/composables/crudLookupFieldRuntime.js +5 -2
- package/src/client/composables/crudSchemaFormHelpers.js +23 -3
- package/src/client/composables/listQueryParamSupport.js +459 -0
- package/src/client/composables/listUiRuntime.js +18 -6
- package/src/client/composables/routeTemplateHelpers.js +122 -0
- package/src/client/composables/useAddEdit.js +10 -0
- package/src/client/composables/useList.js +242 -2
- package/src/client/composables/usePagedCollection.js +55 -4
- package/src/client/composables/useView.js +4 -1
- package/src/client/composables/viewUiRuntime.js +11 -2
- package/src/client/lib/bootstrap.js +1 -1
- package/src/client/lib/menuIcons.js +27 -6
- package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
- package/test/addEditUiRuntime.test.js +18 -0
- package/test/crudLookupFieldRuntime.test.js +51 -1
- package/test/listQueryParamSupport.test.js +190 -0
- package/test/listUiRuntime.test.js +21 -0
- package/test/menuIcons.test.js +2 -0
- package/test/routeTemplateHelpers.test.js +56 -0
- package/test/usePagedCollection.test.js +53 -0
- package/test/viewUiRuntime.test.js +35 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
resolveResourceMessages
|
|
16
16
|
} from "./scopeHelpers.js";
|
|
17
|
+
import { resolveRouteParamNamesInOrder } from "./routeTemplateHelpers.js";
|
|
17
18
|
|
|
18
19
|
function useAddEdit({
|
|
19
20
|
ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
@@ -53,6 +54,7 @@ function useAddEdit({
|
|
|
53
54
|
const addEditUiRuntime = createAddEditUiRuntime({
|
|
54
55
|
recordIdParam,
|
|
55
56
|
routeParams: routeParams ?? computed(() => route?.params || {}),
|
|
57
|
+
routeParamNames: computed(() => resolveRouteParamNamesInOrder(route)),
|
|
56
58
|
routePath: computed(() => route?.path || ""),
|
|
57
59
|
routeRecordId,
|
|
58
60
|
apiUrlTemplate,
|
|
@@ -134,6 +136,12 @@ function useAddEdit({
|
|
|
134
136
|
const isInitialLoading = operationScope.isLoading(endpointResource.isInitialLoading);
|
|
135
137
|
const isFetching = operationScope.isLoading(endpointResource.isFetching);
|
|
136
138
|
const isRefetching = computed(() => Boolean(isFetching.value && !isInitialLoading.value));
|
|
139
|
+
const isFieldLocked = computed(() =>
|
|
140
|
+
Boolean(!canSave.value || addEdit.saving.value || isRefetching.value)
|
|
141
|
+
);
|
|
142
|
+
const isSubmitDisabled = computed(() =>
|
|
143
|
+
Boolean(isInitialLoading.value || isRefetching.value || !canSave.value)
|
|
144
|
+
);
|
|
137
145
|
const loadError = operationScope.loadError(endpointResource.loadError);
|
|
138
146
|
const isLoading = operationScope.isLoading(endpointResource.isLoading);
|
|
139
147
|
setupOperationErrorReporting({
|
|
@@ -148,6 +156,8 @@ function useAddEdit({
|
|
|
148
156
|
isInitialLoading,
|
|
149
157
|
isFetching,
|
|
150
158
|
isRefetching,
|
|
159
|
+
isFieldLocked,
|
|
160
|
+
isSubmitDisabled,
|
|
151
161
|
isLoading,
|
|
152
162
|
isSaving: addEdit.saving,
|
|
153
163
|
fieldErrors: addEdit.fieldErrors,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, onScopeDispose, proxyRefs, ref, watch } from "vue";
|
|
2
|
+
import { useRouter } from "vue-router";
|
|
2
3
|
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
3
4
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
5
|
import { resolveCrudLookupFieldKeys } from "@jskit-ai/kernel/shared/support/crudLookup";
|
|
@@ -7,10 +8,23 @@ import { useListCore } from "./useListCore.js";
|
|
|
7
8
|
import { resolveOperationAdapter } from "./operationAdapters.js";
|
|
8
9
|
import { setupOperationErrorReporting } from "./operationUiHelpers.js";
|
|
9
10
|
import { createListUiRuntime } from "./listUiRuntime.js";
|
|
11
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
10
12
|
import {
|
|
11
13
|
normalizeListSearchConfig,
|
|
12
14
|
matchesLocalSearch
|
|
13
15
|
} from "./listSearchSupport.js";
|
|
16
|
+
import {
|
|
17
|
+
normalizeListSyncToRouteConfig,
|
|
18
|
+
resolveQueryParamDescriptors,
|
|
19
|
+
resolveActiveQueryParamEntries,
|
|
20
|
+
resolveWritableQueryParamBindings,
|
|
21
|
+
buildQueryParamEntriesToken,
|
|
22
|
+
parseRouteBindingValue,
|
|
23
|
+
areQueryParamBindingValuesEqual,
|
|
24
|
+
buildRouteQueryCompareToken,
|
|
25
|
+
mergeManagedQueryParamKeyHistory,
|
|
26
|
+
resolveRouteSyncManagedKeys
|
|
27
|
+
} from "./listQueryParamSupport.js";
|
|
14
28
|
import { resolveLookupFieldDisplayValue } from "./crudLookupFieldLabelSupport.js";
|
|
15
29
|
import {
|
|
16
30
|
resolveRouteParamNamesInOrder,
|
|
@@ -18,6 +32,8 @@ import {
|
|
|
18
32
|
toRouteParamValue
|
|
19
33
|
} from "./routeTemplateHelpers.js";
|
|
20
34
|
|
|
35
|
+
const EMPTY_ROUTE_SYNC_QUERY_PARAM_BLACKLIST = Object.freeze([]);
|
|
36
|
+
|
|
21
37
|
function useList({
|
|
22
38
|
ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
23
39
|
surfaceId = "",
|
|
@@ -40,9 +56,15 @@ function useList({
|
|
|
40
56
|
recordIdSelector = null,
|
|
41
57
|
viewUrlTemplate = "",
|
|
42
58
|
editUrlTemplate = "",
|
|
43
|
-
search = null
|
|
59
|
+
search = null,
|
|
60
|
+
queryParams = null,
|
|
61
|
+
syncToRoute = false
|
|
44
62
|
} = {}) {
|
|
45
63
|
const searchConfig = normalizeListSearchConfig(search);
|
|
64
|
+
const routeSyncConfig = normalizeListSyncToRouteConfig(syncToRoute, {
|
|
65
|
+
defaultSearchParam: searchConfig.queryParam
|
|
66
|
+
});
|
|
67
|
+
const router = routeSyncConfig.enabled === true ? useRouter() : null;
|
|
46
68
|
const searchQuery = ref(searchConfig.initialQuery);
|
|
47
69
|
const debouncedSearchQuery = ref(searchConfig.initialQuery);
|
|
48
70
|
let searchDebounceTimer = null;
|
|
@@ -90,7 +112,46 @@ function useList({
|
|
|
90
112
|
},
|
|
91
113
|
realtime
|
|
92
114
|
});
|
|
115
|
+
if (
|
|
116
|
+
routeSyncConfig.enabled === true &&
|
|
117
|
+
routeSyncConfig.hydrateFromRoute === true &&
|
|
118
|
+
routeSyncConfig.syncSearch === true &&
|
|
119
|
+
searchConfig.enabled === true
|
|
120
|
+
) {
|
|
121
|
+
const routeQuerySource = asPlainObject(operationScope.routeContext.route?.query || {});
|
|
122
|
+
const routeSearchValue = routeQuerySource[routeSyncConfig.searchParam];
|
|
123
|
+
const hydratedSearch = normalizeText(Array.isArray(routeSearchValue) ? routeSearchValue[0] : routeSearchValue);
|
|
124
|
+
|
|
125
|
+
if (searchDebounceTimer) {
|
|
126
|
+
clearTimeout(searchDebounceTimer);
|
|
127
|
+
searchDebounceTimer = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
searchQuery.value = hydratedSearch;
|
|
131
|
+
debouncedSearchQuery.value = hydratedSearch;
|
|
132
|
+
isSearchDebouncing.value = false;
|
|
133
|
+
}
|
|
93
134
|
const canView = operationScope.permissionGate("view");
|
|
135
|
+
const queryParamsContext = computed(() => {
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
surfaceId: operationScope.routeContext.currentSurfaceId.value,
|
|
138
|
+
workspaceSlug: operationScope.workspaceSlugFromRoute.value,
|
|
139
|
+
ownershipFilter: operationScope.normalizedOwnershipFilter
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
const queryParamDescriptors = computed(() => {
|
|
143
|
+
return resolveQueryParamDescriptors(queryParams, queryParamsContext.value);
|
|
144
|
+
});
|
|
145
|
+
const declaredQueryParamKeys = computed(() => {
|
|
146
|
+
return queryParamDescriptors.value.map((descriptor) => descriptor.key);
|
|
147
|
+
});
|
|
148
|
+
const activeQueryParamEntries = computed(() => {
|
|
149
|
+
return resolveActiveQueryParamEntries(queryParamDescriptors.value);
|
|
150
|
+
});
|
|
151
|
+
const activeQueryParamsToken = computed(() => buildQueryParamEntriesToken(activeQueryParamEntries.value));
|
|
152
|
+
const writableQueryParamBindings = computed(() => {
|
|
153
|
+
return resolveWritableQueryParamBindings(queryParamDescriptors.value);
|
|
154
|
+
});
|
|
94
155
|
const parentRouteFilter = computed(() => {
|
|
95
156
|
const lookupFieldKeys = resolveCrudLookupFieldKeys(resource);
|
|
96
157
|
if (lookupFieldKeys.length < 1) {
|
|
@@ -157,6 +218,12 @@ function useList({
|
|
|
157
218
|
searchParams.set(parentFilter.key, parentFilter.value);
|
|
158
219
|
}
|
|
159
220
|
|
|
221
|
+
for (const entry of activeQueryParamEntries.value) {
|
|
222
|
+
for (const value of entry.values) {
|
|
223
|
+
searchParams.append(entry.key, value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
160
227
|
const serializedSearch = searchParams.toString();
|
|
161
228
|
if (!serializedSearch) {
|
|
162
229
|
return basePath;
|
|
@@ -178,6 +245,9 @@ function useList({
|
|
|
178
245
|
if (querySearchEnabled.value) {
|
|
179
246
|
baseQueryKey.push("__search__", searchConfig.queryParam, activeSearchQuery.value);
|
|
180
247
|
}
|
|
248
|
+
if (activeQueryParamsToken.value) {
|
|
249
|
+
baseQueryKey.push("__query__", activeQueryParamsToken.value);
|
|
250
|
+
}
|
|
181
251
|
return baseQueryKey;
|
|
182
252
|
});
|
|
183
253
|
|
|
@@ -192,6 +262,174 @@ function useList({
|
|
|
192
262
|
queryOptions,
|
|
193
263
|
fallbackLoadError
|
|
194
264
|
});
|
|
265
|
+
const routeSyncHydrated = ref(routeSyncConfig.enabled !== true);
|
|
266
|
+
const routeSyncApplying = ref(false);
|
|
267
|
+
const routeSyncManagedKeyHistory = ref([]);
|
|
268
|
+
const routeSyncQueryParamBlacklist = computed(() => {
|
|
269
|
+
if (routeSyncConfig.enabled !== true || routeSyncConfig.syncQueryParams !== true) {
|
|
270
|
+
return EMPTY_ROUTE_SYNC_QUERY_PARAM_BLACKLIST;
|
|
271
|
+
}
|
|
272
|
+
return routeSyncConfig.queryParamBlacklist;
|
|
273
|
+
});
|
|
274
|
+
const routeSyncQueryParamBlacklistSet = computed(() => {
|
|
275
|
+
return new Set(routeSyncQueryParamBlacklist.value);
|
|
276
|
+
});
|
|
277
|
+
if (routeSyncConfig.enabled === true && routeSyncConfig.syncQueryParams === true) {
|
|
278
|
+
watch(declaredQueryParamKeys, (nextKeys) => {
|
|
279
|
+
routeSyncManagedKeyHistory.value = mergeManagedQueryParamKeyHistory(
|
|
280
|
+
routeSyncManagedKeyHistory.value,
|
|
281
|
+
nextKeys
|
|
282
|
+
);
|
|
283
|
+
}, { immediate: true });
|
|
284
|
+
}
|
|
285
|
+
const routeSyncManagedKeys = computed(() => {
|
|
286
|
+
const managedKeys = resolveRouteSyncManagedKeys({
|
|
287
|
+
searchEnabled: searchConfig.enabled,
|
|
288
|
+
searchParam: routeSyncConfig.searchParam,
|
|
289
|
+
syncSearch: routeSyncConfig.enabled === true && routeSyncConfig.syncSearch === true,
|
|
290
|
+
syncQueryParams: routeSyncConfig.enabled === true && routeSyncConfig.syncQueryParams === true,
|
|
291
|
+
declaredKeys: declaredQueryParamKeys.value,
|
|
292
|
+
keyHistory: routeSyncManagedKeyHistory.value
|
|
293
|
+
});
|
|
294
|
+
if (routeSyncConfig.enabled !== true || routeSyncConfig.syncQueryParams !== true) {
|
|
295
|
+
return managedKeys;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const output = new Set(managedKeys);
|
|
299
|
+
for (const key of routeSyncQueryParamBlacklist.value) {
|
|
300
|
+
output.add(key);
|
|
301
|
+
}
|
|
302
|
+
return [...output].sort((left, right) => left.localeCompare(right));
|
|
303
|
+
});
|
|
304
|
+
const routeSyncDesiredQuery = computed(() => {
|
|
305
|
+
if (routeSyncConfig.enabled !== true) {
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const desiredQuery = {};
|
|
310
|
+
if (routeSyncConfig.syncSearch === true && searchConfig.enabled === true) {
|
|
311
|
+
const normalizedSearch = normalizeText(searchQuery.value);
|
|
312
|
+
if (normalizedSearch) {
|
|
313
|
+
desiredQuery[routeSyncConfig.searchParam] = normalizedSearch;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (routeSyncConfig.syncQueryParams === true) {
|
|
317
|
+
for (const entry of activeQueryParamEntries.value) {
|
|
318
|
+
if (routeSyncQueryParamBlacklistSet.value.has(entry.key)) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (entry.values.length === 1) {
|
|
322
|
+
desiredQuery[entry.key] = entry.values[0];
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
desiredQuery[entry.key] = [...entry.values];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return desiredQuery;
|
|
330
|
+
});
|
|
331
|
+
if (routeSyncConfig.enabled === true) {
|
|
332
|
+
watch(
|
|
333
|
+
() => operationScope.routeContext.route?.query || {},
|
|
334
|
+
(routeQuery) => {
|
|
335
|
+
if (routeSyncConfig.hydrateFromRoute !== true || routeSyncApplying.value === true) {
|
|
336
|
+
routeSyncHydrated.value = true;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const routeQuerySource = asPlainObject(routeQuery);
|
|
341
|
+
if (routeSyncConfig.syncSearch === true && searchConfig.enabled === true) {
|
|
342
|
+
const routeSearchValue = routeQuerySource[routeSyncConfig.searchParam];
|
|
343
|
+
const nextSearch = normalizeText(Array.isArray(routeSearchValue) ? routeSearchValue[0] : routeSearchValue);
|
|
344
|
+
if (nextSearch !== searchQuery.value) {
|
|
345
|
+
searchQuery.value = nextSearch;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (routeSyncConfig.syncQueryParams === true) {
|
|
349
|
+
for (const binding of writableQueryParamBindings.value) {
|
|
350
|
+
if (routeSyncQueryParamBlacklistSet.value.has(binding.key)) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const nextValue = parseRouteBindingValue(binding, routeQuerySource[binding.key]);
|
|
354
|
+
const currentValue = typeof binding.get === "function" ? binding.get() : undefined;
|
|
355
|
+
if (areQueryParamBindingValuesEqual(currentValue, nextValue)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
binding.set(nextValue);
|
|
360
|
+
} catch {
|
|
361
|
+
// Ignore non-writable query param bindings.
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
routeSyncHydrated.value = true;
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
immediate: true
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
watch(
|
|
374
|
+
[routeSyncDesiredQuery, routeSyncManagedKeys],
|
|
375
|
+
async ([desiredQuery, managedKeys]) => {
|
|
376
|
+
if (routeSyncHydrated.value !== true || routeSyncApplying.value === true) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const managedKeySet = new Set(Array.isArray(managedKeys) ? managedKeys : []);
|
|
381
|
+
const currentQuery = asPlainObject(operationScope.routeContext.route?.query || {});
|
|
382
|
+
const nextQuery = {};
|
|
383
|
+
|
|
384
|
+
for (const [key, value] of Object.entries(currentQuery)) {
|
|
385
|
+
if (managedKeySet.has(key)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
nextQuery[key] = value;
|
|
389
|
+
}
|
|
390
|
+
for (const [key, value] of Object.entries(asPlainObject(desiredQuery))) {
|
|
391
|
+
nextQuery[key] = value;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (buildRouteQueryCompareToken(currentQuery) === buildRouteQueryCompareToken(nextQuery)) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
routeSyncApplying.value = true;
|
|
399
|
+
try {
|
|
400
|
+
if (routeSyncConfig.mode === "push") {
|
|
401
|
+
await router.push({
|
|
402
|
+
query: nextQuery
|
|
403
|
+
});
|
|
404
|
+
} else {
|
|
405
|
+
await router.replace({
|
|
406
|
+
query: nextQuery
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
routeSyncApplying.value = false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
watch(activeSearchQuery, (nextValue, previousValue) => {
|
|
417
|
+
if (!querySearchEnabled.value) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (nextValue === previousValue) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
list.trimToFirstPage();
|
|
425
|
+
});
|
|
426
|
+
watch(activeQueryParamsToken, (nextValue, previousValue) => {
|
|
427
|
+
if (nextValue === previousValue) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
list.trimToFirstPage();
|
|
432
|
+
});
|
|
195
433
|
const filteredItems = computed(() => {
|
|
196
434
|
const sourceItems = Array.isArray(list.items.value) ? list.items.value : [];
|
|
197
435
|
if (searchConfig.enabled !== true || searchConfig.mode !== "local") {
|
|
@@ -217,6 +455,7 @@ function useList({
|
|
|
217
455
|
recordIdParam,
|
|
218
456
|
recordIdSelector,
|
|
219
457
|
routeParams: computed(() => operationScope.routeContext.route?.params || {}),
|
|
458
|
+
routeParamNames: computed(() => resolveRouteParamNamesInOrder(operationScope.routeContext.route)),
|
|
220
459
|
routePath: computed(() => operationScope.routeContext.route?.path || ""),
|
|
221
460
|
viewUrlTemplate,
|
|
222
461
|
editUrlTemplate
|
|
@@ -261,7 +500,8 @@ function useList({
|
|
|
261
500
|
searchQuery,
|
|
262
501
|
searchLabel: searchConfig.label,
|
|
263
502
|
searchPlaceholder: searchConfig.placeholder,
|
|
264
|
-
isSearchDebouncing
|
|
503
|
+
isSearchDebouncing,
|
|
504
|
+
activeQueryParamsToken
|
|
265
505
|
});
|
|
266
506
|
}
|
|
267
507
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
2
|
-
import { useInfiniteQuery } from "@tanstack/vue-query";
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/vue-query";
|
|
3
3
|
import { asPlainObject } from "./scopeHelpers.js";
|
|
4
4
|
import { resolveEnabledRef } from "./refValueHelpers.js";
|
|
5
5
|
import { toQueryErrorMessage } from "./errorMessageHelpers.js";
|
|
@@ -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 {
|
|
173
|
+
export {
|
|
174
|
+
usePagedCollection,
|
|
175
|
+
trimInfinitePagesToFirst
|
|
176
|
+
};
|
|
@@ -6,7 +6,8 @@ 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
|
+
import { resolveLookupFieldDisplayValue, resolveRecordTitle } from "./crudLookupFieldLabelSupport.js";
|
|
10
|
+
import { resolveRouteParamNamesInOrder } from "./routeTemplateHelpers.js";
|
|
10
11
|
|
|
11
12
|
function useView({
|
|
12
13
|
ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
@@ -37,6 +38,7 @@ function useView({
|
|
|
37
38
|
const viewUiRuntime = createViewUiRuntime({
|
|
38
39
|
recordIdParam,
|
|
39
40
|
routeParams: routeParams ?? computed(() => route?.params || {}),
|
|
41
|
+
routeParamNames: computed(() => resolveRouteParamNamesInOrder(route)),
|
|
40
42
|
routePath: computed(() => route?.path || ""),
|
|
41
43
|
routeRecordId,
|
|
42
44
|
apiUrlTemplate,
|
|
@@ -114,6 +116,7 @@ function useView({
|
|
|
114
116
|
editUrl: viewUiRuntime.editUrl,
|
|
115
117
|
resolveParams: viewUiRuntime.resolveParams,
|
|
116
118
|
resolveFieldDisplay: resolveLookupFieldDisplayValue,
|
|
119
|
+
resolveRecordTitle,
|
|
117
120
|
canView,
|
|
118
121
|
isLoading,
|
|
119
122
|
isFetching,
|
|
@@ -3,7 +3,7 @@ import { asPlainObject } from "./scopeHelpers.js";
|
|
|
3
3
|
import {
|
|
4
4
|
normalizeRouteParamName,
|
|
5
5
|
resolveRouteParamsSource,
|
|
6
|
-
|
|
6
|
+
resolveScopedRoutePathname,
|
|
7
7
|
resolveRouteTemplateLocation,
|
|
8
8
|
toRouteParamValue
|
|
9
9
|
} from "./routeTemplateHelpers.js";
|
|
@@ -22,6 +22,7 @@ function resolveRecordId({ routeParams, recordIdParam, routeRecordId }) {
|
|
|
22
22
|
function createViewUiRuntime({
|
|
23
23
|
recordIdParam = "recordId",
|
|
24
24
|
routeParams = null,
|
|
25
|
+
routeParamNames = null,
|
|
25
26
|
routePath = "",
|
|
26
27
|
routeRecordId = null,
|
|
27
28
|
apiUrlTemplate = "",
|
|
@@ -53,10 +54,18 @@ function createViewUiRuntime({
|
|
|
53
54
|
routeRecordId
|
|
54
55
|
});
|
|
55
56
|
sourceParams[normalizedRecordIdParam] = resolvedRecordId;
|
|
57
|
+
const currentPathname = resolveScopedRoutePathname({
|
|
58
|
+
currentPathname: routePath,
|
|
59
|
+
params: currentRouteParams,
|
|
60
|
+
orderedParamNames: routeParamNames,
|
|
61
|
+
anchorParamName: normalizedRecordIdParam,
|
|
62
|
+
anchorParamValue: resolvedRecordId,
|
|
63
|
+
anchorMode: "at"
|
|
64
|
+
});
|
|
56
65
|
|
|
57
66
|
return resolveRouteTemplateLocation(normalizedTemplate, {
|
|
58
67
|
params: sourceParams,
|
|
59
|
-
currentPathname
|
|
68
|
+
currentPathname
|
|
60
69
|
});
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -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
|
-
|
|
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
|
|
74
|
-
if (
|
|
75
|
-
return
|
|
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
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
104
|
+
const resolvedExplicitIcon = resolveExplicitIconValue(icon);
|
|
105
|
+
if (resolvedExplicitIcon) {
|
|
106
|
+
return resolvedExplicitIcon;
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
const normalizedLabel = normalizeText(label).toLowerCase();
|
|
@@ -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="
|
|
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.
|
|
36
|
+
:subtitle="`/${invite.workspaceSlug} • role: ${invite.roleSid}`"
|
|
37
37
|
class="px-0"
|
|
38
38
|
>
|
|
39
39
|
<template #prepend>
|
|
@@ -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",
|