@jskit-ai/ui-generator 0.1.15 → 0.1.17
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 +19 -16
- package/package.json +3 -2
- package/src/server/buildTemplateContext.js +2 -2
- package/src/server/subcommands/addSubpages.js +4 -5
- package/src/server/subcommands/element.js +1 -2
- package/src/server/subcommands/outlet.js +20 -21
- package/src/server/subcommands/page.js +16 -24
- package/src/server/subcommands/pageSupport.js +57 -114
- package/src/server/subcommands/support.js +5 -31
- package/test/addSubpagesSubcommand.test.js +38 -20
- package/test/buildTemplateContext.test.js +82 -28
- package/test/elementSubcommand.test.js +9 -7
- package/test/outletSubcommand.test.js +43 -17
- package/test/pageSubcommand.test.js +129 -14
- package/README.md +0 -214
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
resolveRequiredAppRoot,
|
|
4
4
|
toPosixPath
|
|
5
5
|
} from "@jskit-ai/kernel/server/support";
|
|
6
|
+
import { normalizeShellOutletTargetId } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
|
|
6
7
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
8
|
import { toCamelCase, toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
|
|
8
9
|
|
|
@@ -44,32 +45,11 @@ function requireSinglePositionalTargetFile(args = [], { context = "ui-generator"
|
|
|
44
45
|
return positionalArgs[0];
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function normalizeExplicitOutletTargetId(value = "") {
|
|
48
|
-
const normalizedValue = normalizeText(value);
|
|
49
|
-
if (!normalizedValue) {
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const separatorIndex = normalizedValue.indexOf(":");
|
|
54
|
-
if (separatorIndex <= 0 || separatorIndex >= normalizedValue.length - 1) {
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const host = normalizeText(normalizedValue.slice(0, separatorIndex));
|
|
59
|
-
const position = normalizeText(normalizedValue.slice(separatorIndex + 1));
|
|
60
|
-
if (!host || !position) {
|
|
61
|
-
return "";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return `${host}:${position}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
48
|
function resolveOutletTargetId(
|
|
68
49
|
rawTarget = "",
|
|
69
50
|
{
|
|
70
51
|
context = "ui-generator",
|
|
71
|
-
optionName = "target"
|
|
72
|
-
defaultPosition = ""
|
|
52
|
+
optionName = "target"
|
|
73
53
|
} = {}
|
|
74
54
|
) {
|
|
75
55
|
const normalizedTarget = normalizeText(rawTarget);
|
|
@@ -77,18 +57,13 @@ function resolveOutletTargetId(
|
|
|
77
57
|
throw new Error(`${context} requires --${optionName}.`);
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
const targetId = normalizedTarget
|
|
81
|
-
? normalizeExplicitOutletTargetId(normalizedTarget)
|
|
82
|
-
: normalizeExplicitOutletTargetId(`${normalizedTarget}:${normalizeText(defaultPosition)}`);
|
|
60
|
+
const targetId = normalizeShellOutletTargetId(normalizedTarget);
|
|
83
61
|
if (!targetId) {
|
|
84
|
-
throw new Error(`${context} option "${optionName}" must be
|
|
62
|
+
throw new Error(`${context} option "${optionName}" must be a target in "host:position" format.`);
|
|
85
63
|
}
|
|
86
64
|
|
|
87
|
-
const separatorIndex = targetId.indexOf(":");
|
|
88
65
|
return Object.freeze({
|
|
89
|
-
id: targetId
|
|
90
|
-
host: targetId.slice(0, separatorIndex),
|
|
91
|
-
position: targetId.slice(separatorIndex + 1)
|
|
66
|
+
id: targetId
|
|
92
67
|
});
|
|
93
68
|
}
|
|
94
69
|
|
|
@@ -295,7 +270,6 @@ export {
|
|
|
295
270
|
toPascalCase,
|
|
296
271
|
requireOption,
|
|
297
272
|
requireSinglePositionalTargetFile,
|
|
298
|
-
normalizeExplicitOutletTargetId,
|
|
299
273
|
resolveOutletTargetId,
|
|
300
274
|
rejectUnexpectedOptions,
|
|
301
275
|
resolvePathWithinApp,
|
|
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import test from "node:test";
|
|
6
|
+
import { readLocalLinkItemComponentSource } from "@jskit-ai/shell-web/server/support/localLinkItemScaffolds";
|
|
6
7
|
import { runGeneratorSubcommand } from "../src/server/subcommands/addSubpages.js";
|
|
7
8
|
|
|
8
9
|
async function withTempApp(run) {
|
|
@@ -105,14 +106,21 @@ test("ui-generator add-subpages derives the default target from an index-route p
|
|
|
105
106
|
|
|
106
107
|
assert.deepEqual(result.touchedFiles, [
|
|
107
108
|
"packages/main/src/client/providers/MainClientProvider.js",
|
|
109
|
+
"src/components/menus/TabLinkItem.vue",
|
|
108
110
|
"src/components/SectionContainerShell.vue",
|
|
109
|
-
"src/components/TabLinkItem.vue",
|
|
110
111
|
`src/pages/${targetFile}`
|
|
111
112
|
]);
|
|
112
113
|
|
|
113
114
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
114
|
-
assert.match(
|
|
115
|
+
assert.match(
|
|
116
|
+
pageSource,
|
|
117
|
+
/<ShellOutlet target="practice:sub-pages" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
|
|
118
|
+
);
|
|
115
119
|
assert.match(pageSource, /<RouterView \/>/);
|
|
120
|
+
assert.equal(
|
|
121
|
+
await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
|
|
122
|
+
await readLocalLinkItemComponentSource("local.main.ui.tab-link-item")
|
|
123
|
+
);
|
|
116
124
|
});
|
|
117
125
|
});
|
|
118
126
|
|
|
@@ -131,7 +139,10 @@ test("ui-generator add-subpages derives the default target from a dynamic file-r
|
|
|
131
139
|
});
|
|
132
140
|
|
|
133
141
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
134
|
-
assert.match(
|
|
142
|
+
assert.match(
|
|
143
|
+
pageSource,
|
|
144
|
+
/<ShellOutlet target="contacts-contact-id:sub-pages" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
|
|
145
|
+
);
|
|
135
146
|
});
|
|
136
147
|
});
|
|
137
148
|
|
|
@@ -150,28 +161,31 @@ test("ui-generator add-subpages derives the default target from a nested route p
|
|
|
150
161
|
});
|
|
151
162
|
|
|
152
163
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
153
|
-
assert.match(
|
|
164
|
+
assert.match(
|
|
165
|
+
pageSource,
|
|
166
|
+
/<ShellOutlet target="catalog-products:sub-pages" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
|
|
167
|
+
);
|
|
154
168
|
});
|
|
155
169
|
});
|
|
156
170
|
|
|
157
|
-
test("ui-generator add-subpages
|
|
171
|
+
test("ui-generator add-subpages rejects explicit target shorthand without a position", async () => {
|
|
158
172
|
await withTempApp(async (appRoot) => {
|
|
159
173
|
await writeAppFixture(appRoot);
|
|
160
174
|
|
|
161
175
|
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
162
176
|
await writePageFile(appRoot, targetFile);
|
|
163
177
|
|
|
164
|
-
await
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
await assert.rejects(
|
|
179
|
+
runGeneratorSubcommand({
|
|
180
|
+
appRoot,
|
|
181
|
+
subcommand: "add-subpages",
|
|
182
|
+
args: [targetFile],
|
|
183
|
+
options: {
|
|
184
|
+
target: "practice-hub"
|
|
185
|
+
}
|
|
186
|
+
}),
|
|
187
|
+
/option "target" must be a target in "host:position" format/
|
|
188
|
+
);
|
|
175
189
|
});
|
|
176
190
|
});
|
|
177
191
|
|
|
@@ -192,7 +206,10 @@ test("ui-generator add-subpages supports explicit target host:position", async (
|
|
|
192
206
|
});
|
|
193
207
|
|
|
194
208
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
195
|
-
assert.match(
|
|
209
|
+
assert.match(
|
|
210
|
+
pageSource,
|
|
211
|
+
/<ShellOutlet target="practice-hub:secondary-tabs" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
|
|
212
|
+
);
|
|
196
213
|
});
|
|
197
214
|
});
|
|
198
215
|
|
|
@@ -209,8 +226,9 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
|
|
|
209
226
|
customSectionShellSource,
|
|
210
227
|
"utf8"
|
|
211
228
|
);
|
|
229
|
+
await mkdir(path.join(appRoot, "src", "components", "menus"), { recursive: true });
|
|
212
230
|
await writeFile(
|
|
213
|
-
path.join(appRoot, "src", "components", "TabLinkItem.vue"),
|
|
231
|
+
path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"),
|
|
214
232
|
customTabLinkSource,
|
|
215
233
|
"utf8"
|
|
216
234
|
);
|
|
@@ -233,7 +251,7 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
|
|
|
233
251
|
customSectionShellSource
|
|
234
252
|
);
|
|
235
253
|
assert.equal(
|
|
236
|
-
await readFile(path.join(appRoot, "src", "components", "TabLinkItem.vue"), "utf8"),
|
|
254
|
+
await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
|
|
237
255
|
customTabLinkSource
|
|
238
256
|
);
|
|
239
257
|
});
|
|
@@ -299,7 +317,7 @@ test("ui-generator add-subpages validates target format", async () => {
|
|
|
299
317
|
target: "practice:"
|
|
300
318
|
}
|
|
301
319
|
}),
|
|
302
|
-
/option "target" must be
|
|
320
|
+
/option "target" must be a target in "host:position" format/
|
|
303
321
|
);
|
|
304
322
|
});
|
|
305
323
|
});
|
|
@@ -31,8 +31,12 @@ async function writeShellLayout(appRoot, source = "") {
|
|
|
31
31
|
source ||
|
|
32
32
|
`<template>
|
|
33
33
|
<div>
|
|
34
|
-
<ShellOutlet
|
|
35
|
-
<ShellOutlet
|
|
34
|
+
<ShellOutlet target="shell-layout:top-right" />
|
|
35
|
+
<ShellOutlet
|
|
36
|
+
target="shell-layout:primary-menu"
|
|
37
|
+
default
|
|
38
|
+
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
39
|
+
/>
|
|
36
40
|
</div>
|
|
37
41
|
</template>
|
|
38
42
|
`
|
|
@@ -57,16 +61,44 @@ test("buildUiPageTemplateContext resolves link placement from default app ShellO
|
|
|
57
61
|
targetFile: "admin/reports/index.vue",
|
|
58
62
|
options: {}
|
|
59
63
|
});
|
|
60
|
-
assert.equal(context.
|
|
61
|
-
assert.equal(context.
|
|
62
|
-
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "users.web.shell.surface-aware-menu-link-item");
|
|
64
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell-layout:primary-menu");
|
|
65
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
|
|
63
66
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
|
|
64
67
|
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
|
|
68
|
+
assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, "");
|
|
65
69
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, "");
|
|
66
70
|
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.reports.link");
|
|
67
71
|
});
|
|
68
72
|
});
|
|
69
73
|
|
|
74
|
+
test("buildUiPageTemplateContext derives an auth guard from an authenticated surface policy", async () => {
|
|
75
|
+
await withTempApp(async (appRoot) => {
|
|
76
|
+
await writeConfig(
|
|
77
|
+
appRoot,
|
|
78
|
+
`export const config = {
|
|
79
|
+
surfaceAccessPolicies: {
|
|
80
|
+
authenticated: {
|
|
81
|
+
requireAuth: true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
surfaceDefinitions: {
|
|
85
|
+
app: { id: "app", pagesRoot: "app", enabled: true, accessPolicyId: "authenticated" }
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
`
|
|
89
|
+
);
|
|
90
|
+
await writeShellLayout(appRoot);
|
|
91
|
+
|
|
92
|
+
const context = await buildUiPageTemplateContext({
|
|
93
|
+
appRoot,
|
|
94
|
+
targetFile: "app/reports/index.vue",
|
|
95
|
+
options: {}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, " when: ({ auth }) => Boolean(auth?.authenticated)\n");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
70
102
|
test("buildUiPageTemplateContext supports explicit link placement override", async () => {
|
|
71
103
|
await withTempApp(async (appRoot) => {
|
|
72
104
|
await writeConfig(
|
|
@@ -87,8 +119,8 @@ test("buildUiPageTemplateContext supports explicit link placement override", asy
|
|
|
87
119
|
"link-placement": "shell-layout:top-right"
|
|
88
120
|
}
|
|
89
121
|
});
|
|
90
|
-
assert.equal(context.
|
|
91
|
-
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "
|
|
122
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell-layout:top-right");
|
|
123
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
|
|
92
124
|
});
|
|
93
125
|
});
|
|
94
126
|
|
|
@@ -133,7 +165,11 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
133
165
|
ui: {
|
|
134
166
|
placements: {
|
|
135
167
|
outlets: [
|
|
136
|
-
{
|
|
168
|
+
{
|
|
169
|
+
target: "workspace-tools:primary-menu",
|
|
170
|
+
defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
171
|
+
source: "src/client/components/UsersWorkspaceToolsWidget.vue"
|
|
172
|
+
}
|
|
137
173
|
]
|
|
138
174
|
}
|
|
139
175
|
}
|
|
@@ -149,8 +185,7 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
149
185
|
"link-placement": "workspace-tools:primary-menu"
|
|
150
186
|
}
|
|
151
187
|
});
|
|
152
|
-
assert.equal(context.
|
|
153
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "primary-menu");
|
|
188
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "workspace-tools:primary-menu");
|
|
154
189
|
});
|
|
155
190
|
});
|
|
156
191
|
|
|
@@ -226,7 +261,7 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
|
|
|
226
261
|
`<template>
|
|
227
262
|
<SectionContainerShell>
|
|
228
263
|
<template #tabs>
|
|
229
|
-
<ShellOutlet
|
|
264
|
+
<ShellOutlet target="contact-view:sub-pages" />
|
|
230
265
|
</template>
|
|
231
266
|
<RouterView />
|
|
232
267
|
</SectionContainerShell>
|
|
@@ -240,8 +275,7 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
|
|
|
240
275
|
options: {}
|
|
241
276
|
});
|
|
242
277
|
|
|
243
|
-
assert.equal(context.
|
|
244
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
278
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "contact-view:sub-pages");
|
|
245
279
|
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
246
280
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
|
|
247
281
|
});
|
|
@@ -265,7 +299,7 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
|
|
|
265
299
|
`<template>
|
|
266
300
|
<SectionContainerShell>
|
|
267
301
|
<template #tabs>
|
|
268
|
-
<ShellOutlet
|
|
302
|
+
<ShellOutlet target="contact-view:sub-pages" />
|
|
269
303
|
</template>
|
|
270
304
|
<RouterView />
|
|
271
305
|
</SectionContainerShell>
|
|
@@ -279,8 +313,7 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
|
|
|
279
313
|
options: {}
|
|
280
314
|
});
|
|
281
315
|
|
|
282
|
-
assert.equal(context.
|
|
283
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
316
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "contact-view:sub-pages");
|
|
284
317
|
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
285
318
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes/history\",\n");
|
|
286
319
|
});
|
|
@@ -304,7 +337,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
|
|
|
304
337
|
`<template>
|
|
305
338
|
<SectionContainerShell>
|
|
306
339
|
<template #tabs>
|
|
307
|
-
<ShellOutlet
|
|
340
|
+
<ShellOutlet target="catalog:sub-pages" />
|
|
308
341
|
</template>
|
|
309
342
|
<RouterView />
|
|
310
343
|
</SectionContainerShell>
|
|
@@ -318,8 +351,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
|
|
|
318
351
|
options: {}
|
|
319
352
|
});
|
|
320
353
|
|
|
321
|
-
assert.equal(context.
|
|
322
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
354
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "catalog:sub-pages");
|
|
323
355
|
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
324
356
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./products\",\n");
|
|
325
357
|
});
|
|
@@ -343,7 +375,7 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
|
|
|
343
375
|
`<template>
|
|
344
376
|
<SectionContainerShell>
|
|
345
377
|
<template #tabs>
|
|
346
|
-
<ShellOutlet
|
|
378
|
+
<ShellOutlet target="catalog:sub-pages" />
|
|
347
379
|
</template>
|
|
348
380
|
<RouterView />
|
|
349
381
|
</SectionContainerShell>
|
|
@@ -356,7 +388,7 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
|
|
|
356
388
|
`<template>
|
|
357
389
|
<SectionContainerShell>
|
|
358
390
|
<template #tabs>
|
|
359
|
-
<ShellOutlet
|
|
391
|
+
<ShellOutlet target="catalog-products:sub-pages" />
|
|
360
392
|
</template>
|
|
361
393
|
<RouterView />
|
|
362
394
|
</SectionContainerShell>
|
|
@@ -370,8 +402,7 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
|
|
|
370
402
|
options: {}
|
|
371
403
|
});
|
|
372
404
|
|
|
373
|
-
assert.equal(context.
|
|
374
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
405
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "catalog-products:sub-pages");
|
|
375
406
|
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
376
407
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./variants\",\n");
|
|
377
408
|
});
|
|
@@ -395,7 +426,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
|
|
|
395
426
|
`<template>
|
|
396
427
|
<SectionContainerShell>
|
|
397
428
|
<template #tabs>
|
|
398
|
-
<ShellOutlet
|
|
429
|
+
<ShellOutlet target="customer-view:sub-pages" />
|
|
399
430
|
</template>
|
|
400
431
|
<RouterView />
|
|
401
432
|
</SectionContainerShell>
|
|
@@ -409,8 +440,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
|
|
|
409
440
|
options: {}
|
|
410
441
|
});
|
|
411
442
|
|
|
412
|
-
assert.equal(context.
|
|
413
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
443
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "customer-view:sub-pages");
|
|
414
444
|
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
415
445
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./pets\",\n");
|
|
416
446
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/customers/[customerId]/pets");
|
|
@@ -468,11 +498,35 @@ test("buildUiPageTemplateContext rejects target files with a leading src segment
|
|
|
468
498
|
targetFile: "src/components/ReportsPanel.vue",
|
|
469
499
|
options: {}
|
|
470
500
|
}),
|
|
471
|
-
/must be relative to src\/pages
|
|
501
|
+
/must be relative to src\/pages\/ or start with src\/pages\/:/
|
|
472
502
|
);
|
|
473
503
|
});
|
|
474
504
|
});
|
|
475
505
|
|
|
506
|
+
test("buildUiPageTemplateContext accepts target files with a src/pages prefix", async () => {
|
|
507
|
+
await withTempApp(async (appRoot) => {
|
|
508
|
+
await writeConfig(
|
|
509
|
+
appRoot,
|
|
510
|
+
`export const config = {
|
|
511
|
+
surfaceDefinitions: {
|
|
512
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
`
|
|
516
|
+
);
|
|
517
|
+
await writeShellLayout(appRoot);
|
|
518
|
+
|
|
519
|
+
const context = await buildUiPageTemplateContext({
|
|
520
|
+
appRoot,
|
|
521
|
+
targetFile: "src/pages/admin/reports/index.vue",
|
|
522
|
+
options: {}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.reports.link");
|
|
526
|
+
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
476
530
|
test("buildUiPageTemplateContext fails when the target file matches no surface", async () => {
|
|
477
531
|
await withTempApp(async (appRoot) => {
|
|
478
532
|
await writeConfig(
|
|
@@ -546,7 +600,7 @@ test("buildUiPageTemplateContext validates link placement format", async () => {
|
|
|
546
600
|
"link-placement": "invalid-placement"
|
|
547
601
|
}
|
|
548
602
|
}),
|
|
549
|
-
/option "placement" must be in "host:position" format/
|
|
603
|
+
/option "placement" must be a target in "host:position" format/
|
|
550
604
|
);
|
|
551
605
|
});
|
|
552
606
|
});
|
|
@@ -23,8 +23,12 @@ async function writeAppFixture(appRoot) {
|
|
|
23
23
|
path.join(appRoot, "src", "components", "ShellLayout.vue"),
|
|
24
24
|
`<template>
|
|
25
25
|
<div>
|
|
26
|
-
<ShellOutlet
|
|
27
|
-
|
|
26
|
+
<ShellOutlet
|
|
27
|
+
target="shell-layout:primary-menu"
|
|
28
|
+
default
|
|
29
|
+
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
30
|
+
/>
|
|
31
|
+
<ShellOutlet target="shell-layout:top-right" />
|
|
28
32
|
</div>
|
|
29
33
|
</template>
|
|
30
34
|
`,
|
|
@@ -34,7 +38,7 @@ async function writeAppFixture(appRoot) {
|
|
|
34
38
|
path.join(appRoot, "src", "pages", "admin", "workspace", "settings", "index.vue"),
|
|
35
39
|
`<template>
|
|
36
40
|
<section>
|
|
37
|
-
<ShellOutlet
|
|
41
|
+
<ShellOutlet target="admin-settings:forms" />
|
|
38
42
|
</section>
|
|
39
43
|
</template>
|
|
40
44
|
`,
|
|
@@ -104,8 +108,7 @@ test("ui-generator placed-element subcommand creates component and outlet placem
|
|
|
104
108
|
|
|
105
109
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
106
110
|
assert.match(placementSource, /id: "ui-generator\.element\.ops-panel"/);
|
|
107
|
-
assert.match(placementSource, /
|
|
108
|
-
assert.match(placementSource, /position: "top-right"/);
|
|
111
|
+
assert.match(placementSource, /target: "shell-layout:top-right"/);
|
|
109
112
|
assert.match(placementSource, /componentToken: "local\.main\.ui\.element\.ops-panel"/);
|
|
110
113
|
});
|
|
111
114
|
});
|
|
@@ -125,8 +128,7 @@ test("ui-generator placed-element subcommand supports explicit placement overrid
|
|
|
125
128
|
});
|
|
126
129
|
|
|
127
130
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
128
|
-
assert.match(placementSource, /
|
|
129
|
-
assert.match(placementSource, /position: "primary-menu"/);
|
|
131
|
+
assert.match(placementSource, /target: "shell-layout:primary-menu"/);
|
|
130
132
|
});
|
|
131
133
|
});
|
|
132
134
|
|
|
@@ -40,7 +40,7 @@ import { computed } from "vue";
|
|
|
40
40
|
subcommand: "outlet",
|
|
41
41
|
args: [targetFile],
|
|
42
42
|
options: {
|
|
43
|
-
target: "contact-view"
|
|
43
|
+
target: "contact-view:sub-pages"
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
|
|
@@ -48,7 +48,7 @@ import { computed } from "vue";
|
|
|
48
48
|
|
|
49
49
|
const output = await readFile(targetPath, "utf8");
|
|
50
50
|
assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
|
|
51
|
-
assert.match(output, /<ShellOutlet
|
|
51
|
+
assert.match(output, /<ShellOutlet target="contact-view:sub-pages" \/>/);
|
|
52
52
|
assert.doesNotMatch(output, /RouterView/);
|
|
53
53
|
assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
|
|
54
54
|
|
|
@@ -57,7 +57,7 @@ import { computed } from "vue";
|
|
|
57
57
|
subcommand: "outlet",
|
|
58
58
|
args: [targetFile],
|
|
59
59
|
options: {
|
|
60
|
-
target: "contact-view"
|
|
60
|
+
target: "contact-view:sub-pages"
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -79,7 +79,7 @@ import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
|
79
79
|
|
|
80
80
|
<template>
|
|
81
81
|
<section>
|
|
82
|
-
<ShellOutlet
|
|
82
|
+
<ShellOutlet target="contact-view:sub-pages" />
|
|
83
83
|
</section>
|
|
84
84
|
</template>
|
|
85
85
|
`,
|
|
@@ -91,12 +91,12 @@ import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
|
91
91
|
subcommand: "outlet",
|
|
92
92
|
args: [targetFile],
|
|
93
93
|
options: {
|
|
94
|
-
target: "contact-view"
|
|
94
|
+
target: "contact-view:sub-pages"
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
const output = await readFile(targetPath, "utf8");
|
|
99
|
-
assert.equal((output.match(/<ShellOutlet
|
|
99
|
+
assert.equal((output.match(/<ShellOutlet target="contact-view:sub-pages" \/>/g) || []).length, 1);
|
|
100
100
|
assert.equal(
|
|
101
101
|
(output.match(/import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/g) || []).length,
|
|
102
102
|
1
|
|
@@ -124,7 +124,7 @@ test("ui-generator outlet creates script setup when missing", async () => {
|
|
|
124
124
|
subcommand: "outlet",
|
|
125
125
|
args: [targetFile],
|
|
126
126
|
options: {
|
|
127
|
-
target: "contact-view"
|
|
127
|
+
target: "contact-view:sub-pages"
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
|
|
@@ -161,7 +161,7 @@ test("ui-generator outlet inserts generated script after existing route block",
|
|
|
161
161
|
subcommand: "outlet",
|
|
162
162
|
args: [targetFile],
|
|
163
163
|
options: {
|
|
164
|
-
target: "contact-view"
|
|
164
|
+
target: "contact-view:sub-pages"
|
|
165
165
|
}
|
|
166
166
|
});
|
|
167
167
|
|
|
@@ -203,12 +203,12 @@ test("ui-generator outlet keeps indentation when injected into nested template b
|
|
|
203
203
|
subcommand: "outlet",
|
|
204
204
|
args: [targetFile],
|
|
205
205
|
options: {
|
|
206
|
-
target: "contact-view"
|
|
206
|
+
target: "contact-view:sub-pages"
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
const output = await readFile(targetPath, "utf8");
|
|
211
|
-
assert.match(output, /\n\s{2}<\/section>\n\s{2}<ShellOutlet
|
|
211
|
+
assert.match(output, /\n\s{2}<\/section>\n\s{2}<ShellOutlet target="contact-view:sub-pages" \/>\n<\/template>/);
|
|
212
212
|
assert.match(output, /<template v-else-if="view\.isLoading">\n\s*<v-skeleton-loader type="heading, text@2, article" \/>\n\s*<\/template>/);
|
|
213
213
|
assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
|
|
214
214
|
});
|
|
@@ -226,11 +226,11 @@ test("ui-generator outlet rejects unsupported options", async () => {
|
|
|
226
226
|
runGeneratorSubcommand({
|
|
227
227
|
appRoot,
|
|
228
228
|
subcommand: "outlet",
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
args: [targetFile],
|
|
230
|
+
options: {
|
|
231
|
+
target: "contact-view:sub-pages",
|
|
232
|
+
bogus: "routed"
|
|
233
|
+
}
|
|
234
234
|
}),
|
|
235
235
|
/ui-generator outlet received unsupported option: --bogus\./
|
|
236
236
|
);
|
|
@@ -255,7 +255,33 @@ test("ui-generator outlet supports explicit target host:position", async () => {
|
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
const output = await readFile(targetPath, "utf8");
|
|
258
|
-
assert.match(output, /<ShellOutlet
|
|
258
|
+
assert.match(output, /<ShellOutlet target="customer-view:summary-actions" \/>/);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("ui-generator outlet rejects non-vue target files without changing them", async () => {
|
|
263
|
+
await withTempApp(async (appRoot) => {
|
|
264
|
+
const targetFile = "src/pages/w/[workspaceSlug]/admin/practice/vets/_components/VetAddEditFormFields.js";
|
|
265
|
+
const targetPath = path.join(appRoot, targetFile);
|
|
266
|
+
const originalSource = "export const fields = [];\n";
|
|
267
|
+
|
|
268
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
269
|
+
await writeFile(targetPath, originalSource, "utf8");
|
|
270
|
+
|
|
271
|
+
await assert.rejects(
|
|
272
|
+
runGeneratorSubcommand({
|
|
273
|
+
appRoot,
|
|
274
|
+
subcommand: "outlet",
|
|
275
|
+
args: [targetFile],
|
|
276
|
+
options: {
|
|
277
|
+
target: "vet-view:sub-pages"
|
|
278
|
+
}
|
|
279
|
+
}),
|
|
280
|
+
/ui-generator outlet target file must be an existing Vue SFC \(\.vue\): src\/pages\/w\/\[workspaceSlug\]\/admin\/practice\/vets\/_components\/VetAddEditFormFields\.js\./
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const output = await readFile(targetPath, "utf8");
|
|
284
|
+
assert.equal(output, originalSource);
|
|
259
285
|
});
|
|
260
286
|
});
|
|
261
287
|
|
|
@@ -276,7 +302,7 @@ test("ui-generator outlet validates target format", async () => {
|
|
|
276
302
|
target: "customer-view:"
|
|
277
303
|
}
|
|
278
304
|
}),
|
|
279
|
-
/ui-generator outlet option "target" must be
|
|
305
|
+
/ui-generator outlet option "target" must be a target in "host:position" format\./
|
|
280
306
|
);
|
|
281
307
|
});
|
|
282
308
|
});
|