@jskit-ai/users-web 0.1.81 → 0.1.83

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 (40) hide show
  1. package/package.descriptor.mjs +51 -9
  2. package/package.json +21 -10
  3. package/src/client/bulkActions.js +47 -0
  4. package/src/client/components/AccountSettingsClientElement.vue +69 -24
  5. package/src/client/components/CrudAddEditScreen.vue +186 -0
  6. package/src/client/components/CrudListBulkActionSurface.vue +126 -0
  7. package/src/client/components/CrudListFilterSurface.vue +377 -0
  8. package/src/client/components/CrudListScreen.vue +434 -0
  9. package/src/client/components/CrudViewScreen.vue +186 -0
  10. package/src/client/components/ProfileClientElement.vue +19 -12
  11. package/src/client/composables/records/useAddEdit.js +23 -2
  12. package/src/client/composables/records/useCrudAddEdit.js +8 -0
  13. package/src/client/composables/records/useCrudList.js +11 -1
  14. package/src/client/composables/records/useCrudView.js +1 -0
  15. package/src/client/composables/records/useView.js +9 -2
  16. package/src/client/composables/runtime/operationUiHelpers.js +7 -3
  17. package/src/client/composables/runtime/useEndpointResource.js +20 -2
  18. package/src/client/composables/runtime/useUiFeedback.js +4 -2
  19. package/src/client/composables/support/resourceLoadStateHelpers.js +33 -1
  20. package/src/client/composables/useAccountSettingsRuntime.js +10 -1
  21. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  22. package/src/client/composables/useCrudListBulkActions.js +147 -0
  23. package/src/client/composables/useCrudListScreen.js +107 -0
  24. package/src/client/composables/useCrudViewScreen.js +67 -0
  25. package/src/client/composables/usePagedCollection.js +6 -1
  26. package/src/client/composables/useRealtimeQueryInvalidation.js +26 -0
  27. package/src/client/filters.js +15 -0
  28. package/src/client/index.js +5 -0
  29. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +34 -8
  30. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +34 -8
  31. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +34 -8
  32. package/test/crudListBulkActionSurface.test.js +27 -0
  33. package/test/crudListFilterSurface.test.js +45 -0
  34. package/test/crudScreenComponents.test.js +62 -0
  35. package/test/errorIntentContract.test.js +31 -0
  36. package/test/exportsContract.test.js +11 -0
  37. package/test/requestTransportOptions.test.js +110 -1
  38. package/test/resourceLoadStateHelpers.test.js +35 -1
  39. package/test/settingsPlacementContract.test.js +61 -0
  40. 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: endpointResource.reload,
207
+ refresh,
187
208
  resource: endpointResource,
188
209
  recordId: addEditUiRuntime.recordId,
189
210
  listUrl: addEditUiRuntime.listUrl,
@@ -6,6 +6,7 @@ import { useAddEdit } from "./useAddEdit.js";
6
6
  import {
7
7
  resolveCrudBoundValues,
8
8
  } from "../crud/crudBindingSupport.js";
9
+ import { resolveOperationRealtimeOptions } from "../useRealtimeQueryInvalidation.js";
9
10
  import {
10
11
  normalizeCrudFormFields,
11
12
  createCrudFormModel,
@@ -64,6 +65,12 @@ function useCrudAddEdit({
64
65
  operationName
65
66
  }
66
67
  );
68
+ const resolvedRealtime = resolveOperationRealtimeOptions({
69
+ realtime: normalizedAddEditOptions.realtime,
70
+ fallbackRealtime: resolvedResource?.operations?.[operationName]?.realtime ||
71
+ resolvedResource?.operations?.list?.realtime ||
72
+ null
73
+ });
67
74
  const saveSuccessOptions = normalizeSaveSuccessOptions(saveSuccess);
68
75
  const defaultFieldErrorKeys = normalizedFields.map((field) => field.key);
69
76
  const providedFieldErrorKeys = normalizeFieldErrorKeys(normalizedAddEditOptions.fieldErrorKeys);
@@ -185,6 +192,7 @@ function useCrudAddEdit({
185
192
  input: resolvedInput,
186
193
  buildRawPayload: resolveBuildRawPayload,
187
194
  mapLoadedToModel: effectiveMapLoadedToModel,
195
+ realtime: resolvedRealtime,
188
196
  onSaveSuccess: handleSaveSuccess
189
197
  });
190
198
  addEditRuntime = addEdit;
@@ -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 { resolveLookupFieldDisplayValue } from "../crud/crudLookupFieldLabelSupport.js";
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 {
@@ -9,6 +12,7 @@ import {
9
12
  toRouteParamValue
10
13
  } from "../support/routeTemplateHelpers.js";
11
14
  import { asPlainObject } from "../support/scopeHelpers.js";
15
+ import { resolveOperationRealtimeOptions } from "../useRealtimeQueryInvalidation.js";
12
16
  import { useList } from "./useList.js";
13
17
 
14
18
  function resolveRequestQueryParamsInput(requestQueryParams, context = {}) {
@@ -46,6 +50,7 @@ function useCrudList({
46
50
  parentBinding = null,
47
51
  recordIdParam = "recordId",
48
52
  route = null,
53
+ realtime = undefined,
49
54
  ...listOptions
50
55
  } = {}) {
51
56
  const sourceRoute = route && typeof route === "object" ? route : useRoute();
@@ -69,6 +74,10 @@ function useCrudList({
69
74
  transport: resolveCrudJsonApiTransport(listOptions.transport, resource, {
70
75
  mode: "list"
71
76
  }),
77
+ realtime: resolveOperationRealtimeOptions({
78
+ realtime,
79
+ fallbackRealtime: resource?.operations?.list?.realtime || null
80
+ }),
72
81
  recordIdParam,
73
82
  requestQueryParams(context = {}) {
74
83
  const baseRequestQueryParams = resolveRequestQueryParamsInput(requestQueryParams, context);
@@ -81,6 +90,7 @@ function useCrudList({
81
90
  });
82
91
 
83
92
  records.resolveFieldDisplay = resolveLookupFieldDisplayValue;
93
+ records.resolveRecordTitle = resolveRecordTitle;
84
94
  return records;
85
95
  }
86
96
 
@@ -29,6 +29,7 @@ function useCrudView({
29
29
  });
30
30
  const view = useView({
31
31
  ...viewOptions,
32
+ resource,
32
33
  transport: resolveCrudJsonApiTransport(viewOptions.transport, resource, {
33
34
  mode: "view"
34
35
  }),
@@ -8,6 +8,7 @@ import { setupOperationErrorReporting } from "../runtime/operationUiHelpers.js";
8
8
  import { createViewUiRuntime } from "../runtime/viewUiRuntime.js";
9
9
  import { createRequestQueryRuntime } from "../support/requestQueryRuntimeSupport.js";
10
10
  import { resolveRouteParamNamesInOrder } from "../support/routeTemplateHelpers.js";
11
+ import { resolveOperationRealtimeOptions } from "../useRealtimeQueryInvalidation.js";
11
12
 
12
13
  function useView({
13
14
  ownershipFilter = ROUTE_VISIBILITY_WORKSPACE,
@@ -33,7 +34,7 @@ function useView({
33
34
  listUrlTemplate = "",
34
35
  editUrlTemplate = "",
35
36
  includeRecordIdInQueryKey = false,
36
- realtime = null,
37
+ realtime = undefined,
37
38
  adapter = null
38
39
  } = {}) {
39
40
  const route = useRoute();
@@ -63,7 +64,12 @@ function useView({
63
64
  permissionSets: {
64
65
  view: viewPermissions
65
66
  },
66
- realtime
67
+ realtime: resolveOperationRealtimeOptions({
68
+ realtime,
69
+ fallbackRealtime: resource?.operations?.view?.realtime ||
70
+ resource?.operations?.list?.realtime ||
71
+ null
72
+ })
67
73
  });
68
74
  const queryParamsContext = computed(() => {
69
75
  return Object.freeze({
@@ -97,6 +103,7 @@ function useView({
97
103
  readMethod,
98
104
  readQuery: requestQueryRuntime.requestQuery,
99
105
  transport,
106
+ refreshOnPull: true,
100
107
  fallbackLoadError
101
108
  });
102
109
 
@@ -12,8 +12,8 @@ function setupOperationErrorReporting({
12
12
  notFoundError = null,
13
13
  loadActionFactory = null,
14
14
  notFoundActionFactory = null,
15
- loadChannel = "banner",
16
- notFoundChannel = "banner",
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 = "banner",
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,11 @@ 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 { hasResolvedQueryData } from "../support/resourceLoadStateHelpers.js";
7
+ import { useOperationRealtime } from "../useRealtimeQueryInvalidation.js";
8
+ import {
9
+ hasResolvedQueryData,
10
+ mergeQueryMeta
11
+ } from "../support/resourceLoadStateHelpers.js";
8
12
 
9
13
  function buildEndpointReadRequestOptions({
10
14
  method = "GET",
@@ -67,6 +71,8 @@ function useEndpointResource({
67
71
  writeMethod = "PATCH",
68
72
  readQuery = null,
69
73
  transport = null,
74
+ refreshOnPull = false,
75
+ realtime = null,
70
76
  queryOptions = null,
71
77
  mutationOptions = null,
72
78
  fallbackLoadError = "Unable to load resource.",
@@ -96,7 +102,18 @@ function useEndpointResource({
96
102
  });
97
103
  },
98
104
  enabled: queryEnabled,
99
- ...(asPlainObject(queryOptions))
105
+ ...(refreshOnPull
106
+ ? mergeQueryMeta(asPlainObject(queryOptions), {
107
+ jskit: {
108
+ refreshOnPull: true
109
+ }
110
+ })
111
+ : asPlainObject(queryOptions))
112
+ });
113
+ const realtimeBinding = useOperationRealtime({
114
+ realtime,
115
+ queryKey,
116
+ enabled: queryEnabled
100
117
  });
101
118
 
102
119
  const mutation = useMutation({
@@ -158,6 +175,7 @@ function useEndpointResource({
158
175
  isSaving,
159
176
  loadError,
160
177
  saveError,
178
+ realtime: realtimeBinding,
161
179
  reload,
162
180
  save
163
181
  });
@@ -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 = "snackbar",
9
- errorChannel = "banner",
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
- export { hasResolvedQueryData };
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 = "banner",
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 };