@jskit-ai/crud-ui-generator 0.1.5 → 0.1.7

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/crud-ui-generator",
4
- version: "0.1.5",
4
+ version: "0.1.7",
5
5
  kind: "generator",
6
6
  description: "Generate app-local CRUD UI scaffolds from resource validators.",
7
7
  options: {
@@ -81,6 +81,22 @@ export default Object.freeze({
81
81
  defaultValue: "",
82
82
  promptLabel: "Menu placement",
83
83
  promptHint: "Optional host:position target (defaults to ShellLayout default outlet)."
84
+ },
85
+ "placement-component-token": {
86
+ required: false,
87
+ inputType: "text",
88
+ defaultValue: "",
89
+ promptLabel: "Placement component token",
90
+ promptHint:
91
+ "Optional component token override for generated menu placement. Use local.main.ui.tab-link-item for routed tab links (auto-provisions src/components/TabLinkItem.vue + MainClientProvider registration)."
92
+ },
93
+ "placement-to": {
94
+ required: false,
95
+ inputType: "text",
96
+ defaultValue: "",
97
+ promptLabel: "Placement to",
98
+ promptHint:
99
+ "Optional explicit props.to value for generated menu placement (example: ./pets). Required when adding placement for dynamic directory-prefix/route-path values."
84
100
  }
85
101
  },
86
102
  dependsOn: [],
@@ -120,7 +136,7 @@ export default Object.freeze({
120
136
  mutations: {
121
137
  dependencies: {
122
138
  runtime: {
123
- "@jskit-ai/users-web": "0.1.36"
139
+ "@jskit-ai/users-web": "0.1.38"
124
140
  },
125
141
  dev: {}
126
142
  },
@@ -163,7 +179,7 @@ export default Object.freeze({
163
179
  }
164
180
  },
165
181
  {
166
- from: "templates/src/pages/admin/ui-generator/NewElement.vue",
182
+ from: "templates/src/pages/admin/ui-generator/NewWrapperElement.vue",
167
183
  toSurface: "${option:surface|lower}",
168
184
  toSurfacePath: "${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}/new.vue",
169
185
  reason: "Install generated new page.",
@@ -174,12 +190,20 @@ export default Object.freeze({
174
190
  export: "buildUiTemplateContext"
175
191
  },
176
192
  when: {
177
- option: "operations",
178
- in: ["new"]
193
+ all: [
194
+ {
195
+ option: "operations",
196
+ in: ["new"]
197
+ },
198
+ {
199
+ option: "operations",
200
+ in: ["edit"]
201
+ }
202
+ ]
179
203
  }
180
204
  },
181
205
  {
182
- from: "templates/src/pages/admin/ui-generator/EditElement.vue",
206
+ from: "templates/src/pages/admin/ui-generator/EditWrapperElement.vue",
183
207
  toSurface: "${option:surface|lower}",
184
208
  toSurfacePath:
185
209
  "${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}/[${option:id-param|trim}]/edit.vue",
@@ -191,8 +215,115 @@ export default Object.freeze({
191
215
  export: "buildUiTemplateContext"
192
216
  },
193
217
  when: {
194
- option: "operations",
195
- in: ["edit"]
218
+ all: [
219
+ {
220
+ option: "operations",
221
+ in: ["new"]
222
+ },
223
+ {
224
+ option: "operations",
225
+ in: ["edit"]
226
+ }
227
+ ]
228
+ }
229
+ },
230
+ {
231
+ from: "templates/src/pages/admin/ui-generator/AddEditForm.vue",
232
+ toSurface: "${option:surface|lower}",
233
+ toSurfacePath:
234
+ "${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}/_components/${option:namespace|singular|pascal|default(Record)}AddEditForm.vue",
235
+ reason: "Install generated shared add/edit form component.",
236
+ category: "ui-generator",
237
+ id: "ui-generator-page-add-edit-form-${option:namespace|snake}",
238
+ templateContext: {
239
+ entrypoint: "src/server/buildTemplateContext.js",
240
+ export: "buildUiTemplateContext"
241
+ },
242
+ when: {
243
+ all: [
244
+ {
245
+ option: "operations",
246
+ in: ["new"]
247
+ },
248
+ {
249
+ option: "operations",
250
+ in: ["edit"]
251
+ }
252
+ ]
253
+ }
254
+ },
255
+ {
256
+ from: "templates/src/pages/admin/ui-generator/AddEditFormFields.js",
257
+ toSurface: "${option:surface|lower}",
258
+ toSurfacePath:
259
+ "${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}/_components/${option:namespace|singular|pascal|default(Record)}AddEditFormFields.js",
260
+ reason: "Install generated shared add/edit form field definitions.",
261
+ category: "ui-generator",
262
+ id: "ui-generator-page-add-edit-form-fields-${option:namespace|snake}",
263
+ templateContext: {
264
+ entrypoint: "src/server/buildTemplateContext.js",
265
+ export: "buildUiTemplateContext"
266
+ },
267
+ when: {
268
+ all: [
269
+ {
270
+ option: "operations",
271
+ in: ["new"]
272
+ },
273
+ {
274
+ option: "operations",
275
+ in: ["edit"]
276
+ }
277
+ ]
278
+ }
279
+ },
280
+ {
281
+ from: "templates/src/pages/admin/ui-generator/NewElement.vue",
282
+ toSurface: "${option:surface|lower}",
283
+ toSurfacePath: "${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}/new.vue",
284
+ reason: "Install generated new page.",
285
+ category: "ui-generator",
286
+ id: "ui-generator-page-new-standalone-${option:namespace|snake}",
287
+ templateContext: {
288
+ entrypoint: "src/server/buildTemplateContext.js",
289
+ export: "buildUiTemplateContext"
290
+ },
291
+ when: {
292
+ all: [
293
+ {
294
+ option: "operations",
295
+ in: ["new"]
296
+ },
297
+ {
298
+ option: "operations",
299
+ notIn: ["edit"]
300
+ }
301
+ ]
302
+ }
303
+ },
304
+ {
305
+ from: "templates/src/pages/admin/ui-generator/EditElement.vue",
306
+ toSurface: "${option:surface|lower}",
307
+ toSurfacePath:
308
+ "${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}/[${option:id-param|trim}]/edit.vue",
309
+ reason: "Install generated edit page.",
310
+ category: "ui-generator",
311
+ id: "ui-generator-page-edit-standalone-${option:namespace|snake}",
312
+ templateContext: {
313
+ entrypoint: "src/server/buildTemplateContext.js",
314
+ export: "buildUiTemplateContext"
315
+ },
316
+ when: {
317
+ all: [
318
+ {
319
+ option: "operations",
320
+ in: ["edit"]
321
+ },
322
+ {
323
+ option: "operations",
324
+ notIn: ["new"]
325
+ }
326
+ ]
196
327
  }
197
328
  }
198
329
  ],
@@ -204,7 +335,7 @@ export default Object.freeze({
204
335
  skipIfContains:
205
336
  "jskit:ui-generator.menu:${option:namespace|kebab}:${option:directory-prefix|path}:${option:container|path}:${option:route-path|path}",
206
337
  value:
207
- "\n// jskit:ui-generator.menu:${option:namespace|kebab}:${option:directory-prefix|path}:${option:container|path}:${option:route-path|path}\n{\n addPlacement({\n id: \"ui-generator.${option:namespace|kebab}.menu\",\n host: \"__JSKIT_UI_MENU_PLACEMENT_HOST__\",\n position: \"__JSKIT_UI_MENU_PLACEMENT_POSITION__\",\n surfaces: [\"${option:surface|lower}\"],\n order: 155,\n componentToken: \"__JSKIT_UI_MENU_COMPONENT_TOKEN__\",\n props: {\n label: \"${option:namespace|plural|pascal}\",\n surface: \"${option:surface|lower}\",\n workspaceSuffix: \"/${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}\",\n nonWorkspaceSuffix: \"/${option:directory-prefix|pathprefix}${option:container|pathprefix}${option:route-path|path}\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n });\n}\n",
338
+ "\n// jskit:ui-generator.menu:${option:namespace|kebab}:${option:directory-prefix|path}:${option:container|path}:${option:route-path|path}\n{\n addPlacement({\n id: \"ui-generator.${option:namespace|kebab}.menu\",\n host: \"__JSKIT_UI_MENU_PLACEMENT_HOST__\",\n position: \"__JSKIT_UI_MENU_PLACEMENT_POSITION__\",\n surfaces: [\"${option:surface|lower}\"],\n order: 155,\n componentToken: \"__JSKIT_UI_MENU_COMPONENT_TOKEN__\",\n props: {\n label: \"${option:namespace|plural|pascal}\",\n surface: \"${option:surface|lower}\",\n workspaceSuffix: \"__JSKIT_UI_MENU_WORKSPACE_SUFFIX__\",\n nonWorkspaceSuffix: \"__JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__\",\n__JSKIT_UI_MENU_TO_PROP_LINE__ },\n when: ({ auth }) => Boolean(auth?.authenticated)\n });\n}\n",
208
339
  reason: "Append generated UI menu placement.",
209
340
  category: "ui-generator",
210
341
  id: "ui-generator-placement-menu",
@@ -219,8 +350,32 @@ export default Object.freeze({
219
350
  in: ["list"]
220
351
  },
221
352
  {
222
- option: "route-path",
223
- notContains: "["
353
+ any: [
354
+ {
355
+ all: [
356
+ {
357
+ option: "route-path",
358
+ notContains: "["
359
+ },
360
+ {
361
+ option: "directory-prefix",
362
+ notContains: "["
363
+ }
364
+ ]
365
+ },
366
+ {
367
+ all: [
368
+ {
369
+ option: "placement",
370
+ contains: ":"
371
+ },
372
+ {
373
+ option: "placement-to",
374
+ notEquals: ""
375
+ }
376
+ ]
377
+ }
378
+ ]
224
379
  }
225
380
  ]
226
381
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-ui-generator",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/crud-core": "0.1.30",
10
- "@jskit-ai/kernel": "0.1.22"
9
+ "@jskit-ai/crud-core": "0.1.32",
10
+ "@jskit-ai/kernel": "0.1.24"
11
11
  },
12
12
  "exports": {
13
13
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -27,7 +27,24 @@ const ALLOWED_OPERATIONS = new Set(["list", "view", "new", "edit"]);
27
27
  const DEFAULT_LIST_HIDDEN_FIELD_KEYS = new Set(["createdAt", "updatedAt"]);
28
28
  const CONTAINER_TOKEN_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
29
29
  const DEFAULT_MENU_COMPONENT_TOKEN = "users.web.shell.surface-aware-menu-link-item";
30
- const CONTAINER_MENU_COMPONENT_TOKEN = "local.main.ui.section-shell.tab-link-item";
30
+ const CONTAINER_MENU_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
31
+
32
+ function splitPathSegments(value = "") {
33
+ return normalizeText(value)
34
+ .replaceAll("\\", "/")
35
+ .split("/")
36
+ .map((entry) => normalizeText(entry))
37
+ .filter(Boolean);
38
+ }
39
+
40
+ function isRouteGroupSegment(value = "") {
41
+ const source = normalizeText(value);
42
+ return source.startsWith("(") && source.endsWith(")");
43
+ }
44
+
45
+ function joinPathSegments(segments = []) {
46
+ return (Array.isArray(segments) ? segments : []).join("/");
47
+ }
31
48
 
32
49
  function resolveContainerOption(options = {}) {
33
50
  const container = normalizeText(options?.container).toLowerCase();
@@ -45,31 +62,50 @@ function resolveContainerOption(options = {}) {
45
62
  }
46
63
 
47
64
  function resolveRoutePathWithContainer(options = {}) {
48
- const routePath = normalizeText(options?.["route-path"]);
49
- if (!routePath) {
50
- return "";
51
- }
52
-
53
65
  const container = resolveContainerOption(options);
54
- if (!container) {
55
- return routePath;
56
- }
66
+ const routeSegments = [
67
+ ...splitPathSegments(options?.["directory-prefix"]),
68
+ ...splitPathSegments(container),
69
+ ...splitPathSegments(options?.["route-path"])
70
+ ];
71
+ return joinPathSegments(routeSegments);
72
+ }
57
73
 
58
- return `${container}/${routePath}`;
74
+ function resolvePlacementUrlSuffix(options = {}) {
75
+ const routeSegments = splitPathSegments(resolveRoutePathWithContainer(options))
76
+ .filter((segment) => !isRouteGroupSegment(segment));
77
+ if (routeSegments.length < 1) {
78
+ return "/";
79
+ }
80
+ return `/${joinPathSegments(routeSegments)}`;
59
81
  }
60
82
 
61
83
  function resolveMenuComponentToken(options = {}) {
84
+ const explicitToken = normalizeText(options?.["placement-component-token"]);
85
+ if (explicitToken) {
86
+ return explicitToken;
87
+ }
88
+
62
89
  const container = resolveContainerOption(options);
63
90
  return container ? CONTAINER_MENU_COMPONENT_TOKEN : DEFAULT_MENU_COMPONENT_TOKEN;
64
91
  }
65
92
 
93
+ function resolveMenuToPropLine(options = {}) {
94
+ const placementTo = normalizeText(options?.["placement-to"]);
95
+ if (!placementTo) {
96
+ return "";
97
+ }
98
+
99
+ return ` to: ${JSON.stringify(placementTo)},\n`;
100
+ }
101
+
66
102
  async function resolveMenuPlacementTarget({ appRoot, options, hasListOperation } = {}) {
67
103
  if (hasListOperation !== true) {
68
104
  return null;
69
105
  }
70
106
 
71
107
  const routePath = resolveRoutePathWithContainer(options);
72
- if (!routePath || routePath.includes("[")) {
108
+ if (!routePath) {
73
109
  return null;
74
110
  }
75
111
 
@@ -209,6 +245,22 @@ function ensureFields(fields, fallbackFields = createFieldDefinitions({})) {
209
245
  return fallbackFields;
210
246
  }
211
247
 
248
+ function resolveViewTitleFallbackFieldKey(fields = []) {
249
+ const sourceFields = Array.isArray(fields) ? fields : [];
250
+ for (const field of sourceFields) {
251
+ if (normalizeText(field?.type).toLowerCase() !== "string") {
252
+ continue;
253
+ }
254
+
255
+ const key = normalizeText(field?.key);
256
+ if (key) {
257
+ return key;
258
+ }
259
+ }
260
+
261
+ return "";
262
+ }
263
+
212
264
  async function buildUiTemplateContext({ appRoot, options } = {}) {
213
265
  const selectedOperations = parseOperationsOption(options);
214
266
  const selectedDisplayFields = parseDisplayFieldsOption(options);
@@ -298,6 +350,9 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
298
350
  const viewFields = hasViewOperation
299
351
  ? filterDisplayFields(selectedDisplayFields, ensureFields(viewFieldsAll))
300
352
  : createFieldDefinitions({});
353
+ const viewTitleFallbackFieldKey = hasViewOperation
354
+ ? resolveViewTitleFallbackFieldKey(viewFieldsAll)
355
+ : "";
301
356
  const createFields = hasNewOperation
302
357
  ? filterDisplayFields(selectedDisplayFields, createFieldsAll)
303
358
  : [];
@@ -325,6 +380,7 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
325
380
  __JSKIT_UI_LIST_REALTIME_EVENTS__: JSON.stringify(listRealtimeEvents),
326
381
  __JSKIT_UI_LIST_RECORD_ID_EXPR__: resolveRecordIdExpression(recordIdFields),
327
382
  __JSKIT_UI_VIEW_COLUMNS__: buildViewColumns(viewFields),
383
+ __JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__: JSON.stringify(viewTitleFallbackFieldKey),
328
384
  __JSKIT_UI_RECORD_CHANGED_EVENT__: JSON.stringify(defaultRecordChangedEvent),
329
385
  __JSKIT_UI_HAS_LIST_ROUTE__: hasListOperation ? "true" : "false",
330
386
  __JSKIT_UI_HAS_VIEW_ROUTE__: hasViewOperation ? "true" : "false",
@@ -338,7 +394,10 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
338
394
  __JSKIT_UI_EDIT_FORM_FIELD_PUSH_LINES__: renderObjectPushLines("UI_EDIT_FORM_FIELDS", editFields),
339
395
  __JSKIT_UI_MENU_PLACEMENT_HOST__: normalizeText(menuPlacementTarget?.host),
340
396
  __JSKIT_UI_MENU_PLACEMENT_POSITION__: normalizeText(menuPlacementTarget?.position),
341
- __JSKIT_UI_MENU_COMPONENT_TOKEN__: resolveMenuComponentToken(options)
397
+ __JSKIT_UI_MENU_COMPONENT_TOKEN__: resolveMenuComponentToken(options),
398
+ __JSKIT_UI_MENU_WORKSPACE_SUFFIX__: resolvePlacementUrlSuffix(options),
399
+ __JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__: resolvePlacementUrlSuffix(options),
400
+ __JSKIT_UI_MENU_TO_PROP_LINE__: resolveMenuToPropLine(options)
342
401
  };
343
402
  }
344
403
 
@@ -12,6 +12,7 @@ import {
12
12
  resolveCrudLookupApiPathFromNamespace,
13
13
  resolveCrudLookupContainerKey
14
14
  } from "@jskit-ai/kernel/shared/support/crudLookup";
15
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
15
16
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
16
17
 
17
18
  const JS_IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
@@ -281,6 +282,62 @@ function toFieldLabel(key) {
281
282
  .join(" ");
282
283
  }
283
284
 
285
+ function isSupportedSelectOptionValue(value) {
286
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
287
+ }
288
+
289
+ function toSelectOptionLabel(value) {
290
+ if (typeof value === "string") {
291
+ const normalizedValue = normalizeText(value);
292
+ return normalizedValue ? toFieldLabel(normalizedValue) : "";
293
+ }
294
+ if (typeof value === "number" || typeof value === "boolean") {
295
+ return String(value);
296
+ }
297
+ return "";
298
+ }
299
+
300
+ function toSelectOptionIdentity(value) {
301
+ return `${typeof value}:${String(value)}`;
302
+ }
303
+
304
+ function normalizeFieldUiOptions(rawOptions, { context = "resource fieldMeta ui.options" } = {}) {
305
+ if (rawOptions === undefined || rawOptions === null) {
306
+ return [];
307
+ }
308
+ if (!Array.isArray(rawOptions)) {
309
+ throw new Error(`${context} must be an array of { value, label? } entries.`);
310
+ }
311
+
312
+ const options = [];
313
+ const seenValues = new Set();
314
+ for (const [index, rawEntry] of rawOptions.entries()) {
315
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
316
+ throw new Error(`${context}[${index}] must be an object.`);
317
+ }
318
+
319
+ const value = rawEntry.value;
320
+ if (!isSupportedSelectOptionValue(value)) {
321
+ throw new Error(`${context}[${index}].value must be a string, number, or boolean.`);
322
+ }
323
+
324
+ const identity = toSelectOptionIdentity(value);
325
+ if (seenValues.has(identity)) {
326
+ continue;
327
+ }
328
+ seenValues.add(identity);
329
+
330
+ const explicitLabel = normalizeText(rawEntry.label);
331
+ const fallbackLabel = toSelectOptionLabel(value);
332
+ options.push({
333
+ value,
334
+ label: explicitLabel || fallbackLabel || String(value)
335
+ });
336
+ }
337
+
338
+ return options;
339
+ }
340
+
284
341
  function stripLookupIdSuffix(key = "") {
285
342
  const normalizedKey = normalizeText(key);
286
343
  if (!normalizedKey) {
@@ -400,6 +457,10 @@ function normalizeLookupRelation(relation = {}) {
400
457
  normalized.containerKey = containerKey;
401
458
  }
402
459
  }
460
+ const surfaceId = normalizeSurfaceId(relation.surfaceId);
461
+ if (surfaceId) {
462
+ normalized.surfaceId = surfaceId;
463
+ }
403
464
  return normalized;
404
465
  }
405
466
 
@@ -425,6 +486,9 @@ function buildResourceFieldMetaMap(resource = {}) {
425
486
  }
426
487
 
427
488
  const relation = normalizeLookupRelation(rawEntry.relation);
489
+ const fieldUiOptions = normalizeFieldUiOptions(rawEntry?.ui?.options, {
490
+ context: `resource.fieldMeta["${key}"].ui.options`
491
+ });
428
492
  if (relation) {
429
493
  nextEntry.relation = relation;
430
494
  const formControl = checkCrudLookupFormControl(rawEntry?.ui?.formControl, {
@@ -437,6 +501,12 @@ function buildResourceFieldMetaMap(resource = {}) {
437
501
  };
438
502
  }
439
503
  }
504
+ if (fieldUiOptions.length > 0) {
505
+ nextEntry.ui = {
506
+ ...(nextEntry.ui || {}),
507
+ options: fieldUiOptions
508
+ };
509
+ }
440
510
 
441
511
  map[key] = nextEntry;
442
512
  }
@@ -559,6 +629,16 @@ function createFormFieldDefinitions(
559
629
 
560
630
  const schemaType = resolveSchemaType(schema);
561
631
  const relation = toLookupRelation(fieldMetaMap, key, { lookupContainerKey });
632
+ const fieldUiOptions = Array.isArray(fieldMetaMap?.[key]?.ui?.options)
633
+ ? fieldMetaMap[key].ui.options
634
+ : [];
635
+ const schemaEnumValues = Array.isArray(schemaType.schema?.enum) ? schemaType.schema.enum : [];
636
+ if (!relation && schemaEnumValues.length > 0 && fieldUiOptions.length < 1) {
637
+ throw new Error(
638
+ `resource form field "${key}" defines schema enum values but is missing resource.fieldMeta["${key}"].ui.options.`
639
+ );
640
+ }
641
+ const selectOptions = relation ? [] : fieldUiOptions;
562
642
  const lookupFormControl = relation
563
643
  ? checkCrudLookupFormControl(fieldMetaMap?.[key]?.ui?.formControl, {
564
644
  context: `resource.fieldMeta["${key}"].ui.formControl`,
@@ -573,9 +653,14 @@ function createFormFieldDefinitions(
573
653
  nullable: schemaType.nullable,
574
654
  relation,
575
655
  inputType: resolveFormInputType(schemaType.type, schemaType.format),
576
- component: resolveFormFieldComponent(schemaType.type, relation),
656
+ component: selectOptions.length > 0
657
+ ? "select"
658
+ : resolveFormFieldComponent(schemaType.type, relation),
577
659
  maxLength: toPositiveInteger(schemaType.schema?.maxLength)
578
660
  };
661
+ if (selectOptions.length > 0) {
662
+ fieldDefinition.options = selectOptions;
663
+ }
579
664
  if (normalizedParentRouteParamKey && key === normalizedParentRouteParamKey) {
580
665
  fieldDefinition.hidden = true;
581
666
  fieldDefinition.routeParamKey = normalizedParentRouteParamKey;
@@ -622,6 +707,10 @@ function escapeHtml(value) {
622
707
  .replaceAll("'", "&#39;");
623
708
  }
624
709
 
710
+ function serializeTemplateBindingValue(value) {
711
+ return JSON.stringify(value).replaceAll("'", "\\u0027");
712
+ }
713
+
625
714
  function buildListHeaderColumns(fields = []) {
626
715
  return (Array.isArray(fields) ? fields : [])
627
716
  .map((field) => ` <th>${escapeHtml(field.label)}</th>`)
@@ -723,11 +812,25 @@ function buildFormColumns(fields = []) {
723
812
  label="${label}"
724
813
  color="primary"
725
814
  hide-details="auto"
726
- :disabled="
727
- !formRuntime.addEdit.canSave ||
728
- formRuntime.addEdit.isSaving ||
729
- formRuntime.addEdit.isRefetching
730
- "
815
+ :disabled="formRuntime.addEdit.isFieldLocked"
816
+ :error-messages='${fieldErrorExpression}'
817
+ />
818
+ </v-col>`;
819
+ }
820
+
821
+ if (component === "select") {
822
+ const selectOptions = Array.isArray(field?.options) ? field.options : [];
823
+ return ` <v-col cols="12" md="6">
824
+ <v-select
825
+ v-model="${formAccessor}"
826
+ label="${label}"
827
+ variant="outlined"
828
+ density="comfortable"
829
+ :items='${serializeTemplateBindingValue(selectOptions)}'
830
+ item-title="label"
831
+ item-value="value"
832
+ :disabled="formRuntime.addEdit.isFieldLocked"
833
+ :clearable="${field.nullable === true ? "true" : "false"}"
731
834
  :error-messages='${fieldErrorExpression}'
732
835
  />
733
836
  </v-col>`;
@@ -747,17 +850,14 @@ function buildFormColumns(fields = []) {
747
850
  label="${label}"
748
851
  variant="outlined"
749
852
  density="comfortable"
853
+ autocomplete="off"
750
854
  :items='resolveLookupItems(${JSON.stringify(key)}, { selectedValue: ${formAccessor}, selectedRecord: formRuntime.addEdit.resource.data })'
751
855
  ${lookupSearchBindings}
752
856
  item-title="label"
753
857
  item-value="value"
754
858
  ${lookupNoFilterLine}
755
859
  :loading='resolveLookupLoading(${JSON.stringify(key)})'
756
- :disabled="
757
- !formRuntime.addEdit.canSave ||
758
- formRuntime.addEdit.isSaving ||
759
- formRuntime.addEdit.isRefetching
760
- "
860
+ :disabled="formRuntime.addEdit.isFieldLocked"
761
861
  :clearable="${field.nullable === true ? "true" : "false"}"
762
862
  :error-messages='${fieldErrorExpression}'
763
863
  />
@@ -776,11 +876,7 @@ function buildFormColumns(fields = []) {
776
876
  variant="outlined"
777
877
  density="comfortable"
778
878
  :maxlength="${maxLength}"
779
- :readonly="
780
- !formRuntime.addEdit.canSave ||
781
- formRuntime.addEdit.isSaving ||
782
- formRuntime.addEdit.isRefetching
783
- "
879
+ :readonly="formRuntime.addEdit.isFieldLocked"
784
880
  :error-messages='${fieldErrorExpression}'
785
881
  />
786
882
  </v-col>`;