@jskit-ai/users-web 0.1.32 → 0.1.34

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 (26) hide show
  1. package/package.descriptor.mjs +10 -6
  2. package/package.json +7 -6
  3. package/src/client/components/WorkspaceMembersClientElement.vue +16 -16
  4. package/src/client/components/WorkspacesClientElement.vue +2 -2
  5. package/src/client/composables/crudLookupFieldLabelSupport.js +107 -0
  6. package/src/client/composables/crudLookupFieldRuntime.js +238 -0
  7. package/src/client/composables/crudSchemaFormHelpers.js +35 -0
  8. package/src/client/composables/listSearchSupport.js +70 -0
  9. package/src/client/composables/resourceLoadStateHelpers.js +10 -0
  10. package/src/client/composables/routeTemplateHelpers.js +54 -1
  11. package/src/client/composables/useAccountSettingsRuntime.js +14 -14
  12. package/src/client/composables/useAddEdit.js +2 -1
  13. package/src/client/composables/useCrudSchemaForm.js +37 -11
  14. package/src/client/composables/useEndpointResource.js +6 -1
  15. package/src/client/composables/useList.js +164 -8
  16. package/src/client/composables/useRealtimeQueryInvalidation.js +33 -8
  17. package/src/client/composables/useView.js +4 -2
  18. package/src/client/composables/useViewCore.js +12 -2
  19. package/templates/src/components/account/settings/AccountSettingsClientElement.vue +3 -6
  20. package/templates/src/composables/useWorkspaceNotFoundState.js +8 -15
  21. package/test/crudLookupFieldRuntime.test.js +189 -0
  22. package/test/resourceLoadStateHelpers.test.js +39 -0
  23. package/test/routeTemplateHelpers.test.js +29 -0
  24. package/test/useCrudSchemaForm.test.js +39 -0
  25. package/test/useListSearchSupport.test.js +61 -0
  26. package/test/viewCoreLoading.test.js +44 -0
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-web",
4
- version: "0.1.32",
4
+ version: "0.1.34",
5
5
  kind: "runtime",
6
6
  description: "Users web module: workspace selector shell element plus workspace/profile/members UI elements.",
7
7
  dependsOn: [
@@ -73,6 +73,10 @@ export default Object.freeze({
73
73
  subpath: "./client/composables/useList",
74
74
  summary: "Exports list operation composable."
75
75
  },
76
+ {
77
+ subpath: "./client/composables/crudLookupFieldRuntime",
78
+ summary: "Exports CRUD lookup field runtime helpers for generated add/edit forms."
79
+ },
76
80
  {
77
81
  subpath: "./client/composables/useCommand",
78
82
  summary: "Exports command operation composable."
@@ -242,11 +246,11 @@ export default Object.freeze({
242
246
  "@uppy/dashboard": "^5.1.1",
243
247
  "@uppy/image-editor": "^4.2.0",
244
248
  "@uppy/xhr-upload": "^5.1.1",
245
- "@jskit-ai/http-runtime": "0.1.17",
246
- "@jskit-ai/realtime": "0.1.17",
247
- "@jskit-ai/kernel": "0.1.18",
248
- "@jskit-ai/shell-web": "0.1.17",
249
- "@jskit-ai/users-core": "0.1.27",
249
+ "@jskit-ai/http-runtime": "0.1.19",
250
+ "@jskit-ai/realtime": "0.1.19",
251
+ "@jskit-ai/kernel": "0.1.20",
252
+ "@jskit-ai/shell-web": "0.1.19",
253
+ "@jskit-ai/users-core": "0.1.29",
250
254
  "vuetify": "^4.0.0"
251
255
  },
252
256
  dev: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -10,6 +10,7 @@
10
10
  "./client/components/WorkspaceMembersClientElement": "./src/client/components/WorkspaceMembersClientElement.vue",
11
11
  "./client/composables/useAddEdit": "./src/client/composables/useAddEdit.js",
12
12
  "./client/composables/useCrudSchemaForm": "./src/client/composables/useCrudSchemaForm.js",
13
+ "./client/composables/crudLookupFieldRuntime": "./src/client/composables/crudLookupFieldRuntime.js",
13
14
  "./client/composables/useList": "./src/client/composables/useList.js",
14
15
  "./client/composables/useView": "./src/client/composables/useView.js",
15
16
  "./client/composables/usePagedCollection": "./src/client/composables/usePagedCollection.js",
@@ -21,11 +22,11 @@
21
22
  "dependencies": {
22
23
  "@tanstack/vue-query": "5.92.12",
23
24
  "@mdi/js": "^7.4.47",
24
- "@jskit-ai/users-core": "0.1.27",
25
- "@jskit-ai/realtime": "0.1.17",
26
- "@jskit-ai/http-runtime": "0.1.17",
27
- "@jskit-ai/kernel": "0.1.18",
28
- "@jskit-ai/shell-web": "0.1.17",
25
+ "@jskit-ai/users-core": "0.1.29",
26
+ "@jskit-ai/realtime": "0.1.19",
27
+ "@jskit-ai/http-runtime": "0.1.19",
28
+ "@jskit-ai/kernel": "0.1.20",
29
+ "@jskit-ai/shell-web": "0.1.19",
29
30
  "vuetify": "^4.0.0"
30
31
  }
31
32
  }
@@ -389,12 +389,12 @@ const status = computed(() => {
389
389
  isCreatingInvite: Boolean(inviteCreateCommand.isRunning.value),
390
390
  isRevokingInvite: Boolean(revokeInviteCommand.isRunning.value),
391
391
  isRemovingMember: Boolean(memberRemoveCommand.isRunning.value),
392
- hasLoadedWorkspaceSettings: !canInviteMembers.value || !workspaceSettingsView.isLoading.value,
393
- hasLoadedMembersList: !canViewMembers.value || !workspaceMembersList.isInitialLoading.value,
394
- hasLoadedInviteList: !canViewMembers.value || !workspaceInvitesList.isInitialLoading.value,
395
- isRefreshingWorkspaceSettings: canInviteMembers.value && Boolean(workspaceSettingsView.isRefetching.value),
396
- isRefreshingMembersList: canViewMembers.value && Boolean(workspaceMembersList.isRefetching.value),
397
- isRefreshingInviteList: canViewMembers.value && Boolean(workspaceInvitesList.isRefetching.value)
392
+ hasLoadedWorkspaceSettings: !canInviteMembers.value || !workspaceSettingsView.isLoading,
393
+ hasLoadedMembersList: !canViewMembers.value || !workspaceMembersList.isInitialLoading,
394
+ hasLoadedInviteList: !canViewMembers.value || !workspaceInvitesList.isInitialLoading,
395
+ isRefreshingWorkspaceSettings: canInviteMembers.value && Boolean(workspaceSettingsView.isRefetching),
396
+ isRefreshingMembersList: canViewMembers.value && Boolean(workspaceMembersList.isRefetching),
397
+ isRefreshingInviteList: canViewMembers.value && Boolean(workspaceInvitesList.isRefetching)
398
398
  };
399
399
  });
400
400
 
@@ -440,7 +440,7 @@ watch(
440
440
  );
441
441
 
442
442
  watch(
443
- () => workspaceSettingsView.record.value,
443
+ () => workspaceSettingsView.record,
444
444
  (payload) => {
445
445
  if (!payload) {
446
446
  return;
@@ -451,7 +451,7 @@ watch(
451
451
  );
452
452
 
453
453
  watch(
454
- () => workspaceSettingsView.loadError.value,
454
+ () => workspaceSettingsView.loadError,
455
455
  (nextLoadError) => {
456
456
  if (!nextLoadError) {
457
457
  return;
@@ -462,7 +462,7 @@ watch(
462
462
  );
463
463
 
464
464
  watch(
465
- () => workspaceRolesView.record.value,
465
+ () => workspaceRolesView.record,
466
466
  (payload) => {
467
467
  if (!payload) {
468
468
  return;
@@ -473,7 +473,7 @@ watch(
473
473
  );
474
474
 
475
475
  watch(
476
- () => workspaceRolesView.loadError.value,
476
+ () => workspaceRolesView.loadError,
477
477
  (nextLoadError) => {
478
478
  if (!nextLoadError) {
479
479
  return;
@@ -483,7 +483,7 @@ watch(
483
483
  );
484
484
 
485
485
  watch(
486
- () => workspaceMembersList.items.value,
486
+ () => workspaceMembersList.items,
487
487
  (nextMembers) => {
488
488
  collections.members = Array.isArray(nextMembers) ? [...nextMembers] : [];
489
489
  },
@@ -491,7 +491,7 @@ watch(
491
491
  );
492
492
 
493
493
  watch(
494
- () => workspaceMembersList.pages.value,
494
+ () => workspaceMembersList.pages,
495
495
  (pages) => {
496
496
  const payload = latestPage(pages);
497
497
  if (!payload) {
@@ -503,7 +503,7 @@ watch(
503
503
  );
504
504
 
505
505
  watch(
506
- () => workspaceMembersList.loadError.value,
506
+ () => workspaceMembersList.loadError,
507
507
  (nextLoadError) => {
508
508
  if (!nextLoadError) {
509
509
  membersFeedback.clear();
@@ -514,7 +514,7 @@ watch(
514
514
  );
515
515
 
516
516
  watch(
517
- () => workspaceInvitesList.items.value,
517
+ () => workspaceInvitesList.items,
518
518
  (nextInvites) => {
519
519
  collections.invites = Array.isArray(nextInvites) ? [...nextInvites] : [];
520
520
  },
@@ -522,7 +522,7 @@ watch(
522
522
  );
523
523
 
524
524
  watch(
525
- () => workspaceInvitesList.pages.value,
525
+ () => workspaceInvitesList.pages,
526
526
  (pages) => {
527
527
  const payload = latestPage(pages);
528
528
  if (!payload) {
@@ -534,7 +534,7 @@ watch(
534
534
  );
535
535
 
536
536
  watch(
537
- () => workspaceInvitesList.loadError.value,
537
+ () => workspaceInvitesList.loadError,
538
538
  (nextLoadError) => {
539
539
  if (!nextLoadError) {
540
540
  teamFeedback.clear();
@@ -125,8 +125,8 @@ const pendingInvites = computed(() => {
125
125
  });
126
126
  const workspaceInvitesEnabled = computed(() => bootstrapModel.workspaceInvitesEnabled === true);
127
127
 
128
- const isBootstrapping = computed(() => Boolean(bootstrapView.isLoading.value));
129
- const isRefreshingBootstrap = computed(() => Boolean(bootstrapView.isRefetching.value));
128
+ const isBootstrapping = computed(() => Boolean(bootstrapView.isLoading));
129
+ const isRefreshingBootstrap = computed(() => Boolean(bootstrapView.isRefetching));
130
130
  const canCreateWorkspace = computed(() => bootstrapModel.workspaceAllowSelfCreate === true);
131
131
  const isCreatingWorkspace = computed(() => Boolean(createWorkspaceCommand.isRunning.value));
132
132
 
@@ -0,0 +1,107 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeCrudLookupContainerKey } from "@jskit-ai/kernel/shared/support/crudLookup";
3
+ import { asPlainObject } from "./scopeHelpers.js";
4
+
5
+ const LOOKUP_LABEL_COMPOSITION_CANDIDATES = Object.freeze([
6
+ Object.freeze(["name", "surname"]),
7
+ Object.freeze(["firstName", "surname"]),
8
+ Object.freeze(["name"]),
9
+ Object.freeze(["firstName"])
10
+ ]);
11
+
12
+ function hasDisplayValue(value) {
13
+ if (value == null) {
14
+ return false;
15
+ }
16
+ if (typeof value === "string") {
17
+ return normalizeText(value).length > 0;
18
+ }
19
+
20
+ return true;
21
+ }
22
+
23
+ function resolveLookupItemLabel(item = {}, labelKey = "") {
24
+ const source = asPlainObject(item);
25
+ for (const candidate of LOOKUP_LABEL_COMPOSITION_CANDIDATES) {
26
+ const parts = [];
27
+ for (const key of candidate) {
28
+ const part = normalizeText(source[key]);
29
+ if (!part) {
30
+ parts.length = 0;
31
+ break;
32
+ }
33
+ parts.push(part);
34
+ }
35
+ if (parts.length === candidate.length) {
36
+ return parts.join(" ");
37
+ }
38
+ }
39
+
40
+ const normalizedLabelKey = normalizeText(labelKey);
41
+ if (!normalizedLabelKey) {
42
+ return "";
43
+ }
44
+
45
+ return normalizeText(source[normalizedLabelKey]);
46
+ }
47
+
48
+ function resolveLookupFieldDescriptor(field = {}, relationKind = "", valueKey = "", labelKey = "") {
49
+ if (typeof field === "string") {
50
+ return {
51
+ key: normalizeText(field),
52
+ relation: {
53
+ kind: normalizeText(relationKind).toLowerCase(),
54
+ valueKey: normalizeText(valueKey) || "id",
55
+ labelKey: normalizeText(labelKey),
56
+ containerKey: ""
57
+ }
58
+ };
59
+ }
60
+
61
+ const sourceField = asPlainObject(field);
62
+ const relation = asPlainObject(sourceField.relation);
63
+ return {
64
+ key: normalizeText(sourceField.key),
65
+ relation: {
66
+ kind: normalizeText(relation.kind).toLowerCase(),
67
+ valueKey: normalizeText(relation.valueKey) || "id",
68
+ labelKey: normalizeText(relation.labelKey),
69
+ containerKey: normalizeText(relation.containerKey)
70
+ }
71
+ };
72
+ }
73
+
74
+ function resolveLookupFieldDisplayValue(record = {}, field = {}, relationKind = "", valueKey = "", labelKey = "") {
75
+ const sourceRecord = asPlainObject(record);
76
+ const descriptor = resolveLookupFieldDescriptor(field, relationKind, valueKey, labelKey);
77
+ const key = descriptor.key;
78
+ if (!key) {
79
+ return "";
80
+ }
81
+
82
+ if (descriptor.relation.kind !== "lookup") {
83
+ return sourceRecord[key];
84
+ }
85
+
86
+ const lookupContainerKey = normalizeCrudLookupContainerKey(descriptor.relation.containerKey, {
87
+ context: `lookup relation "${key}" containerKey`
88
+ });
89
+ const sourceLookups = asPlainObject(sourceRecord[lookupContainerKey]);
90
+ const lookupRecord = asPlainObject(sourceLookups[key]);
91
+ const lookupLabel = resolveLookupItemLabel(lookupRecord, descriptor.relation.labelKey);
92
+ if (lookupLabel) {
93
+ return lookupLabel;
94
+ }
95
+
96
+ const lookupValue = lookupRecord[descriptor.relation.valueKey];
97
+ if (hasDisplayValue(lookupValue)) {
98
+ return lookupValue;
99
+ }
100
+
101
+ return sourceRecord[key];
102
+ }
103
+
104
+ export {
105
+ resolveLookupItemLabel,
106
+ resolveLookupFieldDisplayValue
107
+ };
@@ -0,0 +1,238 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import {
3
+ normalizeCrudLookupApiPath,
4
+ normalizeCrudLookupNamespace,
5
+ normalizeCrudLookupContainerKey,
6
+ resolveCrudLookupApiPathFromNamespace
7
+ } from "@jskit-ai/kernel/shared/support/crudLookup";
8
+ import { useList } from "./useList.js";
9
+ import {
10
+ resolveLookupItemLabel,
11
+ resolveLookupFieldDisplayValue
12
+ } from "./crudLookupFieldLabelSupport.js";
13
+ import { asPlainObject } from "./scopeHelpers.js";
14
+
15
+ function normalizeQueryKeyPrefix(value) {
16
+ const source = Array.isArray(value) ? value : [];
17
+ return source
18
+ .map((entry) => normalizeText(entry))
19
+ .filter(Boolean);
20
+ }
21
+
22
+ function isSameLookupValue(left, right) {
23
+ if (left === right) {
24
+ return true;
25
+ }
26
+
27
+ return String(left ?? "") === String(right ?? "");
28
+ }
29
+
30
+ function normalizeLookupValue(value) {
31
+ if (value == null) {
32
+ return "";
33
+ }
34
+
35
+ return String(value);
36
+ }
37
+
38
+ function createSelectedLookupItem(selectedValue, selectedRecord = {}, entry = {}) {
39
+ if (selectedValue == null || selectedValue === "") {
40
+ return null;
41
+ }
42
+
43
+ const sourceRecord = asPlainObject(selectedRecord);
44
+ const sourceLookups = asPlainObject(sourceRecord[entry.lookupContainerKey]);
45
+ const hydratedLookup = asPlainObject(sourceLookups[entry.fieldKey]);
46
+ const hydratedValue = hydratedLookup[entry.valueKey];
47
+ const value = normalizeLookupValue(
48
+ hydratedValue == null || hydratedValue === "" ? selectedValue : hydratedValue
49
+ );
50
+ if (!value) {
51
+ return null;
52
+ }
53
+
54
+ const displayValue = resolveLookupFieldDisplayValue(
55
+ {
56
+ [entry.fieldKey]: value,
57
+ [entry.lookupContainerKey]: {
58
+ [entry.fieldKey]: hydratedLookup
59
+ }
60
+ },
61
+ {
62
+ key: entry.fieldKey,
63
+ relation: entry.relation
64
+ }
65
+ );
66
+ const label = displayValue == null || displayValue === "" ? value : displayValue;
67
+ return {
68
+ value,
69
+ label: String(label ?? "")
70
+ };
71
+ }
72
+
73
+ function createCrudLookupFieldRuntime({
74
+ formFields = [],
75
+ adapter = null,
76
+ recordIdParam = "recordId",
77
+ queryKeyPrefix = [],
78
+ placementSourcePrefix = "",
79
+ lookupContainerKey = "lookups"
80
+ } = {}) {
81
+ const runtimes = new Map();
82
+ const normalizedRecordIdParam = normalizeText(recordIdParam) || "recordId";
83
+ const normalizedPlacementSourcePrefix = normalizeText(placementSourcePrefix);
84
+ const normalizedQueryKeyPrefix = normalizeQueryKeyPrefix(queryKeyPrefix);
85
+ const defaultLookupContainerKey = normalizeCrudLookupContainerKey(lookupContainerKey, {
86
+ context: "createCrudLookupFieldRuntime lookupContainerKey"
87
+ });
88
+
89
+ for (const field of Array.isArray(formFields) ? formFields : []) {
90
+ const key = normalizeText(field?.key);
91
+ const rawRelation = field?.relation;
92
+ if (!key || !rawRelation || typeof rawRelation !== "object" || Array.isArray(rawRelation) || runtimes.has(key)) {
93
+ continue;
94
+ }
95
+
96
+ const relationKind = normalizeText(rawRelation.kind).toLowerCase();
97
+ const namespace =
98
+ normalizeCrudLookupNamespace(rawRelation.namespace) ||
99
+ normalizeCrudLookupNamespace(rawRelation.apiPath);
100
+ if (relationKind !== "lookup" || !namespace) {
101
+ continue;
102
+ }
103
+ const explicitApiPath = normalizeCrudLookupApiPath(rawRelation.apiPath);
104
+ const apiPath = explicitApiPath || resolveCrudLookupApiPathFromNamespace(namespace);
105
+ const valueKey = normalizeText(rawRelation.valueKey);
106
+ const labelKey = normalizeText(rawRelation.labelKey);
107
+ const relationLookupContainerKey = normalizeCrudLookupContainerKey(rawRelation.containerKey, {
108
+ defaultValue: defaultLookupContainerKey,
109
+ context: `createCrudLookupFieldRuntime formFields["${key}"].relation.containerKey`
110
+ });
111
+ if (!valueKey) {
112
+ continue;
113
+ }
114
+
115
+ const runtime = useList({
116
+ adapter: adapter || undefined,
117
+ apiSuffix: apiPath,
118
+ queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
119
+ ...normalizedQueryKeyPrefix,
120
+ key,
121
+ String(surfaceId || ""),
122
+ String(workspaceSlug || "")
123
+ ],
124
+ search: {
125
+ enabled: true,
126
+ mode: "query",
127
+ queryParam: "q"
128
+ },
129
+ placementSource: normalizedPlacementSourcePrefix
130
+ ? `${normalizedPlacementSourcePrefix}.${key}`
131
+ : `crud.lookup.${key}`,
132
+ fallbackLoadError: `Unable to load lookup options (${apiPath}).`,
133
+ recordIdParam: normalizedRecordIdParam,
134
+ recordIdSelector: (item = {}) => item[valueKey],
135
+ viewUrlTemplate: "",
136
+ editUrlTemplate: ""
137
+ });
138
+
139
+ runtimes.set(key, Object.freeze({
140
+ runtime,
141
+ fieldKey: key,
142
+ lookupContainerKey: relationLookupContainerKey,
143
+ valueKey,
144
+ labelKey,
145
+ relation: Object.freeze({
146
+ kind: "lookup",
147
+ namespace,
148
+ ...(explicitApiPath ? { apiPath: explicitApiPath } : {}),
149
+ containerKey: relationLookupContainerKey,
150
+ valueKey,
151
+ ...(labelKey ? { labelKey } : {})
152
+ })
153
+ }));
154
+ }
155
+
156
+ function resolveLookupItems(fieldKey = "", options = {}) {
157
+ const key = normalizeText(fieldKey);
158
+ const entry = runtimes.get(key);
159
+ if (!entry) {
160
+ return [];
161
+ }
162
+
163
+ const items = (Array.isArray(entry.runtime.items) ? entry.runtime.items : []).map((item = {}) => {
164
+ const value = normalizeLookupValue(item?.[entry.valueKey]);
165
+ const resolvedLabel = resolveLookupItemLabel(item, entry.labelKey);
166
+ const label = resolvedLabel || value;
167
+ return {
168
+ value,
169
+ label: String(label ?? "")
170
+ };
171
+ });
172
+
173
+ const selectedItem = createSelectedLookupItem(
174
+ options?.selectedValue,
175
+ options?.selectedRecord,
176
+ entry
177
+ );
178
+ if (!selectedItem) {
179
+ return items;
180
+ }
181
+
182
+ if (items.some((item) => item?.value === selectedItem.value)) {
183
+ return items;
184
+ }
185
+
186
+ const matchingItem = items.find((item) => isSameLookupValue(item?.value, selectedItem.value));
187
+ if (matchingItem) {
188
+ return [
189
+ {
190
+ ...selectedItem,
191
+ label: String(matchingItem.label ?? selectedItem.label ?? "")
192
+ },
193
+ ...items
194
+ ];
195
+ }
196
+
197
+ return [selectedItem, ...items];
198
+ }
199
+
200
+ function resolveLookupLoading(fieldKey = "") {
201
+ const key = normalizeText(fieldKey);
202
+ const entry = runtimes.get(key);
203
+ if (!entry) {
204
+ return false;
205
+ }
206
+
207
+ return Boolean(entry.runtime.isInitialLoading || entry.runtime.isFetching || entry.runtime.isRefetching);
208
+ }
209
+
210
+ function resolveLookupSearch(fieldKey = "") {
211
+ const key = normalizeText(fieldKey);
212
+ const entry = runtimes.get(key);
213
+ if (!entry) {
214
+ return "";
215
+ }
216
+
217
+ return String(entry.runtime.searchQuery || "");
218
+ }
219
+
220
+ function setLookupSearch(fieldKey = "", searchValue = "") {
221
+ const key = normalizeText(fieldKey);
222
+ const entry = runtimes.get(key);
223
+ if (!entry) {
224
+ return;
225
+ }
226
+
227
+ entry.runtime.searchQuery = String(searchValue || "");
228
+ }
229
+
230
+ return Object.freeze({
231
+ resolveLookupItems,
232
+ resolveLookupLoading,
233
+ resolveLookupSearch,
234
+ setLookupSearch
235
+ });
236
+ }
237
+
238
+ export { createCrudLookupFieldRuntime };
@@ -1,5 +1,6 @@
1
1
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
2
2
  import { asPlainObject } from "./scopeHelpers.js";
3
+ import { toRouteParamValue } from "./routeTemplateHelpers.js";
3
4
 
4
5
  function normalizeCrudFormFields(fields = []) {
5
6
  const normalizedFields = [];
@@ -109,6 +110,38 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
109
110
  }
110
111
  }
111
112
 
113
+ function resolveCrudRouteBoundFieldValues(fields = [], routeParams = {}) {
114
+ const sourceRouteParams = asPlainObject(routeParams);
115
+ const values = {};
116
+
117
+ for (const field of normalizeCrudFormFields(fields)) {
118
+ const routeParamKey = String(field?.routeParamKey || "").trim();
119
+ if (!routeParamKey) {
120
+ continue;
121
+ }
122
+ if (!Object.prototype.hasOwnProperty.call(sourceRouteParams, routeParamKey)) {
123
+ continue;
124
+ }
125
+
126
+ const routeValue = toRouteParamValue(sourceRouteParams[routeParamKey]);
127
+ if (!routeValue) {
128
+ continue;
129
+ }
130
+ values[field.key] = routeValue;
131
+ }
132
+
133
+ return values;
134
+ }
135
+
136
+ function applyCrudRouteBoundFieldValues(fields = [], target = {}, routeParams = {}) {
137
+ const resolved = resolveCrudRouteBoundFieldValues(fields, routeParams);
138
+ const destination = asPlainObject(target);
139
+ for (const [key, value] of Object.entries(resolved)) {
140
+ destination[key] = value;
141
+ }
142
+ return resolved;
143
+ }
144
+
112
145
  function resolveCrudFieldErrors(fieldErrors = {}, fieldKey = "") {
113
146
  const key = String(fieldKey || "").trim();
114
147
  if (!key) {
@@ -147,6 +180,8 @@ export {
147
180
  createCrudFormModel,
148
181
  buildCrudFormPayload,
149
182
  applyCrudPayloadToForm,
183
+ resolveCrudRouteBoundFieldValues,
184
+ applyCrudRouteBoundFieldValues,
150
185
  resolveCrudFieldErrors,
151
186
  parseCrudResourceOperationInput
152
187
  };
@@ -0,0 +1,70 @@
1
+ import {
2
+ normalizeInteger,
3
+ normalizeText,
4
+ normalizeUniqueTextList
5
+ } from "@jskit-ai/kernel/shared/support/normalize";
6
+ import { asPlainObject } from "./scopeHelpers.js";
7
+
8
+ const DEFAULT_LIST_SEARCH_DEBOUNCE_MS = 250;
9
+ const DEFAULT_LIST_SEARCH_MIN_LENGTH = 1;
10
+ const LIST_SEARCH_MODE_LOCAL = "local";
11
+ const LIST_SEARCH_MODE_QUERY = "query";
12
+
13
+ function normalizeListSearchConfig(value = {}) {
14
+ const source = asPlainObject(value);
15
+ const modeRaw = normalizeText(source.mode).toLowerCase();
16
+ const mode = modeRaw === LIST_SEARCH_MODE_LOCAL
17
+ ? LIST_SEARCH_MODE_LOCAL
18
+ : LIST_SEARCH_MODE_QUERY;
19
+
20
+ return Object.freeze({
21
+ enabled: source.enabled === true,
22
+ mode,
23
+ queryParam: normalizeText(source.queryParam) || "q",
24
+ label: normalizeText(source.label) || "Search",
25
+ placeholder: normalizeText(source.placeholder),
26
+ initialQuery: normalizeText(source.initialQuery),
27
+ debounceMs: normalizeInteger(source.debounceMs, {
28
+ fallback: DEFAULT_LIST_SEARCH_DEBOUNCE_MS,
29
+ min: 0
30
+ }),
31
+ minLength: normalizeInteger(source.minLength, {
32
+ fallback: DEFAULT_LIST_SEARCH_MIN_LENGTH,
33
+ min: DEFAULT_LIST_SEARCH_MIN_LENGTH
34
+ }),
35
+ fields: Object.freeze(normalizeUniqueTextList(source.fields))
36
+ });
37
+ }
38
+
39
+ function normalizeSearchableValue(value) {
40
+ if (value == null) {
41
+ return "";
42
+ }
43
+ if (typeof value === "string") {
44
+ return normalizeText(value).toLowerCase();
45
+ }
46
+ if (typeof value === "number" || typeof value === "boolean") {
47
+ return String(value).toLowerCase();
48
+ }
49
+ return "";
50
+ }
51
+
52
+ function matchesLocalSearch(item = {}, query = "", searchFields = []) {
53
+ const source = asPlainObject(item);
54
+ const normalizedQuery = normalizeText(query).toLowerCase();
55
+ if (!normalizedQuery) {
56
+ return true;
57
+ }
58
+
59
+ const fields = Array.isArray(searchFields) ? searchFields : [];
60
+ if (fields.length > 0) {
61
+ return fields.some((field) => normalizeSearchableValue(source[field]).includes(normalizedQuery));
62
+ }
63
+
64
+ return Object.values(source).some((value) => normalizeSearchableValue(value).includes(normalizedQuery));
65
+ }
66
+
67
+ export {
68
+ normalizeListSearchConfig,
69
+ matchesLocalSearch
70
+ };
@@ -0,0 +1,10 @@
1
+ import { unref } from "vue";
2
+
3
+ function hasResolvedQueryData({ query = null, data = null } = {}) {
4
+ const querySucceeded = Boolean(unref(query?.isSuccess));
5
+ const hasDataPayload = unref(data) != null;
6
+
7
+ return querySucceeded || hasDataPayload;
8
+ }
9
+
10
+ export { hasResolvedQueryData };