@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
|
@@ -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
|
|
|
@@ -84,6 +103,8 @@ async function onSubmit() {
|
|
|
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
|
<% } -%>
|
|
@@ -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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"}
|
|
@@ -92,8 +92,12 @@ describe('Vue Intake View Generator', () => {
|
|
|
92
92
|
expect(step1).toContain(
|
|
93
93
|
"import { Stepper, StepErrorSummary, GoabInput } from '@proj/vue-components';",
|
|
94
94
|
);
|
|
95
|
-
expect(step1).toContain(
|
|
96
|
-
|
|
95
|
+
expect(step1).toContain(
|
|
96
|
+
"{ key: 'personal-info', label: 'Personal information' }",
|
|
97
|
+
);
|
|
98
|
+
expect(step1).toContain(
|
|
99
|
+
"{ key: 'contact-info', label: 'Contact information' }",
|
|
100
|
+
);
|
|
97
101
|
// Own step is 'incomplete' when active and not yet completed, per the real
|
|
98
102
|
// goa-form-step status enum (complete/incomplete/not-started -- no "current").
|
|
99
103
|
expect(step1).toContain("step.key === 'personal-info'");
|
|
@@ -125,13 +129,19 @@ describe('Vue Intake View Generator', () => {
|
|
|
125
129
|
it('generates a review view listing every step with an Edit link, and a declaration gate on Submit', async () => {
|
|
126
130
|
await generator(host, baseOptions);
|
|
127
131
|
|
|
128
|
-
const review = host
|
|
132
|
+
const review = host
|
|
133
|
+
.read('apps/test/src/views/ApplicationReviewView.vue')
|
|
134
|
+
.toString();
|
|
129
135
|
expect(review).toContain("editStep('personal-info')");
|
|
130
136
|
expect(review).toContain("editStep('contact-info')");
|
|
131
137
|
expect(review).toContain("record['fullName'] ?? '—'");
|
|
132
138
|
expect(review).toContain("record['email'] ?? '—'");
|
|
133
|
-
expect(review).toContain(
|
|
134
|
-
|
|
139
|
+
expect(review).toContain(
|
|
140
|
+
':disabled="!declared || submitting || undefined"',
|
|
141
|
+
);
|
|
142
|
+
expect(review).toContain(
|
|
143
|
+
"await action('applications', idParam.value, 'submit')",
|
|
144
|
+
);
|
|
135
145
|
expect(review).not.toContain('apiFetch');
|
|
136
146
|
expect(review).toContain('watch(idParam, load, { immediate: true })');
|
|
137
147
|
expect(review).not.toContain('onMounted(');
|
|
@@ -175,8 +185,12 @@ describe('Vue Intake View Generator', () => {
|
|
|
175
185
|
const step = host
|
|
176
186
|
.read('apps/test/src/views/DetailsStepView.vue')
|
|
177
187
|
.toString();
|
|
178
|
-
expect(step).toContain(
|
|
179
|
-
|
|
188
|
+
expect(step).toContain(
|
|
189
|
+
"count: form.count === '' ? null : Number(form.count)",
|
|
190
|
+
);
|
|
191
|
+
expect(step).toContain(
|
|
192
|
+
'occurred: toIsoDateString(form.occurred) || null',
|
|
193
|
+
);
|
|
180
194
|
expect(step).toContain("fromIsoDateString(data['occurred'])");
|
|
181
195
|
});
|
|
182
196
|
|
|
@@ -210,12 +224,39 @@ describe('Vue Intake View Generator', () => {
|
|
|
210
224
|
});
|
|
211
225
|
});
|
|
212
226
|
|
|
213
|
-
|
|
227
|
+
// The previous version of this test asserted `{{ route.params.id }}` -- it
|
|
228
|
+
// encoded the defect rather than the requirement, so the confirmation page
|
|
229
|
+
// shipped presenting the storage id as "your reference number" ("Your
|
|
230
|
+
// reference number is 1001" in a real build).
|
|
231
|
+
it('generates a confirmation view showing the business reference, not the route id', async () => {
|
|
232
|
+
await generator(host, baseOptions);
|
|
233
|
+
const confirmation = host
|
|
234
|
+
.read('apps/test/src/views/ApplicationConfirmationView.vue')
|
|
235
|
+
.toString();
|
|
236
|
+
// Fetches the record and reads the reference field off it.
|
|
237
|
+
expect(confirmation).toContain("record.value?.['reference']");
|
|
238
|
+
expect(confirmation).toContain("{{ loading ? '…' : reference }}");
|
|
239
|
+
// The route id survives only as a fallback, never as the presented value.
|
|
240
|
+
expect(confirmation).not.toContain('{{ route.params.id }}');
|
|
241
|
+
}, 30000);
|
|
242
|
+
|
|
243
|
+
it('honours --referenceField', async () => {
|
|
244
|
+
await generator(host, { ...baseOptions, referenceField: 'claimNumber' });
|
|
245
|
+
expect(
|
|
246
|
+
host
|
|
247
|
+
.read('apps/test/src/views/ApplicationConfirmationView.vue')
|
|
248
|
+
.toString(),
|
|
249
|
+
).toContain("record.value?.['claimNumber']");
|
|
250
|
+
}, 30000);
|
|
251
|
+
|
|
252
|
+
it('gives the confirmation page an h1 and a route out of the flow', async () => {
|
|
214
253
|
await generator(host, baseOptions);
|
|
215
254
|
const confirmation = host
|
|
216
255
|
.read('apps/test/src/views/ApplicationConfirmationView.vue')
|
|
217
256
|
.toString();
|
|
218
|
-
expect(confirmation).toContain('
|
|
257
|
+
expect(confirmation).toContain('<h1');
|
|
258
|
+
expect(confirmation).toContain('What happens next');
|
|
259
|
+
expect(confirmation).toContain('<router-link to="/">');
|
|
219
260
|
}, 30000);
|
|
220
261
|
|
|
221
262
|
it('inserts a route for every step plus review and confirmation, requiring auth by default', async () => {
|
|
@@ -235,9 +276,7 @@ describe('Vue Intake View Generator', () => {
|
|
|
235
276
|
expect(routerTs).toContain(
|
|
236
277
|
"component: () => import('../views/ApplicationConfirmationView.vue')",
|
|
237
278
|
);
|
|
238
|
-
expect(
|
|
239
|
-
routerTs.split('requiresAuth: true').length - 1,
|
|
240
|
-
).toBe(4);
|
|
279
|
+
expect(routerTs.split('requiresAuth: true').length - 1).toBe(4);
|
|
241
280
|
// The existing route is untouched, not replaced.
|
|
242
281
|
expect(routerTs).toContain("{ path: '/', component: HomeView }");
|
|
243
282
|
}, 30000);
|
|
@@ -274,4 +313,34 @@ describe('Vue Intake View Generator', () => {
|
|
|
274
313
|
const after = readProjectConfiguration(host, 'test');
|
|
275
314
|
expect(after).toEqual(before);
|
|
276
315
|
}, 30000);
|
|
316
|
+
|
|
317
|
+
// The check-your-answers page exists so the person can read back what they
|
|
318
|
+
// entered; a stored code ('cattle-mature', 'northwest') is not that. The
|
|
319
|
+
// generator is handed the label for every option and used to discard it.
|
|
320
|
+
it('renders coded answers as labels on the review page', async () => {
|
|
321
|
+
await generator(host, {
|
|
322
|
+
...baseOptions,
|
|
323
|
+
steps: [
|
|
324
|
+
{
|
|
325
|
+
key: 'losses',
|
|
326
|
+
label: 'Losses',
|
|
327
|
+
fields: [
|
|
328
|
+
{
|
|
329
|
+
key: 'species',
|
|
330
|
+
label: 'Species',
|
|
331
|
+
type: 'select',
|
|
332
|
+
options: [{ value: 'cattle-mature', label: 'Cattle (mature)' }],
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
const review = host
|
|
339
|
+
.read('apps/test/src/views/ApplicationReviewView.vue')
|
|
340
|
+
.toString();
|
|
341
|
+
expect(review).toContain(
|
|
342
|
+
"optionLabel(fieldOptions['species'], record['species'])",
|
|
343
|
+
);
|
|
344
|
+
expect(review).toContain('Cattle (mature)');
|
|
345
|
+
}, 30000);
|
|
277
346
|
});
|
|
@@ -7,7 +7,23 @@ import {
|
|
|
7
7
|
<% } -%>
|
|
8
8
|
formatCurrency,
|
|
9
9
|
formatDate,
|
|
10
|
+
<% if (hasCodedColumns || hasBadgeColumns) { -%>
|
|
11
|
+
optionLabel,
|
|
12
|
+
<% } -%>
|
|
13
|
+
<% if (hasBadgeColumns) { -%>
|
|
14
|
+
badgeType,
|
|
15
|
+
<% } -%>
|
|
16
|
+
} from '<%= goaImportPath %>';
|
|
17
|
+
<% if (hasCodedColumns || hasBadgeColumns) { -%>
|
|
18
|
+
import type {
|
|
19
|
+
<% if (hasCodedColumns || hasBadgeColumns) { -%>
|
|
20
|
+
DisplayOption,
|
|
21
|
+
<% } -%>
|
|
22
|
+
<% if (hasBadgeColumns) { -%>
|
|
23
|
+
BadgeType,
|
|
24
|
+
<% } -%>
|
|
10
25
|
} from '<%= goaImportPath %>';
|
|
26
|
+
<% } -%>
|
|
11
27
|
import { useApi } from '../composables/useApi';
|
|
12
28
|
|
|
13
29
|
const { list } = useApi();
|
|
@@ -17,6 +33,31 @@ const columns = [
|
|
|
17
33
|
{ key: '<%= column.key %>', label: '<%= column.label %>', sortable: <%= !!column.sortable %> },
|
|
18
34
|
<% }); -%>
|
|
19
35
|
];
|
|
36
|
+
<% if (hasCodedColumns || hasBadgeColumns) { -%>
|
|
37
|
+
|
|
38
|
+
// Value -> label for coded columns, so the table shows 'Cattle (mature)' rather
|
|
39
|
+
// than the stored 'cattle-mature'. Keyed by column so one lookup covers them all.
|
|
40
|
+
const columnOptions: Record<string, DisplayOption[]> = {
|
|
41
|
+
<% columns.forEach(function (column) { -%>
|
|
42
|
+
<% if (column.options && column.options.length) { -%>
|
|
43
|
+
'<%= column.key %>': <%- JSON.stringify(column.options) %>,
|
|
44
|
+
<% } -%>
|
|
45
|
+
<% }); -%>
|
|
46
|
+
};
|
|
47
|
+
<% } -%>
|
|
48
|
+
<% if (hasBadgeColumns) { -%>
|
|
49
|
+
|
|
50
|
+
// Value -> badge type, so a status carries the right colour. Anything unmapped
|
|
51
|
+
// falls back to the neutral 'information' rather than being asserted as good or
|
|
52
|
+
// bad.
|
|
53
|
+
const columnBadgeTypes: Record<string, Record<string, BadgeType>> = {
|
|
54
|
+
<% columns.forEach(function (column) { -%>
|
|
55
|
+
<% if (column.type === 'badge') { -%>
|
|
56
|
+
'<%= column.key %>': <%- JSON.stringify(column.badgeMap || {}) %>,
|
|
57
|
+
<% } -%>
|
|
58
|
+
<% }); -%>
|
|
59
|
+
};
|
|
60
|
+
<% } -%>
|
|
20
61
|
|
|
21
62
|
// The fetched rows' shape isn't known to this generator -- read fields
|
|
22
63
|
// defensively rather than declaring (and likely getting wrong) a fake interface.
|
|
@@ -186,7 +227,14 @@ onUnmounted(() => {
|
|
|
186
227
|
<% columns.forEach(function (column) { -%>
|
|
187
228
|
<% if (column.type === 'badge') { -%>
|
|
188
229
|
<template #cell-<%= column.key %>="{ row }">
|
|
189
|
-
<goa-badge
|
|
230
|
+
<goa-badge
|
|
231
|
+
:type="badgeType(columnBadgeTypes['<%= column.key %>'], row['<%= column.key %>'])"
|
|
232
|
+
:content="optionLabel(columnOptions['<%= column.key %>'], row['<%= column.key %>'])"
|
|
233
|
+
/>
|
|
234
|
+
</template>
|
|
235
|
+
<% } else if (column.options && column.options.length) { -%>
|
|
236
|
+
<template #cell-<%= column.key %>="{ row }">
|
|
237
|
+
{{ optionLabel(columnOptions['<%= column.key %>'], row['<%= column.key %>']) }}
|
|
190
238
|
</template>
|
|
191
239
|
<% } else if (column.type === 'date') { -%>
|
|
192
240
|
<template #cell-<%= column.key %>="{ row }">
|
|
@@ -3,6 +3,26 @@ export interface WorkspaceViewColumn {
|
|
|
3
3
|
label: string;
|
|
4
4
|
type?: 'text' | 'date' | 'currency' | 'badge';
|
|
5
5
|
sortable?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Value -> label for a coded column. Supply this whenever the stored value is
|
|
8
|
+
* a code rather than prose, or the table renders the raw code.
|
|
9
|
+
*/
|
|
10
|
+
options?: { value: string; label: string }[];
|
|
11
|
+
/**
|
|
12
|
+
* `badge` only. Value -> goa-badge type, e.g.
|
|
13
|
+
* `{ approved: 'success', declined: 'emergency' }`. Unmapped values render
|
|
14
|
+
* neutral.
|
|
15
|
+
*/
|
|
16
|
+
badgeMap?: Record<
|
|
17
|
+
string,
|
|
18
|
+
| 'information'
|
|
19
|
+
| 'success'
|
|
20
|
+
| 'important'
|
|
21
|
+
| 'emergency'
|
|
22
|
+
| 'dark'
|
|
23
|
+
| 'midtone'
|
|
24
|
+
| 'light'
|
|
25
|
+
>;
|
|
6
26
|
}
|
|
7
27
|
|
|
8
28
|
export interface WorkspaceViewFilter {
|
|
@@ -32,6 +52,8 @@ export interface Schema {
|
|
|
32
52
|
heading?: string;
|
|
33
53
|
pageSize?: number;
|
|
34
54
|
filterable?: boolean;
|
|
55
|
+
/** Ionicon name for the side-menu entry. Defaults per generator. */
|
|
56
|
+
icon?: string;
|
|
35
57
|
requiresAuth?: boolean;
|
|
36
58
|
}
|
|
37
59
|
|