@jskit-ai/ui-generator 0.1.14 → 0.1.15
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/README.md +147 -123
- package/package.descriptor.mjs +182 -82
- package/package.json +2 -2
- package/src/server/buildTemplateContext.js +31 -136
- package/src/server/subcommands/addSubpages.js +73 -0
- package/src/server/subcommands/element.js +30 -15
- package/src/server/subcommands/outlet.js +26 -126
- package/src/server/subcommands/page.js +142 -0
- package/src/server/subcommands/pageSupport.js +552 -0
- package/src/server/subcommands/support.js +145 -1
- package/test/addSubpagesSubcommand.test.js +321 -0
- package/test/buildTemplateContext.test.js +426 -65
- package/test/elementSubcommand.test.js +79 -6
- package/test/outletSubcommand.test.js +92 -29
- package/test/packageDescriptor.test.js +10 -0
- package/test/pageSubcommand.test.js +352 -0
- package/src/server/subcommands/container.js +0 -644
- package/templates/src/pages/admin/ui-generator/Page.vue +0 -6
- package/test/containerSubcommand.test.js +0 -307
|
@@ -14,11 +14,20 @@ async function withTempApp(run) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async function
|
|
17
|
+
async function writeFileInApp(appRoot, relativePath, source) {
|
|
18
18
|
const absoluteFile = path.join(appRoot, relativePath);
|
|
19
19
|
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
20
|
-
await writeFile(
|
|
21
|
-
|
|
20
|
+
await writeFile(absoluteFile, source, "utf8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function writeConfig(appRoot, source) {
|
|
24
|
+
await writeFileInApp(appRoot, "config/public.js", source);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function writeShellLayout(appRoot, source = "") {
|
|
28
|
+
await writeFileInApp(
|
|
29
|
+
appRoot,
|
|
30
|
+
"src/components/ShellLayout.vue",
|
|
22
31
|
source ||
|
|
23
32
|
`<template>
|
|
24
33
|
<div>
|
|
@@ -26,67 +35,76 @@ async function writeVueFile(appRoot, relativePath, source = "") {
|
|
|
26
35
|
<ShellOutlet host="shell-layout" position="primary-menu" default />
|
|
27
36
|
</div>
|
|
28
37
|
</template>
|
|
29
|
-
|
|
30
|
-
"utf8"
|
|
38
|
+
`
|
|
31
39
|
);
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
test("buildUiPageTemplateContext resolves placement from default app ShellOutlet target", async () => {
|
|
42
|
+
test("buildUiPageTemplateContext resolves link placement from default app ShellOutlet target", async () => {
|
|
35
43
|
await withTempApp(async (appRoot) => {
|
|
36
|
-
await
|
|
37
|
-
appRoot,
|
|
38
|
-
"src/components/ShellLayout.vue",
|
|
39
|
-
`<template>
|
|
40
|
-
<div>
|
|
41
|
-
<ShellOutlet host="shell-layout" position="top-right" />
|
|
42
|
-
<ShellOutlet host="shell-layout" position="primary-menu" />
|
|
43
|
-
</div>
|
|
44
|
-
</template>
|
|
45
|
-
`
|
|
46
|
-
);
|
|
47
|
-
await writeVueFile(
|
|
44
|
+
await writeConfig(
|
|
48
45
|
appRoot,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</template>
|
|
46
|
+
`export const config = {
|
|
47
|
+
surfaceDefinitions: {
|
|
48
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
49
|
+
}
|
|
50
|
+
};
|
|
55
51
|
`
|
|
56
52
|
);
|
|
53
|
+
await writeShellLayout(appRoot);
|
|
57
54
|
|
|
58
55
|
const context = await buildUiPageTemplateContext({
|
|
59
56
|
appRoot,
|
|
57
|
+
targetFile: "admin/reports/index.vue",
|
|
60
58
|
options: {}
|
|
61
59
|
});
|
|
62
|
-
assert.equal(context.
|
|
63
|
-
assert.equal(context.
|
|
64
|
-
assert.equal(context.
|
|
65
|
-
assert.equal(context.
|
|
66
|
-
assert.equal(context.
|
|
67
|
-
assert.equal(context.
|
|
60
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "shell-layout");
|
|
61
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "primary-menu");
|
|
62
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "users.web.shell.surface-aware-menu-link-item");
|
|
63
|
+
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
|
|
64
|
+
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
|
|
65
|
+
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, "");
|
|
66
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.reports.link");
|
|
68
67
|
});
|
|
69
68
|
});
|
|
70
69
|
|
|
71
|
-
test("buildUiPageTemplateContext supports explicit placement override", async () => {
|
|
70
|
+
test("buildUiPageTemplateContext supports explicit link placement override", async () => {
|
|
72
71
|
await withTempApp(async (appRoot) => {
|
|
73
|
-
await
|
|
72
|
+
await writeConfig(
|
|
73
|
+
appRoot,
|
|
74
|
+
`export const config = {
|
|
75
|
+
surfaceDefinitions: {
|
|
76
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
`
|
|
80
|
+
);
|
|
81
|
+
await writeShellLayout(appRoot);
|
|
74
82
|
|
|
75
83
|
const context = await buildUiPageTemplateContext({
|
|
76
84
|
appRoot,
|
|
85
|
+
targetFile: "admin/reports/index.vue",
|
|
77
86
|
options: {
|
|
78
|
-
placement: "shell-layout:top-right"
|
|
87
|
+
"link-placement": "shell-layout:top-right"
|
|
79
88
|
}
|
|
80
89
|
});
|
|
81
|
-
assert.equal(context.
|
|
82
|
-
assert.equal(context.
|
|
90
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "top-right");
|
|
91
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "users.web.shell.surface-aware-menu-link-item");
|
|
83
92
|
});
|
|
84
93
|
});
|
|
85
94
|
|
|
86
|
-
test("buildUiPageTemplateContext supports explicit package outlet placement", async () => {
|
|
95
|
+
test("buildUiPageTemplateContext supports explicit package outlet link placement", async () => {
|
|
87
96
|
await withTempApp(async (appRoot) => {
|
|
88
|
-
await
|
|
89
|
-
|
|
97
|
+
await writeConfig(
|
|
98
|
+
appRoot,
|
|
99
|
+
`export const config = {
|
|
100
|
+
surfaceDefinitions: {
|
|
101
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
await writeShellLayout(appRoot);
|
|
107
|
+
await writeFileInApp(
|
|
90
108
|
appRoot,
|
|
91
109
|
".jskit/lock.json",
|
|
92
110
|
`${JSON.stringify(
|
|
@@ -106,7 +124,7 @@ test("buildUiPageTemplateContext supports explicit package outlet placement", as
|
|
|
106
124
|
2
|
|
107
125
|
)}\n`
|
|
108
126
|
);
|
|
109
|
-
await
|
|
127
|
+
await writeFileInApp(
|
|
110
128
|
appRoot,
|
|
111
129
|
"node_modules/@example/users-web/package.descriptor.mjs",
|
|
112
130
|
`export default {
|
|
@@ -126,63 +144,406 @@ test("buildUiPageTemplateContext supports explicit package outlet placement", as
|
|
|
126
144
|
|
|
127
145
|
const context = await buildUiPageTemplateContext({
|
|
128
146
|
appRoot,
|
|
147
|
+
targetFile: "admin/reports/index.vue",
|
|
129
148
|
options: {
|
|
130
|
-
placement: "workspace-tools:primary-menu"
|
|
149
|
+
"link-placement": "workspace-tools:primary-menu"
|
|
131
150
|
}
|
|
132
151
|
});
|
|
133
|
-
assert.equal(context.
|
|
134
|
-
assert.equal(context.
|
|
152
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "workspace-tools");
|
|
153
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "primary-menu");
|
|
135
154
|
});
|
|
136
155
|
});
|
|
137
156
|
|
|
138
|
-
test("buildUiPageTemplateContext supports explicit
|
|
157
|
+
test("buildUiPageTemplateContext supports explicit link component token and link-to", async () => {
|
|
139
158
|
await withTempApp(async (appRoot) => {
|
|
140
|
-
await
|
|
159
|
+
await writeConfig(
|
|
160
|
+
appRoot,
|
|
161
|
+
`export const config = {
|
|
162
|
+
surfaceDefinitions: {
|
|
163
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
`
|
|
167
|
+
);
|
|
168
|
+
await writeShellLayout(appRoot);
|
|
141
169
|
|
|
142
170
|
const context = await buildUiPageTemplateContext({
|
|
143
171
|
appRoot,
|
|
172
|
+
targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
|
|
144
173
|
options: {
|
|
145
|
-
|
|
146
|
-
"
|
|
147
|
-
|
|
148
|
-
"placement-component-token": "local.main.ui.tab-link-item",
|
|
149
|
-
"placement-to": "./notes"
|
|
174
|
+
"link-placement": "shell-layout:top-right",
|
|
175
|
+
"link-component-token": "local.main.ui.tab-link-item",
|
|
176
|
+
"link-to": "./notes"
|
|
150
177
|
}
|
|
151
178
|
});
|
|
152
|
-
assert.equal(context.
|
|
153
|
-
assert.equal(context.
|
|
154
|
-
assert.equal(context.
|
|
155
|
-
assert.equal(context.
|
|
179
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
180
|
+
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
181
|
+
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
182
|
+
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
|
|
183
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.contacts.contact-id.notes.link");
|
|
156
184
|
});
|
|
157
185
|
});
|
|
158
186
|
|
|
159
|
-
test("buildUiPageTemplateContext
|
|
187
|
+
test("buildUiPageTemplateContext derives native route suffixes for index-owned child pages", async () => {
|
|
160
188
|
await withTempApp(async (appRoot) => {
|
|
161
|
-
await
|
|
189
|
+
await writeConfig(
|
|
190
|
+
appRoot,
|
|
191
|
+
`export const config = {
|
|
192
|
+
surfaceDefinitions: {
|
|
193
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
`
|
|
197
|
+
);
|
|
198
|
+
await writeShellLayout(appRoot);
|
|
162
199
|
|
|
163
200
|
const context = await buildUiPageTemplateContext({
|
|
164
201
|
appRoot,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
|
|
203
|
+
options: {}
|
|
204
|
+
});
|
|
205
|
+
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
206
|
+
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
|
|
207
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.contacts.contact-id.notes.link");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("buildUiPageTemplateContext infers subpage link placement, tab token, and link-to from a file-route parent host", async () => {
|
|
212
|
+
await withTempApp(async (appRoot) => {
|
|
213
|
+
await writeConfig(
|
|
214
|
+
appRoot,
|
|
215
|
+
`export const config = {
|
|
216
|
+
surfaceDefinitions: {
|
|
217
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
`
|
|
221
|
+
);
|
|
222
|
+
await writeShellLayout(appRoot);
|
|
223
|
+
await writeFileInApp(
|
|
224
|
+
appRoot,
|
|
225
|
+
"src/pages/admin/contacts/[contactId].vue",
|
|
226
|
+
`<template>
|
|
227
|
+
<SectionContainerShell>
|
|
228
|
+
<template #tabs>
|
|
229
|
+
<ShellOutlet host="contact-view" position="sub-pages" />
|
|
230
|
+
</template>
|
|
231
|
+
<RouterView />
|
|
232
|
+
</SectionContainerShell>
|
|
233
|
+
</template>
|
|
234
|
+
`
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const context = await buildUiPageTemplateContext({
|
|
238
|
+
appRoot,
|
|
239
|
+
targetFile: "admin/contacts/[contactId]/notes/index.vue",
|
|
240
|
+
options: {}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "contact-view");
|
|
244
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
245
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
246
|
+
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("buildUiPageTemplateContext inherits a file-route parent host for deeper descendants", async () => {
|
|
251
|
+
await withTempApp(async (appRoot) => {
|
|
252
|
+
await writeConfig(
|
|
253
|
+
appRoot,
|
|
254
|
+
`export const config = {
|
|
255
|
+
surfaceDefinitions: {
|
|
256
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
`
|
|
260
|
+
);
|
|
261
|
+
await writeShellLayout(appRoot);
|
|
262
|
+
await writeFileInApp(
|
|
263
|
+
appRoot,
|
|
264
|
+
"src/pages/admin/contacts/[contactId].vue",
|
|
265
|
+
`<template>
|
|
266
|
+
<SectionContainerShell>
|
|
267
|
+
<template #tabs>
|
|
268
|
+
<ShellOutlet host="contact-view" position="sub-pages" />
|
|
269
|
+
</template>
|
|
270
|
+
<RouterView />
|
|
271
|
+
</SectionContainerShell>
|
|
272
|
+
</template>
|
|
273
|
+
`
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const context = await buildUiPageTemplateContext({
|
|
277
|
+
appRoot,
|
|
278
|
+
targetFile: "admin/contacts/[contactId]/notes/history/index.vue",
|
|
279
|
+
options: {}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "contact-view");
|
|
283
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
284
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
285
|
+
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes/history\",\n");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("buildUiPageTemplateContext infers subpage link placement from an index-route parent host", async () => {
|
|
290
|
+
await withTempApp(async (appRoot) => {
|
|
291
|
+
await writeConfig(
|
|
292
|
+
appRoot,
|
|
293
|
+
`export const config = {
|
|
294
|
+
surfaceDefinitions: {
|
|
295
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
`
|
|
299
|
+
);
|
|
300
|
+
await writeShellLayout(appRoot);
|
|
301
|
+
await writeFileInApp(
|
|
302
|
+
appRoot,
|
|
303
|
+
"src/pages/admin/catalog/index.vue",
|
|
304
|
+
`<template>
|
|
305
|
+
<SectionContainerShell>
|
|
306
|
+
<template #tabs>
|
|
307
|
+
<ShellOutlet host="catalog" position="sub-pages" />
|
|
308
|
+
</template>
|
|
309
|
+
<RouterView />
|
|
310
|
+
</SectionContainerShell>
|
|
311
|
+
</template>
|
|
312
|
+
`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const context = await buildUiPageTemplateContext({
|
|
316
|
+
appRoot,
|
|
317
|
+
targetFile: "admin/catalog/index/products/index.vue",
|
|
318
|
+
options: {}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "catalog");
|
|
322
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
323
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
324
|
+
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./products\",\n");
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("buildUiPageTemplateContext finds the nearest index-route parent host", async () => {
|
|
329
|
+
await withTempApp(async (appRoot) => {
|
|
330
|
+
await writeConfig(
|
|
331
|
+
appRoot,
|
|
332
|
+
`export const config = {
|
|
333
|
+
surfaceDefinitions: {
|
|
334
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
`
|
|
338
|
+
);
|
|
339
|
+
await writeShellLayout(appRoot);
|
|
340
|
+
await writeFileInApp(
|
|
341
|
+
appRoot,
|
|
342
|
+
"src/pages/admin/catalog/index.vue",
|
|
343
|
+
`<template>
|
|
344
|
+
<SectionContainerShell>
|
|
345
|
+
<template #tabs>
|
|
346
|
+
<ShellOutlet host="catalog" position="sub-pages" />
|
|
347
|
+
</template>
|
|
348
|
+
<RouterView />
|
|
349
|
+
</SectionContainerShell>
|
|
350
|
+
</template>
|
|
351
|
+
`
|
|
352
|
+
);
|
|
353
|
+
await writeFileInApp(
|
|
354
|
+
appRoot,
|
|
355
|
+
"src/pages/admin/catalog/index/products/index.vue",
|
|
356
|
+
`<template>
|
|
357
|
+
<SectionContainerShell>
|
|
358
|
+
<template #tabs>
|
|
359
|
+
<ShellOutlet host="catalog-products" position="sub-pages" />
|
|
360
|
+
</template>
|
|
361
|
+
<RouterView />
|
|
362
|
+
</SectionContainerShell>
|
|
363
|
+
</template>
|
|
364
|
+
`
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const context = await buildUiPageTemplateContext({
|
|
368
|
+
appRoot,
|
|
369
|
+
targetFile: "admin/catalog/index/products/index/variants/index.vue",
|
|
370
|
+
options: {}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "catalog-products");
|
|
374
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
375
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
376
|
+
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./variants\",\n");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("buildUiPageTemplateContext infers subpage link placement from an index route hosted record page", async () => {
|
|
381
|
+
await withTempApp(async (appRoot) => {
|
|
382
|
+
await writeConfig(
|
|
383
|
+
appRoot,
|
|
384
|
+
`export const config = {
|
|
385
|
+
surfaceDefinitions: {
|
|
386
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
`
|
|
390
|
+
);
|
|
391
|
+
await writeShellLayout(appRoot);
|
|
392
|
+
await writeFileInApp(
|
|
393
|
+
appRoot,
|
|
394
|
+
"src/pages/admin/customers/[customerId]/index.vue",
|
|
395
|
+
`<template>
|
|
396
|
+
<SectionContainerShell>
|
|
397
|
+
<template #tabs>
|
|
398
|
+
<ShellOutlet host="customer-view" position="sub-pages" />
|
|
399
|
+
</template>
|
|
400
|
+
<RouterView />
|
|
401
|
+
</SectionContainerShell>
|
|
402
|
+
</template>
|
|
403
|
+
`
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const context = await buildUiPageTemplateContext({
|
|
407
|
+
appRoot,
|
|
408
|
+
targetFile: "admin/customers/[customerId]/index/pets/index.vue",
|
|
409
|
+
options: {}
|
|
170
410
|
});
|
|
171
|
-
|
|
172
|
-
assert.equal(context.
|
|
411
|
+
|
|
412
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "customer-view");
|
|
413
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
|
|
414
|
+
assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
|
|
415
|
+
assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./pets\",\n");
|
|
416
|
+
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/customers/[customerId]/pets");
|
|
173
417
|
});
|
|
174
418
|
});
|
|
175
419
|
|
|
176
|
-
test("buildUiPageTemplateContext
|
|
420
|
+
test("buildUiPageTemplateContext derives the same visible route from file and index page shapes", async () => {
|
|
177
421
|
await withTempApp(async (appRoot) => {
|
|
178
|
-
await
|
|
422
|
+
await writeConfig(
|
|
423
|
+
appRoot,
|
|
424
|
+
`export const config = {
|
|
425
|
+
surfaceDefinitions: {
|
|
426
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
`
|
|
430
|
+
);
|
|
431
|
+
await writeShellLayout(appRoot);
|
|
432
|
+
|
|
433
|
+
const fileContext = await buildUiPageTemplateContext({
|
|
434
|
+
appRoot,
|
|
435
|
+
targetFile: "admin/catalog.vue",
|
|
436
|
+
options: {}
|
|
437
|
+
});
|
|
438
|
+
const indexContext = await buildUiPageTemplateContext({
|
|
439
|
+
appRoot,
|
|
440
|
+
targetFile: "admin/catalog/index.vue",
|
|
441
|
+
options: {}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
assert.equal(fileContext.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/catalog");
|
|
445
|
+
assert.equal(indexContext.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/catalog");
|
|
446
|
+
assert.equal(fileContext.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.catalog.link");
|
|
447
|
+
assert.equal(indexContext.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.catalog.link");
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("buildUiPageTemplateContext rejects target files with a leading src segment", async () => {
|
|
452
|
+
await withTempApp(async (appRoot) => {
|
|
453
|
+
await writeConfig(
|
|
454
|
+
appRoot,
|
|
455
|
+
`export const config = {
|
|
456
|
+
surfaceDefinitions: {
|
|
457
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
`
|
|
461
|
+
);
|
|
462
|
+
await writeShellLayout(appRoot);
|
|
463
|
+
|
|
464
|
+
await assert.rejects(
|
|
465
|
+
() =>
|
|
466
|
+
buildUiPageTemplateContext({
|
|
467
|
+
appRoot,
|
|
468
|
+
targetFile: "src/components/ReportsPanel.vue",
|
|
469
|
+
options: {}
|
|
470
|
+
}),
|
|
471
|
+
/must be relative to src\/pages\/, without a leading src\/ segment/
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("buildUiPageTemplateContext fails when the target file matches no surface", async () => {
|
|
477
|
+
await withTempApp(async (appRoot) => {
|
|
478
|
+
await writeConfig(
|
|
479
|
+
appRoot,
|
|
480
|
+
`export const config = {
|
|
481
|
+
surfaceDefinitions: {
|
|
482
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
`
|
|
486
|
+
);
|
|
487
|
+
await writeShellLayout(appRoot);
|
|
488
|
+
|
|
489
|
+
await assert.rejects(
|
|
490
|
+
() =>
|
|
491
|
+
buildUiPageTemplateContext({
|
|
492
|
+
appRoot,
|
|
493
|
+
targetFile: "reports/index.vue",
|
|
494
|
+
options: {}
|
|
495
|
+
}),
|
|
496
|
+
/must be relative to src\/pages\/ and resolve to a configured surface/
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("buildUiPageTemplateContext chooses the most specific matching surface pagesRoot", async () => {
|
|
502
|
+
await withTempApp(async (appRoot) => {
|
|
503
|
+
await writeConfig(
|
|
504
|
+
appRoot,
|
|
505
|
+
`export const config = {
|
|
506
|
+
surfaceDefinitions: {
|
|
507
|
+
app: { id: "app", pagesRoot: "", enabled: true },
|
|
508
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
`
|
|
512
|
+
);
|
|
513
|
+
await writeShellLayout(appRoot);
|
|
514
|
+
|
|
515
|
+
const context = await buildUiPageTemplateContext({
|
|
516
|
+
appRoot,
|
|
517
|
+
targetFile: "admin/reports/index.vue",
|
|
518
|
+
options: {}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.reports.link");
|
|
522
|
+
assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
|
|
523
|
+
assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("buildUiPageTemplateContext validates link placement format", async () => {
|
|
528
|
+
await withTempApp(async (appRoot) => {
|
|
529
|
+
await writeConfig(
|
|
530
|
+
appRoot,
|
|
531
|
+
`export const config = {
|
|
532
|
+
surfaceDefinitions: {
|
|
533
|
+
admin: { id: "admin", pagesRoot: "admin", enabled: true }
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
`
|
|
537
|
+
);
|
|
538
|
+
await writeShellLayout(appRoot);
|
|
179
539
|
|
|
180
540
|
await assert.rejects(
|
|
181
541
|
() =>
|
|
182
542
|
buildUiPageTemplateContext({
|
|
183
543
|
appRoot,
|
|
544
|
+
targetFile: "admin/reports/index.vue",
|
|
184
545
|
options: {
|
|
185
|
-
placement: "invalid-placement"
|
|
546
|
+
"link-placement": "invalid-placement"
|
|
186
547
|
}
|
|
187
548
|
}),
|
|
188
549
|
/option "placement" must be in "host:position" format/
|
|
@@ -76,17 +76,16 @@ export { MainClientProvider, registerMainClientComponent };
|
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
test("ui-generator element subcommand creates component and outlet placement", async () => {
|
|
79
|
+
test("ui-generator placed-element subcommand creates component and outlet placement", async () => {
|
|
80
80
|
await withTempApp(async (appRoot) => {
|
|
81
81
|
await writeAppFixture(appRoot);
|
|
82
82
|
|
|
83
83
|
const result = await runGeneratorSubcommand({
|
|
84
84
|
appRoot,
|
|
85
|
-
subcommand: "element",
|
|
85
|
+
subcommand: "placed-element",
|
|
86
86
|
options: {
|
|
87
87
|
name: "Ops Panel",
|
|
88
|
-
surface: "admin"
|
|
89
|
-
placement: "shell-layout:top-right"
|
|
88
|
+
surface: "admin"
|
|
90
89
|
}
|
|
91
90
|
});
|
|
92
91
|
|
|
@@ -111,12 +110,86 @@ test("ui-generator element subcommand creates component and outlet placement", a
|
|
|
111
110
|
});
|
|
112
111
|
});
|
|
113
112
|
|
|
114
|
-
test("ui-generator element subcommand
|
|
113
|
+
test("ui-generator placed-element subcommand supports explicit placement override", async () => {
|
|
114
|
+
await withTempApp(async (appRoot) => {
|
|
115
|
+
await writeAppFixture(appRoot);
|
|
116
|
+
|
|
117
|
+
await runGeneratorSubcommand({
|
|
118
|
+
appRoot,
|
|
119
|
+
subcommand: "placed-element",
|
|
120
|
+
options: {
|
|
121
|
+
name: "Ops Panel",
|
|
122
|
+
surface: "admin",
|
|
123
|
+
placement: "shell-layout:primary-menu"
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
128
|
+
assert.match(placementSource, /host: "shell-layout"/);
|
|
129
|
+
assert.match(placementSource, /position: "primary-menu"/);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("ui-generator placed-element subcommand refuses to overwrite an existing component without force", async () => {
|
|
134
|
+
await withTempApp(async (appRoot) => {
|
|
135
|
+
await writeAppFixture(appRoot);
|
|
136
|
+
await writeFile(
|
|
137
|
+
path.join(appRoot, "src", "components", "OpsPanelElement.vue"),
|
|
138
|
+
"<template><div>custom</div></template>\n",
|
|
139
|
+
"utf8"
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await assert.rejects(
|
|
143
|
+
() =>
|
|
144
|
+
runGeneratorSubcommand({
|
|
145
|
+
appRoot,
|
|
146
|
+
subcommand: "placed-element",
|
|
147
|
+
options: {
|
|
148
|
+
name: "Ops Panel",
|
|
149
|
+
surface: "admin"
|
|
150
|
+
}
|
|
151
|
+
}),
|
|
152
|
+
/ui-generator placed-element will not overwrite existing component file src\/components\/OpsPanelElement\.vue\. Re-run with --force to overwrite it\./
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("ui-generator placed-element subcommand overwrites an existing component when force is enabled", async () => {
|
|
158
|
+
await withTempApp(async (appRoot) => {
|
|
159
|
+
await writeAppFixture(appRoot);
|
|
160
|
+
await writeFile(
|
|
161
|
+
path.join(appRoot, "src", "components", "OpsPanelElement.vue"),
|
|
162
|
+
"<template><div>custom</div></template>\n",
|
|
163
|
+
"utf8"
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const result = await runGeneratorSubcommand({
|
|
167
|
+
appRoot,
|
|
168
|
+
subcommand: "placed-element",
|
|
169
|
+
options: {
|
|
170
|
+
name: "Ops Panel",
|
|
171
|
+
surface: "admin",
|
|
172
|
+
force: "true"
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
assert.deepEqual(result.touchedFiles, [
|
|
177
|
+
"packages/main/src/client/providers/MainClientProvider.js",
|
|
178
|
+
"src/components/OpsPanelElement.vue",
|
|
179
|
+
"src/placement.js"
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
const componentSource = await readFile(path.join(appRoot, "src", "components", "OpsPanelElement.vue"), "utf8");
|
|
183
|
+
assert.match(componentSource, /<h2 class="text-h6 mb-2">Ops Panel<\/h2>/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("ui-generator placed-element subcommand requires appRoot", async () => {
|
|
115
188
|
await assert.rejects(
|
|
116
189
|
() =>
|
|
117
190
|
runGeneratorSubcommand({
|
|
118
191
|
appRoot: "",
|
|
119
|
-
subcommand: "element",
|
|
192
|
+
subcommand: "placed-element",
|
|
120
193
|
options: {
|
|
121
194
|
name: "Ops Panel",
|
|
122
195
|
surface: "admin"
|