@abgov/nx-adsp 13.18.0-beta.2 → 13.18.0-beta.4

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.
Files changed (27) hide show
  1. package/generators.json +5 -0
  2. package/package.json +1 -1
  3. package/src/generators/vue-app/files/AGENTS.md__tmpl__ +29 -4
  4. package/src/generators/vue-app/files/src/App.spec.ts__tmpl__ +26 -1
  5. package/src/generators/vue-app/files/src/App.vue__tmpl__ +38 -2
  6. package/src/generators/vue-app/schema.d.ts +2 -0
  7. package/src/generators/vue-app/schema.json +6 -0
  8. package/src/generators/vue-app/vue-app.js +2 -1
  9. package/src/generators/vue-app/vue-app.js.map +1 -1
  10. package/src/generators/vue-app/vue-app.spec.ts +30 -0
  11. package/src/generators/vue-components/files/AGENTS.md__tmpl__ +2 -2
  12. package/src/generators/vue-components/files/src/index.ts__tmpl__ +2 -0
  13. package/src/generators/vue-components/files/src/lib/patterns/AppSideMenu.vue__tmpl__ +96 -0
  14. package/src/generators/vue-components/files/src/lib/patterns/RecordDetailShell.vue__tmpl__ +75 -0
  15. package/src/generators/vue-components/files/src/vue-components.spec.ts__tmpl__ +2 -0
  16. package/src/generators/vue-components/vue-components.spec.ts +2 -0
  17. package/src/generators/vue-detail-view/files/src/views/__viewFileName__.vue__tmpl__ +91 -0
  18. package/src/generators/vue-detail-view/schema.d.ts +29 -0
  19. package/src/generators/vue-detail-view/schema.json +50 -0
  20. package/src/generators/vue-detail-view/vue-detail-view.d.ts +3 -0
  21. package/src/generators/vue-detail-view/vue-detail-view.js +50 -0
  22. package/src/generators/vue-detail-view/vue-detail-view.js.map +1 -0
  23. package/src/generators/vue-detail-view/vue-detail-view.spec.ts +156 -0
  24. package/src/utils/vue-router.d.ts +8 -0
  25. package/src/utils/vue-router.js +32 -0
  26. package/src/utils/vue-router.js.map +1 -0
  27. package/src/utils/vue-router.spec.ts +71 -0
package/generators.json CHANGED
@@ -60,6 +60,11 @@
60
60
  "description": "Ensures the shared GoA Vue wrapper library (interim). Invoked by vue-app; runnable directly to repair.",
61
61
  "hidden": true
62
62
  },
63
+ "vue-detail-view": {
64
+ "factory": "./src/generators/vue-detail-view/vue-detail-view",
65
+ "schema": "./src/generators/vue-detail-view/schema.json",
66
+ "description": "Generator that adds a record-detail view (RecordDetailShell + a --fields spec) to an existing vue-app project."
67
+ },
63
68
  "mean": {
64
69
  "factory": "./src/generators/mean/mean",
65
70
  "schema": "./src/generators/mean/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/nx-adsp",
3
- "version": "13.18.0-beta.2",
3
+ "version": "13.18.0-beta.4",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for ADSP apps.",
@@ -31,7 +31,11 @@ executors read options from `project.json` and do not prompt.
31
31
  | File | Purpose |
32
32
  |------|---------|
33
33
  | `src/main.ts` | Entry — registers Pinia, Router, and Keycloak plugin (incl. `onAuthRefreshError`, which flags the session store as expired) |
34
+ <% if (layout === 'internal') { %>
35
+ | `src/App.vue` | Shell — `AppSideMenu` (side-menu shell, Sign in/out as an account item), `SessionExpiredBanner`, `<RouterView>` wrapped in `AppLayout` |
36
+ <% } else { %>
34
37
  | `src/App.vue` | Shell — `AppHeader` with sign-in/out in its `utilities` slot, `SessionExpiredBanner`, hero banner, `<RouterView>` wrapped in `AppLayout`, `AppFooter` |
38
+ <% } %>
35
39
  | `src/stores/session.ts` | Pinia store — `expired` flag behind `SessionExpiredBanner`, set from `main.ts`'s `onAuthRefreshError` hook |
36
40
  | `src/router/index.ts` | Routes — `/protected` guarded with `requiresAuth` meta |
37
41
  | `src/views/HomeView.vue` | Public page — calls public and private APIs |
@@ -155,12 +159,27 @@ across every Vue app in this workspace — fix or extend a wrapper once in
155
159
  > stopgap until GoA DS ships an official `@abgov/vue-components`. When it lands,
156
160
  > delete `primitives/` and repoint imports at that package — the names/props are
157
161
  > intended to match, so it's a scope swap. This does **not** apply to the
158
- > `AppHeader`/`AppLayout`/`AppFooter`/`SessionExpiredBanner` shell components
159
- > below — those live in the same library's `patterns/` folder and are permanent
160
- > (app-shell composition has no equivalent in a design-system package).
162
+ > `AppHeader`/`AppLayout`/`AppFooter`/`AppSideMenu`/`SessionExpiredBanner` shell
163
+ > components below — those live in the same library's `patterns/` folder and are
164
+ > permanent (app-shell composition has no equivalent in a design-system package).
161
165
 
162
166
  ### App shell components
163
167
 
168
+ This app was generated with **`--layout=<%= layout %>`**.
169
+ <% if (layout === 'internal') { %>
170
+ `App.vue` is a staff-facing, side-menu shell built from shared components imported
171
+ from **`<%= goaImportPath %>`**, not hand-rolled markup:
172
+
173
+ | Component | Purpose |
174
+ |---|---|
175
+ | `AppSideMenu` | `goa-work-side-menu`. Takes `heading`/`primaryItems`/`secondaryItems`/`accountItems` props (each item: `{ label, to?, icon?, badge?, current? }`) and emits `itemClick` — it doesn't know about routing or auth, so `App.vue` computes `current`/handles the click itself. `App.vue` currently only populates `accountItems` (a single Sign in/out item); add real nav items to `primaryItems`/`secondaryItems` as routes are added |
176
+ | `AppLayout` | The content gutter every view renders inside (see the key files table). Also renders the skip-to-main-content link |
177
+ | `SessionExpiredBanner` | A `v-model:show` banner with `signIn`/`dismiss` emits. Bound to the `useSessionStore()` Pinia store (`src/stores/session.ts`), which `main.ts`'s `onAuthRefreshError` hook flips when the refresh token itself has expired |
178
+
179
+ There's no `AppHeader`/`AppFooter`/hero banner in this layout — `AppSideMenu` is the
180
+ whole top-level shell, matching the real GoA "internal portal" pattern (no public
181
+ marketing chrome around a staff tool).
182
+ <% } else { %>
164
183
  `App.vue` is built from four shared components imported from
165
184
  **`<%= goaImportPath %>`**, not hand-rolled markup:
166
185
 
@@ -170,10 +189,16 @@ across every Vue app in this workspace — fix or extend a wrapper once in
170
189
  | `AppLayout` | The content gutter every view renders inside (see the key files table). Also renders the skip-to-main-content link |
171
190
  | `AppFooter` | `goa-app-footer` with the standard GoA nav/meta links (Services/Contact/Terms of Use, Privacy/Disclaimer/Accessibility) |
172
191
  | `SessionExpiredBanner` | A `v-model:show` banner with `signIn`/`dismiss` emits. Bound to the `useSessionStore()` Pinia store (`src/stores/session.ts`), which `main.ts`'s `onAuthRefreshError` hook flips when the refresh token itself has expired |
173
-
192
+ <% } %>
174
193
  These are shared across every Vue app in this workspace — fix or extend one once
175
194
  in `libs/vue-components/src/lib/patterns/`, the same way as the `Goab*` wrappers.
176
195
 
196
+ A second, **`--layout=<%= layout === 'internal' ? 'header' : 'internal' %>`** frontend
197
+ can be scaffolded against the same backend with `nx g @abgov/nx-adsp:vue-app <name>
198
+ --pairedProject <%= pairedProject || 'your-backend-project' %> --layout=<%= layout === 'internal' ? 'header' : 'internal' %>`
199
+ — a public-facing app and a staff-facing one sharing one API, without a separate
200
+ composite generator.
201
+
177
202
  ## Adding a view
178
203
 
179
204
  1. Create `src/views/MyFeatureView.vue` as a `<script setup>` SFC. **Don't add your
@@ -34,7 +34,32 @@ const router = createRouter({
34
34
  history: createMemoryHistory(),
35
35
  routes: [{ path: '/', component: { template: '<div />' } }],
36
36
  });
37
+ <% if (layout === 'internal') { %>
38
+ describe('App shell (internal / side-menu)', () => {
39
+ it('shows a Sign in account item when unauthenticated', () => {
40
+ const wrapper = mount(App, {
41
+ global: { plugins: [router, createPinia()], stubs: { RouterView: true } },
42
+ });
43
+ const item = wrapper.find('goa-work-side-menu-item[label="Sign in"]');
44
+ expect(item.exists()).toBe(true);
45
+ });
37
46
 
47
+ it('clicking the account item calls keycloak.login (guards against lost reactivity)', async () => {
48
+ // Same bug class as the header shell: destructuring useKeycloak() would
49
+ // freeze `keycloak` at undefined, so login() would be a permanent no-op.
50
+ const kc = useKeycloak();
51
+ const wrapper = mount(App, {
52
+ global: { plugins: [router, createPinia()], stubs: { RouterView: true } },
53
+ });
54
+ // AppSideMenu binds @click.prevent directly on goa-work-side-menu-item (not
55
+ // an underscore-prefixed event, unlike the goa-* form elements) -- trigger
56
+ // a plain 'click'.
57
+ const item = wrapper.find('goa-work-side-menu-item[label="Sign in"]');
58
+ await item.trigger('click');
59
+ expect(kc.keycloak?.login).toHaveBeenCalled();
60
+ });
61
+ });
62
+ <% } else { %>
38
63
  describe('App shell header', () => {
39
64
  it('shows Sign in when unauthenticated', () => {
40
65
  const wrapper = mount(App, {
@@ -57,7 +82,7 @@ describe('App shell header', () => {
57
82
  expect(kc.keycloak?.login).toHaveBeenCalled();
58
83
  });
59
84
  });
60
-
85
+ <% } %>
61
86
  describe('App shell session-expired banner', () => {
62
87
  it('is hidden until the session store flags an expired session', async () => {
63
88
  const pinia = createPinia();
@@ -2,7 +2,11 @@
2
2
  import { computed } from 'vue';
3
3
  import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
4
4
  import { RouterView, useRoute } from 'vue-router';
5
+ <% if (layout === 'internal') { %>
6
+ import { AppLayout, AppSideMenu, SessionExpiredBanner } from '<%= goaImportPath %>';
7
+ <% } else { %>
5
8
  import { AppHeader, AppLayout, AppFooter, SessionExpiredBanner } from '<%= goaImportPath %>';
9
+ <% } %>
6
10
  import { useSessionStore } from './stores/session';
7
11
 
8
12
  // Keep the reactive instance — do NOT destructure. useKeycloak() returns a
@@ -15,7 +19,7 @@ const session = useSessionStore();
15
19
  // Content width per view, from route meta (default 'page'). Views opt into a
16
20
  // different width with `meta: { layout: 'wide' | 'form' }` in the router.
17
21
  const route = useRoute();
18
- const layout = computed(
22
+ const contentWidth = computed(
19
23
  () => (route.meta.layout as 'page' | 'wide' | 'form' | undefined) ?? 'page'
20
24
  );
21
25
 
@@ -26,9 +30,40 @@ function signInAgain() {
26
30
  session.dismiss();
27
31
  kc.keycloak?.login();
28
32
  }
33
+ <% if (layout === 'internal') { %>
34
+
35
+ // AppSideMenu's account items are presentational -- it doesn't know about
36
+ // Keycloak, it just renders whatever's passed and emits itemClick on click.
37
+ const accountItems = computed(() => [
38
+ kc.authenticated
39
+ ? { label: kc.fullName ? `Sign out (${kc.fullName})` : 'Sign out' }
40
+ : { label: 'Sign in' },
41
+ ]);
42
+
43
+ function onAccountItemClick() {
44
+ if (kc.authenticated) logout();
45
+ else login();
46
+ }
47
+ <% } %>
29
48
  </script>
30
49
 
31
50
  <template>
51
+ <% if (layout === 'internal') { %>
52
+ <AppSideMenu
53
+ heading="<%= projectName %>"
54
+ :account-items="accountItems"
55
+ @item-click="onAccountItemClick"
56
+ >
57
+ <SessionExpiredBanner
58
+ v-model:show="session.expired"
59
+ @sign-in="signInAgain"
60
+ @dismiss="session.dismiss"
61
+ />
62
+ <AppLayout :variant="contentWidth">
63
+ <RouterView />
64
+ </AppLayout>
65
+ </AppSideMenu>
66
+ <% } else { %>
32
67
  <AppHeader heading="<%= projectName %>">
33
68
  <template #utilities>
34
69
  <goa-button-group alignment="end">
@@ -51,10 +86,11 @@ function signInAgain() {
51
86
  @dismiss="session.dismiss"
52
87
  />
53
88
  <goa-hero-banner heading="<%= projectName %>" backgroundurl="/assets/banner.jpg" />
54
- <AppLayout :variant="layout">
89
+ <AppLayout :variant="contentWidth">
55
90
  <RouterView />
56
91
  </AppLayout>
57
92
  <AppFooter />
93
+ <% } %>
58
94
  </template>
59
95
 
60
96
  <style>
@@ -13,6 +13,8 @@ export interface Schema {
13
13
  skipAgent?: boolean;
14
14
  /** Name of an existing backend service project to derive the nginx/dev-server proxy and the adsp:proxy-service: sandbox tag from. */
15
15
  pairedProject?: string;
16
+ /** Top-level app shell: 'header' (default, public-facing) or 'internal' (staff-facing side menu, no header/banner/footer). */
17
+ layout?: 'header' | 'internal';
16
18
  }
17
19
 
18
20
  export interface NormalizedSchema extends Schema {
@@ -83,6 +83,12 @@
83
83
  "type": "boolean",
84
84
  "description": "Skip the consultAgent interaction and generate base scaffolding only.",
85
85
  "default": false
86
+ },
87
+ "layout": {
88
+ "type": "string",
89
+ "description": "Top-level app shell: 'header' is a goa-app-header + hero banner + footer (public-facing). 'internal' is a goa-work-side-menu shell with no header/banner/footer (staff-facing). Both wrap views in the same AppLayout content gutter. Pair two vue-app runs against the same --pairedProject (once each) for a public+internal frontend pairing over one backend.",
90
+ "enum": ["header", "internal"],
91
+ "default": "header"
86
92
  }
87
93
  },
88
94
  "required": ["name", "env"],
@@ -14,6 +14,7 @@ const paired_project_1 = require("../../utils/paired-project");
14
14
  const vue_components_1 = require("../vue-components/vue-components");
15
15
  function normalizeOptions(host, options) {
16
16
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
17
+ var _a;
17
18
  const projectName = (0, devkit_1.names)(options.name).fileName;
18
19
  const projectRoot = `${(0, devkit_1.getWorkspaceLayout)(host).appsDir}/${projectName}`;
19
20
  const openshiftDirectory = `.openshift/${projectName}`;
@@ -36,7 +37,7 @@ function normalizeOptions(host, options) {
36
37
  projectRoot,
37
38
  openshiftDirectory,
38
39
  adsp,
39
- nginxProxies, pairedProjectTag: paired === null || paired === void 0 ? void 0 : paired.tag });
40
+ nginxProxies, pairedProjectTag: paired === null || paired === void 0 ? void 0 : paired.tag, layout: (_a = options.layout) !== null && _a !== void 0 ? _a : 'header' });
40
41
  });
41
42
  }
42
43
  function addFiles(host, options) {
@@ -1 +1 @@
1
- {"version":3,"file":"vue-app.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-app/vue-app.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vue-app.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-app/vue-app.ts"],"names":[],"mappings":"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"}
@@ -172,6 +172,7 @@ describe('Vue App Generator', () => {
172
172
  'AppLayout',
173
173
  'AppHeader',
174
174
  'AppFooter',
175
+ 'AppSideMenu',
175
176
  'SessionExpiredBanner',
176
177
  ]) {
177
178
  expect(host.exists(`${patterns}/${name}.vue`)).toBeTruthy();
@@ -185,6 +186,35 @@ describe('Vue App Generator', () => {
185
186
  expect(app).toContain('<AppFooter />');
186
187
  }, 30000);
187
188
 
189
+ it('--layout=internal generates an AppSideMenu shell instead of AppHeader/AppFooter', async () => {
190
+ await generator(host, { ...options, layout: 'internal' });
191
+
192
+ const app = host.read('apps/test/src/App.vue').toString();
193
+ expect(app).toContain(
194
+ "import { AppLayout, AppSideMenu, SessionExpiredBanner } from '@proj/vue-components';",
195
+ );
196
+ expect(app).not.toContain('AppHeader');
197
+ expect(app).not.toContain('AppFooter');
198
+ expect(app).not.toContain('goa-hero-banner');
199
+ expect(app).toContain('<AppSideMenu');
200
+ expect(app).toContain('heading="test"');
201
+ expect(app).toContain(':account-items="accountItems"');
202
+ expect(app).toContain('@item-click="onAccountItemClick"');
203
+ // The content gutter is shared regardless of shell choice.
204
+ expect(app).toContain('<AppLayout');
205
+
206
+ const agents = host.read('apps/test/AGENTS.md').toString();
207
+ expect(agents).toContain('--layout=internal');
208
+ expect(agents).toContain('AppSideMenu');
209
+ }, 30000);
210
+
211
+ it('defaults --layout to header when omitted', async () => {
212
+ await generator(host, options);
213
+ const app = host.read('apps/test/src/App.vue').toString();
214
+ expect(app).toContain('AppHeader');
215
+ expect(app).not.toContain('AppSideMenu');
216
+ }, 30000);
217
+
188
218
  it('wires SessionExpiredBanner to a session store fed by onAuthRefreshError (not onAuthLogout)', async () => {
189
219
  await generator(host, options);
190
220
 
@@ -7,7 +7,7 @@ app in this workspace imports both instead of carrying its own copy. Generated b
7
7
  | Folder | Contains | Lifespan |
8
8
  |---|---|---|
9
9
  | `src/lib/primitives/` | Thin `v-model`/idiomatic-event wrappers over individual `goa-*` elements (`GoabInput`, `GoabButton`, …) | **Interim** — see below |
10
- | `src/lib/patterns/` | Composite, app-shell components (`AppLayout`, `AppHeader`, `AppFooter`, `SessionExpiredBanner`) | **Permanent** |
10
+ | `src/lib/patterns/` | Composite, app-shell components (`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`) | **Permanent** |
11
11
 
12
12
  > **⚠️ `primitives/` is interim — do not invest in it as permanent.** It exists
13
13
  > only because GoA DS has not yet published an official Vue wrapper package. When
@@ -142,7 +142,7 @@ detail); just leave it to fall through from the caller.
142
142
 
143
143
  A pattern component is app-shell composition — layout, header/footer chrome,
144
144
  banners — not a single-element wrapper. Existing examples: `AppLayout`,
145
- `AppHeader`, `AppFooter`, `SessionExpiredBanner`.
145
+ `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`.
146
146
 
147
147
  - It's fine to compose `primitives/` wrappers inside a pattern component (e.g.
148
148
  `SessionExpiredBanner` uses `GoabButton`) — import them with a relative path
@@ -20,4 +20,6 @@ export { default as GoabModal } from './lib/primitives/GoabModal.vue';
20
20
  export { default as AppLayout } from './lib/patterns/AppLayout.vue';
21
21
  export { default as AppHeader } from './lib/patterns/AppHeader.vue';
22
22
  export { default as AppFooter } from './lib/patterns/AppFooter.vue';
23
+ export { default as AppSideMenu } from './lib/patterns/AppSideMenu.vue';
23
24
  export { default as SessionExpiredBanner } from './lib/patterns/SessionExpiredBanner.vue';
25
+ export { default as RecordDetailShell } from './lib/patterns/RecordDetailShell.vue';
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ // The alternate top-level shell for staff-facing/internal apps (goa-work-side-menu),
3
+ // used instead of AppHeader/AppFooter -- not together with them. Presentational only:
4
+ // unlike the real GovAlta-Pronghorn source this is adapted from, active-route
5
+ // highlighting and navigation are the caller's job (pass `current` on each item,
6
+ // listen for `item-click`) so this component doesn't need vue-router as a dependency.
7
+ interface AppSideMenuItem {
8
+ label: string;
9
+ to?: string;
10
+ icon?: string;
11
+ badge?: string;
12
+ current?: boolean;
13
+ }
14
+
15
+ withDefaults(
16
+ defineProps<{
17
+ heading?: string;
18
+ userName?: string;
19
+ primaryItems?: AppSideMenuItem[];
20
+ secondaryItems?: AppSideMenuItem[];
21
+ accountItems?: AppSideMenuItem[];
22
+ }>(),
23
+ {
24
+ primaryItems: () => [],
25
+ secondaryItems: () => [],
26
+ accountItems: () => [],
27
+ },
28
+ );
29
+
30
+ const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
31
+ </script>
32
+
33
+ <template>
34
+ <div class="app-side-menu">
35
+ <a class="skip-link" href="#main-content">Skip to main content</a>
36
+ <goa-work-side-menu :heading="heading" :user-name="userName">
37
+ <goa-work-side-menu-item
38
+ v-for="item in primaryItems"
39
+ :key="item.label"
40
+ slot="primary"
41
+ :icon="item.icon"
42
+ :label="item.label"
43
+ :url="item.to"
44
+ :current="item.current"
45
+ @click.prevent="emit('itemClick', item)"
46
+ />
47
+ <goa-work-side-menu-item
48
+ v-for="item in secondaryItems"
49
+ :key="item.label"
50
+ slot="secondary"
51
+ :icon="item.icon"
52
+ :label="item.label"
53
+ :badge="item.badge"
54
+ @click.prevent="emit('itemClick', item)"
55
+ />
56
+ <goa-work-side-menu-item
57
+ v-for="item in accountItems"
58
+ :key="item.label"
59
+ slot="account"
60
+ :label="item.label"
61
+ :url="item.to"
62
+ @click.prevent="emit('itemClick', item)"
63
+ />
64
+ </goa-work-side-menu>
65
+ <main id="main-content" class="app-side-menu-content">
66
+ <slot />
67
+ </main>
68
+ </div>
69
+ </template>
70
+
71
+ <style scoped>
72
+ .app-side-menu {
73
+ min-height: 100vh;
74
+ display: flex;
75
+ }
76
+
77
+ .app-side-menu-content {
78
+ flex: 1;
79
+ min-width: 0;
80
+ overflow-y: auto;
81
+ }
82
+
83
+ .skip-link {
84
+ position: absolute;
85
+ left: -9999px;
86
+ top: 0;
87
+ z-index: 100;
88
+ padding: var(--goa-space-s, 0.5rem) var(--goa-space-m, 1rem);
89
+ background: var(--goa-color-interactive-default, #0070c4);
90
+ color: #fff;
91
+ }
92
+
93
+ .skip-link:focus {
94
+ left: var(--goa-space-m, 1rem);
95
+ }
96
+ </style>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ // Chrome for a single-record detail page: heading + optional back button and
3
+ // status badge, loading/error states. NOT the field list or action bar --
4
+ // those vary too much between real detail pages (confirmed across multiple
5
+ // independent GovAlta apps) to genericize; the default slot renders whatever
6
+ // the consuming view needs once the record has loaded.
7
+ import GoabButton from '../primitives/GoabButton.vue';
8
+
9
+ withDefaults(
10
+ defineProps<{
11
+ heading: string;
12
+ loading?: boolean;
13
+ error?: string | null;
14
+ backLabel?: string;
15
+ statusBadge?: {
16
+ type: 'success' | 'emergency' | 'information' | 'important' | 'midtone';
17
+ content: string;
18
+ };
19
+ }>(),
20
+ {
21
+ loading: false,
22
+ error: null,
23
+ },
24
+ );
25
+
26
+ const emit = defineEmits<{ back: []; retry: [] }>();
27
+ </script>
28
+
29
+ <template>
30
+ <div class="record-detail-shell">
31
+ <div class="record-detail-shell-header">
32
+ <h1>{{ heading }}</h1>
33
+ <div class="record-detail-shell-header-actions">
34
+ <goa-badge v-if="statusBadge" :type="statusBadge.type" :content="statusBadge.content" />
35
+ <GoabButton v-if="backLabel" type="tertiary" leadingicon="arrow-back" @click="emit('back')">
36
+ {{ backLabel }}
37
+ </GoabButton>
38
+ </div>
39
+ </div>
40
+
41
+ <div v-if="loading" aria-label="Loading">
42
+ <goa-skeleton type="text" size="4" />
43
+ </div>
44
+
45
+ <goa-callout v-else-if="error" type="emergency" heading="Unable to load">
46
+ <p>{{ error }}</p>
47
+ <goa-spacer vspacing="s" />
48
+ <GoabButton type="tertiary" @click="emit('retry')">Retry</GoabButton>
49
+ </goa-callout>
50
+
51
+ <template v-else>
52
+ <slot />
53
+ </template>
54
+ </div>
55
+ </template>
56
+
57
+ <style scoped>
58
+ .record-detail-shell-header {
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ gap: var(--goa-space-m);
63
+ margin-bottom: var(--goa-space-m);
64
+ }
65
+
66
+ .record-detail-shell-header h1 {
67
+ margin: 0;
68
+ }
69
+
70
+ .record-detail-shell-header-actions {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: var(--goa-space-m);
74
+ }
75
+ </style>
@@ -24,7 +24,9 @@ describe('vue-components', () => {
24
24
  'AppLayout',
25
25
  'AppHeader',
26
26
  'AppFooter',
27
+ 'AppSideMenu',
27
28
  'SessionExpiredBanner',
29
+ 'RecordDetailShell',
28
30
  ]) {
29
31
  expect(lib[name as keyof typeof lib]).toBeTruthy();
30
32
  }
@@ -41,7 +41,9 @@ describe('Vue Components Generator', () => {
41
41
  'AppLayout',
42
42
  'AppHeader',
43
43
  'AppFooter',
44
+ 'AppSideMenu',
44
45
  'SessionExpiredBanner',
46
+ 'RecordDetailShell',
45
47
  ]) {
46
48
  expect(host.exists(`${patterns}/${name}.vue`)).toBeTruthy();
47
49
  }
@@ -0,0 +1,91 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+ import { RecordDetailShell } from '<%= goaImportPath %>';
5
+
6
+ // The fetched record's shape isn't known to this generator -- read fields
7
+ // defensively rather than declaring (and likely getting wrong) a fake interface.
8
+ const record = ref<Record<string, unknown> | null>(null);
9
+ const loading = ref(true);
10
+ const error = ref<string | null>(null);
11
+
12
+ const route = useRoute();
13
+ const router = useRouter();
14
+
15
+ async function load() {
16
+ loading.value = true;
17
+ error.value = null;
18
+ try {
19
+ const res = await fetch(`/api/<%= resource %>/${route.params.id}`);
20
+ if (!res.ok) throw new Error(`Failed to load (${res.status})`);
21
+ record.value = await res.json();
22
+ } catch (e) {
23
+ error.value = e instanceof Error ? e.message : 'Failed to load.';
24
+ } finally {
25
+ loading.value = false;
26
+ }
27
+ }
28
+
29
+ onMounted(load);
30
+
31
+ function goBack() {
32
+ router.back();
33
+ }
34
+
35
+ function formatDate(value: unknown): string {
36
+ if (!value) return '—';
37
+ try {
38
+ return new Date(String(value)).toLocaleString('en-CA');
39
+ } catch {
40
+ return String(value);
41
+ }
42
+ }
43
+
44
+ function formatCurrency(value: unknown): string {
45
+ if (value === undefined || value === null || value === '') return '—';
46
+ const n = Number(value);
47
+ if (Number.isNaN(n)) return String(value);
48
+ return new Intl.NumberFormat('en-CA', { style: 'currency', currency: 'CAD' }).format(n);
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <RecordDetailShell
54
+ heading="<%= heading %>"
55
+ :loading="loading"
56
+ :error="error"
57
+ back-label="Back"
58
+ @back="goBack"
59
+ @retry="load"
60
+ >
61
+ <dl v-if="record" class="detail-fields">
62
+ <% fields.forEach(function (field) { -%>
63
+ <dt><%= field.label %></dt>
64
+ <dd>
65
+ <% if (field.type === 'badge') { -%>
66
+ <goa-badge type="information" :content="String(record['<%= field.key %>'] ?? '—')" />
67
+ <% } else if (field.type === 'date') { -%>
68
+ {{ formatDate(record['<%= field.key %>']) }}
69
+ <% } else if (field.type === 'currency') { -%>
70
+ {{ formatCurrency(record['<%= field.key %>']) }}
71
+ <% } else { -%>
72
+ {{ record['<%= field.key %>'] ?? '—' }}
73
+ <% } -%>
74
+ </dd>
75
+ <% }); -%>
76
+ </dl>
77
+ </RecordDetailShell>
78
+ </template>
79
+
80
+ <style scoped>
81
+ .detail-fields {
82
+ display: grid;
83
+ grid-template-columns: auto 1fr;
84
+ gap: var(--goa-space-xs) var(--goa-space-l);
85
+ margin: 0;
86
+ }
87
+
88
+ .detail-fields dt {
89
+ font-weight: 600;
90
+ }
91
+ </style>
@@ -0,0 +1,29 @@
1
+ export interface DetailViewField {
2
+ key: string;
3
+ label: string;
4
+ type?: 'text' | 'date' | 'currency' | 'badge';
5
+ }
6
+
7
+ export interface Schema {
8
+ project: string;
9
+ name: string;
10
+ resource: string;
11
+ route: string;
12
+ /**
13
+ * JSON string on the real CLI (Nx's array-typed CLI coercion only supports
14
+ * comma-separated primitives, not JSON -- see schema.json). A real array is
15
+ * also accepted for programmatic callers (e.g. tests).
16
+ */
17
+ fields: string | DetailViewField[];
18
+ heading?: string;
19
+ requiresAuth?: boolean;
20
+ }
21
+
22
+ export interface NormalizedSchema extends Omit<Schema, 'fields'> {
23
+ projectRoot: string;
24
+ /** PascalCase view name with a "View" suffix, e.g. ApplicationDetailView. */
25
+ viewFileName: string;
26
+ fields: DetailViewField[];
27
+ heading: string;
28
+ requiresAuth: boolean;
29
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "id": "NxAdspVueDetailView",
4
+ "title": "Vue Record Detail View",
5
+ "description": "Generates a record-detail view (loading/error/loaded states, optional status badge, back button) into an existing vue-app project, built on the shared RecordDetailShell pattern component.",
6
+ "type": "object",
7
+ "properties": {
8
+ "project": {
9
+ "type": "string",
10
+ "description": "The vue-app project to add the view to.",
11
+ "$default": {
12
+ "$source": "argv",
13
+ "index": 0
14
+ },
15
+ "x-prompt": "Which project should the detail view be added to?"
16
+ },
17
+ "name": {
18
+ "type": "string",
19
+ "description": "View name, e.g. 'application-detail' generates src/views/ApplicationDetailView.vue.",
20
+ "$default": {
21
+ "$source": "argv",
22
+ "index": 1
23
+ },
24
+ "x-prompt": "What should the view be called?"
25
+ },
26
+ "resource": {
27
+ "type": "string",
28
+ "description": "API resource path segment -- the view fetches /api/<resource>/:id."
29
+ },
30
+ "route": {
31
+ "type": "string",
32
+ "description": "Route path added to router/index.ts, e.g. /applications/:id. Must contain a :id param."
33
+ },
34
+ "fields": {
35
+ "type": "string",
36
+ "description": "JSON array of fields rendered in the record's info list, in display order -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"badge\"}]'. Each item: { key, label, type?: \"text\"|\"date\"|\"currency\"|\"badge\" (default \"text\") }. A plain array is also accepted when this generator is invoked programmatically. Nx's CLI option coercion only supports comma-separated primitive lists for array-typed schema properties, not JSON -- a JSON string is the only CLI syntax that actually survives Nx's own arg parsing for a structured list like this."
37
+ },
38
+ "heading": {
39
+ "type": "string",
40
+ "description": "Page heading. Defaults to the view name, title-cased."
41
+ },
42
+ "requiresAuth": {
43
+ "type": "boolean",
44
+ "description": "Whether the generated route requires authentication.",
45
+ "default": true
46
+ }
47
+ },
48
+ "required": ["project", "name", "resource", "route", "fields"],
49
+ "additionalProperties": false
50
+ }
@@ -0,0 +1,3 @@
1
+ import { Tree } from '@nx/devkit';
2
+ import { Schema } from './schema';
3
+ export default function (host: Tree, options: Schema): Promise<void>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const tslib_1 = require("tslib");
5
+ const devkit_1 = require("@nx/devkit");
6
+ const path = require("path");
7
+ const vue_router_1 = require("../../utils/vue-router");
8
+ const vue_components_1 = require("../vue-components/vue-components");
9
+ // Nx's own CLI option coercion (coerceTypesInOptions in nx/src/utils/params)
10
+ // only knows how to split an array-typed option on commas -- it has no JSON
11
+ // support, so a real `"type": "array"` schema for --fields silently mangles
12
+ // a JSON array into garbage fragments when invoked from the actual CLI (only
13
+ // programmatic callers, like this generator's own unit tests, ever pass a
14
+ // real array). --fields is `"type": "string"` in schema.json specifically so
15
+ // Nx leaves it alone, and this generator parses the JSON itself.
16
+ function parseFields(fields) {
17
+ const parsed = typeof fields === 'string' ? JSON.parse(fields) : fields;
18
+ if (!Array.isArray(parsed) || parsed.length === 0) {
19
+ throw new Error('--fields must be a non-empty JSON array of { key, label, type? } objects.');
20
+ }
21
+ return parsed;
22
+ }
23
+ function normalizeOptions(host, options) {
24
+ var _a, _b;
25
+ const { root: projectRoot } = (0, devkit_1.readProjectConfiguration)(host, options.project);
26
+ if (!options.route.includes(':id')) {
27
+ throw new Error(`--route "${options.route}" has no :id param -- the generated view reads route.params.id to fetch the record.`);
28
+ }
29
+ const className = (0, devkit_1.names)(options.name).className;
30
+ return Object.assign(Object.assign({}, options), { projectRoot, fields: parseFields(options.fields), viewFileName: `${className}View`,
31
+ // className is PascalCase (e.g. "ApplicationDetail") -- space it out for a
32
+ // readable default heading ("Application Detail").
33
+ heading: (_a = options.heading) !== null && _a !== void 0 ? _a : className.replace(/([A-Z])/g, ' $1').trim(), requiresAuth: (_b = options.requiresAuth) !== null && _b !== void 0 ? _b : true });
34
+ }
35
+ function default_1(host, options) {
36
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
37
+ const normalizedOptions = normalizeOptions(host, options);
38
+ // Idempotent -- ensures RecordDetailShell exists even in a project scaffolded
39
+ // before vue-components carried it.
40
+ yield (0, vue_components_1.default)(host);
41
+ (0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host), tmpl: '' }));
42
+ (0, vue_router_1.insertVueRoute)(host, normalizedOptions.projectRoot, normalizedOptions.project, {
43
+ path: normalizedOptions.route,
44
+ componentImportPath: `../views/${normalizedOptions.viewFileName}.vue`,
45
+ requiresAuth: normalizedOptions.requiresAuth,
46
+ });
47
+ yield (0, devkit_1.formatFiles)(host);
48
+ });
49
+ }
50
+ //# sourceMappingURL=vue-detail-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue-detail-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-detail-view/vue-detail-view.ts"],"names":[],"mappings":";;AAqDA,4BAyBC;;AA9ED,uCAMoB;AACpB,6BAA6B;AAC7B,uDAAwD;AACxD,qEAE0C;AAG1C,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,0EAA0E;AAC1E,6EAA6E;AAC7E,iEAAiE;AACjE,SAAS,WAAW,CAAC,MAAwB;IAC3C,MAAM,MAAM,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAe;;IACnD,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9E,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,YAAY,OAAO,CAAC,KAAK,qFAAqF,CAC/G,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;IAChD,uCACK,OAAO,KACV,WAAW,EACX,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EACnC,YAAY,EAAE,GAAG,SAAS,MAAM;QAChC,2EAA2E;QAC3E,mDAAmD;QACnD,OAAO,EAAE,MAAA,OAAO,CAAC,OAAO,mCAAI,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,EACvE,YAAY,EAAE,MAAA,OAAO,CAAC,YAAY,mCAAI,IAAI,IAC1C;AACJ,CAAC;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAE1D,8EAA8E;QAC9E,oCAAoC;QACpC,MAAM,IAAA,wBAAsB,EAAC,IAAI,CAAC,CAAC;QAEnC,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAC7B,iBAAiB,CAAC,WAAW,kCAExB,iBAAiB,KACpB,aAAa,EAAE,IAAA,wCAAuB,EAAC,IAAI,CAAC,EAC5C,IAAI,EAAE,EAAE,IAEX,CAAC;QAEF,IAAA,2BAAc,EAAC,IAAI,EAAE,iBAAiB,CAAC,WAAW,EAAE,iBAAiB,CAAC,OAAO,EAAE;YAC7E,IAAI,EAAE,iBAAiB,CAAC,KAAK;YAC7B,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,YAAY,MAAM;YACrE,YAAY,EAAE,iBAAiB,CAAC,YAAY;SAC7C,CAAC,CAAC;QAEH,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA"}
@@ -0,0 +1,156 @@
1
+ import {
2
+ addProjectConfiguration,
3
+ readProjectConfiguration,
4
+ Tree,
5
+ } from '@nx/devkit';
6
+ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
7
+ import generator from './vue-detail-view';
8
+ import { Schema } from './schema';
9
+
10
+ // Mirrors the shape vue-app's own template generates -- vue-detail-view retrofits
11
+ // into this file, so the fixture must match what it actually looks for.
12
+ const ROUTER_FIXTURE = `import { createRouter, createWebHistory } from 'vue-router';
13
+ import HomeView from '../views/HomeView.vue';
14
+
15
+ const router = createRouter({
16
+ history: createWebHistory(import.meta.env.BASE_URL),
17
+ routes: [
18
+ { path: '/', component: HomeView },
19
+ ],
20
+ });
21
+
22
+ export default router;
23
+ `;
24
+
25
+ describe('Vue Detail View Generator', () => {
26
+ let host: Tree;
27
+ const baseOptions: Schema = {
28
+ project: 'test',
29
+ name: 'application-detail',
30
+ resource: 'applications',
31
+ route: '/applications/:id',
32
+ fields: [
33
+ { key: 'status', label: 'Status', type: 'badge' },
34
+ { key: 'lastSaved', label: 'Last saved', type: 'date' },
35
+ { key: 'requestTotal', label: 'Request total', type: 'currency' },
36
+ { key: 'serviceModel', label: 'Service Model' },
37
+ ],
38
+ };
39
+
40
+ beforeEach(() => {
41
+ host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
42
+ addProjectConfiguration(host, 'test', { root: 'apps/test' });
43
+ host.write('apps/test/src/router/index.ts', ROUTER_FIXTURE);
44
+ });
45
+
46
+ it('throws when --project does not exist', async () => {
47
+ await expect(
48
+ generator(host, { ...baseOptions, project: 'no-such-app' }),
49
+ ).rejects.toThrow();
50
+ });
51
+
52
+ it('throws when --route has no :id param', async () => {
53
+ await expect(
54
+ generator(host, { ...baseOptions, route: '/applications' }),
55
+ ).rejects.toThrow(/:id/);
56
+ });
57
+
58
+ it("throws a clear error when the project isn't a vue-app (no router/index.ts)", async () => {
59
+ addProjectConfiguration(host, 'not-vue', { root: 'apps/not-vue' });
60
+ await expect(
61
+ generator(host, { ...baseOptions, project: 'not-vue' }),
62
+ ).rejects.toThrow(/router\/index\.ts/);
63
+ });
64
+
65
+ // Regression guard: Nx's own CLI option coercion for `"type": "array"` splits
66
+ // on commas, not JSON -- --fields is `"type": "string"` in schema.json
67
+ // specifically so the real CLI's JSON string survives unmangled. Assert the
68
+ // string form the CLI actually produces, not just the array form direct
69
+ // (unit-test) callers use.
70
+ it('accepts --fields as a JSON string, the form the real CLI produces', async () => {
71
+ await generator(host, {
72
+ ...baseOptions,
73
+ fields: JSON.stringify(baseOptions.fields),
74
+ });
75
+ const view = host
76
+ .read('apps/test/src/views/ApplicationDetailView.vue')
77
+ .toString();
78
+ expect(view).toContain('<dt>Status</dt>');
79
+ }, 30000);
80
+
81
+ it('throws a clear error when --fields is not valid JSON', async () => {
82
+ await expect(
83
+ generator(host, { ...baseOptions, fields: '{not json' }),
84
+ ).rejects.toThrow();
85
+ });
86
+
87
+ it('throws a clear error when --fields parses to an empty array', async () => {
88
+ await expect(
89
+ generator(host, { ...baseOptions, fields: '[]' }),
90
+ ).rejects.toThrow(/non-empty/);
91
+ });
92
+
93
+ it('generates the view with each field type rendered correctly', async () => {
94
+ await generator(host, baseOptions);
95
+
96
+ const view = host
97
+ .read('apps/test/src/views/ApplicationDetailView.vue')
98
+ .toString();
99
+ expect(view).toContain('heading="Application Detail"');
100
+ expect(view).toContain("fetch(`/api/applications/${route.params.id}`)");
101
+ expect(view).toContain(
102
+ "<goa-badge type=\"information\" :content=\"String(record['status'] ?? '—')\" />",
103
+ );
104
+ expect(view).toContain("formatDate(record['lastSaved'])");
105
+ expect(view).toContain("formatCurrency(record['requestTotal'])");
106
+ expect(view).toContain("record['serviceModel'] ?? '—'");
107
+ expect(view).toContain('<dt>Status</dt>');
108
+ expect(view).toContain('<dt>Service Model</dt>');
109
+ // Uses the shared shell, not hand-rolled loading/error markup.
110
+ expect(view).toContain("import { RecordDetailShell } from '@proj/vue-components';");
111
+ expect(view).toContain('<RecordDetailShell');
112
+ }, 30000);
113
+
114
+ it('respects an explicit --heading', async () => {
115
+ await generator(host, { ...baseOptions, heading: 'Grant Application' });
116
+ const view = host
117
+ .read('apps/test/src/views/ApplicationDetailView.vue')
118
+ .toString();
119
+ expect(view).toContain('heading="Grant Application"');
120
+ }, 30000);
121
+
122
+ it('inserts the route into router/index.ts, requiring auth by default', async () => {
123
+ await generator(host, baseOptions);
124
+
125
+ const routerTs = host.read('apps/test/src/router/index.ts').toString();
126
+ expect(routerTs).toContain("path: '/applications/:id'");
127
+ expect(routerTs).toContain(
128
+ "component: () => import('../views/ApplicationDetailView.vue')",
129
+ );
130
+ expect(routerTs).toContain('meta: { requiresAuth: true }');
131
+ // The existing route is untouched, not replaced.
132
+ expect(routerTs).toContain("{ path: '/', component: HomeView }");
133
+ }, 30000);
134
+
135
+ it('omits the requiresAuth meta when --requiresAuth=false', async () => {
136
+ await generator(host, { ...baseOptions, requiresAuth: false });
137
+ const routerTs = host.read('apps/test/src/router/index.ts').toString();
138
+ expect(routerTs).not.toContain('requiresAuth');
139
+ }, 30000);
140
+
141
+ it('ensures the shared RecordDetailShell pattern component exists', async () => {
142
+ await generator(host, baseOptions);
143
+ expect(
144
+ host.exists(
145
+ 'libs/vue-components/src/lib/patterns/RecordDetailShell.vue',
146
+ ),
147
+ ).toBeTruthy();
148
+ }, 30000);
149
+
150
+ it('does not touch the target project configuration', async () => {
151
+ const before = readProjectConfiguration(host, 'test');
152
+ await generator(host, baseOptions);
153
+ const after = readProjectConfiguration(host, 'test');
154
+ expect(after).toEqual(before);
155
+ }, 30000);
156
+ });
@@ -0,0 +1,8 @@
1
+ import { Tree } from '@nx/devkit';
2
+ export interface VueRouteSpec {
3
+ path: string;
4
+ /** Relative import path passed to () => import(...), e.g. '../views/FooView.vue'. */
5
+ componentImportPath: string;
6
+ requiresAuth?: boolean;
7
+ }
8
+ export declare function insertVueRoute(host: Tree, projectRoot: string, project: string, route: VueRouteSpec): void;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.insertVueRoute = insertVueRoute;
4
+ // Deterministic text insertion, not an AST edit -- vue-app's router/index.ts
5
+ // template has a fixed `routes: [ ... ]` shape, so a targeted anchor is
6
+ // simpler and safer here than pulling in an AST library for one array entry.
7
+ function insertVueRoute(host, projectRoot, project, route) {
8
+ var _a;
9
+ const routerPath = `${projectRoot}/src/router/index.ts`;
10
+ const content = (_a = host.read(routerPath)) === null || _a === void 0 ? void 0 : _a.toString();
11
+ if (content === undefined) {
12
+ throw new Error(`Could not find ${routerPath} -- is "${project}" a vue-app project generated by @abgov/nx-adsp:vue-app?`);
13
+ }
14
+ const marker = 'routes: [';
15
+ const markerIndex = content.indexOf(marker);
16
+ if (markerIndex === -1) {
17
+ throw new Error(`Could not find "${marker}" in ${routerPath} -- has its shape changed since @abgov/nx-adsp:vue-app generated it?`);
18
+ }
19
+ const metaLine = route.requiresAuth
20
+ ? `\n meta: { requiresAuth: true },`
21
+ : '';
22
+ const routeSnippet = `{\n` +
23
+ ` path: '${route.path}',\n` +
24
+ ` component: () => import('${route.componentImportPath}'),${metaLine}\n` +
25
+ ` },`;
26
+ const insertAt = markerIndex + marker.length;
27
+ const updated = content.slice(0, insertAt) +
28
+ `\n ${routeSnippet}` +
29
+ content.slice(insertAt);
30
+ host.write(routerPath, updated);
31
+ }
32
+ //# sourceMappingURL=vue-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue-router.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/vue-router.ts"],"names":[],"mappings":";;AAYA,wCAqCC;AAxCD,6EAA6E;AAC7E,wEAAwE;AACxE,6EAA6E;AAC7E,SAAgB,cAAc,CAC5B,IAAU,EACV,WAAmB,EACnB,OAAe,EACf,KAAmB;;IAEnB,MAAM,UAAU,GAAG,GAAG,WAAW,sBAAsB,CAAC;IACxD,MAAM,OAAO,GAAG,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,0CAAE,QAAQ,EAAE,CAAC;IAClD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,kBAAkB,UAAU,WAAW,OAAO,0DAA0D,CACzG,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC;IAC3B,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,mBAAmB,MAAM,QAAQ,UAAU,sEAAsE,CAClH,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY;QACjC,CAAC,CAAC,uCAAuC;QACzC,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,YAAY,GAChB,KAAK;QACL,gBAAgB,KAAK,CAAC,IAAI,MAAM;QAChC,kCAAkC,KAAK,CAAC,mBAAmB,MAAM,QAAQ,IAAI;QAC7E,QAAQ,CAAC;IAEX,MAAM,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,MAAM,OAAO,GACX,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC;QAC1B,SAAS,YAAY,EAAE;QACvB,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC1B,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1,71 @@
1
+ import { Tree } from '@nx/devkit';
2
+ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
3
+ import { insertVueRoute } from './vue-router';
4
+
5
+ const ROUTER_FIXTURE = `import { createRouter, createWebHistory } from 'vue-router';
6
+ import HomeView from '../views/HomeView.vue';
7
+
8
+ const router = createRouter({
9
+ history: createWebHistory(import.meta.env.BASE_URL),
10
+ routes: [
11
+ { path: '/', component: HomeView },
12
+ ],
13
+ });
14
+
15
+ export default router;
16
+ `;
17
+
18
+ describe('insertVueRoute', () => {
19
+ let host: Tree;
20
+
21
+ beforeEach(() => {
22
+ host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
23
+ host.write('apps/test/src/router/index.ts', ROUTER_FIXTURE);
24
+ });
25
+
26
+ it('inserts a new route ahead of the existing ones, with a requiresAuth meta', () => {
27
+ insertVueRoute(host, 'apps/test', 'test', {
28
+ path: '/things/:id',
29
+ componentImportPath: '../views/ThingView.vue',
30
+ requiresAuth: true,
31
+ });
32
+
33
+ const routerTs = host.read('apps/test/src/router/index.ts').toString();
34
+ expect(routerTs).toContain("path: '/things/:id'");
35
+ expect(routerTs).toContain(
36
+ "component: () => import('../views/ThingView.vue')",
37
+ );
38
+ expect(routerTs).toContain('meta: { requiresAuth: true }');
39
+ expect(routerTs).toContain("{ path: '/', component: HomeView }");
40
+ });
41
+
42
+ it('omits the meta line when requiresAuth is not set', () => {
43
+ insertVueRoute(host, 'apps/test', 'test', {
44
+ path: '/public-things',
45
+ componentImportPath: '../views/ThingsListView.vue',
46
+ });
47
+
48
+ const routerTs = host.read('apps/test/src/router/index.ts').toString();
49
+ expect(routerTs).not.toContain('requiresAuth');
50
+ });
51
+
52
+ it('throws a clear error when the project has no router/index.ts', () => {
53
+ host.write('apps/no-router/src/main.ts', '// no router here\n');
54
+ expect(() =>
55
+ insertVueRoute(host, 'apps/no-router', 'no-router', {
56
+ path: '/x',
57
+ componentImportPath: '../views/XView.vue',
58
+ }),
59
+ ).toThrow(/router\/index\.ts/);
60
+ });
61
+
62
+ it('throws when routes: [ has been renamed or removed', () => {
63
+ host.write('apps/test/src/router/index.ts', 'export default {};\n');
64
+ expect(() =>
65
+ insertVueRoute(host, 'apps/test', 'test', {
66
+ path: '/x',
67
+ componentImportPath: '../views/XView.vue',
68
+ }),
69
+ ).toThrow(/routes: \[/);
70
+ });
71
+ });