@jskit-ai/ui-generator 0.1.47 → 0.1.49
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 +40 -23
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +8 -1
- package/src/server/subcommands/addSubpages.js +77 -2
- package/src/server/subcommands/element.js +25 -7
- package/src/server/subcommands/outlet.js +139 -5
- package/src/server/subcommands/page.js +7 -2
- package/src/server/subcommands/pageSupport.js +1 -1
- package/src/server/subcommands/support.js +2 -0
- package/test/addSubpagesSubcommand.test.js +31 -7
- package/test/buildTemplateContext.test.js +196 -34
- package/test/elementSubcommand.test.js +76 -11
- package/test/outletSubcommand.test.js +47 -14
- package/test/pageSubcommand.test.js +100 -12
|
@@ -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
|
);
|
|
@@ -99,14 +115,21 @@ test("ui-generator add-subpages derives the default target from an index-route p
|
|
|
99
115
|
"packages/main/src/client/providers/MainClientProvider.js",
|
|
100
116
|
"src/components/menus/SurfaceAwareMenuLinkItem.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: \{/);
|
|
110
133
|
assert.match(pageSource, /<RouterView \/>/);
|
|
111
134
|
assert.equal(
|
|
112
135
|
await readFile(path.join(appRoot, "src", "components", "menus", "SurfaceAwareMenuLinkItem.vue"), "utf8"),
|
|
@@ -132,7 +155,7 @@ test("ui-generator add-subpages derives the default target from a dynamic file-r
|
|
|
132
155
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
133
156
|
assert.match(
|
|
134
157
|
pageSource,
|
|
135
|
-
/<ShellOutlet target="contacts-contact-id:sub-pages"
|
|
158
|
+
/<ShellOutlet target="contacts-contact-id:sub-pages" \/>/
|
|
136
159
|
);
|
|
137
160
|
});
|
|
138
161
|
});
|
|
@@ -154,7 +177,7 @@ test("ui-generator add-subpages derives the default target from a nested route p
|
|
|
154
177
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
155
178
|
assert.match(
|
|
156
179
|
pageSource,
|
|
157
|
-
/<ShellOutlet target="catalog-products:sub-pages"
|
|
180
|
+
/<ShellOutlet target="catalog-products:sub-pages" \/>/
|
|
158
181
|
);
|
|
159
182
|
});
|
|
160
183
|
});
|
|
@@ -199,7 +222,7 @@ test("ui-generator add-subpages supports explicit target host:position", async (
|
|
|
199
222
|
const pageSource = await readPageFile(appRoot, targetFile);
|
|
200
223
|
assert.match(
|
|
201
224
|
pageSource,
|
|
202
|
-
/<ShellOutlet target="practice-hub:secondary-tabs"
|
|
225
|
+
/<ShellOutlet target="practice-hub:secondary-tabs" \/>/
|
|
203
226
|
);
|
|
204
227
|
});
|
|
205
228
|
});
|
|
@@ -235,7 +258,8 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
|
|
|
235
258
|
|
|
236
259
|
assert.deepEqual(result.touchedFiles, [
|
|
237
260
|
"packages/main/src/client/providers/MainClientProvider.js",
|
|
238
|
-
`src/pages/${targetFile}
|
|
261
|
+
`src/pages/${targetFile}`,
|
|
262
|
+
"src/placementTopology.js"
|
|
239
263
|
]);
|
|
240
264
|
assert.equal(
|
|
241
265
|
await readFile(path.join(appRoot, "src", "components", "SectionContainerShell.vue"), "utf8"),
|
|
@@ -332,7 +356,7 @@ test("ui-generator add-subpages accepts target files with a src/pages prefix", a
|
|
|
332
356
|
const pageSource = await readFile(path.join(appRoot, targetFile), "utf8");
|
|
333
357
|
assert.match(
|
|
334
358
|
pageSource,
|
|
335
|
-
/<ShellOutlet target="practice:sub-pages"
|
|
359
|
+
/<ShellOutlet target="practice:sub-pages" \/>/
|
|
336
360
|
);
|
|
337
361
|
assert.match(pageSource, /<RouterView \/>/);
|
|
338
362
|
});
|
|
@@ -35,10 +35,71 @@ 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
|
+
];
|
|
95
|
+
await writeFileInApp(
|
|
96
|
+
appRoot,
|
|
97
|
+
"src/placementTopology.js",
|
|
98
|
+
`export default {
|
|
99
|
+
placements: [
|
|
100
|
+
${[...defaultEntries, ...entries].join(",\n")}
|
|
101
|
+
]
|
|
102
|
+
};
|
|
42
103
|
`
|
|
43
104
|
);
|
|
44
105
|
}
|
|
@@ -61,8 +122,9 @@ test("buildUiPageTemplateContext resolves link placement from default app ShellO
|
|
|
61
122
|
targetFile: "admin/reports/index.vue",
|
|
62
123
|
options: {}
|
|
63
124
|
});
|
|
64
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell
|
|
65
|
-
assert.equal(context.
|
|
125
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.primary-nav");
|
|
126
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, "");
|
|
127
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
66
128
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
|
|
67
129
|
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
|
|
68
130
|
assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, "");
|
|
@@ -116,15 +178,15 @@ test("buildUiPageTemplateContext supports explicit link placement override", asy
|
|
|
116
178
|
appRoot,
|
|
117
179
|
targetFile: "admin/reports/index.vue",
|
|
118
180
|
options: {
|
|
119
|
-
"link-placement": "shell
|
|
181
|
+
"link-placement": "shell.status"
|
|
120
182
|
}
|
|
121
183
|
});
|
|
122
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell
|
|
123
|
-
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "
|
|
184
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
|
|
185
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
124
186
|
});
|
|
125
187
|
});
|
|
126
188
|
|
|
127
|
-
test("buildUiPageTemplateContext supports explicit package
|
|
189
|
+
test("buildUiPageTemplateContext supports explicit package semantic link placement", async () => {
|
|
128
190
|
await withTempApp(async (appRoot) => {
|
|
129
191
|
await writeConfig(
|
|
130
192
|
appRoot,
|
|
@@ -136,6 +198,22 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
136
198
|
`
|
|
137
199
|
);
|
|
138
200
|
await writeShellLayout(appRoot);
|
|
201
|
+
await writePlacementTopology(appRoot, [
|
|
202
|
+
renderTopologyEntry({
|
|
203
|
+
id: "page.section-nav",
|
|
204
|
+
owner: "catalog",
|
|
205
|
+
surfaces: ["admin"],
|
|
206
|
+
outlet: "catalog:sub-pages",
|
|
207
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
208
|
+
}),
|
|
209
|
+
renderTopologyEntry({
|
|
210
|
+
id: "page.section-nav",
|
|
211
|
+
owner: "catalog-products",
|
|
212
|
+
surfaces: ["admin"],
|
|
213
|
+
outlet: "catalog-products:sub-pages",
|
|
214
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
215
|
+
})
|
|
216
|
+
]);
|
|
139
217
|
await writeFileInApp(
|
|
140
218
|
appRoot,
|
|
141
219
|
".jskit/lock.json",
|
|
@@ -164,13 +242,34 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
164
242
|
metadata: {
|
|
165
243
|
ui: {
|
|
166
244
|
placements: {
|
|
167
|
-
|
|
245
|
+
topology: {
|
|
246
|
+
placements: [
|
|
168
247
|
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
248
|
+
id: "admin.tools-menu",
|
|
249
|
+
surfaces: ["admin"],
|
|
250
|
+
variants: {
|
|
251
|
+
compact: {
|
|
252
|
+
outlet: "admin-cog:primary-menu",
|
|
253
|
+
renderers: {
|
|
254
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
medium: {
|
|
258
|
+
outlet: "admin-cog:primary-menu",
|
|
259
|
+
renderers: {
|
|
260
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
expanded: {
|
|
264
|
+
outlet: "admin-cog:primary-menu",
|
|
265
|
+
renderers: {
|
|
266
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
172
270
|
}
|
|
173
271
|
]
|
|
272
|
+
}
|
|
174
273
|
}
|
|
175
274
|
}
|
|
176
275
|
}
|
|
@@ -182,10 +281,10 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
|
|
|
182
281
|
appRoot,
|
|
183
282
|
targetFile: "admin/reports/index.vue",
|
|
184
283
|
options: {
|
|
185
|
-
"link-placement": "admin-
|
|
284
|
+
"link-placement": "admin.tools-menu"
|
|
186
285
|
}
|
|
187
286
|
});
|
|
188
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin-
|
|
287
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin.tools-menu");
|
|
189
288
|
});
|
|
190
289
|
});
|
|
191
290
|
|
|
@@ -200,15 +299,21 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
|
|
|
200
299
|
};
|
|
201
300
|
`
|
|
202
301
|
);
|
|
302
|
+
await writePlacementTopology(appRoot, [
|
|
303
|
+
renderTopologyEntry({
|
|
304
|
+
id: "page.section-nav",
|
|
305
|
+
owner: "home-settings",
|
|
306
|
+
surfaces: ["home"],
|
|
307
|
+
outlet: "home-settings:primary-menu",
|
|
308
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
309
|
+
})
|
|
310
|
+
]);
|
|
203
311
|
await writeFileInApp(
|
|
204
312
|
appRoot,
|
|
205
313
|
"src/pages/home/settings.vue",
|
|
206
314
|
`<template>
|
|
207
315
|
<section>
|
|
208
|
-
<ShellOutlet
|
|
209
|
-
target="home-settings:primary-menu"
|
|
210
|
-
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
211
|
-
/>
|
|
316
|
+
<ShellOutlet target="home-settings:primary-menu" />
|
|
212
317
|
<RouterView />
|
|
213
318
|
</section>
|
|
214
319
|
</template>
|
|
@@ -221,13 +326,14 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
|
|
|
221
326
|
options: {}
|
|
222
327
|
});
|
|
223
328
|
|
|
224
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
225
|
-
assert.equal(context.
|
|
329
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
330
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"home-settings\",\n");
|
|
331
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
226
332
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, "");
|
|
227
333
|
});
|
|
228
334
|
});
|
|
229
335
|
|
|
230
|
-
test("buildUiPageTemplateContext supports explicit
|
|
336
|
+
test("buildUiPageTemplateContext supports explicit semantic placement and link-to", async () => {
|
|
231
337
|
await withTempApp(async (appRoot) => {
|
|
232
338
|
await writeConfig(
|
|
233
339
|
appRoot,
|
|
@@ -244,12 +350,12 @@ test("buildUiPageTemplateContext supports explicit link component token and link
|
|
|
244
350
|
appRoot,
|
|
245
351
|
targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
|
|
246
352
|
options: {
|
|
247
|
-
"link-placement": "shell
|
|
248
|
-
"link-component-token": "local.main.ui.tab-link-item",
|
|
353
|
+
"link-placement": "shell.status",
|
|
249
354
|
"link-to": "./notes"
|
|
250
355
|
}
|
|
251
356
|
});
|
|
252
|
-
assert.equal(context.
|
|
357
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
|
|
358
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
253
359
|
assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
|
|
254
360
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
255
361
|
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
@@ -294,6 +400,15 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
|
|
|
294
400
|
`
|
|
295
401
|
);
|
|
296
402
|
await writeShellLayout(appRoot);
|
|
403
|
+
await writePlacementTopology(appRoot, [
|
|
404
|
+
renderTopologyEntry({
|
|
405
|
+
id: "page.section-nav",
|
|
406
|
+
owner: "contact-view",
|
|
407
|
+
surfaces: ["admin"],
|
|
408
|
+
outlet: "contact-view:sub-pages",
|
|
409
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
410
|
+
})
|
|
411
|
+
]);
|
|
297
412
|
await writeFileInApp(
|
|
298
413
|
appRoot,
|
|
299
414
|
"src/pages/admin/contacts/[contactId].vue",
|
|
@@ -314,8 +429,9 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
|
|
|
314
429
|
options: {}
|
|
315
430
|
});
|
|
316
431
|
|
|
317
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
318
|
-
assert.equal(context.
|
|
432
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
433
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"contact-view\",\n");
|
|
434
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
319
435
|
assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
|
|
320
436
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
|
|
321
437
|
});
|
|
@@ -333,6 +449,15 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
|
|
|
333
449
|
`
|
|
334
450
|
);
|
|
335
451
|
await writeShellLayout(appRoot);
|
|
452
|
+
await writePlacementTopology(appRoot, [
|
|
453
|
+
renderTopologyEntry({
|
|
454
|
+
id: "page.section-nav",
|
|
455
|
+
owner: "contact-view",
|
|
456
|
+
surfaces: ["admin"],
|
|
457
|
+
outlet: "contact-view:sub-pages",
|
|
458
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
459
|
+
})
|
|
460
|
+
]);
|
|
336
461
|
await writeFileInApp(
|
|
337
462
|
appRoot,
|
|
338
463
|
"src/pages/admin/contacts/[contactId].vue",
|
|
@@ -353,8 +478,8 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
|
|
|
353
478
|
options: {}
|
|
354
479
|
});
|
|
355
480
|
|
|
356
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
357
|
-
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "
|
|
481
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
482
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
358
483
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes/history\",\n");
|
|
359
484
|
});
|
|
360
485
|
});
|
|
@@ -371,6 +496,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
|
|
|
371
496
|
`
|
|
372
497
|
);
|
|
373
498
|
await writeShellLayout(appRoot);
|
|
499
|
+
await writePlacementTopology(appRoot, [
|
|
500
|
+
renderTopologyEntry({
|
|
501
|
+
id: "page.section-nav",
|
|
502
|
+
owner: "catalog",
|
|
503
|
+
surfaces: ["admin"],
|
|
504
|
+
outlet: "catalog:sub-pages",
|
|
505
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
506
|
+
})
|
|
507
|
+
]);
|
|
374
508
|
await writeFileInApp(
|
|
375
509
|
appRoot,
|
|
376
510
|
"src/pages/admin/catalog/index.vue",
|
|
@@ -391,8 +525,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
|
|
|
391
525
|
options: {}
|
|
392
526
|
});
|
|
393
527
|
|
|
394
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
395
|
-
assert.equal(context.
|
|
528
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
529
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog\",\n");
|
|
530
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
396
531
|
assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
|
|
397
532
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./products\",\n");
|
|
398
533
|
});
|
|
@@ -410,6 +545,22 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
|
|
|
410
545
|
`
|
|
411
546
|
);
|
|
412
547
|
await writeShellLayout(appRoot);
|
|
548
|
+
await writePlacementTopology(appRoot, [
|
|
549
|
+
renderTopologyEntry({
|
|
550
|
+
id: "page.section-nav",
|
|
551
|
+
owner: "catalog",
|
|
552
|
+
surfaces: ["admin"],
|
|
553
|
+
outlet: "catalog:sub-pages",
|
|
554
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
555
|
+
}),
|
|
556
|
+
renderTopologyEntry({
|
|
557
|
+
id: "page.section-nav",
|
|
558
|
+
owner: "catalog-products",
|
|
559
|
+
surfaces: ["admin"],
|
|
560
|
+
outlet: "catalog-products:sub-pages",
|
|
561
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
562
|
+
})
|
|
563
|
+
]);
|
|
413
564
|
await writeFileInApp(
|
|
414
565
|
appRoot,
|
|
415
566
|
"src/pages/admin/catalog/index.vue",
|
|
@@ -443,8 +594,9 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
|
|
|
443
594
|
options: {}
|
|
444
595
|
});
|
|
445
596
|
|
|
446
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
447
|
-
assert.equal(context.
|
|
597
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
598
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog-products\",\n");
|
|
599
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
448
600
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./variants\",\n");
|
|
449
601
|
});
|
|
450
602
|
});
|
|
@@ -461,6 +613,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
|
|
|
461
613
|
`
|
|
462
614
|
);
|
|
463
615
|
await writeShellLayout(appRoot);
|
|
616
|
+
await writePlacementTopology(appRoot, [
|
|
617
|
+
renderTopologyEntry({
|
|
618
|
+
id: "page.section-nav",
|
|
619
|
+
owner: "customer-view",
|
|
620
|
+
surfaces: ["admin"],
|
|
621
|
+
outlet: "customer-view:sub-pages",
|
|
622
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
623
|
+
})
|
|
624
|
+
]);
|
|
464
625
|
await writeFileInApp(
|
|
465
626
|
appRoot,
|
|
466
627
|
"src/pages/admin/customers/[customerId]/index.vue",
|
|
@@ -481,8 +642,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
|
|
|
481
642
|
options: {}
|
|
482
643
|
});
|
|
483
644
|
|
|
484
|
-
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "
|
|
485
|
-
assert.equal(context.
|
|
645
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
|
|
646
|
+
assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"customer-view\",\n");
|
|
647
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
|
|
486
648
|
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./pets\",\n");
|
|
487
649
|
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/customers/[customerId]/pets");
|
|
488
650
|
});
|
|
@@ -641,7 +803,7 @@ test("buildUiPageTemplateContext validates link placement format", async () => {
|
|
|
641
803
|
"link-placement": "invalid-placement"
|
|
642
804
|
}
|
|
643
805
|
}),
|
|
644
|
-
/option "placement" must be a target in "
|
|
806
|
+
/option "placement" must be a semantic target in "area.slot" format/
|
|
645
807
|
);
|
|
646
808
|
});
|
|
647
809
|
});
|
|
@@ -14,6 +14,39 @@ async function withTempApp(run) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function renderTopologyEntry({
|
|
18
|
+
id = "",
|
|
19
|
+
owner = "",
|
|
20
|
+
surfaces = ["*"],
|
|
21
|
+
defaultPlacement = false,
|
|
22
|
+
outlet = "",
|
|
23
|
+
linkRenderer = ""
|
|
24
|
+
} = {}) {
|
|
25
|
+
const ownerLine = owner ? ` owner: "${owner}",\n` : "";
|
|
26
|
+
const defaultLine = defaultPlacement ? " default: true,\n" : "";
|
|
27
|
+
const rendererLines = linkRenderer
|
|
28
|
+
? `,
|
|
29
|
+
renderers: {
|
|
30
|
+
link: "${linkRenderer}"
|
|
31
|
+
}`
|
|
32
|
+
: "";
|
|
33
|
+
return ` {
|
|
34
|
+
id: "${id}",
|
|
35
|
+
${ownerLine} surfaces: ${JSON.stringify(surfaces)},
|
|
36
|
+
${defaultLine} variants: {
|
|
37
|
+
compact: {
|
|
38
|
+
outlet: "${outlet}"${rendererLines}
|
|
39
|
+
},
|
|
40
|
+
medium: {
|
|
41
|
+
outlet: "${outlet}"${rendererLines}
|
|
42
|
+
},
|
|
43
|
+
expanded: {
|
|
44
|
+
outlet: "${outlet}"${rendererLines}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
17
50
|
async function writeAppFixture(appRoot) {
|
|
18
51
|
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
19
52
|
await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
|
|
@@ -43,11 +76,39 @@ async function writeAppFixture(appRoot) {
|
|
|
43
76
|
<ShellOutlet
|
|
44
77
|
target="shell-layout:primary-menu"
|
|
45
78
|
default
|
|
46
|
-
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
47
79
|
/>
|
|
48
80
|
<ShellOutlet target="shell-layout:top-right" />
|
|
49
81
|
</div>
|
|
50
82
|
</template>
|
|
83
|
+
`,
|
|
84
|
+
"utf8"
|
|
85
|
+
);
|
|
86
|
+
await writeFile(
|
|
87
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
88
|
+
`export default {
|
|
89
|
+
placements: [
|
|
90
|
+
${[
|
|
91
|
+
renderTopologyEntry({
|
|
92
|
+
id: "shell.primary-nav",
|
|
93
|
+
surfaces: ["*"],
|
|
94
|
+
defaultPlacement: true,
|
|
95
|
+
outlet: "shell-layout:primary-menu",
|
|
96
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
97
|
+
}),
|
|
98
|
+
renderTopologyEntry({
|
|
99
|
+
id: "shell.status",
|
|
100
|
+
surfaces: ["*"],
|
|
101
|
+
outlet: "shell-layout:top-right"
|
|
102
|
+
}),
|
|
103
|
+
renderTopologyEntry({
|
|
104
|
+
id: "settings.sections",
|
|
105
|
+
owner: "admin-settings",
|
|
106
|
+
surfaces: ["admin"],
|
|
107
|
+
outlet: "admin-settings:forms"
|
|
108
|
+
})
|
|
109
|
+
].join(",\n")}
|
|
110
|
+
]
|
|
111
|
+
};
|
|
51
112
|
`,
|
|
52
113
|
"utf8"
|
|
53
114
|
);
|
|
@@ -116,7 +177,8 @@ test("ui-generator placed-element subcommand creates component and outlet placem
|
|
|
116
177
|
|
|
117
178
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
118
179
|
assert.match(placementSource, /id: "ui-generator\.element\.ops-panel"/);
|
|
119
|
-
assert.match(placementSource, /target: "shell
|
|
180
|
+
assert.match(placementSource, /target: "shell\.status"/);
|
|
181
|
+
assert.match(placementSource, /kind: "component"/);
|
|
120
182
|
assert.match(placementSource, /componentToken: "local\.main\.ui\.element\.ops-panel"/);
|
|
121
183
|
});
|
|
122
184
|
});
|
|
@@ -131,16 +193,16 @@ test("ui-generator placed-element subcommand supports explicit placement overrid
|
|
|
131
193
|
options: {
|
|
132
194
|
name: "Ops Panel",
|
|
133
195
|
surface: "admin",
|
|
134
|
-
placement: "shell
|
|
196
|
+
placement: "shell.primary-nav"
|
|
135
197
|
}
|
|
136
198
|
});
|
|
137
199
|
|
|
138
200
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
139
|
-
assert.match(placementSource, /target: "shell
|
|
201
|
+
assert.match(placementSource, /target: "shell\.primary-nav"/);
|
|
140
202
|
});
|
|
141
203
|
});
|
|
142
204
|
|
|
143
|
-
test("ui-generator placed-element infers surface from
|
|
205
|
+
test("ui-generator placed-element infers surface from an owner-scoped semantic placement", async () => {
|
|
144
206
|
await withTempApp(async (appRoot) => {
|
|
145
207
|
await writeAppFixture(appRoot);
|
|
146
208
|
|
|
@@ -149,12 +211,14 @@ test("ui-generator placed-element infers surface from a page-owned placement tar
|
|
|
149
211
|
subcommand: "placed-element",
|
|
150
212
|
options: {
|
|
151
213
|
name: "Ops Panel",
|
|
152
|
-
placement: "
|
|
214
|
+
placement: "settings.sections",
|
|
215
|
+
owner: "admin-settings"
|
|
153
216
|
}
|
|
154
217
|
});
|
|
155
218
|
|
|
156
219
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
157
|
-
assert.match(placementSource, /target: "
|
|
220
|
+
assert.match(placementSource, /target: "settings\.sections"/);
|
|
221
|
+
assert.match(placementSource, /owner: "admin-settings"/);
|
|
158
222
|
assert.match(placementSource, /surfaces: \["admin"\]/);
|
|
159
223
|
});
|
|
160
224
|
});
|
|
@@ -172,7 +236,7 @@ test("ui-generator placed-element infers the only enabled surface for shared she
|
|
|
172
236
|
});
|
|
173
237
|
|
|
174
238
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
175
|
-
assert.match(placementSource, /target: "shell
|
|
239
|
+
assert.match(placementSource, /target: "shell\.status"/);
|
|
176
240
|
assert.match(placementSource, /surfaces: \["admin"\]/);
|
|
177
241
|
});
|
|
178
242
|
});
|
|
@@ -210,7 +274,7 @@ test("ui-generator placed-element requires explicit surface when a shared shell
|
|
|
210
274
|
name: "Ops Panel"
|
|
211
275
|
}
|
|
212
276
|
}),
|
|
213
|
-
/could not infer a surface for placement target "shell
|
|
277
|
+
/could not infer a surface for placement target "shell.status". Pass --surface explicitly/
|
|
214
278
|
);
|
|
215
279
|
});
|
|
216
280
|
});
|
|
@@ -226,11 +290,12 @@ test("ui-generator placed-element rejects explicit surfaces that conflict with p
|
|
|
226
290
|
subcommand: "placed-element",
|
|
227
291
|
options: {
|
|
228
292
|
name: "Ops Panel",
|
|
229
|
-
placement: "
|
|
293
|
+
placement: "settings.sections",
|
|
294
|
+
owner: "admin-settings",
|
|
230
295
|
surface: "console"
|
|
231
296
|
}
|
|
232
297
|
}),
|
|
233
|
-
/target "
|
|
298
|
+
/target "settings.sections" is not available on surface "console"/
|
|
234
299
|
);
|
|
235
300
|
});
|
|
236
301
|
});
|