@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.
- 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__ +3 -5
- 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 +96 -13
- package/src/generators/vue-app/files/AGENTS.md__tmpl__ +56 -11
- package/src/generators/vue-app/files/src/App.vue__tmpl__ +2 -1
- 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 +133 -13
- package/src/generators/vue-components/files/AGENTS.md__tmpl__ +36 -17
- 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/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 +94 -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__ +37 -1
- package/src/generators/vue-intake-view/files/steps/src/views/__stepViewFileName__.vue__tmpl__ +1 -1
- 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 +113 -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
|
@@ -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
|
-
|
|
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 [
|
|
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(
|
|
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
|
-
|
|
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.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
expect(
|
|
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
|
|
527
|
-
|
|
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
|
-
**
|
|
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
|
|
@@ -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="
|
|
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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"}
|