@jskit-ai/crud-ui-generator 0.1.41 → 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.41",
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.73"
183
+ "@jskit-ai/users-web": "0.1.74"
172
184
  },
173
185
  dev: {}
174
186
  },
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-ui-generator",
3
- "version": "0.1.41",
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.66",
10
- "@jskit-ai/kernel": "0.1.58",
11
- "@jskit-ai/resource-crud-core": "0.1.3"
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,6 +57,14 @@ 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";
@@ -222,6 +231,17 @@ function parseDisplayFieldsOption(options) {
222
231
  return Object.freeze(unique);
223
232
  }
224
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
+
225
245
  function validateDisplayFieldsForOperation(selectedFieldKeys, fields, operationName) {
226
246
  const selectedFields = Array.isArray(selectedFieldKeys) ? selectedFieldKeys : [];
227
247
  if (selectedFields.length < 1) {
@@ -461,11 +481,52 @@ function resolveCrudRelativePath(namespace = "") {
461
481
  })}`;
462
482
  }
463
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
+
464
524
  async function buildUiTemplateContext({ appRoot, options } = {}) {
465
525
  const targetRoot = requireTargetRootOption(options);
466
526
  const listTargetFile = resolveListTargetFile(targetRoot);
467
527
  const selectedOperations = parseOperationsOption(options);
468
528
  const selectedDisplayFields = parseDisplayFieldsOption(options);
529
+ const parentTitleMode = parseParentTitleOption(options);
469
530
  const pageTarget = await resolvePageTargetDetails({
470
531
  appRoot,
471
532
  targetFile: listTargetFile,
@@ -607,6 +668,13 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
607
668
  __JSKIT_UI_RESOURCE_SINGULAR_TITLE__: resourceLabels.singularTitle,
608
669
  __JSKIT_UI_RESOURCE_PLURAL_TITLE__: resourceLabels.pluralTitle,
609
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
+ }),
610
678
  __JSKIT_UI_FORM_COMPONENT_FILE__: DEFAULT_FORM_COMPONENT_FILE,
611
679
  __JSKIT_UI_FORM_FIELDS_FILE__: DEFAULT_FORM_FIELDS_FILE,
612
680
  __JSKIT_UI_SURFACE_ID__: pageTarget.surfaceId,
@@ -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
 
@@ -137,25 +137,7 @@ const records = useCrudList({
137
137
  : null
138
138
  });
139
139
 
140
- const parentTitle = useCrudListParentTitle({
141
- listRuntime: records,
142
- resource: uiResource,
143
- adapter: UI_OPERATION_ADAPTER || undefined,
144
- recordIdParam: UI_RECORD_ID_PARAM,
145
- queryKeyPrefix: ["ui-generator", "__JSKIT_UI_RESOURCE_NAMESPACE__", "list", "parent-title"],
146
- placementSource: "ui-generator.__JSKIT_UI_RESOURCE_NAMESPACE__.list.parent-title",
147
- fallbackLoadError: "Unable to load parent record.",
148
- notFoundMessage: "Parent record not found."
149
- });
150
-
151
- const listHeadingTitle = computed(() => {
152
- const resolvedParentTitle = String(parentTitle.title || "").trim();
153
- if (!resolvedParentTitle) {
154
- return "__JSKIT_UI_ROUTE_TITLE__";
155
- }
156
-
157
- return `__JSKIT_UI_ROUTE_TITLE__ for ${resolvedParentTitle}`;
158
- });
140
+ __JSKIT_UI_LIST_HEADING_TITLE_SETUP__
159
141
  </script>
160
142
 
161
143
  <style scoped>
@@ -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);
@@ -715,6 +736,23 @@ test("buildUiTemplateContext validates operations against the supported CRUD set
715
736
  });
716
737
  });
717
738
 
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
+
718
756
  test("crud ui templates derive JSON:API transport from the shared CRUD resource", async () => {
719
757
  const testDirectory = path.dirname(fileURLToPath(import.meta.url));
720
758
  const templateRoot = path.resolve(testDirectory, "..", "templates", "src", "pages", "admin", "ui-generator");
@@ -13,6 +13,16 @@ test("crud-ui-generator operations option exposes structured csv-enum metadata",
13
13
  assert.equal(descriptor.metadata?.generatorSubcommands?.crud?.optionNames?.includes("operations"), true);
14
14
  });
15
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
+
16
26
  test("crud-ui-generator placement scaffold includes an explicit stock icon prop", () => {
17
27
  const placementMutation = descriptor?.mutations?.text?.find(
18
28
  (entry) => String(entry?.id || "").trim() === "crud-ui-placement-menu"