@jskit-ai/ui-generator 0.1.48 → 0.1.50
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 +117 -24
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +44 -3
- package/src/server/subcommands/addSubpages.js +89 -2
- package/src/server/subcommands/element.js +42 -10
- package/src/server/subcommands/outlet.js +399 -20
- package/src/server/subcommands/page.js +49 -23
- package/src/server/subcommands/pageSupport.js +115 -37
- package/src/server/subcommands/support.js +128 -23
- package/test/addSubpagesSubcommand.test.js +163 -15
- package/test/buildTemplateContext.test.js +227 -34
- package/test/elementSubcommand.test.js +89 -12
- package/test/outletSubcommand.test.js +305 -14
- package/test/packageDescriptor.test.js +11 -0
- package/test/pageSubcommand.test.js +234 -17
|
@@ -39,6 +39,22 @@ export { addPlacement };
|
|
|
39
39
|
export default function getPlacements() {
|
|
40
40
|
return [];
|
|
41
41
|
}
|
|
42
|
+
`,
|
|
43
|
+
"utf8"
|
|
44
|
+
);
|
|
45
|
+
await writeFile(
|
|
46
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
47
|
+
`const placements = [];
|
|
48
|
+
|
|
49
|
+
function addPlacementTopology(value = {}) {
|
|
50
|
+
placements.push(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { addPlacementTopology };
|
|
54
|
+
|
|
55
|
+
export default function getPlacementTopology() {
|
|
56
|
+
return { placements };
|
|
57
|
+
}
|
|
42
58
|
`,
|
|
43
59
|
"utf8"
|
|
44
60
|
);
|
|
@@ -97,20 +113,28 @@ test("ui-generator add-subpages derives the default target from an index-route p
|
|
|
97
113
|
|
|
98
114
|
assert.deepEqual(result.touchedFiles, [
|
|
99
115
|
"packages/main/src/client/providers/MainClientProvider.js",
|
|
100
|
-
"src/components/menus/
|
|
116
|
+
"src/components/menus/TabLinkItem.vue",
|
|
101
117
|
"src/components/SectionContainerShell.vue",
|
|
102
|
-
`src/pages/${targetFile}
|
|
118
|
+
`src/pages/${targetFile}`,
|
|
119
|
+
"src/placementTopology.js"
|
|
103
120
|
]);
|
|
104
121
|
|
|
105
122
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
106
123
|
assert.match(
|
|
107
124
|
pageSource,
|
|
108
|
-
/<ShellOutlet target="practice:sub-pages"
|
|
125
|
+
/<ShellOutlet target="practice:sub-pages" \/>/
|
|
109
126
|
);
|
|
127
|
+
const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
|
|
128
|
+
assert.match(topologySource, /id: "page\.section-nav"/);
|
|
129
|
+
assert.match(topologySource, /owner: "practice"/);
|
|
130
|
+
assert.match(topologySource, /compact: \{/);
|
|
131
|
+
assert.match(topologySource, /medium: \{/);
|
|
132
|
+
assert.match(topologySource, /expanded: \{/);
|
|
133
|
+
assert.match(topologySource, /link: "local\.main\.ui\.tab-link-item"/);
|
|
110
134
|
assert.match(pageSource, /<RouterView \/>/);
|
|
111
135
|
assert.equal(
|
|
112
|
-
await readFile(path.join(appRoot, "src", "components", "menus", "
|
|
113
|
-
await readLocalLinkItemComponentSource("local.main.ui.
|
|
136
|
+
await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
|
|
137
|
+
await readLocalLinkItemComponentSource("local.main.ui.tab-link-item")
|
|
114
138
|
);
|
|
115
139
|
});
|
|
116
140
|
});
|
|
@@ -132,7 +156,7 @@ test("ui-generator add-subpages derives the default target from a dynamic file-r
|
|
|
132
156
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
133
157
|
assert.match(
|
|
134
158
|
pageSource,
|
|
135
|
-
/<ShellOutlet target="contacts-contact-id:sub-pages"
|
|
159
|
+
/<ShellOutlet target="contacts-contact-id:sub-pages" \/>/
|
|
136
160
|
);
|
|
137
161
|
});
|
|
138
162
|
});
|
|
@@ -154,7 +178,7 @@ test("ui-generator add-subpages derives the default target from a nested route p
|
|
|
154
178
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
155
179
|
assert.match(
|
|
156
180
|
pageSource,
|
|
157
|
-
/<ShellOutlet target="catalog-products:sub-pages"
|
|
181
|
+
/<ShellOutlet target="catalog-products:sub-pages" \/>/
|
|
158
182
|
);
|
|
159
183
|
});
|
|
160
184
|
});
|
|
@@ -199,11 +223,46 @@ test("ui-generator add-subpages supports explicit target host:position", async (
|
|
|
199
223
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
200
224
|
assert.match(
|
|
201
225
|
pageSource,
|
|
202
|
-
/<ShellOutlet target="practice-hub:secondary-tabs"
|
|
226
|
+
/<ShellOutlet target="practice-hub:secondary-tabs" \/>/
|
|
203
227
|
);
|
|
204
228
|
});
|
|
205
229
|
});
|
|
206
230
|
|
|
231
|
+
test("ui-generator add-subpages creates script setup instead of adding template imports to normal script", async () => {
|
|
232
|
+
await withTempApp(async (appRoot) => {
|
|
233
|
+
await writeAppFixture(appRoot);
|
|
234
|
+
|
|
235
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
236
|
+
await writePageFile(
|
|
237
|
+
appRoot,
|
|
238
|
+
targetFile,
|
|
239
|
+
`<script>
|
|
240
|
+
export default {
|
|
241
|
+
name: "PracticePage"
|
|
242
|
+
};
|
|
243
|
+
</script>
|
|
244
|
+
|
|
245
|
+
<template>
|
|
246
|
+
<section>Practice</section>
|
|
247
|
+
</template>
|
|
248
|
+
`
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
await runGeneratorSubcommand({
|
|
252
|
+
appRoot,
|
|
253
|
+
subcommand: "add-subpages",
|
|
254
|
+
args: [targetFile],
|
|
255
|
+
options: {}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const pageSource = await readPageFile(appRoot, targetFile);
|
|
259
|
+
assert.match(pageSource, /<script setup>\nimport ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
|
|
260
|
+
assert.match(pageSource, /import \{ RouterView \} from "vue-router";/);
|
|
261
|
+
assert.match(pageSource, /import SectionContainerShell from "\/src\/components\/SectionContainerShell\.vue";/);
|
|
262
|
+
assert.match(pageSource, /<script>\nexport default/);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
207
266
|
test("ui-generator add-subpages does not rewrite existing scaffold support components", async () => {
|
|
208
267
|
await withTempApp(async (appRoot) => {
|
|
209
268
|
await writeAppFixture(appRoot);
|
|
@@ -211,7 +270,7 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
|
|
|
211
270
|
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
212
271
|
await writePageFile(appRoot, targetFile);
|
|
213
272
|
const customSectionShellSource = `<template><section class="custom-shell"><slot /></section></template>\n`;
|
|
214
|
-
const
|
|
273
|
+
const customTabLinkSource = `<template><button class="custom-tab-link"><slot /></button></template>\n`;
|
|
215
274
|
await writeFile(
|
|
216
275
|
path.join(appRoot, "src", "components", "SectionContainerShell.vue"),
|
|
217
276
|
customSectionShellSource,
|
|
@@ -219,8 +278,8 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
|
|
|
219
278
|
);
|
|
220
279
|
await mkdir(path.join(appRoot, "src", "components", "menus"), { recursive: true });
|
|
221
280
|
await writeFile(
|
|
222
|
-
path.join(appRoot, "src", "components", "menus", "
|
|
223
|
-
|
|
281
|
+
path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"),
|
|
282
|
+
customTabLinkSource,
|
|
224
283
|
"utf8"
|
|
225
284
|
);
|
|
226
285
|
|
|
@@ -235,16 +294,105 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
|
|
|
235
294
|
|
|
236
295
|
assert.deepEqual(result.touchedFiles, [
|
|
237
296
|
"packages/main/src/client/providers/MainClientProvider.js",
|
|
238
|
-
`src/pages/${targetFile}
|
|
297
|
+
`src/pages/${targetFile}`,
|
|
298
|
+
"src/placementTopology.js"
|
|
239
299
|
]);
|
|
240
300
|
assert.equal(
|
|
241
301
|
await readFile(path.join(appRoot, "src", "components", "SectionContainerShell.vue"), "utf8"),
|
|
242
302
|
customSectionShellSource
|
|
243
303
|
);
|
|
244
304
|
assert.equal(
|
|
245
|
-
await readFile(path.join(appRoot, "src", "components", "menus", "
|
|
246
|
-
|
|
305
|
+
await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
|
|
306
|
+
customTabLinkSource
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("ui-generator add-subpages validates topology before changing page or support files", async () => {
|
|
312
|
+
await withTempApp(async (appRoot) => {
|
|
313
|
+
await writeAppFixture(appRoot);
|
|
314
|
+
|
|
315
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
316
|
+
const originalPageSource = "<template><section>Practice</section></template>\n";
|
|
317
|
+
await writePageFile(appRoot, targetFile, originalPageSource);
|
|
318
|
+
const providerPath = path.join(appRoot, "packages", "main", "src", "client", "providers", "MainClientProvider.js");
|
|
319
|
+
const originalProviderSource = await readFile(providerPath, "utf8");
|
|
320
|
+
await writeFile(
|
|
321
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
322
|
+
`export default {
|
|
323
|
+
placements: [
|
|
324
|
+
{
|
|
325
|
+
id: "page.section-nav",
|
|
326
|
+
owner: "practice",
|
|
327
|
+
variants: {}
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
`,
|
|
332
|
+
"utf8"
|
|
247
333
|
);
|
|
334
|
+
|
|
335
|
+
await assert.rejects(
|
|
336
|
+
runGeneratorSubcommand({
|
|
337
|
+
appRoot,
|
|
338
|
+
subcommand: "add-subpages",
|
|
339
|
+
args: [targetFile],
|
|
340
|
+
options: {}
|
|
341
|
+
}),
|
|
342
|
+
/requires compact topology variant/
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
assert.equal(await readPageFile(appRoot, targetFile), originalPageSource);
|
|
346
|
+
assert.equal(await readFile(providerPath, "utf8"), originalProviderSource);
|
|
347
|
+
await assert.rejects(
|
|
348
|
+
readFile(path.join(appRoot, "src", "components", "SectionContainerShell.vue"), "utf8"),
|
|
349
|
+
/ENOENT/
|
|
350
|
+
);
|
|
351
|
+
await assert.rejects(
|
|
352
|
+
readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
|
|
353
|
+
/ENOENT/
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("ui-generator add-subpages rejects existing section-nav topology for a different outlet", async () => {
|
|
359
|
+
await withTempApp(async (appRoot) => {
|
|
360
|
+
await writeAppFixture(appRoot);
|
|
361
|
+
|
|
362
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
363
|
+
const originalPageSource = "<template><section>Practice</section></template>\n";
|
|
364
|
+
await writePageFile(appRoot, targetFile, originalPageSource);
|
|
365
|
+
await writeFile(
|
|
366
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
367
|
+
`export default {
|
|
368
|
+
placements: [
|
|
369
|
+
{
|
|
370
|
+
id: "page.section-nav",
|
|
371
|
+
owner: "practice",
|
|
372
|
+
surfaces: ["admin"],
|
|
373
|
+
variants: {
|
|
374
|
+
compact: { outlet: "practice:existing-tabs" },
|
|
375
|
+
medium: { outlet: "practice:existing-tabs" },
|
|
376
|
+
expanded: { outlet: "practice:existing-tabs" }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
};
|
|
381
|
+
`,
|
|
382
|
+
"utf8"
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
await assert.rejects(
|
|
386
|
+
runGeneratorSubcommand({
|
|
387
|
+
appRoot,
|
|
388
|
+
subcommand: "add-subpages",
|
|
389
|
+
args: [targetFile],
|
|
390
|
+
options: {}
|
|
391
|
+
}),
|
|
392
|
+
/semantic placement "page\.section-nav" for owner "practice" already exists with different outlet mapping/
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
assert.equal(await readPageFile(appRoot, targetFile), originalPageSource);
|
|
248
396
|
});
|
|
249
397
|
});
|
|
250
398
|
|
|
@@ -332,7 +480,7 @@ test("ui-generator add-subpages accepts target files with a src/pages prefix", a
|
|
|
332
480
|
const pageSource = await readFile(path.join(appRoot, targetFile), "utf8");
|
|
333
481
|
assert.match(
|
|
334
482
|
pageSource,
|
|
335
|
-
/<ShellOutlet target="practice:sub-pages"
|
|
483
|
+
/<ShellOutlet target="practice:sub-pages" \/>/
|
|
336
484
|
);
|
|
337
485
|
assert.match(pageSource, /<RouterView \/>/);
|
|
338
486
|
});
|
|
@@ -35,10 +35,77 @@ async function writeShellLayout(appRoot, source = "") {
|
|
|
35
35
|
<ShellOutlet
|
|
36
36
|
target="shell-layout:primary-menu"
|
|
37
37
|
default
|
|
38
|
-
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
39
38
|
/>
|
|
40
39
|
</div>
|
|
41
40
|
</template>
|
|
41
|
+
`
|
|
42
|
+
);
|
|
43
|
+
await writePlacementTopology(appRoot);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderTopologyVariant(outlet, { linkRenderer = "" } = {}) {
|
|
47
|
+
const rendererLines = linkRenderer
|
|
48
|
+
? `,
|
|
49
|
+
renderers: {
|
|
50
|
+
link: "${linkRenderer}"
|
|
51
|
+
}`
|
|
52
|
+
: "";
|
|
53
|
+
return `{
|
|
54
|
+
outlet: "${outlet}"${rendererLines}
|
|
55
|
+
}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderTopologyEntry({
|
|
59
|
+
id = "",
|
|
60
|
+
owner = "",
|
|
61
|
+
surfaces = ["*"],
|
|
62
|
+
defaultPlacement = false,
|
|
63
|
+
outlet = "",
|
|
64
|
+
linkRenderer = ""
|
|
65
|
+
} = {}) {
|
|
66
|
+
const ownerLine = owner ? ` owner: "${owner}",\n` : "";
|
|
67
|
+
const defaultLine = defaultPlacement ? " default: true,\n" : "";
|
|
68
|
+
return ` {
|
|
69
|
+
id: "${id}",
|
|
70
|
+
${ownerLine} surfaces: ${JSON.stringify(surfaces)},
|
|
71
|
+
${defaultLine} variants: {
|
|
72
|
+
compact: ${renderTopologyVariant(outlet, { linkRenderer })},
|
|
73
|
+
medium: ${renderTopologyVariant(outlet, { linkRenderer })},
|
|
74
|
+
expanded: ${renderTopologyVariant(outlet, { linkRenderer })}
|
|
75
|
+
}
|
|
76
|
+
}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function writePlacementTopology(appRoot, entries = []) {
|
|
80
|
+
const defaultEntries = [
|
|
81
|
+
renderTopologyEntry({
|
|
82
|
+
id: "shell.primary-nav",
|
|
83
|
+
surfaces: ["*"],
|
|
84
|
+
defaultPlacement: true,
|
|
85
|
+
outlet: "shell-layout:primary-menu",
|
|
86
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
87
|
+
}),
|
|
88
|
+
renderTopologyEntry({
|
|
89
|
+
id: "shell.status",
|
|
90
|
+
surfaces: ["*"],
|
|
91
|
+
outlet: "shell-layout:top-right",
|
|
92
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
93
|
+
}),
|
|
94
|
+
renderTopologyEntry({
|
|
95
|
+
id: "shell.global-actions",
|
|
96
|
+
surfaces: ["*"],
|
|
97
|
+
outlet: "shell-layout:top-right",
|
|
98
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
99
|
+
})
|
|
100
|
+
];
|
|
101
|
+
await writeFileInApp(
|
|
102
|
+
appRoot,
|
|
103
|
+
"src/placementTopology.js",
|
|
104
|
+
`export default {
|
|
105
|
+
placements: [
|
|
106
|
+
${[...defaultEntries, ...entries].join(",\n")}
|
|
107
|
+
]
|
|
108
|
+
};
|
|
42
109
|
`
|
|
43
110
|
);
|
|
44
111
|
}
|
|
@@ -61,8 +128,9 @@ test("buildUiPageTemplateContext resolves link placement from default app ShellO
|
|
|
61
128
|
targetFile: "admin/reports/index.vue",
|
|
62
129
|
options: {}
|
|
63
130
|
});
|
|
64
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell
|
|
65
|
-
assert.equal(context.
|
|
131
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.primary-nav");
|
|
132
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, "");
|
|
133
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
66
134
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
|
|
67
135
|
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
|
|
68
136
|
assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, "");
|
|
@@ -116,15 +184,40 @@ test("buildUiPageTemplateContext supports explicit link placement override", asy
|
|
|
116
184
|
appRoot,
|
|
117
185
|
targetFile: "admin/reports/index.vue",
|
|
118
186
|
options: {
|
|
119
|
-
"link-placement": "shell
|
|
187
|
+
"link-placement": "shell.status"
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
|
|
191
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("buildUiPageTemplateContext maps utility navigation role to global actions", async () => {
|
|
196
|
+
await withTempApp(async (appRoot) => {
|
|
197
|
+
await writeConfig(
|
|
198
|
+
appRoot,
|
|
199
|
+
`export const config = {
|
|
200
|
+
surfaceDefinitions: {
|
|
201
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
`
|
|
205
|
+
);
|
|
206
|
+
await writeShellLayout(appRoot);
|
|
207
|
+
|
|
208
|
+
const context = await buildUiPageTemplateContext({
|
|
209
|
+
appRoot,
|
|
210
|
+
targetFile: "admin/help/index.vue",
|
|
211
|
+
options: {
|
|
212
|
+
"navigation-role": "utility"
|
|
120
213
|
}
|
|
121
214
|
});
|
|
122
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell-
|
|
123
|
-
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "
|
|
215
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.global-actions");
|
|
216
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
124
217
|
});
|
|
125
218
|
});
|
|
126
219
|
|
|
127
|
-
test("buildUiPageTemplateContext supports explicit package
|
|
220
|
+
test("buildUiPageTemplateContext supports explicit package semantic link placement", async () => {
|
|
128
221
|
await withTempApp(async (appRoot) => {
|
|
129
222
|
await writeConfig(
|
|
130
223
|
appRoot,
|
|
@@ -136,6 +229,22 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
136
229
|
`
|
|
137
230
|
);
|
|
138
231
|
await writeShellLayout(appRoot);
|
|
232
|
+
await writePlacementTopology(appRoot, [
|
|
233
|
+
renderTopologyEntry({
|
|
234
|
+
id: "page.section-nav",
|
|
235
|
+
owner: "catalog",
|
|
236
|
+
surfaces: ["admin"],
|
|
237
|
+
outlet: "catalog:sub-pages",
|
|
238
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
239
|
+
}),
|
|
240
|
+
renderTopologyEntry({
|
|
241
|
+
id: "page.section-nav",
|
|
242
|
+
owner: "catalog-products",
|
|
243
|
+
surfaces: ["admin"],
|
|
244
|
+
outlet: "catalog-products:sub-pages",
|
|
245
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
246
|
+
})
|
|
247
|
+
]);
|
|
139
248
|
await writeFileInApp(
|
|
140
249
|
appRoot,
|
|
141
250
|
".jskit/lock.json",
|
|
@@ -164,13 +273,34 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
164
273
|
metadata: {
|
|
165
274
|
ui: {
|
|
166
275
|
placements: {
|
|
167
|
-
|
|
276
|
+
topology: {
|
|
277
|
+
placements: [
|
|
168
278
|
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
279
|
+
id: "admin.tools-menu",
|
|
280
|
+
surfaces: ["admin"],
|
|
281
|
+
variants: {
|
|
282
|
+
compact: {
|
|
283
|
+
outlet: "admin-cog:primary-menu",
|
|
284
|
+
renderers: {
|
|
285
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
medium: {
|
|
289
|
+
outlet: "admin-cog:primary-menu",
|
|
290
|
+
renderers: {
|
|
291
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
expanded: {
|
|
295
|
+
outlet: "admin-cog:primary-menu",
|
|
296
|
+
renderers: {
|
|
297
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
172
301
|
}
|
|
173
302
|
]
|
|
303
|
+
}
|
|
174
304
|
}
|
|
175
305
|
}
|
|
176
306
|
}
|
|
@@ -182,10 +312,10 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
182
312
|
appRoot,
|
|
183
313
|
targetFile: "admin/reports/index.vue",
|
|
184
314
|
options: {
|
|
185
|
-
"link-placement": "admin-
|
|
315
|
+
"link-placement": "admin.tools-menu"
|
|
186
316
|
}
|
|
187
317
|
});
|
|
188
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin-
|
|
318
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin.tools-menu");
|
|
189
319
|
});
|
|
190
320
|
});
|
|
191
321
|
|
|
@@ -200,15 +330,21 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
|
|
|
200
330
|
};
|
|
201
331
|
`
|
|
202
332
|
);
|
|
333
|
+
await writePlacementTopology(appRoot, [
|
|
334
|
+
renderTopologyEntry({
|
|
335
|
+
id: "page.section-nav",
|
|
336
|
+
owner: "home-settings",
|
|
337
|
+
surfaces: ["home"],
|
|
338
|
+
outlet: "home-settings:primary-menu",
|
|
339
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
340
|
+
})
|
|
341
|
+
]);
|
|
203
342
|
await writeFileInApp(
|
|
204
343
|
appRoot,
|
|
205
344
|
"src/pages/home/settings.vue",
|
|
206
345
|
`<template>
|
|
207
346
|
<section>
|
|
208
|
-
<ShellOutlet
|
|
209
|
-
target="home-settings:primary-menu"
|
|
210
|
-
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
211
|
-
/>
|
|
347
|
+
<ShellOutlet target="home-settings:primary-menu" />
|
|
212
348
|
<RouterView />
|
|
213
349
|
</section>
|
|
214
350
|
</template>
|
|
@@ -221,13 +357,14 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
|
|
|
221
357
|
options: {}
|
|
222
358
|
});
|
|
223
359
|
|
|
224
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
225
|
-
assert.equal(context.
|
|
360
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
361
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"home-settings\",\n");
|
|
362
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
226
363
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, "");
|
|
227
364
|
});
|
|
228
365
|
});
|
|
229
366
|
|
|
230
|
-
test("buildUiPageTemplateContext supports explicit
|
|
367
|
+
test("buildUiPageTemplateContext supports explicit semantic placement and link-to", async () => {
|
|
231
368
|
await withTempApp(async (appRoot) => {
|
|
232
369
|
await writeConfig(
|
|
233
370
|
appRoot,
|
|
@@ -244,12 +381,12 @@ test("buildUiPageTemplateContext supports explicit link component token and link
|
|
|
244
381
|
appRoot,
|
|
245
382
|
targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
|
|
246
383
|
options: {
|
|
247
|
-
"link-placement": "shell
|
|
248
|
-
"link-component-token": "local.main.ui.tab-link-item",
|
|
384
|
+
"link-placement": "shell.status",
|
|
249
385
|
"link-to": "./notes"
|
|
250
386
|
}
|
|
251
387
|
});
|
|
252
|
-
assert.equal(context.
|
|
388
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
|
|
389
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
253
390
|
assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
|
|
254
391
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
255
392
|
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
@@ -294,6 +431,15 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
|
|
|
294
431
|
`
|
|
295
432
|
);
|
|
296
433
|
await writeShellLayout(appRoot);
|
|
434
|
+
await writePlacementTopology(appRoot, [
|
|
435
|
+
renderTopologyEntry({
|
|
436
|
+
id: "page.section-nav",
|
|
437
|
+
owner: "contact-view",
|
|
438
|
+
surfaces: ["admin"],
|
|
439
|
+
outlet: "contact-view:sub-pages",
|
|
440
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
441
|
+
})
|
|
442
|
+
]);
|
|
297
443
|
await writeFileInApp(
|
|
298
444
|
appRoot,
|
|
299
445
|
"src/pages/admin/contacts/[contactId].vue",
|
|
@@ -314,8 +460,9 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
|
|
|
314
460
|
options: {}
|
|
315
461
|
});
|
|
316
462
|
|
|
317
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
318
|
-
assert.equal(context.
|
|
463
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
464
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"contact-view\",\n");
|
|
465
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
319
466
|
assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
|
|
320
467
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
|
|
321
468
|
});
|
|
@@ -333,6 +480,15 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
|
|
|
333
480
|
`
|
|
334
481
|
);
|
|
335
482
|
await writeShellLayout(appRoot);
|
|
483
|
+
await writePlacementTopology(appRoot, [
|
|
484
|
+
renderTopologyEntry({
|
|
485
|
+
id: "page.section-nav",
|
|
486
|
+
owner: "contact-view",
|
|
487
|
+
surfaces: ["admin"],
|
|
488
|
+
outlet: "contact-view:sub-pages",
|
|
489
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
490
|
+
})
|
|
491
|
+
]);
|
|
336
492
|
await writeFileInApp(
|
|
337
493
|
appRoot,
|
|
338
494
|
"src/pages/admin/contacts/[contactId].vue",
|
|
@@ -353,8 +509,8 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
|
|
|
353
509
|
options: {}
|
|
354
510
|
});
|
|
355
511
|
|
|
356
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
357
|
-
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "
|
|
512
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
513
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
358
514
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes/history\",\n");
|
|
359
515
|
});
|
|
360
516
|
});
|
|
@@ -371,6 +527,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
|
|
|
371
527
|
`
|
|
372
528
|
);
|
|
373
529
|
await writeShellLayout(appRoot);
|
|
530
|
+
await writePlacementTopology(appRoot, [
|
|
531
|
+
renderTopologyEntry({
|
|
532
|
+
id: "page.section-nav",
|
|
533
|
+
owner: "catalog",
|
|
534
|
+
surfaces: ["admin"],
|
|
535
|
+
outlet: "catalog:sub-pages",
|
|
536
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
537
|
+
})
|
|
538
|
+
]);
|
|
374
539
|
await writeFileInApp(
|
|
375
540
|
appRoot,
|
|
376
541
|
"src/pages/admin/catalog/index.vue",
|
|
@@ -391,8 +556,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
|
|
|
391
556
|
options: {}
|
|
392
557
|
});
|
|
393
558
|
|
|
394
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
395
|
-
assert.equal(context.
|
|
559
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
560
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog\",\n");
|
|
561
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
396
562
|
assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
|
|
397
563
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./products\",\n");
|
|
398
564
|
});
|
|
@@ -410,6 +576,22 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
|
|
|
410
576
|
`
|
|
411
577
|
);
|
|
412
578
|
await writeShellLayout(appRoot);
|
|
579
|
+
await writePlacementTopology(appRoot, [
|
|
580
|
+
renderTopologyEntry({
|
|
581
|
+
id: "page.section-nav",
|
|
582
|
+
owner: "catalog",
|
|
583
|
+
surfaces: ["admin"],
|
|
584
|
+
outlet: "catalog:sub-pages",
|
|
585
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
586
|
+
}),
|
|
587
|
+
renderTopologyEntry({
|
|
588
|
+
id: "page.section-nav",
|
|
589
|
+
owner: "catalog-products",
|
|
590
|
+
surfaces: ["admin"],
|
|
591
|
+
outlet: "catalog-products:sub-pages",
|
|
592
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
593
|
+
})
|
|
594
|
+
]);
|
|
413
595
|
await writeFileInApp(
|
|
414
596
|
appRoot,
|
|
415
597
|
"src/pages/admin/catalog/index.vue",
|
|
@@ -443,8 +625,9 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
|
|
|
443
625
|
options: {}
|
|
444
626
|
});
|
|
445
627
|
|
|
446
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
447
|
-
assert.equal(context.
|
|
628
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
629
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog-products\",\n");
|
|
630
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
448
631
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./variants\",\n");
|
|
449
632
|
});
|
|
450
633
|
});
|
|
@@ -461,6 +644,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
|
|
|
461
644
|
`
|
|
462
645
|
);
|
|
463
646
|
await writeShellLayout(appRoot);
|
|
647
|
+
await writePlacementTopology(appRoot, [
|
|
648
|
+
renderTopologyEntry({
|
|
649
|
+
id: "page.section-nav",
|
|
650
|
+
owner: "customer-view",
|
|
651
|
+
surfaces: ["admin"],
|
|
652
|
+
outlet: "customer-view:sub-pages",
|
|
653
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
654
|
+
})
|
|
655
|
+
]);
|
|
464
656
|
await writeFileInApp(
|
|
465
657
|
appRoot,
|
|
466
658
|
"src/pages/admin/customers/[customerId]/index.vue",
|
|
@@ -481,8 +673,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
|
|
|
481
673
|
options: {}
|
|
482
674
|
});
|
|
483
675
|
|
|
484
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
485
|
-
assert.equal(context.
|
|
676
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
677
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"customer-view\",\n");
|
|
678
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
486
679
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./pets\",\n");
|
|
487
680
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/customers/[customerId]/pets");
|
|
488
681
|
});
|
|
@@ -641,7 +834,7 @@ test("buildUiPageTemplateContext validates link placement format", async () => {
|
|
|
641
834
|
"link-placement": "invalid-placement"
|
|
642
835
|
}
|
|
643
836
|
}),
|
|
644
|
-
/option "placement" must be a target in "
|
|
837
|
+
/option "placement" must be a semantic target in "area.slot" format/
|
|
645
838
|
);
|
|
646
839
|
});
|
|
647
840
|
});
|