@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
|
@@ -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,
|
|
@@ -179,7 +185,9 @@ describe('Vue Components Generator', () => {
|
|
|
179
185
|
// Shared value formatters: every view renders a date/count the same way
|
|
180
186
|
// instead of inlining its own toLocaleString (which is what a real app did
|
|
181
187
|
// in three separate views before this existed).
|
|
182
|
-
expect(
|
|
188
|
+
expect(
|
|
189
|
+
host.exists('libs/vue-components/src/lib/formatters.ts'),
|
|
190
|
+
).toBeTruthy();
|
|
183
191
|
expect(
|
|
184
192
|
host.exists('libs/vue-components/src/lib/formatters.spec.ts'),
|
|
185
193
|
).toBeTruthy();
|
|
@@ -191,6 +199,12 @@ describe('Vue Components Generator', () => {
|
|
|
191
199
|
'formatDateTime',
|
|
192
200
|
'formatNumber',
|
|
193
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',
|
|
194
208
|
]) {
|
|
195
209
|
expect(formatters).toContain(`export function ${fn}`);
|
|
196
210
|
}
|
|
@@ -322,4 +336,82 @@ describe('Vue Components Generator', () => {
|
|
|
322
336
|
expect(host.exists('libs/vue-components/package.json')).toBeFalsy();
|
|
323
337
|
});
|
|
324
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);
|
|
325
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') { -%>
|
|
@@ -2,6 +2,24 @@ export interface DetailViewField {
|
|
|
2
2
|
key: string;
|
|
3
3
|
label: string;
|
|
4
4
|
type?: 'text' | 'date' | 'currency' | 'badge';
|
|
5
|
+
/**
|
|
6
|
+
* Value -> label for a coded field. Supply this whenever the stored value is a
|
|
7
|
+
* code rather than prose, or the view renders the raw code.
|
|
8
|
+
*/
|
|
9
|
+
options?: { value: string; label: string }[];
|
|
10
|
+
/**
|
|
11
|
+
* `badge` only. Value -> goa-badge type. Unmapped values render neutral.
|
|
12
|
+
*/
|
|
13
|
+
badgeMap?: Record<
|
|
14
|
+
string,
|
|
15
|
+
| 'information'
|
|
16
|
+
| 'success'
|
|
17
|
+
| 'important'
|
|
18
|
+
| 'emergency'
|
|
19
|
+
| 'dark'
|
|
20
|
+
| 'midtone'
|
|
21
|
+
| 'light'
|
|
22
|
+
>;
|
|
5
23
|
}
|
|
6
24
|
|
|
7
25
|
export interface Schema {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"resource": {
|
|
27
27
|
"type": "string",
|
|
28
|
-
"description": "API resource path segment -- the view fetches /api/<resource>/:id."
|
|
28
|
+
"description": "API resource path segment -- the view fetches /api/v1/<resource>/:id."
|
|
29
29
|
},
|
|
30
30
|
"route": {
|
|
31
31
|
"type": "string",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"fields": {
|
|
35
35
|
"type": "string",
|
|
36
|
-
"description": "JSON array of fields rendered in the record's info list, in display order -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"badge\"}]'. Each item: { key, label, type?: \"text\"|\"date\"|\"currency\"|\"badge\" (default \"text\") }. A plain array is also accepted when this generator is invoked programmatically. Nx's CLI option coercion only supports comma-separated primitive lists for array-typed schema properties, not JSON -- a JSON string is the only CLI syntax that actually survives Nx's own arg parsing for a structured list like this."
|
|
36
|
+
"description": "JSON array of fields rendered in the record's info list, in display order -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"badge\"}]'. Each item: { key, label, type?: \"text\"|\"date\"|\"currency\"|\"badge\" (default \"text\"), options?: [{value,label}], badgeMap?: {value: badgeType} }. Supply `options` for any field whose stored value is a code, or the view renders the raw code. Supply `badgeMap` on a `badge` field to give each status its colour; unmapped values render neutral. A plain array is also accepted when this generator is invoked programmatically. Nx's CLI option coercion only supports comma-separated primitive lists for array-typed schema properties, not JSON -- a JSON string is the only CLI syntax that actually survives Nx's own arg parsing for a structured list like this."
|
|
37
37
|
},
|
|
38
38
|
"heading": {
|
|
39
39
|
"type": "string",
|
|
@@ -38,7 +38,10 @@ function default_1(host, options) {
|
|
|
38
38
|
// Idempotent -- ensures RecordDetailShell exists even in a project scaffolded
|
|
39
39
|
// before vue-components carried it.
|
|
40
40
|
yield (0, vue_components_1.default)(host);
|
|
41
|
-
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host),
|
|
41
|
+
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host),
|
|
42
|
+
// Derived here, not tested inline: EJS throws a ReferenceError on a bare
|
|
43
|
+
// undefined variable, so a flag the template reads must always be defined.
|
|
44
|
+
hasCodedFields: normalizedOptions.fields.some((field) => { var _a; return (_a = field.options) === null || _a === void 0 ? void 0 : _a.length; }), hasBadgeFields: normalizedOptions.fields.some((field) => field.type === 'badge'), tmpl: '' }));
|
|
42
45
|
(0, vue_router_1.insertVueRoute)(host, normalizedOptions.projectRoot, normalizedOptions.project, {
|
|
43
46
|
path: normalizedOptions.route,
|
|
44
47
|
componentImportPath: `../views/${normalizedOptions.viewFileName}.vue`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-detail-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-detail-view/vue-detail-view.ts"],"names":[],"mappings":";;AAqDA,
|
|
1
|
+
{"version":3,"file":"vue-detail-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-detail-view/vue-detail-view.ts"],"names":[],"mappings":";;AAqDA,4BAsCC;;AA3FD,uCAMoB;AACpB,6BAA6B;AAC7B,uDAAwD;AACxD,qEAE0C;AAG1C,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,0EAA0E;AAC1E,6EAA6E;AAC7E,iEAAiE;AACjE,SAAS,WAAW,CAAC,MAAwB;IAC3C,MAAM,MAAM,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAe;;IACnD,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9E,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,YAAY,OAAO,CAAC,KAAK,qFAAqF,CAC/G,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;IAChD,uCACK,OAAO,KACV,WAAW,EACX,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EACnC,YAAY,EAAE,GAAG,SAAS,MAAM;QAChC,2EAA2E;QAC3E,mDAAmD;QACnD,OAAO,EAAE,MAAA,OAAO,CAAC,OAAO,mCAAI,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,EACvE,YAAY,EAAE,MAAA,OAAO,CAAC,YAAY,mCAAI,IAAI,IAC1C;AACJ,CAAC;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAE1D,8EAA8E;QAC9E,oCAAoC;QACpC,MAAM,IAAA,wBAAsB,EAAC,IAAI,CAAC,CAAC;QAEnC,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAC7B,iBAAiB,CAAC,WAAW,kCAExB,iBAAiB,KACpB,aAAa,EAAE,IAAA,wCAAuB,EAAC,IAAI,CAAC;YAC5C,yEAAyE;YACzE,2EAA2E;YAC3E,cAAc,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAC3C,CAAC,KAAK,EAAE,EAAE,WAAC,OAAA,MAAA,KAAK,CAAC,OAAO,0CAAE,MAAM,CAAA,EAAA,CACjC,EACD,cAAc,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAC3C,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAClC,EACD,IAAI,EAAE,EAAE,IAEX,CAAC;QAEF,IAAA,2BAAc,EACZ,IAAI,EACJ,iBAAiB,CAAC,WAAW,EAC7B,iBAAiB,CAAC,OAAO,EACzB;YACE,IAAI,EAAE,iBAAiB,CAAC,KAAK;YAC7B,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,YAAY,MAAM;YACrE,YAAY,EAAE,iBAAiB,CAAC,YAAY;SAC7C,CACF,CAAC;QAEF,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA"}
|
|
@@ -102,19 +102,23 @@ describe('Vue Detail View Generator', () => {
|
|
|
102
102
|
.read('apps/test/src/views/ApplicationDetailView.vue')
|
|
103
103
|
.toString();
|
|
104
104
|
expect(view).toContain('heading="Application Detail"');
|
|
105
|
-
expect(view).toContain(
|
|
105
|
+
expect(view).toContain(
|
|
106
|
+
"await get('applications', String(route.params.id))",
|
|
107
|
+
);
|
|
106
108
|
expect(view).not.toContain('apiFetch');
|
|
107
109
|
|
|
108
110
|
// Vue Router reuses this component across an id-only change, so the fetch
|
|
109
111
|
// is driven by a watch rather than onMounted.
|
|
110
|
-
expect(view).toContain(
|
|
112
|
+
expect(view).toContain(
|
|
113
|
+
'watch(() => route.params.id, load, { immediate: true })',
|
|
114
|
+
);
|
|
111
115
|
expect(view).not.toContain('onMounted(');
|
|
112
116
|
expect(view).not.toContain('function formatCurrency');
|
|
113
117
|
|
|
114
118
|
expect(view).toContain('formatDate');
|
|
115
119
|
expect(view).not.toContain('function formatDate');
|
|
116
120
|
expect(view).toContain(
|
|
117
|
-
"
|
|
121
|
+
"badgeType(fieldBadgeTypes['status'], record['status'])",
|
|
118
122
|
);
|
|
119
123
|
// A type: 'date' column is a calendar date -- formatDate, not
|
|
120
124
|
// formatDateTime, which would show an invented midnight.
|
|
@@ -168,9 +172,7 @@ describe('Vue Detail View Generator', () => {
|
|
|
168
172
|
it('ensures the shared RecordDetailShell pattern component exists', async () => {
|
|
169
173
|
await generator(host, baseOptions);
|
|
170
174
|
expect(
|
|
171
|
-
host.exists(
|
|
172
|
-
'libs/vue-components/src/lib/patterns/RecordDetailShell.vue',
|
|
173
|
-
),
|
|
175
|
+
host.exists('libs/vue-components/src/lib/patterns/RecordDetailShell.vue'),
|
|
174
176
|
).toBeTruthy();
|
|
175
177
|
}, 30000);
|
|
176
178
|
|
|
@@ -180,4 +182,32 @@ describe('Vue Detail View Generator', () => {
|
|
|
180
182
|
const after = readProjectConfiguration(host, 'test');
|
|
181
183
|
expect(after).toEqual(before);
|
|
182
184
|
}, 30000);
|
|
185
|
+
|
|
186
|
+
it('maps badge values to semantic types and renders coded fields as labels', async () => {
|
|
187
|
+
await generator(host, {
|
|
188
|
+
...baseOptions,
|
|
189
|
+
fields: [
|
|
190
|
+
{
|
|
191
|
+
key: 'status',
|
|
192
|
+
label: 'Status',
|
|
193
|
+
type: 'badge',
|
|
194
|
+
badgeMap: { declined: 'emergency' },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: 'region',
|
|
198
|
+
label: 'Region',
|
|
199
|
+
options: [{ value: 'northwest', label: 'North west' }],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
const view = host
|
|
204
|
+
.read('apps/test/src/views/ApplicationDetailView.vue')
|
|
205
|
+
.toString();
|
|
206
|
+
// Quote style depends on whether formatFiles ran, so match the pair.
|
|
207
|
+
expect(view).toMatch(/["']?declined["']?\s*:\s*["']emergency["']/);
|
|
208
|
+
expect(view).toContain("badgeType(fieldBadgeTypes['status']");
|
|
209
|
+
expect(view).toContain(
|
|
210
|
+
"optionLabel(fieldOptions['region'], record['region'])",
|
|
211
|
+
);
|
|
212
|
+
}, 30000);
|
|
183
213
|
});
|
|
@@ -1,20 +1,74 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue';
|
|
2
3
|
import { useRoute } from 'vue-router';
|
|
4
|
+
import { useApi } from '../composables/useApi';
|
|
3
5
|
|
|
4
6
|
const route = useRoute();
|
|
7
|
+
const { get } = useApi();
|
|
8
|
+
|
|
9
|
+
// The confirmation page fetches the submitted record rather than reading the id
|
|
10
|
+
// out of the route, so it can show the business reference the person will quote
|
|
11
|
+
// back to us. The route id is a storage detail -- presenting it as "your
|
|
12
|
+
// reference number" hands them a number that means nothing to anyone they later
|
|
13
|
+
// talk to.
|
|
14
|
+
const record = ref<Record<string, unknown> | null>(null);
|
|
15
|
+
const loading = ref(true);
|
|
16
|
+
|
|
17
|
+
// Falls back to the route id only when the record has no reference field, so the
|
|
18
|
+
// page still shows something actionable rather than a blank.
|
|
19
|
+
const reference = ref(String(route.params.id ?? ''));
|
|
20
|
+
|
|
21
|
+
onMounted(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const id = String(route.params.id ?? '');
|
|
24
|
+
record.value = (await get('<%- resource %>', id)) as Record<string, unknown>;
|
|
25
|
+
const value = record.value?.['<%- referenceField %>'];
|
|
26
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
27
|
+
reference.value = String(value);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// A failed lookup must not blank the confirmation: the submission already
|
|
31
|
+
// succeeded, and the route id is still a usable fallback reference.
|
|
32
|
+
} finally {
|
|
33
|
+
loading.value = false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
5
36
|
</script>
|
|
6
37
|
|
|
7
38
|
<template>
|
|
8
39
|
<div>
|
|
9
|
-
<goa-callout type="success" heading="
|
|
10
|
-
<
|
|
11
|
-
Your reference number is
|
|
12
|
-
|
|
13
|
-
</
|
|
40
|
+
<goa-callout type="success" heading="<%- heading %> submitted">
|
|
41
|
+
<h1 class="confirmation-reference">
|
|
42
|
+
Your reference number is
|
|
43
|
+
<strong>{{ loading ? '…' : reference }}</strong>
|
|
44
|
+
</h1>
|
|
45
|
+
<p>Keep this for your records. You'll need it to check on your submission.</p>
|
|
14
46
|
</goa-callout>
|
|
15
47
|
|
|
48
|
+
<goa-spacer vspacing="l" />
|
|
49
|
+
|
|
50
|
+
<h2>What happens next</h2>
|
|
51
|
+
<ol>
|
|
52
|
+
<li>We'll review what you submitted.</li>
|
|
53
|
+
<li>We'll contact you if we need anything further.</li>
|
|
54
|
+
<li>We'll let you know the outcome.</li>
|
|
55
|
+
</ol>
|
|
56
|
+
|
|
16
57
|
<goa-spacer vspacing="m" />
|
|
17
58
|
|
|
18
|
-
<
|
|
59
|
+
<router-link to="/">
|
|
60
|
+
<goa-button type="secondary">Return to the start</goa-button>
|
|
61
|
+
</router-link>
|
|
19
62
|
</div>
|
|
20
63
|
</template>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
/* The reference is the page's heading in the GDS confirmation pattern -- an h1
|
|
67
|
+
for landmark/screen-reader order, sized to sit inside the callout rather than
|
|
68
|
+
at page-title scale. */
|
|
69
|
+
.confirmation-reference {
|
|
70
|
+
font-size: var(--goa-typography-heading-m-font-size, 1.25rem);
|
|
71
|
+
line-height: var(--goa-typography-heading-m-line-height, 1.5);
|
|
72
|
+
margin: 0 0 var(--goa-space-s, 0.75rem);
|
|
73
|
+
}
|
|
74
|
+
</style>
|
package/src/generators/vue-intake-view/files/shared/src/views/__reviewViewFileName__.vue__tmpl__
CHANGED
|
@@ -5,11 +5,30 @@ import { GoabCheckbox } from '<%= goaImportPath %>';
|
|
|
5
5
|
<% if (steps.some((s) => s.fields.some((f) => f.type === 'date'))) { -%>
|
|
6
6
|
import { formatDate } from '<%= goaImportPath %>';
|
|
7
7
|
<% } -%>
|
|
8
|
+
<% if (hasCodedFields) { -%>
|
|
9
|
+
import { optionLabel } from '<%= goaImportPath %>';
|
|
10
|
+
import type { DisplayOption } from '<%= goaImportPath %>';
|
|
11
|
+
<% } -%>
|
|
8
12
|
import { useApi } from '../composables/useApi';
|
|
9
13
|
|
|
10
14
|
const route = useRoute();
|
|
11
15
|
const router = useRouter();
|
|
12
16
|
const { get, action } = useApi();
|
|
17
|
+
<% if (hasCodedFields) { -%>
|
|
18
|
+
|
|
19
|
+
// Value -> label for every coded field across the steps. Check-your-answers is
|
|
20
|
+
// where this matters most: the page exists so the person can read back what they
|
|
21
|
+
// entered, and a stored code ('cattle-mature', 'northwest') is not that.
|
|
22
|
+
const fieldOptions: Record<string, DisplayOption[]> = {
|
|
23
|
+
<% steps.forEach(function (step) { -%>
|
|
24
|
+
<% step.fields.forEach(function (field) { -%>
|
|
25
|
+
<% if (field.options && field.options.length) { -%>
|
|
26
|
+
'<%- field.key %>': <%- JSON.stringify(field.options) %>,
|
|
27
|
+
<% } -%>
|
|
28
|
+
<% }); -%>
|
|
29
|
+
<% }); -%>
|
|
30
|
+
};
|
|
31
|
+
<% } -%>
|
|
13
32
|
|
|
14
33
|
const idParam = computed(() => String(route.params.id ?? ''));
|
|
15
34
|
|
|
@@ -79,11 +98,13 @@ async function onSubmit() {
|
|
|
79
98
|
Edit
|
|
80
99
|
</goa-button>
|
|
81
100
|
</div>
|
|
82
|
-
<dl>
|
|
101
|
+
<dl class="review-fields">
|
|
83
102
|
<% step.fields.forEach(function (field) { -%>
|
|
84
103
|
<dt><%- field.label %></dt>
|
|
85
104
|
<% if (field.type === 'date') { -%>
|
|
86
105
|
<dd>{{ formatDate(record['<%- field.key %>']) }}</dd>
|
|
106
|
+
<% } else if (field.options && field.options.length) { -%>
|
|
107
|
+
<dd>{{ optionLabel(fieldOptions['<%- field.key %>'], record['<%- field.key %>']) }}</dd>
|
|
87
108
|
<% } else { -%>
|
|
88
109
|
<dd>{{ record['<%- field.key %>'] ?? '—' }}</dd>
|
|
89
110
|
<% } -%>
|
|
@@ -125,4 +146,19 @@ async function onSubmit() {
|
|
|
125
146
|
align-items: center;
|
|
126
147
|
gap: var(--goa-space-m);
|
|
127
148
|
}
|
|
149
|
+
|
|
150
|
+
/* A bare <dl> stacks its terms and values flush, so every answer ran into the
|
|
151
|
+
next one on the page whose whole job is being read back. Two columns, label
|
|
152
|
+
distinguished by weight -- matching vue-detail-view's .detail-fields, since
|
|
153
|
+
both render a record as label/value pairs and should not look different. */
|
|
154
|
+
.review-fields {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: auto 1fr;
|
|
157
|
+
gap: var(--goa-space-xs) var(--goa-space-l);
|
|
158
|
+
margin: 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.review-fields dt {
|
|
162
|
+
font-weight: 600;
|
|
163
|
+
}
|
|
128
164
|
</style>
|
package/src/generators/vue-intake-view/files/steps/src/views/__stepViewFileName__.vue__tmpl__
CHANGED
|
@@ -213,7 +213,7 @@ function goBack() {
|
|
|
213
213
|
<StepErrorSummary :errors="errors" />
|
|
214
214
|
|
|
215
215
|
<% stepFields.forEach(function (field) { -%>
|
|
216
|
-
<goa-form-item id="field-<%- field.key %>" label="<%- field.label %>"<% if (field.required !== false) { %> requirement="required"<% } %>>
|
|
216
|
+
<goa-form-item mb="l" id="field-<%- field.key %>" label="<%- field.label %>"<% if (field.required !== false) { %> requirement="required"<% } %>>
|
|
217
217
|
<% if (field.type === 'textarea') { -%>
|
|
218
218
|
<GoabTextarea v-model="form.<%- field.key %>" name="<%- field.key %>" />
|
|
219
219
|
<% } else if (field.type === 'number') { -%>
|
|
@@ -24,6 +24,14 @@ export interface Schema {
|
|
|
24
24
|
* programmatic callers (e.g. tests).
|
|
25
25
|
*/
|
|
26
26
|
steps: string | IntakeViewStep[];
|
|
27
|
+
/**
|
|
28
|
+
* Field on the submitted record holding the business reference shown on the
|
|
29
|
+
* confirmation page, e.g. `reference`. Defaults to `reference`; falls back to
|
|
30
|
+
* the route id when the record has no such field.
|
|
31
|
+
*/
|
|
32
|
+
referenceField?: string;
|
|
33
|
+
/** Human title used in headings, e.g. "Compensation claim". */
|
|
34
|
+
heading?: string;
|
|
27
35
|
requiresAuth?: boolean;
|
|
28
36
|
}
|
|
29
37
|
|
|
@@ -31,6 +39,8 @@ export interface NormalizedSchema extends Omit<Schema, 'steps'> {
|
|
|
31
39
|
projectRoot: string;
|
|
32
40
|
steps: IntakeViewStep[];
|
|
33
41
|
requiresAuth: boolean;
|
|
42
|
+
referenceField: string;
|
|
43
|
+
heading: string;
|
|
34
44
|
/** PascalCase base name, e.g. "Application" for --name application. */
|
|
35
45
|
baseName: string;
|
|
36
46
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/schema",
|
|
3
3
|
"id": "NxAdspVueIntakeView",
|
|
4
4
|
"title": "Vue Multi-Step Intake View",
|
|
5
|
-
"description": "Generates a route-per-step intake wizard (Stepper + StepErrorSummary, a required read-only review step, and a confirmation page) into an existing vue-app project. Cross-step state is server-persisted -- each step PUTs/POSTs to /api/<resource>/:id and refetches on mount; there's no client-side draft caching.",
|
|
5
|
+
"description": "Generates a route-per-step intake wizard (Stepper + StepErrorSummary, a required read-only review step, and a confirmation page) into an existing vue-app project. Cross-step state is server-persisted -- each step PUTs/POSTs to /api/v1/<resource>/:id and refetches on mount; there's no client-side draft caching.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"project": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"resource": {
|
|
27
27
|
"type": "string",
|
|
28
|
-
"description": "API resource path segment. Each step fetches/saves /api/<resource>/:id; the review step's Submit posts /api/<resource>/:id/submit."
|
|
28
|
+
"description": "API resource path segment. Each step fetches/saves /api/v1/<resource>/:id; the review step's Submit posts /api/v1/<resource>/:id/submit."
|
|
29
29
|
},
|
|
30
30
|
"route": {
|
|
31
31
|
"type": "string",
|
|
@@ -39,14 +39,17 @@
|
|
|
39
39
|
"type": "boolean",
|
|
40
40
|
"description": "Whether the generated routes require authentication.",
|
|
41
41
|
"default": true
|
|
42
|
+
},
|
|
43
|
+
"referenceField": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Field on the submitted record holding the business reference shown on the confirmation page, e.g. 'reference'. The confirmation fetches the record and displays this rather than the route id -- an internal id presented as a reference number is meaningless to the person quoting it back. Falls back to the route id when the record has no such field.",
|
|
46
|
+
"default": "reference"
|
|
47
|
+
},
|
|
48
|
+
"heading": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Human title used in the confirmation heading, e.g. 'Compensation claim'. Defaults to the class-cased --name."
|
|
42
51
|
}
|
|
43
52
|
},
|
|
44
|
-
"required": [
|
|
45
|
-
"project",
|
|
46
|
-
"name",
|
|
47
|
-
"resource",
|
|
48
|
-
"route",
|
|
49
|
-
"steps"
|
|
50
|
-
],
|
|
53
|
+
"required": ["project", "name", "resource", "route", "steps"],
|
|
51
54
|
"additionalProperties": false
|
|
52
55
|
}
|
|
@@ -28,9 +28,12 @@ function parseSteps(steps) {
|
|
|
28
28
|
return parsed;
|
|
29
29
|
}
|
|
30
30
|
function normalizeOptions(host, options) {
|
|
31
|
-
var _a;
|
|
31
|
+
var _a, _b, _c;
|
|
32
32
|
const { root: projectRoot } = (0, devkit_1.readProjectConfiguration)(host, options.project);
|
|
33
|
-
return Object.assign(Object.assign({}, options), { projectRoot, steps: parseSteps(options.steps), requiresAuth: (_a = options.requiresAuth) !== null && _a !== void 0 ? _a : true,
|
|
33
|
+
return Object.assign(Object.assign({}, options), { projectRoot, steps: parseSteps(options.steps), requiresAuth: (_a = options.requiresAuth) !== null && _a !== void 0 ? _a : true, referenceField: (_b = options.referenceField) !== null && _b !== void 0 ? _b : 'reference',
|
|
34
|
+
// Sentence-case the name for prose headings ("claim" -> "Claim"), so the
|
|
35
|
+
// confirmation reads as a service page rather than repeating a slug.
|
|
36
|
+
heading: (_c = options.heading) !== null && _c !== void 0 ? _c : (0, devkit_1.names)(options.name).className, baseName: (0, devkit_1.names)(options.name).className });
|
|
34
37
|
}
|
|
35
38
|
// PascalCase view name for a step, e.g. "personal-info" -> "PersonalInfoStepView".
|
|
36
39
|
function stepViewFileName(stepKey) {
|
|
@@ -61,7 +64,10 @@ function default_1(host, options) {
|
|
|
61
64
|
});
|
|
62
65
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files/shared'), projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath,
|
|
63
66
|
reviewViewFileName,
|
|
64
|
-
confirmationViewFileName,
|
|
67
|
+
confirmationViewFileName,
|
|
68
|
+
// Derived here, not tested inline: EJS throws a ReferenceError on a bare
|
|
69
|
+
// undefined variable, so a flag the template reads must always be defined.
|
|
70
|
+
hasCodedFields: steps.some((step) => step.fields.some((field) => { var _a; return (_a = field.options) === null || _a === void 0 ? void 0 : _a.length; })), tmpl: '' }));
|
|
65
71
|
// Registered in reverse so the final router file lists steps in forward
|
|
66
72
|
// order (each insertVueRoute call adds right after `routes: [`, pushing
|
|
67
73
|
// earlier insertions down).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-intake-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-intake-view/vue-intake-view.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"vue-intake-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-intake-view/vue-intake-view.ts"],"names":[],"mappings":"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"}
|