@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.
Files changed (38) hide show
  1. package/package.descriptor.mjs +137 -21
  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/components/UsersHomeToolsWidget.vue +0 -1
  12. package/src/client/composables/records/useAddEdit.js +23 -2
  13. package/src/client/composables/records/useCrudList.js +5 -1
  14. package/src/client/composables/records/useView.js +1 -0
  15. package/src/client/composables/runtime/operationUiHelpers.js +7 -3
  16. package/src/client/composables/runtime/useEndpointResource.js +12 -2
  17. package/src/client/composables/runtime/useUiFeedback.js +4 -2
  18. package/src/client/composables/support/resourceLoadStateHelpers.js +33 -1
  19. package/src/client/composables/useAccountSettingsRuntime.js +10 -1
  20. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  21. package/src/client/composables/useCrudListBulkActions.js +147 -0
  22. package/src/client/composables/useCrudListScreen.js +107 -0
  23. package/src/client/composables/useCrudViewScreen.js +67 -0
  24. package/src/client/composables/usePagedCollection.js +6 -1
  25. package/src/client/filters.js +15 -0
  26. package/src/client/index.js +5 -0
  27. package/src/shared/toolsOutletContracts.js +0 -4
  28. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +34 -8
  29. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +34 -8
  30. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +34 -8
  31. package/test/crudListBulkActionSurface.test.js +27 -0
  32. package/test/crudListFilterSurface.test.js +45 -0
  33. package/test/crudScreenComponents.test.js +62 -0
  34. package/test/errorIntentContract.test.js +31 -0
  35. package/test/exportsContract.test.js +11 -0
  36. package/test/resourceLoadStateHelpers.test.js +35 -1
  37. package/test/settingsPlacementContract.test.js +146 -14
  38. 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,
@@ -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 {
@@ -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
 
@@ -97,6 +97,7 @@ function useView({
97
97
  readMethod,
98
98
  readQuery: requestQueryRuntime.requestQuery,
99
99
  transport,
100
+ refreshOnPull: true,
100
101
  fallbackLoadError
101
102
  });
102
103
 
@@ -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,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 { hasResolvedQueryData } from "../support/resourceLoadStateHelpers.js";
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
- ...(asPlainObject(queryOptions))
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 = "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 };
@@ -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(() => {