@jskit-ai/users-web 0.1.80 → 0.1.82
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 +137 -21
- package/package.json +21 -10
- package/src/client/bulkActions.js +47 -0
- package/src/client/components/AccountSettingsClientElement.vue +69 -24
- package/src/client/components/CrudAddEditScreen.vue +186 -0
- package/src/client/components/CrudListBulkActionSurface.vue +126 -0
- package/src/client/components/CrudListFilterSurface.vue +377 -0
- package/src/client/components/CrudListScreen.vue +434 -0
- package/src/client/components/CrudViewScreen.vue +186 -0
- package/src/client/components/ProfileClientElement.vue +19 -12
- package/src/client/components/UsersHomeToolsWidget.vue +0 -1
- package/src/client/composables/records/useAddEdit.js +23 -2
- package/src/client/composables/records/useCrudList.js +5 -1
- package/src/client/composables/records/useView.js +1 -0
- package/src/client/composables/runtime/operationUiHelpers.js +7 -3
- package/src/client/composables/runtime/useEndpointResource.js +12 -2
- package/src/client/composables/runtime/useUiFeedback.js +4 -2
- package/src/client/composables/support/resourceLoadStateHelpers.js +33 -1
- package/src/client/composables/useAccountSettingsRuntime.js +10 -1
- package/src/client/composables/useCrudAddEditScreen.js +88 -0
- package/src/client/composables/useCrudListBulkActions.js +147 -0
- package/src/client/composables/useCrudListScreen.js +107 -0
- package/src/client/composables/useCrudViewScreen.js +67 -0
- package/src/client/composables/usePagedCollection.js +6 -1
- package/src/client/filters.js +15 -0
- package/src/client/index.js +5 -0
- package/src/shared/toolsOutletContracts.js +0 -4
- package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +34 -8
- package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +34 -8
- package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +34 -8
- package/test/crudListBulkActionSurface.test.js +27 -0
- package/test/crudListFilterSurface.test.js +45 -0
- package/test/crudScreenComponents.test.js +62 -0
- package/test/errorIntentContract.test.js +31 -0
- package/test/exportsContract.test.js +11 -0
- package/test/resourceLoadStateHelpers.test.js +35 -1
- package/test/settingsPlacementContract.test.js +146 -14
- package/test/useCrudListBulkActions.test.js +65 -0
|
@@ -163,9 +163,29 @@ function useAddEdit({
|
|
|
163
163
|
);
|
|
164
164
|
const loadError = operationScope.loadError(endpointResource.loadError);
|
|
165
165
|
const isLoading = operationScope.isLoading(endpointResource.isLoading);
|
|
166
|
+
const canRetryLoad = computed(() => Boolean(readEnabled !== false && endpointResource?.reload));
|
|
167
|
+
|
|
168
|
+
async function refresh() {
|
|
169
|
+
if (!canRetryLoad.value) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return endpointResource.reload();
|
|
174
|
+
}
|
|
175
|
+
|
|
166
176
|
setupOperationErrorReporting({
|
|
167
177
|
source: `${placementSource}.load`,
|
|
168
|
-
loadError
|
|
178
|
+
loadError,
|
|
179
|
+
dedupeWindowMs: 0,
|
|
180
|
+
loadActionFactory: () => canRetryLoad.value
|
|
181
|
+
? {
|
|
182
|
+
label: "Retry",
|
|
183
|
+
dismissOnRun: true,
|
|
184
|
+
handler() {
|
|
185
|
+
void refresh();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
: null
|
|
169
189
|
});
|
|
170
190
|
|
|
171
191
|
return proxyRefs({
|
|
@@ -177,13 +197,14 @@ function useAddEdit({
|
|
|
177
197
|
isRefetching,
|
|
178
198
|
isFieldLocked,
|
|
179
199
|
isSubmitDisabled,
|
|
200
|
+
canRetryLoad,
|
|
180
201
|
isLoading,
|
|
181
202
|
isSaving: addEdit.saving,
|
|
182
203
|
fieldErrors: addEdit.fieldErrors,
|
|
183
204
|
message: addEdit.message,
|
|
184
205
|
messageType: addEdit.messageType,
|
|
185
206
|
submit: addEdit.submit,
|
|
186
|
-
refresh
|
|
207
|
+
refresh,
|
|
187
208
|
resource: endpointResource,
|
|
188
209
|
recordId: addEditUiRuntime.recordId,
|
|
189
210
|
listUrl: addEditUiRuntime.listUrl,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { computed, unref } from "vue";
|
|
2
2
|
import { useRoute } from "vue-router";
|
|
3
3
|
import { resolveCrudJsonApiTransport } from "../crud/crudJsonApiTransportSupport.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveLookupFieldDisplayValue,
|
|
6
|
+
resolveRecordTitle
|
|
7
|
+
} from "../crud/crudLookupFieldLabelSupport.js";
|
|
5
8
|
import { resolveCrudBoundValues } from "../crud/crudBindingSupport.js";
|
|
6
9
|
import { resolveCrudListParentDescriptor } from "../internal/crudListParentTitleSupport.js";
|
|
7
10
|
import {
|
|
@@ -81,6 +84,7 @@ function useCrudList({
|
|
|
81
84
|
});
|
|
82
85
|
|
|
83
86
|
records.resolveFieldDisplay = resolveLookupFieldDisplayValue;
|
|
87
|
+
records.resolveRecordTitle = resolveRecordTitle;
|
|
84
88
|
return records;
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -12,8 +12,8 @@ function setupOperationErrorReporting({
|
|
|
12
12
|
notFoundError = null,
|
|
13
13
|
loadActionFactory = null,
|
|
14
14
|
notFoundActionFactory = null,
|
|
15
|
-
loadChannel = "
|
|
16
|
-
notFoundChannel = "
|
|
15
|
+
loadChannel = "",
|
|
16
|
+
notFoundChannel = "",
|
|
17
17
|
loadSeverity = "error",
|
|
18
18
|
notFoundSeverity = "warning",
|
|
19
19
|
dedupeWindowMs = 2000
|
|
@@ -27,7 +27,8 @@ function setupOperationErrorReporting({
|
|
|
27
27
|
|
|
28
28
|
function watchMessage(value, {
|
|
29
29
|
kind = "load",
|
|
30
|
-
channel = "
|
|
30
|
+
channel = "",
|
|
31
|
+
intent = "resource-load",
|
|
31
32
|
severity = "error",
|
|
32
33
|
actionFactory = null
|
|
33
34
|
} = {}) {
|
|
@@ -60,6 +61,7 @@ function setupOperationErrorReporting({
|
|
|
60
61
|
const reportResult = runtime.report({
|
|
61
62
|
source: normalizedSource,
|
|
62
63
|
message: nextMessage,
|
|
64
|
+
intent,
|
|
63
65
|
severity,
|
|
64
66
|
channel,
|
|
65
67
|
action,
|
|
@@ -83,6 +85,7 @@ function setupOperationErrorReporting({
|
|
|
83
85
|
watchMessage(loadError, {
|
|
84
86
|
kind: "load",
|
|
85
87
|
channel: loadChannel,
|
|
88
|
+
intent: "resource-load",
|
|
86
89
|
severity: loadSeverity,
|
|
87
90
|
actionFactory: loadActionFactory
|
|
88
91
|
});
|
|
@@ -92,6 +95,7 @@ function setupOperationErrorReporting({
|
|
|
92
95
|
watchMessage(notFoundError, {
|
|
93
96
|
kind: "not-found",
|
|
94
97
|
channel: notFoundChannel,
|
|
98
|
+
intent: "resource-load",
|
|
95
99
|
severity: notFoundSeverity,
|
|
96
100
|
actionFactory: notFoundActionFactory
|
|
97
101
|
});
|
|
@@ -4,7 +4,10 @@ import { usersWebHttpClient } from "../../lib/httpClient.js";
|
|
|
4
4
|
import { asPlainObject } from "../support/scopeHelpers.js";
|
|
5
5
|
import { resolveEnabledRef, resolveTextRef } from "../support/refValueHelpers.js";
|
|
6
6
|
import { toQueryErrorMessage } from "../support/errorMessageHelpers.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
hasResolvedQueryData,
|
|
9
|
+
mergeQueryMeta
|
|
10
|
+
} from "../support/resourceLoadStateHelpers.js";
|
|
8
11
|
|
|
9
12
|
function buildEndpointReadRequestOptions({
|
|
10
13
|
method = "GET",
|
|
@@ -67,6 +70,7 @@ function useEndpointResource({
|
|
|
67
70
|
writeMethod = "PATCH",
|
|
68
71
|
readQuery = null,
|
|
69
72
|
transport = null,
|
|
73
|
+
refreshOnPull = false,
|
|
70
74
|
queryOptions = null,
|
|
71
75
|
mutationOptions = null,
|
|
72
76
|
fallbackLoadError = "Unable to load resource.",
|
|
@@ -96,7 +100,13 @@ function useEndpointResource({
|
|
|
96
100
|
});
|
|
97
101
|
},
|
|
98
102
|
enabled: queryEnabled,
|
|
99
|
-
...(
|
|
103
|
+
...(refreshOnPull
|
|
104
|
+
? mergeQueryMeta(asPlainObject(queryOptions), {
|
|
105
|
+
jskit: {
|
|
106
|
+
refreshOnPull: true
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
: asPlainObject(queryOptions))
|
|
100
110
|
});
|
|
101
111
|
|
|
102
112
|
const mutation = useMutation({
|
|
@@ -5,8 +5,8 @@ import { toUiErrorMessage } from "../support/errorMessageHelpers.js";
|
|
|
5
5
|
function useUiFeedback({
|
|
6
6
|
initialType = "success",
|
|
7
7
|
source = "users-web.ui-feedback",
|
|
8
|
-
successChannel = "
|
|
9
|
-
errorChannel = "
|
|
8
|
+
successChannel = "",
|
|
9
|
+
errorChannel = "",
|
|
10
10
|
dedupeWindowMs = 2000
|
|
11
11
|
} = {}) {
|
|
12
12
|
const message = ref("");
|
|
@@ -55,6 +55,7 @@ function useUiFeedback({
|
|
|
55
55
|
errorRuntime.report({
|
|
56
56
|
source: normalizedSource,
|
|
57
57
|
message: normalizedMessage,
|
|
58
|
+
intent: "action-feedback",
|
|
58
59
|
severity: "success",
|
|
59
60
|
channel: successChannel,
|
|
60
61
|
dedupeKey: `${normalizedSource}:success:${normalizedMessage}`,
|
|
@@ -73,6 +74,7 @@ function useUiFeedback({
|
|
|
73
74
|
source: normalizedSource,
|
|
74
75
|
message: message.value,
|
|
75
76
|
cause: errorValue || null,
|
|
77
|
+
intent: "action-feedback",
|
|
76
78
|
severity: "error",
|
|
77
79
|
channel: errorChannel,
|
|
78
80
|
dedupeKey: `${normalizedSource}:error:${message.value}`,
|
|
@@ -7,4 +7,36 @@ function hasResolvedQueryData({ query = null, data = null } = {}) {
|
|
|
7
7
|
return querySucceeded || hasDataPayload;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
function mergeQueryMeta(queryOptions = null, meta = {}) {
|
|
11
|
+
const sourceOptions =
|
|
12
|
+
queryOptions && typeof queryOptions === "object" && !Array.isArray(queryOptions) ? queryOptions : {};
|
|
13
|
+
const sourceMeta =
|
|
14
|
+
sourceOptions.meta && typeof sourceOptions.meta === "object" && !Array.isArray(sourceOptions.meta)
|
|
15
|
+
? sourceOptions.meta
|
|
16
|
+
: {};
|
|
17
|
+
const sourceJskitMeta =
|
|
18
|
+
sourceMeta.jskit && typeof sourceMeta.jskit === "object" && !Array.isArray(sourceMeta.jskit)
|
|
19
|
+
? sourceMeta.jskit
|
|
20
|
+
: {};
|
|
21
|
+
const nextJskitMeta =
|
|
22
|
+
meta.jskit && typeof meta.jskit === "object" && !Array.isArray(meta.jskit)
|
|
23
|
+
? meta.jskit
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...sourceOptions,
|
|
28
|
+
meta: {
|
|
29
|
+
...sourceMeta,
|
|
30
|
+
...meta,
|
|
31
|
+
jskit: {
|
|
32
|
+
...sourceJskitMeta,
|
|
33
|
+
...nextJskitMeta
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
hasResolvedQueryData,
|
|
41
|
+
mergeQueryMeta
|
|
42
|
+
};
|
|
@@ -127,7 +127,7 @@ function useAccountSettingsRuntime() {
|
|
|
127
127
|
function reportAccountFeedback({
|
|
128
128
|
message,
|
|
129
129
|
severity = "error",
|
|
130
|
-
channel = "
|
|
130
|
+
channel = "",
|
|
131
131
|
dedupeKey = ""
|
|
132
132
|
} = {}) {
|
|
133
133
|
const normalizedMessage = String(message || "").trim();
|
|
@@ -138,6 +138,7 @@ function useAccountSettingsRuntime() {
|
|
|
138
138
|
errorRuntime.report({
|
|
139
139
|
source: "users-web.account-settings-runtime",
|
|
140
140
|
message: normalizedMessage,
|
|
141
|
+
intent: "action-feedback",
|
|
141
142
|
severity,
|
|
142
143
|
channel,
|
|
143
144
|
dedupeKey: dedupeKey || `users-web.account-settings-runtime:${severity}:${normalizedMessage}`,
|
|
@@ -301,6 +302,12 @@ function useAccountSettingsRuntime() {
|
|
|
301
302
|
|
|
302
303
|
const loadingSettings = computed(() => Boolean(settingsView.isLoading));
|
|
303
304
|
const refreshingSettings = computed(() => Boolean(settingsView.isRefetching));
|
|
305
|
+
const settingsLoadError = computed(() => String(settingsView.loadError || "").trim());
|
|
306
|
+
|
|
307
|
+
async function refreshSettings() {
|
|
308
|
+
return settingsView.refresh();
|
|
309
|
+
}
|
|
310
|
+
|
|
304
311
|
async function submitProfile() {
|
|
305
312
|
await profileAddEdit.submit();
|
|
306
313
|
}
|
|
@@ -384,6 +391,8 @@ function useAccountSettingsRuntime() {
|
|
|
384
391
|
backNavigationTarget,
|
|
385
392
|
loadingSettings,
|
|
386
393
|
refreshingSettings,
|
|
394
|
+
settingsLoadError,
|
|
395
|
+
refreshSettings,
|
|
387
396
|
profile,
|
|
388
397
|
preferences,
|
|
389
398
|
notifications
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { useRoute } from "vue-router";
|
|
3
|
+
import { useCrudAddEdit } from "./records/useCrudAddEdit.js";
|
|
4
|
+
|
|
5
|
+
function normalizeProvidedScreen(screen = null) {
|
|
6
|
+
return screen && typeof screen === "object" && !Array.isArray(screen)
|
|
7
|
+
? screen
|
|
8
|
+
: null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function useCrudAddEditScreen({
|
|
12
|
+
screen = null,
|
|
13
|
+
mode = "new",
|
|
14
|
+
title = "",
|
|
15
|
+
subtitle = "",
|
|
16
|
+
saveLabel = "Save",
|
|
17
|
+
cancelTo = "",
|
|
18
|
+
resource = null,
|
|
19
|
+
operationName = "",
|
|
20
|
+
formFields = [],
|
|
21
|
+
addEditOptions = {},
|
|
22
|
+
saveSuccess = {},
|
|
23
|
+
fieldBinding = null,
|
|
24
|
+
createModel = null,
|
|
25
|
+
buildPayload = null,
|
|
26
|
+
mapPayloadToModel = null,
|
|
27
|
+
input = null,
|
|
28
|
+
preserveCancelQuery = false
|
|
29
|
+
} = {}) {
|
|
30
|
+
const providedScreen = normalizeProvidedScreen(screen);
|
|
31
|
+
if (providedScreen) {
|
|
32
|
+
return providedScreen;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const route = useRoute();
|
|
36
|
+
const formRuntime = useCrudAddEdit({
|
|
37
|
+
resource,
|
|
38
|
+
operationName,
|
|
39
|
+
formFields,
|
|
40
|
+
addEditOptions,
|
|
41
|
+
saveSuccess,
|
|
42
|
+
fieldBinding,
|
|
43
|
+
createModel,
|
|
44
|
+
buildPayload,
|
|
45
|
+
mapPayloadToModel,
|
|
46
|
+
input
|
|
47
|
+
});
|
|
48
|
+
const resolvedMode = computed(() => String(unref(mode) || "new").trim() || "new");
|
|
49
|
+
const resolvedTitle = computed(() => String(unref(title) || "").trim());
|
|
50
|
+
const resolvedSubtitle = computed(() => String(unref(subtitle) || "").trim());
|
|
51
|
+
const resolvedSaveLabel = computed(() => String(unref(saveLabel) || "Save").trim() || "Save");
|
|
52
|
+
const resolvedCancelTo = computed(() => unref(cancelTo));
|
|
53
|
+
|
|
54
|
+
function resolveCancelTo(target = resolvedCancelTo.value) {
|
|
55
|
+
const resolvedTarget = unref(target);
|
|
56
|
+
if (!resolvedTarget) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof resolvedTarget === "string") {
|
|
61
|
+
const resolvedPath = formRuntime.addEdit.resolveParams(resolvedTarget);
|
|
62
|
+
if (!preserveCancelQuery || !resolvedPath) {
|
|
63
|
+
return resolvedPath;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
path: resolvedPath,
|
|
67
|
+
query: route.query
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return resolvedTarget;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
mode: resolvedMode,
|
|
76
|
+
title: resolvedTitle,
|
|
77
|
+
subtitle: resolvedSubtitle,
|
|
78
|
+
saveLabel: resolvedSaveLabel,
|
|
79
|
+
cancelTo: resolvedCancelTo,
|
|
80
|
+
formRuntime,
|
|
81
|
+
addEdit: formRuntime.addEdit,
|
|
82
|
+
formState: formRuntime.form,
|
|
83
|
+
resolveFieldErrors: formRuntime.resolveFieldErrors,
|
|
84
|
+
resolveCancelTo
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { useCrudAddEditScreen };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import { defineCrudListBulkActions } from "../bulkActions.js";
|
|
4
|
+
|
|
5
|
+
function normalizeSelectedId(value = "") {
|
|
6
|
+
return normalizeText(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function resolveActionKey(actionOrKey = "") {
|
|
10
|
+
return normalizeText(
|
|
11
|
+
actionOrKey && typeof actionOrKey === "object"
|
|
12
|
+
? actionOrKey.key
|
|
13
|
+
: actionOrKey
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function useCrudListBulkActions(actions = [], {
|
|
18
|
+
resolveRecordId = null,
|
|
19
|
+
resolveContext = null
|
|
20
|
+
} = {}) {
|
|
21
|
+
const normalizedActions = defineCrudListBulkActions(actions);
|
|
22
|
+
const selectedIds = ref([]);
|
|
23
|
+
const selectedRecordMap = new Map();
|
|
24
|
+
const executingActionKey = ref("");
|
|
25
|
+
|
|
26
|
+
const selectedCount = computed(() => selectedIds.value.length);
|
|
27
|
+
const hasSelection = computed(() => selectedCount.value > 0);
|
|
28
|
+
const hasActions = computed(() => normalizedActions.length > 0);
|
|
29
|
+
|
|
30
|
+
function getRecordId(record = {}, index = 0) {
|
|
31
|
+
const resolvedId = typeof resolveRecordId === "function"
|
|
32
|
+
? resolveRecordId(record, index)
|
|
33
|
+
: record?.id ?? record?.attributes?.id ?? index;
|
|
34
|
+
return normalizeSelectedId(resolvedId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setRecordSelected(record = {}, index = 0, selected = true) {
|
|
38
|
+
const recordId = getRecordId(record, index);
|
|
39
|
+
if (!recordId) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const nextIds = new Set(selectedIds.value);
|
|
44
|
+
if (selected) {
|
|
45
|
+
nextIds.add(recordId);
|
|
46
|
+
selectedRecordMap.set(recordId, record);
|
|
47
|
+
} else {
|
|
48
|
+
nextIds.delete(recordId);
|
|
49
|
+
selectedRecordMap.delete(recordId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
selectedIds.value = Array.from(nextIds);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isRecordSelected(record = {}, index = 0) {
|
|
56
|
+
const recordId = getRecordId(record, index);
|
|
57
|
+
return recordId ? selectedIds.value.includes(recordId) : false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setVisibleSelected(records = [], selected = true) {
|
|
61
|
+
for (const [index, record] of (Array.isArray(records) ? records : []).entries()) {
|
|
62
|
+
setRecordSelected(record, index, selected);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function allVisibleSelected(records = []) {
|
|
67
|
+
const visibleRecords = Array.isArray(records) ? records : [];
|
|
68
|
+
return visibleRecords.length > 0 && visibleRecords.every((record, index) => isRecordSelected(record, index));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function someVisibleSelected(records = []) {
|
|
72
|
+
return (Array.isArray(records) ? records : []).some((record, index) => isRecordSelected(record, index));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearSelection() {
|
|
76
|
+
selectedIds.value = [];
|
|
77
|
+
selectedRecordMap.clear();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findAction(actionOrKey = "") {
|
|
81
|
+
const actionKey = resolveActionKey(actionOrKey);
|
|
82
|
+
return normalizedActions.find((action) => action.key === actionKey) || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isActionExecuting(actionOrKey = "") {
|
|
86
|
+
const actionKey = resolveActionKey(actionOrKey);
|
|
87
|
+
return Boolean(actionKey && executingActionKey.value === actionKey);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isActionDisabled(actionOrKey = "") {
|
|
91
|
+
const action = findAction(actionOrKey);
|
|
92
|
+
if (!action || !hasSelection.value || executingActionKey.value) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (typeof action.disabled === "function") {
|
|
96
|
+
return Boolean(action.disabled({
|
|
97
|
+
selectedIds: selectedIds.value.slice(),
|
|
98
|
+
selectedRecords: Array.from(selectedRecordMap.values()),
|
|
99
|
+
action
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
return Boolean(action.disabled);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function execute(actionOrKey = "") {
|
|
106
|
+
const action = findAction(actionOrKey);
|
|
107
|
+
if (!action || typeof action.run !== "function" || isActionDisabled(action)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
executingActionKey.value = action.key;
|
|
112
|
+
try {
|
|
113
|
+
const baseContext = typeof resolveContext === "function" ? resolveContext() : {};
|
|
114
|
+
return await action.run({
|
|
115
|
+
...(baseContext && typeof baseContext === "object" && !Array.isArray(baseContext) ? baseContext : {}),
|
|
116
|
+
action,
|
|
117
|
+
ids: selectedIds.value.slice(),
|
|
118
|
+
selectedIds: selectedIds.value.slice(),
|
|
119
|
+
selectedRecords: Array.from(selectedRecordMap.values()),
|
|
120
|
+
clearSelection
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
executingActionKey.value = "";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
actions: normalizedActions,
|
|
129
|
+
selectedIds,
|
|
130
|
+
selectedCount,
|
|
131
|
+
hasActions,
|
|
132
|
+
hasSelection,
|
|
133
|
+
executingActionKey,
|
|
134
|
+
setRecordSelected,
|
|
135
|
+
isRecordSelected,
|
|
136
|
+
setVisibleSelected,
|
|
137
|
+
allVisibleSelected,
|
|
138
|
+
someVisibleSelected,
|
|
139
|
+
clearSelection,
|
|
140
|
+
findAction,
|
|
141
|
+
isActionDisabled,
|
|
142
|
+
isActionExecuting,
|
|
143
|
+
execute
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { useCrudListBulkActions };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useCrudList } from "./records/useCrudList.js";
|
|
3
|
+
import { useCrudListBulkActions } from "./useCrudListBulkActions.js";
|
|
4
|
+
import { useCrudListFilters } from "./useCrudListFilters.js";
|
|
5
|
+
|
|
6
|
+
function formatCrudListCardValue(value) {
|
|
7
|
+
if (value === null || value === undefined || value === "") {
|
|
8
|
+
return "-";
|
|
9
|
+
}
|
|
10
|
+
if (value === true) {
|
|
11
|
+
return "Yes";
|
|
12
|
+
}
|
|
13
|
+
if (value === false) {
|
|
14
|
+
return "No";
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function useCrudListScreen({
|
|
20
|
+
adapter = null,
|
|
21
|
+
resource = null,
|
|
22
|
+
resourceNamespace = "resource",
|
|
23
|
+
apiSuffix = "",
|
|
24
|
+
recordIdParam = "recordId",
|
|
25
|
+
recordIdSelector = null,
|
|
26
|
+
titleFallbackFieldKey = "",
|
|
27
|
+
viewUrlTemplate = "",
|
|
28
|
+
editUrlTemplate = "",
|
|
29
|
+
newUrlTemplate = "",
|
|
30
|
+
recordChangedEvents = [],
|
|
31
|
+
listFilters = {},
|
|
32
|
+
listBulkActions = [],
|
|
33
|
+
routeQueryBlacklist = Object.freeze(["include", "cursor", "limit"]),
|
|
34
|
+
fallbackLoadError = "Unable to load records."
|
|
35
|
+
} = {}) {
|
|
36
|
+
const filterRuntime = useCrudListFilters(listFilters);
|
|
37
|
+
const normalizedRecordChangedEvents = Array.isArray(recordChangedEvents)
|
|
38
|
+
? recordChangedEvents
|
|
39
|
+
: [];
|
|
40
|
+
const normalizedResourceNamespace = String(resourceNamespace || "resource").trim() || "resource";
|
|
41
|
+
const records = useCrudList({
|
|
42
|
+
adapter: adapter || undefined,
|
|
43
|
+
resource,
|
|
44
|
+
apiSuffix,
|
|
45
|
+
queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
|
|
46
|
+
"ui-generator",
|
|
47
|
+
normalizedResourceNamespace,
|
|
48
|
+
"list",
|
|
49
|
+
String(surfaceId || ""),
|
|
50
|
+
String(workspaceSlug || "")
|
|
51
|
+
],
|
|
52
|
+
search: {
|
|
53
|
+
enabled: true,
|
|
54
|
+
mode: "query"
|
|
55
|
+
},
|
|
56
|
+
queryParams: filterRuntime.queryParams,
|
|
57
|
+
syncToRoute: {
|
|
58
|
+
enabled: true,
|
|
59
|
+
mode: "replace",
|
|
60
|
+
search: true,
|
|
61
|
+
queryParams: true,
|
|
62
|
+
queryParamBlacklist: routeQueryBlacklist
|
|
63
|
+
},
|
|
64
|
+
placementSource: `ui-generator.${normalizedResourceNamespace}.list`,
|
|
65
|
+
fallbackLoadError,
|
|
66
|
+
recordIdParam,
|
|
67
|
+
recordIdSelector,
|
|
68
|
+
viewUrlTemplate,
|
|
69
|
+
editUrlTemplate,
|
|
70
|
+
realtime: normalizedRecordChangedEvents.length > 0
|
|
71
|
+
? {
|
|
72
|
+
events: normalizedRecordChangedEvents
|
|
73
|
+
}
|
|
74
|
+
: null
|
|
75
|
+
});
|
|
76
|
+
const bulkActions = useCrudListBulkActions(listBulkActions, {
|
|
77
|
+
resolveRecordId: (record, index) => records.resolveRowKey(record, index),
|
|
78
|
+
resolveContext: () => ({
|
|
79
|
+
records,
|
|
80
|
+
reload: records.reload
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
const listPrimaryAction = computed(() =>
|
|
84
|
+
newUrlTemplate ? records.resolveParams(newUrlTemplate) : ""
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
function resolveRecordTitle(record) {
|
|
88
|
+
return records.resolveRecordTitle(record, {
|
|
89
|
+
fallbackKey: titleFallbackFieldKey,
|
|
90
|
+
defaultValue: "Record"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
records,
|
|
96
|
+
listFilters,
|
|
97
|
+
filterRuntime,
|
|
98
|
+
bulkActions,
|
|
99
|
+
listPrimaryAction,
|
|
100
|
+
hasViewUrl: Boolean(viewUrlTemplate),
|
|
101
|
+
hasEditUrl: Boolean(editUrlTemplate),
|
|
102
|
+
resolveRecordTitle,
|
|
103
|
+
formatListCardValue: formatCrudListCardValue
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { useCrudListScreen };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { useRoute } from "vue-router";
|
|
3
|
+
import { useCrudView } from "./records/useCrudView.js";
|
|
4
|
+
|
|
5
|
+
function useCrudViewScreen({
|
|
6
|
+
adapter = null,
|
|
7
|
+
resource = null,
|
|
8
|
+
resourceNamespace = "resource",
|
|
9
|
+
apiUrlTemplate = "",
|
|
10
|
+
recordIdParam = "recordId",
|
|
11
|
+
titleFallbackFieldKey = "",
|
|
12
|
+
listUrlTemplate = "",
|
|
13
|
+
editUrlTemplate = "",
|
|
14
|
+
recordChangedEvent = "",
|
|
15
|
+
fallbackLoadError = "Unable to load record.",
|
|
16
|
+
notFoundMessage = "Record not found."
|
|
17
|
+
} = {}) {
|
|
18
|
+
const route = useRoute();
|
|
19
|
+
const normalizedResourceNamespace = String(resourceNamespace || "resource").trim() || "resource";
|
|
20
|
+
const view = useCrudView({
|
|
21
|
+
adapter: adapter || undefined,
|
|
22
|
+
resource,
|
|
23
|
+
apiUrlTemplate,
|
|
24
|
+
recordIdParam,
|
|
25
|
+
includeRecordIdInQueryKey: true,
|
|
26
|
+
queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
|
|
27
|
+
"ui-generator",
|
|
28
|
+
normalizedResourceNamespace,
|
|
29
|
+
"view",
|
|
30
|
+
String(surfaceId || ""),
|
|
31
|
+
String(workspaceSlug || "")
|
|
32
|
+
],
|
|
33
|
+
placementSource: `ui-generator.${normalizedResourceNamespace}.view`,
|
|
34
|
+
fallbackLoadError,
|
|
35
|
+
notFoundMessage,
|
|
36
|
+
listUrlTemplate,
|
|
37
|
+
editUrlTemplate,
|
|
38
|
+
realtime: recordChangedEvent
|
|
39
|
+
? {
|
|
40
|
+
event: recordChangedEvent
|
|
41
|
+
}
|
|
42
|
+
: null
|
|
43
|
+
});
|
|
44
|
+
const recordTitle = computed(() =>
|
|
45
|
+
view.resolveRecordTitle(view.record, {
|
|
46
|
+
fallbackKey: titleFallbackFieldKey,
|
|
47
|
+
defaultValue: "Record"
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
function resolveRouteLocation(urlTemplate = "") {
|
|
52
|
+
const path = view.resolveParams(unref(urlTemplate));
|
|
53
|
+
return path ? { path, query: route.query } : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const listLocation = computed(() => resolveRouteLocation(listUrlTemplate));
|
|
57
|
+
const editLocation = computed(() => resolveRouteLocation(editUrlTemplate));
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
view,
|
|
61
|
+
recordTitle,
|
|
62
|
+
listLocation,
|
|
63
|
+
editLocation
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { useCrudViewScreen };
|
|
@@ -3,6 +3,7 @@ import { useInfiniteQuery, useQueryClient } from "@tanstack/vue-query";
|
|
|
3
3
|
import { asPlainObject } from "./support/scopeHelpers.js";
|
|
4
4
|
import { resolveEnabledRef } from "./support/refValueHelpers.js";
|
|
5
5
|
import { toQueryErrorMessage } from "./support/errorMessageHelpers.js";
|
|
6
|
+
import { mergeQueryMeta } from "./support/resourceLoadStateHelpers.js";
|
|
6
7
|
|
|
7
8
|
function defaultSelectItems(page) {
|
|
8
9
|
return Array.isArray(page?.items) ? page.items : [];
|
|
@@ -83,7 +84,11 @@ function usePagedCollection({
|
|
|
83
84
|
queryFn,
|
|
84
85
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
|
85
86
|
getNextPageParam(lastPage, allPages, lastPageParam, allPageParams),
|
|
86
|
-
...(asPlainObject(queryOptions)
|
|
87
|
+
...mergeQueryMeta(asPlainObject(queryOptions), {
|
|
88
|
+
jskit: {
|
|
89
|
+
refreshOnPull: true
|
|
90
|
+
}
|
|
91
|
+
})
|
|
87
92
|
});
|
|
88
93
|
|
|
89
94
|
const pages = computed(() => {
|