@abgov/nx-adsp 13.25.0 → 13.27.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-components/files/AGENTS.md__tmpl__ +4 -2
- package/src/generators/vue-components/files/src/index.ts__tmpl__ +2 -0
- package/src/generators/vue-components/files/src/lib/patterns/FilterBar.spec.ts__tmpl__ +121 -0
- package/src/generators/vue-components/files/src/lib/patterns/FilterBar.vue__tmpl__ +158 -0
- package/src/generators/vue-components/files/src/lib/primitives/GoabDatePicker.spec.ts__tmpl__ +29 -0
- package/src/generators/vue-components/files/src/lib/primitives/GoabDatePicker.vue__tmpl__ +27 -0
- package/src/generators/vue-components/files/src/vue-components.spec.ts__tmpl__ +2 -0
- package/src/generators/vue-components/vue-components.spec.ts +44 -0
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ invoked automatically by `vue-app` and by every `vue-*-view` generator.
|
|
|
7
7
|
| Folder | Contains | Lifespan |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| `src/lib/primitives/` | Thin `v-model`/idiomatic-event wrappers over individual `goa-*` elements (`GoabInput`, `GoabButton`, …) | **Interim** — see below |
|
|
10
|
-
| `src/lib/patterns/` | Composite, app-shell components (`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`, `WorkspaceTable`, `Stepper`, `StepErrorSummary`) | **Permanent** |
|
|
10
|
+
| `src/lib/patterns/` | Composite, app-shell components (`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`, `WorkspaceTable`, `Stepper`, `StepErrorSummary`, `FilterBar`) | **Permanent** |
|
|
11
11
|
| `src/lib/formatters.ts` | Shared value formatting for views (`formatDate`, `formatDateTime`, `formatNumber`, `formatPercent`) | **Permanent** |
|
|
12
12
|
|
|
13
13
|
**Not every pattern component is present in every app.** `AppLayout`, `AppHeader`,
|
|
@@ -66,10 +66,11 @@ inline styles.
|
|
|
66
66
|
| A loading `<div>` or spinner markup | `<goa-skeleton type="…">` or `<goa-spinner>` | Skeletons match the shape they replace (`card`, `text`, `table`) |
|
|
67
67
|
| A `<label>` + error `<span>` around an input | `<goa-form-item label="…" error="…">` | Wrap the `Goab*` input in this, don't restyle it |
|
|
68
68
|
| A row of buttons with flex styling | `<goa-button-group alignment="…">` | |
|
|
69
|
+
| A hand-built filter row above a table | `<FilterBar>` (this library) | Controls, collapse, active-filter chips and clear-all. It emits a query-ready values object; the *view* owns resetting the page, debouncing, and URL sync |
|
|
69
70
|
|
|
70
71
|
Also available bare and worth knowing before hand-rolling: `goa-accordion`, `goa-details`,
|
|
71
72
|
`goa-divider`, `goa-chip`, `goa-filter-chip`, `goa-icon`, `goa-icon-button`, `goa-link`,
|
|
72
|
-
`goa-notification`, `goa-popover`, `goa-tooltip`, `goa-
|
|
73
|
+
`goa-notification`, `goa-popover`, `goa-tooltip`, `goa-file-upload-input`,
|
|
73
74
|
`goa-file-upload-card`, `goa-circular-progress`, `goa-linear-progress`, `goa-hero-banner`,
|
|
74
75
|
`goa-page-block`. For the full set, list the registered element names from the installed package:
|
|
75
76
|
|
|
@@ -111,6 +112,7 @@ the design system supports keeps working without being re-declared.
|
|
|
111
112
|
| `GoabCheckbox` | `goa-checkbox` | `boolean` | `$event.detail.checked` |
|
|
112
113
|
| `GoabButton` | `goa-button` | — (re-exposes `_click` as `@click`) | — |
|
|
113
114
|
| `GoabModal` | `goa-modal` | `open: boolean` (`v-model:open`) | `_close` clears it |
|
|
115
|
+
| `GoabDatePicker` | `goa-date-picker` | `Date \| undefined` | `$event.detail.value` is a **`Date`**, not a string (`valueStr` carries the string form) |
|
|
114
116
|
|
|
115
117
|
Most value elements put the value on `detail.value`; some carry extra keys
|
|
116
118
|
(dropdown adds `label`, radio-group adds `labels`) you can ignore unless you need
|
|
@@ -20,6 +20,7 @@ export { default as GoabCheckbox } from './lib/primitives/GoabCheckbox.vue';
|
|
|
20
20
|
export { default as GoabRadioGroup } from './lib/primitives/GoabRadioGroup.vue';
|
|
21
21
|
export { default as GoabButton } from './lib/primitives/GoabButton.vue';
|
|
22
22
|
export { default as GoabModal } from './lib/primitives/GoabModal.vue';
|
|
23
|
+
export { default as GoabDatePicker } from './lib/primitives/GoabDatePicker.vue';
|
|
23
24
|
|
|
24
25
|
export { default as AppLayout } from './lib/patterns/AppLayout.vue';
|
|
25
26
|
export { default as AppHeader } from './lib/patterns/AppHeader.vue';
|
|
@@ -30,6 +31,7 @@ export { default as RecordDetailShell } from './lib/patterns/RecordDetailShell.v
|
|
|
30
31
|
export { default as WorkspaceTable } from './lib/patterns/WorkspaceTable.vue';
|
|
31
32
|
export { default as Stepper } from './lib/patterns/Stepper.vue';
|
|
32
33
|
export { default as StepErrorSummary } from './lib/patterns/StepErrorSummary.vue';
|
|
34
|
+
export { default as FilterBar } from './lib/patterns/FilterBar.vue';
|
|
33
35
|
|
|
34
36
|
export {
|
|
35
37
|
formatCurrency,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import FilterBar from './FilterBar.vue';
|
|
4
|
+
|
|
5
|
+
// FilterBar is presentational: values in via v-model, values out via
|
|
6
|
+
// update:modelValue, and nothing else. These tests pin that contract -- notably
|
|
7
|
+
// that it never reaches for the page, the router, or the network.
|
|
8
|
+
//
|
|
9
|
+
// These mount goa-* elements, so Vue logs "Failed to resolve component" unless
|
|
10
|
+
// this project's vite config sets isCustomElement (see AGENTS.md > Testing).
|
|
11
|
+
// The assertions pass either way -- the option only silences the warnings.
|
|
12
|
+
const filters = [
|
|
13
|
+
{
|
|
14
|
+
key: 'status',
|
|
15
|
+
label: 'Status',
|
|
16
|
+
type: 'dropdown' as const,
|
|
17
|
+
options: [
|
|
18
|
+
{ value: 'active', label: 'Active' },
|
|
19
|
+
{ value: 'closed', label: 'Closed' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{ key: 'from', label: 'From', type: 'date' as const },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function mountBar(modelValue: Record<string, string> = {}) {
|
|
26
|
+
return mount(FilterBar, {
|
|
27
|
+
props: { filters, modelValue },
|
|
28
|
+
global: { stubs: { GoabDropdown: true, GoabDatePicker: true, GoabButton: true } },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('FilterBar', () => {
|
|
33
|
+
it('renders one form item per filter', () => {
|
|
34
|
+
const wrapper = mountBar();
|
|
35
|
+
expect(wrapper.findAll('goa-form-item')).toHaveLength(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('shows no chips and no clear-all when nothing is filtered', () => {
|
|
39
|
+
const wrapper = mountBar();
|
|
40
|
+
expect(wrapper.find('goa-filter-chip').exists()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders a chip per active filter, labelled with the option label not its value', () => {
|
|
44
|
+
const wrapper = mountBar({ status: 'active' });
|
|
45
|
+
const chips = wrapper.findAll('goa-filter-chip');
|
|
46
|
+
expect(chips).toHaveLength(1);
|
|
47
|
+
expect(chips[0].attributes('content')).toBe('Status: Active');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('falls back to the raw value when no option matches (e.g. a date)', () => {
|
|
51
|
+
const wrapper = mountBar({ from: '2026-08-28' });
|
|
52
|
+
expect(wrapper.find('goa-filter-chip').attributes('content')).toBe(
|
|
53
|
+
'From: 2026-08-28',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('emits the whole values object, not a partial patch', async () => {
|
|
58
|
+
const wrapper = mountBar({ status: 'active' });
|
|
59
|
+
// Dismissing a chip clears that one key and keeps the rest of the object.
|
|
60
|
+
await wrapper.find('goa-filter-chip').trigger('_click');
|
|
61
|
+
const emitted = wrapper.emitted('update:modelValue');
|
|
62
|
+
expect(emitted?.[0][0]).toEqual({ status: '' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('collapses behind goa-details only when asked', () => {
|
|
66
|
+
expect(mountBar().find('goa-details').exists()).toBe(false);
|
|
67
|
+
const collapsible = mount(FilterBar, {
|
|
68
|
+
props: { filters, modelValue: {}, collapsible: true, heading: 'Filters' },
|
|
69
|
+
global: { stubs: { GoabDropdown: true, GoabDatePicker: true, GoabButton: true } },
|
|
70
|
+
});
|
|
71
|
+
expect(collapsible.find('goa-details').exists()).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('FilterBar date handling', () => {
|
|
76
|
+
it('serialises a picked Date to YYYY-MM-DD for the query string', async () => {
|
|
77
|
+
const wrapper = mount(FilterBar, {
|
|
78
|
+
props: { filters, modelValue: {} },
|
|
79
|
+
global: { stubs: { GoabDropdown: true, GoabButton: true } },
|
|
80
|
+
});
|
|
81
|
+
const picker = wrapper.findComponent({ name: 'GoabDatePicker' });
|
|
82
|
+
// Local midnight is what a date picker yields for a picked calendar date.
|
|
83
|
+
// A UTC instant here would make the assertion timezone-dependent.
|
|
84
|
+
await picker.vm.$emit('update:modelValue', new Date(2026, 7, 28));
|
|
85
|
+
expect(wrapper.emitted('update:modelValue')?.[0][0]).toEqual({
|
|
86
|
+
from: '2026-08-28',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('clears the key when the date is cleared', async () => {
|
|
91
|
+
const wrapper = mount(FilterBar, {
|
|
92
|
+
props: { filters, modelValue: { from: '2026-08-28' } },
|
|
93
|
+
global: { stubs: { GoabDropdown: true, GoabButton: true } },
|
|
94
|
+
});
|
|
95
|
+
const picker = wrapper.findComponent({ name: 'GoabDatePicker' });
|
|
96
|
+
await picker.vm.$emit('update:modelValue', undefined);
|
|
97
|
+
expect(wrapper.emitted('update:modelValue')?.[0][0]).toEqual({ from: '' });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const dateOnlyFilters = [{ key: 'from', label: 'From', type: 'date' as const }];
|
|
102
|
+
const stubs = { GoabDropdown: true, GoabButton: true };
|
|
103
|
+
|
|
104
|
+
describe('date round-trip preserves the calendar date the user picked', () => {
|
|
105
|
+
it('serialises the picked calendar date, not its UTC date', async () => {
|
|
106
|
+
const wrapper = mount(FilterBar, { props: { filters: dateOnlyFilters, modelValue: {} }, global: { stubs } });
|
|
107
|
+
// What a picker yields for "28 Aug 2026": local midnight.
|
|
108
|
+
const localMidnight = new Date(2026, 7, 28, 0, 0, 0);
|
|
109
|
+
await wrapper.findComponent({ name: 'GoabDatePicker' }).vm.$emit('update:modelValue', localMidnight);
|
|
110
|
+
expect(wrapper.emitted('update:modelValue')?.[0][0]).toEqual({ from: '2026-08-28' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('parses a stored YYYY-MM-DD back to the same local calendar date', () => {
|
|
114
|
+
const wrapper = mount(FilterBar, {
|
|
115
|
+
props: { filters: dateOnlyFilters, modelValue: { from: '2026-08-28' } },
|
|
116
|
+
global: { stubs },
|
|
117
|
+
});
|
|
118
|
+
const back = wrapper.findComponent({ name: 'GoabDatePicker' }).props('modelValue') as Date;
|
|
119
|
+
expect([back.getFullYear(), back.getMonth() + 1, back.getDate()]).toEqual([2026, 8, 28]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The presentation half of a filtered list: control layout, collapse, the
|
|
3
|
+
// active-filter chips, and clear-all. Deliberately NOT the query half — it
|
|
4
|
+
// emits a values object and nothing else, so it stays presentational per this
|
|
5
|
+
// library's rules (accept data via props, report via emit, no side effects).
|
|
6
|
+
//
|
|
7
|
+
// What that means for the consumer: resetting the page to 1, debouncing, and
|
|
8
|
+
// syncing to the URL all belong to the view, which owns `page` and the router.
|
|
9
|
+
// A component can't reach either without becoming stateful.
|
|
10
|
+
//
|
|
11
|
+
// Values are strings, query-ready, so a view can hand the object straight to
|
|
12
|
+
// useApi's `filters`. A `date` filter converts its Date to YYYY-MM-DD here,
|
|
13
|
+
// which is a format decision this component can make because it chose the
|
|
14
|
+
// control; anything richer belongs in the view.
|
|
15
|
+
//
|
|
16
|
+
// Both date conversions use local calendar parts, never toISOString() or
|
|
17
|
+
// `new Date(string)`. Those route through UTC, which shifts the date by a day on
|
|
18
|
+
// one side of midnight or the other: in Alberta (UTC-6/-7) `new Date('2026-08-28')`
|
|
19
|
+
// is 27 Aug 18:00 local, so a stored date would render as the day before.
|
|
20
|
+
import { computed } from 'vue';
|
|
21
|
+
import GoabDropdown from '../primitives/GoabDropdown.vue';
|
|
22
|
+
import GoabDatePicker from '../primitives/GoabDatePicker.vue';
|
|
23
|
+
import GoabButton from '../primitives/GoabButton.vue';
|
|
24
|
+
|
|
25
|
+
export interface FilterOption {
|
|
26
|
+
value: string;
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FilterDescriptor {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
type: 'dropdown' | 'date';
|
|
34
|
+
/** Dropdown only. May arrive after mount when fetched — it's a prop. */
|
|
35
|
+
options?: FilterOption[];
|
|
36
|
+
/** Dropdown only. Label for the "no filter" choice. */
|
|
37
|
+
anyLabel?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const props = withDefaults(
|
|
41
|
+
defineProps<{
|
|
42
|
+
filters: FilterDescriptor[];
|
|
43
|
+
/** Collapse the controls behind a disclosure. Chips stay visible. */
|
|
44
|
+
collapsible?: boolean;
|
|
45
|
+
heading?: string;
|
|
46
|
+
}>(),
|
|
47
|
+
{ collapsible: false, heading: 'Filters' },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Record<key, value>; an empty string means "not filtering on this".
|
|
51
|
+
const model = defineModel<Record<string, string>>({ default: () => ({}) });
|
|
52
|
+
|
|
53
|
+
const active = computed(() =>
|
|
54
|
+
props.filters
|
|
55
|
+
.filter((filter) => !!model.value[filter.key])
|
|
56
|
+
.map((filter) => ({
|
|
57
|
+
...filter,
|
|
58
|
+
value: model.value[filter.key],
|
|
59
|
+
display:
|
|
60
|
+
filter.options?.find((o) => o.value === model.value[filter.key])
|
|
61
|
+
?.label ?? model.value[filter.key],
|
|
62
|
+
})),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Parsed once per model change, not per render: handing goa-date-picker a fresh
|
|
66
|
+
// Date object every render would re-set its value on each tick.
|
|
67
|
+
const dateValues = computed(() =>
|
|
68
|
+
Object.fromEntries(
|
|
69
|
+
props.filters
|
|
70
|
+
.filter((filter) => filter.type === 'date')
|
|
71
|
+
.map((filter) => [
|
|
72
|
+
filter.key,
|
|
73
|
+
model.value[filter.key]
|
|
74
|
+
? fromIsoDate(model.value[filter.key])
|
|
75
|
+
: undefined,
|
|
76
|
+
]),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Local-calendar YYYY-MM-DD -- see the header note on why not toISOString().
|
|
81
|
+
function toIsoDate(date: Date): string {
|
|
82
|
+
const pad = (part: number) => String(part).padStart(2, '0');
|
|
83
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fromIsoDate(value: string): Date | undefined {
|
|
87
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
88
|
+
return year && month && day ? new Date(year, month - 1, day) : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function set(key: string, value: string) {
|
|
92
|
+
model.value = { ...model.value, [key]: value };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function setDate(key: string, date: Date | undefined) {
|
|
96
|
+
set(key, date ? toIsoDate(date) : '');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function clearAll() {
|
|
100
|
+
model.value = Object.fromEntries(
|
|
101
|
+
props.filters.map((filter) => [filter.key, '']),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div>
|
|
108
|
+
<component
|
|
109
|
+
:is="collapsible ? 'goa-details' : 'div'"
|
|
110
|
+
v-bind="collapsible ? { heading } : {}"
|
|
111
|
+
>
|
|
112
|
+
<goa-block gap="m" direction="row" alignment="end">
|
|
113
|
+
<goa-form-item
|
|
114
|
+
v-for="filter in filters"
|
|
115
|
+
:key="filter.key"
|
|
116
|
+
:label="filter.label"
|
|
117
|
+
>
|
|
118
|
+
<GoabDropdown
|
|
119
|
+
v-if="filter.type === 'dropdown'"
|
|
120
|
+
:model-value="model[filter.key] ?? ''"
|
|
121
|
+
:name="filter.key"
|
|
122
|
+
@update:model-value="(value: string) => set(filter.key, value)"
|
|
123
|
+
>
|
|
124
|
+
<goa-dropdown-item value="" :label="filter.anyLabel ?? 'Any'" />
|
|
125
|
+
<goa-dropdown-item
|
|
126
|
+
v-for="option in filter.options ?? []"
|
|
127
|
+
:key="option.value"
|
|
128
|
+
:value="option.value"
|
|
129
|
+
:label="option.label"
|
|
130
|
+
/>
|
|
131
|
+
</GoabDropdown>
|
|
132
|
+
<GoabDatePicker
|
|
133
|
+
v-else
|
|
134
|
+
:model-value="dateValues[filter.key]"
|
|
135
|
+
:name="filter.key"
|
|
136
|
+
@update:model-value="(date: Date | undefined) => setDate(filter.key, date)"
|
|
137
|
+
/>
|
|
138
|
+
</goa-form-item>
|
|
139
|
+
</goa-block>
|
|
140
|
+
</component>
|
|
141
|
+
|
|
142
|
+
<template v-if="active.length">
|
|
143
|
+
<goa-spacer vspacing="s" />
|
|
144
|
+
<goa-block gap="xs" direction="row" alignment="center">
|
|
145
|
+
<goa-filter-chip
|
|
146
|
+
v-for="filter in active"
|
|
147
|
+
:key="filter.key"
|
|
148
|
+
:content="`${filter.label}: ${filter.display}`"
|
|
149
|
+
:aria-label="`Remove ${filter.label} filter`"
|
|
150
|
+
@_click="set(filter.key, '')"
|
|
151
|
+
/>
|
|
152
|
+
<GoabButton type="tertiary" size="compact" @click="clearAll">
|
|
153
|
+
Clear all
|
|
154
|
+
</GoabButton>
|
|
155
|
+
</goa-block>
|
|
156
|
+
</template>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import GoabDatePicker from './GoabDatePicker.vue';
|
|
4
|
+
|
|
5
|
+
// Pins the event contract read out of the installed @abgov/web-components:
|
|
6
|
+
// goa-date-picker dispatches _change with { name, value: Date, valueStr: string }.
|
|
7
|
+
// Every other value wrapper here reads a string off detail.value; this one does
|
|
8
|
+
// not, so the contract is worth a test rather than a comment alone.
|
|
9
|
+
describe('GoabDatePicker', () => {
|
|
10
|
+
it('takes the Date off detail.value, not detail.valueStr', async () => {
|
|
11
|
+
const wrapper = mount(GoabDatePicker, { props: { modelValue: undefined } });
|
|
12
|
+
const date = new Date('2026-08-28T00:00:00.000Z');
|
|
13
|
+
await wrapper
|
|
14
|
+
.find('goa-date-picker')
|
|
15
|
+
.element.dispatchEvent(
|
|
16
|
+
new CustomEvent('_change', {
|
|
17
|
+
detail: { name: 'from', value: date, valueStr: '2026-08-28' },
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
expect(wrapper.emitted('update:modelValue')?.[0][0]).toBeInstanceOf(Date);
|
|
21
|
+
expect(wrapper.emitted('update:modelValue')?.[0][0]).toEqual(date);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('passes the model down as the element value', () => {
|
|
25
|
+
const date = new Date('2026-01-02T00:00:00.000Z');
|
|
26
|
+
const wrapper = mount(GoabDatePicker, { props: { modelValue: date } });
|
|
27
|
+
expect(wrapper.find('goa-date-picker').exists()).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// INTERIM WRAPPER — remove once GoA DS publishes @abgov/vue-components (see
|
|
3
|
+
// GoabInput.vue for the full rationale). Adds v-model to <goa-date-picker>.
|
|
4
|
+
//
|
|
5
|
+
// The model is a Date, not a string: goa-date-picker's _change detail is
|
|
6
|
+
// { name, value: Date, valueStr: string } — verified against the installed
|
|
7
|
+
// package, where the dispatch reads `value: j.date, valueStr: r`. That makes it
|
|
8
|
+
// the one element here whose detail.value isn't the string the other value
|
|
9
|
+
// wrappers read, so it does not use skeleton A verbatim.
|
|
10
|
+
//
|
|
11
|
+
// Consumers that need a wire format (a query parameter, a JSON body) convert at
|
|
12
|
+
// that boundary — `date?.toISOString().slice(0, 10)` for YYYY-MM-DD — rather
|
|
13
|
+
// than this wrapper guessing which format is wanted.
|
|
14
|
+
//
|
|
15
|
+
// Usage: <GoabDatePicker v-model="startDate" name="startDate" :max="new Date()" />
|
|
16
|
+
const model = defineModel<Date | undefined>();
|
|
17
|
+
|
|
18
|
+
function onChange(e: Event) {
|
|
19
|
+
model.value = (e as CustomEvent<{ value: Date }>).detail.value;
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<goa-date-picker :value="model" @_change="onChange">
|
|
25
|
+
<slot />
|
|
26
|
+
</goa-date-picker>
|
|
27
|
+
</template>
|
|
@@ -14,6 +14,7 @@ describe('vue-components', () => {
|
|
|
14
14
|
'GoabRadioGroup',
|
|
15
15
|
'GoabButton',
|
|
16
16
|
'GoabModal',
|
|
17
|
+
'GoabDatePicker',
|
|
17
18
|
]) {
|
|
18
19
|
expect(lib[name as keyof typeof lib]).toBeTruthy();
|
|
19
20
|
}
|
|
@@ -41,6 +42,7 @@ describe('vue-components', () => {
|
|
|
41
42
|
'WorkspaceTable',
|
|
42
43
|
'Stepper',
|
|
43
44
|
'StepErrorSummary',
|
|
45
|
+
'FilterBar',
|
|
44
46
|
]) {
|
|
45
47
|
expect(lib[name as keyof typeof lib]).toBeTruthy();
|
|
46
48
|
}
|
|
@@ -58,6 +58,50 @@ describe('Vue Components Generator', () => {
|
|
|
58
58
|
expect(index).toContain('export { default as AppLayout }');
|
|
59
59
|
expect(index).toContain('@abgov/vue-components'); // interim marker
|
|
60
60
|
expect(index).toContain("from './lib/formatters'");
|
|
61
|
+
expect(index).toContain('export { default as GoabDatePicker }');
|
|
62
|
+
expect(index).toContain('export { default as FilterBar }');
|
|
63
|
+
|
|
64
|
+
// GoabDatePicker is the one wrapper whose _change detail.value is a Date
|
|
65
|
+
// rather than a string -- verified against the installed web-components,
|
|
66
|
+
// where the dispatch reads `value: j.date, valueStr: r`.
|
|
67
|
+
const datePicker = host
|
|
68
|
+
.read('libs/vue-components/src/lib/primitives/GoabDatePicker.vue')
|
|
69
|
+
.toString();
|
|
70
|
+
expect(datePicker).toContain('defineModel<Date | undefined>()');
|
|
71
|
+
expect(datePicker).toContain('CustomEvent<{ value: Date }>');
|
|
72
|
+
|
|
73
|
+
// FilterBar stays presentational: it emits a query-ready values object and
|
|
74
|
+
// never touches the page, the router, or the network.
|
|
75
|
+
const filterBar = host
|
|
76
|
+
.read('libs/vue-components/src/lib/patterns/FilterBar.vue')
|
|
77
|
+
.toString();
|
|
78
|
+
expect(filterBar).toContain('goa-filter-chip');
|
|
79
|
+
expect(filterBar).toContain('goa-details');
|
|
80
|
+
// Precise API forms, not bare words -- 'page' appears in the component's own
|
|
81
|
+
// comment explaining that the *view* owns it.
|
|
82
|
+
for (const forbidden of [
|
|
83
|
+
'useRouter',
|
|
84
|
+
'useRoute',
|
|
85
|
+
'apiFetch',
|
|
86
|
+
'fetch(',
|
|
87
|
+
'page.value',
|
|
88
|
+
]) {
|
|
89
|
+
expect(filterBar).not.toContain(forbidden);
|
|
90
|
+
}
|
|
91
|
+
// Local calendar parts, not UTC: in Alberta new Date('2026-08-28') is
|
|
92
|
+
// 27 Aug 18:00 local, so a stored date would render as the day before.
|
|
93
|
+
expect(filterBar).toContain('function toIsoDate');
|
|
94
|
+
expect(filterBar).toContain('function fromIsoDate');
|
|
95
|
+
// The call form, not the bare name: the component's own comments explain why
|
|
96
|
+
// toISOString() is wrong, so the name legitimately appears in them.
|
|
97
|
+
expect(filterBar).not.toContain('date.toISOString()');
|
|
98
|
+
|
|
99
|
+
for (const spec of [
|
|
100
|
+
'libs/vue-components/src/lib/patterns/FilterBar.spec.ts',
|
|
101
|
+
'libs/vue-components/src/lib/primitives/GoabDatePicker.spec.ts',
|
|
102
|
+
]) {
|
|
103
|
+
expect(host.exists(spec)).toBeTruthy();
|
|
104
|
+
}
|
|
61
105
|
|
|
62
106
|
// Shared value formatters: every view renders a date/count the same way
|
|
63
107
|
// instead of inlining its own toLocaleString (which is what a real app did
|