@abgov/nx-adsp 13.31.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__ +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__ +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/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__ +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
|
@@ -141,3 +141,61 @@ export function formatPercent(
|
|
|
141
141
|
}).format(value);
|
|
142
142
|
return `${rounded}%`;
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
/** One choice in a coded set -- the shape a generator's `options` list uses. */
|
|
146
|
+
export interface DisplayOption {
|
|
147
|
+
value: string;
|
|
148
|
+
label: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The human label for a coded value, e.g. 'cattle-mature' -> 'Cattle (mature)'.
|
|
153
|
+
*
|
|
154
|
+
* Every view that renders a value from a `select`/dropdown field must go through
|
|
155
|
+
* this. The generators are handed the label for each value at generation time,
|
|
156
|
+
* and rendering the bare value instead puts raw domain codes ('northwest',
|
|
157
|
+
* 'wolf') in front of the person filling the form -- most damagingly on a
|
|
158
|
+
* check-your-answers page, where the whole point is that they can read back what
|
|
159
|
+
* they entered.
|
|
160
|
+
*
|
|
161
|
+
* Falls back to the value itself, not to a dash: an unmapped code is a data or
|
|
162
|
+
* configuration problem, and showing it is far more debuggable than hiding it.
|
|
163
|
+
*/
|
|
164
|
+
export function optionLabel(
|
|
165
|
+
options: readonly DisplayOption[] | undefined,
|
|
166
|
+
value: unknown,
|
|
167
|
+
): string {
|
|
168
|
+
if (value === null || value === undefined || value === '') return '—';
|
|
169
|
+
const match = options?.find((option) => option.value === String(value));
|
|
170
|
+
return match ? match.label : String(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** goa-badge's `type`, which carries the semantic colour. */
|
|
174
|
+
export type BadgeType =
|
|
175
|
+
| 'information'
|
|
176
|
+
| 'success'
|
|
177
|
+
| 'important'
|
|
178
|
+
| 'emergency'
|
|
179
|
+
| 'dark'
|
|
180
|
+
| 'midtone'
|
|
181
|
+
| 'light';
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The badge type for a status value, from a value -> type map.
|
|
185
|
+
*
|
|
186
|
+
* A badge's colour is the fastest thing a reader takes in, so a status rendered
|
|
187
|
+
* with the wrong one actively misinforms: a declined claim in neutral blue reads
|
|
188
|
+
* as routine. Views pass a map (supplied per column/field at generation time)
|
|
189
|
+
* rather than a hardcoded type.
|
|
190
|
+
*
|
|
191
|
+
* Falls back to 'information' -- neutral, which is the honest choice for a value
|
|
192
|
+
* whose severity we were not told.
|
|
193
|
+
*/
|
|
194
|
+
export function badgeType(
|
|
195
|
+
map: Readonly<Record<string, BadgeType>> | undefined,
|
|
196
|
+
value: unknown,
|
|
197
|
+
fallback: BadgeType = 'information',
|
|
198
|
+
): BadgeType {
|
|
199
|
+
if (value === null || value === undefined || value === '') return fallback;
|
|
200
|
+
return map?.[String(value)] ?? fallback;
|
|
201
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { useSlots } from 'vue';
|
|
2
|
+
import { computed, useSlots } from 'vue';
|
|
3
3
|
|
|
4
4
|
// The alternate top-level shell for staff-facing/internal apps (goa-work-side-menu),
|
|
5
5
|
// used instead of AppHeader/AppFooter -- not together with them. Presentational only:
|
|
@@ -23,7 +23,7 @@ interface AppSideMenuItem {
|
|
|
23
23
|
current?: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
withDefaults(
|
|
26
|
+
const props = withDefaults(
|
|
27
27
|
defineProps<{
|
|
28
28
|
heading?: string;
|
|
29
29
|
userName?: string;
|
|
@@ -38,13 +38,24 @@ withDefaults(
|
|
|
38
38
|
},
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
+
// goa-work-side-menu renders its profile button whenever the `account` slot has
|
|
42
|
+
// content, and that button's ONLY accessible name is `user-name` — the element
|
|
43
|
+
// exposes no aria-label prop, and the name also feeds the icon's own label. Left
|
|
44
|
+
// empty it is an unnamed button (axe `button-name`, critical) plus an unlabelled
|
|
45
|
+
// icon (`role-img-alt`), on every route of the app.
|
|
46
|
+
//
|
|
47
|
+
// Resolved here rather than defaulted via withDefaults: a default only applies to
|
|
48
|
+
// `undefined`, and the realistic case is a caller passing a signed-out user's
|
|
49
|
+
// empty-string name. Trimmed so whitespace doesn't read as a name either.
|
|
50
|
+
const profileName = computed(() => props.userName?.trim() || 'Account');
|
|
51
|
+
|
|
41
52
|
const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
|
|
42
53
|
</script>
|
|
43
54
|
|
|
44
55
|
<template>
|
|
45
56
|
<div class="app-side-menu">
|
|
46
57
|
<a class="skip-link" href="#main-content">Skip to main content</a>
|
|
47
|
-
<goa-work-side-menu :heading="heading" :user-name="
|
|
58
|
+
<goa-work-side-menu :heading="heading" :user-name="profileName">
|
|
48
59
|
<goa-work-side-menu-item
|
|
49
60
|
v-for="item in primaryItems"
|
|
50
61
|
:key="item.label"
|
|
@@ -78,6 +78,10 @@ const dateValues = computed(() =>
|
|
|
78
78
|
),
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
+
// Takes a plain string, never undefined: an unset filter is '', which is what
|
|
82
|
+
// clearAll() resets to and what the dropdown renders as its "Any" option. The
|
|
83
|
+
// dropdown wrapper's defineModel<string>() emits string | undefined, so its
|
|
84
|
+
// handler normalises before calling this rather than widening the shape here.
|
|
81
85
|
function set(key: string, value: string) {
|
|
82
86
|
model.value = { ...model.value, [key]: value };
|
|
83
87
|
}
|
|
@@ -109,7 +113,9 @@ function clearAll() {
|
|
|
109
113
|
v-if="filter.type === 'dropdown'"
|
|
110
114
|
:model-value="model[filter.key] ?? ''"
|
|
111
115
|
:name="filter.key"
|
|
112
|
-
@update:model-value="
|
|
116
|
+
@update:model-value="
|
|
117
|
+
(value: string | undefined) => set(filter.key, value ?? '')
|
|
118
|
+
"
|
|
113
119
|
>
|
|
114
120
|
<goa-dropdown-item value="" :label="filter.anyLabel ?? 'Any'" />
|
|
115
121
|
<goa-dropdown-item
|
|
@@ -143,7 +143,13 @@ function onNativeSort(event: Event) {
|
|
|
143
143
|
:pagenumber="page"
|
|
144
144
|
:itemcount="itemCount"
|
|
145
145
|
:perpagecount="perPageCount"
|
|
146
|
-
@_change="
|
|
146
|
+
@_change="
|
|
147
|
+
(e: Event) =>
|
|
148
|
+
emit(
|
|
149
|
+
'pageChange',
|
|
150
|
+
(e as CustomEvent<{ page: number }>).detail?.page ?? 1,
|
|
151
|
+
)
|
|
152
|
+
"
|
|
147
153
|
/>
|
|
148
154
|
</template>
|
|
149
155
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tree } from '@nx/devkit';
|
|
2
2
|
export declare function ensurePackageExports(host: Tree, libRoot: string): void;
|
|
3
3
|
export declare const LIB_NAME = "vue-components";
|
|
4
|
+
export declare function vueComponentsLibRoot(host: Tree): string;
|
|
4
5
|
export declare function vueComponentsImportPath(host: Tree): string;
|
|
5
6
|
export default function (host: Tree): Promise<void>;
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LIB_NAME = void 0;
|
|
4
4
|
exports.ensurePackageExports = ensurePackageExports;
|
|
5
|
+
exports.vueComponentsLibRoot = vueComponentsLibRoot;
|
|
5
6
|
exports.vueComponentsImportPath = vueComponentsImportPath;
|
|
6
7
|
exports.default = default_1;
|
|
7
8
|
const tslib_1 = require("tslib");
|
|
8
9
|
const devkit_1 = require("@nx/devkit");
|
|
9
|
-
const internal_1 = require("@nx/eslint/internal");
|
|
10
10
|
const path = require("path");
|
|
11
|
+
const vue_eslint_1 = require("../../utils/vue-eslint");
|
|
12
|
+
const vue_tsconfig_1 = require("../../utils/vue-tsconfig");
|
|
11
13
|
// Backfills package.json module-resolution fields for the library. In a TS-solution
|
|
12
14
|
// workspace @nx/vue's library resolves via package.json `exports`, but its
|
|
13
15
|
// programmatic default (useProjectJson skips writing them here) leaves them off, so
|
|
@@ -32,48 +34,24 @@ function ensurePackageExports(host, libRoot) {
|
|
|
32
34
|
return pkg;
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
|
-
// Turns off vue/no-deprecated-slot-attribute for the lib. GoabModal projects into
|
|
36
|
-
// goa-modal's native web-component slot via `slot="actions"` — legitimate custom-
|
|
37
|
-
// element usage, not the deprecated Vue 2 component-slot syntax the rule targets.
|
|
38
|
-
// The whole lib wraps web components, so it's scoped to the lib (consuming apps,
|
|
39
|
-
// which use `<template #actions>`, are unaffected).
|
|
40
|
-
//
|
|
41
|
-
// Uses @nx/eslint's own override helpers rather than hand-editing a specific
|
|
42
|
-
// file format: create-nx-workspace's current default is flat config
|
|
43
|
-
// (eslint.config.mjs), which is a JS module exporting an array, not JSON — an
|
|
44
|
-
// earlier version of this function only handled legacy .eslintrc.json, so it
|
|
45
|
-
// silently no-op'd (just a console warning) on flat-config workspaces, and
|
|
46
|
-
// `nx lint vue-components` failed for real on unmodified generator output.
|
|
47
|
-
// addOverrideToLintConfig/lintConfigHasOverride detect flat vs legacy
|
|
48
|
-
// internally (via @nx/eslint's own useFlatConfig check) and edit whichever is
|
|
49
|
-
// actually in effect — AST-aware for flat config, not string splicing.
|
|
50
|
-
function disableSlotAttributeRule(host, libRoot) {
|
|
51
|
-
if (!(0, internal_1.isEslintConfigSupported)(host, libRoot)) {
|
|
52
|
-
console.warn(`\n⚠ No ESLint config found for ${libRoot} — could not disable vue/no-deprecated-slot-attribute.\n` +
|
|
53
|
-
` GoabModal uses goa-modal's native \`slot\` attribute; if lint flags it, turn\n` +
|
|
54
|
-
` that rule off for this library.\n`);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
// `files` is `string | string[]` per the Linter type — normalize before checking.
|
|
58
|
-
const isVueOverride = (o) => (Array.isArray(o.files) ? o.files : o.files ? [o.files] : []).some((f) => f.includes('vue'));
|
|
59
|
-
const alreadyDisabled = (0, internal_1.lintConfigHasOverride)(host, libRoot, (o) => {
|
|
60
|
-
var _a;
|
|
61
|
-
return isVueOverride(o) &&
|
|
62
|
-
((_a = o.rules) === null || _a === void 0 ? void 0 : _a['vue/no-deprecated-slot-attribute']) === 'off';
|
|
63
|
-
});
|
|
64
|
-
if (alreadyDisabled) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
(0, internal_1.addOverrideToLintConfig)(host, libRoot, {
|
|
68
|
-
files: ['*.vue'],
|
|
69
|
-
rules: { 'vue/no-deprecated-slot-attribute': 'off' },
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
37
|
exports.LIB_NAME = 'vue-components';
|
|
73
38
|
// Import specifier for the shared wrapper lib, derived from the workspace npm
|
|
74
39
|
// scope so it mirrors the eventual official `@abgov/vue-components` (same tail —
|
|
75
40
|
// swapping to it is a scope change). Falls back to a scope built from the root
|
|
76
41
|
// package name when the workspace isn't scoped.
|
|
42
|
+
// Workspace-relative root of the shared wrapper lib, for docs that need to name a
|
|
43
|
+
// path rather than an import specifier. Reads the project graph first so it is
|
|
44
|
+
// correct in a TS-solution/flat workspace (where the lib sits at the root, not under
|
|
45
|
+
// libs/) and only falls back to the layout when the lib hasn't been generated yet.
|
|
46
|
+
function vueComponentsLibRoot(host) {
|
|
47
|
+
try {
|
|
48
|
+
return (0, devkit_1.readProjectConfiguration)(host, exports.LIB_NAME).root;
|
|
49
|
+
}
|
|
50
|
+
catch (_a) {
|
|
51
|
+
const { libsDir } = (0, devkit_1.getWorkspaceLayout)(host);
|
|
52
|
+
return libsDir && libsDir !== '.' ? `${libsDir}/${exports.LIB_NAME}` : exports.LIB_NAME;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
77
55
|
function vueComponentsImportPath(host) {
|
|
78
56
|
var _a, _b;
|
|
79
57
|
let name = 'workspace';
|
|
@@ -118,8 +96,9 @@ function default_1(host) {
|
|
|
118
96
|
importPath: vueComponentsImportPath(host),
|
|
119
97
|
skipFormat: true,
|
|
120
98
|
});
|
|
121
|
-
disableSlotAttributeRule(host, libRoot);
|
|
99
|
+
(0, vue_eslint_1.disableSlotAttributeRule)(host, libRoot, "GoabModal uses goa-modal's native `slot` attribute");
|
|
122
100
|
ensurePackageExports(host, libRoot);
|
|
101
|
+
(0, vue_tsconfig_1.ensureBundlerModuleOption)(host, `${libRoot}/tsconfig.json`);
|
|
123
102
|
}
|
|
124
103
|
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), libRoot, { tmpl: '' });
|
|
125
104
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-components.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-components/vue-components.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"vue-components.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-components/vue-components.ts"],"names":[],"mappings":";;;AAkBA,oDAkBC;AAYD,oDAOC;AAED,0DAUC;AAMD,4BAuCC;;AAhHD,uCAMoB;AACpB,6BAA6B;AAC7B,uDAAkE;AAClE,2DAAqE;AAErE,oFAAoF;AACpF,2EAA2E;AAC3E,oFAAoF;AACpF,kFAAkF;AAClF,6EAA6E;AAC7E,kFAAkF;AAClF,4DAA4D;AAC5D,SAAgB,oBAAoB,CAAC,IAAU,EAAE,OAAe;IAC9D,MAAM,OAAO,GAAG,GAAG,OAAO,eAAe,CAAC;IAC1C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO;IAClC,MAAM,GAAG,GAAG,gBAAgB,CAAC;IAC7B,IAAA,mBAAU,EACR,IAAI,EACJ,OAAO,EACP,CAAC,GAAG,EAAE,EAAE;;QACN,MAAA,GAAG,CAAC,IAAI,oCAAR,GAAG,CAAC,IAAI,GAAK,GAAG,EAAC;QACjB,MAAA,GAAG,CAAC,MAAM,oCAAV,GAAG,CAAC,MAAM,GAAK,GAAG,EAAC;QACnB,MAAA,GAAG,CAAC,KAAK,oCAAT,GAAG,CAAC,KAAK,GAAK,GAAG,EAAC;QAClB,MAAA,GAAG,CAAC,OAAO,oCAAX,GAAG,CAAC,OAAO,GAAK;YACd,gBAAgB,EAAE,gBAAgB;YAClC,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE;SAC/C,EAAC;QACF,OAAO,GAAG,CAAC;IACb,CAAC,CACF,CAAC;AACJ,CAAC;AAEY,QAAA,QAAQ,GAAG,gBAAgB,CAAC;AAEzC,8EAA8E;AAC9E,iFAAiF;AACjF,+EAA+E;AAC/E,gDAAgD;AAChD,kFAAkF;AAClF,+EAA+E;AAC/E,qFAAqF;AACrF,mFAAmF;AACnF,SAAgB,oBAAoB,CAAC,IAAU;IAC7C,IAAI,CAAC;QACH,OAAO,IAAA,iCAAwB,EAAC,IAAI,EAAE,gBAAQ,CAAC,CAAC,IAAI,CAAC;IACvD,CAAC;IAAC,WAAM,CAAC;QACP,MAAM,EAAE,OAAO,EAAE,GAAG,IAAA,2BAAkB,EAAC,IAAI,CAAC,CAAC;QAC7C,OAAO,OAAO,IAAI,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,gBAAQ,EAAE,CAAC,CAAC,CAAC,gBAAQ,CAAC;IAC1E,CAAC;AACH,CAAC;AAED,SAAgB,uBAAuB,CAAC,IAAU;;IAChD,IAAI,IAAI,GAAG,WAAW,CAAC;IACvB,IAAI,CAAC;QACH,IAAI;YACF,IAAI,CAAC,KAAK,CAAC,MAAA,MAAA,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,0CAAE,QAAQ,EAAE,mCAAI,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC;IAC3E,CAAC;IAAC,WAAM,CAAC;QACP,mBAAmB;IACrB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IACrE,OAAO,GAAG,KAAK,IAAI,gBAAQ,EAAE,CAAC;AAChC,CAAC;AAED,kFAAkF;AAClF,kFAAkF;AAClF,4EAA4E;AAC5E,oFAAoF;AACpF,mBAA+B,IAAU;;QACvC,MAAM,OAAO,GAAG,GAAG,IAAA,2BAAkB,EAAC,IAAI,CAAC,CAAC,OAAO,IAAI,gBAAQ,EAAE,CAAC;QAElE,IAAI,MAAM,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC;YACH,IAAA,iCAAwB,EAAC,IAAI,EAAE,gBAAQ,CAAC,CAAC;QAC3C,CAAC;QAAC,WAAM,CAAC;YACP,MAAM,GAAG,KAAK,CAAC;QACjB,CAAC;QAED,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,qCAAO,SAAS,GAAE,KAAK,CAAC,GAAG,EAAE;gBAC9D,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;YACJ,CAAC,CAAC,CAAC;YACH,MAAM,gBAAgB,CAAC,IAAI,EAAE;gBAC3B,SAAS,EAAE,OAAO;gBAClB,IAAI,EAAE,gBAAQ;gBACd,MAAM,EAAE,QAAQ;gBAChB,cAAc,EAAE,QAAQ;gBACxB,0EAA0E;gBAC1E,2EAA2E;gBAC3E,8DAA8D;gBAC9D,SAAS,EAAE,KAAK;gBAChB,OAAO,EAAE,MAAM;gBACf,UAAU,EAAE,uBAAuB,CAAC,IAAI,CAAC;gBACzC,UAAU,EAAE,IAAI;aACjB,CAAC,CAAC;YACH,IAAA,qCAAwB,EACtB,IAAI,EACJ,OAAO,EACP,oDAAoD,CACrD,CAAC;YACF,oBAAoB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACpC,IAAA,wCAAyB,EAAC,IAAI,EAAE,GAAG,OAAO,gBAAgB,CAAC,CAAC;QAC9D,CAAC;QAED,IAAA,sBAAa,EAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5E,CAAC;CAAA"}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
readJson,
|
|
3
|
+
readProjectConfiguration,
|
|
4
|
+
Tree,
|
|
5
|
+
updateJson,
|
|
6
|
+
writeJson,
|
|
7
|
+
} from '@nx/devkit';
|
|
2
8
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
3
9
|
import generator, {
|
|
4
10
|
ensurePackageExports,
|
|
@@ -179,7 +185,9 @@ describe('Vue Components Generator', () => {
|
|
|
179
185
|
// Shared value formatters: every view renders a date/count the same way
|
|
180
186
|
// instead of inlining its own toLocaleString (which is what a real app did
|
|
181
187
|
// in three separate views before this existed).
|
|
182
|
-
expect(
|
|
188
|
+
expect(
|
|
189
|
+
host.exists('libs/vue-components/src/lib/formatters.ts'),
|
|
190
|
+
).toBeTruthy();
|
|
183
191
|
expect(
|
|
184
192
|
host.exists('libs/vue-components/src/lib/formatters.spec.ts'),
|
|
185
193
|
).toBeTruthy();
|
|
@@ -191,6 +199,12 @@ describe('Vue Components Generator', () => {
|
|
|
191
199
|
'formatDateTime',
|
|
192
200
|
'formatNumber',
|
|
193
201
|
'formatPercent',
|
|
202
|
+
// Display of a *coded* value, the other half of formatting. Views that
|
|
203
|
+
// skipped these put raw domain codes and neutral-blue badges in front of
|
|
204
|
+
// citizens, so they live beside the date/number formatters rather than
|
|
205
|
+
// being re-derived per view.
|
|
206
|
+
'optionLabel',
|
|
207
|
+
'badgeType',
|
|
194
208
|
]) {
|
|
195
209
|
expect(formatters).toContain(`export function ${fn}`);
|
|
196
210
|
}
|
|
@@ -322,4 +336,82 @@ describe('Vue Components Generator', () => {
|
|
|
322
336
|
expect(host.exists('libs/vue-components/package.json')).toBeFalsy();
|
|
323
337
|
});
|
|
324
338
|
});
|
|
339
|
+
|
|
340
|
+
it('exports the display helpers from the lib barrel, not just the module', async () => {
|
|
341
|
+
await generator(host);
|
|
342
|
+
const index = host.read('libs/vue-components/src/index.ts').toString();
|
|
343
|
+
expect(index).toContain('optionLabel');
|
|
344
|
+
expect(index).toContain('badgeType');
|
|
345
|
+
expect(index).toContain('DisplayOption');
|
|
346
|
+
expect(index).toContain('BadgeType');
|
|
347
|
+
}, 30000);
|
|
348
|
+
|
|
349
|
+
it('falls back to the raw value for an unmapped code rather than hiding it', async () => {
|
|
350
|
+
await generator(host);
|
|
351
|
+
const formatters = host
|
|
352
|
+
.read('libs/vue-components/src/lib/formatters.ts')
|
|
353
|
+
.toString();
|
|
354
|
+
// An unmapped code is a data/config problem; showing it is debuggable,
|
|
355
|
+
// blanking it is not.
|
|
356
|
+
expect(formatters).toContain('return match ? match.label : String(value)');
|
|
357
|
+
// And an unmapped badge value stays neutral rather than being asserted good
|
|
358
|
+
// or bad.
|
|
359
|
+
expect(formatters).toContain("fallback: BadgeType = 'information'");
|
|
360
|
+
}, 30000);
|
|
361
|
+
|
|
362
|
+
// Regression: @nx/vue writes `moduleResolution: "bundler"` into the lib's
|
|
363
|
+
// tsconfig but leaves `module` inherited. Against a create-nx-workspace base
|
|
364
|
+
// (`module: "nodenext"`) that pair is invalid -- TS5095 and TS5109 -- so
|
|
365
|
+
// `nx build` failed on completely unmodified generator output.
|
|
366
|
+
it('gives the lib a module setting compatible with its bundler moduleResolution', async () => {
|
|
367
|
+
writeJson(host, 'tsconfig.base.json', {
|
|
368
|
+
compilerOptions: {
|
|
369
|
+
module: 'nodenext',
|
|
370
|
+
moduleResolution: 'nodenext',
|
|
371
|
+
target: 'es2022',
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
await generator(host);
|
|
375
|
+
const compilerOptions = readJson(
|
|
376
|
+
host,
|
|
377
|
+
'libs/vue-components/tsconfig.json',
|
|
378
|
+
).compilerOptions;
|
|
379
|
+
expect(compilerOptions.moduleResolution).toBe('bundler');
|
|
380
|
+
expect(compilerOptions.module).toBe('esnext');
|
|
381
|
+
}, 30000);
|
|
382
|
+
|
|
383
|
+
it('leaves a tsconfig that already declares module alone', async () => {
|
|
384
|
+
await generator(host);
|
|
385
|
+
// Pin something deliberate, then re-run: the backfill must not overwrite it.
|
|
386
|
+
updateJson(host, 'libs/vue-components/tsconfig.json', (tsconfig) => {
|
|
387
|
+
tsconfig.compilerOptions.module = 'preserve';
|
|
388
|
+
return tsconfig;
|
|
389
|
+
});
|
|
390
|
+
await generator(host);
|
|
391
|
+
expect(
|
|
392
|
+
readJson(host, 'libs/vue-components/tsconfig.json').compilerOptions
|
|
393
|
+
.module,
|
|
394
|
+
).toBe('preserve');
|
|
395
|
+
}, 30000);
|
|
396
|
+
|
|
397
|
+
// Regression: goa-work-side-menu's profile button takes its accessible name
|
|
398
|
+
// only from `user-name` (no aria-label prop exists on the element), and the
|
|
399
|
+
// shell rendered the account slot -- which is what creates that button --
|
|
400
|
+
// without ever supplying one. Measured on pristine generator output, that was
|
|
401
|
+
// an unnamed critical `button-name` violation plus an unlabelled `role-img-alt`
|
|
402
|
+
// icon on every route of an internal-layout app.
|
|
403
|
+
it('never leaves the side menu profile button without an accessible name', async () => {
|
|
404
|
+
await generator(host);
|
|
405
|
+
const sideMenu = host
|
|
406
|
+
.read('libs/vue-components/src/lib/patterns/AppSideMenu.vue')
|
|
407
|
+
.toString();
|
|
408
|
+
|
|
409
|
+
// Resolved, not passed straight through: an empty string is the realistic
|
|
410
|
+
// input (a signed-out user), and withDefaults only covers `undefined`.
|
|
411
|
+
expect(sideMenu).toContain(
|
|
412
|
+
"const profileName = computed(() => props.userName?.trim() || 'Account')",
|
|
413
|
+
);
|
|
414
|
+
expect(sideMenu).toContain(':user-name="profileName"');
|
|
415
|
+
expect(sideMenu).not.toContain(':user-name="userName"');
|
|
416
|
+
}, 30000);
|
|
325
417
|
});
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch } from 'vue';
|
|
3
3
|
import { useRoute, useRouter } from 'vue-router';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
RecordDetailShell,
|
|
6
|
+
formatCurrency,
|
|
7
|
+
formatDate,
|
|
8
|
+
<% if (hasCodedFields || hasBadgeFields) { -%>
|
|
9
|
+
optionLabel,
|
|
10
|
+
<% } -%>
|
|
11
|
+
<% if (hasBadgeFields) { -%>
|
|
12
|
+
badgeType,
|
|
13
|
+
<% } -%>
|
|
14
|
+
} from '<%= goaImportPath %>';
|
|
15
|
+
<% if (hasCodedFields || hasBadgeFields) { -%>
|
|
16
|
+
import type {
|
|
17
|
+
<% if (hasCodedFields || hasBadgeFields) { -%>
|
|
18
|
+
DisplayOption,
|
|
19
|
+
<% } -%>
|
|
20
|
+
<% if (hasBadgeFields) { -%>
|
|
21
|
+
BadgeType,
|
|
22
|
+
<% } -%>
|
|
23
|
+
} from '<%= goaImportPath %>';
|
|
24
|
+
<% } -%>
|
|
5
25
|
import { useApi } from '../composables/useApi';
|
|
26
|
+
<% if (hasCodedFields || hasBadgeFields) { -%>
|
|
27
|
+
|
|
28
|
+
// Value -> label for coded fields, so the record reads back in prose rather than
|
|
29
|
+
// in stored codes.
|
|
30
|
+
const fieldOptions: Record<string, DisplayOption[]> = {
|
|
31
|
+
<% fields.forEach(function (field) { -%>
|
|
32
|
+
<% if (field.options && field.options.length) { -%>
|
|
33
|
+
'<%= field.key %>': <%- JSON.stringify(field.options) %>,
|
|
34
|
+
<% } -%>
|
|
35
|
+
<% }); -%>
|
|
36
|
+
};
|
|
37
|
+
<% } -%>
|
|
38
|
+
<% if (hasBadgeFields) { -%>
|
|
39
|
+
|
|
40
|
+
// Value -> badge type; unmapped values render neutral rather than being asserted
|
|
41
|
+
// as good or bad.
|
|
42
|
+
const fieldBadgeTypes: Record<string, Record<string, BadgeType>> = {
|
|
43
|
+
<% fields.forEach(function (field) { -%>
|
|
44
|
+
<% if (field.type === 'badge') { -%>
|
|
45
|
+
'<%= field.key %>': <%- JSON.stringify(field.badgeMap || {}) %>,
|
|
46
|
+
<% } -%>
|
|
47
|
+
<% }); -%>
|
|
48
|
+
};
|
|
49
|
+
<% } -%>
|
|
6
50
|
|
|
7
51
|
// The fetched record's shape isn't known to this generator -- read fields
|
|
8
52
|
// defensively rather than declaring (and likely getting wrong) a fake interface.
|
|
@@ -52,7 +96,12 @@ function goBack() {
|
|
|
52
96
|
<dt><%= field.label %></dt>
|
|
53
97
|
<dd>
|
|
54
98
|
<% if (field.type === 'badge') { -%>
|
|
55
|
-
<goa-badge
|
|
99
|
+
<goa-badge
|
|
100
|
+
:type="badgeType(fieldBadgeTypes['<%= field.key %>'], record['<%= field.key %>'])"
|
|
101
|
+
:content="optionLabel(fieldOptions['<%= field.key %>'], record['<%= field.key %>'])"
|
|
102
|
+
/>
|
|
103
|
+
<% } else if (field.options && field.options.length) { -%>
|
|
104
|
+
{{ optionLabel(fieldOptions['<%= field.key %>'], record['<%= field.key %>']) }}
|
|
56
105
|
<% } else if (field.type === 'date') { -%>
|
|
57
106
|
{{ formatDate(record['<%= field.key %>']) }}
|
|
58
107
|
<% } else if (field.type === 'currency') { -%>
|
|
@@ -2,6 +2,24 @@ export interface DetailViewField {
|
|
|
2
2
|
key: string;
|
|
3
3
|
label: string;
|
|
4
4
|
type?: 'text' | 'date' | 'currency' | 'badge';
|
|
5
|
+
/**
|
|
6
|
+
* Value -> label for a coded field. Supply this whenever the stored value is a
|
|
7
|
+
* code rather than prose, or the view renders the raw code.
|
|
8
|
+
*/
|
|
9
|
+
options?: { value: string; label: string }[];
|
|
10
|
+
/**
|
|
11
|
+
* `badge` only. Value -> goa-badge type. Unmapped values render neutral.
|
|
12
|
+
*/
|
|
13
|
+
badgeMap?: Record<
|
|
14
|
+
string,
|
|
15
|
+
| 'information'
|
|
16
|
+
| 'success'
|
|
17
|
+
| 'important'
|
|
18
|
+
| 'emergency'
|
|
19
|
+
| 'dark'
|
|
20
|
+
| 'midtone'
|
|
21
|
+
| 'light'
|
|
22
|
+
>;
|
|
5
23
|
}
|
|
6
24
|
|
|
7
25
|
export interface Schema {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"resource": {
|
|
27
27
|
"type": "string",
|
|
28
|
-
"description": "API resource path segment -- the view fetches /api/<resource>/:id."
|
|
28
|
+
"description": "API resource path segment -- the view fetches /api/v1/<resource>/:id."
|
|
29
29
|
},
|
|
30
30
|
"route": {
|
|
31
31
|
"type": "string",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"fields": {
|
|
35
35
|
"type": "string",
|
|
36
|
-
"description": "JSON array of fields rendered in the record's info list, in display order -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"badge\"}]'. Each item: { key, label, type?: \"text\"|\"date\"|\"currency\"|\"badge\" (default \"text\") }. A plain array is also accepted when this generator is invoked programmatically. Nx's CLI option coercion only supports comma-separated primitive lists for array-typed schema properties, not JSON -- a JSON string is the only CLI syntax that actually survives Nx's own arg parsing for a structured list like this."
|
|
36
|
+
"description": "JSON array of fields rendered in the record's info list, in display order -- e.g. '[{\"key\":\"status\",\"label\":\"Status\",\"type\":\"badge\"}]'. Each item: { key, label, type?: \"text\"|\"date\"|\"currency\"|\"badge\" (default \"text\"), options?: [{value,label}], badgeMap?: {value: badgeType} }. Supply `options` for any field whose stored value is a code, or the view renders the raw code. Supply `badgeMap` on a `badge` field to give each status its colour; unmapped values render neutral. A plain array is also accepted when this generator is invoked programmatically. Nx's CLI option coercion only supports comma-separated primitive lists for array-typed schema properties, not JSON -- a JSON string is the only CLI syntax that actually survives Nx's own arg parsing for a structured list like this."
|
|
37
37
|
},
|
|
38
38
|
"heading": {
|
|
39
39
|
"type": "string",
|
|
@@ -38,7 +38,10 @@ function default_1(host, options) {
|
|
|
38
38
|
// Idempotent -- ensures RecordDetailShell exists even in a project scaffolded
|
|
39
39
|
// before vue-components carried it.
|
|
40
40
|
yield (0, vue_components_1.default)(host);
|
|
41
|
-
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host),
|
|
41
|
+
(0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host),
|
|
42
|
+
// Derived here, not tested inline: EJS throws a ReferenceError on a bare
|
|
43
|
+
// undefined variable, so a flag the template reads must always be defined.
|
|
44
|
+
hasCodedFields: normalizedOptions.fields.some((field) => { var _a; return (_a = field.options) === null || _a === void 0 ? void 0 : _a.length; }), hasBadgeFields: normalizedOptions.fields.some((field) => field.type === 'badge'), tmpl: '' }));
|
|
42
45
|
(0, vue_router_1.insertVueRoute)(host, normalizedOptions.projectRoot, normalizedOptions.project, {
|
|
43
46
|
path: normalizedOptions.route,
|
|
44
47
|
componentImportPath: `../views/${normalizedOptions.viewFileName}.vue`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-detail-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-detail-view/vue-detail-view.ts"],"names":[],"mappings":";;AAqDA,
|
|
1
|
+
{"version":3,"file":"vue-detail-view.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-detail-view/vue-detail-view.ts"],"names":[],"mappings":";;AAqDA,4BAsCC;;AA3FD,uCAMoB;AACpB,6BAA6B;AAC7B,uDAAwD;AACxD,qEAE0C;AAG1C,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,0EAA0E;AAC1E,6EAA6E;AAC7E,iEAAiE;AACjE,SAAS,WAAW,CAAC,MAAwB;IAC3C,MAAM,MAAM,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAe;;IACnD,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9E,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,YAAY,OAAO,CAAC,KAAK,qFAAqF,CAC/G,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;IAChD,uCACK,OAAO,KACV,WAAW,EACX,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EACnC,YAAY,EAAE,GAAG,SAAS,MAAM;QAChC,2EAA2E;QAC3E,mDAAmD;QACnD,OAAO,EAAE,MAAA,OAAO,CAAC,OAAO,mCAAI,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,EACvE,YAAY,EAAE,MAAA,OAAO,CAAC,YAAY,mCAAI,IAAI,IAC1C;AACJ,CAAC;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAE1D,8EAA8E;QAC9E,oCAAoC;QACpC,MAAM,IAAA,wBAAsB,EAAC,IAAI,CAAC,CAAC;QAEnC,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAC7B,iBAAiB,CAAC,WAAW,kCAExB,iBAAiB,KACpB,aAAa,EAAE,IAAA,wCAAuB,EAAC,IAAI,CAAC;YAC5C,yEAAyE;YACzE,2EAA2E;YAC3E,cAAc,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAC3C,CAAC,KAAK,EAAE,EAAE,WAAC,OAAA,MAAA,KAAK,CAAC,OAAO,0CAAE,MAAM,CAAA,EAAA,CACjC,EACD,cAAc,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAC3C,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAClC,EACD,IAAI,EAAE,EAAE,IAEX,CAAC;QAEF,IAAA,2BAAc,EACZ,IAAI,EACJ,iBAAiB,CAAC,WAAW,EAC7B,iBAAiB,CAAC,OAAO,EACzB;YACE,IAAI,EAAE,iBAAiB,CAAC,KAAK;YAC7B,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,YAAY,MAAM;YACrE,YAAY,EAAE,iBAAiB,CAAC,YAAY;SAC7C,CACF,CAAC;QAEF,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA"}
|
|
@@ -102,19 +102,23 @@ describe('Vue Detail View Generator', () => {
|
|
|
102
102
|
.read('apps/test/src/views/ApplicationDetailView.vue')
|
|
103
103
|
.toString();
|
|
104
104
|
expect(view).toContain('heading="Application Detail"');
|
|
105
|
-
expect(view).toContain(
|
|
105
|
+
expect(view).toContain(
|
|
106
|
+
"await get('applications', String(route.params.id))",
|
|
107
|
+
);
|
|
106
108
|
expect(view).not.toContain('apiFetch');
|
|
107
109
|
|
|
108
110
|
// Vue Router reuses this component across an id-only change, so the fetch
|
|
109
111
|
// is driven by a watch rather than onMounted.
|
|
110
|
-
expect(view).toContain(
|
|
112
|
+
expect(view).toContain(
|
|
113
|
+
'watch(() => route.params.id, load, { immediate: true })',
|
|
114
|
+
);
|
|
111
115
|
expect(view).not.toContain('onMounted(');
|
|
112
116
|
expect(view).not.toContain('function formatCurrency');
|
|
113
117
|
|
|
114
118
|
expect(view).toContain('formatDate');
|
|
115
119
|
expect(view).not.toContain('function formatDate');
|
|
116
120
|
expect(view).toContain(
|
|
117
|
-
"
|
|
121
|
+
"badgeType(fieldBadgeTypes['status'], record['status'])",
|
|
118
122
|
);
|
|
119
123
|
// A type: 'date' column is a calendar date -- formatDate, not
|
|
120
124
|
// formatDateTime, which would show an invented midnight.
|
|
@@ -168,9 +172,7 @@ describe('Vue Detail View Generator', () => {
|
|
|
168
172
|
it('ensures the shared RecordDetailShell pattern component exists', async () => {
|
|
169
173
|
await generator(host, baseOptions);
|
|
170
174
|
expect(
|
|
171
|
-
host.exists(
|
|
172
|
-
'libs/vue-components/src/lib/patterns/RecordDetailShell.vue',
|
|
173
|
-
),
|
|
175
|
+
host.exists('libs/vue-components/src/lib/patterns/RecordDetailShell.vue'),
|
|
174
176
|
).toBeTruthy();
|
|
175
177
|
}, 30000);
|
|
176
178
|
|
|
@@ -180,4 +182,32 @@ describe('Vue Detail View Generator', () => {
|
|
|
180
182
|
const after = readProjectConfiguration(host, 'test');
|
|
181
183
|
expect(after).toEqual(before);
|
|
182
184
|
}, 30000);
|
|
185
|
+
|
|
186
|
+
it('maps badge values to semantic types and renders coded fields as labels', async () => {
|
|
187
|
+
await generator(host, {
|
|
188
|
+
...baseOptions,
|
|
189
|
+
fields: [
|
|
190
|
+
{
|
|
191
|
+
key: 'status',
|
|
192
|
+
label: 'Status',
|
|
193
|
+
type: 'badge',
|
|
194
|
+
badgeMap: { declined: 'emergency' },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: 'region',
|
|
198
|
+
label: 'Region',
|
|
199
|
+
options: [{ value: 'northwest', label: 'North west' }],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
const view = host
|
|
204
|
+
.read('apps/test/src/views/ApplicationDetailView.vue')
|
|
205
|
+
.toString();
|
|
206
|
+
// Quote style depends on whether formatFiles ran, so match the pair.
|
|
207
|
+
expect(view).toMatch(/["']?declined["']?\s*:\s*["']emergency["']/);
|
|
208
|
+
expect(view).toContain("badgeType(fieldBadgeTypes['status']");
|
|
209
|
+
expect(view).toContain(
|
|
210
|
+
"optionLabel(fieldOptions['region'], record['region'])",
|
|
211
|
+
);
|
|
212
|
+
}, 30000);
|
|
183
213
|
});
|