@jskit-ai/users-web 0.1.36 → 0.1.38

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 (79) hide show
  1. package/package.descriptor.mjs +8 -22
  2. package/package.json +16 -11
  3. package/src/client/components/MembersAdminClientElement.vue +5 -5
  4. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
  5. package/src/client/components/WorkspaceMembersClientElement.vue +19 -19
  6. package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
  7. package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
  8. package/src/client/components/WorkspacesClientElement.vue +4 -4
  9. package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
  10. package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
  11. package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -4
  12. package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +2 -2
  13. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  14. package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +37 -5
  15. package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +11 -4
  16. package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +178 -5
  17. package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
  18. package/src/client/composables/internal/useOperationScope.js +1 -1
  19. package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +18 -8
  20. package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
  21. package/src/client/composables/records/useCrudList.js +83 -0
  22. package/src/client/composables/records/useCrudView.js +35 -0
  23. package/src/client/composables/records/useList.js +482 -0
  24. package/src/client/composables/{useView.js → records/useView.js} +7 -7
  25. package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +13 -4
  26. package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +20 -8
  27. package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
  28. package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
  29. package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
  30. package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
  31. package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +13 -4
  32. package/src/client/composables/support/listQueryParamSupport.js +459 -0
  33. package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +122 -0
  34. package/src/client/composables/useAccess.js +2 -2
  35. package/src/client/composables/useAccountSettingsRuntime.js +6 -6
  36. package/src/client/composables/useBootstrapQuery.js +1 -1
  37. package/src/client/composables/useCommand.js +5 -5
  38. package/src/client/composables/useCrudListParentTitle.js +131 -0
  39. package/src/client/composables/usePagedCollection.js +58 -7
  40. package/src/client/composables/useScopeRuntime.js +1 -1
  41. package/src/client/lib/bootstrap.js +1 -1
  42. package/src/client/lib/menuIcons.js +27 -6
  43. package/src/client/support/menuLinkTarget.js +93 -0
  44. package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
  45. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
  46. package/test/addEditUiRuntime.test.js +19 -1
  47. package/test/crudBindingSupport.test.js +110 -0
  48. package/test/crudLookupFieldRuntime.test.js +52 -2
  49. package/test/errorMessageHelpers.test.js +1 -1
  50. package/test/exportsContract.test.js +10 -1
  51. package/test/listQueryParamSupport.test.js +190 -0
  52. package/test/listUiRuntime.test.js +22 -1
  53. package/test/menuIcons.test.js +2 -0
  54. package/test/menuLinkTarget.test.js +116 -0
  55. package/test/permissions.test.js +2 -2
  56. package/test/refValueHelpers.test.js +1 -1
  57. package/test/resourceLoadStateHelpers.test.js +1 -1
  58. package/test/routeTemplateHelpers.test.js +57 -1
  59. package/test/scopeHelpers.test.js +1 -1
  60. package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
  61. package/test/useCrudListParentTitle.test.js +143 -0
  62. package/test/useListSearchSupport.test.js +1 -1
  63. package/test/usePagedCollection.test.js +53 -0
  64. package/test/viewCoreLoading.test.js +1 -1
  65. package/test/viewUiRuntime.test.js +36 -1
  66. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -241
  67. package/src/client/composables/useList.js +0 -268
  68. /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
  69. /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
  70. /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
  71. /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
  72. /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
  73. /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
  74. /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
  75. /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
  76. /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
  77. /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
  78. /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
  79. /package/src/client/composables/{scopeHelpers.js → support/scopeHelpers.js} +0 -0
@@ -5,12 +5,13 @@ import {
5
5
  normalizeCrudLookupContainerKey,
6
6
  resolveCrudLookupApiPathFromNamespace
7
7
  } from "@jskit-ai/kernel/shared/support/crudLookup";
8
- import { useList } from "./useList.js";
8
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
9
+ import { useList } from "../records/useList.js";
9
10
  import {
10
11
  resolveLookupItemLabel,
11
12
  resolveLookupFieldDisplayValue
12
13
  } from "./crudLookupFieldLabelSupport.js";
13
- import { asPlainObject } from "./scopeHelpers.js";
14
+ import { asPlainObject } from "../support/scopeHelpers.js";
14
15
 
15
16
  function normalizeQueryKeyPrefix(value) {
16
17
  const source = Array.isArray(value) ? value : [];
@@ -66,7 +67,8 @@ function createSelectedLookupItem(selectedValue, selectedRecord = {}, entry = {}
66
67
  const label = displayValue == null || displayValue === "" ? value : displayValue;
67
68
  return {
68
69
  value,
69
- label: String(label ?? "")
70
+ label: String(label ?? ""),
71
+ record: hydratedLookup
70
72
  };
71
73
  }
72
74
 
@@ -108,12 +110,14 @@ function createCrudLookupFieldRuntime({
108
110
  defaultValue: defaultLookupContainerKey,
109
111
  context: `createCrudLookupFieldRuntime formFields["${key}"].relation.containerKey`
110
112
  });
113
+ const relationSurfaceId = normalizeSurfaceId(rawRelation.surfaceId);
111
114
  if (!valueKey) {
112
115
  continue;
113
116
  }
114
117
 
115
118
  const runtime = useList({
116
119
  adapter: adapter || undefined,
120
+ ...(relationSurfaceId ? { surfaceId: relationSurfaceId } : {}),
117
121
  apiSuffix: apiPath,
118
122
  queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
119
123
  ...normalizedQueryKeyPrefix,
@@ -146,6 +150,7 @@ function createCrudLookupFieldRuntime({
146
150
  kind: "lookup",
147
151
  namespace,
148
152
  ...(explicitApiPath ? { apiPath: explicitApiPath } : {}),
153
+ ...(relationSurfaceId ? { surfaceId: relationSurfaceId } : {}),
149
154
  containerKey: relationLookupContainerKey,
150
155
  valueKey,
151
156
  ...(labelKey ? { labelKey } : {})
@@ -161,12 +166,14 @@ function createCrudLookupFieldRuntime({
161
166
  }
162
167
 
163
168
  const items = (Array.isArray(entry.runtime.items) ? entry.runtime.items : []).map((item = {}) => {
169
+ const sourceRecord = asPlainObject(item);
164
170
  const value = normalizeLookupValue(item?.[entry.valueKey]);
165
171
  const resolvedLabel = resolveLookupItemLabel(item, entry.labelKey);
166
172
  const label = resolvedLabel || value;
167
173
  return {
168
174
  value,
169
- label: String(label ?? "")
175
+ label: String(label ?? ""),
176
+ record: sourceRecord
170
177
  };
171
178
  });
172
179
 
@@ -1,6 +1,24 @@
1
1
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
2
- import { asPlainObject } from "./scopeHelpers.js";
3
- import { toRouteParamValue } from "./routeTemplateHelpers.js";
2
+ import { asPlainObject } from "../support/scopeHelpers.js";
3
+ import { toRouteParamValue } from "../support/routeTemplateHelpers.js";
4
+
5
+ const EMPTY_FIELD_ERROR_LIST = Object.freeze([]);
6
+ const fieldErrorListCache = new Map();
7
+
8
+ function resolveStableFieldErrorList(fieldKey, message) {
9
+ if (!message) {
10
+ return EMPTY_FIELD_ERROR_LIST;
11
+ }
12
+
13
+ const cacheKey = `${fieldKey}::${message}`;
14
+ if (fieldErrorListCache.has(cacheKey)) {
15
+ return fieldErrorListCache.get(cacheKey);
16
+ }
17
+
18
+ const nextValue = Object.freeze([message]);
19
+ fieldErrorListCache.set(cacheKey, nextValue);
20
+ return nextValue;
21
+ }
4
22
 
5
23
  function normalizeCrudFormFields(fields = []) {
6
24
  const normalizedFields = [];
@@ -26,6 +44,84 @@ function resolveFormFieldType(field = {}) {
26
44
  return String(field.type || "").trim().toLowerCase();
27
45
  }
28
46
 
47
+ function resolveFormFieldFormat(field = {}) {
48
+ return String(field.format || "").trim().toLowerCase();
49
+ }
50
+
51
+ function padDateTimePart(value) {
52
+ return String(value).padStart(2, "0");
53
+ }
54
+
55
+ function normalizeTimeWhitespace(value) {
56
+ return String(value ?? "").replaceAll(/\s+/gu, " ").trim();
57
+ }
58
+
59
+ function toTimeInputValue(value) {
60
+ const normalized = normalizeTimeWhitespace(value);
61
+ if (!normalized) {
62
+ return "";
63
+ }
64
+
65
+ const twentyFourHourMatch = normalized.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/u);
66
+ if (twentyFourHourMatch) {
67
+ const hours = Number(twentyFourHourMatch[1]);
68
+ const minutes = Number(twentyFourHourMatch[2]);
69
+ if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
70
+ return `${padDateTimePart(hours)}:${padDateTimePart(minutes)}`;
71
+ }
72
+ return normalized;
73
+ }
74
+
75
+ const meridiemMatch = normalized.match(/^(\d{1,2}):(\d{2})\s*([ap]\.?m\.?)$/iu);
76
+ if (!meridiemMatch) {
77
+ return normalized;
78
+ }
79
+
80
+ const rawHours = Number(meridiemMatch[1]);
81
+ const minutes = Number(meridiemMatch[2]);
82
+ if (rawHours < 1 || rawHours > 12 || minutes < 0 || minutes > 59) {
83
+ return normalized;
84
+ }
85
+
86
+ let hours = rawHours % 12;
87
+ if (String(meridiemMatch[3] || "").toLowerCase().startsWith("p")) {
88
+ hours += 12;
89
+ }
90
+
91
+ return `${padDateTimePart(hours)}:${padDateTimePart(minutes)}`;
92
+ }
93
+
94
+ function toDateTimeLocalInputValue(value) {
95
+ if (value == null || value === "") {
96
+ return "";
97
+ }
98
+
99
+ const date = value instanceof Date ? value : new Date(value);
100
+ if (Number.isNaN(date.getTime())) {
101
+ return String(value);
102
+ }
103
+
104
+ return [
105
+ date.getFullYear(),
106
+ padDateTimePart(date.getMonth() + 1),
107
+ padDateTimePart(date.getDate())
108
+ ].join("-") + `T${padDateTimePart(date.getHours())}:${padDateTimePart(date.getMinutes())}`;
109
+ }
110
+
111
+ function toIsoUtcDateTimeValue(value) {
112
+ const normalized = String(value ?? "").trim();
113
+ if (!normalized) {
114
+ return "";
115
+ }
116
+
117
+ const date = new Date(normalized);
118
+ if (Number.isNaN(date.getTime())) {
119
+ return normalized;
120
+ }
121
+
122
+ return date.toISOString();
123
+ }
124
+
29
125
  function resolveFormFieldInitialValue(field = {}) {
30
126
  if (Object.prototype.hasOwnProperty.call(field, "initialValue")) {
31
127
  return field.initialValue;
@@ -42,6 +138,23 @@ function resolveFormFieldInitialValue(field = {}) {
42
138
  return "";
43
139
  }
44
140
 
141
+ function shouldSerializeClearedFieldAsNull(field = {}) {
142
+ if (field?.nullable !== true) {
143
+ return false;
144
+ }
145
+
146
+ const fieldType = resolveFormFieldType(field);
147
+ const fieldFormat = resolveFormFieldFormat(field);
148
+
149
+ return (
150
+ fieldType === "integer" ||
151
+ fieldType === "number" ||
152
+ fieldFormat === "date" ||
153
+ fieldFormat === "date-time" ||
154
+ fieldFormat === "time"
155
+ );
156
+ }
157
+
45
158
  function createCrudFormModel(fields = []) {
46
159
  const model = {};
47
160
  for (const field of normalizeCrudFormFields(fields)) {
@@ -58,6 +171,8 @@ function buildCrudFormPayload(fields = [], model = {}) {
58
171
  for (const field of normalizeCrudFormFields(fields)) {
59
172
  const fieldKey = field.key;
60
173
  const fieldType = resolveFormFieldType(field);
174
+ const fieldFormat = resolveFormFieldFormat(field);
175
+ const clearAsNull = shouldSerializeClearedFieldAsNull(field);
61
176
  const rawValue = sourceModel[fieldKey];
62
177
 
63
178
  if (fieldType === "boolean") {
@@ -68,6 +183,9 @@ function buildCrudFormPayload(fields = [], model = {}) {
68
183
  if (fieldType === "integer" || fieldType === "number") {
69
184
  const normalizedValue = String(rawValue ?? "").trim();
70
185
  if (!normalizedValue) {
186
+ if (clearAsNull) {
187
+ payload[fieldKey] = null;
188
+ }
71
189
  continue;
72
190
  }
73
191
 
@@ -79,6 +197,48 @@ function buildCrudFormPayload(fields = [], model = {}) {
79
197
  }
80
198
 
81
199
  if (rawValue == null) {
200
+ if (clearAsNull) {
201
+ payload[fieldKey] = null;
202
+ }
203
+ continue;
204
+ }
205
+
206
+ if (fieldFormat === "date") {
207
+ const normalizedValue = String(rawValue).trim();
208
+ if (!normalizedValue) {
209
+ if (clearAsNull) {
210
+ payload[fieldKey] = null;
211
+ }
212
+ continue;
213
+ }
214
+
215
+ payload[fieldKey] = normalizedValue;
216
+ continue;
217
+ }
218
+
219
+ if (fieldFormat === "date-time") {
220
+ const normalizedValue = toIsoUtcDateTimeValue(rawValue);
221
+ if (!normalizedValue) {
222
+ if (clearAsNull) {
223
+ payload[fieldKey] = null;
224
+ }
225
+ continue;
226
+ }
227
+
228
+ payload[fieldKey] = normalizedValue;
229
+ continue;
230
+ }
231
+
232
+ if (fieldFormat === "time") {
233
+ const normalizedValue = toTimeInputValue(rawValue);
234
+ if (!normalizedValue) {
235
+ if (clearAsNull) {
236
+ payload[fieldKey] = null;
237
+ }
238
+ continue;
239
+ }
240
+
241
+ payload[fieldKey] = normalizedValue;
82
242
  continue;
83
243
  }
84
244
 
@@ -94,6 +254,7 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
94
254
  for (const field of normalizeCrudFormFields(fields)) {
95
255
  const fieldKey = field.key;
96
256
  const fieldType = resolveFormFieldType(field);
257
+ const fieldFormat = resolveFormFieldFormat(field);
97
258
  const rawValue = sourcePayload[fieldKey];
98
259
 
99
260
  if (fieldType === "boolean") {
@@ -106,6 +267,16 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
106
267
  continue;
107
268
  }
108
269
 
270
+ if (fieldFormat === "date-time") {
271
+ targetModel[fieldKey] = toDateTimeLocalInputValue(rawValue);
272
+ continue;
273
+ }
274
+
275
+ if (fieldFormat === "time") {
276
+ targetModel[fieldKey] = toTimeInputValue(rawValue);
277
+ continue;
278
+ }
279
+
109
280
  targetModel[fieldKey] = rawValue == null ? "" : String(rawValue);
110
281
  }
111
282
  }
@@ -150,11 +321,12 @@ function resolveCrudFieldErrors(fieldErrors = {}, fieldKey = "") {
150
321
 
151
322
  const source = asPlainObject(fieldErrors);
152
323
  const message = String(source[key] || "").trim();
324
+
153
325
  if (!message) {
154
- return [];
326
+ return resolveStableFieldErrorList(key, "");
155
327
  }
156
328
 
157
- return [message];
329
+ return resolveStableFieldErrorList(key, message);
158
330
  }
159
331
 
160
332
  function parseCrudResourceOperationInput({
@@ -167,12 +339,13 @@ function parseCrudResourceOperationInput({
167
339
  const operations = asPlainObject(asPlainObject(resource).operations);
168
340
  const operation = asPlainObject(operations[normalizedOperationName]);
169
341
 
170
- return validateOperationSection({
342
+ const parsed = validateOperationSection({
171
343
  operation,
172
344
  section: "bodyValidator",
173
345
  value: rawPayload,
174
346
  context
175
347
  });
348
+ return parsed;
176
349
  }
177
350
 
178
351
  export {
@@ -0,0 +1,168 @@
1
+ import {
2
+ normalizeCrudLookupContainerKey,
3
+ resolveCrudLookupApiPathFromNamespace,
4
+ resolveCrudLookupFieldKeyFromRouteParam
5
+ } from "@jskit-ai/kernel/shared/support/crudLookup";
6
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
7
+ import { resolveLookupFieldDisplayValue, resolveRecordTitle } from "../crud/crudLookupFieldLabelSupport.js";
8
+ import { resolveRouteParamNamesInOrder, toRouteParamValue } from "../support/routeTemplateHelpers.js";
9
+
10
+ function singularizeLabel(value = "") {
11
+ const normalizedValue = normalizeText(value);
12
+ if (!normalizedValue) {
13
+ return "";
14
+ }
15
+ if (normalizedValue.endsWith("ies")) {
16
+ return `${normalizedValue.slice(0, -3)}y`;
17
+ }
18
+ if (normalizedValue.endsWith("s") && !normalizedValue.endsWith("ss")) {
19
+ return normalizedValue.slice(0, -1);
20
+ }
21
+ return normalizedValue;
22
+ }
23
+
24
+ function toTitleLabel(value = "") {
25
+ const normalizedValue = normalizeText(value)
26
+ .replace(/Id$/u, "")
27
+ .replace(/[_-]+/gu, " ")
28
+ .replace(/([a-z0-9])([A-Z])/gu, "$1 $2")
29
+ .trim();
30
+ if (!normalizedValue) {
31
+ return "";
32
+ }
33
+
34
+ return normalizedValue
35
+ .split(/\s+/u)
36
+ .filter(Boolean)
37
+ .map((entry) => entry.charAt(0).toUpperCase() + entry.slice(1))
38
+ .join(" ");
39
+ }
40
+
41
+ function resolveEntityLabel(routeParamKey = "", relationNamespace = "") {
42
+ const routeLabel = singularizeLabel(toTitleLabel(routeParamKey));
43
+ if (routeLabel) {
44
+ return routeLabel;
45
+ }
46
+
47
+ const namespaceLabel = singularizeLabel(toTitleLabel(relationNamespace));
48
+ if (namespaceLabel) {
49
+ return namespaceLabel;
50
+ }
51
+
52
+ return "Record";
53
+ }
54
+
55
+ function resolveLookupFieldMeta(resource = {}, fieldKey = "") {
56
+ const normalizedFieldKey = normalizeText(fieldKey);
57
+ if (!normalizedFieldKey) {
58
+ return null;
59
+ }
60
+
61
+ const entries = Array.isArray(resource?.fieldMeta) ? resource.fieldMeta : [];
62
+ for (const entry of entries) {
63
+ if (normalizeText(entry?.key) !== normalizedFieldKey) {
64
+ continue;
65
+ }
66
+
67
+ const relation = entry?.relation;
68
+ if (!relation || normalizeText(relation.kind).toLowerCase() !== "lookup") {
69
+ return null;
70
+ }
71
+
72
+ return entry;
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ function resolveCrudListParentDescriptor({ resource = {}, route = null, recordIdParam = "recordId" } = {}) {
79
+ const orderedRouteParamNames = resolveRouteParamNamesInOrder(route);
80
+ if (orderedRouteParamNames.length < 1) {
81
+ return null;
82
+ }
83
+
84
+ const normalizedRecordIdParam = normalizeText(recordIdParam) || "recordId";
85
+ for (const routeParamKey of [...orderedRouteParamNames].reverse()) {
86
+ if (routeParamKey === "workspaceSlug" || routeParamKey === normalizedRecordIdParam) {
87
+ continue;
88
+ }
89
+
90
+ const fieldKey = resolveCrudLookupFieldKeyFromRouteParam(resource, routeParamKey);
91
+ if (!fieldKey) {
92
+ continue;
93
+ }
94
+
95
+ const fieldMeta = resolveLookupFieldMeta(resource, fieldKey);
96
+ if (!fieldMeta) {
97
+ continue;
98
+ }
99
+
100
+ const relation = fieldMeta.relation || {};
101
+ const relationNamespace = normalizeText(relation.namespace);
102
+ const containerKey = normalizeCrudLookupContainerKey(relation.containerKey, {
103
+ defaultValue: resource?.contract?.lookup?.containerKey || "lookups"
104
+ });
105
+
106
+ return Object.freeze({
107
+ fieldKey,
108
+ routeParamKey,
109
+ relationNamespace,
110
+ entityLabel: resolveEntityLabel(routeParamKey, relationNamespace),
111
+ labelKey: normalizeText(relation.labelKey),
112
+ fieldDescriptor: Object.freeze({
113
+ key: fieldKey,
114
+ relation: Object.freeze({
115
+ kind: "lookup",
116
+ valueKey: normalizeText(relation.valueKey) || "id",
117
+ labelKey: normalizeText(relation.labelKey),
118
+ containerKey
119
+ })
120
+ }),
121
+ apiUrlTemplate: relationNamespace
122
+ ? `${resolveCrudLookupApiPathFromNamespace(relationNamespace)}/:${routeParamKey}`
123
+ : ""
124
+ });
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ function resolveCrudListParentTitleFromItems(items = [], descriptor = null) {
131
+ const sourceItems = Array.isArray(items) ? items : [];
132
+ if (!descriptor?.fieldDescriptor) {
133
+ return "";
134
+ }
135
+
136
+ for (const item of sourceItems) {
137
+ const resolvedTitle = normalizeText(resolveLookupFieldDisplayValue(item, descriptor.fieldDescriptor));
138
+ if (resolvedTitle) {
139
+ return resolvedTitle;
140
+ }
141
+ }
142
+
143
+ return "";
144
+ }
145
+
146
+ function resolveCrudListParentRecordTitle(record = {}, descriptor = null) {
147
+ const resolvedTitle = resolveRecordTitle(record, {
148
+ fallbackKey: normalizeText(descriptor?.labelKey),
149
+ defaultValue: ""
150
+ });
151
+ if (resolvedTitle && resolvedTitle !== "-") {
152
+ return resolvedTitle;
153
+ }
154
+
155
+ const entityLabel = normalizeText(descriptor?.entityLabel) || "Record";
156
+ const recordId = toRouteParamValue(record?.id);
157
+ if (recordId) {
158
+ return `${entityLabel} #${recordId}`;
159
+ }
160
+
161
+ return "";
162
+ }
163
+
164
+ export {
165
+ resolveCrudListParentDescriptor,
166
+ resolveCrudListParentRecordTitle,
167
+ resolveCrudListParentTitleFromItems
168
+ };
@@ -7,7 +7,7 @@ import {
7
7
  resolvePermissionAccess,
8
8
  resolveEnabled,
9
9
  resolveQueryKey
10
- } from "../scopeHelpers.js";
10
+ } from "../support/scopeHelpers.js";
11
11
 
12
12
  function normalizePermissionSets(permissionSets = {}) {
13
13
  const source = permissionSets && typeof permissionSets === "object" && !Array.isArray(permissionSets)
@@ -1,19 +1,20 @@
1
1
  import { computed, proxyRefs } from "vue";
2
2
  import { useRoute } from "vue-router";
3
3
  import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
4
- import { useAddEditCore } from "./useAddEditCore.js";
5
- import { useEndpointResource } from "./useEndpointResource.js";
6
- import { resolveOperationAdapter } from "./operationAdapters.js";
7
- import { createAddEditUiRuntime } from "./addEditUiRuntime.js";
8
- import { useUiFeedback } from "./useUiFeedback.js";
9
- import { useFieldErrorBag } from "./useFieldErrorBag.js";
4
+ import { useAddEditCore } from "../runtime/useAddEditCore.js";
5
+ import { useEndpointResource } from "../runtime/useEndpointResource.js";
6
+ import { resolveOperationAdapter } from "../runtime/operationAdapters.js";
7
+ import { createAddEditUiRuntime } from "../runtime/addEditUiRuntime.js";
8
+ import { useUiFeedback } from "../runtime/useUiFeedback.js";
9
+ import { useFieldErrorBag } from "../runtime/useFieldErrorBag.js";
10
10
  import {
11
11
  setupRouteChangeCleanup,
12
12
  setupOperationErrorReporting
13
- } from "./operationUiHelpers.js";
13
+ } from "../runtime/operationUiHelpers.js";
14
14
  import {
15
15
  resolveResourceMessages
16
- } from "./scopeHelpers.js";
16
+ } from "../support/scopeHelpers.js";
17
+ import { resolveRouteParamNamesInOrder } from "../support/routeTemplateHelpers.js";
17
18
 
18
19
  function useAddEdit({
19
20
  ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
@@ -53,6 +54,7 @@ function useAddEdit({
53
54
  const addEditUiRuntime = createAddEditUiRuntime({
54
55
  recordIdParam,
55
56
  routeParams: routeParams ?? computed(() => route?.params || {}),
57
+ routeParamNames: computed(() => resolveRouteParamNamesInOrder(route)),
56
58
  routePath: computed(() => route?.path || ""),
57
59
  routeRecordId,
58
60
  apiUrlTemplate,
@@ -134,6 +136,12 @@ function useAddEdit({
134
136
  const isInitialLoading = operationScope.isLoading(endpointResource.isInitialLoading);
135
137
  const isFetching = operationScope.isLoading(endpointResource.isFetching);
136
138
  const isRefetching = computed(() => Boolean(isFetching.value && !isInitialLoading.value));
139
+ const isFieldLocked = computed(() =>
140
+ Boolean(!canSave.value || addEdit.saving.value || isRefetching.value)
141
+ );
142
+ const isSubmitDisabled = computed(() =>
143
+ Boolean(isInitialLoading.value || isRefetching.value || !canSave.value)
144
+ );
137
145
  const loadError = operationScope.loadError(endpointResource.loadError);
138
146
  const isLoading = operationScope.isLoading(endpointResource.isLoading);
139
147
  setupOperationErrorReporting({
@@ -148,6 +156,8 @@ function useAddEdit({
148
156
  isInitialLoading,
149
157
  isFetching,
150
158
  isRefetching,
159
+ isFieldLocked,
160
+ isSubmitDisabled,
151
161
  isLoading,
152
162
  isSaving: addEdit.saving,
153
163
  fieldErrors: addEdit.fieldErrors,
@@ -1,17 +1,20 @@
1
1
  import { computed, proxyRefs, reactive, watch } from "vue";
2
2
  import { useRoute, useRouter } from "vue-router";
3
- import { asPlainObject } from "./scopeHelpers.js";
3
+ import { asPlainObject } from "../support/scopeHelpers.js";
4
4
  import { useAddEdit } from "./useAddEdit.js";
5
+ import {
6
+ resolveCrudBoundValues,
7
+ } from "../crud/crudBindingSupport.js";
5
8
  import {
6
9
  normalizeCrudFormFields,
7
10
  createCrudFormModel,
8
11
  buildCrudFormPayload,
9
12
  applyCrudPayloadToForm,
10
- applyCrudRouteBoundFieldValues,
13
+ resolveCrudRouteBoundFieldValues,
11
14
  resolveCrudFieldErrors,
12
15
  parseCrudResourceOperationInput
13
- } from "./crudSchemaFormHelpers.js";
14
- import { hasResolvedQueryData } from "./resourceLoadStateHelpers.js";
16
+ } from "../crud/crudSchemaFormHelpers.js";
17
+ import { hasResolvedQueryData } from "../support/resourceLoadStateHelpers.js";
15
18
 
16
19
  function normalizeFieldErrorKeys(keys = []) {
17
20
  return Array.isArray(keys)
@@ -36,12 +39,13 @@ function normalizeSaveSuccessOptions(options = {}) {
36
39
  });
37
40
  }
38
41
 
39
- function useCrudSchemaForm({
42
+ function useCrudAddEdit({
40
43
  resource = null,
41
44
  operationName = "",
42
45
  formFields = [],
43
46
  addEditOptions = {},
44
47
  saveSuccess = {},
48
+ fieldBinding = null,
45
49
  createModel = null,
46
50
  buildPayload = null,
47
51
  mapPayloadToModel = null,
@@ -61,18 +65,31 @@ function useCrudSchemaForm({
61
65
  ? asPlainObject(createModel(normalizedFields))
62
66
  : createCrudFormModel(normalizedFields);
63
67
  const form = hasProvidedModel ? providedModel : reactive(defaultModel);
68
+ const boundFieldValues = computed(() => {
69
+ return resolveCrudBoundValues({
70
+ binding: fieldBinding,
71
+ routeValues: resolveCrudRouteBoundFieldValues(normalizedFields, route?.params || {}),
72
+ context: Object.freeze({
73
+ route,
74
+ fields: normalizedFields
75
+ })
76
+ });
77
+ });
64
78
 
65
- function applyRouteBoundValues(target = {}) {
66
- return applyCrudRouteBoundFieldValues(normalizedFields, target, route?.params || {});
79
+ function applyBoundFieldValues(target = {}) {
80
+ Object.assign(target, boundFieldValues.value);
81
+ return target;
67
82
  }
68
83
 
69
- applyRouteBoundValues(form);
70
84
  watch(
71
- () => route?.params,
85
+ boundFieldValues,
72
86
  () => {
73
- applyRouteBoundValues(form);
87
+ applyBoundFieldValues(form);
74
88
  },
75
- { deep: true }
89
+ {
90
+ immediate: true,
91
+ deep: true
92
+ }
76
93
  );
77
94
  const parseInputOverride = typeof parseInput === "function"
78
95
  ? parseInput
@@ -109,7 +126,7 @@ function useCrudSchemaForm({
109
126
  fields: normalizedFields
110
127
  })
111
128
  : buildCrudFormPayload(normalizedFields, model);
112
- applyRouteBoundValues(payload);
129
+ applyBoundFieldValues(payload);
113
130
 
114
131
  return payload;
115
132
  }
@@ -120,12 +137,12 @@ function useCrudSchemaForm({
120
137
  ...context,
121
138
  fields: normalizedFields
122
139
  });
123
- applyRouteBoundValues(model);
140
+ applyBoundFieldValues(model);
124
141
  }
125
142
  : (shouldApplyDefaultMapPayload
126
143
  ? (model = {}, payload = {}) => {
127
144
  applyCrudPayloadToForm(normalizedFields, model, payload);
128
- applyRouteBoundValues(model);
145
+ applyBoundFieldValues(model);
129
146
  }
130
147
  : undefined);
131
148
 
@@ -200,4 +217,4 @@ function useCrudSchemaForm({
200
217
  });
201
218
  }
202
219
 
203
- export { useCrudSchemaForm };
220
+ export { useCrudAddEdit };