@jskit-ai/crud-ui-generator 0.1.31 → 0.1.32

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.31",
4
+ version: "0.1.32",
5
5
  kind: "generator",
6
6
  description: "Generate CRUD route trees from resource validators at an explicit route root relative to src/pages/.",
7
7
  options: {
@@ -168,7 +168,7 @@ export default Object.freeze({
168
168
  mutations: {
169
169
  dependencies: {
170
170
  runtime: {
171
- "@jskit-ai/users-web": "0.1.63"
171
+ "@jskit-ai/users-web": "0.1.64"
172
172
  },
173
173
  dev: {}
174
174
  },
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-ui-generator",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/crud-core": "0.1.56",
10
- "@jskit-ai/kernel": "0.1.48"
9
+ "@jskit-ai/crud-core": "0.1.57",
10
+ "@jskit-ai/kernel": "0.1.49"
11
11
  },
12
12
  "exports": {
13
13
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -258,6 +258,77 @@ function filterDisplayFields(selectedFieldKeys, fields) {
258
258
  });
259
259
  }
260
260
 
261
+ function rewriteGeneratedBlockIndent(source = "", { trimPrefix = "", addPrefix = "" } = {}) {
262
+ const text = String(source || "");
263
+ const trimmedPrefix = String(trimPrefix || "");
264
+ const extraPrefix = String(addPrefix || "");
265
+ if (!text) {
266
+ return "";
267
+ }
268
+
269
+ return text
270
+ .split("\n")
271
+ .map((line) => {
272
+ let nextLine = line;
273
+ if (trimmedPrefix && nextLine.startsWith(trimmedPrefix)) {
274
+ nextLine = nextLine.slice(trimmedPrefix.length);
275
+ }
276
+ return `${extraPrefix}${nextLine}`;
277
+ })
278
+ .join("\n");
279
+ }
280
+
281
+ function hasLookupFormFields(fields = []) {
282
+ return (Array.isArray(fields) ? fields : []).some((field) => normalizeText(field?.component).toLowerCase() === "lookup");
283
+ }
284
+
285
+ function buildLookupImportLine(fields = []) {
286
+ return hasLookupFormFields(fields)
287
+ ? 'import { createCrudLookupFieldRuntime } from "@jskit-ai/users-web/client/composables/crudLookupFieldRuntime";'
288
+ : "";
289
+ }
290
+
291
+ function buildLookupRuntimeSetup(fields = [], {
292
+ formFieldsVariable = "",
293
+ resourceNamespace = "",
294
+ mode = ""
295
+ } = {}) {
296
+ if (!hasLookupFormFields(fields)) {
297
+ return "";
298
+ }
299
+
300
+ const normalizedFormFieldsVariable = normalizeText(formFieldsVariable) || "UI_FORM_FIELDS";
301
+ const normalizedResourceNamespace = normalizeText(resourceNamespace) || "resource";
302
+ const normalizedMode = normalizeText(mode) || "new";
303
+
304
+ return `const lookupFieldRuntime = createCrudLookupFieldRuntime({
305
+ formFields: ${normalizedFormFieldsVariable},
306
+ adapter: UI_OPERATION_ADAPTER || undefined,
307
+ recordIdParam: UI_RECORD_ID_PARAM,
308
+ lookupContainerKey: uiResource?.contract?.lookup?.containerKey,
309
+ queryKeyPrefix: ["ui-generator", "${normalizedResourceNamespace}", "lookup", "${normalizedMode}"],
310
+ placementSourcePrefix: "ui-generator.${normalizedResourceNamespace}.${normalizedMode}.lookup"
311
+ });
312
+ const {
313
+ resolveLookupItems,
314
+ resolveLookupLoading,
315
+ resolveLookupSearch,
316
+ setLookupSearch
317
+ } = lookupFieldRuntime;
318
+ `;
319
+ }
320
+
321
+ function buildLookupFormProps(fields = []) {
322
+ if (!hasLookupFormFields(fields)) {
323
+ return "";
324
+ }
325
+
326
+ return ` :resolve-lookup-items="resolveLookupItems"
327
+ :resolve-lookup-loading="resolveLookupLoading"
328
+ :resolve-lookup-search="resolveLookupSearch"
329
+ :set-lookup-search="setLookupSearch"`;
330
+ }
331
+
261
332
  function filterDefaultHiddenListFields(selectedFieldKeys, fields, { recordIdFieldKey = "" } = {}) {
262
333
  const selectedFields = Array.isArray(selectedFieldKeys) ? selectedFieldKeys : [];
263
334
  const availableFields = Array.isArray(fields) ? fields : [];
@@ -485,6 +556,8 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
485
556
  const menuMarker = hasListOperation
486
557
  ? `jskit:crud-ui-generator.page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}`
487
558
  : "";
559
+ const createFormColumns = buildFormColumns(createFields);
560
+ const editFormColumns = buildFormColumns(editFields);
488
561
 
489
562
  return {
490
563
  __JSKIT_UI_RESOURCE_IMPORT_PATH__: `/${normalizeRelativeAppPath(options?.["resource-file"])}`,
@@ -508,12 +581,37 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
508
581
  __JSKIT_UI_HAS_VIEW_ROUTE__: hasViewOperation ? "true" : "false",
509
582
  __JSKIT_UI_HAS_NEW_ROUTE__: hasNewOperation ? "true" : "false",
510
583
  __JSKIT_UI_HAS_EDIT_ROUTE__: hasEditOperation ? "true" : "false",
511
- __JSKIT_UI_CREATE_FORM_COLUMNS__: buildFormColumns(createFields),
512
- __JSKIT_UI_EDIT_FORM_COLUMNS__: buildFormColumns(editFields),
584
+ __JSKIT_UI_LIST_PAGE_VIEW_URL__: JSON.stringify(hasViewOperation ? `./:${normalizeText(options?.["id-param"]) || "recordId"}` : ""),
585
+ __JSKIT_UI_LIST_PAGE_EDIT_URL__: JSON.stringify(hasEditOperation ? `./:${normalizeText(options?.["id-param"]) || "recordId"}/edit` : ""),
586
+ __JSKIT_UI_LIST_PAGE_NEW_URL__: JSON.stringify(hasNewOperation ? "./new" : ""),
587
+ __JSKIT_UI_NEW_PAGE_LIST_URL__: JSON.stringify(hasListOperation ? ".." : ""),
588
+ __JSKIT_UI_NEW_PAGE_VIEW_URL__: JSON.stringify(hasViewOperation ? `../:${normalizeText(options?.["id-param"]) || "recordId"}` : ""),
589
+ __JSKIT_UI_EDIT_PAGE_LIST_URL__: JSON.stringify(hasListOperation ? "../.." : ""),
590
+ __JSKIT_UI_EDIT_PAGE_VIEW_URL__: JSON.stringify(hasViewOperation ? ".." : ""),
591
+ __JSKIT_UI_VIEW_PAGE_LIST_URL__: JSON.stringify(hasListOperation ? ".." : ""),
592
+ __JSKIT_UI_VIEW_PAGE_EDIT_URL__: JSON.stringify(hasEditOperation ? "./edit" : ""),
593
+ __JSKIT_UI_CREATE_FORM_COLUMNS__: createFormColumns,
594
+ __JSKIT_UI_EDIT_FORM_COLUMNS__: editFormColumns,
595
+ __JSKIT_UI_CREATE_FORM_COLUMNS_DIRECT__: rewriteGeneratedBlockIndent(createFormColumns, { trimPrefix: " " }),
596
+ __JSKIT_UI_EDIT_FORM_COLUMNS_DIRECT__: rewriteGeneratedBlockIndent(editFormColumns, { trimPrefix: " " }),
513
597
  __JSKIT_UI_CREATE_FORM_FIELDS__: JSON.stringify(createFields),
514
598
  __JSKIT_UI_EDIT_FORM_FIELDS__: JSON.stringify(editFields),
515
599
  __JSKIT_UI_CREATE_FORM_FIELD_PUSH_LINES__: renderObjectPushLines("UI_CREATE_FORM_FIELDS", createFields),
516
600
  __JSKIT_UI_EDIT_FORM_FIELD_PUSH_LINES__: renderObjectPushLines("UI_EDIT_FORM_FIELDS", editFields),
601
+ __JSKIT_UI_CREATE_LOOKUP_IMPORT_LINE__: buildLookupImportLine(createFields),
602
+ __JSKIT_UI_EDIT_LOOKUP_IMPORT_LINE__: buildLookupImportLine(editFields),
603
+ __JSKIT_UI_CREATE_LOOKUP_RUNTIME_SETUP__: buildLookupRuntimeSetup(createFields, {
604
+ formFieldsVariable: "UI_CREATE_FORM_FIELDS",
605
+ resourceNamespace,
606
+ mode: "new"
607
+ }),
608
+ __JSKIT_UI_EDIT_LOOKUP_RUNTIME_SETUP__: buildLookupRuntimeSetup(editFields, {
609
+ formFieldsVariable: "UI_EDIT_FORM_FIELDS",
610
+ resourceNamespace,
611
+ mode: "edit"
612
+ }),
613
+ __JSKIT_UI_CREATE_LOOKUP_FORM_PROPS__: buildLookupFormProps(createFields),
614
+ __JSKIT_UI_EDIT_LOOKUP_FORM_PROPS__: buildLookupFormProps(editFields),
517
615
  __JSKIT_UI_MENU_MARKER__: menuMarker,
518
616
  __JSKIT_UI_MENU_PLACEMENT_ID__: String(pageLinkTarget?.pageTarget?.placementId || ""),
519
617
  __JSKIT_UI_MENU_PLACEMENT_TARGET__: String(pageLinkTarget?.placementTarget?.id || ""),
@@ -728,7 +728,18 @@ function escapeHtml(value) {
728
728
  }
729
729
 
730
730
  function serializeTemplateBindingValue(value) {
731
- return JSON.stringify(value).replaceAll("'", "\\u0027");
731
+ return JSON.stringify(value)
732
+ .replaceAll("&", "&")
733
+ .replaceAll('"', """)
734
+ .replaceAll("<", "&lt;")
735
+ .replaceAll(">", "&gt;")
736
+ .replaceAll("'", "\\u0027");
737
+ }
738
+
739
+ function renderTemplateJsStringLiteral(value) {
740
+ return `'${String(value || "")
741
+ .replaceAll("\\", "\\\\")
742
+ .replaceAll("'", "\\'")}'`;
732
743
  }
733
744
 
734
745
  function buildListHeaderColumns(fields = []) {
@@ -822,84 +833,93 @@ function buildFormColumns(fields = []) {
822
833
  }
823
834
 
824
835
  const label = escapeHtml(field?.label || toFieldLabel(key));
825
- const formAccessor = toAccessorExpression("formRuntime.form", key);
826
- const fieldErrorExpression = `formRuntime.resolveFieldErrors(${JSON.stringify(key)})`;
836
+ const formAccessor = toAccessorExpression("formState", key);
837
+ const fieldKeyLiteral = renderTemplateJsStringLiteral(key);
838
+ const fieldErrorExpression = `resolveFieldErrors(${fieldKeyLiteral})`;
827
839
  const component = normalizeText(field?.component).toLowerCase();
828
840
  if (component === "switch") {
829
- return ` <v-col cols="12" md="6">
830
- <v-switch
831
- v-model="${formAccessor}"
832
- label="${label}"
833
- color="primary"
834
- hide-details="auto"
835
- :disabled="formRuntime.addEdit.isFieldLocked"
836
- :error-messages='${fieldErrorExpression}'
837
- />
838
- </v-col>`;
841
+ return ` <v-col cols="12" md="6">
842
+ <v-switch
843
+ v-model="${formAccessor}"
844
+ label="${label}"
845
+ color="primary"
846
+ hide-details="auto"
847
+ :disabled="addEdit.isFieldLocked"
848
+ :error-messages="${fieldErrorExpression}"
849
+ />
850
+ </v-col>`;
839
851
  }
840
852
 
841
853
  if (component === "select") {
842
854
  const selectOptions = Array.isArray(field?.options) ? field.options : [];
843
- return ` <v-col cols="12" md="6">
844
- <v-select
845
- v-model="${formAccessor}"
846
- label="${label}"
847
- variant="outlined"
848
- density="comfortable"
849
- :items='${serializeTemplateBindingValue(selectOptions)}'
850
- item-title="label"
851
- item-value="value"
852
- :disabled="formRuntime.addEdit.isFieldLocked"
853
- :clearable="${field.nullable === true ? "true" : "false"}"
854
- :error-messages='${fieldErrorExpression}'
855
- />
856
- </v-col>`;
855
+ return ` <v-col cols="12" md="6">
856
+ <v-select
857
+ v-model="${formAccessor}"
858
+ label="${label}"
859
+ variant="outlined"
860
+ density="comfortable"
861
+ :items="${serializeTemplateBindingValue(selectOptions)}"
862
+ item-title="label"
863
+ item-value="value"
864
+ :disabled="addEdit.isFieldLocked"
865
+ :clearable="${field.nullable === true ? "true" : "false"}"
866
+ :error-messages="${fieldErrorExpression}"
867
+ />
868
+ </v-col>`;
857
869
  }
858
870
 
859
871
  if (component === "lookup") {
860
872
  const lookupFormControl = field?.lookupFormControl === "select" ? "select" : "autocomplete";
861
873
  const useAutocomplete = lookupFormControl !== "select";
862
874
  const lookupComponentTag = useAutocomplete ? "v-autocomplete" : "v-select";
863
- const lookupSearchBindings = useAutocomplete
864
- ? `\n :search='resolveLookupSearch(${JSON.stringify(key)})'\n @update:search='setLookupSearch(${JSON.stringify(key)}, $event)'`
865
- : "";
866
- const lookupNoFilterLine = useAutocomplete ? "\n no-filter" : "";
867
- return ` <v-col cols="12" md="6">
868
- <${lookupComponentTag}
869
- v-model="${formAccessor}"
870
- label="${label}"
871
- variant="outlined"
872
- density="comfortable"
873
- autocomplete="off"
874
- :items='resolveLookupItems(${JSON.stringify(key)}, { selectedValue: ${formAccessor}, selectedRecord: formRuntime.addEdit.resource.data })'
875
- ${lookupSearchBindings}
876
- item-title="label"
877
- item-value="value"
878
- ${lookupNoFilterLine}
879
- :loading='resolveLookupLoading(${JSON.stringify(key)})'
880
- :disabled="formRuntime.addEdit.isFieldLocked"
881
- :clearable="${field.nullable === true ? "true" : "false"}"
882
- :error-messages='${fieldErrorExpression}'
883
- />
884
- </v-col>`;
875
+ const lookupAttributeLines = [
876
+ ` :items="resolveLookupItems(${fieldKeyLiteral}, { selectedValue: ${formAccessor}, selectedRecord: addEdit.resource.data })"`
877
+ ];
878
+ if (useAutocomplete) {
879
+ lookupAttributeLines.push(
880
+ ` :search="resolveLookupSearch(${fieldKeyLiteral})"`,
881
+ ` @update:search="setLookupSearch(${fieldKeyLiteral}, $event)"`
882
+ );
883
+ }
884
+ lookupAttributeLines.push(
885
+ ` item-title="label"`,
886
+ ` item-value="value"`
887
+ );
888
+ if (useAutocomplete) {
889
+ lookupAttributeLines.push(" no-filter");
890
+ }
891
+ return ` <v-col cols="12" md="6">
892
+ <${lookupComponentTag}
893
+ v-model="${formAccessor}"
894
+ label="${label}"
895
+ variant="outlined"
896
+ density="comfortable"
897
+ autocomplete="off"
898
+ ${lookupAttributeLines.join("\n")}
899
+ :loading="resolveLookupLoading(${fieldKeyLiteral})"
900
+ :disabled="addEdit.isFieldLocked"
901
+ :clearable="${field.nullable === true ? "true" : "false"}"
902
+ :error-messages="${fieldErrorExpression}"
903
+ />
904
+ </v-col>`;
885
905
  }
886
906
 
887
907
  const inputType = normalizeText(field?.inputType) || "text";
888
908
  const maxLength = Number.isInteger(field?.maxLength) && field.maxLength > 0
889
909
  ? String(field.maxLength)
890
910
  : "undefined";
891
- return ` <v-col cols="12" md="6">
892
- <v-text-field
893
- v-model="${formAccessor}"
894
- label="${label}"
895
- type="${escapeHtml(inputType)}"
896
- variant="outlined"
897
- density="comfortable"
898
- :maxlength="${maxLength}"
899
- :readonly="formRuntime.addEdit.isFieldLocked"
900
- :error-messages='${fieldErrorExpression}'
901
- />
902
- </v-col>`;
911
+ return ` <v-col cols="12" md="6">
912
+ <v-text-field
913
+ v-model="${formAccessor}"
914
+ label="${label}"
915
+ type="${escapeHtml(inputType)}"
916
+ variant="outlined"
917
+ density="comfortable"
918
+ :maxlength="${maxLength}"
919
+ :readonly="addEdit.isFieldLocked"
920
+ :error-messages="${fieldErrorExpression}"
921
+ />
922
+ </v-col>`;
903
923
  })
904
924
  .filter(Boolean)
905
925
  .join("\n");
@@ -12,9 +12,9 @@
12
12
  <v-btn v-if="cancelTo" variant="tonal" :to="resolveCancelTo(cancelTo)">Cancel</v-btn>
13
13
  <v-btn
14
14
  color="primary"
15
- :loading="formRuntime.addEdit.isSaving"
16
- :disabled="formRuntime.addEdit.isSubmitDisabled"
17
- @click="formRuntime.addEdit.submit"
15
+ :loading="addEdit.isSaving"
16
+ :disabled="addEdit.isSubmitDisabled"
17
+ @click="addEdit.submit"
18
18
  >
19
19
  {{ saveLabel }}
20
20
  </v-btn>
@@ -23,14 +23,14 @@
23
23
  </v-card-item>
24
24
 
25
25
  <v-card-text class="pt-0">
26
- <p v-if="formRuntime.addEdit.loadError" class="text-body-2 text-medium-emphasis mb-0">
27
- {{ formRuntime.addEdit.loadError }}
26
+ <p v-if="addEdit.loadError" class="text-body-2 text-medium-emphasis mb-0">
27
+ {{ addEdit.loadError }}
28
28
  </p>
29
29
  <template v-else-if="formRuntime.showFormSkeleton">
30
30
  <v-skeleton-loader type="heading, text@2, article" />
31
31
  </template>
32
- <v-form v-else @submit.prevent="formRuntime.addEdit.submit" novalidate>
33
- <v-progress-linear v-if="formRuntime.addEdit.isRefetching" indeterminate class="mb-4" />
32
+ <v-form v-else @submit.prevent="addEdit.submit" novalidate>
33
+ <v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
34
34
  <v-row>
35
35
  <template v-if="mode === 'new'">
36
36
  <!-- jskit:crud-ui-fields:new -->
@@ -91,6 +91,14 @@ const props = defineProps({
91
91
  }
92
92
  });
93
93
 
94
+ const formRuntime = props.formRuntime;
95
+ const addEdit = formRuntime.addEdit;
96
+ const formState = formRuntime.form;
97
+
98
+ function resolveFieldErrors(fieldKey) {
99
+ return formRuntime.resolveFieldErrors(fieldKey);
100
+ }
101
+
94
102
  function resolveCancelTo(target) {
95
103
  if (!target) {
96
104
  return "";
@@ -37,7 +37,7 @@
37
37
  <v-progress-linear v-if="formRuntime.addEdit.isRefetching" indeterminate class="mb-4" />
38
38
  <v-row>
39
39
  <!-- jskit:crud-ui-fields:edit -->
40
- __JSKIT_UI_EDIT_FORM_COLUMNS__
40
+ __JSKIT_UI_EDIT_FORM_COLUMNS_DIRECT__
41
41
  </v-row>
42
42
  </v-form>
43
43
  </v-card-text>
@@ -49,15 +49,15 @@ __JSKIT_UI_EDIT_FORM_COLUMNS__
49
49
  import { computed } from "vue";
50
50
  import { useRoute } from "vue-router";
51
51
  import { useCrudAddEdit } from "@jskit-ai/users-web/client/composables/useCrudAddEdit";
52
- import { createCrudLookupFieldRuntime } from "@jskit-ai/users-web/client/composables/crudLookupFieldRuntime";
52
+ __JSKIT_UI_EDIT_LOOKUP_IMPORT_LINE__
53
53
  import { resource as uiResource } from "__JSKIT_UI_RESOURCE_IMPORT_PATH__";
54
54
 
55
55
  const UI_OPERATION_ADAPTER = null;
56
56
  const UI_RECORD_ID_PARAM = "__JSKIT_UI_RECORD_ID_PARAM__";
57
57
  const UI_API_BASE_URL = "__JSKIT_UI_API_BASE_URL__";
58
58
  const UI_EDIT_API_URL = `${UI_API_BASE_URL}/:${UI_RECORD_ID_PARAM}`;
59
- const UI_LIST_URL = __JSKIT_UI_HAS_LIST_ROUTE__ ? "../.." : "";
60
- const UI_VIEW_URL = __JSKIT_UI_HAS_VIEW_ROUTE__ ? ".." : "";
59
+ const UI_LIST_URL = __JSKIT_UI_EDIT_PAGE_LIST_URL__;
60
+ const UI_VIEW_URL = __JSKIT_UI_EDIT_PAGE_VIEW_URL__;
61
61
  const UI_CANCEL_URL = UI_VIEW_URL || UI_LIST_URL;
62
62
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
63
63
  const UI_EDIT_FORM_FIELDS = [];
@@ -79,20 +79,7 @@ const routeRecordId = computed(() => {
79
79
  return String(source ?? "").trim();
80
80
  });
81
81
 
82
- const lookupFieldRuntime = createCrudLookupFieldRuntime({
83
- formFields: UI_EDIT_FORM_FIELDS,
84
- adapter: UI_OPERATION_ADAPTER || undefined,
85
- recordIdParam: UI_RECORD_ID_PARAM,
86
- lookupContainerKey: uiResource?.contract?.lookup?.containerKey,
87
- queryKeyPrefix: ["ui-generator", "__JSKIT_UI_RESOURCE_NAMESPACE__", "lookup", "edit"],
88
- placementSourcePrefix: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.edit.lookup"
89
- });
90
- const {
91
- resolveLookupItems,
92
- resolveLookupLoading,
93
- resolveLookupSearch,
94
- setLookupSearch
95
- } = lookupFieldRuntime;
82
+ __JSKIT_UI_EDIT_LOOKUP_RUNTIME_SETUP__
96
83
 
97
84
  const formRuntime = useCrudAddEdit({
98
85
  resource: uiResource,
@@ -128,4 +115,10 @@ const formRuntime = useCrudAddEdit({
128
115
  listUrlTemplate: UI_LIST_URL
129
116
  }
130
117
  });
118
+ const addEdit = formRuntime.addEdit;
119
+ const formState = formRuntime.form;
120
+
121
+ function resolveFieldErrors(fieldKey) {
122
+ return formRuntime.resolveFieldErrors(fieldKey);
123
+ }
131
124
  </script>
@@ -6,10 +6,7 @@
6
6
  subtitle="Update the selected __JSKIT_UI_RESOURCE_SINGULAR_TITLE__."
7
7
  save-label="Save changes"
8
8
  :cancel-to="cancelTo"
9
- :resolve-lookup-items="resolveLookupItems"
10
- :resolve-lookup-loading="resolveLookupLoading"
11
- :resolve-lookup-search="resolveLookupSearch"
12
- :set-lookup-search="setLookupSearch"
9
+ __JSKIT_UI_EDIT_LOOKUP_FORM_PROPS__
13
10
  />
14
11
  </template>
15
12
 
@@ -17,7 +14,7 @@
17
14
  import { computed } from "vue";
18
15
  import { useRoute } from "vue-router";
19
16
  import { useCrudAddEdit } from "@jskit-ai/users-web/client/composables/useCrudAddEdit";
20
- import { createCrudLookupFieldRuntime } from "@jskit-ai/users-web/client/composables/crudLookupFieldRuntime";
17
+ __JSKIT_UI_EDIT_LOOKUP_IMPORT_LINE__
21
18
  import { resource as uiResource } from "__JSKIT_UI_RESOURCE_IMPORT_PATH__";
22
19
  import CrudAddEditForm from "../_components/__JSKIT_UI_FORM_COMPONENT_FILE__";
23
20
  import { UI_EDIT_FORM_FIELDS } from "../_components/__JSKIT_UI_FORM_FIELDS_FILE__";
@@ -26,8 +23,8 @@ const UI_OPERATION_ADAPTER = null;
26
23
  const UI_RECORD_ID_PARAM = "__JSKIT_UI_RECORD_ID_PARAM__";
27
24
  const UI_API_BASE_URL = "__JSKIT_UI_API_BASE_URL__";
28
25
  const UI_EDIT_API_URL = `${UI_API_BASE_URL}/:${UI_RECORD_ID_PARAM}`;
29
- const UI_LIST_URL = __JSKIT_UI_HAS_LIST_ROUTE__ ? "../.." : "";
30
- const UI_VIEW_URL = __JSKIT_UI_HAS_VIEW_ROUTE__ ? ".." : "";
26
+ const UI_LIST_URL = __JSKIT_UI_EDIT_PAGE_LIST_URL__;
27
+ const UI_VIEW_URL = __JSKIT_UI_EDIT_PAGE_VIEW_URL__;
31
28
  const UI_CANCEL_URL = UI_VIEW_URL || UI_LIST_URL;
32
29
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
33
30
  const route = useRoute();
@@ -44,20 +41,7 @@ const routeRecordId = computed(() => {
44
41
  return String(source ?? "").trim();
45
42
  });
46
43
 
47
- const lookupFieldRuntime = createCrudLookupFieldRuntime({
48
- formFields: UI_EDIT_FORM_FIELDS,
49
- adapter: UI_OPERATION_ADAPTER || undefined,
50
- recordIdParam: UI_RECORD_ID_PARAM,
51
- lookupContainerKey: uiResource?.contract?.lookup?.containerKey,
52
- queryKeyPrefix: ["ui-generator", "__JSKIT_UI_RESOURCE_NAMESPACE__", "lookup", "edit"],
53
- placementSourcePrefix: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.edit.lookup"
54
- });
55
- const {
56
- resolveLookupItems,
57
- resolveLookupLoading,
58
- resolveLookupSearch,
59
- setLookupSearch
60
- } = lookupFieldRuntime;
44
+ __JSKIT_UI_EDIT_LOOKUP_RUNTIME_SETUP__
61
45
 
62
46
  const formRuntime = useCrudAddEdit({
63
47
  resource: uiResource,
@@ -40,8 +40,8 @@
40
40
  <tr>
41
41
  __JSKIT_UI_LIST_HEADER_COLUMNS__
42
42
  <!-- jskit:crud-ui-fields:list-header -->
43
- <th v-if="UI_VIEW_URL" class="text-right"></th>
44
- <th v-if="UI_EDIT_URL" class="text-right"></th>
43
+ <th v-if="UI_VIEW_URL" class="text-right" />
44
+ <th v-if="UI_EDIT_URL" class="text-right" />
45
45
  </tr>
46
46
  </thead>
47
47
  <tbody>
@@ -90,9 +90,9 @@ import { resource as uiResource } from "__JSKIT_UI_RESOURCE_IMPORT_PATH__";
90
90
  const UI_OPERATION_ADAPTER = null;
91
91
  const UI_RECORD_ID_PARAM = "__JSKIT_UI_RECORD_ID_PARAM__";
92
92
  const UI_LIST_API_URL = "__JSKIT_UI_API_BASE_URL__";
93
- const UI_VIEW_URL = __JSKIT_UI_HAS_VIEW_ROUTE__ ? `./:${UI_RECORD_ID_PARAM}` : "";
94
- const UI_EDIT_URL = __JSKIT_UI_HAS_EDIT_ROUTE__ ? `./:${UI_RECORD_ID_PARAM}/edit` : "";
95
- const UI_NEW_URL = __JSKIT_UI_HAS_NEW_ROUTE__ ? "./new" : "";
93
+ const UI_VIEW_URL = __JSKIT_UI_LIST_PAGE_VIEW_URL__;
94
+ const UI_EDIT_URL = __JSKIT_UI_LIST_PAGE_EDIT_URL__;
95
+ const UI_NEW_URL = __JSKIT_UI_LIST_PAGE_NEW_URL__;
96
96
  const UI_RECORD_CHANGED_EVENTS = __JSKIT_UI_LIST_REALTIME_EVENTS__;
97
97
  const UI_ROUTE_QUERY_BLACKLIST = Object.freeze(["include", "cursor", "limit"]);
98
98
 
@@ -27,7 +27,7 @@
27
27
  <v-form v-else @submit.prevent="formRuntime.addEdit.submit" novalidate>
28
28
  <v-row>
29
29
  <!-- jskit:crud-ui-fields:new -->
30
- __JSKIT_UI_CREATE_FORM_COLUMNS__
30
+ __JSKIT_UI_CREATE_FORM_COLUMNS_DIRECT__
31
31
  </v-row>
32
32
  </v-form>
33
33
  </v-card-text>
@@ -37,14 +37,14 @@ __JSKIT_UI_CREATE_FORM_COLUMNS__
37
37
 
38
38
  <script setup>
39
39
  import { useCrudAddEdit } from "@jskit-ai/users-web/client/composables/useCrudAddEdit";
40
- import { createCrudLookupFieldRuntime } from "@jskit-ai/users-web/client/composables/crudLookupFieldRuntime";
40
+ __JSKIT_UI_CREATE_LOOKUP_IMPORT_LINE__
41
41
  import { resource as uiResource } from "__JSKIT_UI_RESOURCE_IMPORT_PATH__";
42
42
 
43
43
  const UI_OPERATION_ADAPTER = null;
44
44
  const UI_RECORD_ID_PARAM = "__JSKIT_UI_RECORD_ID_PARAM__";
45
45
  const UI_CREATE_API_URL = "__JSKIT_UI_API_BASE_URL__";
46
- const UI_LIST_URL = __JSKIT_UI_HAS_LIST_ROUTE__ ? ".." : "";
47
- const UI_VIEW_URL = __JSKIT_UI_HAS_VIEW_ROUTE__ ? `../:${UI_RECORD_ID_PARAM}` : "";
46
+ const UI_LIST_URL = __JSKIT_UI_NEW_PAGE_LIST_URL__;
47
+ const UI_VIEW_URL = __JSKIT_UI_NEW_PAGE_VIEW_URL__;
48
48
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
49
49
  const UI_CREATE_FORM_FIELDS = [];
50
50
 
@@ -54,20 +54,7 @@ void UI_CREATE_FORM_FIELDS;
54
54
  __JSKIT_UI_CREATE_FORM_FIELD_PUSH_LINES__
55
55
  Object.freeze(UI_CREATE_FORM_FIELDS);
56
56
 
57
- const lookupFieldRuntime = createCrudLookupFieldRuntime({
58
- formFields: UI_CREATE_FORM_FIELDS,
59
- adapter: UI_OPERATION_ADAPTER || undefined,
60
- recordIdParam: UI_RECORD_ID_PARAM,
61
- lookupContainerKey: uiResource?.contract?.lookup?.containerKey,
62
- queryKeyPrefix: ["ui-generator", "__JSKIT_UI_RESOURCE_NAMESPACE__", "lookup", "new"],
63
- placementSourcePrefix: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.new.lookup"
64
- });
65
- const {
66
- resolveLookupItems,
67
- resolveLookupLoading,
68
- resolveLookupSearch,
69
- setLookupSearch
70
- } = lookupFieldRuntime;
57
+ __JSKIT_UI_CREATE_LOOKUP_RUNTIME_SETUP__
71
58
 
72
59
  const formRuntime = useCrudAddEdit({
73
60
  resource: uiResource,
@@ -101,4 +88,10 @@ const formRuntime = useCrudAddEdit({
101
88
  listUrlTemplate: UI_LIST_URL
102
89
  }
103
90
  });
91
+ const addEdit = formRuntime.addEdit;
92
+ const formState = formRuntime.form;
93
+
94
+ function resolveFieldErrors(fieldKey) {
95
+ return formRuntime.resolveFieldErrors(fieldKey);
96
+ }
104
97
  </script>
@@ -6,16 +6,13 @@
6
6
  subtitle="Create a new __JSKIT_UI_RESOURCE_SINGULAR_TITLE__."
7
7
  save-label="Save __JSKIT_UI_RESOURCE_SINGULAR_TITLE__"
8
8
  :cancel-to="UI_CANCEL_URL"
9
- :resolve-lookup-items="resolveLookupItems"
10
- :resolve-lookup-loading="resolveLookupLoading"
11
- :resolve-lookup-search="resolveLookupSearch"
12
- :set-lookup-search="setLookupSearch"
9
+ __JSKIT_UI_CREATE_LOOKUP_FORM_PROPS__
13
10
  />
14
11
  </template>
15
12
 
16
13
  <script setup>
17
14
  import { useCrudAddEdit } from "@jskit-ai/users-web/client/composables/useCrudAddEdit";
18
- import { createCrudLookupFieldRuntime } from "@jskit-ai/users-web/client/composables/crudLookupFieldRuntime";
15
+ __JSKIT_UI_CREATE_LOOKUP_IMPORT_LINE__
19
16
  import { resource as uiResource } from "__JSKIT_UI_RESOURCE_IMPORT_PATH__";
20
17
  import CrudAddEditForm from "./_components/__JSKIT_UI_FORM_COMPONENT_FILE__";
21
18
  import { UI_CREATE_FORM_FIELDS } from "./_components/__JSKIT_UI_FORM_FIELDS_FILE__";
@@ -23,28 +20,15 @@ import { UI_CREATE_FORM_FIELDS } from "./_components/__JSKIT_UI_FORM_FIELDS_FILE
23
20
  const UI_OPERATION_ADAPTER = null;
24
21
  const UI_RECORD_ID_PARAM = "__JSKIT_UI_RECORD_ID_PARAM__";
25
22
  const UI_CREATE_API_URL = "__JSKIT_UI_API_BASE_URL__";
26
- const UI_LIST_URL = __JSKIT_UI_HAS_LIST_ROUTE__ ? ".." : "";
27
- const UI_VIEW_URL = __JSKIT_UI_HAS_VIEW_ROUTE__ ? `../:${UI_RECORD_ID_PARAM}` : "";
23
+ const UI_LIST_URL = __JSKIT_UI_NEW_PAGE_LIST_URL__;
24
+ const UI_VIEW_URL = __JSKIT_UI_NEW_PAGE_VIEW_URL__;
28
25
  const UI_CANCEL_URL = UI_LIST_URL;
29
26
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
30
27
 
31
28
  // jskit:crud-ui-fields-target ./_components/__JSKIT_UI_FORM_COMPONENT_FILE__
32
29
  // jskit:crud-ui-form-fields-target ./_components/__JSKIT_UI_FORM_FIELDS_FILE__
33
30
 
34
- const lookupFieldRuntime = createCrudLookupFieldRuntime({
35
- formFields: UI_CREATE_FORM_FIELDS,
36
- adapter: UI_OPERATION_ADAPTER || undefined,
37
- recordIdParam: UI_RECORD_ID_PARAM,
38
- lookupContainerKey: uiResource?.contract?.lookup?.containerKey,
39
- queryKeyPrefix: ["ui-generator", "__JSKIT_UI_RESOURCE_NAMESPACE__", "lookup", "new"],
40
- placementSourcePrefix: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.new.lookup"
41
- });
42
- const {
43
- resolveLookupItems,
44
- resolveLookupLoading,
45
- resolveLookupSearch,
46
- setLookupSearch
47
- } = lookupFieldRuntime;
31
+ __JSKIT_UI_CREATE_LOOKUP_RUNTIME_SETUP__
48
32
 
49
33
  const formRuntime = useCrudAddEdit({
50
34
  resource: uiResource,
@@ -61,8 +61,8 @@ const UI_OPERATION_ADAPTER = null;
61
61
  const UI_RECORD_ID_PARAM = "__JSKIT_UI_RECORD_ID_PARAM__";
62
62
  const UI_API_BASE_URL = "__JSKIT_UI_API_BASE_URL__";
63
63
  const UI_VIEW_API_URL = `${UI_API_BASE_URL}/:${UI_RECORD_ID_PARAM}`;
64
- const UI_LIST_URL = __JSKIT_UI_HAS_LIST_ROUTE__ ? ".." : "";
65
- const UI_EDIT_URL = __JSKIT_UI_HAS_EDIT_ROUTE__ ? "./edit" : "";
64
+ const UI_LIST_URL = __JSKIT_UI_VIEW_PAGE_LIST_URL__;
65
+ const UI_EDIT_URL = __JSKIT_UI_VIEW_PAGE_EDIT_URL__;
66
66
  const UI_VIEW_TITLE_FALLBACK_FIELD_KEY = __JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__;
67
67
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
68
68
 
@@ -130,12 +130,16 @@ UI_EDIT_FORM_FIELDS.push({ key: "firstName", component: "text" });
130
130
  const editSource = await readFile(path.join(appRoot, editFile), "utf8");
131
131
  assert.match(
132
132
  editSource,
133
- /<v-autocomplete[\s\S]*resolveLookupItems\("vetId", \{ selectedValue: formRuntime\.form\.vetId, selectedRecord: formRuntime\.addEdit\.resource\.data \}\)/
133
+ /<v-autocomplete[\s\S]*resolveLookupItems\('vetId', \{ selectedValue: formState\.vetId, selectedRecord: addEdit\.resource\.data \}\)/
134
134
  );
135
- assert.match(editSource, /:items='resolveLookupItems\("vetId", \{ selectedValue: formRuntime\.form\.vetId, selectedRecord: formRuntime\.addEdit\.resource\.data \}\)'/);
136
- assert.match(editSource, /:search='resolveLookupSearch\("vetId"\)'/);
137
- assert.match(editSource, /@update:search='setLookupSearch\("vetId", \$event\)'/);
138
- assert.match(editSource, /:loading='resolveLookupLoading\("vetId"\)'/);
135
+ assert.match(editSource, /:items="resolveLookupItems\('vetId', \{ selectedValue: formState\.vetId, selectedRecord: addEdit\.resource\.data \}\)"/);
136
+ assert.match(
137
+ editSource,
138
+ /\n {18}:items="resolveLookupItems\('vetId', \{ selectedValue: formState\.vetId, selectedRecord: addEdit\.resource\.data \}\)"\n {18}:search="resolveLookupSearch\('vetId'\)"\n {18}@update:search="setLookupSearch\('vetId', \$event\)"\n {18}item-title="label"\n {18}item-value="value"\n {18}no-filter/
139
+ );
140
+ assert.match(editSource, /:search="resolveLookupSearch\('vetId'\)"/);
141
+ assert.match(editSource, /@update:search="setLookupSearch\('vetId', \$event\)"/);
142
+ assert.match(editSource, /:loading="resolveLookupLoading\('vetId'\)"/);
139
143
  assert.match(editSource, /UI_EDIT_FORM_FIELDS\.push\(\{[\s\S]*"key": "vetId"/);
140
144
 
141
145
  const second = await runGeneratorSubcommand({
@@ -200,7 +204,7 @@ UI_EDIT_FORM_FIELDS.push({ key: "firstName", component: "text" });
200
204
  const addEditFormSource = await readFile(path.join(appRoot, addEditFormFile), "utf8");
201
205
  assert.match(
202
206
  addEditFormSource,
203
- /<v-autocomplete[\s\S]*resolveLookupItems\("vetId", \{ selectedValue: formRuntime\.form\.vetId, selectedRecord: formRuntime\.addEdit\.resource\.data \}\)/
207
+ /<v-autocomplete[\s\S]*resolveLookupItems\('vetId', \{ selectedValue: formState\.vetId, selectedRecord: addEdit\.resource\.data \}\)/
204
208
  );
205
209
 
206
210
  const addEditFieldsSource = await readFile(path.join(appRoot, addEditFieldsFile), "utf8");
@@ -282,7 +286,7 @@ const UI_EDIT_FORM_FIELDS = [];
282
286
  });
283
287
 
284
288
  const addEditFormSource = await readFile(path.join(appRoot, addEditFormFile), "utf8");
285
- assert.equal((addEditFormSource.match(/resolveLookupItems\("vetId"/g) || []).length, 2);
289
+ assert.equal((addEditFormSource.match(/resolveLookupItems\('vetId'/g) || []).length, 2);
286
290
 
287
291
  const addEditFieldsSource = await readFile(path.join(appRoot, addEditFieldsFile), "utf8");
288
292
  assert.match(addEditFieldsSource, /UI_CREATE_FORM_FIELDS\.push\(\{[\s\S]*"key": "vetId"/);
@@ -190,6 +190,82 @@ const resource = {
190
190
  export { resource };
191
191
  `;
192
192
 
193
+ const SELECT_RESOURCE_SOURCE = `const recordSchema = {
194
+ type: "object",
195
+ properties: {
196
+ id: { type: "integer" },
197
+ type: { type: "string" }
198
+ },
199
+ additionalProperties: false
200
+ };
201
+
202
+ const bodySchema = {
203
+ type: "object",
204
+ properties: {
205
+ type: { type: "string", enum: ["dryer", "pallet racking", "freezer", "coolroom"] }
206
+ },
207
+ additionalProperties: false
208
+ };
209
+
210
+ const resource = {
211
+ namespace: "locations",
212
+ operations: {
213
+ list: {
214
+ outputValidator: {
215
+ schema: {
216
+ type: "object",
217
+ properties: {
218
+ items: {
219
+ type: "array",
220
+ items: recordSchema
221
+ },
222
+ nextCursor: { type: ["string", "null"] }
223
+ },
224
+ additionalProperties: false
225
+ }
226
+ }
227
+ },
228
+ view: {
229
+ outputValidator: {
230
+ schema: recordSchema
231
+ }
232
+ },
233
+ create: {
234
+ bodyValidator: {
235
+ schema: bodySchema
236
+ },
237
+ outputValidator: {
238
+ schema: recordSchema
239
+ }
240
+ },
241
+ patch: {
242
+ bodyValidator: {
243
+ schema: bodySchema
244
+ },
245
+ outputValidator: {
246
+ schema: recordSchema
247
+ }
248
+ }
249
+ },
250
+ fieldMeta: [
251
+ {
252
+ key: "type",
253
+ ui: {
254
+ formControl: "select",
255
+ options: [
256
+ { value: "dryer", label: "Dryer" },
257
+ { value: "pallet racking", label: "Pallet Racking" },
258
+ { value: "freezer", label: "Freezer" },
259
+ { value: "coolroom", label: "Coolroom" }
260
+ ]
261
+ }
262
+ }
263
+ ]
264
+ };
265
+
266
+ export { resource };
267
+ `;
268
+
193
269
  const LOOKUP_RESOURCE_SOURCE = `const recordSchema = {
194
270
  type: "object",
195
271
  properties: {
@@ -325,11 +401,20 @@ test("buildUiTemplateContext derives CRUD placeholders from the explicit target-
325
401
  assert.match(context.__JSKIT_UI_LIST_HEADER_COLUMNS__, /First Name/);
326
402
  assert.match(context.__JSKIT_UI_LIST_ROW_COLUMNS__, /record\.firstName/);
327
403
  assert.match(context.__JSKIT_UI_VIEW_COLUMNS__, /view\.record\?\.firstName/);
328
- assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /formRuntime\.form\.firstName/);
329
- assert.match(context.__JSKIT_UI_EDIT_FORM_COLUMNS__, /formRuntime\.form\.email/);
404
+ assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /formState\.firstName/);
405
+ assert.match(context.__JSKIT_UI_EDIT_FORM_COLUMNS__, /formState\.email/);
330
406
  assert.equal(context.__JSKIT_UI_RECORD_CHANGED_EVENT__, "\"customers.record.changed\"");
331
407
  assert.equal(context.__JSKIT_UI_LIST_RECORD_ID_EXPR__, "item.id");
332
408
  assert.equal(context.__JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__, "\"firstName\"");
409
+ assert.equal(context.__JSKIT_UI_LIST_PAGE_VIEW_URL__, "\"./:customerId\"");
410
+ assert.equal(context.__JSKIT_UI_LIST_PAGE_EDIT_URL__, "\"./:customerId/edit\"");
411
+ assert.equal(context.__JSKIT_UI_LIST_PAGE_NEW_URL__, "\"./new\"");
412
+ assert.equal(context.__JSKIT_UI_NEW_PAGE_LIST_URL__, "\"..\"");
413
+ assert.equal(context.__JSKIT_UI_NEW_PAGE_VIEW_URL__, "\"../:customerId\"");
414
+ assert.equal(context.__JSKIT_UI_EDIT_PAGE_LIST_URL__, "\"../..\"");
415
+ assert.equal(context.__JSKIT_UI_EDIT_PAGE_VIEW_URL__, "\"..\"");
416
+ assert.equal(context.__JSKIT_UI_VIEW_PAGE_LIST_URL__, "\"..\"");
417
+ assert.equal(context.__JSKIT_UI_VIEW_PAGE_EDIT_URL__, "\"./edit\"");
333
418
  });
334
419
  });
335
420
 
@@ -343,7 +428,7 @@ test("buildUiTemplateContext keeps non-nullable booleans as switches", async ()
343
428
  });
344
429
 
345
430
  assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /<v-switch/);
346
- assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /formRuntime\.form\.vip/);
431
+ assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /formState\.vip/);
347
432
  });
348
433
  });
349
434
 
@@ -365,6 +450,76 @@ test("buildUiTemplateContext renders nullable booleans as tri-state selects by d
365
450
  });
366
451
  });
367
452
 
453
+ test("buildUiTemplateContext omits lookup runtime placeholders when form fields do not include lookups", async () => {
454
+ await withTempApp(async (appRoot) => {
455
+ await writeResource(appRoot, RESOURCE_FILE, FULL_RESOURCE_SOURCE);
456
+
457
+ const context = await buildUiTemplateContext({
458
+ appRoot,
459
+ options: createOptions()
460
+ });
461
+
462
+ assert.equal(context.__JSKIT_UI_CREATE_LOOKUP_IMPORT_LINE__, "");
463
+ assert.equal(context.__JSKIT_UI_EDIT_LOOKUP_IMPORT_LINE__, "");
464
+ assert.equal(context.__JSKIT_UI_CREATE_LOOKUP_RUNTIME_SETUP__, "");
465
+ assert.equal(context.__JSKIT_UI_EDIT_LOOKUP_RUNTIME_SETUP__, "");
466
+ assert.equal(context.__JSKIT_UI_CREATE_LOOKUP_FORM_PROPS__, "");
467
+ assert.equal(context.__JSKIT_UI_EDIT_LOOKUP_FORM_PROPS__, "");
468
+ });
469
+ });
470
+
471
+ test("buildUiTemplateContext indents direct-page form columns without changing shared form columns", async () => {
472
+ await withTempApp(async (appRoot) => {
473
+ await writeResource(appRoot, RESOURCE_FILE, FULL_RESOURCE_SOURCE);
474
+
475
+ const context = await buildUiTemplateContext({
476
+ appRoot,
477
+ options: createOptions()
478
+ });
479
+
480
+ assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /^ {14}<v-col/m);
481
+ assert.match(context.__JSKIT_UI_EDIT_FORM_COLUMNS__, /^ {14}<v-col/m);
482
+ assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS_DIRECT__, /^ {12}<v-col/m);
483
+ assert.match(context.__JSKIT_UI_EDIT_FORM_COLUMNS_DIRECT__, /^ {12}<v-col/m);
484
+ });
485
+ });
486
+
487
+ test("buildUiTemplateContext includes lookup runtime placeholders when form fields include lookups", async () => {
488
+ await withTempApp(async (appRoot) => {
489
+ await writeResource(appRoot, RESOURCE_FILE, LOOKUP_RESOURCE_SOURCE);
490
+
491
+ const context = await buildUiTemplateContext({
492
+ appRoot,
493
+ options: createOptions({
494
+ "display-fields": "serviceId"
495
+ })
496
+ });
497
+
498
+ assert.match(context.__JSKIT_UI_CREATE_LOOKUP_IMPORT_LINE__, /createCrudLookupFieldRuntime/);
499
+ assert.match(context.__JSKIT_UI_EDIT_LOOKUP_IMPORT_LINE__, /createCrudLookupFieldRuntime/);
500
+ assert.match(context.__JSKIT_UI_CREATE_LOOKUP_RUNTIME_SETUP__, /resolveLookupItems/);
501
+ assert.match(context.__JSKIT_UI_EDIT_LOOKUP_RUNTIME_SETUP__, /resolveLookupItems/);
502
+ assert.match(context.__JSKIT_UI_CREATE_LOOKUP_FORM_PROPS__, /resolve-lookup-items/);
503
+ assert.match(context.__JSKIT_UI_EDIT_LOOKUP_FORM_PROPS__, /resolve-lookup-items/);
504
+ });
505
+ });
506
+
507
+ test("buildUiTemplateContext escapes select option bindings safely for Vue attributes", async () => {
508
+ await withTempApp(async (appRoot) => {
509
+ await writeResource(appRoot, RESOURCE_FILE, SELECT_RESOURCE_SOURCE);
510
+
511
+ const context = await buildUiTemplateContext({
512
+ appRoot,
513
+ options: createOptions()
514
+ });
515
+
516
+ assert.match(
517
+ context.__JSKIT_UI_CREATE_FORM_COLUMNS__,
518
+ /:items="\[\{&quot;value&quot;:&quot;dryer&quot;,&quot;label&quot;:&quot;Dryer&quot;\},\{&quot;value&quot;:&quot;pallet racking&quot;,&quot;label&quot;:&quot;Pallet Racking&quot;\},\{&quot;value&quot;:&quot;freezer&quot;,&quot;label&quot;:&quot;Freezer&quot;\},\{&quot;value&quot;:&quot;coolroom&quot;,&quot;label&quot;:&quot;Coolroom&quot;\}\]"/
519
+ );
520
+ });
521
+ });
522
+
368
523
  test("buildUiTemplateContext derives menu auth visibility from the target surface policy", async () => {
369
524
  await withTempApp(async (appRoot) => {
370
525
  await writeFile(