@abgov/nx-adsp 13.30.0 → 13.31.1

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 (70) 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__ +1 -3
  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 +74 -13
  16. package/src/generators/vue-app/files/AGENTS.md__tmpl__ +56 -11
  17. package/src/generators/vue-app/files/src/App.vue__tmpl__ +18 -4
  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 +152 -17
  23. package/src/generators/vue-components/files/AGENTS.md__tmpl__ +35 -16
  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/AppLayout.spec.ts__tmpl__ +32 -0
  27. package/src/generators/vue-components/files/src/lib/patterns/AppLayout.vue__tmpl__ +28 -21
  28. package/src/generators/vue-components/files/src/lib/patterns/AppSideMenu.vue__tmpl__ +14 -3
  29. package/src/generators/vue-components/files/src/lib/patterns/FilterBar.vue__tmpl__ +7 -1
  30. package/src/generators/vue-components/files/src/lib/patterns/WorkspaceTable.vue__tmpl__ +7 -1
  31. package/src/generators/vue-components/vue-components.d.ts +1 -0
  32. package/src/generators/vue-components/vue-components.js +18 -39
  33. package/src/generators/vue-components/vue-components.js.map +1 -1
  34. package/src/generators/vue-components/vue-components.spec.ts +95 -2
  35. package/src/generators/vue-detail-view/files/src/views/__viewFileName__.vue__tmpl__ +51 -2
  36. package/src/generators/vue-detail-view/schema.d.ts +18 -0
  37. package/src/generators/vue-detail-view/schema.json +2 -2
  38. package/src/generators/vue-detail-view/vue-detail-view.js +4 -1
  39. package/src/generators/vue-detail-view/vue-detail-view.js.map +1 -1
  40. package/src/generators/vue-detail-view/vue-detail-view.spec.ts +36 -6
  41. package/src/generators/vue-intake-view/files/shared/src/views/__confirmationViewFileName__.vue__tmpl__ +60 -6
  42. package/src/generators/vue-intake-view/files/shared/src/views/__reviewViewFileName__.vue__tmpl__ +21 -0
  43. package/src/generators/vue-intake-view/schema.d.ts +10 -0
  44. package/src/generators/vue-intake-view/schema.json +12 -9
  45. package/src/generators/vue-intake-view/vue-intake-view.js +9 -3
  46. package/src/generators/vue-intake-view/vue-intake-view.js.map +1 -1
  47. package/src/generators/vue-intake-view/vue-intake-view.spec.ts +81 -12
  48. package/src/generators/vue-workspace-view/files/src/views/__viewFileName__.vue__tmpl__ +49 -1
  49. package/src/generators/vue-workspace-view/schema.d.ts +22 -0
  50. package/src/generators/vue-workspace-view/schema.json +8 -9
  51. package/src/generators/vue-workspace-view/vue-workspace-view.js +7 -1
  52. package/src/generators/vue-workspace-view/vue-workspace-view.js.map +1 -1
  53. package/src/generators/vue-workspace-view/vue-workspace-view.spec.ts +73 -7
  54. package/src/utils/paired-project.d.ts +10 -0
  55. package/src/utils/paired-project.js +125 -18
  56. package/src/utils/paired-project.js.map +1 -1
  57. package/src/utils/paired-project.spec.ts +126 -0
  58. package/src/utils/quality.js +43 -9
  59. package/src/utils/quality.js.map +1 -1
  60. package/src/utils/quality.spec.ts +35 -2
  61. package/src/utils/vue-eslint.d.ts +2 -0
  62. package/src/utils/vue-eslint.js +48 -0
  63. package/src/utils/vue-eslint.js.map +1 -0
  64. package/src/utils/vue-side-menu.d.ts +13 -1
  65. package/src/utils/vue-side-menu.js +50 -5
  66. package/src/utils/vue-side-menu.js.map +1 -1
  67. package/src/utils/vue-side-menu.spec.ts +100 -0
  68. package/src/utils/vue-tsconfig.d.ts +26 -0
  69. package/src/utils/vue-tsconfig.js +49 -0
  70. package/src/utils/vue-tsconfig.js.map +1 -0
@@ -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
@@ -66,6 +62,7 @@ inline styles.
66
62
  | A loading `<div>` or spinner markup | `<goa-skeleton type="…">` or `<goa-spinner>` | Skeletons match the shape they replace (`card`, `text`, `table`) |
67
63
  | A `<label>` + error `<span>` around an input | `<goa-form-item label="…" error="…">` | Wrap the `Goab*` input in this, don't restyle it |
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
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import AppLayout from './AppLayout.vue';
4
+
5
+ // Note on how width is asserted: in this test environment goa-page-block is not
6
+ // a defined custom element, so Vue falls back to setting the width as an
7
+ // ATTRIBUTE. In a real browser the element defines the property, so Vue sets it
8
+ // as a property and getAttribute('width') returns null -- verified against a
9
+ // running app, where the element then applies --max-width correctly. These tests
10
+ // therefore guard the variant -> width mapping, which is the part that can
11
+ // regress; they are not evidence about attribute-vs-property binding.
12
+ describe('AppLayout', () => {
13
+ const widthFor = (variant?: 'page' | 'wide' | 'form') =>
14
+ mount(AppLayout, { props: variant ? { variant } : {} })
15
+ .find('goa-page-block')
16
+ .attributes('width');
17
+
18
+ it('delegates the content gutter to goa-page-block', () => {
19
+ expect(mount(AppLayout).find('goa-page-block').exists()).toBe(true);
20
+ });
21
+
22
+ it('maps each variant to the width the element expects', () => {
23
+ expect(widthFor('form')).toBe('640px');
24
+ expect(widthFor()).toBe('1000px');
25
+ expect(widthFor('wide')).toBe('1200px');
26
+ });
27
+
28
+ it('renders slotted content inside the block', () => {
29
+ const w = mount(AppLayout, { slots: { default: '<p>hello</p>' } });
30
+ expect(w.find('goa-page-block').html()).toContain('<p>hello</p>');
31
+ });
32
+ });
@@ -14,36 +14,43 @@
14
14
  // copy would duplicate the anchor screen readers/keyboard nav jump to. The
15
15
  // top-level shell (App.vue for --layout=header, AppSideMenu for --layout=internal)
16
16
  // owns that landmark instead.
17
- withDefaults(defineProps<{ variant?: 'page' | 'wide' | 'form' }>(), {
18
- variant: 'page',
19
- });
17
+ import { computed } from 'vue';
18
+
19
+ const props = withDefaults(
20
+ defineProps<{ variant?: 'page' | 'wide' | 'form' }>(),
21
+ { variant: 'page' },
22
+ );
23
+
24
+ // goa-page-block takes a CSS dimension (or "full"); the variant names stay the
25
+ // public API so a view says what kind of page it is, not how wide it should be.
26
+ const WIDTHS = {
27
+ form: '640px',
28
+ page: '1000px',
29
+ wide: '1200px',
30
+ } as const;
31
+
32
+ const maxWidth = computed(() => WIDTHS[props.variant]);
20
33
  </script>
21
34
 
22
35
  <template>
23
- <div :class="`${variant}-content`">
24
- <slot />
25
- </div>
36
+ <goa-page-block :width="maxWidth">
37
+ <!-- goa-page-block supplies the centering, the max-width and a responsive
38
+ HORIZONTAL gutter, but no vertical padding -- GoA's own public-form
39
+ reference does the same thing, wrapping the block's children to add it. -->
40
+ <div class="app-layout-inset">
41
+ <slot />
42
+ </div>
43
+ </goa-page-block>
26
44
  </template>
27
45
 
28
46
  <style scoped>
29
- .page-content,
30
- .wide-content,
31
- .form-content {
32
- margin-inline: auto;
33
- box-sizing: border-box;
34
- /* Design-token gutter; fallbacks apply only if tokens aren't loaded. */
35
- padding: var(--goa-space-xl, 2.5rem) var(--goa-space-l, 1.5rem);
47
+ .app-layout-inset {
48
+ padding-block: var(--goa-space-xl, 2.5rem);
36
49
  }
37
50
 
38
- .form-content { max-width: 640px; }
39
- .page-content { max-width: 1000px; }
40
- .wide-content { max-width: 1200px; }
41
-
42
51
  @media (max-width: 623.98px) {
43
- .page-content,
44
- .wide-content,
45
- .form-content {
46
- padding: var(--goa-space-m, 1rem);
52
+ .app-layout-inset {
53
+ padding-block: var(--goa-space-m, 1rem);
47
54
  }
48
55
  }
49
56
  </style>
@@ -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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"}
@@ -1,4 +1,10 @@
1
- import { readProjectConfiguration, Tree } from '@nx/devkit';
1
+ import {
2
+ readJson,
3
+ readProjectConfiguration,
4
+ Tree,
5
+ updateJson,
6
+ writeJson,
7
+ } from '@nx/devkit';
2
8
  import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
3
9
  import generator, {
4
10
  ensurePackageExports,
@@ -171,6 +177,7 @@ describe('Vue Components Generator', () => {
171
177
  'libs/vue-components/src/lib/primitives/GoabDatePicker.spec.ts',
172
178
  'libs/vue-components/src/lib/patterns/WorkspaceTable.spec.ts',
173
179
  'libs/vue-components/src/lib/patterns/Stepper.spec.ts',
180
+ 'libs/vue-components/src/lib/patterns/AppLayout.spec.ts',
174
181
  ]) {
175
182
  expect(host.exists(spec)).toBeTruthy();
176
183
  }
@@ -178,7 +185,9 @@ describe('Vue Components Generator', () => {
178
185
  // Shared value formatters: every view renders a date/count the same way
179
186
  // instead of inlining its own toLocaleString (which is what a real app did
180
187
  // in three separate views before this existed).
181
- expect(host.exists('libs/vue-components/src/lib/formatters.ts')).toBeTruthy();
188
+ expect(
189
+ host.exists('libs/vue-components/src/lib/formatters.ts'),
190
+ ).toBeTruthy();
182
191
  expect(
183
192
  host.exists('libs/vue-components/src/lib/formatters.spec.ts'),
184
193
  ).toBeTruthy();
@@ -190,6 +199,12 @@ describe('Vue Components Generator', () => {
190
199
  'formatDateTime',
191
200
  'formatNumber',
192
201
  'formatPercent',
202
+ // Display of a *coded* value, the other half of formatting. Views that
203
+ // skipped these put raw domain codes and neutral-blue badges in front of
204
+ // citizens, so they live beside the date/number formatters rather than
205
+ // being re-derived per view.
206
+ 'optionLabel',
207
+ 'badgeType',
193
208
  ]) {
194
209
  expect(formatters).toContain(`export function ${fn}`);
195
210
  }
@@ -321,4 +336,82 @@ describe('Vue Components Generator', () => {
321
336
  expect(host.exists('libs/vue-components/package.json')).toBeFalsy();
322
337
  });
323
338
  });
339
+
340
+ it('exports the display helpers from the lib barrel, not just the module', async () => {
341
+ await generator(host);
342
+ const index = host.read('libs/vue-components/src/index.ts').toString();
343
+ expect(index).toContain('optionLabel');
344
+ expect(index).toContain('badgeType');
345
+ expect(index).toContain('DisplayOption');
346
+ expect(index).toContain('BadgeType');
347
+ }, 30000);
348
+
349
+ it('falls back to the raw value for an unmapped code rather than hiding it', async () => {
350
+ await generator(host);
351
+ const formatters = host
352
+ .read('libs/vue-components/src/lib/formatters.ts')
353
+ .toString();
354
+ // An unmapped code is a data/config problem; showing it is debuggable,
355
+ // blanking it is not.
356
+ expect(formatters).toContain('return match ? match.label : String(value)');
357
+ // And an unmapped badge value stays neutral rather than being asserted good
358
+ // or bad.
359
+ expect(formatters).toContain("fallback: BadgeType = 'information'");
360
+ }, 30000);
361
+
362
+ // Regression: @nx/vue writes `moduleResolution: "bundler"` into the lib's
363
+ // tsconfig but leaves `module` inherited. Against a create-nx-workspace base
364
+ // (`module: "nodenext"`) that pair is invalid -- TS5095 and TS5109 -- so
365
+ // `nx build` failed on completely unmodified generator output.
366
+ it('gives the lib a module setting compatible with its bundler moduleResolution', async () => {
367
+ writeJson(host, 'tsconfig.base.json', {
368
+ compilerOptions: {
369
+ module: 'nodenext',
370
+ moduleResolution: 'nodenext',
371
+ target: 'es2022',
372
+ },
373
+ });
374
+ await generator(host);
375
+ const compilerOptions = readJson(
376
+ host,
377
+ 'libs/vue-components/tsconfig.json',
378
+ ).compilerOptions;
379
+ expect(compilerOptions.moduleResolution).toBe('bundler');
380
+ expect(compilerOptions.module).toBe('esnext');
381
+ }, 30000);
382
+
383
+ it('leaves a tsconfig that already declares module alone', async () => {
384
+ await generator(host);
385
+ // Pin something deliberate, then re-run: the backfill must not overwrite it.
386
+ updateJson(host, 'libs/vue-components/tsconfig.json', (tsconfig) => {
387
+ tsconfig.compilerOptions.module = 'preserve';
388
+ return tsconfig;
389
+ });
390
+ await generator(host);
391
+ expect(
392
+ readJson(host, 'libs/vue-components/tsconfig.json').compilerOptions
393
+ .module,
394
+ ).toBe('preserve');
395
+ }, 30000);
396
+
397
+ // Regression: goa-work-side-menu's profile button takes its accessible name
398
+ // only from `user-name` (no aria-label prop exists on the element), and the
399
+ // shell rendered the account slot -- which is what creates that button --
400
+ // without ever supplying one. Measured on pristine generator output, that was
401
+ // an unnamed critical `button-name` violation plus an unlabelled `role-img-alt`
402
+ // icon on every route of an internal-layout app.
403
+ it('never leaves the side menu profile button without an accessible name', async () => {
404
+ await generator(host);
405
+ const sideMenu = host
406
+ .read('libs/vue-components/src/lib/patterns/AppSideMenu.vue')
407
+ .toString();
408
+
409
+ // Resolved, not passed straight through: an empty string is the realistic
410
+ // input (a signed-out user), and withDefaults only covers `undefined`.
411
+ expect(sideMenu).toContain(
412
+ "const profileName = computed(() => props.userName?.trim() || 'Account')",
413
+ );
414
+ expect(sideMenu).toContain(':user-name="profileName"');
415
+ expect(sideMenu).not.toContain(':user-name="userName"');
416
+ }, 30000);
324
417
  });
@@ -1,8 +1,52 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch } from 'vue';
3
3
  import { useRoute, useRouter } from 'vue-router';
4
- import { RecordDetailShell, formatCurrency, formatDate } from '<%= goaImportPath %>';
4
+ import {
5
+ RecordDetailShell,
6
+ formatCurrency,
7
+ formatDate,
8
+ <% if (hasCodedFields || hasBadgeFields) { -%>
9
+ optionLabel,
10
+ <% } -%>
11
+ <% if (hasBadgeFields) { -%>
12
+ badgeType,
13
+ <% } -%>
14
+ } from '<%= goaImportPath %>';
15
+ <% if (hasCodedFields || hasBadgeFields) { -%>
16
+ import type {
17
+ <% if (hasCodedFields || hasBadgeFields) { -%>
18
+ DisplayOption,
19
+ <% } -%>
20
+ <% if (hasBadgeFields) { -%>
21
+ BadgeType,
22
+ <% } -%>
23
+ } from '<%= goaImportPath %>';
24
+ <% } -%>
5
25
  import { useApi } from '../composables/useApi';
26
+ <% if (hasCodedFields || hasBadgeFields) { -%>
27
+
28
+ // Value -> label for coded fields, so the record reads back in prose rather than
29
+ // in stored codes.
30
+ const fieldOptions: Record<string, DisplayOption[]> = {
31
+ <% fields.forEach(function (field) { -%>
32
+ <% if (field.options && field.options.length) { -%>
33
+ '<%= field.key %>': <%- JSON.stringify(field.options) %>,
34
+ <% } -%>
35
+ <% }); -%>
36
+ };
37
+ <% } -%>
38
+ <% if (hasBadgeFields) { -%>
39
+
40
+ // Value -> badge type; unmapped values render neutral rather than being asserted
41
+ // as good or bad.
42
+ const fieldBadgeTypes: Record<string, Record<string, BadgeType>> = {
43
+ <% fields.forEach(function (field) { -%>
44
+ <% if (field.type === 'badge') { -%>
45
+ '<%= field.key %>': <%- JSON.stringify(field.badgeMap || {}) %>,
46
+ <% } -%>
47
+ <% }); -%>
48
+ };
49
+ <% } -%>
6
50
 
7
51
  // The fetched record's shape isn't known to this generator -- read fields
8
52
  // defensively rather than declaring (and likely getting wrong) a fake interface.
@@ -52,7 +96,12 @@ function goBack() {
52
96
  <dt><%= field.label %></dt>
53
97
  <dd>
54
98
  <% if (field.type === 'badge') { -%>
55
- <goa-badge type="information" :content="String(record['<%= field.key %>'] ?? '—')" />
99
+ <goa-badge
100
+ :type="badgeType(fieldBadgeTypes['<%= field.key %>'], record['<%= field.key %>'])"
101
+ :content="optionLabel(fieldOptions['<%= field.key %>'], record['<%= field.key %>'])"
102
+ />
103
+ <% } else if (field.options && field.options.length) { -%>
104
+ {{ optionLabel(fieldOptions['<%= field.key %>'], record['<%= field.key %>']) }}
56
105
  <% } else if (field.type === 'date') { -%>
57
106
  {{ formatDate(record['<%= field.key %>']) }}
58
107
  <% } else if (field.type === 'currency') { -%>