@abgov/nx-adsp 13.31.0 → 13.31.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.
Files changed (69) hide show
  1. package/README.md +41 -32
  2. package/package.json +1 -1
  3. package/src/generators/angular-app/angular-app.js +2 -2
  4. package/src/generators/angular-app/files/AGENTS.md__tmpl__ +16 -0
  5. package/src/generators/express-service/express-service.spec.ts +26 -13
  6. package/src/generators/express-service/schema.json +1 -1
  7. package/src/generators/react-app/files/AGENTS.md__tmpl__ +16 -0
  8. package/src/generators/react-app/react-app.js +2 -2
  9. package/src/generators/vue-admin-crud/files/src/views/__editViewFileName__.vue__tmpl__ +3 -5
  10. package/src/generators/vue-admin-crud/files/src/views/__listViewFileName__.vue__tmpl__ +33 -4
  11. package/src/generators/vue-admin-crud/schema.d.ts +2 -0
  12. package/src/generators/vue-admin-crud/schema.json +7 -8
  13. package/src/generators/vue-admin-crud/vue-admin-crud.js +6 -1
  14. package/src/generators/vue-admin-crud/vue-admin-crud.js.map +1 -1
  15. package/src/generators/vue-admin-crud/vue-admin-crud.spec.ts +96 -13
  16. package/src/generators/vue-app/files/AGENTS.md__tmpl__ +56 -11
  17. package/src/generators/vue-app/files/src/App.vue__tmpl__ +2 -1
  18. package/src/generators/vue-app/files/src/composables/useApi.spec.ts__tmpl__ +5 -3
  19. package/src/generators/vue-app/files/src/composables/useApi.ts__tmpl__ +14 -2
  20. package/src/generators/vue-app/vue-app.js +33 -6
  21. package/src/generators/vue-app/vue-app.js.map +1 -1
  22. package/src/generators/vue-app/vue-app.spec.ts +133 -13
  23. package/src/generators/vue-components/files/AGENTS.md__tmpl__ +36 -17
  24. package/src/generators/vue-components/files/src/index.ts__tmpl__ +3 -0
  25. package/src/generators/vue-components/files/src/lib/formatters.ts__tmpl__ +58 -0
  26. package/src/generators/vue-components/files/src/lib/patterns/AppSideMenu.vue__tmpl__ +14 -3
  27. package/src/generators/vue-components/files/src/lib/patterns/FilterBar.vue__tmpl__ +7 -1
  28. package/src/generators/vue-components/files/src/lib/patterns/WorkspaceTable.vue__tmpl__ +7 -1
  29. package/src/generators/vue-components/vue-components.d.ts +1 -0
  30. package/src/generators/vue-components/vue-components.js +18 -39
  31. package/src/generators/vue-components/vue-components.js.map +1 -1
  32. package/src/generators/vue-components/vue-components.spec.ts +94 -2
  33. package/src/generators/vue-detail-view/files/src/views/__viewFileName__.vue__tmpl__ +51 -2
  34. package/src/generators/vue-detail-view/schema.d.ts +18 -0
  35. package/src/generators/vue-detail-view/schema.json +2 -2
  36. package/src/generators/vue-detail-view/vue-detail-view.js +4 -1
  37. package/src/generators/vue-detail-view/vue-detail-view.js.map +1 -1
  38. package/src/generators/vue-detail-view/vue-detail-view.spec.ts +36 -6
  39. package/src/generators/vue-intake-view/files/shared/src/views/__confirmationViewFileName__.vue__tmpl__ +60 -6
  40. package/src/generators/vue-intake-view/files/shared/src/views/__reviewViewFileName__.vue__tmpl__ +37 -1
  41. package/src/generators/vue-intake-view/files/steps/src/views/__stepViewFileName__.vue__tmpl__ +1 -1
  42. package/src/generators/vue-intake-view/schema.d.ts +10 -0
  43. package/src/generators/vue-intake-view/schema.json +12 -9
  44. package/src/generators/vue-intake-view/vue-intake-view.js +9 -3
  45. package/src/generators/vue-intake-view/vue-intake-view.js.map +1 -1
  46. package/src/generators/vue-intake-view/vue-intake-view.spec.ts +113 -12
  47. package/src/generators/vue-workspace-view/files/src/views/__viewFileName__.vue__tmpl__ +49 -1
  48. package/src/generators/vue-workspace-view/schema.d.ts +22 -0
  49. package/src/generators/vue-workspace-view/schema.json +8 -9
  50. package/src/generators/vue-workspace-view/vue-workspace-view.js +7 -1
  51. package/src/generators/vue-workspace-view/vue-workspace-view.js.map +1 -1
  52. package/src/generators/vue-workspace-view/vue-workspace-view.spec.ts +73 -7
  53. package/src/utils/paired-project.d.ts +10 -0
  54. package/src/utils/paired-project.js +125 -18
  55. package/src/utils/paired-project.js.map +1 -1
  56. package/src/utils/paired-project.spec.ts +126 -0
  57. package/src/utils/quality.js +43 -9
  58. package/src/utils/quality.js.map +1 -1
  59. package/src/utils/quality.spec.ts +35 -2
  60. package/src/utils/vue-eslint.d.ts +2 -0
  61. package/src/utils/vue-eslint.js +48 -0
  62. package/src/utils/vue-eslint.js.map +1 -0
  63. package/src/utils/vue-side-menu.d.ts +13 -1
  64. package/src/utils/vue-side-menu.js +50 -5
  65. package/src/utils/vue-side-menu.js.map +1 -1
  66. package/src/utils/vue-side-menu.spec.ts +100 -0
  67. package/src/utils/vue-tsconfig.d.ts +26 -0
  68. package/src/utils/vue-tsconfig.js +49 -0
  69. package/src/utils/vue-tsconfig.js.map +1 -0
@@ -232,7 +232,9 @@ describe('Vue App Generator', () => {
232
232
  // nav order, grouping and labels are the owning team's decisions.
233
233
  expect(app).toContain(':primary-items="primaryItems"');
234
234
  expect(app).toContain('const primaryItems = [');
235
- expect(app).toContain("{ label: 'Home', to: '/' },");
235
+ // The icon is not decoration: goa-work-side-menu-item renders a blank
236
+ // item without one.
237
+ expect(app).toContain("{ label: 'Home', to: '/', icon: 'home' },");
236
238
  expect(app).toContain('@item-click="onItemClick"');
237
239
  // The content gutter is shared regardless of shell choice.
238
240
  expect(app).toContain('<AppLayout');
@@ -327,7 +329,12 @@ describe('Vue App Generator', () => {
327
329
  // response-envelope keys get inlined into every generated view.
328
330
  expect(useApi).toContain('apiConvention');
329
331
  expect(useApi).toContain('THE GLUE LAYER');
330
- for (const method of ['function list', 'function get', 'function save', 'function action']) {
332
+ for (const method of [
333
+ 'function list',
334
+ 'function get',
335
+ 'function save',
336
+ 'function action',
337
+ ]) {
331
338
  expect(useApi).toContain(method);
332
339
  }
333
340
  // The documented escape hatch for a limit/offset backend.
@@ -443,7 +450,9 @@ describe('Vue App Generator', () => {
443
450
  .read('libs/vue-components/src/lib/patterns/AppHeader.vue')
444
451
  .toString();
445
452
  expect(appHeader).toContain('version="2"');
446
- expect(appHeader).toMatch(/<div[^>]*slot="utilities"[^>]*>[\s\S]*<slot name="utilities"/);
453
+ expect(appHeader).toMatch(
454
+ /<div[^>]*slot="utilities"[^>]*>[\s\S]*<slot name="utilities"/,
455
+ );
447
456
 
448
457
  const app = host.read('apps/test/src/App.vue').toString();
449
458
  expect(app).toMatch(/<template #utilities>[\s\S]*<goa-button-group/);
@@ -501,17 +510,36 @@ describe('Vue App Generator', () => {
501
510
  expect(nginxConf).toContain('http://test-service2:3333/');
502
511
  });
503
512
 
504
- it('writes vite dev proxy config when proxy is configured', async () => {
513
+ // Regression: this was a JSON file carrying `pathRewrite`, which is
514
+ // webpack-dev-server syntax. Vite has no such key and ignores unknown ones
515
+ // silently, so the dev server proxied to the right host and forwarded the path
516
+ // unrewritten -- every generated API call 404'd, with no warning. The fix has
517
+ // to be a module because the thing Vite needs is a function.
518
+ it('writes an executable vite dev proxy module whose rewrite actually rewrites', async () => {
505
519
  await generator(host, {
506
520
  ...options,
507
521
  proxy: { location: '/test/', proxyPass: 'http://test-service:3333/api/' },
508
522
  });
509
- expect(host.exists('apps/test/vite.proxy.json')).toBeTruthy();
510
- const proxyConf = JSON.parse(
511
- host.read('apps/test/vite.proxy.json').toString(),
512
- );
513
- expect(proxyConf['/test/'].target).toBe('http://localhost:3333');
514
- expect(proxyConf['/test/'].pathRewrite['^/test/']).toBe('/api/');
523
+ expect(host.exists('apps/test/vite.proxy.cjs')).toBeTruthy();
524
+ // The inert JSON form must not be left beside it.
525
+ expect(host.exists('apps/test/vite.proxy.json')).toBeFalsy();
526
+
527
+ const source = host.read('apps/test/vite.proxy.cjs').toString();
528
+ expect(source).not.toContain('pathRewrite');
529
+
530
+ // Evaluate it the way @nx/vite does (require) and exercise the function,
531
+ // rather than asserting on the text of a config that has to *behave*.
532
+ const module = { exports: {} };
533
+ new Function('module', 'exports', source)(module, module.exports);
534
+ const proxy = module.exports as Record<
535
+ string,
536
+ { target: string; rewrite: (path: string) => string }
537
+ >;
538
+ expect(proxy['/test/'].target).toBe('http://localhost:3333');
539
+ expect(typeof proxy['/test/'].rewrite).toBe('function');
540
+ expect(proxy['/test/'].rewrite('/test/v1/things')).toBe('/api/v1/things');
541
+ // Only the leading location is replaced, not a later occurrence of it.
542
+ expect(proxy['/test/'].rewrite('/test/a/test/b')).toBe('/api/a/test/b');
515
543
  });
516
544
 
517
545
  it('derives the nginx/dev proxy and the sandbox tag from --pairedProject alone', async () => {
@@ -523,10 +551,19 @@ describe('Vue App Generator', () => {
523
551
  const nginxConf = host.read('apps/test/public/nginx.conf').toString();
524
552
  expect(nginxConf).toContain('http://test-service:3333/test-service/');
525
553
 
526
- const proxyConf = JSON.parse(
527
- host.read('apps/test/vite.proxy.json').toString(),
554
+ const source = host.read('apps/test/vite.proxy.cjs').toString();
555
+ const module = { exports: {} };
556
+ new Function('module', 'exports', source)(module, module.exports);
557
+ const proxy = module.exports as Record<
558
+ string,
559
+ { target: string; rewrite: (path: string) => string }
560
+ >;
561
+ expect(proxy['/api/'].target).toBe('http://localhost:3333');
562
+ // /api/v1/... must reach the service's own mount point, which is what
563
+ // useApi's base path is built against.
564
+ expect(proxy['/api/'].rewrite('/api/v1/claims')).toBe(
565
+ '/test-service/v1/claims',
528
566
  );
529
- expect(proxyConf['/api/'].target).toBe('http://localhost:3333');
530
567
 
531
568
  const config = readProjectConfiguration(host, 'test');
532
569
  expect(config.tags).toContain('adsp:proxy-service:test-service:3333');
@@ -565,4 +602,87 @@ describe('Vue App Generator', () => {
565
602
  expect(nginxConf).toContain('http://test-service:3333/test-service/');
566
603
  expect(nginxConf).toContain('http://other-service:9000/');
567
604
  });
605
+
606
+ // Regression: the shell projects into goa-one-column-layout's native `header`
607
+ // and `footer` slots. The rule was only turned off for the vue-components lib,
608
+ // so when the shell adopted the element `nx lint` began failing on unmodified
609
+ // generator output -- two `vue/no-deprecated-slot-attribute` errors in the
610
+ // generator's own App.vue. Asserted for both layouts: the attribute is
611
+ // legitimate custom-element usage in either shell, and scoping the override to
612
+ // the one template that happens to emit it today is what caused the bug.
613
+ it.each(['header', 'internal'] as const)(
614
+ 'disables vue/no-deprecated-slot-attribute for a %s-layout app',
615
+ async (layout) => {
616
+ await generator(host, { ...options, layout });
617
+ // create-nx-workspace's current default is flat config, so that is what
618
+ // @nx/vue writes for the app; read whichever is actually in effect rather
619
+ // than assuming a filename.
620
+ const config =
621
+ host.read('apps/test/eslint.config.mjs')?.toString() ??
622
+ host.read('apps/test/.eslintrc.json')?.toString() ??
623
+ '';
624
+ // Quote style differs between the flat-config writer and the legacy JSON
625
+ // one, so match the rule and its value together rather than a literal.
626
+ expect(config).toMatch(
627
+ /vue\/no-deprecated-slot-attribute["']?\s*:\s*["']off["']/,
628
+ );
629
+ },
630
+ );
631
+
632
+ it('emits native slot attributes the disabled rule would otherwise flag', async () => {
633
+ await generator(host, { ...options, layout: 'header' });
634
+ const app = host.read('apps/test/src/App.vue')?.toString() ?? '';
635
+ expect(app).toContain('slot="header"');
636
+ expect(app).toContain('slot="footer"');
637
+ });
638
+
639
+ // Regression: the generated guidance hardcoded `apps/` and `libs/` prefixes. In a
640
+ // TS-solution workspace projects sit at the workspace root, so every path in the
641
+ // AGENTS.md an agent is told to read was wrong. Asserted against the roots actually
642
+ // in the tree rather than a literal, so this holds under either layout.
643
+ it('points its guidance at the real project roots, not hardcoded apps//libs/', async () => {
644
+ addProjectConfiguration(host, 'test-service', {
645
+ root: 'apps/test-service',
646
+ });
647
+ await generator(host, { ...options, pairedProject: 'test-service' });
648
+
649
+ const agents = host.read('apps/test/AGENTS.md')?.toString() ?? '';
650
+ const libRoot = readProjectConfiguration(host, 'vue-components').root;
651
+ const serviceRoot = readProjectConfiguration(host, 'test-service').root;
652
+
653
+ expect(agents).toContain(`${libRoot}/src/lib/primitives/`);
654
+ expect(agents).toContain(`${serviceRoot}/AGENTS.md`);
655
+ // No path may name the lib without going through its resolved root.
656
+ const strayLibPaths = agents
657
+ .split('\n')
658
+ .filter(
659
+ (line) =>
660
+ line.includes('vue-components/src/') && !line.includes(libRoot),
661
+ );
662
+ expect(strayLibPaths).toEqual([]);
663
+ });
664
+
665
+ it('documents the options that decide what a person sees', async () => {
666
+ await generator(host, options);
667
+ const agents = host.read('apps/test/AGENTS.md')?.toString() ?? '';
668
+ // These live in the vue-components catalog too, but the "Adding a view"
669
+ // decision table is where a generator gets chosen -- an agent that never
670
+ // opens the lib's own docs still has to be told to pass them.
671
+ for (const option of [
672
+ 'badgeMap',
673
+ 'referenceField',
674
+ '--icon',
675
+ 'optionLabel',
676
+ ]) {
677
+ expect(agents).toContain(option);
678
+ }
679
+ });
680
+
681
+ it('feeds the signed-in user into the side menu profile button', async () => {
682
+ await generator(host, { ...options, layout: 'internal' });
683
+ const app = host.read('apps/test/src/App.vue')?.toString() ?? '';
684
+ // AppSideMenu falls back to a generic name, but the real one has to reach it
685
+ // or an authenticated user's own menu never shows who they are.
686
+ expect(app).toContain(':user-name="kc.fullName"');
687
+ });
568
688
  });
@@ -8,24 +8,20 @@ invoked automatically by `vue-app` and by every `vue-*-view` generator.
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`, `FilterBar`) | **Permanent** |
11
- | `src/lib/formatters.ts` | Shared value formatting for views (`formatDate`, `formatDateTime`, `formatNumber`, `formatPercent`) | **Permanent** |
11
+ | `src/lib/formatters.ts` | Shared value formatting for views (`formatDate`, `formatDateTime`, `formatNumber`, `formatPercent`, `optionLabel`, `badgeType`) | **Permanent** |
12
12
 
13
- **Not every pattern component is present in every app.** `AppLayout`, `AppHeader`,
14
- `AppFooter`, `AppSideMenu`, and `SessionExpiredBanner` are base app-shell `vue-app`
15
- provisions and wires all five into `App.vue` for every generated app, unconditionally.
16
- The rest are on-demand: they only exist in a given app's `src/lib/patterns/` if that
17
- app has run the matching view generator at least once.
13
+ **Every pattern component listed above is present in every app.** This lib is generated
14
+ in one shot, so all of `primitives/` and all of `patterns/` exist as soon as any Vue app
15
+ has been generated there is no per-component provisioning step and nothing to check for
16
+ before building on one.
18
17
 
19
- | Component | Provisioned by |
20
- |---|---|
21
- | `RecordDetailShell` | `vue-detail-view` |
22
- | `WorkspaceTable` | `vue-workspace-view`, `vue-admin-crud` |
23
- | `Stepper`, `StepErrorSummary` | `vue-intake-view` |
24
-
25
- Don't assume one of these on-demand components exists in a given app check whether
26
- its generator has actually been run (e.g. `nx list @abgov/nx-adsp` won't tell you;
27
- look for the component file itself, or the view file that would import it) before
28
- building on it.
18
+ Which components are *wired up* does vary: `vue-app` mounts the shell components
19
+ (`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`) in
20
+ `App.vue`, while the rest are imported by whichever view generator uses them —
21
+ `RecordDetailShell` by `vue-detail-view`, `WorkspaceTable` by `vue-workspace-view` and
22
+ `vue-admin-crud`, `Stepper`/`StepErrorSummary` by `vue-intake-view`, `FilterBar` by a
23
+ filterable workspace view. An unused component is idle, not missing: import it directly
24
+ in a hand-authored view rather than running a generator to "provision" it.
29
25
 
30
26
  > **⚠️ `primitives/` is interim — do not invest in it as permanent.** It exists
31
27
  > only because GoA DS has not yet published an official Vue wrapper package. When
@@ -64,8 +60,9 @@ inline styles.
64
60
  | A styled status pill | `<goa-badge type="…">` | |
65
61
  | A hand-built alert/notice box | `<goa-callout type="…" heading="…">` | |
66
62
  | A loading `<div>` or spinner markup | `<goa-skeleton type="…">` or `<goa-spinner>` | Skeletons match the shape they replace (`card`, `text`, `table`) |
67
- | A `<label>` + error `<span>` around an input | `<goa-form-item label="…" error="…">` | Wrap the `Goab*` input in this, don't restyle it |
63
+ | A `<label>` + error `<span>` around an input | `<goa-form-item label="…" error="…" mb="l">` | Wrap the `Goab*` input in this, don't restyle it. `mb` defaults to none, so stacked items sit flush against each other — pass `mb="l"` (the design system's own convention for vertical forms). Omit it when the items are inside a `goa-block`, which supplies its own gap |
68
64
  | A row of buttons with flex styling | `<goa-button-group alignment="…">` | |
65
+ | A hand-built multi-select (checkbox list, chips) | `<goa-dropdown-multiselect>` | Added in web-components 2.4.0 |
69
66
  | A hand-built filter row above a table | `<FilterBar>` (this library) | Controls, collapse, active-filter chips and clear-all. It emits a query-ready values object; the *view* owns resetting the page, debouncing, and URL sync |
70
67
 
71
68
  Also available bare and worth knowing before hand-rolling: `goa-accordion`, `goa-details`,
@@ -84,6 +81,28 @@ For values rather than layout, use `formatters.ts` (`formatDate`, `formatDateTim
84
81
  the same way, and each formatter already returns an em dash for a null/unparseable value so a
85
82
  partially-populated record needs no per-field guard.
86
83
 
84
+ ### Never render a stored code to a person
85
+
86
+ Two more formatters exist because rendering a *coded* value is where generated views have most often
87
+ been wrong, and the failure is invisible to a unit test — it only shows up when someone reads the
88
+ screen.
89
+
90
+ | Don't | Do |
91
+ | --- | --- |
92
+ | `{{ record['species'] }}` → shows `cattle-mature` | `{{ optionLabel(fieldOptions['species'], record['species']) }}` → `Cattle (mature)` |
93
+ | `<goa-badge type="information" :content="row['status']" />` — every status the same neutral blue | `<goa-badge :type="badgeType(columnBadgeTypes['status'], row['status'])" :content="optionLabel(...)" />` |
94
+
95
+ `optionLabel` falls back to the raw value for an unmapped code (a data or config problem you want to
96
+ see, not hide); `badgeType` falls back to `information`, which is the honest colour for a value whose
97
+ severity nobody declared.
98
+
99
+ The view generators build these maps for you — but only from what you pass them. Supply `options` on
100
+ any `--fields`/`--columns` entry whose stored value is a code, and `badgeMap` on a `badge` entry, e.g.
101
+ `badgeMap: { approved: 'success', declined: 'emergency' }`. Omit them and the view renders the code.
102
+ A badge's colour is the fastest thing a reader takes in, so a declined claim in neutral blue reads as
103
+ routine — this matters most on a check-your-answers page, whose entire purpose is that the person can
104
+ read back what they entered.
105
+
87
106
  ## What these wrappers do (and why they're needed)
88
107
 
89
108
  `goa-*` are framework-agnostic custom elements (built with Svelte). Three quirks
@@ -37,8 +37,11 @@ export {
37
37
  fromIsoDateString,
38
38
  toIsoDateString,
39
39
  formatCurrency,
40
+ optionLabel,
41
+ badgeType,
40
42
  formatDate,
41
43
  formatDateTime,
42
44
  formatNumber,
43
45
  formatPercent,
44
46
  } from './lib/formatters';
47
+ export type { DisplayOption, BadgeType } from './lib/formatters';
@@ -141,3 +141,61 @@ export function formatPercent(
141
141
  }).format(value);
142
142
  return `${rounded}%`;
143
143
  }
144
+
145
+ /** One choice in a coded set -- the shape a generator's `options` list uses. */
146
+ export interface DisplayOption {
147
+ value: string;
148
+ label: string;
149
+ }
150
+
151
+ /**
152
+ * The human label for a coded value, e.g. 'cattle-mature' -> 'Cattle (mature)'.
153
+ *
154
+ * Every view that renders a value from a `select`/dropdown field must go through
155
+ * this. The generators are handed the label for each value at generation time,
156
+ * and rendering the bare value instead puts raw domain codes ('northwest',
157
+ * 'wolf') in front of the person filling the form -- most damagingly on a
158
+ * check-your-answers page, where the whole point is that they can read back what
159
+ * they entered.
160
+ *
161
+ * Falls back to the value itself, not to a dash: an unmapped code is a data or
162
+ * configuration problem, and showing it is far more debuggable than hiding it.
163
+ */
164
+ export function optionLabel(
165
+ options: readonly DisplayOption[] | undefined,
166
+ value: unknown,
167
+ ): string {
168
+ if (value === null || value === undefined || value === '') return '—';
169
+ const match = options?.find((option) => option.value === String(value));
170
+ return match ? match.label : String(value);
171
+ }
172
+
173
+ /** goa-badge's `type`, which carries the semantic colour. */
174
+ export type BadgeType =
175
+ | 'information'
176
+ | 'success'
177
+ | 'important'
178
+ | 'emergency'
179
+ | 'dark'
180
+ | 'midtone'
181
+ | 'light';
182
+
183
+ /**
184
+ * The badge type for a status value, from a value -> type map.
185
+ *
186
+ * A badge's colour is the fastest thing a reader takes in, so a status rendered
187
+ * with the wrong one actively misinforms: a declined claim in neutral blue reads
188
+ * as routine. Views pass a map (supplied per column/field at generation time)
189
+ * rather than a hardcoded type.
190
+ *
191
+ * Falls back to 'information' -- neutral, which is the honest choice for a value
192
+ * whose severity we were not told.
193
+ */
194
+ export function badgeType(
195
+ map: Readonly<Record<string, BadgeType>> | undefined,
196
+ value: unknown,
197
+ fallback: BadgeType = 'information',
198
+ ): BadgeType {
199
+ if (value === null || value === undefined || value === '') return fallback;
200
+ return map?.[String(value)] ?? fallback;
201
+ }
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { useSlots } from 'vue';
2
+ import { computed, useSlots } from 'vue';
3
3
 
4
4
  // The alternate top-level shell for staff-facing/internal apps (goa-work-side-menu),
5
5
  // used instead of AppHeader/AppFooter -- not together with them. Presentational only:
@@ -23,7 +23,7 @@ interface AppSideMenuItem {
23
23
  current?: boolean;
24
24
  }
25
25
 
26
- withDefaults(
26
+ const props = withDefaults(
27
27
  defineProps<{
28
28
  heading?: string;
29
29
  userName?: string;
@@ -38,13 +38,24 @@ withDefaults(
38
38
  },
39
39
  );
40
40
 
41
+ // goa-work-side-menu renders its profile button whenever the `account` slot has
42
+ // content, and that button's ONLY accessible name is `user-name` — the element
43
+ // exposes no aria-label prop, and the name also feeds the icon's own label. Left
44
+ // empty it is an unnamed button (axe `button-name`, critical) plus an unlabelled
45
+ // icon (`role-img-alt`), on every route of the app.
46
+ //
47
+ // Resolved here rather than defaulted via withDefaults: a default only applies to
48
+ // `undefined`, and the realistic case is a caller passing a signed-out user's
49
+ // empty-string name. Trimmed so whitespace doesn't read as a name either.
50
+ const profileName = computed(() => props.userName?.trim() || 'Account');
51
+
41
52
  const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
42
53
  </script>
43
54
 
44
55
  <template>
45
56
  <div class="app-side-menu">
46
57
  <a class="skip-link" href="#main-content">Skip to main content</a>
47
- <goa-work-side-menu :heading="heading" :user-name="userName">
58
+ <goa-work-side-menu :heading="heading" :user-name="profileName">
48
59
  <goa-work-side-menu-item
49
60
  v-for="item in primaryItems"
50
61
  :key="item.label"
@@ -78,6 +78,10 @@ const dateValues = computed(() =>
78
78
  ),
79
79
  );
80
80
 
81
+ // Takes a plain string, never undefined: an unset filter is '', which is what
82
+ // clearAll() resets to and what the dropdown renders as its "Any" option. The
83
+ // dropdown wrapper's defineModel<string>() emits string | undefined, so its
84
+ // handler normalises before calling this rather than widening the shape here.
81
85
  function set(key: string, value: string) {
82
86
  model.value = { ...model.value, [key]: value };
83
87
  }
@@ -109,7 +113,9 @@ function clearAll() {
109
113
  v-if="filter.type === 'dropdown'"
110
114
  :model-value="model[filter.key] ?? ''"
111
115
  :name="filter.key"
112
- @update:model-value="(value: string) => set(filter.key, value)"
116
+ @update:model-value="
117
+ (value: string | undefined) => set(filter.key, value ?? '')
118
+ "
113
119
  >
114
120
  <goa-dropdown-item value="" :label="filter.anyLabel ?? 'Any'" />
115
121
  <goa-dropdown-item
@@ -143,7 +143,13 @@ function onNativeSort(event: Event) {
143
143
  :pagenumber="page"
144
144
  :itemcount="itemCount"
145
145
  :perpagecount="perPageCount"
146
- @_change="(e) => emit('pageChange', (e as CustomEvent<{ page: number }>).detail?.page ?? 1)"
146
+ @_change="
147
+ (e: Event) =>
148
+ emit(
149
+ 'pageChange',
150
+ (e as CustomEvent<{ page: number }>).detail?.page ?? 1,
151
+ )
152
+ "
147
153
  />
148
154
  </template>
149
155
  </div>
@@ -1,5 +1,6 @@
1
1
  import { Tree } from '@nx/devkit';
2
2
  export declare function ensurePackageExports(host: Tree, libRoot: string): void;
3
3
  export declare const LIB_NAME = "vue-components";
4
+ export declare function vueComponentsLibRoot(host: Tree): string;
4
5
  export declare function vueComponentsImportPath(host: Tree): string;
5
6
  export default function (host: Tree): Promise<void>;
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LIB_NAME = void 0;
4
4
  exports.ensurePackageExports = ensurePackageExports;
5
+ exports.vueComponentsLibRoot = vueComponentsLibRoot;
5
6
  exports.vueComponentsImportPath = vueComponentsImportPath;
6
7
  exports.default = default_1;
7
8
  const tslib_1 = require("tslib");
8
9
  const devkit_1 = require("@nx/devkit");
9
- const internal_1 = require("@nx/eslint/internal");
10
10
  const path = require("path");
11
+ const vue_eslint_1 = require("../../utils/vue-eslint");
12
+ const vue_tsconfig_1 = require("../../utils/vue-tsconfig");
11
13
  // Backfills package.json module-resolution fields for the library. In a TS-solution
12
14
  // workspace @nx/vue's library resolves via package.json `exports`, but its
13
15
  // programmatic default (useProjectJson skips writing them here) leaves them off, so
@@ -32,48 +34,24 @@ function ensurePackageExports(host, libRoot) {
32
34
  return pkg;
33
35
  });
34
36
  }
35
- // Turns off vue/no-deprecated-slot-attribute for the lib. GoabModal projects into
36
- // goa-modal's native web-component slot via `slot="actions"` — legitimate custom-
37
- // element usage, not the deprecated Vue 2 component-slot syntax the rule targets.
38
- // The whole lib wraps web components, so it's scoped to the lib (consuming apps,
39
- // which use `<template #actions>`, are unaffected).
40
- //
41
- // Uses @nx/eslint's own override helpers rather than hand-editing a specific
42
- // file format: create-nx-workspace's current default is flat config
43
- // (eslint.config.mjs), which is a JS module exporting an array, not JSON — an
44
- // earlier version of this function only handled legacy .eslintrc.json, so it
45
- // silently no-op'd (just a console warning) on flat-config workspaces, and
46
- // `nx lint vue-components` failed for real on unmodified generator output.
47
- // addOverrideToLintConfig/lintConfigHasOverride detect flat vs legacy
48
- // internally (via @nx/eslint's own useFlatConfig check) and edit whichever is
49
- // actually in effect — AST-aware for flat config, not string splicing.
50
- function disableSlotAttributeRule(host, libRoot) {
51
- if (!(0, internal_1.isEslintConfigSupported)(host, libRoot)) {
52
- console.warn(`\n⚠ No ESLint config found for ${libRoot} — could not disable vue/no-deprecated-slot-attribute.\n` +
53
- ` GoabModal uses goa-modal's native \`slot\` attribute; if lint flags it, turn\n` +
54
- ` that rule off for this library.\n`);
55
- return;
56
- }
57
- // `files` is `string | string[]` per the Linter type — normalize before checking.
58
- const isVueOverride = (o) => (Array.isArray(o.files) ? o.files : o.files ? [o.files] : []).some((f) => f.includes('vue'));
59
- const alreadyDisabled = (0, internal_1.lintConfigHasOverride)(host, libRoot, (o) => {
60
- var _a;
61
- return isVueOverride(o) &&
62
- ((_a = o.rules) === null || _a === void 0 ? void 0 : _a['vue/no-deprecated-slot-attribute']) === 'off';
63
- });
64
- if (alreadyDisabled) {
65
- return;
66
- }
67
- (0, internal_1.addOverrideToLintConfig)(host, libRoot, {
68
- files: ['*.vue'],
69
- rules: { 'vue/no-deprecated-slot-attribute': 'off' },
70
- });
71
- }
72
37
  exports.LIB_NAME = 'vue-components';
73
38
  // Import specifier for the shared wrapper lib, derived from the workspace npm
74
39
  // scope so it mirrors the eventual official `@abgov/vue-components` (same tail —
75
40
  // swapping to it is a scope change). Falls back to a scope built from the root
76
41
  // package name when the workspace isn't scoped.
42
+ // Workspace-relative root of the shared wrapper lib, for docs that need to name a
43
+ // path rather than an import specifier. Reads the project graph first so it is
44
+ // correct in a TS-solution/flat workspace (where the lib sits at the root, not under
45
+ // libs/) and only falls back to the layout when the lib hasn't been generated yet.
46
+ function vueComponentsLibRoot(host) {
47
+ try {
48
+ return (0, devkit_1.readProjectConfiguration)(host, exports.LIB_NAME).root;
49
+ }
50
+ catch (_a) {
51
+ const { libsDir } = (0, devkit_1.getWorkspaceLayout)(host);
52
+ return libsDir && libsDir !== '.' ? `${libsDir}/${exports.LIB_NAME}` : exports.LIB_NAME;
53
+ }
54
+ }
77
55
  function vueComponentsImportPath(host) {
78
56
  var _a, _b;
79
57
  let name = 'workspace';
@@ -118,8 +96,9 @@ function default_1(host) {
118
96
  importPath: vueComponentsImportPath(host),
119
97
  skipFormat: true,
120
98
  });
121
- disableSlotAttributeRule(host, libRoot);
99
+ (0, vue_eslint_1.disableSlotAttributeRule)(host, libRoot, "GoabModal uses goa-modal's native `slot` attribute");
122
100
  ensurePackageExports(host, libRoot);
101
+ (0, vue_tsconfig_1.ensureBundlerModuleOption)(host, `${libRoot}/tsconfig.json`);
123
102
  }
124
103
  (0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), libRoot, { tmpl: '' });
125
104
  });
@@ -1 +1 @@
1
- {"version":3,"file":"vue-components.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-components/vue-components.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vue-components.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-components/vue-components.ts"],"names":[],"mappings":"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"}