@abgov/nx-adsp 13.18.0-beta.7 → 13.18.0-beta.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/nx-adsp",
3
- "version": "13.18.0-beta.7",
3
+ "version": "13.18.0-beta.9",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for ADSP apps.",
@@ -172,8 +172,8 @@ from **`<%= goaImportPath %>`**, not hand-rolled markup:
172
172
 
173
173
  | Component | Purpose |
174
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 |
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. Also owns the skip-to-main-content landmark for this layout — `AppLayout` deliberately doesn't duplicate it. Also takes an optional `#topbar` slot — a slim row above the routed content, for something like a notification bell; only renders when given content, unused by default |
176
+ | `AppLayout` | The content gutter every view renders inside (see the key files table) |
177
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
178
 
179
179
  There's no `AppHeader`/`AppFooter`/hero banner in this layout — `AppSideMenu` is the
@@ -186,7 +186,7 @@ marketing chrome around a staff tool).
186
186
  | Component | Purpose |
187
187
  |---|---|
188
188
  | `AppHeader` | `goa-microsite-header` + `goa-app-header`. Takes a `heading` prop; put account/sign-in actions in its `utilities` slot (a bare child with no slot is silently dropped by `goa-app-header`) |
189
- | `AppLayout` | The content gutter every view renders inside (see the key files table). Also renders the skip-to-main-content link |
189
+ | `AppLayout` | The content gutter every view renders inside (see the key files table). `App.vue` itself owns the skip-to-main-content landmark for this layout — `AppLayout` deliberately doesn't duplicate it |
190
190
  | `AppFooter` | `goa-app-footer` with the standard GoA nav/meta links (Services/Contact/Terms of Use, Privacy/Disclaimer/Accessibility) |
191
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 |
192
192
  <% } %>
@@ -201,6 +201,23 @@ composite generator.
201
201
 
202
202
  ## Adding a view
203
203
 
204
+ **Check whether a generator already covers this view before hand-authoring one.** Each
205
+ retrofits into this project directly — file(s), route(s), and the shared component it's
206
+ built on, all in one step:
207
+
208
+ | If the view is... | Use | Built on |
209
+ |---|---|---|
210
+ | A single record's detail page (loading/error, optional status badge, back button) | `nx g @abgov/nx-adsp:vue-detail-view` | `RecordDetailShell` |
211
+ | A staff-facing, paginated/sortable/filterable list | `nx g @abgov/nx-adsp:vue-workspace-view` | `WorkspaceTable` |
212
+ | A simple admin list + create/update edit screen (no pagination) | `nx g @abgov/nx-adsp:vue-admin-crud` | `WorkspaceTable` |
213
+ | A multi-step submission flow (route-per-step, review, confirmation) | `nx g @abgov/nx-adsp:vue-intake-view` | `Stepper`, `StepErrorSummary` |
214
+
215
+ Run `nx g @abgov/nx-adsp:<generator> --help` for its full option list (each takes a
216
+ `--fields`/`--columns`/`--steps` JSON-string spec, not a CLI array flag — see the
217
+ generator's own schema description for why). Only hand-author a view when none of these
218
+ match, or once a generator's output needs customization it doesn't cover — its own comments
219
+ mark what's meant to be edited.
220
+
204
221
  1. Create `src/views/MyFeatureView.vue` as a `<script setup>` SFC. **Don't add your
205
222
  own page margins/centering** — every view renders inside `AppLayout`'s gutter, so
206
223
  any top-level tag (`<div>`, `<section>`, …) is already centered and padded.
@@ -64,6 +64,7 @@ function onAccountItemClick() {
64
64
  </AppLayout>
65
65
  </AppSideMenu>
66
66
  <% } else { %>
67
+ <a class="skip-link" href="#main-content">Skip to main content</a>
67
68
  <AppHeader heading="<%= projectName %>">
68
69
  <template #utilities>
69
70
  <goa-button-group alignment="end">
@@ -86,13 +87,34 @@ function onAccountItemClick() {
86
87
  @dismiss="session.dismiss"
87
88
  />
88
89
  <goa-hero-banner heading="<%= projectName %>" backgroundurl="/assets/banner.jpg" />
89
- <AppLayout :variant="contentWidth">
90
- <RouterView />
91
- </AppLayout>
90
+ <main id="main-content">
91
+ <AppLayout :variant="contentWidth">
92
+ <RouterView />
93
+ </AppLayout>
94
+ </main>
92
95
  <AppFooter />
93
96
  <% } %>
94
97
  </template>
95
98
 
96
99
  <style>
97
100
  body { margin: 0; }
101
+ <% if (layout !== 'internal') { %>
102
+ /* Visually hidden until focused (e.g. via Tab from the top of the page) — lets
103
+ keyboard/screen-reader users jump past the header straight to the content.
104
+ The internal/side-menu shell has its own scoped copy of this; this layout
105
+ has no equivalent shell component, so App.vue owns it directly here. */
106
+ .skip-link {
107
+ position: absolute;
108
+ left: -9999px;
109
+ top: 0;
110
+ z-index: 100;
111
+ padding: var(--goa-space-s, 0.5rem) var(--goa-space-m, 1rem);
112
+ background: var(--goa-color-interactive-default, #0070c4);
113
+ color: #fff;
114
+ }
115
+
116
+ .skip-link:focus {
117
+ left: var(--goa-space-m, 1rem);
118
+ }
119
+ <% } %>
98
120
  </style>
@@ -124,14 +124,21 @@ describe('Vue App Generator', () => {
124
124
  expect(layout).toContain('form-content');
125
125
  expect(layout).toContain('wide-content');
126
126
  expect(layout).toContain('--goa-space');
127
- // Skip-to-main-content link for keyboard/screen-reader users.
128
- expect(layout).toContain('href="#main-content"');
127
+ // AppLayout must NOT own the skip-to-main-content landmark itself: it nests
128
+ // inside AppSideMenu for --layout=internal, which already provides one, and
129
+ // a second <main id="main-content"> would duplicate the landmark/id.
130
+ expect(layout).not.toContain('id="main-content"');
131
+ expect(layout).not.toContain('skip-link');
129
132
 
130
133
  // App.vue uses AppLayout and no longer relies on the `main > section` gutter
131
134
  // (which silently failed when a view's top-level tag wasn't <section>).
135
+ // For --layout=header (the default), App.vue itself owns the skip-link
136
+ // landmark instead, since there's no shell component to hold it.
132
137
  const app = host.read('apps/test/src/App.vue').toString();
133
138
  expect(app).toContain('AppLayout');
134
139
  expect(app).not.toContain('main > section');
140
+ expect(app).toContain('href="#main-content"');
141
+ expect(app).toContain('id="main-content"');
135
142
  });
136
143
 
137
144
  it('provisions the shared GoA wrapper library and points the app at it', async () => {
@@ -202,6 +209,11 @@ describe('Vue App Generator', () => {
202
209
  expect(app).toContain('@item-click="onAccountItemClick"');
203
210
  // The content gutter is shared regardless of shell choice.
204
211
  expect(app).toContain('<AppLayout');
212
+ // App.vue itself must not add a second skip-to-main-content landmark —
213
+ // AppSideMenu (imported, not inlined) already owns the one and only
214
+ // #main-content for this layout.
215
+ expect(app).not.toContain('id="main-content"');
216
+ expect(app).not.toContain('skip-link');
205
217
 
206
218
  const agents = host.read('apps/test/AGENTS.md').toString();
207
219
  expect(agents).toContain('--layout=internal');
@@ -1,14 +1,31 @@
1
1
  # AGENTS.md — vue-components (GoA Design System Vue wrappers + app shell)
2
2
 
3
- A shared, workspace-local library with two distinct kinds of content every Vue
4
- app in this workspace imports both instead of carrying its own copy. Generated by
5
- `@abgov/nx-adsp:vue-components`.
3
+ A shared, workspace-local library every Vue app in this workspace imports from
4
+ it instead of carrying its own copy. Generated by `@abgov/nx-adsp:vue-components`,
5
+ invoked automatically by `vue-app` and by every `vue-*-view` generator.
6
6
 
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
10
  | `src/lib/patterns/` | Composite, app-shell components (`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`, `WorkspaceTable`, `Stepper`, `StepErrorSummary`) | **Permanent** |
11
11
 
12
+ **Not every pattern component is present in every app.** `AppLayout`, `AppHeader`,
13
+ `AppFooter`, `AppSideMenu`, and `SessionExpiredBanner` are base app-shell — `vue-app`
14
+ provisions and wires all five into `App.vue` for every generated app, unconditionally.
15
+ The rest are on-demand: they only exist in a given app's `src/lib/patterns/` if that
16
+ app has run the matching view generator at least once.
17
+
18
+ | Component | Provisioned by |
19
+ |---|---|
20
+ | `RecordDetailShell` | `vue-detail-view` |
21
+ | `WorkspaceTable` | `vue-workspace-view`, `vue-admin-crud` |
22
+ | `Stepper`, `StepErrorSummary` | `vue-intake-view` |
23
+
24
+ Don't assume one of these on-demand components exists in a given app — check whether
25
+ its generator has actually been run (e.g. `nx list @abgov/nx-adsp` won't tell you;
26
+ look for the component file itself, or the view file that would import it) before
27
+ building on it.
28
+
12
29
  > **⚠️ `primitives/` is interim — do not invest in it as permanent.** It exists
13
30
  > only because GoA DS has not yet published an official Vue wrapper package. When
14
31
  > `@abgov/vue-components` ships, delete `primitives/` and repoint imports at it —
@@ -8,37 +8,24 @@
8
8
  // meta: { layout: 'form' } // single-column forms → 640px
9
9
  // (default) // general content → 1000px
10
10
  // meta: { layout: 'wide' } // data-heavy pages / tables → 1200px
11
+ //
12
+ // Deliberately not a landmark/skip-target itself: this component nests inside
13
+ // AppSideMenu for --layout=internal, which already provides one, and a second
14
+ // copy would duplicate the anchor screen readers/keyboard nav jump to. The
15
+ // top-level shell (App.vue for --layout=header, AppSideMenu for --layout=internal)
16
+ // owns that landmark instead.
11
17
  withDefaults(defineProps<{ variant?: 'page' | 'wide' | 'form' }>(), {
12
18
  variant: 'page',
13
19
  });
14
20
  </script>
15
21
 
16
22
  <template>
17
- <a class="skip-link" href="#main-content">Skip to main content</a>
18
- <main id="main-content">
19
- <div :class="`${variant}-content`">
20
- <slot />
21
- </div>
22
- </main>
23
+ <div :class="`${variant}-content`">
24
+ <slot />
25
+ </div>
23
26
  </template>
24
27
 
25
28
  <style scoped>
26
- /* Visually hidden until focused (e.g. via Tab from the top of the page) — lets
27
- keyboard/screen-reader users jump past the header straight to the content. */
28
- .skip-link {
29
- position: absolute;
30
- left: -9999px;
31
- top: 0;
32
- z-index: 100;
33
- padding: var(--goa-space-s, 0.5rem) var(--goa-space-m, 1rem);
34
- background: var(--goa-color-interactive-default, #0070c4);
35
- color: #fff;
36
- }
37
-
38
- .skip-link:focus {
39
- left: var(--goa-space-m, 1rem);
40
- }
41
-
42
29
  .page-content,
43
30
  .wide-content,
44
31
  .form-content {
@@ -1,9 +1,20 @@
1
1
  <script setup lang="ts">
2
+ import { useSlots } from 'vue';
3
+
2
4
  // The alternate top-level shell for staff-facing/internal apps (goa-work-side-menu),
3
5
  // used instead of AppHeader/AppFooter -- not together with them. Presentational only:
4
6
  // unlike the real GovAlta-Pronghorn source this is adapted from, active-route
5
7
  // highlighting and navigation are the caller's job (pass `current` on each item,
6
8
  // listen for `item-click`) so this component doesn't need vue-router as a dependency.
9
+ //
10
+ // The real reference source also has a slim in-content "topbar" row for
11
+ // module-registered header actions (e.g. a notification bell) -- there's no
12
+ // AppHeader/utilities slot in this layout to hold that kind of thing. Exposed
13
+ // here as a plain named slot (`#topbar`), not a module-registration system --
14
+ // that's app-specific plumbing, not something a shared, presentational
15
+ // component should own.
16
+ const slots = useSlots();
17
+
7
18
  interface AppSideMenuItem {
8
19
  label: string;
9
20
  to?: string;
@@ -63,6 +74,9 @@ const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
63
74
  />
64
75
  </goa-work-side-menu>
65
76
  <main id="main-content" class="app-side-menu-content">
77
+ <div v-if="slots.topbar" class="app-side-menu-topbar">
78
+ <slot name="topbar" />
79
+ </div>
66
80
  <slot />
67
81
  </main>
68
82
  </div>
@@ -80,6 +94,15 @@ const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
80
94
  overflow-y: auto;
81
95
  }
82
96
 
97
+ .app-side-menu-topbar {
98
+ display: flex;
99
+ justify-content: flex-end;
100
+ align-items: center;
101
+ gap: var(--goa-space-s, 0.75rem);
102
+ padding: var(--goa-space-xs, 0.5rem) var(--goa-space-m, 1rem);
103
+ border-bottom: 1px solid var(--goa-color-greyscale-100, #f1f1f1);
104
+ }
105
+
83
106
  .skip-link {
84
107
  position: absolute;
85
108
  left: -9999px;
@@ -76,6 +76,19 @@ describe('Vue Components Generator', () => {
76
76
  expect(agents).toContain('defineModel<boolean>');
77
77
  }, 30000);
78
78
 
79
+ it('AppSideMenu exposes an optional #topbar slot for header-action-style content', async () => {
80
+ await generator(host);
81
+
82
+ const sideMenu = host
83
+ .read('libs/vue-components/src/lib/patterns/AppSideMenu.vue')
84
+ .toString();
85
+ // Named slot, only rendered when actually given content -- no empty bar
86
+ // shows by default, matching the "unused by default" doc claim.
87
+ expect(sideMenu).toContain('<slot name="topbar" />');
88
+ expect(sideMenu).toContain('v-if="slots.topbar"');
89
+ expect(sideMenu).toContain('useSlots');
90
+ }, 30000);
91
+
79
92
  it('disables vue/no-deprecated-slot-attribute in flat config too, not just .eslintrc.json', async () => {
80
93
  // useFlatConfig() (from @nx/eslint) treats a root flat-config file's
81
94
  // presence as authoritative, regardless of the installed ESLint version —