@jskit-ai/crud-ui-generator 0.1.40 → 0.1.42

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.40",
4
+ version: "0.1.42",
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: {
@@ -43,6 +43,15 @@ export default Object.freeze({
43
43
  promptLabel: "Route id param",
44
44
  promptHint: "Route param used by view and edit pages (default: recordId)."
45
45
  },
46
+ "parent-title": {
47
+ required: false,
48
+ inputType: "text",
49
+ defaultValue: "contextual",
50
+ validationType: "enum",
51
+ allowedValues: ["contextual", "none"],
52
+ promptLabel: "Parent title mode",
53
+ promptHint: "Whether list pages should show a parent-aware heading: contextual | none."
54
+ },
46
55
  force: {
47
56
  required: false,
48
57
  inputType: "flag",
@@ -108,6 +117,7 @@ export default Object.freeze({
108
117
  "operations",
109
118
  "display-fields",
110
119
  "id-param",
120
+ "parent-title",
111
121
  "link-placement",
112
122
  "link-component-token",
113
123
  "namespace",
@@ -130,7 +140,8 @@ export default Object.freeze({
130
140
  lines: [
131
141
  "npx jskit generate crud-ui-generator crud \\",
132
142
  " admin/catalog/index/products \\",
133
- " --resource-file packages/products/src/shared/productResource.js"
143
+ " --resource-file packages/products/src/shared/productResource.js \\",
144
+ " --parent-title contextual"
134
145
  ]
135
146
  },
136
147
  {
@@ -141,6 +152,7 @@ export default Object.freeze({
141
152
  " --resource-file packages/pets/src/shared/petResource.js \\",
142
153
  " --id-param petId \\",
143
154
  " --display-fields name,breedId,sex \\",
155
+ " --parent-title none \\",
144
156
  " --force"
145
157
  ]
146
158
  }
@@ -168,7 +180,7 @@ export default Object.freeze({
168
180
  mutations: {
169
181
  dependencies: {
170
182
  runtime: {
171
- "@jskit-ai/users-web": "0.1.72"
183
+ "@jskit-ai/users-web": "0.1.74"
172
184
  },
173
185
  dev: {}
174
186
  },
@@ -353,7 +365,7 @@ export default Object.freeze({
353
365
  position: "bottom",
354
366
  skipIfContains: "__JSKIT_UI_MENU_MARKER__",
355
367
  value:
356
- "\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 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",
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",
357
369
  reason: "Append generated CRUD list-page placement.",
358
370
  category: "crud-ui-generator",
359
371
  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.40",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/crud-core": "0.1.65",
10
- "@jskit-ai/kernel": "0.1.57",
11
- "@jskit-ai/resource-crud-core": "0.1.2"
9
+ "@jskit-ai/crud-core": "0.1.67",
10
+ "@jskit-ai/kernel": "0.1.59",
11
+ "@jskit-ai/resource-crud-core": "0.1.4"
12
12
  },
13
13
  "exports": {
14
14
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -32,6 +32,7 @@ import {
32
32
  import descriptor from "../../package.descriptor.mjs";
33
33
 
34
34
  const DEFAULT_ALLOWED_OPERATIONS = Object.freeze(["list", "view", "new", "edit"]);
35
+ const DEFAULT_ALLOWED_PARENT_TITLE_VALUES = Object.freeze(["contextual", "none"]);
35
36
  function resolveAllowedValues(schema = {}, fallbackValues = []) {
36
37
  const resolvedValues = [];
37
38
  const seen = new Set();
@@ -56,9 +57,18 @@ function resolveAllowedValues(schema = {}, fallbackValues = []) {
56
57
  const OPERATION_VALUES = resolveAllowedValues(descriptor?.options?.operations, DEFAULT_ALLOWED_OPERATIONS);
57
58
  const ALLOWED_OPERATIONS = new Set(OPERATION_VALUES);
58
59
  const DEFAULT_OPERATIONS = normalizeText(descriptor?.options?.operations?.defaultValue) || OPERATION_VALUES.join(",");
60
+ const PARENT_TITLE_VALUES = resolveAllowedValues(
61
+ descriptor?.options?.["parent-title"],
62
+ DEFAULT_ALLOWED_PARENT_TITLE_VALUES
63
+ );
64
+ const ALLOWED_PARENT_TITLE_VALUES = new Set(PARENT_TITLE_VALUES);
65
+ const DEFAULT_PARENT_TITLE_MODE = normalizeText(descriptor?.options?.["parent-title"]?.defaultValue).toLowerCase()
66
+ || PARENT_TITLE_VALUES[0]
67
+ || "contextual";
59
68
  const DEFAULT_LIST_HIDDEN_FIELD_KEYS = new Set(["createdAt", "updatedAt"]);
60
69
  const DEFAULT_FORM_COMPONENT_FILE = "CrudAddEditForm.vue";
61
70
  const DEFAULT_FORM_FIELDS_FILE = "CrudAddEditFormFields.js";
71
+ const DEFAULT_GENERATED_LINK_ICON = "mdi-view-list-outline";
62
72
 
63
73
  function splitTextIntoWords(value = "") {
64
74
  const normalized = String(value || "")
@@ -221,6 +231,17 @@ function parseDisplayFieldsOption(options) {
221
231
  return Object.freeze(unique);
222
232
  }
223
233
 
234
+ function parseParentTitleOption(options) {
235
+ const parentTitleMode = normalizeText(options?.["parent-title"]).toLowerCase() || DEFAULT_PARENT_TITLE_MODE;
236
+ if (!ALLOWED_PARENT_TITLE_VALUES.has(parentTitleMode)) {
237
+ throw new Error(
238
+ `crud-ui-generator option "parent-title" supports only: ${PARENT_TITLE_VALUES.join(", ")}.`
239
+ );
240
+ }
241
+
242
+ return parentTitleMode;
243
+ }
244
+
224
245
  function validateDisplayFieldsForOperation(selectedFieldKeys, fields, operationName) {
225
246
  const selectedFields = Array.isArray(selectedFieldKeys) ? selectedFieldKeys : [];
226
247
  if (selectedFields.length < 1) {
@@ -460,11 +481,52 @@ function resolveCrudRelativePath(namespace = "") {
460
481
  })}`;
461
482
  }
462
483
 
484
+ function buildListParentTitleImportLine(parentTitleMode = "contextual") {
485
+ if (parentTitleMode !== "contextual") {
486
+ return "";
487
+ }
488
+
489
+ return 'import { useCrudListParentTitle } from "@jskit-ai/users-web/client/composables/useCrudListParentTitle";';
490
+ }
491
+
492
+ function buildListHeadingTitleSetup({
493
+ parentTitleMode = "contextual",
494
+ resourceNamespace = "",
495
+ routeTitle = "Records"
496
+ } = {}) {
497
+ const normalizedRouteTitle = normalizeText(routeTitle) || "Records";
498
+ const routeTitleLiteral = JSON.stringify(normalizedRouteTitle);
499
+ if (parentTitleMode !== "contextual") {
500
+ return `const listHeadingTitle = computed(() => ${routeTitleLiteral});`;
501
+ }
502
+
503
+ return `const parentTitle = useCrudListParentTitle({
504
+ listRuntime: records,
505
+ resource: uiResource,
506
+ adapter: UI_OPERATION_ADAPTER || undefined,
507
+ recordIdParam: UI_RECORD_ID_PARAM,
508
+ queryKeyPrefix: ["ui-generator", "${resourceNamespace}", "list", "parent-title"],
509
+ placementSource: "ui-generator.${resourceNamespace}.list.parent-title",
510
+ fallbackLoadError: "Unable to load parent record.",
511
+ notFoundMessage: "Parent record not found."
512
+ });
513
+
514
+ const listHeadingTitle = computed(() => {
515
+ const resolvedParentTitle = String(parentTitle.title || "").trim();
516
+ if (!resolvedParentTitle) {
517
+ return ${routeTitleLiteral};
518
+ }
519
+
520
+ return ${JSON.stringify(`${normalizedRouteTitle} for `)} + resolvedParentTitle;
521
+ });`;
522
+ }
523
+
463
524
  async function buildUiTemplateContext({ appRoot, options } = {}) {
464
525
  const targetRoot = requireTargetRootOption(options);
465
526
  const listTargetFile = resolveListTargetFile(targetRoot);
466
527
  const selectedOperations = parseOperationsOption(options);
467
528
  const selectedDisplayFields = parseDisplayFieldsOption(options);
529
+ const parentTitleMode = parseParentTitleOption(options);
468
530
  const pageTarget = await resolvePageTargetDetails({
469
531
  appRoot,
470
532
  targetFile: listTargetFile,
@@ -606,6 +668,13 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
606
668
  __JSKIT_UI_RESOURCE_SINGULAR_TITLE__: resourceLabels.singularTitle,
607
669
  __JSKIT_UI_RESOURCE_PLURAL_TITLE__: resourceLabels.pluralTitle,
608
670
  __JSKIT_UI_ROUTE_TITLE__: pageTarget.defaultName,
671
+ __JSKIT_UI_PARENT_TITLE_MODE__: parentTitleMode,
672
+ __JSKIT_UI_LIST_PARENT_TITLE_IMPORT_LINE__: buildListParentTitleImportLine(parentTitleMode),
673
+ __JSKIT_UI_LIST_HEADING_TITLE_SETUP__: buildListHeadingTitleSetup({
674
+ parentTitleMode,
675
+ resourceNamespace,
676
+ routeTitle: pageTarget.defaultName
677
+ }),
609
678
  __JSKIT_UI_FORM_COMPONENT_FILE__: DEFAULT_FORM_COMPONENT_FILE,
610
679
  __JSKIT_UI_FORM_FIELDS_FILE__: DEFAULT_FORM_FIELDS_FILE,
611
680
  __JSKIT_UI_SURFACE_ID__: pageTarget.surfaceId,
@@ -659,6 +728,7 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
659
728
  __JSKIT_UI_MENU_PLACEMENT_ID__: String(pageLinkTarget?.pageTarget?.placementId || ""),
660
729
  __JSKIT_UI_MENU_PLACEMENT_TARGET__: String(pageLinkTarget?.placementTarget?.id || ""),
661
730
  __JSKIT_UI_MENU_COMPONENT_TOKEN__: String(pageLinkTarget?.componentToken || ""),
731
+ __JSKIT_UI_MENU_ICON__: DEFAULT_GENERATED_LINK_ICON,
662
732
  __JSKIT_UI_MENU_WORKSPACE_SUFFIX__: String(pageLinkTarget?.pageTarget?.routeUrlSuffix || ""),
663
733
  __JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__: String(pageLinkTarget?.pageTarget?.routeUrlSuffix || ""),
664
734
  __JSKIT_UI_MENU_WHEN_LINE__: String(pageLinkTarget?.whenLine || ""),
@@ -63,12 +63,6 @@ const UI_VIEW_URL = __JSKIT_UI_EDIT_PAGE_VIEW_URL__;
63
63
  const UI_CANCEL_URL = UI_VIEW_URL || UI_LIST_URL;
64
64
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
65
65
  const UI_EDIT_FORM_FIELDS = [];
66
- const UI_EDIT_TRANSPORT = Object.freeze({
67
- kind: "jsonapi-resource",
68
- requestType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
69
- responseType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
70
- responseKind: "record"
71
- });
72
66
 
73
67
  // @jskit-contract crud.ui.form-fields.__JSKIT_UI_RESOURCE_NAMESPACE__.edit.v1
74
68
  void UI_EDIT_FORM_FIELDS;
@@ -105,7 +99,6 @@ const formRuntime = useCrudAddEdit({
105
99
  routeRecordId.value
106
100
  ],
107
101
  placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.edit",
108
- transport: UI_EDIT_TRANSPORT,
109
102
  writeMethod: "PATCH",
110
103
  fallbackLoadError: "Unable to load record.",
111
104
  fallbackSaveError: "Unable to save record.",
@@ -26,12 +26,6 @@ const UI_LIST_URL = __JSKIT_UI_EDIT_PAGE_LIST_URL__;
26
26
  const UI_VIEW_URL = __JSKIT_UI_EDIT_PAGE_VIEW_URL__;
27
27
  const UI_CANCEL_URL = UI_VIEW_URL || UI_LIST_URL;
28
28
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
29
- const UI_EDIT_TRANSPORT = Object.freeze({
30
- kind: "jsonapi-resource",
31
- requestType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
32
- responseType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
33
- responseKind: "record"
34
- });
35
29
  const route = useRoute();
36
30
 
37
31
  // jskit:crud-ui-fields-target ../_components/__JSKIT_UI_FORM_COMPONENT_FILE__
@@ -64,7 +58,6 @@ const formRuntime = useCrudAddEdit({
64
58
  routeRecordId.value
65
59
  ],
66
60
  placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.edit",
67
- transport: UI_EDIT_TRANSPORT,
68
61
  writeMethod: "PATCH",
69
62
  fallbackLoadError: "Unable to load record.",
70
63
  fallbackSaveError: "Unable to save record.",
@@ -89,7 +89,7 @@ __JSKIT_UI_LIST_ROW_COLUMNS__
89
89
 
90
90
  <script setup>
91
91
  import { computed } from "vue";
92
- import { useCrudListParentTitle } from "@jskit-ai/users-web/client/composables/useCrudListParentTitle";
92
+ __JSKIT_UI_LIST_PARENT_TITLE_IMPORT_LINE__
93
93
  import { useCrudList } from "@jskit-ai/users-web/client/composables/useCrudList";
94
94
  import { resource as uiResource } from "__JSKIT_UI_RESOURCE_IMPORT_PATH__";
95
95
 
@@ -101,11 +101,6 @@ const UI_EDIT_URL = __JSKIT_UI_LIST_PAGE_EDIT_URL__;
101
101
  const UI_NEW_URL = __JSKIT_UI_LIST_PAGE_NEW_URL__;
102
102
  const UI_RECORD_CHANGED_EVENTS = __JSKIT_UI_LIST_REALTIME_EVENTS__;
103
103
  const UI_ROUTE_QUERY_BLACKLIST = Object.freeze(["include", "cursor", "limit"]);
104
- const UI_LIST_TRANSPORT = Object.freeze({
105
- kind: "jsonapi-resource",
106
- responseType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
107
- responseKind: "collection"
108
- });
109
104
 
110
105
  const records = useCrudList({
111
106
  adapter: UI_OPERATION_ADAPTER || undefined,
@@ -131,7 +126,6 @@ const records = useCrudList({
131
126
  },
132
127
  placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.list",
133
128
  fallbackLoadError: "Unable to load records.",
134
- transport: UI_LIST_TRANSPORT,
135
129
  recordIdParam: UI_RECORD_ID_PARAM,
136
130
  recordIdSelector: (item = {}) => __JSKIT_UI_LIST_RECORD_ID_EXPR__,
137
131
  viewUrlTemplate: UI_VIEW_URL,
@@ -143,25 +137,7 @@ const records = useCrudList({
143
137
  : null
144
138
  });
145
139
 
146
- const parentTitle = useCrudListParentTitle({
147
- listRuntime: records,
148
- resource: uiResource,
149
- adapter: UI_OPERATION_ADAPTER || undefined,
150
- recordIdParam: UI_RECORD_ID_PARAM,
151
- queryKeyPrefix: ["ui-generator", "__JSKIT_UI_RESOURCE_NAMESPACE__", "list", "parent-title"],
152
- placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.list.parent-title",
153
- fallbackLoadError: "Unable to load parent record.",
154
- notFoundMessage: "Parent record not found."
155
- });
156
-
157
- const listHeadingTitle = computed(() => {
158
- const resolvedParentTitle = String(parentTitle.title || "").trim();
159
- if (!resolvedParentTitle) {
160
- return "__JSKIT_UI_ROUTE_TITLE__";
161
- }
162
-
163
- return `__JSKIT_UI_ROUTE_TITLE__ for ${resolvedParentTitle}`;
164
- });
140
+ __JSKIT_UI_LIST_HEADING_TITLE_SETUP__
165
141
  </script>
166
142
 
167
143
  <style scoped>
@@ -50,12 +50,6 @@ const UI_LIST_URL = __JSKIT_UI_NEW_PAGE_LIST_URL__;
50
50
  const UI_VIEW_URL = __JSKIT_UI_NEW_PAGE_VIEW_URL__;
51
51
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
52
52
  const UI_CREATE_FORM_FIELDS = [];
53
- const UI_CREATE_TRANSPORT = Object.freeze({
54
- kind: "jsonapi-resource",
55
- requestType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
56
- responseType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
57
- responseKind: "record"
58
- });
59
53
 
60
54
  // @jskit-contract crud.ui.form-fields.__JSKIT_UI_RESOURCE_NAMESPACE__.new.v1
61
55
  void UI_CREATE_FORM_FIELDS;
@@ -81,7 +75,6 @@ const formRuntime = useCrudAddEdit({
81
75
  ],
82
76
  placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.new",
83
77
  readEnabled: false,
84
- transport: UI_CREATE_TRANSPORT,
85
78
  writeMethod: "POST",
86
79
  fallbackSaveError: "Unable to save record.",
87
80
  recordIdParam: UI_RECORD_ID_PARAM,
@@ -23,12 +23,6 @@ const UI_LIST_URL = __JSKIT_UI_NEW_PAGE_LIST_URL__;
23
23
  const UI_VIEW_URL = __JSKIT_UI_NEW_PAGE_VIEW_URL__;
24
24
  const UI_CANCEL_URL = UI_LIST_URL;
25
25
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
26
- const UI_CREATE_TRANSPORT = Object.freeze({
27
- kind: "jsonapi-resource",
28
- requestType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
29
- responseType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
30
- responseKind: "record"
31
- });
32
26
 
33
27
  // jskit:crud-ui-fields-target ./_components/__JSKIT_UI_FORM_COMPONENT_FILE__
34
28
  // jskit:crud-ui-form-fields-target ./_components/__JSKIT_UI_FORM_FIELDS_FILE__
@@ -51,7 +45,6 @@ const formRuntime = useCrudAddEdit({
51
45
  ],
52
46
  placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.new",
53
47
  readEnabled: false,
54
- transport: UI_CREATE_TRANSPORT,
55
48
  writeMethod: "POST",
56
49
  fallbackSaveError: "Unable to save record.",
57
50
  recordIdParam: UI_RECORD_ID_PARAM,
@@ -57,6 +57,7 @@ __JSKIT_UI_VIEW_COLUMNS__
57
57
 
58
58
  <script setup>
59
59
  import { useCrudView } from "@jskit-ai/users-web/client/composables/useCrudView";
60
+ import { resource as uiResource } from "__JSKIT_UI_RESOURCE_IMPORT_PATH__";
60
61
 
61
62
  const UI_OPERATION_ADAPTER = null;
62
63
  const UI_RECORD_ID_PARAM = "__JSKIT_UI_RECORD_ID_PARAM__";
@@ -66,14 +67,10 @@ const UI_LIST_URL = __JSKIT_UI_VIEW_PAGE_LIST_URL__;
66
67
  const UI_EDIT_URL = __JSKIT_UI_VIEW_PAGE_EDIT_URL__;
67
68
  const UI_VIEW_TITLE_FALLBACK_FIELD_KEY = __JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__;
68
69
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
69
- const UI_VIEW_TRANSPORT = Object.freeze({
70
- kind: "jsonapi-resource",
71
- responseType: "__JSKIT_UI_RESOURCE_NAMESPACE__",
72
- responseKind: "record"
73
- });
74
70
 
75
71
  const view = useCrudView({
76
72
  adapter: UI_OPERATION_ADAPTER || undefined,
73
+ resource: uiResource,
77
74
  apiUrlTemplate: UI_VIEW_API_URL,
78
75
  recordIdParam: UI_RECORD_ID_PARAM,
79
76
  includeRecordIdInQueryKey: true,
@@ -85,7 +82,6 @@ const view = useCrudView({
85
82
  String(workspaceSlug || "")
86
83
  ],
87
84
  placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.view",
88
- transport: UI_VIEW_TRANSPORT,
89
85
  fallbackLoadError: "Unable to load record.",
90
86
  notFoundMessage: "Record not found.",
91
87
  listUrlTemplate: UI_LIST_URL,
@@ -320,6 +320,9 @@ test("buildUiTemplateContext derives CRUD placeholders from the explicit target-
320
320
  assert.equal(context.__JSKIT_UI_RESOURCE_SINGULAR_TITLE__, "Customer");
321
321
  assert.equal(context.__JSKIT_UI_RESOURCE_PLURAL_TITLE__, "Customers");
322
322
  assert.equal(context.__JSKIT_UI_ROUTE_TITLE__, "Customers");
323
+ assert.equal(context.__JSKIT_UI_PARENT_TITLE_MODE__, "contextual");
324
+ assert.match(context.__JSKIT_UI_LIST_PARENT_TITLE_IMPORT_LINE__, /useCrudListParentTitle/);
325
+ assert.match(context.__JSKIT_UI_LIST_HEADING_TITLE_SETUP__, /Customers for /);
323
326
  assert.equal(context.__JSKIT_UI_FORM_COMPONENT_FILE__, "CrudAddEditForm.vue");
324
327
  assert.equal(context.__JSKIT_UI_FORM_FIELDS_FILE__, "CrudAddEditFormFields.js");
325
328
  assert.equal(context.__JSKIT_UI_SURFACE_ID__, "admin");
@@ -350,6 +353,24 @@ test("buildUiTemplateContext derives CRUD placeholders from the explicit target-
350
353
  });
351
354
  });
352
355
 
356
+ test("buildUiTemplateContext can suppress parent-title heading generation with parent-title none", async () => {
357
+ await withTempApp(async (appRoot) => {
358
+ await writeResource(appRoot, RESOURCE_FILE, FULL_RESOURCE_SOURCE);
359
+
360
+ const context = await buildUiTemplateContext({
361
+ appRoot,
362
+ options: createOptions({
363
+ "parent-title": "none"
364
+ })
365
+ });
366
+
367
+ assert.equal(context.__JSKIT_UI_PARENT_TITLE_MODE__, "none");
368
+ assert.equal(context.__JSKIT_UI_LIST_PARENT_TITLE_IMPORT_LINE__, "");
369
+ assert.doesNotMatch(context.__JSKIT_UI_LIST_HEADING_TITLE_SETUP__, /useCrudListParentTitle/);
370
+ assert.match(context.__JSKIT_UI_LIST_HEADING_TITLE_SETUP__, /computed\(\(\) => "Customers"\)/);
371
+ });
372
+ });
373
+
353
374
  test("buildUiTemplateContext keeps non-nullable booleans as switches", async () => {
354
375
  await withTempApp(async (appRoot) => {
355
376
  await writeResource(appRoot, RESOURCE_FILE, FULL_RESOURCE_SOURCE);
@@ -628,7 +649,8 @@ test("buildUiTemplateContext infers tab placement and relative link-to from the
628
649
  });
629
650
 
630
651
  assert.equal(context.__JSKIT_UI_MENU_PLACEMENT_TARGET__, "catalog:sub-pages");
631
- assert.equal(context.__JSKIT_UI_MENU_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
652
+ assert.equal(context.__JSKIT_UI_MENU_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
653
+ assert.equal(context.__JSKIT_UI_MENU_ICON__, "mdi-view-list-outline");
632
654
  assert.equal(context.__JSKIT_UI_MENU_TO_PROP_LINE__, " to: \"./products\",\n");
633
655
  assert.equal(context.__JSKIT_UI_MENU_WORKSPACE_SUFFIX__, "/catalog/products");
634
656
  });
@@ -714,7 +736,24 @@ test("buildUiTemplateContext validates operations against the supported CRUD set
714
736
  });
715
737
  });
716
738
 
717
- test("crud ui templates opt into shared JSON:API client transport", async () => {
739
+ test("buildUiTemplateContext validates parent-title against the supported modes", async () => {
740
+ await withTempApp(async (appRoot) => {
741
+ await writeResource(appRoot, RESOURCE_FILE, FULL_RESOURCE_SOURCE);
742
+
743
+ await assert.rejects(
744
+ () =>
745
+ buildUiTemplateContext({
746
+ appRoot,
747
+ options: createOptions({
748
+ "parent-title": "always"
749
+ })
750
+ }),
751
+ /parent-title" supports only: contextual, none/
752
+ );
753
+ });
754
+ });
755
+
756
+ test("crud ui templates derive JSON:API transport from the shared CRUD resource", async () => {
718
757
  const testDirectory = path.dirname(fileURLToPath(import.meta.url));
719
758
  const templateRoot = path.resolve(testDirectory, "..", "templates", "src", "pages", "admin", "ui-generator");
720
759
 
@@ -725,25 +764,28 @@ test("crud ui templates opt into shared JSON:API client transport", async () =>
725
764
  const newWrapperTemplateSource = await readFile(path.join(templateRoot, "NewWrapperElement.vue"), "utf8");
726
765
  const editWrapperTemplateSource = await readFile(path.join(templateRoot, "EditWrapperElement.vue"), "utf8");
727
766
 
728
- assert.match(listTemplateSource, /const UI_LIST_TRANSPORT = Object\.freeze\(\{/);
729
- assert.match(listTemplateSource, /responseKind: "collection"/);
730
- assert.match(listTemplateSource, /transport: UI_LIST_TRANSPORT,/);
767
+ assert.match(listTemplateSource, /resource: uiResource,/);
768
+ assert.doesNotMatch(listTemplateSource, /const UI_LIST_TRANSPORT = Object\.freeze\(\{/);
769
+ assert.doesNotMatch(listTemplateSource, /transport:\s*UI_LIST_TRANSPORT,/);
731
770
 
732
- assert.match(viewTemplateSource, /const UI_VIEW_TRANSPORT = Object\.freeze\(\{/);
733
- assert.match(viewTemplateSource, /responseKind: "record"/);
734
- assert.match(viewTemplateSource, /transport: UI_VIEW_TRANSPORT,/);
771
+ assert.match(viewTemplateSource, /import \{ resource as uiResource \} from/);
772
+ assert.match(viewTemplateSource, /resource: uiResource,/);
773
+ assert.doesNotMatch(viewTemplateSource, /const UI_VIEW_TRANSPORT = Object\.freeze\(\{/);
774
+ assert.doesNotMatch(viewTemplateSource, /transport:\s*UI_VIEW_TRANSPORT,/);
735
775
 
736
- assert.match(newTemplateSource, /const UI_CREATE_TRANSPORT = Object\.freeze\(\{/);
737
- assert.match(newTemplateSource, /requestType: "__JSKIT_UI_RESOURCE_NAMESPACE__"/);
738
- assert.match(newTemplateSource, /transport: UI_CREATE_TRANSPORT,/);
776
+ assert.match(newTemplateSource, /resource: uiResource,/);
777
+ assert.doesNotMatch(newTemplateSource, /const UI_CREATE_TRANSPORT = Object\.freeze\(\{/);
778
+ assert.doesNotMatch(newTemplateSource, /transport:\s*UI_CREATE_TRANSPORT,/);
739
779
 
740
- assert.match(editTemplateSource, /const UI_EDIT_TRANSPORT = Object\.freeze\(\{/);
741
- assert.match(editTemplateSource, /requestType: "__JSKIT_UI_RESOURCE_NAMESPACE__"/);
742
- assert.match(editTemplateSource, /transport: UI_EDIT_TRANSPORT,/);
780
+ assert.match(editTemplateSource, /resource: uiResource,/);
781
+ assert.doesNotMatch(editTemplateSource, /const UI_EDIT_TRANSPORT = Object\.freeze\(\{/);
782
+ assert.doesNotMatch(editTemplateSource, /transport:\s*UI_EDIT_TRANSPORT,/);
743
783
 
744
- assert.match(newWrapperTemplateSource, /const UI_CREATE_TRANSPORT = Object\.freeze\(\{/);
745
- assert.match(newWrapperTemplateSource, /transport: UI_CREATE_TRANSPORT,/);
784
+ assert.match(newWrapperTemplateSource, /resource: uiResource,/);
785
+ assert.doesNotMatch(newWrapperTemplateSource, /const UI_CREATE_TRANSPORT = Object\.freeze\(\{/);
786
+ assert.doesNotMatch(newWrapperTemplateSource, /transport:\s*UI_CREATE_TRANSPORT,/);
746
787
 
747
- assert.match(editWrapperTemplateSource, /const UI_EDIT_TRANSPORT = Object\.freeze\(\{/);
748
- assert.match(editWrapperTemplateSource, /transport: UI_EDIT_TRANSPORT,/);
788
+ assert.match(editWrapperTemplateSource, /resource: uiResource,/);
789
+ assert.doesNotMatch(editWrapperTemplateSource, /const UI_EDIT_TRANSPORT = Object\.freeze\(\{/);
790
+ assert.doesNotMatch(editWrapperTemplateSource, /transport:\s*UI_EDIT_TRANSPORT,/);
749
791
  });
@@ -12,3 +12,20 @@ test("crud-ui-generator operations option exposes structured csv-enum metadata",
12
12
  assert.equal(descriptor.options?.operations?.defaultValue, "list,view,new,edit");
13
13
  assert.equal(descriptor.metadata?.generatorSubcommands?.crud?.optionNames?.includes("operations"), true);
14
14
  });
15
+
16
+ test("crud-ui-generator parent-title option exposes structured enum metadata", () => {
17
+ assert.equal(descriptor.options?.["parent-title"]?.validationType, "enum");
18
+ assert.deepEqual(
19
+ descriptor.options?.["parent-title"]?.allowedValues,
20
+ ["contextual", "none"]
21
+ );
22
+ assert.equal(descriptor.options?.["parent-title"]?.defaultValue, "contextual");
23
+ assert.equal(descriptor.metadata?.generatorSubcommands?.crud?.optionNames?.includes("parent-title"), true);
24
+ });
25
+
26
+ test("crud-ui-generator placement scaffold includes an explicit stock icon prop", () => {
27
+ const placementMutation = descriptor?.mutations?.text?.find(
28
+ (entry) => String(entry?.id || "").trim() === "crud-ui-placement-menu"
29
+ );
30
+ assert.match(String(placementMutation?.value || ""), /icon: "__JSKIT_UI_MENU_ICON__"/);
31
+ });