@jskit-ai/crud-ui-generator 0.1.4 → 0.1.6
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 +45 -5
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +71 -12
- package/src/server/resourceSupport.js +106 -16
- package/templates/src/pages/admin/ui-generator/EditElement.vue +2 -6
- package/templates/src/pages/admin/ui-generator/ListElement.vue +12 -4
- package/templates/src/pages/admin/ui-generator/NewElement.vue +1 -5
- package/templates/src/pages/admin/ui-generator/ViewElement.vue +24 -3
- package/test/buildTemplateContext.test.js +223 -1
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.6",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "Generate app-local CRUD UI scaffolds from resource validators.",
|
|
7
7
|
options: {
|
|
@@ -81,6 +81,22 @@ export default Object.freeze({
|
|
|
81
81
|
defaultValue: "",
|
|
82
82
|
promptLabel: "Menu placement",
|
|
83
83
|
promptHint: "Optional host:position target (defaults to ShellLayout default outlet)."
|
|
84
|
+
},
|
|
85
|
+
"placement-component-token": {
|
|
86
|
+
required: false,
|
|
87
|
+
inputType: "text",
|
|
88
|
+
defaultValue: "",
|
|
89
|
+
promptLabel: "Placement component token",
|
|
90
|
+
promptHint:
|
|
91
|
+
"Optional component token override for generated menu placement. Use local.main.ui.tab-link-item for routed tab links (auto-provisions src/components/TabLinkItem.vue + MainClientProvider registration)."
|
|
92
|
+
},
|
|
93
|
+
"placement-to": {
|
|
94
|
+
required: false,
|
|
95
|
+
inputType: "text",
|
|
96
|
+
defaultValue: "",
|
|
97
|
+
promptLabel: "Placement to",
|
|
98
|
+
promptHint:
|
|
99
|
+
"Optional explicit props.to value for generated menu placement (example: ./pets). Required when adding placement for dynamic directory-prefix/route-path values."
|
|
84
100
|
}
|
|
85
101
|
},
|
|
86
102
|
dependsOn: [],
|
|
@@ -120,7 +136,7 @@ export default Object.freeze({
|
|
|
120
136
|
mutations: {
|
|
121
137
|
dependencies: {
|
|
122
138
|
runtime: {
|
|
123
|
-
"@jskit-ai/users-web": "0.1.
|
|
139
|
+
"@jskit-ai/users-web": "0.1.37"
|
|
124
140
|
},
|
|
125
141
|
dev: {}
|
|
126
142
|
},
|
|
@@ -204,7 +220,7 @@ export default Object.freeze({
|
|
|
204
220
|
skipIfContains:
|
|
205
221
|
"jskit:ui-generator.menu:${option:namespace|kebab}:${option:directory-prefix|path}:${option:container|path}:${option:route-path|path}",
|
|
206
222
|
value:
|
|
207
|
-
"\n// jskit:ui-generator.menu:${option:namespace|kebab}:${option:directory-prefix|path}:${option:container|path}:${option:route-path|path}\n{\n addPlacement({\n id: \"ui-generator.${option:namespace|kebab}.menu\",\n host: \"__JSKIT_UI_MENU_PLACEMENT_HOST__\",\n position: \"__JSKIT_UI_MENU_PLACEMENT_POSITION__\",\n surfaces: [\"${option:surface|lower}\"],\n order: 155,\n componentToken: \"__JSKIT_UI_MENU_COMPONENT_TOKEN__\",\n props: {\n label: \"${option:namespace|plural|pascal}\",\n surface: \"${option:surface|lower}\",\n workspaceSuffix: \"
|
|
223
|
+
"\n// jskit:ui-generator.menu:${option:namespace|kebab}:${option:directory-prefix|path}:${option:container|path}:${option:route-path|path}\n{\n addPlacement({\n id: \"ui-generator.${option:namespace|kebab}.menu\",\n host: \"__JSKIT_UI_MENU_PLACEMENT_HOST__\",\n position: \"__JSKIT_UI_MENU_PLACEMENT_POSITION__\",\n surfaces: [\"${option:surface|lower}\"],\n order: 155,\n componentToken: \"__JSKIT_UI_MENU_COMPONENT_TOKEN__\",\n props: {\n label: \"${option:namespace|plural|pascal}\",\n surface: \"${option:surface|lower}\",\n workspaceSuffix: \"__JSKIT_UI_MENU_WORKSPACE_SUFFIX__\",\n nonWorkspaceSuffix: \"__JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__\",\n__JSKIT_UI_MENU_TO_PROP_LINE__ },\n when: ({ auth }) => Boolean(auth?.authenticated)\n });\n}\n",
|
|
208
224
|
reason: "Append generated UI menu placement.",
|
|
209
225
|
category: "ui-generator",
|
|
210
226
|
id: "ui-generator-placement-menu",
|
|
@@ -219,8 +235,32 @@ export default Object.freeze({
|
|
|
219
235
|
in: ["list"]
|
|
220
236
|
},
|
|
221
237
|
{
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
any: [
|
|
239
|
+
{
|
|
240
|
+
all: [
|
|
241
|
+
{
|
|
242
|
+
option: "route-path",
|
|
243
|
+
notContains: "["
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
option: "directory-prefix",
|
|
247
|
+
notContains: "["
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
all: [
|
|
253
|
+
{
|
|
254
|
+
option: "placement",
|
|
255
|
+
contains: ":"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
option: "placement-to",
|
|
259
|
+
notEquals: ""
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
]
|
|
224
264
|
}
|
|
225
265
|
]
|
|
226
266
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-ui-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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.
|
|
9
|
+
"@jskit-ai/crud-core": "0.1.31",
|
|
10
|
+
"@jskit-ai/kernel": "0.1.23"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
"./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
|
|
@@ -27,7 +27,24 @@ const ALLOWED_OPERATIONS = new Set(["list", "view", "new", "edit"]);
|
|
|
27
27
|
const DEFAULT_LIST_HIDDEN_FIELD_KEYS = new Set(["createdAt", "updatedAt"]);
|
|
28
28
|
const CONTAINER_TOKEN_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
|
|
29
29
|
const DEFAULT_MENU_COMPONENT_TOKEN = "users.web.shell.surface-aware-menu-link-item";
|
|
30
|
-
const CONTAINER_MENU_COMPONENT_TOKEN = "local.main.ui.
|
|
30
|
+
const CONTAINER_MENU_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
|
|
31
|
+
|
|
32
|
+
function splitPathSegments(value = "") {
|
|
33
|
+
return normalizeText(value)
|
|
34
|
+
.replaceAll("\\", "/")
|
|
35
|
+
.split("/")
|
|
36
|
+
.map((entry) => normalizeText(entry))
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRouteGroupSegment(value = "") {
|
|
41
|
+
const source = normalizeText(value);
|
|
42
|
+
return source.startsWith("(") && source.endsWith(")");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function joinPathSegments(segments = []) {
|
|
46
|
+
return (Array.isArray(segments) ? segments : []).join("/");
|
|
47
|
+
}
|
|
31
48
|
|
|
32
49
|
function resolveContainerOption(options = {}) {
|
|
33
50
|
const container = normalizeText(options?.container).toLowerCase();
|
|
@@ -45,31 +62,50 @@ function resolveContainerOption(options = {}) {
|
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
function resolveRoutePathWithContainer(options = {}) {
|
|
48
|
-
const routePath = normalizeText(options?.["route-path"]);
|
|
49
|
-
if (!routePath) {
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
65
|
const container = resolveContainerOption(options);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
const routeSegments = [
|
|
67
|
+
...splitPathSegments(options?.["directory-prefix"]),
|
|
68
|
+
...splitPathSegments(container),
|
|
69
|
+
...splitPathSegments(options?.["route-path"])
|
|
70
|
+
];
|
|
71
|
+
return joinPathSegments(routeSegments);
|
|
72
|
+
}
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
function resolvePlacementUrlSuffix(options = {}) {
|
|
75
|
+
const routeSegments = splitPathSegments(resolveRoutePathWithContainer(options))
|
|
76
|
+
.filter((segment) => !isRouteGroupSegment(segment));
|
|
77
|
+
if (routeSegments.length < 1) {
|
|
78
|
+
return "/";
|
|
79
|
+
}
|
|
80
|
+
return `/${joinPathSegments(routeSegments)}`;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
function resolveMenuComponentToken(options = {}) {
|
|
84
|
+
const explicitToken = normalizeText(options?.["placement-component-token"]);
|
|
85
|
+
if (explicitToken) {
|
|
86
|
+
return explicitToken;
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
const container = resolveContainerOption(options);
|
|
63
90
|
return container ? CONTAINER_MENU_COMPONENT_TOKEN : DEFAULT_MENU_COMPONENT_TOKEN;
|
|
64
91
|
}
|
|
65
92
|
|
|
93
|
+
function resolveMenuToPropLine(options = {}) {
|
|
94
|
+
const placementTo = normalizeText(options?.["placement-to"]);
|
|
95
|
+
if (!placementTo) {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return ` to: ${JSON.stringify(placementTo)},\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
66
102
|
async function resolveMenuPlacementTarget({ appRoot, options, hasListOperation } = {}) {
|
|
67
103
|
if (hasListOperation !== true) {
|
|
68
104
|
return null;
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
const routePath = resolveRoutePathWithContainer(options);
|
|
72
|
-
if (!routePath
|
|
108
|
+
if (!routePath) {
|
|
73
109
|
return null;
|
|
74
110
|
}
|
|
75
111
|
|
|
@@ -209,6 +245,22 @@ function ensureFields(fields, fallbackFields = createFieldDefinitions({})) {
|
|
|
209
245
|
return fallbackFields;
|
|
210
246
|
}
|
|
211
247
|
|
|
248
|
+
function resolveViewTitleFallbackFieldKey(fields = []) {
|
|
249
|
+
const sourceFields = Array.isArray(fields) ? fields : [];
|
|
250
|
+
for (const field of sourceFields) {
|
|
251
|
+
if (normalizeText(field?.type).toLowerCase() !== "string") {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const key = normalizeText(field?.key);
|
|
256
|
+
if (key) {
|
|
257
|
+
return key;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
|
|
212
264
|
async function buildUiTemplateContext({ appRoot, options } = {}) {
|
|
213
265
|
const selectedOperations = parseOperationsOption(options);
|
|
214
266
|
const selectedDisplayFields = parseDisplayFieldsOption(options);
|
|
@@ -298,6 +350,9 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
|
|
|
298
350
|
const viewFields = hasViewOperation
|
|
299
351
|
? filterDisplayFields(selectedDisplayFields, ensureFields(viewFieldsAll))
|
|
300
352
|
: createFieldDefinitions({});
|
|
353
|
+
const viewTitleFallbackFieldKey = hasViewOperation
|
|
354
|
+
? resolveViewTitleFallbackFieldKey(viewFieldsAll)
|
|
355
|
+
: "";
|
|
301
356
|
const createFields = hasNewOperation
|
|
302
357
|
? filterDisplayFields(selectedDisplayFields, createFieldsAll)
|
|
303
358
|
: [];
|
|
@@ -325,6 +380,7 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
|
|
|
325
380
|
__JSKIT_UI_LIST_REALTIME_EVENTS__: JSON.stringify(listRealtimeEvents),
|
|
326
381
|
__JSKIT_UI_LIST_RECORD_ID_EXPR__: resolveRecordIdExpression(recordIdFields),
|
|
327
382
|
__JSKIT_UI_VIEW_COLUMNS__: buildViewColumns(viewFields),
|
|
383
|
+
__JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__: JSON.stringify(viewTitleFallbackFieldKey),
|
|
328
384
|
__JSKIT_UI_RECORD_CHANGED_EVENT__: JSON.stringify(defaultRecordChangedEvent),
|
|
329
385
|
__JSKIT_UI_HAS_LIST_ROUTE__: hasListOperation ? "true" : "false",
|
|
330
386
|
__JSKIT_UI_HAS_VIEW_ROUTE__: hasViewOperation ? "true" : "false",
|
|
@@ -338,7 +394,10 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
|
|
|
338
394
|
__JSKIT_UI_EDIT_FORM_FIELD_PUSH_LINES__: renderObjectPushLines("UI_EDIT_FORM_FIELDS", editFields),
|
|
339
395
|
__JSKIT_UI_MENU_PLACEMENT_HOST__: normalizeText(menuPlacementTarget?.host),
|
|
340
396
|
__JSKIT_UI_MENU_PLACEMENT_POSITION__: normalizeText(menuPlacementTarget?.position),
|
|
341
|
-
__JSKIT_UI_MENU_COMPONENT_TOKEN__: resolveMenuComponentToken(options)
|
|
397
|
+
__JSKIT_UI_MENU_COMPONENT_TOKEN__: resolveMenuComponentToken(options),
|
|
398
|
+
__JSKIT_UI_MENU_WORKSPACE_SUFFIX__: resolvePlacementUrlSuffix(options),
|
|
399
|
+
__JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__: resolvePlacementUrlSuffix(options),
|
|
400
|
+
__JSKIT_UI_MENU_TO_PROP_LINE__: resolveMenuToPropLine(options)
|
|
342
401
|
};
|
|
343
402
|
}
|
|
344
403
|
|
|
@@ -281,6 +281,62 @@ function toFieldLabel(key) {
|
|
|
281
281
|
.join(" ");
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
function isSupportedSelectOptionValue(value) {
|
|
285
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function toSelectOptionLabel(value) {
|
|
289
|
+
if (typeof value === "string") {
|
|
290
|
+
const normalizedValue = normalizeText(value);
|
|
291
|
+
return normalizedValue ? toFieldLabel(normalizedValue) : "";
|
|
292
|
+
}
|
|
293
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
294
|
+
return String(value);
|
|
295
|
+
}
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function toSelectOptionIdentity(value) {
|
|
300
|
+
return `${typeof value}:${String(value)}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizeFieldUiOptions(rawOptions, { context = "resource fieldMeta ui.options" } = {}) {
|
|
304
|
+
if (rawOptions === undefined || rawOptions === null) {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
if (!Array.isArray(rawOptions)) {
|
|
308
|
+
throw new Error(`${context} must be an array of { value, label? } entries.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const options = [];
|
|
312
|
+
const seenValues = new Set();
|
|
313
|
+
for (const [index, rawEntry] of rawOptions.entries()) {
|
|
314
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
315
|
+
throw new Error(`${context}[${index}] must be an object.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const value = rawEntry.value;
|
|
319
|
+
if (!isSupportedSelectOptionValue(value)) {
|
|
320
|
+
throw new Error(`${context}[${index}].value must be a string, number, or boolean.`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const identity = toSelectOptionIdentity(value);
|
|
324
|
+
if (seenValues.has(identity)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
seenValues.add(identity);
|
|
328
|
+
|
|
329
|
+
const explicitLabel = normalizeText(rawEntry.label);
|
|
330
|
+
const fallbackLabel = toSelectOptionLabel(value);
|
|
331
|
+
options.push({
|
|
332
|
+
value,
|
|
333
|
+
label: explicitLabel || fallbackLabel || String(value)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return options;
|
|
338
|
+
}
|
|
339
|
+
|
|
284
340
|
function stripLookupIdSuffix(key = "") {
|
|
285
341
|
const normalizedKey = normalizeText(key);
|
|
286
342
|
if (!normalizedKey) {
|
|
@@ -425,6 +481,9 @@ function buildResourceFieldMetaMap(resource = {}) {
|
|
|
425
481
|
}
|
|
426
482
|
|
|
427
483
|
const relation = normalizeLookupRelation(rawEntry.relation);
|
|
484
|
+
const fieldUiOptions = normalizeFieldUiOptions(rawEntry?.ui?.options, {
|
|
485
|
+
context: `resource.fieldMeta["${key}"].ui.options`
|
|
486
|
+
});
|
|
428
487
|
if (relation) {
|
|
429
488
|
nextEntry.relation = relation;
|
|
430
489
|
const formControl = checkCrudLookupFormControl(rawEntry?.ui?.formControl, {
|
|
@@ -437,6 +496,12 @@ function buildResourceFieldMetaMap(resource = {}) {
|
|
|
437
496
|
};
|
|
438
497
|
}
|
|
439
498
|
}
|
|
499
|
+
if (fieldUiOptions.length > 0) {
|
|
500
|
+
nextEntry.ui = {
|
|
501
|
+
...(nextEntry.ui || {}),
|
|
502
|
+
options: fieldUiOptions
|
|
503
|
+
};
|
|
504
|
+
}
|
|
440
505
|
|
|
441
506
|
map[key] = nextEntry;
|
|
442
507
|
}
|
|
@@ -559,6 +624,16 @@ function createFormFieldDefinitions(
|
|
|
559
624
|
|
|
560
625
|
const schemaType = resolveSchemaType(schema);
|
|
561
626
|
const relation = toLookupRelation(fieldMetaMap, key, { lookupContainerKey });
|
|
627
|
+
const fieldUiOptions = Array.isArray(fieldMetaMap?.[key]?.ui?.options)
|
|
628
|
+
? fieldMetaMap[key].ui.options
|
|
629
|
+
: [];
|
|
630
|
+
const schemaEnumValues = Array.isArray(schemaType.schema?.enum) ? schemaType.schema.enum : [];
|
|
631
|
+
if (!relation && schemaEnumValues.length > 0 && fieldUiOptions.length < 1) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`resource form field "${key}" defines schema enum values but is missing resource.fieldMeta["${key}"].ui.options.`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
const selectOptions = relation ? [] : fieldUiOptions;
|
|
562
637
|
const lookupFormControl = relation
|
|
563
638
|
? checkCrudLookupFormControl(fieldMetaMap?.[key]?.ui?.formControl, {
|
|
564
639
|
context: `resource.fieldMeta["${key}"].ui.formControl`,
|
|
@@ -573,9 +648,14 @@ function createFormFieldDefinitions(
|
|
|
573
648
|
nullable: schemaType.nullable,
|
|
574
649
|
relation,
|
|
575
650
|
inputType: resolveFormInputType(schemaType.type, schemaType.format),
|
|
576
|
-
component:
|
|
651
|
+
component: selectOptions.length > 0
|
|
652
|
+
? "select"
|
|
653
|
+
: resolveFormFieldComponent(schemaType.type, relation),
|
|
577
654
|
maxLength: toPositiveInteger(schemaType.schema?.maxLength)
|
|
578
655
|
};
|
|
656
|
+
if (selectOptions.length > 0) {
|
|
657
|
+
fieldDefinition.options = selectOptions;
|
|
658
|
+
}
|
|
579
659
|
if (normalizedParentRouteParamKey && key === normalizedParentRouteParamKey) {
|
|
580
660
|
fieldDefinition.hidden = true;
|
|
581
661
|
fieldDefinition.routeParamKey = normalizedParentRouteParamKey;
|
|
@@ -622,6 +702,10 @@ function escapeHtml(value) {
|
|
|
622
702
|
.replaceAll("'", "'");
|
|
623
703
|
}
|
|
624
704
|
|
|
705
|
+
function serializeTemplateBindingValue(value) {
|
|
706
|
+
return JSON.stringify(value).replaceAll("'", "\\u0027");
|
|
707
|
+
}
|
|
708
|
+
|
|
625
709
|
function buildListHeaderColumns(fields = []) {
|
|
626
710
|
return (Array.isArray(fields) ? fields : [])
|
|
627
711
|
.map((field) => ` <th>${escapeHtml(field.label)}</th>`)
|
|
@@ -723,11 +807,25 @@ function buildFormColumns(fields = []) {
|
|
|
723
807
|
label="${label}"
|
|
724
808
|
color="primary"
|
|
725
809
|
hide-details="auto"
|
|
726
|
-
:disabled="
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
810
|
+
:disabled="formRuntime.addEdit.isFieldLocked"
|
|
811
|
+
:error-messages='${fieldErrorExpression}'
|
|
812
|
+
/>
|
|
813
|
+
</v-col>`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (component === "select") {
|
|
817
|
+
const selectOptions = Array.isArray(field?.options) ? field.options : [];
|
|
818
|
+
return ` <v-col cols="12" md="6">
|
|
819
|
+
<v-select
|
|
820
|
+
v-model="${formAccessor}"
|
|
821
|
+
label="${label}"
|
|
822
|
+
variant="outlined"
|
|
823
|
+
density="comfortable"
|
|
824
|
+
:items='${serializeTemplateBindingValue(selectOptions)}'
|
|
825
|
+
item-title="label"
|
|
826
|
+
item-value="value"
|
|
827
|
+
:disabled="formRuntime.addEdit.isFieldLocked"
|
|
828
|
+
:clearable="${field.nullable === true ? "true" : "false"}"
|
|
731
829
|
:error-messages='${fieldErrorExpression}'
|
|
732
830
|
/>
|
|
733
831
|
</v-col>`;
|
|
@@ -753,11 +851,7 @@ function buildFormColumns(fields = []) {
|
|
|
753
851
|
item-value="value"
|
|
754
852
|
${lookupNoFilterLine}
|
|
755
853
|
:loading='resolveLookupLoading(${JSON.stringify(key)})'
|
|
756
|
-
:disabled="
|
|
757
|
-
!formRuntime.addEdit.canSave ||
|
|
758
|
-
formRuntime.addEdit.isSaving ||
|
|
759
|
-
formRuntime.addEdit.isRefetching
|
|
760
|
-
"
|
|
854
|
+
:disabled="formRuntime.addEdit.isFieldLocked"
|
|
761
855
|
:clearable="${field.nullable === true ? "true" : "false"}"
|
|
762
856
|
:error-messages='${fieldErrorExpression}'
|
|
763
857
|
/>
|
|
@@ -776,11 +870,7 @@ function buildFormColumns(fields = []) {
|
|
|
776
870
|
variant="outlined"
|
|
777
871
|
density="comfortable"
|
|
778
872
|
:maxlength="${maxLength}"
|
|
779
|
-
:readonly="
|
|
780
|
-
!formRuntime.addEdit.canSave ||
|
|
781
|
-
formRuntime.addEdit.isSaving ||
|
|
782
|
-
formRuntime.addEdit.isRefetching
|
|
783
|
-
"
|
|
873
|
+
:readonly="formRuntime.addEdit.isFieldLocked"
|
|
784
874
|
:error-messages='${fieldErrorExpression}'
|
|
785
875
|
/>
|
|
786
876
|
</v-col>`;
|
|
@@ -11,18 +11,14 @@
|
|
|
11
11
|
<v-btn
|
|
12
12
|
v-if="UI_CANCEL_URL"
|
|
13
13
|
variant="text"
|
|
14
|
-
:to="formRuntime.addEdit.resolveParams(UI_CANCEL_URL)"
|
|
14
|
+
:to="{ path: formRuntime.addEdit.resolveParams(UI_CANCEL_URL), query: $route.query }"
|
|
15
15
|
>
|
|
16
16
|
Cancel
|
|
17
17
|
</v-btn>
|
|
18
18
|
<v-btn
|
|
19
19
|
color="primary"
|
|
20
20
|
:loading="formRuntime.addEdit.isSaving"
|
|
21
|
-
:disabled="
|
|
22
|
-
formRuntime.addEdit.isInitialLoading ||
|
|
23
|
-
formRuntime.addEdit.isRefetching ||
|
|
24
|
-
!formRuntime.addEdit.canSave
|
|
25
|
-
"
|
|
21
|
+
:disabled="formRuntime.addEdit.isSubmitDisabled"
|
|
26
22
|
@click="formRuntime.addEdit.submit"
|
|
27
23
|
>
|
|
28
24
|
Save changes
|
|
@@ -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"
|
|
44
|
-
<th v-if="UI_EDIT_URL" class="text-right"
|
|
43
|
+
<th v-if="UI_VIEW_URL" class="text-right"></th>
|
|
44
|
+
<th v-if="UI_EDIT_URL" class="text-right"></th>
|
|
45
45
|
</tr>
|
|
46
46
|
</thead>
|
|
47
47
|
<tbody>
|
|
@@ -52,7 +52,7 @@ __JSKIT_UI_LIST_ROW_COLUMNS__
|
|
|
52
52
|
<v-btn
|
|
53
53
|
size="small"
|
|
54
54
|
variant="text"
|
|
55
|
-
:to="records.resolveViewUrl(record)"
|
|
55
|
+
:to="{ path: records.resolveViewUrl(record), query: $route.query }"
|
|
56
56
|
:disabled="!records.resolveViewUrl(record)"
|
|
57
57
|
>
|
|
58
58
|
Open
|
|
@@ -62,7 +62,7 @@ __JSKIT_UI_LIST_ROW_COLUMNS__
|
|
|
62
62
|
<v-btn
|
|
63
63
|
size="small"
|
|
64
64
|
variant="text"
|
|
65
|
-
:to="records.resolveEditUrl(record)"
|
|
65
|
+
:to="{ path: records.resolveEditUrl(record), query: $route.query }"
|
|
66
66
|
:disabled="!records.resolveEditUrl(record)"
|
|
67
67
|
>
|
|
68
68
|
Edit
|
|
@@ -92,6 +92,7 @@ const UI_VIEW_URL = __JSKIT_UI_HAS_VIEW_ROUTE__ ? `./:${UI_RECORD_ID_PARAM}` : "
|
|
|
92
92
|
const UI_EDIT_URL = __JSKIT_UI_HAS_EDIT_ROUTE__ ? `./:${UI_RECORD_ID_PARAM}/edit` : "";
|
|
93
93
|
const UI_NEW_URL = __JSKIT_UI_HAS_NEW_ROUTE__ ? "./new" : "";
|
|
94
94
|
const UI_RECORD_CHANGED_EVENTS = __JSKIT_UI_LIST_REALTIME_EVENTS__;
|
|
95
|
+
const UI_ROUTE_QUERY_BLACKLIST = Object.freeze(["include", "cursor", "limit"]);
|
|
95
96
|
|
|
96
97
|
const records = useList({
|
|
97
98
|
adapter: UI_OPERATION_ADAPTER || undefined,
|
|
@@ -108,6 +109,13 @@ const records = useList({
|
|
|
108
109
|
enabled: true,
|
|
109
110
|
mode: "query"
|
|
110
111
|
},
|
|
112
|
+
syncToRoute: {
|
|
113
|
+
enabled: true,
|
|
114
|
+
mode: "replace",
|
|
115
|
+
search: true,
|
|
116
|
+
queryParams: true,
|
|
117
|
+
queryParamBlacklist: UI_ROUTE_QUERY_BLACKLIST
|
|
118
|
+
},
|
|
111
119
|
placementSource: "ui-generator.${option:namespace|kebab}.list",
|
|
112
120
|
fallbackLoadError: "Unable to load records.",
|
|
113
121
|
recordIdParam: UI_RECORD_ID_PARAM,
|
|
@@ -12,11 +12,7 @@
|
|
|
12
12
|
<v-btn
|
|
13
13
|
color="primary"
|
|
14
14
|
:loading="formRuntime.addEdit.isSaving"
|
|
15
|
-
:disabled="
|
|
16
|
-
formRuntime.addEdit.isInitialLoading ||
|
|
17
|
-
formRuntime.addEdit.isRefetching ||
|
|
18
|
-
!formRuntime.addEdit.canSave
|
|
19
|
-
"
|
|
15
|
+
:disabled="formRuntime.addEdit.isSubmitDisabled"
|
|
20
16
|
@click="formRuntime.addEdit.submit"
|
|
21
17
|
>
|
|
22
18
|
Save ${option:namespace|singular|default(record)}
|
|
@@ -4,12 +4,32 @@
|
|
|
4
4
|
<v-card-item>
|
|
5
5
|
<div class="d-flex align-center ga-3 flex-wrap w-100">
|
|
6
6
|
<div>
|
|
7
|
-
<v-card-title class="px-0"
|
|
7
|
+
<v-card-title class="px-0">
|
|
8
|
+
{{
|
|
9
|
+
view.resolveRecordTitle(view.record, {
|
|
10
|
+
fallbackKey: UI_VIEW_TITLE_FALLBACK_FIELD_KEY,
|
|
11
|
+
defaultValue: "${option:namespace|singular|pascal|default(Record)}"
|
|
12
|
+
})
|
|
13
|
+
}}
|
|
14
|
+
</v-card-title>
|
|
8
15
|
<v-card-subtitle class="px-0">View and manage this ${option:namespace|singular|default(record)}.</v-card-subtitle>
|
|
9
16
|
</div>
|
|
10
17
|
<v-spacer />
|
|
11
|
-
<v-btn
|
|
12
|
-
|
|
18
|
+
<v-btn
|
|
19
|
+
v-if="UI_LIST_URL"
|
|
20
|
+
variant="text"
|
|
21
|
+
:to="{ path: view.resolveParams(UI_LIST_URL), query: $route.query }"
|
|
22
|
+
>
|
|
23
|
+
Back to ${option:namespace|plural|default(records)}
|
|
24
|
+
</v-btn>
|
|
25
|
+
<v-btn
|
|
26
|
+
v-if="UI_EDIT_URL"
|
|
27
|
+
color="primary"
|
|
28
|
+
variant="outlined"
|
|
29
|
+
:to="{ path: view.resolveParams(UI_EDIT_URL), query: $route.query }"
|
|
30
|
+
>
|
|
31
|
+
Edit
|
|
32
|
+
</v-btn>
|
|
13
33
|
</div>
|
|
14
34
|
</v-card-item>
|
|
15
35
|
<v-divider />
|
|
@@ -43,6 +63,7 @@ const UI_API_BASE_URL = "${option:api-path|trim}";
|
|
|
43
63
|
const UI_VIEW_API_URL = `${UI_API_BASE_URL}/:${UI_RECORD_ID_PARAM}`;
|
|
44
64
|
const UI_LIST_URL = __JSKIT_UI_HAS_LIST_ROUTE__ ? ".." : "";
|
|
45
65
|
const UI_EDIT_URL = __JSKIT_UI_HAS_EDIT_ROUTE__ ? "./edit" : "";
|
|
66
|
+
const UI_VIEW_TITLE_FALLBACK_FIELD_KEY = __JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__;
|
|
46
67
|
const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
|
|
47
68
|
|
|
48
69
|
const view = useView({
|
|
@@ -193,6 +193,7 @@ test("buildUiTemplateContext derives list/view/new/edit placeholders from resour
|
|
|
193
193
|
assert.doesNotMatch(context.__JSKIT_UI_LIST_ROW_COLUMNS__, /record\.updatedAt/);
|
|
194
194
|
assert.match(context.__JSKIT_UI_VIEW_COLUMNS__, /view\.record\?\.vip/);
|
|
195
195
|
assert.match(context.__JSKIT_UI_VIEW_COLUMNS__, /view\.record\?\.updatedAt/);
|
|
196
|
+
assert.equal(context.__JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__, "\"firstName\"");
|
|
196
197
|
assert.equal(context.__JSKIT_UI_LIST_RECORD_ID_EXPR__, "item.id");
|
|
197
198
|
assert.equal(context.__JSKIT_UI_RECORD_CHANGED_EVENT__, "\"customers.record.changed\"");
|
|
198
199
|
assert.equal(context.__JSKIT_UI_LIST_REALTIME_EVENTS__, "[\"customers.record.changed\"]");
|
|
@@ -495,10 +496,149 @@ export { resource };
|
|
|
495
496
|
assert.equal(context.__JSKIT_UI_HAS_VIEW_ROUTE__, "true");
|
|
496
497
|
assert.equal(context.__JSKIT_UI_HAS_NEW_ROUTE__, "false");
|
|
497
498
|
assert.equal(context.__JSKIT_UI_HAS_EDIT_ROUTE__, "false");
|
|
499
|
+
assert.equal(context.__JSKIT_UI_VIEW_TITLE_FALLBACK_FIELD_KEY__, "\"fullName\"");
|
|
498
500
|
assert.equal(context.__JSKIT_UI_RECORD_CHANGED_EVENT__, "\"customers.record.changed\"");
|
|
499
501
|
});
|
|
500
502
|
});
|
|
501
503
|
|
|
504
|
+
test("buildUiTemplateContext renders enum fields as select controls in forms", async () => {
|
|
505
|
+
await withTempApp(async (appRoot) => {
|
|
506
|
+
const resourceFile = "packages/pets/src/shared/petResource.js";
|
|
507
|
+
await writeResource(
|
|
508
|
+
appRoot,
|
|
509
|
+
resourceFile,
|
|
510
|
+
`const resource = {
|
|
511
|
+
operations: {
|
|
512
|
+
create: {
|
|
513
|
+
bodyValidator: {
|
|
514
|
+
schema: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: {
|
|
517
|
+
temperament: { type: "string", enum: ["relaxed", "friendly_excitable", "unknown"] }
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
outputValidator: {
|
|
522
|
+
schema: {
|
|
523
|
+
type: "object",
|
|
524
|
+
properties: {
|
|
525
|
+
id: { type: "integer" }
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
patch: {
|
|
531
|
+
bodyValidator: {
|
|
532
|
+
schema: {
|
|
533
|
+
type: "object",
|
|
534
|
+
properties: {
|
|
535
|
+
temperament: { type: "string", enum: ["relaxed", "friendly_excitable", "unknown"] }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
outputValidator: {
|
|
540
|
+
schema: {
|
|
541
|
+
type: "object",
|
|
542
|
+
properties: {
|
|
543
|
+
id: { type: "integer" }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
fieldMeta: [
|
|
550
|
+
{
|
|
551
|
+
key: "temperament",
|
|
552
|
+
ui: {
|
|
553
|
+
formControl: "select",
|
|
554
|
+
options: [
|
|
555
|
+
{ value: "relaxed", label: "Relaxed" },
|
|
556
|
+
{ value: "friendly_excitable", label: "Friendly / Excitable" },
|
|
557
|
+
{ value: "unknown", label: "Unknown temperament" }
|
|
558
|
+
]
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
export { resource };
|
|
565
|
+
`
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const context = await buildUiTemplateContext({
|
|
569
|
+
appRoot,
|
|
570
|
+
options: {
|
|
571
|
+
namespace: "pets-ui",
|
|
572
|
+
"api-path": "/pets",
|
|
573
|
+
operations: "new,edit",
|
|
574
|
+
"resource-file": resourceFile
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const createFields = JSON.parse(context.__JSKIT_UI_CREATE_FORM_FIELDS__);
|
|
579
|
+
const temperament = createFields.find((field) => field.key === "temperament");
|
|
580
|
+
assert.ok(temperament);
|
|
581
|
+
assert.equal(temperament.component, "select");
|
|
582
|
+
assert.deepEqual(temperament.options, [
|
|
583
|
+
{ value: "relaxed", label: "Relaxed" },
|
|
584
|
+
{ value: "friendly_excitable", label: "Friendly / Excitable" },
|
|
585
|
+
{ value: "unknown", label: "Unknown temperament" }
|
|
586
|
+
]);
|
|
587
|
+
assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /<v-select/);
|
|
588
|
+
assert.match(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /:disabled="formRuntime\.addEdit\.isFieldLocked"/);
|
|
589
|
+
assert.doesNotMatch(context.__JSKIT_UI_CREATE_FORM_COLUMNS__, /!formRuntime\.addEdit\.canSave/);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("buildUiTemplateContext fails when enum schema field omits resource fieldMeta ui.options", async () => {
|
|
594
|
+
await withTempApp(async (appRoot) => {
|
|
595
|
+
const resourceFile = "packages/pets/src/shared/petResource.js";
|
|
596
|
+
await writeResource(
|
|
597
|
+
appRoot,
|
|
598
|
+
resourceFile,
|
|
599
|
+
`const resource = {
|
|
600
|
+
operations: {
|
|
601
|
+
create: {
|
|
602
|
+
bodyValidator: {
|
|
603
|
+
schema: {
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: {
|
|
606
|
+
temperament: { type: "string", enum: ["relaxed", "unknown"] }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
outputValidator: {
|
|
611
|
+
schema: {
|
|
612
|
+
type: "object",
|
|
613
|
+
properties: {
|
|
614
|
+
id: { type: "integer" }
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
export { resource };
|
|
623
|
+
`
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
await assert.rejects(
|
|
627
|
+
() =>
|
|
628
|
+
buildUiTemplateContext({
|
|
629
|
+
appRoot,
|
|
630
|
+
options: {
|
|
631
|
+
namespace: "pets-ui",
|
|
632
|
+
"api-path": "/pets",
|
|
633
|
+
operations: "new",
|
|
634
|
+
"resource-file": resourceFile
|
|
635
|
+
}
|
|
636
|
+
}),
|
|
637
|
+
/schema enum values but is missing resource\.fieldMeta\["temperament"\]\.ui\.options/
|
|
638
|
+
);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
502
642
|
test("buildUiTemplateContext maps lookup relations from resource fieldMeta into lookup form fields", async () => {
|
|
503
643
|
await withTempApp(async (appRoot) => {
|
|
504
644
|
const resourceFile = "packages/contacts/src/shared/contactResource.js";
|
|
@@ -1049,7 +1189,7 @@ test("buildUiTemplateContext defaults list placement to container host when cont
|
|
|
1049
1189
|
|
|
1050
1190
|
assert.equal(context.__JSKIT_UI_MENU_PLACEMENT_HOST__, "practice");
|
|
1051
1191
|
assert.equal(context.__JSKIT_UI_MENU_PLACEMENT_POSITION__, "sub-pages");
|
|
1052
|
-
assert.equal(context.__JSKIT_UI_MENU_COMPONENT_TOKEN__, "local.main.ui.
|
|
1192
|
+
assert.equal(context.__JSKIT_UI_MENU_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
1053
1193
|
});
|
|
1054
1194
|
});
|
|
1055
1195
|
|
|
@@ -1105,3 +1245,85 @@ test("buildUiTemplateContext applies explicit placement override and validates t
|
|
|
1105
1245
|
);
|
|
1106
1246
|
});
|
|
1107
1247
|
});
|
|
1248
|
+
|
|
1249
|
+
test("buildUiTemplateContext allows placement component token override", async () => {
|
|
1250
|
+
await withTempApp(async (appRoot) => {
|
|
1251
|
+
const resourceFile = "packages/customers/src/shared/customerResource.js";
|
|
1252
|
+
await writeResource(appRoot, resourceFile, FULL_RESOURCE_SOURCE);
|
|
1253
|
+
await writeShellLayout(appRoot);
|
|
1254
|
+
|
|
1255
|
+
const context = await buildUiTemplateContext({
|
|
1256
|
+
appRoot,
|
|
1257
|
+
options: {
|
|
1258
|
+
namespace: "customers-ui",
|
|
1259
|
+
"api-path": "/crud/customers",
|
|
1260
|
+
"route-path": "ops/customers",
|
|
1261
|
+
operations: "list",
|
|
1262
|
+
placement: "shell-layout:secondary-menu",
|
|
1263
|
+
"placement-component-token": "local.main.ui.tab-link-item",
|
|
1264
|
+
"resource-file": resourceFile,
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
assert.equal(context.__JSKIT_UI_MENU_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
test("buildUiTemplateContext renders optional placement-to props line", async () => {
|
|
1273
|
+
await withTempApp(async (appRoot) => {
|
|
1274
|
+
const resourceFile = "packages/customers/src/shared/customerResource.js";
|
|
1275
|
+
await writeResource(appRoot, resourceFile, FULL_RESOURCE_SOURCE);
|
|
1276
|
+
await writeShellLayout(appRoot);
|
|
1277
|
+
|
|
1278
|
+
const defaultContext = await buildUiTemplateContext({
|
|
1279
|
+
appRoot,
|
|
1280
|
+
options: {
|
|
1281
|
+
namespace: "customers-ui",
|
|
1282
|
+
"api-path": "/crud/customers",
|
|
1283
|
+
"route-path": "ops/customers",
|
|
1284
|
+
operations: "list",
|
|
1285
|
+
placement: "shell-layout:secondary-menu",
|
|
1286
|
+
"resource-file": resourceFile,
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
assert.equal(defaultContext.__JSKIT_UI_MENU_TO_PROP_LINE__, "");
|
|
1290
|
+
|
|
1291
|
+
const explicitToContext = await buildUiTemplateContext({
|
|
1292
|
+
appRoot,
|
|
1293
|
+
options: {
|
|
1294
|
+
namespace: "customers-ui",
|
|
1295
|
+
"api-path": "/crud/customers",
|
|
1296
|
+
"route-path": "ops/customers",
|
|
1297
|
+
operations: "list",
|
|
1298
|
+
placement: "shell-layout:secondary-menu",
|
|
1299
|
+
"placement-to": "./pets",
|
|
1300
|
+
"resource-file": resourceFile,
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
assert.equal(explicitToContext.__JSKIT_UI_MENU_TO_PROP_LINE__, " to: \"./pets\",\n");
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
test("buildUiTemplateContext strips route-group filesystem segments from menu URL suffix", async () => {
|
|
1308
|
+
await withTempApp(async (appRoot) => {
|
|
1309
|
+
const resourceFile = "packages/customers/src/shared/customerResource.js";
|
|
1310
|
+
await writeResource(appRoot, resourceFile, FULL_RESOURCE_SOURCE);
|
|
1311
|
+
await writeShellLayout(appRoot);
|
|
1312
|
+
|
|
1313
|
+
const context = await buildUiTemplateContext({
|
|
1314
|
+
appRoot,
|
|
1315
|
+
options: {
|
|
1316
|
+
namespace: "customers-ui",
|
|
1317
|
+
"api-path": "/crud/customers",
|
|
1318
|
+
"directory-prefix": "ops/(nestedChildren)",
|
|
1319
|
+
"route-path": "customers",
|
|
1320
|
+
operations: "list",
|
|
1321
|
+
placement: "shell-layout:secondary-menu",
|
|
1322
|
+
"resource-file": resourceFile
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
assert.equal(context.__JSKIT_UI_MENU_WORKSPACE_SUFFIX__, "/ops/customers");
|
|
1327
|
+
assert.equal(context.__JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__, "/ops/customers");
|
|
1328
|
+
});
|
|
1329
|
+
});
|