@abgov/nx-adsp 13.27.1 → 13.28.0
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/package.json +1 -1
- package/src/generators/vue-admin-crud/vue-admin-crud.js +2 -0
- package/src/generators/vue-admin-crud/vue-admin-crud.js.map +1 -1
- package/src/generators/vue-admin-crud/vue-admin-crud.spec.ts +1 -1
- package/src/generators/vue-components/files/src/lib/patterns/Stepper.spec.ts__tmpl__ +42 -0
- package/src/generators/vue-components/files/src/lib/patterns/Stepper.vue__tmpl__ +18 -2
- package/src/generators/vue-components/vue-components.spec.ts +9 -0
- package/src/generators/vue-detail-view/vue-detail-view.spec.ts +2 -0
- package/src/generators/vue-intake-view/files/steps/src/views/__stepViewFileName__.vue__tmpl__ +1 -1
- package/src/generators/vue-intake-view/vue-intake-view.js +7 -1
- package/src/generators/vue-intake-view/vue-intake-view.js.map +1 -1
- package/src/generators/vue-intake-view/vue-intake-view.spec.ts +9 -1
- package/src/generators/vue-workspace-view/files/src/views/__viewFileName__.vue__tmpl__ +53 -1
- package/src/generators/vue-workspace-view/schema.d.ts +15 -1
- package/src/generators/vue-workspace-view/schema.json +11 -1
- package/src/generators/vue-workspace-view/vue-workspace-view.js +18 -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 +68 -1
- package/src/utils/vue-router.d.ts +7 -0
- package/src/utils/vue-router.js +6 -2
- package/src/utils/vue-router.js.map +1 -1
package/package.json
CHANGED
|
@@ -43,11 +43,13 @@ function default_1(host, options) {
|
|
|
43
43
|
path: `${normalizedOptions.route}/:id`,
|
|
44
44
|
componentImportPath: `../views/${normalizedOptions.editViewFileName}.vue`,
|
|
45
45
|
requiresAuth: normalizedOptions.requiresAuth,
|
|
46
|
+
layout: 'form',
|
|
46
47
|
});
|
|
47
48
|
(0, vue_router_1.insertVueRoute)(host, normalizedOptions.projectRoot, normalizedOptions.project, {
|
|
48
49
|
path: normalizedOptions.route,
|
|
49
50
|
componentImportPath: `../views/${normalizedOptions.listViewFileName}.vue`,
|
|
50
51
|
requiresAuth: normalizedOptions.requiresAuth,
|
|
52
|
+
layout: 'wide',
|
|
51
53
|
});
|
|
52
54
|
yield (0, devkit_1.formatFiles)(host);
|
|
53
55
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-admin-crud.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-admin-crud/vue-admin-crud.ts"],"names":[],"mappings":";;AAkDA,
|
|
1
|
+
{"version":3,"file":"vue-admin-crud.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-admin-crud/vue-admin-crud.ts"],"names":[],"mappings":";;AAkDA,4BAmCC;;AArFD,uCAMoB;AACpB,6BAA6B;AAC7B,uDAAwD;AACxD,qEAE0C;AAG1C,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAC9E,2EAA2E;AAC3E,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,sFAAsF,CACvF,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,MAAM,SAAS,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;IAChD,0EAA0E;IAC1E,+BAA+B;IAC/B,MAAM,OAAO,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/E,uCACK,OAAO,KACV,WAAW,EACX,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EACnC,gBAAgB,EAAE,GAAG,SAAS,UAAU,EACxC,gBAAgB,EAAE,GAAG,SAAS,UAAU,EACxC,OAAO,EACP,aAAa,EAAE,MAAA,OAAO,CAAC,aAAa,mCAAI,OAAO,EAC/C,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,2EAA2E;QAC3E,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,EAC5C,IAAI,EAAE,EAAE,IAEX,CAAC;QAEF,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,IAAA,2BAAc,EAAC,IAAI,EAAE,iBAAiB,CAAC,WAAW,EAAE,iBAAiB,CAAC,OAAO,EAAE;YAC7E,IAAI,EAAE,GAAG,iBAAiB,CAAC,KAAK,MAAM;YACtC,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,gBAAgB,MAAM;YACzE,YAAY,EAAE,iBAAiB,CAAC,YAAY;YAC5C,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;QACH,IAAA,2BAAc,EAAC,IAAI,EAAE,iBAAiB,CAAC,WAAW,EAAE,iBAAiB,CAAC,OAAO,EAAE;YAC7E,IAAI,EAAE,iBAAiB,CAAC,KAAK;YAC7B,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,gBAAgB,MAAM;YACzE,YAAY,EAAE,iBAAiB,CAAC,YAAY;YAC5C,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;QAEH,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA"}
|
|
@@ -177,7 +177,7 @@ describe('Vue Admin CRUD Generator', () => {
|
|
|
177
177
|
"component: () => import('../views/RegionsEditView.vue')",
|
|
178
178
|
);
|
|
179
179
|
expect(
|
|
180
|
-
routerTs.split('
|
|
180
|
+
routerTs.split('requiresAuth: true').length - 1,
|
|
181
181
|
).toBe(2);
|
|
182
182
|
// The existing route is untouched, not replaced.
|
|
183
183
|
expect(routerTs).toContain("{ path: '/', component: HomeView }");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import Stepper from './Stepper.vue';
|
|
4
|
+
|
|
5
|
+
// goa-form-stepper's own `step` prop (1-based) drives the progress bar and the
|
|
6
|
+
// current-step emphasis; goa-form-step's status enum has no "current" value.
|
|
7
|
+
// Without it the element defaults to 1 and every page of a wizard shows step one
|
|
8
|
+
// as current.
|
|
9
|
+
const steps = [
|
|
10
|
+
{ key: 'a', label: 'A', status: 'complete' as const },
|
|
11
|
+
{ key: 'b', label: 'B', status: 'incomplete' as const },
|
|
12
|
+
{ key: 'c', label: 'C', status: 'not-started' as const },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const stepAttr = (step?: number) =>
|
|
16
|
+
mount(Stepper, { props: step === undefined ? { steps } : { steps, step } })
|
|
17
|
+
.find('goa-form-stepper')
|
|
18
|
+
.attributes('step');
|
|
19
|
+
|
|
20
|
+
describe('Stepper', () => {
|
|
21
|
+
it('forwards the current step to goa-form-stepper', () => {
|
|
22
|
+
expect(stepAttr(2)).toBe('2');
|
|
23
|
+
expect(stepAttr(3)).toBe('3');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('defaults to the first step', () => {
|
|
27
|
+
expect(stepAttr()).toBe('1');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('clamps out of range rather than handing the element a bad progress value', () => {
|
|
31
|
+
// goa-form-stepper sets a <progress> value from this; out of range yields a
|
|
32
|
+
// non-finite value that throws in the browser.
|
|
33
|
+
expect(stepAttr(0)).toBe('1');
|
|
34
|
+
expect(stepAttr(-5)).toBe('1');
|
|
35
|
+
expect(stepAttr(99)).toBe('3');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('clamps to 1 when there are no steps', () => {
|
|
39
|
+
const el = mount(Stepper, { props: { steps: [], step: 4 } }).find('goa-form-stepper');
|
|
40
|
+
expect(el.attributes('step')).toBe('1');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
2
3
|
// Wraps the real, native goa-form-stepper/goa-form-step pair (confirmed via
|
|
3
4
|
// design.alberta.ca/components/form-stepper -- there wasn't one when the spec
|
|
4
5
|
// this is based on was written, hence "nobody used a native GoA stepper"; that
|
|
@@ -17,12 +18,27 @@ export interface StepperStep {
|
|
|
17
18
|
status: 'complete' | 'incomplete' | 'not-started';
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// `step` is goa-form-stepper's OWN prop (1-based) and is what drives the
|
|
22
|
+
// progress bar and the current-step emphasis. Without it the element defaults
|
|
23
|
+
// to 1, so every page of a wizard renders step one as current and the internal
|
|
24
|
+
// <progress> sits at zero. goa-form-step's status enum has no "current" value —
|
|
25
|
+
// that state lives on the parent, which is easy to miss when reading only the
|
|
26
|
+
// child's API.
|
|
27
|
+
const props = withDefaults(
|
|
28
|
+
defineProps<{ steps: StepperStep[]; step?: number }>(),
|
|
29
|
+
{ step: 1 },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Clamped: the element sets a <progress> value from this, and an out-of-range
|
|
33
|
+
// number yields a non-finite value that throws in the browser.
|
|
34
|
+
const currentStep = computed(() =>
|
|
35
|
+
Math.min(Math.max(props.step, 1), Math.max(props.steps.length, 1)),
|
|
36
|
+
);
|
|
21
37
|
const emit = defineEmits<{ select: [key: string] }>();
|
|
22
38
|
</script>
|
|
23
39
|
|
|
24
40
|
<template>
|
|
25
|
-
<goa-form-stepper>
|
|
41
|
+
<goa-form-stepper :step="currentStep">
|
|
26
42
|
<goa-form-step
|
|
27
43
|
v-for="(step, index) in props.steps"
|
|
28
44
|
:key="step.key"
|
|
@@ -93,6 +93,14 @@ describe('Vue Components Generator', () => {
|
|
|
93
93
|
const formattersSrc = host
|
|
94
94
|
.read('libs/vue-components/src/lib/formatters.ts')
|
|
95
95
|
.toString();
|
|
96
|
+
// goa-form-stepper's `step` prop drives the progress bar; without it every
|
|
97
|
+
// page of a wizard renders step one as current.
|
|
98
|
+
const stepper = host
|
|
99
|
+
.read('libs/vue-components/src/lib/patterns/Stepper.vue')
|
|
100
|
+
.toString();
|
|
101
|
+
expect(stepper).toContain(':step="currentStep"');
|
|
102
|
+
expect(stepper).toContain('Math.min(Math.max(props.step, 1)');
|
|
103
|
+
|
|
96
104
|
expect(formattersSrc).toContain('DATE_ONLY');
|
|
97
105
|
expect(formattersSrc).toContain('local.getFullYear() === year');
|
|
98
106
|
|
|
@@ -121,6 +129,7 @@ describe('Vue Components Generator', () => {
|
|
|
121
129
|
'libs/vue-components/src/lib/patterns/FilterBar.spec.ts',
|
|
122
130
|
'libs/vue-components/src/lib/primitives/GoabDatePicker.spec.ts',
|
|
123
131
|
'libs/vue-components/src/lib/patterns/WorkspaceTable.spec.ts',
|
|
132
|
+
'libs/vue-components/src/lib/patterns/Stepper.spec.ts',
|
|
124
133
|
]) {
|
|
125
134
|
expect(host.exists(spec)).toBeTruthy();
|
|
126
135
|
}
|
|
@@ -153,6 +153,8 @@ describe('Vue Detail View Generator', () => {
|
|
|
153
153
|
"component: () => import('../views/ApplicationDetailView.vue')",
|
|
154
154
|
);
|
|
155
155
|
expect(routerTs).toContain('meta: { requiresAuth: true }');
|
|
156
|
+
// A record info list is neither a table nor a form -- default width.
|
|
157
|
+
expect(routerTs).not.toContain('layout:');
|
|
156
158
|
// The existing route is untouched, not replaced.
|
|
157
159
|
expect(routerTs).toContain("{ path: '/', component: HomeView }");
|
|
158
160
|
}, 30000);
|
|
@@ -53,7 +53,11 @@ function default_1(host, options) {
|
|
|
53
53
|
steps.forEach((step, index) => {
|
|
54
54
|
const nextStepKey = index === steps.length - 1 ? 'review' : steps[index + 1].key;
|
|
55
55
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files/steps'), projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath, stepViewFileName: stepViewFileName(step.key), stepKey: step.key, stepLabel: step.label, stepFields: step.fields, stepperSteps,
|
|
56
|
-
nextStepKey,
|
|
56
|
+
nextStepKey,
|
|
57
|
+
// 1-based, for goa-form-stepper's own `step` prop. The generator knows
|
|
58
|
+
// which step it is emitting; the view would otherwise have to re-derive
|
|
59
|
+
// it from the route.
|
|
60
|
+
stepNumber: index + 1, tmpl: '' }));
|
|
57
61
|
});
|
|
58
62
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files/shared'), projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath,
|
|
59
63
|
reviewViewFileName,
|
|
@@ -70,6 +74,7 @@ function default_1(host, options) {
|
|
|
70
74
|
path: `${normalizedOptions.route}/:id/review`,
|
|
71
75
|
componentImportPath: `../views/${reviewViewFileName}.vue`,
|
|
72
76
|
requiresAuth: normalizedOptions.requiresAuth,
|
|
77
|
+
layout: 'form',
|
|
73
78
|
});
|
|
74
79
|
for (let i = steps.length - 1; i >= 0; i--) {
|
|
75
80
|
const step = steps[i];
|
|
@@ -77,6 +82,7 @@ function default_1(host, options) {
|
|
|
77
82
|
path: `${normalizedOptions.route}/:id/${step.key}`,
|
|
78
83
|
componentImportPath: `../views/${stepViewFileName(step.key)}.vue`,
|
|
79
84
|
requiresAuth: normalizedOptions.requiresAuth,
|
|
85
|
+
layout: 'form',
|
|
80
86
|
});
|
|
81
87
|
}
|
|
82
88
|
yield (0, devkit_1.formatFiles)(host);
|
|
@@ -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":";;AAsDA,
|
|
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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"}
|
|
@@ -107,6 +107,14 @@ describe('Vue Intake View Generator', () => {
|
|
|
107
107
|
.toString();
|
|
108
108
|
// Last step in the list moves to review, not another step.
|
|
109
109
|
expect(step2).toContain('/applications/${nextId}/review');
|
|
110
|
+
// Each step tells the Stepper which step it is (1-based) -- the generator
|
|
111
|
+
// knows, so the view doesn't re-derive it from the route. Without it every
|
|
112
|
+
// page renders step one as current.
|
|
113
|
+
expect(step1).toContain(':step="1"');
|
|
114
|
+
expect(step2).toContain(':step="2"');
|
|
115
|
+
// Single-column forms ask for the narrow variant.
|
|
116
|
+
const routerForm = host.read('apps/test/src/router/index.ts').toString();
|
|
117
|
+
expect(routerForm).toContain("layout: 'form'");
|
|
110
118
|
// A required field gets a validation block + requirement="required"; an
|
|
111
119
|
// explicitly non-required one gets neither.
|
|
112
120
|
expect(step2).toContain("found.push({ message: 'Email is required.'");
|
|
@@ -156,7 +164,7 @@ describe('Vue Intake View Generator', () => {
|
|
|
156
164
|
"component: () => import('../views/ApplicationConfirmationView.vue')",
|
|
157
165
|
);
|
|
158
166
|
expect(
|
|
159
|
-
routerTs.split('
|
|
167
|
+
routerTs.split('requiresAuth: true').length - 1,
|
|
160
168
|
).toBe(4);
|
|
161
169
|
// The existing route is untouched, not replaced.
|
|
162
170
|
expect(routerTs).toContain("{ path: '/', component: HomeView }");
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, onMounted<% if (filterable) { %>, onUnmounted<% } %> } from 'vue';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
WorkspaceTable,
|
|
5
|
+
<% if (filters.length) { -%>
|
|
6
|
+
FilterBar,
|
|
7
|
+
<% } -%>
|
|
8
|
+
formatCurrency,
|
|
9
|
+
formatDate,
|
|
10
|
+
} from '<%= goaImportPath %>';
|
|
4
11
|
import { useApi } from '../composables/useApi';
|
|
5
12
|
|
|
6
13
|
const { list } = useApi();
|
|
@@ -25,6 +32,38 @@ const search = ref('');
|
|
|
25
32
|
let searchDebounce: ReturnType<typeof setTimeout> | undefined;
|
|
26
33
|
<% } -%>
|
|
27
34
|
|
|
35
|
+
<% if (filters.length) { -%>
|
|
36
|
+
// A ref, not a const: a dropdown's options can be fetched (load them in `load`
|
|
37
|
+
// or a second call and assign into the matching descriptor) without changing
|
|
38
|
+
// FilterBar's contract.
|
|
39
|
+
const filterDescriptors = ref([
|
|
40
|
+
<% filters.forEach(function (filter) { -%>
|
|
41
|
+
{
|
|
42
|
+
key: '<%- filter.key %>',
|
|
43
|
+
label: '<%- filter.label %>',
|
|
44
|
+
type: '<%- filter.type %>' as const,
|
|
45
|
+
<% if (filter.type === 'dropdown') { -%>
|
|
46
|
+
options: <%- JSON.stringify(filter.options ?? []) %>,
|
|
47
|
+
<% if (filter.anyLabel) { -%>
|
|
48
|
+
anyLabel: '<%- filter.anyLabel %>',
|
|
49
|
+
<% } -%>
|
|
50
|
+
<% } -%>
|
|
51
|
+
},
|
|
52
|
+
<% }); -%>
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// Query-ready values, keyed by filter. FilterBar owns the controls and the
|
|
56
|
+
// chips; the page reset and the reload below belong here because `page` is this
|
|
57
|
+
// view's state.
|
|
58
|
+
const filterValues = ref<Record<string, string>>({});
|
|
59
|
+
|
|
60
|
+
function onFiltersChange(next: Record<string, string>) {
|
|
61
|
+
filterValues.value = next;
|
|
62
|
+
page.value = 1;
|
|
63
|
+
void load();
|
|
64
|
+
}
|
|
65
|
+
<% } -%>
|
|
66
|
+
|
|
28
67
|
const PAGE_SIZE = <%= pageSize %>;
|
|
29
68
|
|
|
30
69
|
// load() fires from page change, sort<% if (filterable) { %>, and the debounced search<% } %>, so two requests can
|
|
@@ -48,6 +87,9 @@ async function load() {
|
|
|
48
87
|
<% } -%>
|
|
49
88
|
sortBy: sortBy.value,
|
|
50
89
|
sortDir: sortDir.value,
|
|
90
|
+
<% if (filters.length) { -%>
|
|
91
|
+
filters: filterValues.value,
|
|
92
|
+
<% } -%>
|
|
51
93
|
});
|
|
52
94
|
if (sequence !== loadSequence) return;
|
|
53
95
|
rows.value = result.rows;
|
|
@@ -118,6 +160,16 @@ onUnmounted(() => {
|
|
|
118
160
|
|
|
119
161
|
<goa-spacer vspacing="l" />
|
|
120
162
|
<% } -%>
|
|
163
|
+
<% if (filters.length) { -%>
|
|
164
|
+
|
|
165
|
+
<FilterBar
|
|
166
|
+
:filters="filterDescriptors"
|
|
167
|
+
:model-value="filterValues"
|
|
168
|
+
@update:model-value="onFiltersChange"
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
<goa-spacer vspacing="l" />
|
|
172
|
+
<% } -%>
|
|
121
173
|
|
|
122
174
|
<WorkspaceTable
|
|
123
175
|
:columns="columns"
|
|
@@ -5,6 +5,16 @@ export interface WorkspaceViewColumn {
|
|
|
5
5
|
sortable?: boolean;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface WorkspaceViewFilter {
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
type: 'dropdown' | 'date';
|
|
12
|
+
/** Dropdown only. Omit and populate `filterDescriptors` at runtime to fetch them. */
|
|
13
|
+
options?: { value: string; label: string }[];
|
|
14
|
+
/** Dropdown only. Label for the "no filter" choice. Defaults to "Any". */
|
|
15
|
+
anyLabel?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export interface Schema {
|
|
9
19
|
project: string;
|
|
10
20
|
name: string;
|
|
@@ -16,6 +26,8 @@ export interface Schema {
|
|
|
16
26
|
* also accepted for programmatic callers (e.g. tests).
|
|
17
27
|
*/
|
|
18
28
|
columns: string | WorkspaceViewColumn[];
|
|
29
|
+
/** Same JSON-string-on-the-CLI reasoning as `columns`. */
|
|
30
|
+
filters?: string | WorkspaceViewFilter[];
|
|
19
31
|
detailRoute?: string;
|
|
20
32
|
heading?: string;
|
|
21
33
|
pageSize?: number;
|
|
@@ -23,11 +35,13 @@ export interface Schema {
|
|
|
23
35
|
requiresAuth?: boolean;
|
|
24
36
|
}
|
|
25
37
|
|
|
26
|
-
export interface NormalizedSchema extends Omit<Schema, 'columns'> {
|
|
38
|
+
export interface NormalizedSchema extends Omit<Schema, 'columns' | 'filters'> {
|
|
27
39
|
projectRoot: string;
|
|
28
40
|
/** PascalCase view name with a "ListView" suffix, e.g. ApplicationsListView. */
|
|
29
41
|
viewFileName: string;
|
|
30
42
|
columns: WorkspaceViewColumn[];
|
|
43
|
+
/** Empty when --filters wasn't given, so the template can skip the FilterBar. */
|
|
44
|
+
filters: WorkspaceViewFilter[];
|
|
31
45
|
heading: string;
|
|
32
46
|
pageSize: number;
|
|
33
47
|
filterable: boolean;
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"type": "string",
|
|
36
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."
|
|
37
37
|
},
|
|
38
|
+
"filters": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "JSON array of filter controls rendered above the table by FilterBar, beyond the --filterable search box -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"dropdown\",\"options\":[{\"value\":\"open\",\"label\":\"Open\"}]},{\"key\":\"from\",\"label\":\"From\",\"type\":\"date\"}]'. Each item: { key, label, type: \"dropdown\"|\"date\", options?: [{value,label}], anyLabel? }. Values are sent as query parameters alongside search/page/sort, so the API decides what each key means. Omit for no filter bar. Same JSON-string-on-the-CLI reasoning as --columns."
|
|
41
|
+
},
|
|
38
42
|
"detailRoute": {
|
|
39
43
|
"type": "string",
|
|
40
44
|
"description": "If set, each row gets a \"View\" action linking to `${detailRoute}/${row.id}` -- typically a vue-detail-view's route with the :id segment dropped. Omit to leave the actions column for manual customization."
|
|
@@ -59,6 +63,12 @@
|
|
|
59
63
|
"default": true
|
|
60
64
|
}
|
|
61
65
|
},
|
|
62
|
-
"required": [
|
|
66
|
+
"required": [
|
|
67
|
+
"project",
|
|
68
|
+
"name",
|
|
69
|
+
"resource",
|
|
70
|
+
"route",
|
|
71
|
+
"columns"
|
|
72
|
+
],
|
|
63
73
|
"additionalProperties": false
|
|
64
74
|
}
|
|
@@ -20,11 +20,26 @@ function parseColumns(columns) {
|
|
|
20
20
|
}
|
|
21
21
|
return parsed;
|
|
22
22
|
}
|
|
23
|
+
// Same JSON-string-on-the-CLI reasoning as parseColumns above.
|
|
24
|
+
function parseFilters(filters) {
|
|
25
|
+
if (filters === undefined || filters === '')
|
|
26
|
+
return [];
|
|
27
|
+
const parsed = typeof filters === 'string' ? JSON.parse(filters) : filters;
|
|
28
|
+
if (!Array.isArray(parsed)) {
|
|
29
|
+
throw new Error('--filters must be a JSON array of { key, label, type: "dropdown"|"date", options?, anyLabel? } objects.');
|
|
30
|
+
}
|
|
31
|
+
for (const filter of parsed) {
|
|
32
|
+
if (filter.type !== 'dropdown' && filter.type !== 'date') {
|
|
33
|
+
throw new Error(`--filters[].type must be "dropdown" or "date"; got "${filter.type}" for "${filter.key}".`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
23
38
|
function normalizeOptions(host, options) {
|
|
24
39
|
var _a, _b, _c, _d, _e;
|
|
25
40
|
const { root: projectRoot } = (0, devkit_1.readProjectConfiguration)(host, options.project);
|
|
26
41
|
const className = (0, devkit_1.names)(options.name).className;
|
|
27
|
-
return Object.assign(Object.assign({}, options), { projectRoot, columns: parseColumns(options.columns), viewFileName: `${className}ListView`,
|
|
42
|
+
return Object.assign(Object.assign({}, options), { projectRoot, columns: parseColumns(options.columns), filters: parseFilters(options.filters), viewFileName: `${className}ListView`,
|
|
28
43
|
// className is PascalCase (e.g. "Applications") -- space it out for a
|
|
29
44
|
// readable default heading ("Applications").
|
|
30
45
|
heading: (_a = options.heading) !== null && _a !== void 0 ? _a : className.replace(/([A-Z])/g, ' $1').trim(), pageSize: (_b = options.pageSize) !== null && _b !== void 0 ? _b : 20, filterable: (_c = options.filterable) !== null && _c !== void 0 ? _c : true, requiresAuth: (_d = options.requiresAuth) !== null && _d !== void 0 ? _d : true,
|
|
@@ -44,6 +59,8 @@ function default_1(host, options) {
|
|
|
44
59
|
path: normalizedOptions.route,
|
|
45
60
|
componentImportPath: `../views/${normalizedOptions.viewFileName}.vue`,
|
|
46
61
|
requiresAuth: normalizedOptions.requiresAuth,
|
|
62
|
+
// A multi-column table at the 1000px default wraps every cell.
|
|
63
|
+
layout: 'wide',
|
|
47
64
|
});
|
|
48
65
|
yield (0, devkit_1.formatFiles)(host);
|
|
49
66
|
});
|
|
@@ -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":";;
|
|
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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"}
|
|
@@ -149,6 +149,71 @@ describe('Vue Workspace View Generator', () => {
|
|
|
149
149
|
expect(view).toContain('searchDebounce');
|
|
150
150
|
}, 30000);
|
|
151
151
|
|
|
152
|
+
describe('--filters', () => {
|
|
153
|
+
const filters = JSON.stringify([
|
|
154
|
+
{
|
|
155
|
+
key: 'status',
|
|
156
|
+
label: 'Status',
|
|
157
|
+
type: 'dropdown',
|
|
158
|
+
options: [{ value: 'open', label: 'Open' }],
|
|
159
|
+
anyLabel: 'All statuses',
|
|
160
|
+
},
|
|
161
|
+
{ key: 'from', label: 'From', type: 'date' },
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
it('wires the shared FilterBar and sends the values as query filters', async () => {
|
|
165
|
+
await generator(host, { ...baseOptions, filters });
|
|
166
|
+
const view = host
|
|
167
|
+
.read('apps/test/src/views/ApplicationsListView.vue')
|
|
168
|
+
.toString();
|
|
169
|
+
|
|
170
|
+
expect(view).toContain('FilterBar');
|
|
171
|
+
expect(view).toContain(':filters="filterDescriptors"');
|
|
172
|
+
expect(view).toContain('@update:model-value="onFiltersChange"');
|
|
173
|
+
// The values object goes straight through useApi's adapter.
|
|
174
|
+
expect(view).toContain('filters: filterValues.value');
|
|
175
|
+
// The view owns the page reset, not FilterBar -- it owns `page`.
|
|
176
|
+
expect(view).toContain('page.value = 1');
|
|
177
|
+
// Descriptors are a ref so fetched dropdown options can be assigned in.
|
|
178
|
+
expect(view).toContain('const filterDescriptors = ref([');
|
|
179
|
+
expect(view).toContain("anyLabel: 'All statuses'");
|
|
180
|
+
expect(view).toContain("type: 'date' as const");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('leaves output byte-identical when --filters is omitted', async () => {
|
|
184
|
+
await generator(host, baseOptions);
|
|
185
|
+
const without = host
|
|
186
|
+
.read('apps/test/src/views/ApplicationsListView.vue')
|
|
187
|
+
.toString();
|
|
188
|
+
expect(without).not.toContain('FilterBar');
|
|
189
|
+
expect(without).not.toContain('filterValues');
|
|
190
|
+
expect(without).not.toContain('onFiltersChange');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects a filter type it cannot render, naming the offender', async () => {
|
|
194
|
+
await expect(
|
|
195
|
+
generator(host, {
|
|
196
|
+
...baseOptions,
|
|
197
|
+
filters: JSON.stringify([
|
|
198
|
+
{ key: 'x', label: 'X', type: 'checkbox' },
|
|
199
|
+
]),
|
|
200
|
+
}),
|
|
201
|
+
).rejects.toThrow('--filters[].type must be "dropdown" or "date"; got "checkbox" for "x".');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('accepts a real array, the form a programmatic caller passes', async () => {
|
|
205
|
+
await generator(host, {
|
|
206
|
+
...baseOptions,
|
|
207
|
+
filters: [{ key: 'status', label: 'Status', type: 'dropdown' }],
|
|
208
|
+
});
|
|
209
|
+
const view = host
|
|
210
|
+
.read('apps/test/src/views/ApplicationsListView.vue')
|
|
211
|
+
.toString();
|
|
212
|
+
expect(view).toContain('FilterBar');
|
|
213
|
+
expect(view).toContain('options: []');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
152
217
|
it('omits onUnmounted along with the debounce when --filterable=false', async () => {
|
|
153
218
|
// onUnmounted exists only to clear the search debounce, so importing it
|
|
154
219
|
// unconditionally would be an unused import and fail the generated lint.
|
|
@@ -206,7 +271,9 @@ describe('Vue Workspace View Generator', () => {
|
|
|
206
271
|
expect(routerTs).toContain(
|
|
207
272
|
"component: () => import('../views/ApplicationsListView.vue')",
|
|
208
273
|
);
|
|
209
|
-
expect(routerTs).toContain('
|
|
274
|
+
expect(routerTs).toContain('requiresAuth: true');
|
|
275
|
+
// A data table asks for the wide variant -- the generator knows it emitted one.
|
|
276
|
+
expect(routerTs).toContain("layout: 'wide'");
|
|
210
277
|
expect(routerTs).toContain("{ path: '/', component: HomeView }");
|
|
211
278
|
}, 30000);
|
|
212
279
|
|
|
@@ -4,5 +4,12 @@ export interface VueRouteSpec {
|
|
|
4
4
|
/** Relative import path passed to () => import(...), e.g. '../views/FooView.vue'. */
|
|
5
5
|
componentImportPath: string;
|
|
6
6
|
requiresAuth?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Content width for the view, read by App.vue's `contentWidth` and passed to
|
|
9
|
+
* AppLayout. Omit for the 1000px default; a generator emitting a data table
|
|
10
|
+
* should say 'wide' and one emitting a single-column form 'form', since it
|
|
11
|
+
* already knows which it produced.
|
|
12
|
+
*/
|
|
13
|
+
layout?: 'wide' | 'form';
|
|
7
14
|
}
|
|
8
15
|
export declare function insertVueRoute(host: Tree, projectRoot: string, project: string, route: VueRouteSpec): void;
|
package/src/utils/vue-router.js
CHANGED
|
@@ -16,8 +16,12 @@ function insertVueRoute(host, projectRoot, project, route) {
|
|
|
16
16
|
if (markerIndex === -1) {
|
|
17
17
|
throw new Error(`Could not find "${marker}" in ${routerPath} -- has its shape changed since @abgov/nx-adsp:vue-app generated it?`);
|
|
18
18
|
}
|
|
19
|
-
const
|
|
20
|
-
?
|
|
19
|
+
const metaEntries = [
|
|
20
|
+
...(route.requiresAuth ? ['requiresAuth: true'] : []),
|
|
21
|
+
...(route.layout ? [`layout: '${route.layout}'`] : []),
|
|
22
|
+
];
|
|
23
|
+
const metaLine = metaEntries.length
|
|
24
|
+
? `\n meta: { ${metaEntries.join(', ')} },`
|
|
21
25
|
: '';
|
|
22
26
|
const routeSnippet = `{\n` +
|
|
23
27
|
` path: '${route.path}',\n` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-router.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/vue-router.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"vue-router.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/vue-router.ts"],"names":[],"mappings":";;AAmBA,wCAyCC;AA5CD,6EAA6E;AAC7E,wEAAwE;AACxE,6EAA6E;AAC7E,SAAgB,cAAc,CAC5B,IAAU,EACV,WAAmB,EACnB,OAAe,EACf,KAAmB;;IAEnB,MAAM,UAAU,GAAG,GAAG,WAAW,sBAAsB,CAAC;IACxD,MAAM,OAAO,GAAG,MAAA,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,0CAAE,QAAQ,EAAE,CAAC;IAClD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,kBAAkB,UAAU,WAAW,OAAO,0DAA0D,CACzG,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC;IAC3B,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,mBAAmB,MAAM,QAAQ,UAAU,sEAAsE,CAClH,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG;QAClB,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACvD,CAAC;IACF,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM;QACjC,CAAC,CAAC,mBAAmB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QAChD,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,YAAY,GAChB,KAAK;QACL,gBAAgB,KAAK,CAAC,IAAI,MAAM;QAChC,kCAAkC,KAAK,CAAC,mBAAmB,MAAM,QAAQ,IAAI;QAC7E,QAAQ,CAAC;IAEX,MAAM,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,MAAM,OAAO,GACX,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC;QAC1B,SAAS,YAAY,EAAE;QACvB,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC1B,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC"}
|