@jskit-ai/users-web 0.1.33 → 0.1.35
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 +10 -6
- package/package.json +7 -6
- package/src/client/components/WorkspaceMembersClientElement.vue +16 -16
- package/src/client/components/WorkspacesClientElement.vue +2 -2
- package/src/client/composables/crudLookupFieldLabelSupport.js +107 -0
- package/src/client/composables/crudLookupFieldRuntime.js +238 -0
- package/src/client/composables/crudSchemaFormHelpers.js +35 -0
- package/src/client/composables/listSearchSupport.js +70 -0
- package/src/client/composables/resourceLoadStateHelpers.js +10 -0
- package/src/client/composables/routeTemplateHelpers.js +54 -1
- package/src/client/composables/useAccountSettingsRuntime.js +14 -14
- package/src/client/composables/useAddEdit.js +2 -1
- package/src/client/composables/useCrudSchemaForm.js +37 -11
- package/src/client/composables/useEndpointResource.js +6 -1
- package/src/client/composables/useList.js +164 -8
- package/src/client/composables/useRealtimeQueryInvalidation.js +33 -8
- package/src/client/composables/useView.js +4 -2
- package/src/client/composables/useViewCore.js +12 -2
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +3 -6
- package/templates/src/composables/useWorkspaceNotFoundState.js +8 -15
- package/test/crudLookupFieldRuntime.test.js +189 -0
- package/test/resourceLoadStateHelpers.test.js +39 -0
- package/test/routeTemplateHelpers.test.js +29 -0
- package/test/useCrudSchemaForm.test.js +39 -0
- package/test/useListSearchSupport.test.js +61 -0
- 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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
351
|
-
const refreshingSettings = computed(() => Boolean(settingsView.isRefetching
|
|
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
|
|
354
|
-
const refreshingInvites = computed(() => Boolean(pendingInvitesView.isRefetching
|
|
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:
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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(() =>
|
|
184
|
+
const showFormSkeleton = computed(() => {
|
|
185
|
+
const hasResolvedData = hasResolvedQueryData({
|
|
186
|
+
query: addEdit?.resource?.query,
|
|
187
|
+
data: addEdit?.resource?.data
|
|
188
|
+
});
|
|
166
189
|
|
|
167
|
-
|
|
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
|
|
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:
|
|
50
|
-
path:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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(() =>
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 !==
|
|
27
|
+
() => Boolean(workspaceBootstrapStatus.value) && workspaceBootstrapStatus.value !== RESOLVED_WORKSPACE_STATUS
|
|
35
28
|
);
|
|
36
29
|
const workspaceUnavailableMessage = computed(
|
|
37
|
-
() => STATUS_MESSAGES[workspaceBootstrapStatus.value] ||
|
|
30
|
+
() => STATUS_MESSAGES[workspaceBootstrapStatus.value] || DEFAULT_WORKSPACE_UNAVAILABLE_MESSAGE
|
|
38
31
|
);
|
|
39
32
|
|
|
40
33
|
return Object.freeze({
|