@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
|
@@ -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 () => {
|
|
214
232
|
await generator(host, baseOptions);
|
|
215
233
|
const confirmation = host
|
|
216
234
|
.read('apps/test/src/views/ApplicationConfirmationView.vue')
|
|
217
235
|
.toString();
|
|
218
|
-
|
|
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 () => {
|
|
253
|
+
await generator(host, baseOptions);
|
|
254
|
+
const confirmation = host
|
|
255
|
+
.read('apps/test/src/views/ApplicationConfirmationView.vue')
|
|
256
|
+
.toString();
|
|
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,66 @@ 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);
|
|
346
|
+
|
|
347
|
+
it('spaces stacked form items in each step', async () => {
|
|
348
|
+
await generator(host, baseOptions);
|
|
349
|
+
const step = host
|
|
350
|
+
.read('apps/test/src/views/PersonalInfoStepView.vue')
|
|
351
|
+
.toString();
|
|
352
|
+
// Counted per tag rather than as a contiguous string: formatFiles reflows a
|
|
353
|
+
// multi-attribute tag across lines, so `<goa-form-item mb="l"` is not a
|
|
354
|
+
// reliable substring.
|
|
355
|
+
const tags = step
|
|
356
|
+
.split('<goa-form-item')
|
|
357
|
+
.slice(1)
|
|
358
|
+
.map((fragment) => fragment.slice(0, fragment.indexOf('>')));
|
|
359
|
+
expect(tags.length).toBeGreaterThan(0);
|
|
360
|
+
for (const tag of tags) {
|
|
361
|
+
expect(tag).toContain('mb="l"');
|
|
362
|
+
}
|
|
363
|
+
}, 30000);
|
|
364
|
+
|
|
365
|
+
// Regression: the review page's <dl> was unstyled, so terms and values stacked
|
|
366
|
+
// flush -- every answer ran into the next on the page whose entire purpose is
|
|
367
|
+
// being read back before submitting. vue-detail-view already styled its
|
|
368
|
+
// equivalent list; this is the same fix, not a new idea.
|
|
369
|
+
it('gives the review page label/value list real spacing', async () => {
|
|
370
|
+
await generator(host, baseOptions);
|
|
371
|
+
const review = host
|
|
372
|
+
.read('apps/test/src/views/ApplicationReviewView.vue')
|
|
373
|
+
.toString();
|
|
374
|
+
expect(review).toContain('<dl class="review-fields">');
|
|
375
|
+
expect(review).toContain('grid-template-columns: auto 1fr');
|
|
376
|
+
expect(review).toContain('gap: var(--goa-space-xs) var(--goa-space-l)');
|
|
377
|
+
}, 30000);
|
|
277
378
|
});
|
|
@@ -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
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"resource": {
|
|
27
27
|
"type": "string",
|
|
28
|
-
"description": "API resource path segment -- the view fetches /api/<resource>?page=&limit=&search=&sortBy=&sortDir=."
|
|
28
|
+
"description": "API resource path segment -- the view fetches /api/v1/<resource>?page=&limit=&search=&sortBy=&sortDir=."
|
|
29
29
|
},
|
|
30
30
|
"route": {
|
|
31
31
|
"type": "string",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"columns": {
|
|
35
35
|
"type": "string",
|
|
36
|
-
"description": "JSON array of table columns, in display order -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"badge\",\"sortable\":true}]'. Each item: { key, label, type?: \"text\"|\"date\"|\"currency\"|\"badge\" (default \"text\"), sortable?: boolean }. 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 table columns, in display order -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"badge\",\"sortable\":true}]'. Each item: { key, label, type?: \"text\"|\"date\"|\"currency\"|\"badge\" (default \"text\"), sortable?: boolean, options?: [{value,label}], badgeMap?: {value: badgeType} }. Supply `options` for any column whose stored value is a code, or the table renders the raw code. Supply `badgeMap` on a `badge` column to give each status its colour (e.g. {\"approved\":\"success\",\"declined\":\"emergency\"}); 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
|
"filters": {
|
|
39
39
|
"type": "string",
|
|
@@ -57,18 +57,17 @@
|
|
|
57
57
|
"description": "Whether to generate a debounced search input above the table.",
|
|
58
58
|
"default": true
|
|
59
59
|
},
|
|
60
|
+
"icon": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Ionicon name for the generated side-menu entry, e.g. `list`. goa-work-side-menu-item renders a blank item without one.",
|
|
63
|
+
"default": "list"
|
|
64
|
+
},
|
|
60
65
|
"requiresAuth": {
|
|
61
66
|
"type": "boolean",
|
|
62
67
|
"description": "Whether the generated route requires authentication.",
|
|
63
68
|
"default": true
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
|
-
"required": [
|
|
67
|
-
"project",
|
|
68
|
-
"name",
|
|
69
|
-
"resource",
|
|
70
|
-
"route",
|
|
71
|
-
"columns"
|
|
72
|
-
],
|
|
71
|
+
"required": ["project", "name", "resource", "route", "columns"],
|
|
73
72
|
"additionalProperties": false
|
|
74
73
|
}
|
|
@@ -51,11 +51,16 @@ function normalizeOptions(host, options) {
|
|
|
51
51
|
}
|
|
52
52
|
function default_1(host, options) {
|
|
53
53
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
54
|
+
var _a;
|
|
54
55
|
const normalizedOptions = normalizeOptions(host, options);
|
|
55
56
|
// Idempotent -- ensures WorkspaceTable exists even in a project scaffolded
|
|
56
57
|
// before vue-components carried it.
|
|
57
58
|
yield (0, vue_components_1.default)(host);
|
|
58
|
-
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host),
|
|
59
|
+
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host),
|
|
60
|
+
// Derived here rather than tested inline in the template: EJS throws a
|
|
61
|
+
// ReferenceError on a bare undefined variable, so a flag the template
|
|
62
|
+
// reads must always be defined.
|
|
63
|
+
hasCodedColumns: normalizedOptions.columns.some((column) => { var _a; return (_a = column.options) === null || _a === void 0 ? void 0 : _a.length; }), hasBadgeColumns: normalizedOptions.columns.some((column) => column.type === 'badge'), tmpl: '' }));
|
|
59
64
|
(0, vue_router_1.insertVueRoute)(host, normalizedOptions.projectRoot, normalizedOptions.project, {
|
|
60
65
|
path: normalizedOptions.route,
|
|
61
66
|
componentImportPath: `../views/${normalizedOptions.viewFileName}.vue`,
|
|
@@ -67,6 +72,7 @@ function default_1(host, options) {
|
|
|
67
72
|
(0, vue_side_menu_1.insertSideMenuItem)(host, normalizedOptions.projectRoot, {
|
|
68
73
|
label: normalizedOptions.heading,
|
|
69
74
|
to: normalizedOptions.route,
|
|
75
|
+
icon: (_a = normalizedOptions.icon) !== null && _a !== void 0 ? _a : 'list',
|
|
70
76
|
});
|
|
71
77
|
yield (0, devkit_1.formatFiles)(host);
|
|
72
78
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-workspace-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-workspace-view/vue-workspace-view.ts"],"names":[],"mappings":";;AA+EA,
|
|
1
|
+
{"version":3,"file":"vue-workspace-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-workspace-view/vue-workspace-view.ts"],"names":[],"mappings":"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"}
|
|
@@ -125,7 +125,9 @@ describe('Vue Workspace View Generator', () => {
|
|
|
125
125
|
expect(view).toContain('let loadSequence = 0');
|
|
126
126
|
expect(view).toContain('const sequence = ++loadSequence');
|
|
127
127
|
expect(view).toContain('if (sequence !== loadSequence) return;');
|
|
128
|
-
expect(view).toContain(
|
|
128
|
+
expect(view).toContain(
|
|
129
|
+
'if (sequence === loadSequence) loading.value = false;',
|
|
130
|
+
);
|
|
129
131
|
|
|
130
132
|
// The debounce timer must not outlive the component.
|
|
131
133
|
expect(view).toContain('onUnmounted(');
|
|
@@ -138,7 +140,7 @@ describe('Vue Workspace View Generator', () => {
|
|
|
138
140
|
expect(view).not.toContain('Intl.');
|
|
139
141
|
expect(view).not.toContain('function formatCurrency');
|
|
140
142
|
expect(view).toContain(
|
|
141
|
-
"
|
|
143
|
+
"badgeType(columnBadgeTypes['status'], row['status'])",
|
|
142
144
|
);
|
|
143
145
|
// A type: 'date' column is a calendar date -- formatDate, not
|
|
144
146
|
// formatDateTime, which would show an invented midnight.
|
|
@@ -205,11 +207,11 @@ describe('Vue Workspace View Generator', () => {
|
|
|
205
207
|
await expect(
|
|
206
208
|
generator(host, {
|
|
207
209
|
...baseOptions,
|
|
208
|
-
filters: JSON.stringify([
|
|
209
|
-
{ key: 'x', label: 'X', type: 'checkbox' },
|
|
210
|
-
]),
|
|
210
|
+
filters: JSON.stringify([{ key: 'x', label: 'X', type: 'checkbox' }]),
|
|
211
211
|
}),
|
|
212
|
-
).rejects.toThrow(
|
|
212
|
+
).rejects.toThrow(
|
|
213
|
+
'--filters[].type must be "dropdown" or "date"; got "checkbox" for "x".',
|
|
214
|
+
);
|
|
213
215
|
});
|
|
214
216
|
|
|
215
217
|
it('accepts a real array, the form a programmatic caller passes', async () => {
|
|
@@ -265,7 +267,11 @@ describe('Vue Workspace View Generator', () => {
|
|
|
265
267
|
}, 30000);
|
|
266
268
|
|
|
267
269
|
it('respects an explicit --heading and --pageSize', async () => {
|
|
268
|
-
await generator(host, {
|
|
270
|
+
await generator(host, {
|
|
271
|
+
...baseOptions,
|
|
272
|
+
heading: 'Grant Applications',
|
|
273
|
+
pageSize: 50,
|
|
274
|
+
});
|
|
269
275
|
const view = host
|
|
270
276
|
.read('apps/test/src/views/ApplicationsListView.vue')
|
|
271
277
|
.toString();
|
|
@@ -318,4 +324,64 @@ describe('Vue Workspace View Generator', () => {
|
|
|
318
324
|
const after = readProjectConfiguration(host, 'test');
|
|
319
325
|
expect(after).toEqual(before);
|
|
320
326
|
}, 30000);
|
|
327
|
+
|
|
328
|
+
// Regression: every badge column was hardcoded type="information", so a
|
|
329
|
+
// declined claim rendered neutral blue -- a badge's colour is the fastest thing
|
|
330
|
+
// a reader takes in, so the wrong one actively misinforms.
|
|
331
|
+
it('maps badge values to semantic types and labels', async () => {
|
|
332
|
+
await generator(host, {
|
|
333
|
+
...baseOptions,
|
|
334
|
+
columns: [
|
|
335
|
+
{ key: 'reference', label: 'Reference' },
|
|
336
|
+
{
|
|
337
|
+
key: 'status',
|
|
338
|
+
label: 'Status',
|
|
339
|
+
type: 'badge',
|
|
340
|
+
options: [{ value: 'declined', label: 'Declined' }],
|
|
341
|
+
badgeMap: { approved: 'success', declined: 'emergency' },
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
const view = host
|
|
346
|
+
.read('apps/test/src/views/ApplicationsListView.vue')
|
|
347
|
+
.toString();
|
|
348
|
+
// Quote style depends on whether formatFiles ran, so match the pairs.
|
|
349
|
+
expect(view).toMatch(/["']?declined["']?\s*:\s*["']emergency["']/);
|
|
350
|
+
expect(view).toMatch(/["']?approved["']?\s*:\s*["']success["']/);
|
|
351
|
+
expect(view).toContain("badgeType(columnBadgeTypes['status']");
|
|
352
|
+
expect(view).toContain("optionLabel(columnOptions['status']");
|
|
353
|
+
}, 30000);
|
|
354
|
+
|
|
355
|
+
it('renders a coded non-badge column using its label', async () => {
|
|
356
|
+
await generator(host, {
|
|
357
|
+
...baseOptions,
|
|
358
|
+
columns: [
|
|
359
|
+
{
|
|
360
|
+
key: 'region',
|
|
361
|
+
label: 'Region',
|
|
362
|
+
options: [{ value: 'northwest', label: 'North west' }],
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
});
|
|
366
|
+
const view = host
|
|
367
|
+
.read('apps/test/src/views/ApplicationsListView.vue')
|
|
368
|
+
.toString();
|
|
369
|
+
expect(view).toContain(
|
|
370
|
+
"optionLabel(columnOptions['region'], row['region'])",
|
|
371
|
+
);
|
|
372
|
+
expect(view).toContain('North west');
|
|
373
|
+
}, 30000);
|
|
374
|
+
|
|
375
|
+
it('omits the lookup maps entirely when no column needs them', async () => {
|
|
376
|
+
await generator(host, {
|
|
377
|
+
...baseOptions,
|
|
378
|
+
columns: [{ key: 'reference', label: 'Reference' }],
|
|
379
|
+
});
|
|
380
|
+
const view = host
|
|
381
|
+
.read('apps/test/src/views/ApplicationsListView.vue')
|
|
382
|
+
.toString();
|
|
383
|
+
expect(view).not.toContain('columnOptions');
|
|
384
|
+
expect(view).not.toContain('columnBadgeTypes');
|
|
385
|
+
expect(view).not.toContain('optionLabel');
|
|
386
|
+
}, 30000);
|
|
321
387
|
});
|
|
@@ -11,6 +11,16 @@ type DevProxyEntry = {
|
|
|
11
11
|
pathRewrite: Record<string, string>;
|
|
12
12
|
};
|
|
13
13
|
export declare function buildDevProxyConf(nginxProxies: NginxProxyConfiguration[]): Record<string, DevProxyEntry>;
|
|
14
|
+
export declare const VITE_PROXY_FILENAME = "vite.proxy.cjs";
|
|
15
|
+
export interface ViteProxyRoute {
|
|
16
|
+
location: string;
|
|
17
|
+
target: string;
|
|
18
|
+
secure: boolean;
|
|
19
|
+
rewriteTo?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseViteDevProxyRoutes(source: string): ViteProxyRoute[];
|
|
22
|
+
export declare function viteDevProxyRoutes(nginxProxies: NginxProxyConfiguration[]): ViteProxyRoute[];
|
|
23
|
+
export declare function buildViteDevProxyModule(routes: ViteProxyRoute[]): string;
|
|
14
24
|
export declare function resolvePairedProjectProxy(host: Tree, pairedProject: string | undefined): PairedProjectProxy | undefined;
|
|
15
25
|
export interface PairedFrontendApp {
|
|
16
26
|
tag: string;
|