@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.
- package/README.md +41 -32
- package/package.json +1 -1
- package/src/generators/angular-app/angular-app.js +2 -2
- package/src/generators/angular-app/files/AGENTS.md__tmpl__ +16 -0
- package/src/generators/express-service/express-service.spec.ts +26 -13
- package/src/generators/express-service/schema.json +1 -1
- package/src/generators/react-app/files/AGENTS.md__tmpl__ +16 -0
- package/src/generators/react-app/react-app.js +2 -2
- package/src/generators/vue-admin-crud/files/src/views/__editViewFileName__.vue__tmpl__ +1 -3
- package/src/generators/vue-admin-crud/files/src/views/__listViewFileName__.vue__tmpl__ +33 -4
- package/src/generators/vue-admin-crud/schema.d.ts +2 -0
- package/src/generators/vue-admin-crud/schema.json +7 -8
- package/src/generators/vue-admin-crud/vue-admin-crud.js +6 -1
- package/src/generators/vue-admin-crud/vue-admin-crud.js.map +1 -1
- package/src/generators/vue-admin-crud/vue-admin-crud.spec.ts +74 -13
- package/src/generators/vue-app/files/AGENTS.md__tmpl__ +56 -11
- package/src/generators/vue-app/files/src/App.vue__tmpl__ +18 -4
- package/src/generators/vue-app/files/src/composables/useApi.spec.ts__tmpl__ +5 -3
- package/src/generators/vue-app/files/src/composables/useApi.ts__tmpl__ +14 -2
- package/src/generators/vue-app/vue-app.js +33 -6
- package/src/generators/vue-app/vue-app.js.map +1 -1
- package/src/generators/vue-app/vue-app.spec.ts +152 -17
- package/src/generators/vue-components/files/AGENTS.md__tmpl__ +35 -16
- package/src/generators/vue-components/files/src/index.ts__tmpl__ +3 -0
- package/src/generators/vue-components/files/src/lib/formatters.ts__tmpl__ +58 -0
- package/src/generators/vue-components/files/src/lib/patterns/AppLayout.spec.ts__tmpl__ +32 -0
- package/src/generators/vue-components/files/src/lib/patterns/AppLayout.vue__tmpl__ +28 -21
- package/src/generators/vue-components/files/src/lib/patterns/AppSideMenu.vue__tmpl__ +14 -3
- package/src/generators/vue-components/files/src/lib/patterns/FilterBar.vue__tmpl__ +7 -1
- package/src/generators/vue-components/files/src/lib/patterns/WorkspaceTable.vue__tmpl__ +7 -1
- package/src/generators/vue-components/vue-components.d.ts +1 -0
- package/src/generators/vue-components/vue-components.js +18 -39
- package/src/generators/vue-components/vue-components.js.map +1 -1
- package/src/generators/vue-components/vue-components.spec.ts +95 -2
- package/src/generators/vue-detail-view/files/src/views/__viewFileName__.vue__tmpl__ +51 -2
- package/src/generators/vue-detail-view/schema.d.ts +18 -0
- package/src/generators/vue-detail-view/schema.json +2 -2
- package/src/generators/vue-detail-view/vue-detail-view.js +4 -1
- package/src/generators/vue-detail-view/vue-detail-view.js.map +1 -1
- package/src/generators/vue-detail-view/vue-detail-view.spec.ts +36 -6
- package/src/generators/vue-intake-view/files/shared/src/views/__confirmationViewFileName__.vue__tmpl__ +60 -6
- package/src/generators/vue-intake-view/files/shared/src/views/__reviewViewFileName__.vue__tmpl__ +21 -0
- package/src/generators/vue-intake-view/schema.d.ts +10 -0
- package/src/generators/vue-intake-view/schema.json +12 -9
- package/src/generators/vue-intake-view/vue-intake-view.js +9 -3
- package/src/generators/vue-intake-view/vue-intake-view.js.map +1 -1
- package/src/generators/vue-intake-view/vue-intake-view.spec.ts +81 -12
- package/src/generators/vue-workspace-view/files/src/views/__viewFileName__.vue__tmpl__ +49 -1
- package/src/generators/vue-workspace-view/schema.d.ts +22 -0
- package/src/generators/vue-workspace-view/schema.json +8 -9
- package/src/generators/vue-workspace-view/vue-workspace-view.js +7 -1
- package/src/generators/vue-workspace-view/vue-workspace-view.js.map +1 -1
- package/src/generators/vue-workspace-view/vue-workspace-view.spec.ts +73 -7
- package/src/utils/paired-project.d.ts +10 -0
- package/src/utils/paired-project.js +125 -18
- package/src/utils/paired-project.js.map +1 -1
- package/src/utils/paired-project.spec.ts +126 -0
- package/src/utils/quality.js +43 -9
- package/src/utils/quality.js.map +1 -1
- package/src/utils/quality.spec.ts +35 -2
- package/src/utils/vue-eslint.d.ts +2 -0
- package/src/utils/vue-eslint.js +48 -0
- package/src/utils/vue-eslint.js.map +1 -0
- package/src/utils/vue-side-menu.d.ts +13 -1
- package/src/utils/vue-side-menu.js +50 -5
- package/src/utils/vue-side-menu.js.map +1 -1
- package/src/utils/vue-side-menu.spec.ts +100 -0
- package/src/utils/vue-tsconfig.d.ts +26 -0
- package/src/utils/vue-tsconfig.js +49 -0
- 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
|
-
**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
.
|
|
30
|
-
|
|
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
|
-
.
|
|
44
|
-
|
|
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="
|
|
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="
|
|
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="
|
|
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":";;;
|
|
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 {
|
|
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(
|
|
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 {
|
|
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
|
|
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') { -%>
|