@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.
- package/package.descriptor.mjs +8 -22
- package/package.json +16 -11
- package/src/client/components/MembersAdminClientElement.vue +5 -5
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
- package/src/client/components/WorkspaceMembersClientElement.vue +19 -19
- package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
- package/src/client/components/WorkspacesClientElement.vue +4 -4
- package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
- package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
- package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -4
- package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +2 -2
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +37 -5
- package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +11 -4
- package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +178 -5
- package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
- package/src/client/composables/internal/useOperationScope.js +1 -1
- package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +18 -8
- package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
- package/src/client/composables/records/useCrudList.js +83 -0
- package/src/client/composables/records/useCrudView.js +35 -0
- package/src/client/composables/records/useList.js +482 -0
- package/src/client/composables/{useView.js → records/useView.js} +7 -7
- package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +13 -4
- package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +20 -8
- package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
- package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
- package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
- package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
- package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +13 -4
- package/src/client/composables/support/listQueryParamSupport.js +459 -0
- package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +122 -0
- package/src/client/composables/useAccess.js +2 -2
- package/src/client/composables/useAccountSettingsRuntime.js +6 -6
- package/src/client/composables/useBootstrapQuery.js +1 -1
- package/src/client/composables/useCommand.js +5 -5
- package/src/client/composables/useCrudListParentTitle.js +131 -0
- package/src/client/composables/usePagedCollection.js +58 -7
- package/src/client/composables/useScopeRuntime.js +1 -1
- package/src/client/lib/bootstrap.js +1 -1
- package/src/client/lib/menuIcons.js +27 -6
- package/src/client/support/menuLinkTarget.js +93 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
- package/test/addEditUiRuntime.test.js +19 -1
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudLookupFieldRuntime.test.js +52 -2
- package/test/errorMessageHelpers.test.js +1 -1
- package/test/exportsContract.test.js +10 -1
- package/test/listQueryParamSupport.test.js +190 -0
- package/test/listUiRuntime.test.js +22 -1
- package/test/menuIcons.test.js +2 -0
- package/test/menuLinkTarget.test.js +116 -0
- package/test/permissions.test.js +2 -2
- package/test/refValueHelpers.test.js +1 -1
- package/test/resourceLoadStateHelpers.test.js +1 -1
- package/test/routeTemplateHelpers.test.js +57 -1
- package/test/scopeHelpers.test.js +1 -1
- package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
- package/test/useCrudListParentTitle.test.js +143 -0
- package/test/useListSearchSupport.test.js +1 -1
- package/test/usePagedCollection.test.js +53 -0
- package/test/viewCoreLoading.test.js +1 -1
- package/test/viewUiRuntime.test.js +36 -1
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -241
- package/src/client/composables/useList.js +0 -268
- /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
- /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
- /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
- /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
- /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
- /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
- /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
- /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
- /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
- /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
- /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
- /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 {
|
|
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 "
|
|
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 "
|
|
3
|
-
import { toRouteParamValue } from "
|
|
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
|
|
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
|
-
|
|
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 "
|
|
5
|
-
import { useEndpointResource } from "
|
|
6
|
-
import { resolveOperationAdapter } from "
|
|
7
|
-
import { createAddEditUiRuntime } from "
|
|
8
|
-
import { useUiFeedback } from "
|
|
9
|
-
import { useFieldErrorBag } from "
|
|
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 "
|
|
13
|
+
} from "../runtime/operationUiHelpers.js";
|
|
14
14
|
import {
|
|
15
15
|
resolveResourceMessages
|
|
16
|
-
} from "
|
|
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 "
|
|
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
|
-
|
|
13
|
+
resolveCrudRouteBoundFieldValues,
|
|
11
14
|
resolveCrudFieldErrors,
|
|
12
15
|
parseCrudResourceOperationInput
|
|
13
|
-
} from "
|
|
14
|
-
import { hasResolvedQueryData } from "
|
|
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
|
|
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
|
|
66
|
-
|
|
79
|
+
function applyBoundFieldValues(target = {}) {
|
|
80
|
+
Object.assign(target, boundFieldValues.value);
|
|
81
|
+
return target;
|
|
67
82
|
}
|
|
68
83
|
|
|
69
|
-
applyRouteBoundValues(form);
|
|
70
84
|
watch(
|
|
71
|
-
|
|
85
|
+
boundFieldValues,
|
|
72
86
|
() => {
|
|
73
|
-
|
|
87
|
+
applyBoundFieldValues(form);
|
|
74
88
|
},
|
|
75
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
applyBoundFieldValues(model);
|
|
124
141
|
}
|
|
125
142
|
: (shouldApplyDefaultMapPayload
|
|
126
143
|
? (model = {}, payload = {}) => {
|
|
127
144
|
applyCrudPayloadToForm(normalizedFields, model, payload);
|
|
128
|
-
|
|
145
|
+
applyBoundFieldValues(model);
|
|
129
146
|
}
|
|
130
147
|
: undefined);
|
|
131
148
|
|
|
@@ -200,4 +217,4 @@ function useCrudSchemaForm({
|
|
|
200
217
|
});
|
|
201
218
|
}
|
|
202
219
|
|
|
203
|
-
export {
|
|
220
|
+
export { useCrudAddEdit };
|