@jskit-ai/crud-ui-generator 0.1.48 → 0.1.50

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,9 @@
1
+ import { GENERATED_UI_NAVIGATION_ROLE_OPTION } from "@jskit-ai/kernel/shared/support/generatedUiContract";
2
+
1
3
  export default Object.freeze({
2
4
  packageVersion: 1,
3
5
  packageId: "@jskit-ai/crud-ui-generator",
4
- version: "0.1.48",
6
+ version: "0.1.50",
5
7
  kind: "generator",
6
8
  description: "Generate CRUD route trees from resource validators at an explicit route root relative to src/pages/.",
7
9
  options: {
@@ -64,16 +66,9 @@ export default Object.freeze({
64
66
  inputType: "text",
65
67
  defaultValue: "",
66
68
  promptLabel: "Link placement",
67
- promptHint: "Optional target override for the generated list-page link placement (format: host:position)."
68
- },
69
- "link-component-token": {
70
- required: false,
71
- inputType: "text",
72
- defaultValue: "",
73
- promptLabel: "Link component token",
74
- promptHint:
75
- "Optional component token override for the generated list-page link placement (example: local.main.ui.tab-link-item)."
69
+ promptHint: "Optional semantic target override for the generated list-page link placement (format: area.slot)."
76
70
  },
71
+ "navigation-role": GENERATED_UI_NAVIGATION_ROLE_OPTION,
77
72
  namespace: {
78
73
  required: false,
79
74
  inputType: "text",
@@ -118,8 +113,8 @@ export default Object.freeze({
118
113
  "display-fields",
119
114
  "id-param",
120
115
  "parent-title",
116
+ "navigation-role",
121
117
  "link-placement",
122
- "link-component-token",
123
118
  "namespace",
124
119
  "force"
125
120
  ],
@@ -180,7 +175,7 @@ export default Object.freeze({
180
175
  mutations: {
181
176
  dependencies: {
182
177
  runtime: {
183
- "@jskit-ai/users-web": "0.1.80"
178
+ "@jskit-ai/users-web": "0.1.82"
184
179
  },
185
180
  dev: {}
186
181
  },
@@ -204,6 +199,28 @@ export default Object.freeze({
204
199
  in: ["list"]
205
200
  }
206
201
  },
202
+ {
203
+ from: "templates/src/pages/admin/ui-generator/listFilters.js",
204
+ to: "src/pages/${option:target-root|trim}/listFilters.js",
205
+ reason: "Install generated client-side list filter definitions.",
206
+ category: "crud-ui-generator",
207
+ id: "crud-ui-page-list-filters-${option:target-root|snake}",
208
+ when: {
209
+ option: "operations",
210
+ in: ["list"]
211
+ }
212
+ },
213
+ {
214
+ from: "templates/src/pages/admin/ui-generator/listBulkActions.js",
215
+ to: "src/pages/${option:target-root|trim}/listBulkActions.js",
216
+ reason: "Install generated client-side list bulk action definitions.",
217
+ category: "crud-ui-generator",
218
+ id: "crud-ui-page-list-bulk-actions-${option:target-root|snake}",
219
+ when: {
220
+ option: "operations",
221
+ in: ["list"]
222
+ }
223
+ },
207
224
  {
208
225
  from: "templates/src/pages/admin/ui-generator/ViewElement.vue",
209
226
  to: "src/pages/${option:target-root|trim}/[${option:id-param|trim}]/index.vue",
@@ -364,8 +381,7 @@ export default Object.freeze({
364
381
  file: "src/placement.js",
365
382
  position: "bottom",
366
383
  skipIfContains: "__JSKIT_UI_MENU_MARKER__",
367
- value:
368
- "\n// __JSKIT_UI_MENU_MARKER__\n{\n addPlacement({\n id: \"__JSKIT_UI_MENU_PLACEMENT_ID__\",\n target: \"__JSKIT_UI_MENU_PLACEMENT_TARGET__\",\n surfaces: [\"__JSKIT_UI_SURFACE_ID__\"],\n order: 155,\n componentToken: \"__JSKIT_UI_MENU_COMPONENT_TOKEN__\",\n props: {\n label: \"__JSKIT_UI_MENU_LABEL__\",\n icon: \"__JSKIT_UI_MENU_ICON__\",\n surface: \"__JSKIT_UI_SURFACE_ID__\",\n scopedSuffix: \"__JSKIT_UI_MENU_WORKSPACE_SUFFIX__\",\n unscopedSuffix: \"__JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__\",\n__JSKIT_UI_MENU_TO_PROP_LINE__ },\n__JSKIT_UI_MENU_WHEN_LINE__ });\n}\n",
384
+ value: "__JSKIT_UI_MENU_APPEND_BLOCK__",
369
385
  reason: "Append generated CRUD list-page placement.",
370
386
  category: "crud-ui-generator",
371
387
  id: "crud-ui-placement-menu",
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-ui-generator",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/crud-core": "0.1.73",
10
- "@jskit-ai/kernel": "0.1.65",
11
- "@jskit-ai/resource-crud-core": "0.1.10"
9
+ "@jskit-ai/crud-core": "0.1.75",
10
+ "@jskit-ai/kernel": "0.1.67",
11
+ "@jskit-ai/resource-crud-core": "0.1.12"
12
12
  },
13
13
  "exports": {
14
14
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -7,6 +7,10 @@ import {
7
7
  resolvePageTargetDetails
8
8
  } from "@jskit-ai/kernel/server/support";
9
9
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
10
+ import {
11
+ resolveGeneratedUiNavigationRoleLinkPlacement,
12
+ shouldCreateGeneratedUiNavigationLink
13
+ } from "@jskit-ai/kernel/shared/support/generatedUiContract";
10
14
  import {
11
15
  loadResourceDefinition,
12
16
  requireOperation,
@@ -22,6 +26,7 @@ import {
22
26
  resolveNearestParentRouteParamKey,
23
27
  buildListHeaderColumns,
24
28
  buildListRowColumns,
29
+ buildListCardFields,
25
30
  buildViewColumns,
26
31
  buildFormColumns,
27
32
  resolveRecordIdFieldKey,
@@ -157,6 +162,18 @@ function toTitleFromKebab(value = "", fallback = "") {
157
162
  return wordsToTitle(words);
158
163
  }
159
164
 
165
+ function buildCrudListCopy(resourceLabels = {}) {
166
+ const pluralTitle = normalizeText(resourceLabels.pluralTitle) || "Records";
167
+ const singularTitle = normalizeText(resourceLabels.singularTitle) || "Record";
168
+ return Object.freeze({
169
+ loadErrorTitle: `Unable to load ${pluralTitle}`,
170
+ loadErrorBody: "Check the connection and try again.",
171
+ emptyTitle: `No ${pluralTitle} yet`,
172
+ emptyBody: `Create the first ${singularTitle} to start using this workflow.`,
173
+ createLabel: `New ${singularTitle}`
174
+ });
175
+ }
176
+
160
177
  function normalizeRelativeAppPath(value = "") {
161
178
  return String(value || "")
162
179
  .replaceAll("\\", "/")
@@ -242,6 +259,20 @@ function parseParentTitleOption(options) {
242
259
  return parentTitleMode;
243
260
  }
244
261
 
262
+ function shouldCreateNavigationLink(options = {}, inferenceContext = {}) {
263
+ return shouldCreateGeneratedUiNavigationLink(options, {
264
+ dynamicRoutePolicy: "any",
265
+ routePath: inferenceContext?.routePath
266
+ });
267
+ }
268
+
269
+ function resolveNavigationRoleLinkPlacement(options = {}, inferenceContext = {}) {
270
+ return resolveGeneratedUiNavigationRoleLinkPlacement(options, {
271
+ dynamicRoutePolicy: "any",
272
+ routePath: inferenceContext?.routePath
273
+ });
274
+ }
275
+
245
276
  function validateDisplayFieldsForOperation(selectedFieldKeys, fields, operationName) {
246
277
  const selectedFields = Array.isArray(selectedFieldKeys) ? selectedFieldKeys : [];
247
278
  if (selectedFields.length < 1) {
@@ -303,6 +334,58 @@ function hasLookupFormFields(fields = []) {
303
334
  return (Array.isArray(fields) ? fields : []).some((field) => normalizeText(field?.component).toLowerCase() === "lookup");
304
335
  }
305
336
 
337
+ function hasLookupDisplayFields(fields = []) {
338
+ return (Array.isArray(fields) ? fields : []).some((field) => normalizeText(field?.relation?.kind).toLowerCase() === "lookup");
339
+ }
340
+
341
+ function buildListCardSlotProps(fields = []) {
342
+ const normalizedFields = Array.isArray(fields) ? fields : [];
343
+ if (normalizedFields.length < 1) {
344
+ return "{}";
345
+ }
346
+
347
+ const props = ["record"];
348
+ if (hasLookupDisplayFields(normalizedFields)) {
349
+ props.push("records");
350
+ }
351
+ props.push("formatListCardValue");
352
+ return `{ ${props.join(", ")} }`;
353
+ }
354
+
355
+ function buildListRowSlotProps(fields = []) {
356
+ const normalizedFields = Array.isArray(fields) ? fields : [];
357
+ if (normalizedFields.length < 1) {
358
+ return "{}";
359
+ }
360
+
361
+ const props = ["record"];
362
+ if (hasLookupDisplayFields(normalizedFields)) {
363
+ props.push("records");
364
+ }
365
+ return `{ ${props.join(", ")} }`;
366
+ }
367
+
368
+ function buildCrudFieldsSlotProps(fields = [], { includeMode = false } = {}) {
369
+ const props = [];
370
+ if (includeMode) {
371
+ props.push("mode");
372
+ }
373
+
374
+ props.push("formState", "addEdit", "resolveFieldErrors");
375
+ if (hasLookupFormFields(fields)) {
376
+ props.push(
377
+ "resolveLookupItems: fieldLookupItems",
378
+ "resolveLookupLoading: fieldLookupLoading",
379
+ "resolveLookupSearch: fieldLookupSearch",
380
+ "setLookupSearch: setFieldLookupSearch"
381
+ );
382
+ }
383
+
384
+ return `{
385
+ ${props.join(",\n ")}
386
+ }`;
387
+ }
388
+
306
389
  function buildLookupImportLine(fields = []) {
307
390
  return hasLookupFormFields(fields)
308
391
  ? 'import { createCrudLookupFieldRuntime } from "@jskit-ai/users-web/client/composables/crudLookupFieldRuntime";'
@@ -339,16 +422,17 @@ const {
339
422
  `;
340
423
  }
341
424
 
342
- function buildLookupFormProps(fields = []) {
425
+ function buildLookupFormProps(fields = [], { sourcePrefix = "" } = {}) {
343
426
  if (!hasLookupFormFields(fields)) {
344
427
  return "";
345
428
  }
346
429
 
430
+ const prefix = String(sourcePrefix || "");
347
431
  return `
348
- :resolve-lookup-items="resolveLookupItems"
349
- :resolve-lookup-loading="resolveLookupLoading"
350
- :resolve-lookup-search="resolveLookupSearch"
351
- :set-lookup-search="setLookupSearch"`;
432
+ :resolve-lookup-items="${prefix}resolveLookupItems"
433
+ :resolve-lookup-loading="${prefix}resolveLookupLoading"
434
+ :resolve-lookup-search="${prefix}resolveLookupSearch"
435
+ :set-lookup-search="${prefix}setLookupSearch"`;
352
436
  }
353
437
 
354
438
  function buildLookupFormPropDefinitions({ createFields = [], editFields = [] } = {}) {
@@ -468,6 +552,16 @@ function resolveTargetRootRelativeRoutePath(pageTarget = {}) {
468
552
  return visibleRouteSegments.length > 0 ? `/${visibleRouteSegments.join("/")}` : "/";
469
553
  }
470
554
 
555
+ function resolveNavigationInferenceRoutePath(pageTarget = {}) {
556
+ const visibleRouteSegments = Array.isArray(pageTarget?.visibleRouteSegments)
557
+ ? pageTarget.visibleRouteSegments.map((entry) => normalizeText(entry)).filter(Boolean)
558
+ : [];
559
+ if (visibleRouteSegments.length > 0) {
560
+ return `/${visibleRouteSegments.join("/")}`;
561
+ }
562
+ return String(pageTarget?.routeUrlSuffix || "");
563
+ }
564
+
471
565
  function resolveMenuToPropLine(linkTo = "") {
472
566
  if (!linkTo) {
473
567
  return "";
@@ -475,6 +569,52 @@ function resolveMenuToPropLine(linkTo = "") {
475
569
  return ` to: ${JSON.stringify(linkTo)},\n`;
476
570
  }
477
571
 
572
+ function resolveMenuOwnerLine(owner = "") {
573
+ const normalizedOwner = normalizeText(owner);
574
+ if (!normalizedOwner) {
575
+ return "";
576
+ }
577
+ return ` owner: ${JSON.stringify(normalizedOwner)},\n`;
578
+ }
579
+
580
+ function buildMenuAppendBlock({
581
+ hasListOperation = false,
582
+ menuMarker = "",
583
+ pageLinkTarget = null,
584
+ pageTarget = {}
585
+ } = {}) {
586
+ const placementId = String(pageLinkTarget?.pageTarget?.placementId || "");
587
+ const placementTarget = String(pageLinkTarget?.placementTarget?.id || "");
588
+ if (!hasListOperation || !placementId || !placementTarget) {
589
+ return "";
590
+ }
591
+
592
+ const surfaceId = String(pageTarget?.surfaceId || "");
593
+ const routeUrlSuffix = String(pageLinkTarget?.pageTarget?.routeUrlSuffix || "");
594
+ const menuToPropLine = resolveMenuToPropLine(pageLinkTarget?.linkTo || "");
595
+ const whenLine = String(pageLinkTarget?.whenLine || "");
596
+
597
+ return `
598
+ // ${menuMarker}
599
+ {
600
+ addPlacement({
601
+ id: ${JSON.stringify(placementId)},
602
+ target: ${JSON.stringify(placementTarget)},
603
+ ${resolveMenuOwnerLine(pageLinkTarget?.placementTarget?.owner || "")} kind: "link",
604
+ surfaces: [${JSON.stringify(surfaceId)}],
605
+ order: 155,
606
+ props: {
607
+ label: ${JSON.stringify(pageTarget?.defaultName || "")},
608
+ icon: ${JSON.stringify(DEFAULT_GENERATED_LINK_ICON)},
609
+ surface: ${JSON.stringify(surfaceId)},
610
+ scopedSuffix: ${JSON.stringify(routeUrlSuffix)},
611
+ unscopedSuffix: ${JSON.stringify(routeUrlSuffix)},
612
+ ${menuToPropLine} },
613
+ ${whenLine} });
614
+ }
615
+ `;
616
+ }
617
+
478
618
  function resolveCrudRelativePath(namespace = "") {
479
619
  return `/${requireCrudNamespace(namespace, {
480
620
  context: "crud-ui-generator resource namespace"
@@ -501,7 +641,7 @@ function buildListHeadingTitleSetup({
501
641
  }
502
642
 
503
643
  return `const parentTitle = useCrudListParentTitle({
504
- listRuntime: records,
644
+ listRuntime,
505
645
  resource: uiResource,
506
646
  adapter: UI_OPERATION_ADAPTER || undefined,
507
647
  recordIdParam: UI_RECORD_ID_PARAM,
@@ -643,14 +783,20 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
643
783
  const viewTitleFallbackFieldKey = hasViewOperation
644
784
  ? resolveViewTitleFallbackFieldKey(viewFieldsAll, { recordIdFieldKey })
645
785
  : "";
786
+ const listTitleFallbackFieldKey = hasListOperation
787
+ ? resolveViewTitleFallbackFieldKey(listFieldsAll, { recordIdFieldKey: listRecordIdFieldKey })
788
+ : "";
646
789
 
647
- const pageLinkTarget = hasListOperation
790
+ const pageLinkTarget = hasListOperation && shouldCreateNavigationLink(options, {
791
+ routePath: resolveNavigationInferenceRoutePath(pageTarget)
792
+ })
648
793
  ? await resolvePageLinkTargetDetails({
649
794
  appRoot,
650
795
  pageTarget,
651
796
  targetFile: listTargetFile,
652
- placement: options?.["link-placement"],
653
- componentToken: options?.["link-component-token"],
797
+ placement: resolveNavigationRoleLinkPlacement(options, {
798
+ routePath: resolveNavigationInferenceRoutePath(pageTarget)
799
+ }),
654
800
  context: "crud-ui-generator"
655
801
  })
656
802
  : null;
@@ -659,6 +805,8 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
659
805
  : "";
660
806
  const createFormColumns = buildFormColumns(createFields);
661
807
  const editFormColumns = buildFormColumns(editFields);
808
+ const sharedFormFields = Object.freeze([...createFields, ...editFields]);
809
+ const listCopy = buildCrudListCopy(resourceLabels);
662
810
 
663
811
  return {
664
812
  __JSKIT_UI_RESOURCE_IMPORT_PATH__: `/${normalizeRelativeAppPath(options?.["resource-file"])}`,
@@ -667,6 +815,11 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
667
815
  __JSKIT_UI_RESOURCE_NAMESPACE__: resourceNamespace,
668
816
  __JSKIT_UI_RESOURCE_SINGULAR_TITLE__: resourceLabels.singularTitle,
669
817
  __JSKIT_UI_RESOURCE_PLURAL_TITLE__: resourceLabels.pluralTitle,
818
+ __JSKIT_UI_LIST_LOAD_ERROR_TITLE__: listCopy.loadErrorTitle,
819
+ __JSKIT_UI_LIST_LOAD_ERROR_BODY__: listCopy.loadErrorBody,
820
+ __JSKIT_UI_LIST_EMPTY_TITLE__: listCopy.emptyTitle,
821
+ __JSKIT_UI_LIST_EMPTY_BODY__: listCopy.emptyBody,
822
+ __JSKIT_UI_LIST_CREATE_LABEL__: listCopy.createLabel,
670
823
  __JSKIT_UI_ROUTE_TITLE__: pageTarget.defaultName,
671
824
  __JSKIT_UI_PARENT_TITLE_MODE__: parentTitleMode,
672
825
  __JSKIT_UI_LIST_PARENT_TITLE_IMPORT_LINE__: buildListParentTitleImportLine(parentTitleMode),
@@ -680,6 +833,10 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
680
833
  __JSKIT_UI_SURFACE_ID__: pageTarget.surfaceId,
681
834
  __JSKIT_UI_LIST_HEADER_COLUMNS__: buildListHeaderColumns(listFields),
682
835
  __JSKIT_UI_LIST_ROW_COLUMNS__: buildListRowColumns(listFields),
836
+ __JSKIT_UI_LIST_CARD_FIELDS__: buildListCardFields(listFields),
837
+ __JSKIT_UI_LIST_CARD_SLOT_PROPS__: buildListCardSlotProps(listFields),
838
+ __JSKIT_UI_LIST_ROW_SLOT_PROPS__: buildListRowSlotProps(listFields),
839
+ __JSKIT_UI_LIST_TITLE_FALLBACK_FIELD_KEY__: JSON.stringify(listTitleFallbackFieldKey),
683
840
  __JSKIT_UI_LIST_REALTIME_EVENTS__: JSON.stringify(listRealtimeEvents),
684
841
  __JSKIT_UI_LIST_RECORD_ID_EXPR__: resolveRecordIdExpression(recordIdFields),
685
842
  __JSKIT_UI_VIEW_COLUMNS__: buildViewColumns(viewFields),
@@ -702,6 +859,9 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
702
859
  __JSKIT_UI_EDIT_FORM_COLUMNS__: editFormColumns,
703
860
  __JSKIT_UI_CREATE_FORM_COLUMNS_DIRECT__: rewriteGeneratedBlockIndent(createFormColumns, { trimPrefix: " " }),
704
861
  __JSKIT_UI_EDIT_FORM_COLUMNS_DIRECT__: rewriteGeneratedBlockIndent(editFormColumns, { trimPrefix: " " }),
862
+ __JSKIT_UI_CREATE_FORM_SLOT_PROPS__: buildCrudFieldsSlotProps(createFields),
863
+ __JSKIT_UI_EDIT_FORM_SLOT_PROPS__: buildCrudFieldsSlotProps(editFields),
864
+ __JSKIT_UI_SHARED_FORM_SLOT_PROPS__: buildCrudFieldsSlotProps(sharedFormFields, { includeMode: true }),
705
865
  __JSKIT_UI_CREATE_FORM_FIELDS__: JSON.stringify(createFields),
706
866
  __JSKIT_UI_EDIT_FORM_FIELDS__: JSON.stringify(editFields),
707
867
  __JSKIT_UI_CREATE_FORM_FIELD_PUSH_LINES__: renderObjectPushLines("UI_CREATE_FORM_FIELDS", createFields),
@@ -720,6 +880,7 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
720
880
  }),
721
881
  __JSKIT_UI_CREATE_LOOKUP_FORM_PROPS__: buildLookupFormProps(createFields),
722
882
  __JSKIT_UI_EDIT_LOOKUP_FORM_PROPS__: buildLookupFormProps(editFields),
883
+ __JSKIT_UI_SHARED_LOOKUP_FORM_PROPS__: buildLookupFormProps(sharedFormFields, { sourcePrefix: "props." }),
723
884
  __JSKIT_UI_FORM_LOOKUP_PROP_DEFS__: buildLookupFormPropDefinitions({
724
885
  createFields,
725
886
  editFields
@@ -727,13 +888,19 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
727
888
  __JSKIT_UI_MENU_MARKER__: menuMarker,
728
889
  __JSKIT_UI_MENU_PLACEMENT_ID__: String(pageLinkTarget?.pageTarget?.placementId || ""),
729
890
  __JSKIT_UI_MENU_PLACEMENT_TARGET__: String(pageLinkTarget?.placementTarget?.id || ""),
730
- __JSKIT_UI_MENU_COMPONENT_TOKEN__: String(pageLinkTarget?.componentToken || ""),
891
+ __JSKIT_UI_MENU_OWNER_LINE__: resolveMenuOwnerLine(pageLinkTarget?.placementTarget?.owner || ""),
731
892
  __JSKIT_UI_MENU_ICON__: DEFAULT_GENERATED_LINK_ICON,
732
893
  __JSKIT_UI_MENU_WORKSPACE_SUFFIX__: String(pageLinkTarget?.pageTarget?.routeUrlSuffix || ""),
733
894
  __JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__: String(pageLinkTarget?.pageTarget?.routeUrlSuffix || ""),
734
895
  __JSKIT_UI_MENU_WHEN_LINE__: String(pageLinkTarget?.whenLine || ""),
735
896
  __JSKIT_UI_MENU_TO_PROP_LINE__: resolveMenuToPropLine(pageLinkTarget?.linkTo || ""),
736
- __JSKIT_UI_MENU_LABEL__: pageTarget.defaultName
897
+ __JSKIT_UI_MENU_LABEL__: pageTarget.defaultName,
898
+ __JSKIT_UI_MENU_APPEND_BLOCK__: buildMenuAppendBlock({
899
+ hasListOperation,
900
+ menuMarker,
901
+ pageLinkTarget,
902
+ pageTarget
903
+ })
737
904
  };
738
905
  }
739
906
 
@@ -835,7 +835,7 @@ function renderTemplateJsStringLiteral(value) {
835
835
 
836
836
  function buildListHeaderColumns(fields = []) {
837
837
  return (Array.isArray(fields) ? fields : [])
838
- .map((field) => ` <th>${escapeHtml(field.label)}</th>`)
838
+ .map((field) => ` <th>${escapeHtml(field.label)}</th>`)
839
839
  .join("\n");
840
840
  }
841
841
 
@@ -888,10 +888,25 @@ function buildListRowColumns(fields = []) {
888
888
  return (Array.isArray(fields) ? fields : [])
889
889
  .map((field) => {
890
890
  if (isLookupField(field)) {
891
- return ` <td>{{ records.resolveFieldDisplay(record, ${toLookupDisplayFieldExpression(field)}) }}</td>`;
891
+ return ` <td>{{ records.resolveFieldDisplay(record, ${toLookupDisplayFieldExpression(field)}) }}</td>`;
892
892
  }
893
893
 
894
- return ` <td>{{ ${toAccessorExpression("record", field.key)} }}</td>`;
894
+ return ` <td>{{ ${toAccessorExpression("record", field.key)} }}</td>`;
895
+ })
896
+ .join("\n");
897
+ }
898
+
899
+ function buildListCardFields(fields = []) {
900
+ return (Array.isArray(fields) ? fields : [])
901
+ .map((field) => {
902
+ const valueExpression = isLookupField(field)
903
+ ? `records.resolveFieldDisplay(record, ${toLookupDisplayFieldExpression(field)})`
904
+ : toAccessorExpression("record", field.key);
905
+
906
+ return ` <div class="ui-generator-list-card__field">
907
+ <span class="ui-generator-list-card__field-label">${escapeHtml(field.label)}</span>
908
+ <span class="ui-generator-list-card__field-value">{{ formatListCardValue(${valueExpression}) }}</span>
909
+ </div>`;
895
910
  })
896
911
  .join("\n");
897
912
  }
@@ -903,10 +918,10 @@ function buildViewColumns(fields = []) {
903
918
  ? `view.resolveFieldDisplay(view.record, ${toLookupDisplayFieldExpression(field)})`
904
919
  : toOptionalAccessorExpression("view.record", field.key);
905
920
 
906
- return ` <v-col cols="12" md="6">
907
- <div class="text-caption text-medium-emphasis">${escapeHtml(field.label)}</div>
908
- <div class="text-body-1">{{ ${valueExpression} }}</div>
909
- </v-col>`;
921
+ return ` <v-col cols="12" md="6">
922
+ <div class="text-caption text-medium-emphasis">${escapeHtml(field.label)}</div>
923
+ <div class="text-body-1">{{ ${valueExpression} }}</div>
924
+ </v-col>`;
910
925
  })
911
926
  .join("\n");
912
927
  }
@@ -929,34 +944,34 @@ function buildFormColumns(fields = []) {
929
944
  const fieldErrorExpression = `resolveFieldErrors(${fieldKeyLiteral})`;
930
945
  const component = normalizeText(field?.component).toLowerCase();
931
946
  if (component === "switch") {
932
- return ` <v-col cols="12" md="6">
933
- <v-switch
934
- v-model="${formAccessor}"
935
- label="${label}"
936
- color="primary"
937
- hide-details="auto"
938
- :disabled="addEdit.isFieldLocked"
939
- :error-messages="${fieldErrorExpression}"
940
- />
941
- </v-col>`;
947
+ return ` <v-col cols="12" md="6">
948
+ <v-switch
949
+ v-model="${formAccessor}"
950
+ label="${label}"
951
+ color="primary"
952
+ hide-details="auto"
953
+ :disabled="addEdit.isFieldLocked"
954
+ :error-messages="${fieldErrorExpression}"
955
+ />
956
+ </v-col>`;
942
957
  }
943
958
 
944
959
  if (component === "select") {
945
960
  const selectOptions = Array.isArray(field?.options) ? field.options : [];
946
- return ` <v-col cols="12" md="6">
947
- <v-select
948
- v-model="${formAccessor}"
949
- label="${label}"
950
- variant="outlined"
951
- density="comfortable"
952
- :items="${serializeTemplateBindingValue(selectOptions)}"
953
- item-title="label"
954
- item-value="value"
955
- :disabled="addEdit.isFieldLocked"
956
- :clearable="${field.nullable === true ? "true" : "false"}"
957
- :error-messages="${fieldErrorExpression}"
958
- />
959
- </v-col>`;
961
+ return ` <v-col cols="12" md="6">
962
+ <v-select
963
+ v-model="${formAccessor}"
964
+ label="${label}"
965
+ variant="outlined"
966
+ density="comfortable"
967
+ :items="${serializeTemplateBindingValue(selectOptions)}"
968
+ item-title="label"
969
+ item-value="value"
970
+ :disabled="addEdit.isFieldLocked"
971
+ :clearable="${field.nullable === true ? "true" : "false"}"
972
+ :error-messages="${fieldErrorExpression}"
973
+ />
974
+ </v-col>`;
960
975
  }
961
976
 
962
977
  if (component === "lookup") {
@@ -964,53 +979,53 @@ function buildFormColumns(fields = []) {
964
979
  const useAutocomplete = lookupFormControl !== "select";
965
980
  const lookupComponentTag = useAutocomplete ? "v-autocomplete" : "v-select";
966
981
  const lookupAttributeLines = [
967
- ` :items="resolveLookupItems(${fieldKeyLiteral}, { selectedValue: ${formAccessor}, selectedRecord: addEdit.resource.data })"`
982
+ ` :items="fieldLookupItems(${fieldKeyLiteral}, { selectedValue: ${formAccessor}, selectedRecord: addEdit.resource.data })"`
968
983
  ];
969
984
  if (useAutocomplete) {
970
985
  lookupAttributeLines.push(
971
- ` :search="resolveLookupSearch(${fieldKeyLiteral})"`,
972
- ` @update:search="setLookupSearch(${fieldKeyLiteral}, $event)"`
986
+ ` :search="fieldLookupSearch(${fieldKeyLiteral})"`,
987
+ ` @update:search="setFieldLookupSearch(${fieldKeyLiteral}, $event)"`
973
988
  );
974
989
  }
975
990
  lookupAttributeLines.push(
976
- ` item-title="label"`,
977
- ` item-value="value"`
991
+ ` item-title="label"`,
992
+ ` item-value="value"`
978
993
  );
979
994
  if (useAutocomplete) {
980
- lookupAttributeLines.push(" no-filter");
995
+ lookupAttributeLines.push(" no-filter");
981
996
  }
982
- return ` <v-col cols="12" md="6">
983
- <${lookupComponentTag}
984
- v-model="${formAccessor}"
985
- label="${label}"
986
- variant="outlined"
987
- density="comfortable"
988
- autocomplete="off"
997
+ return ` <v-col cols="12" md="6">
998
+ <${lookupComponentTag}
999
+ v-model="${formAccessor}"
1000
+ label="${label}"
1001
+ variant="outlined"
1002
+ density="comfortable"
1003
+ autocomplete="off"
989
1004
  ${lookupAttributeLines.join("\n")}
990
- :loading="resolveLookupLoading(${fieldKeyLiteral})"
991
- :disabled="addEdit.isFieldLocked"
992
- :clearable="${field.nullable === true ? "true" : "false"}"
993
- :error-messages="${fieldErrorExpression}"
994
- />
995
- </v-col>`;
1005
+ :loading="fieldLookupLoading(${fieldKeyLiteral})"
1006
+ :disabled="addEdit.isFieldLocked"
1007
+ :clearable="${field.nullable === true ? "true" : "false"}"
1008
+ :error-messages="${fieldErrorExpression}"
1009
+ />
1010
+ </v-col>`;
996
1011
  }
997
1012
 
998
1013
  const inputType = normalizeText(field?.inputType) || "text";
999
1014
  const maxLength = Number.isInteger(field?.maxLength) && field.maxLength > 0
1000
1015
  ? String(field.maxLength)
1001
1016
  : "undefined";
1002
- return ` <v-col cols="12" md="6">
1003
- <v-text-field
1004
- v-model="${formAccessor}"
1005
- label="${label}"
1006
- type="${escapeHtml(inputType)}"
1007
- variant="outlined"
1008
- density="comfortable"
1009
- :maxlength="${maxLength}"
1010
- :readonly="addEdit.isFieldLocked"
1011
- :error-messages="${fieldErrorExpression}"
1012
- />
1013
- </v-col>`;
1017
+ return ` <v-col cols="12" md="6">
1018
+ <v-text-field
1019
+ v-model="${formAccessor}"
1020
+ label="${label}"
1021
+ type="${escapeHtml(inputType)}"
1022
+ variant="outlined"
1023
+ density="comfortable"
1024
+ :maxlength="${maxLength}"
1025
+ :readonly="addEdit.isFieldLocked"
1026
+ :error-messages="${fieldErrorExpression}"
1027
+ />
1028
+ </v-col>`;
1014
1029
  })
1015
1030
  .filter(Boolean)
1016
1031
  .join("\n");
@@ -1069,6 +1084,7 @@ export {
1069
1084
  resolveNearestParentRouteParamKey,
1070
1085
  buildListHeaderColumns,
1071
1086
  buildListRowColumns,
1087
+ buildListCardFields,
1072
1088
  buildViewColumns,
1073
1089
  buildFormColumns,
1074
1090
  resolveRecordIdFieldKey,