@abgov/nx-adsp 13.28.0 → 13.29.0
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.json +1 -1
- package/src/generators/vue-admin-crud/vue-admin-crud.js +6 -0
- package/src/generators/vue-admin-crud/vue-admin-crud.js.map +1 -1
- package/src/generators/vue-app/files/AGENTS.md__tmpl__ +20 -23
- package/src/generators/vue-app/files/src/App.vue__tmpl__ +9 -0
- package/src/generators/vue-app/vue-app.spec.ts +12 -0
- package/src/generators/vue-components/files/src/lib/patterns/AppSideMenu.vue__tmpl__ +17 -1
- package/src/generators/vue-components/vue-components.spec.ts +10 -0
- package/src/generators/vue-workspace-view/vue-workspace-view.js +6 -0
- package/src/generators/vue-workspace-view/vue-workspace-view.js.map +1 -1
- package/src/generators/vue-workspace-view/vue-workspace-view.spec.ts +19 -0
- package/src/utils/vue-side-menu.d.ts +19 -0
- package/src/utils/vue-side-menu.js +41 -0
- package/src/utils/vue-side-menu.js.map +1 -0
- package/src/utils/vue-side-menu.spec.ts +71 -0
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const devkit_1 = require("@nx/devkit");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const vue_router_1 = require("../../utils/vue-router");
|
|
8
|
+
const vue_side_menu_1 = require("../../utils/vue-side-menu");
|
|
8
9
|
const vue_components_1 = require("../vue-components/vue-components");
|
|
9
10
|
// Nx's own CLI option coercion (coerceTypesInOptions in nx/src/utils/params)
|
|
10
11
|
// only knows how to split an array-typed option on commas -- it has no JSON
|
|
@@ -51,6 +52,11 @@ function default_1(host, options) {
|
|
|
51
52
|
requiresAuth: normalizedOptions.requiresAuth,
|
|
52
53
|
layout: 'wide',
|
|
53
54
|
});
|
|
55
|
+
// The list is the nav destination; the edit form is reached from it.
|
|
56
|
+
(0, vue_side_menu_1.insertSideMenuItem)(host, normalizedOptions.projectRoot, {
|
|
57
|
+
label: normalizedOptions.heading,
|
|
58
|
+
to: normalizedOptions.route,
|
|
59
|
+
});
|
|
54
60
|
yield (0, devkit_1.formatFiles)(host);
|
|
55
61
|
});
|
|
56
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-admin-crud.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-admin-crud/vue-admin-crud.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"vue-admin-crud.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-admin-crud/vue-admin-crud.ts"],"names":[],"mappings":"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"}
|
|
@@ -234,34 +234,31 @@ mark what's meant to be edited.
|
|
|
234
234
|
{ path: '/queue', component: () => import('../views/QueueView.vue'), meta: { layout: 'wide' } }
|
|
235
235
|
{ path: '/apply', component: () => import('../views/ApplyView.vue'), meta: { layout: 'form' } }
|
|
236
236
|
```
|
|
237
|
-
3. **For the `internal` layout:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
3. **For the `internal` layout: check the side-menu nav.** `src/App.vue` has a plain
|
|
238
|
+
`primaryItems` array — the app's own navigation list, seeded with Home. Each `vue-*-view`
|
|
239
|
+
generator that adds a staff screen appends its entry automatically, so a generated route is
|
|
240
|
+
navigable without you doing anything.
|
|
241
|
+
|
|
242
|
+
It is a literal array, not derived from the router, on purpose: nav order, grouping and
|
|
243
|
+
labels are decisions this app owns. Reorder, relabel, group into `secondaryItems`, or prune
|
|
244
|
+
freely — the generators only ever append, and never touch an entry that already exists for
|
|
245
|
+
that route.
|
|
242
246
|
|
|
243
247
|
```typescript
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
{ label: 'My Feature', to: '/my-feature', icon: 'list', current: route.path.startsWith('/my-feature') },
|
|
249
|
-
])
|
|
248
|
+
const primaryItems = [
|
|
249
|
+
{ label: 'Home', to: '/', icon: 'home' },
|
|
250
|
+
{ label: 'Review queue', to: '/queue' }, // added by vue-workspace-view
|
|
251
|
+
];
|
|
250
252
|
```
|
|
251
253
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
:primary-items="primaryItems"
|
|
258
|
-
:account-items="accountItems"
|
|
259
|
-
@item-click="onItemClick"
|
|
260
|
-
>
|
|
261
|
-
```
|
|
254
|
+
Two things worth adding by hand: **`icon`** is effectively required —
|
|
255
|
+
`goa-work-side-menu-item` renders a blank item without one, and the generators can't guess it
|
|
256
|
+
(icon names: [design.alberta.ca/components/icons](https://design.alberta.ca/components/icons)).
|
|
257
|
+
And **`current`** for active highlighting, e.g. `current: route.path.startsWith('/queue')`,
|
|
258
|
+
which needs the `route` already in scope.
|
|
262
259
|
|
|
263
|
-
The `header` layout has no side menu
|
|
264
|
-
|
|
260
|
+
The `header` layout has no side menu, so it has no `primaryItems` array and the generators
|
|
261
|
+
skip the step silently — use route-level breadcrumbs or a nav inside the view instead.
|
|
265
262
|
|
|
266
263
|
## Backend API calls (proxy setup)
|
|
267
264
|
|
|
@@ -32,6 +32,14 @@ function signInAgain() {
|
|
|
32
32
|
}
|
|
33
33
|
<% if (layout === 'internal') { %>
|
|
34
34
|
|
|
35
|
+
// The app's main navigation. Every `vue-*-view` generator that adds a staff
|
|
36
|
+
// screen appends an entry here, and it is yours to reorder, relabel, group or
|
|
37
|
+
// prune -- a generated app is a starting point, not a fixed shell. Items are
|
|
38
|
+
// `{ label, to }`; add `icon` for a leading icon or `badge` for a count.
|
|
39
|
+
const primaryItems = [
|
|
40
|
+
{ label: 'Home', to: '/' },
|
|
41
|
+
];
|
|
42
|
+
|
|
35
43
|
// AppSideMenu's items are presentational — it doesn't know about Keycloak or
|
|
36
44
|
// routing; it just renders whatever's passed and emits itemClick on click.
|
|
37
45
|
const accountItems = computed(() => [
|
|
@@ -56,6 +64,7 @@ function onItemClick(item: { to?: string }) {
|
|
|
56
64
|
<% if (layout === 'internal') { %>
|
|
57
65
|
<AppSideMenu
|
|
58
66
|
heading="<%= projectName %>"
|
|
67
|
+
:primary-items="primaryItems"
|
|
59
68
|
:account-items="accountItems"
|
|
60
69
|
@item-click="onItemClick"
|
|
61
70
|
>
|
|
@@ -193,6 +193,12 @@ describe('Vue App Generator', () => {
|
|
|
193
193
|
expect(app).toContain('<AppFooter />');
|
|
194
194
|
}, 30000);
|
|
195
195
|
|
|
196
|
+
it('the header layout has no side-menu nav array (there is no side menu)', async () => {
|
|
197
|
+
await generator(host, options);
|
|
198
|
+
const app = host.read('apps/test/src/App.vue').toString();
|
|
199
|
+
expect(app).not.toContain('primaryItems');
|
|
200
|
+
});
|
|
201
|
+
|
|
196
202
|
it('--layout=internal generates an AppSideMenu shell instead of AppHeader/AppFooter', async () => {
|
|
197
203
|
await generator(host, { ...options, layout: 'internal' });
|
|
198
204
|
|
|
@@ -206,6 +212,12 @@ describe('Vue App Generator', () => {
|
|
|
206
212
|
expect(app).toContain('<AppSideMenu');
|
|
207
213
|
expect(app).toContain('heading="test"');
|
|
208
214
|
expect(app).toContain(':account-items="accountItems"');
|
|
215
|
+
// A plain, editable nav array -- the app's own list, seeded with Home and
|
|
216
|
+
// appended to by each staff view generator. Not derived from the router:
|
|
217
|
+
// nav order, grouping and labels are the owning team's decisions.
|
|
218
|
+
expect(app).toContain(':primary-items="primaryItems"');
|
|
219
|
+
expect(app).toContain('const primaryItems = [');
|
|
220
|
+
expect(app).toContain("{ label: 'Home', to: '/' },");
|
|
209
221
|
expect(app).toContain('@item-click="onItemClick"');
|
|
210
222
|
// The content gutter is shared regardless of shell choice.
|
|
211
223
|
expect(app).toContain('<AppLayout');
|
|
@@ -86,10 +86,26 @@ const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
|
|
|
86
86
|
|
|
87
87
|
<style scoped>
|
|
88
88
|
.app-side-menu {
|
|
89
|
-
|
|
89
|
+
/* Bounded height, so .app-side-menu-content's overflow-y below actually
|
|
90
|
+
engages. With min-height the flex container grows past the viewport, the
|
|
91
|
+
content pane never overflows within it, and the DOCUMENT scrolls instead --
|
|
92
|
+
carrying the side menu off the top, which is the one thing a persistent
|
|
93
|
+
nav must not do. The overflow-y rule below was always intended to be the
|
|
94
|
+
scroll container; it was simply inert.
|
|
95
|
+
dvh tracks mobile browser chrome; vh is the fallback for older engines. */
|
|
96
|
+
height: 100vh;
|
|
97
|
+
height: 100dvh;
|
|
98
|
+
overflow: hidden;
|
|
90
99
|
display: flex;
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
.app-side-menu > goa-work-side-menu {
|
|
103
|
+
/* A nav longer than the viewport scrolls within its own column rather than
|
|
104
|
+
being clipped by the container's overflow: hidden. */
|
|
105
|
+
overflow-y: auto;
|
|
106
|
+
flex: 0 0 auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
.app-side-menu-content {
|
|
94
110
|
flex: 1;
|
|
95
111
|
min-width: 0;
|
|
@@ -93,6 +93,16 @@ describe('Vue Components Generator', () => {
|
|
|
93
93
|
const formattersSrc = host
|
|
94
94
|
.read('libs/vue-components/src/lib/formatters.ts')
|
|
95
95
|
.toString();
|
|
96
|
+
// The staff shell must scroll its content, not the document -- otherwise
|
|
97
|
+
// the persistent side menu scrolls off the top. Verified in a real browser;
|
|
98
|
+
// this pins the rule so it can't regress to min-height.
|
|
99
|
+
const sideMenu = host
|
|
100
|
+
.read('libs/vue-components/src/lib/patterns/AppSideMenu.vue')
|
|
101
|
+
.toString();
|
|
102
|
+
expect(sideMenu).toContain('height: 100dvh');
|
|
103
|
+
expect(sideMenu).toContain('overflow: hidden');
|
|
104
|
+
expect(sideMenu).not.toContain('min-height: 100vh');
|
|
105
|
+
|
|
96
106
|
// goa-form-stepper's `step` prop drives the progress bar; without it every
|
|
97
107
|
// page of a wizard renders step one as current.
|
|
98
108
|
const stepper = host
|
|
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const devkit_1 = require("@nx/devkit");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const vue_router_1 = require("../../utils/vue-router");
|
|
8
|
+
const vue_side_menu_1 = require("../../utils/vue-side-menu");
|
|
8
9
|
const vue_components_1 = require("../vue-components/vue-components");
|
|
9
10
|
// Nx's own CLI option coercion (coerceTypesInOptions in nx/src/utils/params)
|
|
10
11
|
// only knows how to split an array-typed option on commas -- it has no JSON
|
|
@@ -62,6 +63,11 @@ function default_1(host, options) {
|
|
|
62
63
|
// A multi-column table at the 1000px default wraps every cell.
|
|
63
64
|
layout: 'wide',
|
|
64
65
|
});
|
|
66
|
+
// No-op on a header-layout app, which has no side menu.
|
|
67
|
+
(0, vue_side_menu_1.insertSideMenuItem)(host, normalizedOptions.projectRoot, {
|
|
68
|
+
label: normalizedOptions.heading,
|
|
69
|
+
to: normalizedOptions.route,
|
|
70
|
+
});
|
|
65
71
|
yield (0, devkit_1.formatFiles)(host);
|
|
66
72
|
});
|
|
67
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-workspace-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-workspace-view/vue-workspace-view.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"vue-workspace-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-workspace-view/vue-workspace-view.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,13 @@ import { Schema } from './schema';
|
|
|
9
9
|
|
|
10
10
|
// Mirrors the shape vue-app's own template generates -- vue-workspace-view
|
|
11
11
|
// retrofits into this file, so the fixture must match what it actually looks for.
|
|
12
|
+
const APP_VUE_INTERNAL_FIXTURE = `<script setup lang="ts">
|
|
13
|
+
const primaryItems = [
|
|
14
|
+
{ label: 'Home', to: '/' },
|
|
15
|
+
];
|
|
16
|
+
</script>
|
|
17
|
+
`;
|
|
18
|
+
|
|
12
19
|
const ROUTER_FIXTURE = `import { createRouter, createWebHistory } from 'vue-router';
|
|
13
20
|
import HomeView from '../views/HomeView.vue';
|
|
14
21
|
|
|
@@ -41,6 +48,7 @@ describe('Vue Workspace View Generator', () => {
|
|
|
41
48
|
host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
42
49
|
addProjectConfiguration(host, 'test', { root: 'apps/test' });
|
|
43
50
|
host.write('apps/test/src/router/index.ts', ROUTER_FIXTURE);
|
|
51
|
+
host.write('apps/test/src/App.vue', APP_VUE_INTERNAL_FIXTURE);
|
|
44
52
|
});
|
|
45
53
|
|
|
46
54
|
it('throws when --project does not exist', async () => {
|
|
@@ -263,6 +271,13 @@ describe('Vue Workspace View Generator', () => {
|
|
|
263
271
|
expect(view).toContain(':per-page-count="50"');
|
|
264
272
|
}, 30000);
|
|
265
273
|
|
|
274
|
+
it('adds nothing to the side menu when the app has no App.vue', async () => {
|
|
275
|
+
host.delete('apps/test/src/App.vue');
|
|
276
|
+
// A header-layout app has no side menu; adding a staff view to one must not
|
|
277
|
+
// fail, it simply has no nav to join.
|
|
278
|
+
await expect(generator(host, baseOptions)).resolves.not.toThrow();
|
|
279
|
+
});
|
|
280
|
+
|
|
266
281
|
it('inserts the route into router/index.ts, requiring auth by default', async () => {
|
|
267
282
|
await generator(host, baseOptions);
|
|
268
283
|
|
|
@@ -274,6 +289,10 @@ describe('Vue Workspace View Generator', () => {
|
|
|
274
289
|
expect(routerTs).toContain('requiresAuth: true');
|
|
275
290
|
// A data table asks for the wide variant -- the generator knows it emitted one.
|
|
276
291
|
expect(routerTs).toContain("layout: 'wide'");
|
|
292
|
+
// A staff queue is a nav destination, so the generator adds it to the
|
|
293
|
+
// app's side-menu list rather than leaving the route reachable only by URL.
|
|
294
|
+
const appVue = host.read('apps/test/src/App.vue').toString();
|
|
295
|
+
expect(appVue).toContain("to: '/applications'");
|
|
277
296
|
expect(routerTs).toContain("{ path: '/', component: HomeView }");
|
|
278
297
|
}, 30000);
|
|
279
298
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit';
|
|
2
|
+
export interface SideMenuItemSpec {
|
|
3
|
+
/** Nav label shown in the side menu, e.g. 'Review queue'. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Route path the item navigates to, e.g. '/queue'. */
|
|
6
|
+
to: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Adds a nav item to a generated app's side menu.
|
|
10
|
+
*
|
|
11
|
+
* A no-op when the app has no `primaryItems` array. That is the signal the app
|
|
12
|
+
* was generated with `--layout=header` (a public app has no side menu at all),
|
|
13
|
+
* so adding a staff view to it should not fail or warn -- there is simply no nav
|
|
14
|
+
* to add to.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent: re-running a generator, or two generators claiming the same path,
|
|
17
|
+
* leaves one entry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function insertSideMenuItem(host: Tree, projectRoot: string, item: SideMenuItemSpec): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.insertSideMenuItem = insertSideMenuItem;
|
|
4
|
+
// Deterministic text insertion into vue-app's `primaryItems` array, mirroring
|
|
5
|
+
// insertVueRoute's approach for the same reason: the template's shape is fixed,
|
|
6
|
+
// so a targeted anchor beats pulling in an AST library for one array entry.
|
|
7
|
+
//
|
|
8
|
+
// The array is a plain literal in App.vue rather than something derived from the
|
|
9
|
+
// router, deliberately: a generated app is an opinionated starting point, and
|
|
10
|
+
// nav order, grouping and labels are decisions the owning team should be able to
|
|
11
|
+
// read and change in one obvious place -- not behaviour resolved at runtime by
|
|
12
|
+
// the shell that every app then has to work around.
|
|
13
|
+
const ANCHOR = 'const primaryItems = [';
|
|
14
|
+
/**
|
|
15
|
+
* Adds a nav item to a generated app's side menu.
|
|
16
|
+
*
|
|
17
|
+
* A no-op when the app has no `primaryItems` array. That is the signal the app
|
|
18
|
+
* was generated with `--layout=header` (a public app has no side menu at all),
|
|
19
|
+
* so adding a staff view to it should not fail or warn -- there is simply no nav
|
|
20
|
+
* to add to.
|
|
21
|
+
*
|
|
22
|
+
* Idempotent: re-running a generator, or two generators claiming the same path,
|
|
23
|
+
* leaves one entry.
|
|
24
|
+
*/
|
|
25
|
+
function insertSideMenuItem(host, projectRoot, item) {
|
|
26
|
+
var _a;
|
|
27
|
+
const appPath = `${projectRoot}/src/App.vue`;
|
|
28
|
+
const content = (_a = host.read(appPath)) === null || _a === void 0 ? void 0 : _a.toString();
|
|
29
|
+
if (content === undefined)
|
|
30
|
+
return;
|
|
31
|
+
const anchorIndex = content.indexOf(ANCHOR);
|
|
32
|
+
if (anchorIndex === -1)
|
|
33
|
+
return;
|
|
34
|
+
if (content.includes(`to: '${item.to}'`))
|
|
35
|
+
return;
|
|
36
|
+
const insertAt = anchorIndex + ANCHOR.length;
|
|
37
|
+
host.write(appPath, content.slice(0, insertAt) +
|
|
38
|
+
`\n { label: '${item.label}', to: '${item.to}' },` +
|
|
39
|
+
content.slice(insertAt));
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=vue-side-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vue-side-menu.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/vue-side-menu.ts"],"names":[],"mappings":";;AA+BA,gDAqBC;AA3CD,8EAA8E;AAC9E,gFAAgF;AAChF,4EAA4E;AAC5E,EAAE;AACF,iFAAiF;AACjF,8EAA8E;AAC9E,iFAAiF;AACjF,+EAA+E;AAC/E,oDAAoD;AACpD,MAAM,MAAM,GAAG,wBAAwB,CAAC;AAExC;;;;;;;;;;GAUG;AACH,SAAgB,kBAAkB,CAChC,IAAU,EACV,WAAmB,EACnB,IAAsB;;IAEtB,MAAM,OAAO,GAAG,GAAG,WAAW,cAAc,CAAC;IAC7C,MAAM,OAAO,GAAG,MAAA,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,0CAAE,QAAQ,EAAE,CAAC;IAC/C,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO;IAElC,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,WAAW,KAAK,CAAC,CAAC;QAAE,OAAO;IAE/B,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,EAAE,GAAG,CAAC;QAAE,OAAO;IAEjD,MAAM,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,IAAI,CAAC,KAAK,CACR,OAAO,EACP,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC;QACxB,iBAAiB,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,EAAE,MAAM;QACnD,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAC1B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit';
|
|
2
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
3
|
+
import { insertSideMenuItem } from './vue-side-menu';
|
|
4
|
+
|
|
5
|
+
const INTERNAL_APP = `<script setup lang="ts">
|
|
6
|
+
const primaryItems = [
|
|
7
|
+
{ label: 'Home', to: '/' },
|
|
8
|
+
];
|
|
9
|
+
</script>
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
// A header-layout app has no side menu at all, so no array to insert into.
|
|
13
|
+
const HEADER_APP = `<script setup lang="ts">
|
|
14
|
+
const accountItems = computed(() => []);
|
|
15
|
+
</script>
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
describe('insertSideMenuItem', () => {
|
|
19
|
+
let host: Tree;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('inserts an item into the app nav array', () => {
|
|
26
|
+
host.write('apps/staff/src/App.vue', INTERNAL_APP);
|
|
27
|
+
insertSideMenuItem(host, 'apps/staff', {
|
|
28
|
+
label: 'Review queue',
|
|
29
|
+
to: '/queue',
|
|
30
|
+
});
|
|
31
|
+
const app = host.read('apps/staff/src/App.vue').toString();
|
|
32
|
+
expect(app).toContain("{ label: 'Review queue', to: '/queue' },");
|
|
33
|
+
// The seeded Home entry survives.
|
|
34
|
+
expect(app).toContain("{ label: 'Home', to: '/' },");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('is idempotent on the same route', () => {
|
|
38
|
+
host.write('apps/staff/src/App.vue', INTERNAL_APP);
|
|
39
|
+
const item = { label: 'Review queue', to: '/queue' };
|
|
40
|
+
insertSideMenuItem(host, 'apps/staff', item);
|
|
41
|
+
insertSideMenuItem(host, 'apps/staff', item);
|
|
42
|
+
const app = host.read('apps/staff/src/App.vue').toString();
|
|
43
|
+
expect(app.split("to: '/queue'").length - 1).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not re-add a route already present under a different label', () => {
|
|
47
|
+
host.write('apps/staff/src/App.vue', INTERNAL_APP);
|
|
48
|
+
insertSideMenuItem(host, 'apps/staff', { label: 'Queue', to: '/queue' });
|
|
49
|
+
insertSideMenuItem(host, 'apps/staff', { label: 'Renamed', to: '/queue' });
|
|
50
|
+
const app = host.read('apps/staff/src/App.vue').toString();
|
|
51
|
+
expect(app).toContain("label: 'Queue'");
|
|
52
|
+
expect(app).not.toContain("label: 'Renamed'");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('is a silent no-op on a header-layout app with no side menu', () => {
|
|
56
|
+
host.write('apps/public/src/App.vue', HEADER_APP);
|
|
57
|
+
expect(() =>
|
|
58
|
+
insertSideMenuItem(host, 'apps/public', {
|
|
59
|
+
label: 'Review queue',
|
|
60
|
+
to: '/queue',
|
|
61
|
+
}),
|
|
62
|
+
).not.toThrow();
|
|
63
|
+
expect(host.read('apps/public/src/App.vue').toString()).toBe(HEADER_APP);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('is a silent no-op when there is no App.vue at all', () => {
|
|
67
|
+
expect(() =>
|
|
68
|
+
insertSideMenuItem(host, 'apps/missing', { label: 'X', to: '/x' }),
|
|
69
|
+
).not.toThrow();
|
|
70
|
+
});
|
|
71
|
+
});
|