@jskit-ai/crud-ui-generator 0.1.41 → 0.1.43
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.
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
4
|
+
version: "0.1.43",
|
|
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.
|
|
183
|
+
"@jskit-ai/users-web": "0.1.75"
|
|
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.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@jskit-ai/crud-core": "0.1.
|
|
10
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
9
|
+
"@jskit-ai/crud-core": "0.1.68",
|
|
10
|
+
"@jskit-ai/kernel": "0.1.60",
|
|
11
|
+
"@jskit-ai/resource-crud-core": "0.1.5"
|
|
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
|
-
|
|
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
|
-
|
|
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"
|