@abgov/nx-adsp 13.17.1 → 13.18.0-beta.2
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-app/files/AGENTS.md__tmpl__ +25 -7
- package/src/generators/vue-app/files/src/App.spec.ts__tmpl__ +44 -2
- package/src/generators/vue-app/files/src/App.vue__tmpl__ +18 -10
- package/src/generators/vue-app/files/src/main.ts__tmpl__ +10 -1
- package/src/generators/vue-app/files/src/stores/session.ts__tmpl__ +16 -0
- package/src/generators/vue-app/vue-app.js +3 -0
- package/src/generators/vue-app/vue-app.js.map +1 -1
- package/src/generators/vue-app/vue-app.spec.ts +68 -11
- package/src/generators/vue-components/files/AGENTS.md__tmpl__ +44 -14
- package/src/generators/vue-components/files/src/index.ts__tmpl__ +23 -12
- package/src/generators/vue-components/files/src/lib/patterns/AppFooter.vue__tmpl__ +14 -0
- package/src/generators/vue-components/files/src/lib/patterns/AppHeader.vue__tmpl__ +19 -0
- package/src/generators/{vue-app/files/src/components → vue-components/files/src/lib/patterns}/AppLayout.vue__tmpl__ +17 -0
- package/src/generators/vue-components/files/src/lib/patterns/SessionExpiredBanner.vue__tmpl__ +28 -0
- package/src/generators/vue-components/files/src/vue-components.spec.ts__tmpl__ +15 -4
- package/src/generators/vue-components/vue-components.spec.ts +14 -3
- /package/src/generators/vue-components/files/src/lib/{GoabButton.vue__tmpl__ → primitives/GoabButton.vue__tmpl__} +0 -0
- /package/src/generators/vue-components/files/src/lib/{GoabCheckbox.vue__tmpl__ → primitives/GoabCheckbox.vue__tmpl__} +0 -0
- /package/src/generators/vue-components/files/src/lib/{GoabDropdown.vue__tmpl__ → primitives/GoabDropdown.vue__tmpl__} +0 -0
- /package/src/generators/vue-components/files/src/lib/{GoabInput.vue__tmpl__ → primitives/GoabInput.vue__tmpl__} +0 -0
- /package/src/generators/vue-components/files/src/lib/{GoabModal.vue__tmpl__ → primitives/GoabModal.vue__tmpl__} +0 -0
- /package/src/generators/vue-components/files/src/lib/{GoabRadioGroup.vue__tmpl__ → primitives/GoabRadioGroup.vue__tmpl__} +0 -0
- /package/src/generators/vue-components/files/src/lib/{GoabTextarea.vue__tmpl__ → primitives/GoabTextarea.vue__tmpl__} +0 -0
package/package.json
CHANGED
|
@@ -30,9 +30,9 @@ executors read options from `project.json` and do not prompt.
|
|
|
30
30
|
|
|
31
31
|
| File | Purpose |
|
|
32
32
|
|------|---------|
|
|
33
|
-
| `src/main.ts` | Entry — registers Pinia, Router, and Keycloak plugin |
|
|
34
|
-
| `src/App.vue` | Shell —
|
|
35
|
-
| `src/
|
|
33
|
+
| `src/main.ts` | Entry — registers Pinia, Router, and Keycloak plugin (incl. `onAuthRefreshError`, which flags the session store as expired) |
|
|
34
|
+
| `src/App.vue` | Shell — `AppHeader` with sign-in/out in its `utilities` slot, `SessionExpiredBanner`, hero banner, `<RouterView>` wrapped in `AppLayout`, `AppFooter` |
|
|
35
|
+
| `src/stores/session.ts` | Pinia store — `expired` flag behind `SessionExpiredBanner`, set from `main.ts`'s `onAuthRefreshError` hook |
|
|
36
36
|
| `src/router/index.ts` | Routes — `/protected` guarded with `requiresAuth` meta |
|
|
37
37
|
| `src/views/HomeView.vue` | Public page — calls public and private APIs |
|
|
38
38
|
| `src/views/ProtectedView.vue` | Authenticated page — shows user info from token |
|
|
@@ -151,10 +151,28 @@ the design system supports still works. For components without a wrapper, use th
|
|
|
151
151
|
across every Vue app in this workspace — fix or extend a wrapper once in
|
|
152
152
|
`libs/vue-components` (or run `nx g @abgov/nx-adsp:vue-components` to restore it).
|
|
153
153
|
|
|
154
|
-
> **Interim:** these wrappers are a
|
|
155
|
-
> `@abgov/vue-components`. When it lands,
|
|
156
|
-
> repoint imports at that package — the names/props are
|
|
157
|
-
> it's a scope swap.
|
|
154
|
+
> **Interim:** these wrappers (`libs/vue-components/src/lib/primitives/`) are a
|
|
155
|
+
> stopgap until GoA DS ships an official `@abgov/vue-components`. When it lands,
|
|
156
|
+
> delete `primitives/` and repoint imports at that package — the names/props are
|
|
157
|
+
> 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).
|
|
161
|
+
|
|
162
|
+
### App shell components
|
|
163
|
+
|
|
164
|
+
`App.vue` is built from four shared components imported from
|
|
165
|
+
**`<%= goaImportPath %>`**, not hand-rolled markup:
|
|
166
|
+
|
|
167
|
+
| Component | Purpose |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `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`) |
|
|
170
|
+
| `AppLayout` | The content gutter every view renders inside (see the key files table). Also renders the skip-to-main-content link |
|
|
171
|
+
| `AppFooter` | `goa-app-footer` with the standard GoA nav/meta links (Services/Contact/Terms of Use, Privacy/Disclaimer/Accessibility) |
|
|
172
|
+
| `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
|
+
|
|
174
|
+
These are shared across every Vue app in this workspace — fix or extend one once
|
|
175
|
+
in `libs/vue-components/src/lib/patterns/`, the same way as the `Goab*` wrappers.
|
|
158
176
|
|
|
159
177
|
## Adding a view
|
|
160
178
|
|
|
@@ -16,8 +16,10 @@ vi.mock('@dsb-norge/vue-keycloak-js', async () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
19
|
+
import { createPinia } from 'pinia';
|
|
19
20
|
import { createMemoryHistory, createRouter } from 'vue-router';
|
|
20
21
|
import App from './App.vue';
|
|
22
|
+
import { useSessionStore } from './stores/session';
|
|
21
23
|
|
|
22
24
|
// App.vue reads useRoute() for the layout-width feature (route.meta.layout,
|
|
23
25
|
// default 'page') — that needs a real router installed via `global.plugins`,
|
|
@@ -35,7 +37,9 @@ const router = createRouter({
|
|
|
35
37
|
|
|
36
38
|
describe('App shell header', () => {
|
|
37
39
|
it('shows Sign in when unauthenticated', () => {
|
|
38
|
-
const wrapper = mount(App, {
|
|
40
|
+
const wrapper = mount(App, {
|
|
41
|
+
global: { plugins: [router, createPinia()], stubs: { RouterView: true } },
|
|
42
|
+
});
|
|
39
43
|
const group = wrapper.find('goa-button-group');
|
|
40
44
|
expect(group.exists()).toBe(true);
|
|
41
45
|
expect(group.html()).toContain('Sign in');
|
|
@@ -45,9 +49,47 @@ describe('App shell header', () => {
|
|
|
45
49
|
// The bug: destructuring useKeycloak() froze `keycloak` at undefined, so
|
|
46
50
|
// login() was a permanent no-op. Reactive access must reach the instance.
|
|
47
51
|
const kc = useKeycloak();
|
|
48
|
-
const wrapper = mount(App, {
|
|
52
|
+
const wrapper = mount(App, {
|
|
53
|
+
global: { plugins: [router, createPinia()], stubs: { RouterView: true } },
|
|
54
|
+
});
|
|
49
55
|
const btn = wrapper.findAll('goa-button').find((b) => b.text().includes('Sign in'));
|
|
50
56
|
await btn?.trigger('_click');
|
|
51
57
|
expect(kc.keycloak?.login).toHaveBeenCalled();
|
|
52
58
|
});
|
|
53
59
|
});
|
|
60
|
+
|
|
61
|
+
describe('App shell session-expired banner', () => {
|
|
62
|
+
it('is hidden until the session store flags an expired session', async () => {
|
|
63
|
+
const pinia = createPinia();
|
|
64
|
+
const wrapper = mount(App, {
|
|
65
|
+
global: { plugins: [router, pinia], stubs: { RouterView: true } },
|
|
66
|
+
});
|
|
67
|
+
expect(wrapper.find('.session-expired-banner').exists()).toBe(false);
|
|
68
|
+
|
|
69
|
+
useSessionStore(pinia).markExpired();
|
|
70
|
+
await wrapper.vm.$nextTick();
|
|
71
|
+
expect(wrapper.find('.session-expired-banner').exists()).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('Sign in on the banner dismisses it and calls keycloak.login', async () => {
|
|
75
|
+
const kc = useKeycloak();
|
|
76
|
+
const pinia = createPinia();
|
|
77
|
+
const wrapper = mount(App, {
|
|
78
|
+
global: { plugins: [router, pinia], stubs: { RouterView: true } },
|
|
79
|
+
});
|
|
80
|
+
useSessionStore(pinia).markExpired();
|
|
81
|
+
await wrapper.vm.$nextTick();
|
|
82
|
+
|
|
83
|
+
// SessionExpiredBanner's "Sign in" is the GoabButton wrapper, which re-exposes
|
|
84
|
+
// goa-button's `_click` as a plain `click` on itself -- but the underlying DOM
|
|
85
|
+
// node vue-test-utils sees is still the raw <goa-button>, so trigger `_click`
|
|
86
|
+
// (same as AppHeader's raw Sign in button above), not `click`.
|
|
87
|
+
const btn = wrapper
|
|
88
|
+
.findAll('goa-button')
|
|
89
|
+
.find((b) => b.text().includes('Sign in') && b.attributes('type') === 'primary');
|
|
90
|
+
await btn?.trigger('_click');
|
|
91
|
+
|
|
92
|
+
expect(kc.keycloak?.login).toHaveBeenCalled();
|
|
93
|
+
expect(useSessionStore(pinia).expired).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -2,13 +2,15 @@
|
|
|
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
|
-
import AppLayout from '
|
|
5
|
+
import { AppHeader, AppLayout, AppFooter, SessionExpiredBanner } from '<%= goaImportPath %>';
|
|
6
|
+
import { useSessionStore } from './stores/session';
|
|
6
7
|
|
|
7
8
|
// Keep the reactive instance — do NOT destructure. useKeycloak() returns a
|
|
8
9
|
// readonly(reactive()); destructuring snapshots each field at setup time, so
|
|
9
10
|
// `keycloak` would be frozen at its initial `undefined` (login() a no-op) and
|
|
10
11
|
// `authenticated` would never flip. Access fields on `kc` to stay reactive.
|
|
11
12
|
const kc = useKeycloak();
|
|
13
|
+
const session = useSessionStore();
|
|
12
14
|
|
|
13
15
|
// Content width per view, from route meta (default 'page'). Views opt into a
|
|
14
16
|
// different width with `meta: { layout: 'wide' | 'form' }` in the router.
|
|
@@ -19,16 +21,16 @@ const layout = computed(
|
|
|
19
21
|
|
|
20
22
|
function login() { kc.keycloak?.login(); }
|
|
21
23
|
function logout() { kc.keycloak?.logout({ redirectUri: window.location.origin }); }
|
|
24
|
+
|
|
25
|
+
function signInAgain() {
|
|
26
|
+
session.dismiss();
|
|
27
|
+
kc.keycloak?.login();
|
|
28
|
+
}
|
|
22
29
|
</script>
|
|
23
30
|
|
|
24
31
|
<template>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
<!-- goa-app-header only renders content placed in a named slot — the
|
|
28
|
-
"utilities" slot is where GoA's own design system docs put
|
|
29
|
-
account/sign-in actions (as opposed to "navigation" for nav links).
|
|
30
|
-
A bare child with no slot attribute renders nothing. -->
|
|
31
|
-
<div slot="utilities">
|
|
32
|
+
<AppHeader heading="<%= projectName %>">
|
|
33
|
+
<template #utilities>
|
|
32
34
|
<goa-button-group alignment="end">
|
|
33
35
|
<span v-if="kc.authenticated && kc.fullName">{{ kc.fullName }}</span>
|
|
34
36
|
<!-- Gate on !authenticated only, never on `ready`: with no load-time SSO
|
|
@@ -41,12 +43,18 @@ function logout() { kc.keycloak?.logout({ redirectUri: window.location.origin })
|
|
|
41
43
|
Sign out
|
|
42
44
|
</goa-button>
|
|
43
45
|
</goa-button-group>
|
|
44
|
-
</
|
|
45
|
-
</
|
|
46
|
+
</template>
|
|
47
|
+
</AppHeader>
|
|
48
|
+
<SessionExpiredBanner
|
|
49
|
+
v-model:show="session.expired"
|
|
50
|
+
@sign-in="signInAgain"
|
|
51
|
+
@dismiss="session.dismiss"
|
|
52
|
+
/>
|
|
46
53
|
<goa-hero-banner heading="<%= projectName %>" backgroundurl="/assets/banner.jpg" />
|
|
47
54
|
<AppLayout :variant="layout">
|
|
48
55
|
<RouterView />
|
|
49
56
|
</AppLayout>
|
|
57
|
+
<AppFooter />
|
|
50
58
|
</template>
|
|
51
59
|
|
|
52
60
|
<style>
|
|
@@ -12,10 +12,12 @@ import keycloakPlugin from '@dsb-norge/vue-keycloak-js';
|
|
|
12
12
|
import App from './App.vue';
|
|
13
13
|
import router from './router';
|
|
14
14
|
import { environment } from './environments/environment';
|
|
15
|
+
import { useSessionStore } from './stores/session';
|
|
15
16
|
|
|
16
17
|
const app = createApp(App);
|
|
18
|
+
const pinia = createPinia();
|
|
17
19
|
|
|
18
|
-
app.use(
|
|
20
|
+
app.use(pinia);
|
|
19
21
|
app.use(router);
|
|
20
22
|
app.use(keycloakPlugin, {
|
|
21
23
|
config: {
|
|
@@ -41,6 +43,13 @@ app.use(keycloakPlugin, {
|
|
|
41
43
|
pkceMethod: 'S256',
|
|
42
44
|
checkLoginIframe: false,
|
|
43
45
|
},
|
|
46
|
+
// Not onAuthLogout: keycloak-js only invokes that via the session-status
|
|
47
|
+
// iframe (disabled above by checkLoginIframe: false) or in Cordova mode --
|
|
48
|
+
// it would never fire here. The wrapper's own background auto-refresh
|
|
49
|
+
// (autoRefreshToken, on by default) periodically calls updateToken();
|
|
50
|
+
// onAuthRefreshError fires when that fails because the refresh token itself
|
|
51
|
+
// has expired, which is the real, always-on "session expired" signal.
|
|
52
|
+
onAuthRefreshError: () => useSessionStore(pinia).markExpired(),
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
app.mount('#app');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
|
|
3
|
+
// Tracks whether the user's session has expired, surfaced via
|
|
4
|
+
// SessionExpiredBanner in App.vue. Set from main.ts's onAuthRefreshError hook
|
|
5
|
+
// -- see that file for why that hook, not onAuthLogout, is the real signal.
|
|
6
|
+
export const useSessionStore = defineStore('session', {
|
|
7
|
+
state: () => ({ expired: false }),
|
|
8
|
+
actions: {
|
|
9
|
+
markExpired() {
|
|
10
|
+
this.expired = true;
|
|
11
|
+
},
|
|
12
|
+
dismiss() {
|
|
13
|
+
this.expired = false;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -122,6 +122,9 @@ function default_1(host, options) {
|
|
|
122
122
|
'src/app/App.spec.ts',
|
|
123
123
|
'src/app/NxWelcome.vue',
|
|
124
124
|
'src/components/HelloWorld.vue',
|
|
125
|
+
// AppLayout now lives in the shared vue-components lib (patterns/) —
|
|
126
|
+
// remove a stale copy from an app scaffolded before that move.
|
|
127
|
+
'src/components/AppLayout.vue',
|
|
125
128
|
'src/views/AboutView.vue',
|
|
126
129
|
'vite.config.mts',
|
|
127
130
|
]) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-app.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-app/vue-app.ts"],"names":[],"mappings":";;AAsHA,
|
|
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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"}
|
|
@@ -116,15 +116,16 @@ describe('Vue App Generator', () => {
|
|
|
116
116
|
it('wraps views in a shared AppLayout gutter (not a bare tag selector)', async () => {
|
|
117
117
|
await generator(host, options);
|
|
118
118
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.toString();
|
|
119
|
+
// Layout is a pattern component in the shared lib, not copied into the app.
|
|
120
|
+
const layoutPath = 'libs/vue-components/src/lib/patterns/AppLayout.vue';
|
|
121
|
+
expect(host.exists(layoutPath)).toBeTruthy();
|
|
122
|
+
const layout = host.read(layoutPath).toString();
|
|
124
123
|
// Three named width variants, token-driven padding.
|
|
125
124
|
expect(layout).toContain('form-content');
|
|
126
125
|
expect(layout).toContain('wide-content');
|
|
127
126
|
expect(layout).toContain('--goa-space');
|
|
127
|
+
// Skip-to-main-content link for keyboard/screen-reader users.
|
|
128
|
+
expect(layout).toContain('href="#main-content"');
|
|
128
129
|
|
|
129
130
|
// App.vue uses AppLayout and no longer relies on the `main > section` gutter
|
|
130
131
|
// (which silently failed when a view's top-level tag wasn't <section>).
|
|
@@ -137,7 +138,7 @@ describe('Vue App Generator', () => {
|
|
|
137
138
|
await generator(host, options);
|
|
138
139
|
|
|
139
140
|
// Wrappers live in a shared workspace lib, not copied into the app.
|
|
140
|
-
const
|
|
141
|
+
const primitives = 'libs/vue-components/src/lib/primitives';
|
|
141
142
|
for (const name of [
|
|
142
143
|
'GoabInput',
|
|
143
144
|
'GoabTextarea',
|
|
@@ -147,12 +148,12 @@ describe('Vue App Generator', () => {
|
|
|
147
148
|
'GoabButton',
|
|
148
149
|
'GoabModal',
|
|
149
150
|
]) {
|
|
150
|
-
expect(host.exists(`${
|
|
151
|
+
expect(host.exists(`${primitives}/${name}.vue`)).toBeTruthy();
|
|
151
152
|
}
|
|
152
153
|
expect(host.exists('apps/test/src/components/goa')).toBeFalsy();
|
|
153
154
|
|
|
154
155
|
// Real v-model wiring: bind :value and read the new value off the GoA event.
|
|
155
|
-
const input = host.read(`${
|
|
156
|
+
const input = host.read(`${primitives}/GoabInput.vue`).toString();
|
|
156
157
|
expect(input).toContain('defineModel');
|
|
157
158
|
expect(input).toContain('.detail.value');
|
|
158
159
|
|
|
@@ -163,6 +164,55 @@ describe('Vue App Generator', () => {
|
|
|
163
164
|
expect(agents).toContain('/vue-components');
|
|
164
165
|
}, 30000);
|
|
165
166
|
|
|
167
|
+
it('provisions the shared app-shell pattern components and App.vue imports them', async () => {
|
|
168
|
+
await generator(host, options);
|
|
169
|
+
|
|
170
|
+
const patterns = 'libs/vue-components/src/lib/patterns';
|
|
171
|
+
for (const name of [
|
|
172
|
+
'AppLayout',
|
|
173
|
+
'AppHeader',
|
|
174
|
+
'AppFooter',
|
|
175
|
+
'SessionExpiredBanner',
|
|
176
|
+
]) {
|
|
177
|
+
expect(host.exists(`${patterns}/${name}.vue`)).toBeTruthy();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const app = host.read('apps/test/src/App.vue').toString();
|
|
181
|
+
expect(app).toContain(
|
|
182
|
+
"import { AppHeader, AppLayout, AppFooter, SessionExpiredBanner } from '@proj/vue-components';",
|
|
183
|
+
);
|
|
184
|
+
expect(app).toContain('<AppHeader heading="test">');
|
|
185
|
+
expect(app).toContain('<AppFooter />');
|
|
186
|
+
}, 30000);
|
|
187
|
+
|
|
188
|
+
it('wires SessionExpiredBanner to a session store fed by onAuthRefreshError (not onAuthLogout)', async () => {
|
|
189
|
+
await generator(host, options);
|
|
190
|
+
|
|
191
|
+
expect(host.exists('apps/test/src/stores/session.ts')).toBeTruthy();
|
|
192
|
+
const store = host.read('apps/test/src/stores/session.ts').toString();
|
|
193
|
+
expect(store).toContain("defineStore('session'");
|
|
194
|
+
expect(store).toContain('markExpired');
|
|
195
|
+
|
|
196
|
+
// Strip // comments — main.ts legitimately explains, in prose, why
|
|
197
|
+
// onAuthLogout is the wrong hook; assert against the actual init code.
|
|
198
|
+
const mainTsCode = host
|
|
199
|
+
.read('apps/test/src/main.ts')
|
|
200
|
+
.toString()
|
|
201
|
+
.split('\n')
|
|
202
|
+
.filter((l) => !l.trim().startsWith('//'))
|
|
203
|
+
.join('\n');
|
|
204
|
+
expect(mainTsCode).toContain('onAuthRefreshError');
|
|
205
|
+
// Regression guard: onAuthLogout only fires via the session-status iframe
|
|
206
|
+
// (disabled here) or Cordova mode — it would never fire in this app.
|
|
207
|
+
expect(mainTsCode).not.toContain('onAuthLogout');
|
|
208
|
+
expect(mainTsCode).toContain('useSessionStore(pinia).markExpired()');
|
|
209
|
+
|
|
210
|
+
const app = host.read('apps/test/src/App.vue').toString();
|
|
211
|
+
expect(app).toContain('v-model:show="session.expired"');
|
|
212
|
+
expect(app).toContain('@sign-in="signInAgain"');
|
|
213
|
+
expect(app).toContain('@dismiss="session.dismiss"');
|
|
214
|
+
}, 30000);
|
|
215
|
+
|
|
166
216
|
it('inits Keycloak with no hidden iframes so init never hangs', async () => {
|
|
167
217
|
await generator(host, options);
|
|
168
218
|
// Strip // comments — they legitimately reference the disabled iframe options
|
|
@@ -255,11 +305,18 @@ describe('Vue App Generator', () => {
|
|
|
255
305
|
// goa-app-header (v2) only renders content placed in a named slot — GoA's
|
|
256
306
|
// own design system docs put account/sign-in actions in "utilities" (vs.
|
|
257
307
|
// "navigation" for nav links). A bare child with no slot attribute
|
|
258
|
-
// renders nothing, silently dropping the sign-in button.
|
|
259
|
-
//
|
|
308
|
+
// renders nothing, silently dropping the sign-in button. AppHeader (the
|
|
309
|
+
// shared pattern component) owns the native `slot="utilities"` div; App.vue
|
|
310
|
+
// just feeds its named Vue slot. Regression guard for exactly that gap,
|
|
311
|
+
// now spanning both files since AppHeader was extracted out of App.vue.
|
|
260
312
|
await generator(host, options);
|
|
313
|
+
const appHeader = host
|
|
314
|
+
.read('libs/vue-components/src/lib/patterns/AppHeader.vue')
|
|
315
|
+
.toString();
|
|
316
|
+
expect(appHeader).toMatch(/<div[^>]*slot="utilities"[^>]*>[\s\S]*<slot name="utilities"/);
|
|
317
|
+
|
|
261
318
|
const app = host.read('apps/test/src/App.vue').toString();
|
|
262
|
-
expect(app).toMatch(/<
|
|
319
|
+
expect(app).toMatch(/<template #utilities>[\s\S]*<goa-button-group/);
|
|
263
320
|
}, 30000);
|
|
264
321
|
|
|
265
322
|
it('removes the @nx/vue demo scaffold and ships a passing App test', async () => {
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
# AGENTS.md — vue-components (GoA Design System Vue wrappers)
|
|
1
|
+
# AGENTS.md — vue-components (GoA Design System Vue wrappers + app shell)
|
|
2
2
|
|
|
3
|
-
A shared, workspace-local library
|
|
4
|
-
|
|
5
|
-
imports these instead of carrying its own copy. Generated by
|
|
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
|
|
6
5
|
`@abgov/nx-adsp:vue-components`.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
| Folder | Contains | Lifespan |
|
|
8
|
+
|---|---|---|
|
|
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** |
|
|
11
|
+
|
|
12
|
+
> **⚠️ `primitives/` is interim — do not invest in it as permanent.** It exists
|
|
13
|
+
> only because GoA DS has not yet published an official Vue wrapper package. When
|
|
14
|
+
> `@abgov/vue-components` ships, delete `primitives/` and repoint imports at it —
|
|
11
15
|
> the component names and props are deliberately kept matching, so it should be a
|
|
12
16
|
> scope swap, not a rewrite. Don't add features here that would make that swap
|
|
13
17
|
> harder (see "Don't" below).
|
|
18
|
+
>
|
|
19
|
+
> **`patterns/` is not part of that swap.** App-shell composition (layout,
|
|
20
|
+
> header, footer, banners) has no equivalent in an official design-system
|
|
21
|
+
> package — a design system ships primitives, not your app's shell. Nothing in
|
|
22
|
+
> `patterns/` is deleted when `primitives/` is.
|
|
14
23
|
|
|
15
24
|
## What these wrappers do (and why they're needed)
|
|
16
25
|
|
|
@@ -45,7 +54,7 @@ Most value elements put the value on `detail.value`; some carry extra keys
|
|
|
45
54
|
(dropdown adds `label`, radio-group adds `labels`) you can ignore unless you need
|
|
46
55
|
them. Always confirm against the component's page — see below.
|
|
47
56
|
|
|
48
|
-
## Wrapping a new component
|
|
57
|
+
## Wrapping a new component (`primitives/`)
|
|
49
58
|
|
|
50
59
|
### 1. Find the event contract
|
|
51
60
|
|
|
@@ -129,14 +138,35 @@ Don't re-declare props you don't transform — let them fall through. `name` in
|
|
|
129
138
|
particular is required by most `goa-*` form elements (it's echoed in the event
|
|
130
139
|
detail); just leave it to fall through from the caller.
|
|
131
140
|
|
|
141
|
+
## Building a pattern component (`patterns/`)
|
|
142
|
+
|
|
143
|
+
A pattern component is app-shell composition — layout, header/footer chrome,
|
|
144
|
+
banners — not a single-element wrapper. Existing examples: `AppLayout`,
|
|
145
|
+
`AppHeader`, `AppFooter`, `SessionExpiredBanner`.
|
|
146
|
+
|
|
147
|
+
- It's fine to compose `primitives/` wrappers inside a pattern component (e.g.
|
|
148
|
+
`SessionExpiredBanner` uses `GoabButton`) — import them with a relative path
|
|
149
|
+
(`../primitives/GoabButton.vue`), not the package's own public import path.
|
|
150
|
+
- Stay presentational: accept data and behavior via props/`v-model`/`emit`, don't
|
|
151
|
+
fetch data, call routing APIs, or encode business rules. A pattern component
|
|
152
|
+
used by every app in the workspace can't assume any one app's routes or
|
|
153
|
+
domain — that belongs in the app that uses it (see `SessionExpiredBanner`'s own
|
|
154
|
+
comment for a live example: the component renders the banner, but which
|
|
155
|
+
Keycloak hook flips `show` is main.ts's job, in the consuming app).
|
|
156
|
+
- `export` it from `src/index.ts` under the "patterns" group, with a one-line
|
|
157
|
+
purpose comment in this file's app-shell components table (mirrored in each
|
|
158
|
+
consuming generator's own `AGENTS.md`, e.g. `vue-app/files/AGENTS.md__tmpl__`).
|
|
159
|
+
|
|
132
160
|
## Don't
|
|
133
161
|
|
|
134
|
-
- **Don't
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
- **Don't add new `goa-*` element wrappers to `patterns/`, or new composite
|
|
163
|
+
components to `primitives/`.** Single-element wrappers belong in
|
|
164
|
+
`primitives/` (interim, deleted on the `@abgov/vue-components` swap);
|
|
165
|
+
composites belong in `patterns/` (permanent). Putting one in the wrong folder
|
|
166
|
+
either blocks that swap or gets deleted by it.
|
|
167
|
+
- **Don't add app-specific logic** anywhere in this library (fetching, routing,
|
|
168
|
+
business rules) — it's shared across every app in the workspace. Keep every
|
|
169
|
+
component, wrapper or pattern, presentational.
|
|
140
170
|
|
|
141
171
|
## Testing
|
|
142
172
|
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
// GoA Design System Vue wrappers
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export { default as
|
|
1
|
+
// GoA Design System Vue wrappers, shared across every Vue app in this workspace.
|
|
2
|
+
//
|
|
3
|
+
// primitives/ — thin v-model/idiomatic-event wrappers over individual goa-*
|
|
4
|
+
// elements. INTERIM: once GoA DS publishes an official @abgov/vue-components,
|
|
5
|
+
// delete these files and repoint imports at it — names/props are kept
|
|
6
|
+
// matching, so it's a scope swap, not a rewrite.
|
|
7
|
+
//
|
|
8
|
+
// patterns/ — composite, app-shell components (layout, header, footer, ...).
|
|
9
|
+
// PERMANENT: these have no equivalent in an official design-system package
|
|
10
|
+
// (that's app-shell composition, not a design-system primitive) and are not
|
|
11
|
+
// deleted when primitives/ is.
|
|
12
|
+
export { default as GoabInput } from './lib/primitives/GoabInput.vue';
|
|
13
|
+
export { default as GoabTextarea } from './lib/primitives/GoabTextarea.vue';
|
|
14
|
+
export { default as GoabDropdown } from './lib/primitives/GoabDropdown.vue';
|
|
15
|
+
export { default as GoabCheckbox } from './lib/primitives/GoabCheckbox.vue';
|
|
16
|
+
export { default as GoabRadioGroup } from './lib/primitives/GoabRadioGroup.vue';
|
|
17
|
+
export { default as GoabButton } from './lib/primitives/GoabButton.vue';
|
|
18
|
+
export { default as GoabModal } from './lib/primitives/GoabModal.vue';
|
|
19
|
+
|
|
20
|
+
export { default as AppLayout } from './lib/patterns/AppLayout.vue';
|
|
21
|
+
export { default as AppHeader } from './lib/patterns/AppHeader.vue';
|
|
22
|
+
export { default as AppFooter } from './lib/patterns/AppFooter.vue';
|
|
23
|
+
export { default as SessionExpiredBanner } from './lib/patterns/SessionExpiredBanner.vue';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<goa-app-footer>
|
|
3
|
+
<goa-app-footer-nav-section slot="nav" heading="Resources">
|
|
4
|
+
<a href="https://alberta.ca/services.aspx">Services</a>
|
|
5
|
+
<a href="https://alberta.ca/contact.aspx">Contact</a>
|
|
6
|
+
<a href="https://alberta.ca/terms-of-use.aspx">Terms of Use</a>
|
|
7
|
+
</goa-app-footer-nav-section>
|
|
8
|
+
<goa-app-footer-meta-section slot="meta">
|
|
9
|
+
<a href="https://alberta.ca/security-and-privacy.aspx">Privacy</a>
|
|
10
|
+
<a href="https://alberta.ca/disclaimer.aspx">Disclaimer</a>
|
|
11
|
+
<a href="https://alberta.ca/accessibility.aspx">Accessibility</a>
|
|
12
|
+
</goa-app-footer-meta-section>
|
|
13
|
+
</goa-app-footer>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useSlots } from 'vue';
|
|
3
|
+
|
|
4
|
+
defineProps<{ heading: string }>();
|
|
5
|
+
const slots = useSlots();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<goa-microsite-header type="alpha" />
|
|
10
|
+
<goa-app-header url="/" :heading="heading">
|
|
11
|
+
<!-- goa-app-header only renders content placed in a named slot — the
|
|
12
|
+
"utilities" slot is where GoA's own design system docs put
|
|
13
|
+
account/sign-in actions (as opposed to "navigation" for nav links).
|
|
14
|
+
A bare child with no slot attribute renders nothing. -->
|
|
15
|
+
<div v-if="slots.utilities" slot="utilities">
|
|
16
|
+
<slot name="utilities" />
|
|
17
|
+
</div>
|
|
18
|
+
</goa-app-header>
|
|
19
|
+
</template>
|
|
@@ -14,6 +14,7 @@ withDefaults(defineProps<{ variant?: 'page' | 'wide' | 'form' }>(), {
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
+
<a class="skip-link" href="#main-content">Skip to main content</a>
|
|
17
18
|
<main id="main-content">
|
|
18
19
|
<div :class="`${variant}-content`">
|
|
19
20
|
<slot />
|
|
@@ -22,6 +23,22 @@ withDefaults(defineProps<{ variant?: 'page' | 'wide' | 'form' }>(), {
|
|
|
22
23
|
</template>
|
|
23
24
|
|
|
24
25
|
<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
|
+
|
|
25
42
|
.page-content,
|
|
26
43
|
.wide-content,
|
|
27
44
|
.form-content {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Presentational only — flipping `show` is the consuming app's job, not this
|
|
3
|
+
// component's. vue-app wires it to its session store's `expired` flag, itself
|
|
4
|
+
// set from main.ts's Keycloak plugin `onAuthRefreshError` hook — NOT
|
|
5
|
+
// `onAuthLogout`, which keycloak-js only fires via the session-status iframe
|
|
6
|
+
// (typically disabled, e.g. via checkLoginIframe: false) or Cordova mode, so
|
|
7
|
+
// it would never fire in a normal web deployment. onAuthRefreshError fires
|
|
8
|
+
// from the wrapper's own background auto-refresh failing because the refresh
|
|
9
|
+
// token itself expired — the real, always-on signal.
|
|
10
|
+
import GoabButton from '../primitives/GoabButton.vue';
|
|
11
|
+
|
|
12
|
+
const show = defineModel<boolean>('show', { default: false });
|
|
13
|
+
const emit = defineEmits<{ signIn: []; dismiss: [] }>();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div v-if="show" class="session-expired-banner">
|
|
18
|
+
<goa-callout type="emergency" heading="Session expired">
|
|
19
|
+
<p>Your session has expired. Sign in again to continue.</p>
|
|
20
|
+
<goa-spacer vspacing="s" />
|
|
21
|
+
<goa-button-group gap="relaxed">
|
|
22
|
+
<GoabButton type="primary" @click="emit('signIn')">Sign in</GoabButton>
|
|
23
|
+
<GoabButton type="tertiary" @click="emit('dismiss')">Dismiss</GoabButton>
|
|
24
|
+
</goa-button-group>
|
|
25
|
+
</goa-callout>
|
|
26
|
+
<goa-spacer vspacing="m" />
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import * as lib from './index';
|
|
3
3
|
|
|
4
|
-
// Smoke test: the barrel resolves and every wrapper SFC compiles under
|
|
5
|
-
// runner. It also gives the library a test target with real content,
|
|
6
|
-
// `nx test vue-components` doesn't fail on "no test files found".
|
|
4
|
+
// Smoke test: the barrel resolves and every wrapper/pattern SFC compiles under
|
|
5
|
+
// the test runner. It also gives the library a test target with real content,
|
|
6
|
+
// so `nx test vue-components` doesn't fail on "no test files found".
|
|
7
7
|
describe('vue-components', () => {
|
|
8
|
-
it('exports every GoA wrapper', () => {
|
|
8
|
+
it('exports every GoA primitive wrapper', () => {
|
|
9
9
|
for (const name of [
|
|
10
10
|
'GoabInput',
|
|
11
11
|
'GoabTextarea',
|
|
@@ -18,4 +18,15 @@ describe('vue-components', () => {
|
|
|
18
18
|
expect(lib[name as keyof typeof lib]).toBeTruthy();
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
it('exports every app-shell pattern component', () => {
|
|
23
|
+
for (const name of [
|
|
24
|
+
'AppLayout',
|
|
25
|
+
'AppHeader',
|
|
26
|
+
'AppFooter',
|
|
27
|
+
'SessionExpiredBanner',
|
|
28
|
+
]) {
|
|
29
|
+
expect(lib[name as keyof typeof lib]).toBeTruthy();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
21
32
|
});
|
|
@@ -23,7 +23,7 @@ describe('Vue Components Generator', () => {
|
|
|
23
23
|
const config = readProjectConfiguration(host, 'vue-components');
|
|
24
24
|
expect(config.root).toBe('libs/vue-components');
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const primitives = 'libs/vue-components/src/lib/primitives';
|
|
27
27
|
for (const name of [
|
|
28
28
|
'GoabInput',
|
|
29
29
|
'GoabTextarea',
|
|
@@ -33,10 +33,21 @@ describe('Vue Components Generator', () => {
|
|
|
33
33
|
'GoabButton',
|
|
34
34
|
'GoabModal',
|
|
35
35
|
]) {
|
|
36
|
-
expect(host.exists(`${
|
|
36
|
+
expect(host.exists(`${primitives}/${name}.vue`)).toBeTruthy();
|
|
37
|
+
}
|
|
38
|
+
// Permanent app-shell pattern components live alongside the interim wrappers.
|
|
39
|
+
const patterns = 'libs/vue-components/src/lib/patterns';
|
|
40
|
+
for (const name of [
|
|
41
|
+
'AppLayout',
|
|
42
|
+
'AppHeader',
|
|
43
|
+
'AppFooter',
|
|
44
|
+
'SessionExpiredBanner',
|
|
45
|
+
]) {
|
|
46
|
+
expect(host.exists(`${patterns}/${name}.vue`)).toBeTruthy();
|
|
37
47
|
}
|
|
38
48
|
const index = host.read('libs/vue-components/src/index.ts').toString();
|
|
39
49
|
expect(index).toContain('export { default as GoabInput }');
|
|
50
|
+
expect(index).toContain('export { default as AppLayout }');
|
|
40
51
|
expect(index).toContain('@abgov/vue-components'); // interim marker
|
|
41
52
|
|
|
42
53
|
// Ships a spec so the vitest test target isn't empty (vitest exits non-zero
|
|
@@ -79,7 +90,7 @@ describe('Vue Components Generator', () => {
|
|
|
79
90
|
await generator(host);
|
|
80
91
|
await expect(generator(host)).resolves.not.toThrow();
|
|
81
92
|
expect(
|
|
82
|
-
host.exists('libs/vue-components/src/lib/GoabInput.vue'),
|
|
93
|
+
host.exists('libs/vue-components/src/lib/primitives/GoabInput.vue'),
|
|
83
94
|
).toBeTruthy();
|
|
84
95
|
}, 30000);
|
|
85
96
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|