@genspectrum/dashboard-components 0.16.4 → 0.17.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/custom-elements.json +86 -61
- package/dist/{LineageFilterChangedEvent-COWV-Y0k.js → LineageFilterChangedEvent-DkvWdq_G.js} +2 -2
- package/dist/LineageFilterChangedEvent-DkvWdq_G.js.map +1 -0
- package/dist/components.d.ts +46 -34
- package/dist/components.js +304 -146
- package/dist/components.js.map +1 -1
- package/dist/style.css +76 -9
- package/dist/util.d.ts +11 -13
- package/dist/util.js +1 -1
- package/package.json +1 -1
- package/src/preact/components/clearable-select.stories.tsx +75 -0
- package/src/preact/components/clearable-select.tsx +76 -0
- package/src/preact/components/downshift-combobox.tsx +9 -7
- package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +31 -33
- package/src/preact/dateRangeFilter/computeInitialValues.ts +2 -15
- package/src/preact/dateRangeFilter/date-picker.tsx +66 -0
- package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +69 -31
- package/src/preact/dateRangeFilter/date-range-filter.tsx +136 -139
- package/src/preact/dateRangeFilter/dateRangeOption.ts +11 -11
- package/src/preact/shared/WithClassName/WithClassName.ts +1 -0
- package/src/preact/shared/icons/DeleteIcon.tsx +3 -0
- package/src/preact/shared/stories/expectOptionSelected.tsx +7 -0
- package/src/utilEntrypoint.ts +1 -1
- package/src/web-components/MutationAnnotations.mdx +33 -0
- package/src/web-components/ResizeContainer.mdx +1 -1
- package/src/web-components/errorHandling.mdx +1 -1
- package/src/web-components/gs-app.ts +2 -2
- package/src/web-components/input/gs-date-range-filter.stories.ts +38 -32
- package/src/web-components/input/gs-date-range-filter.tsx +8 -2
- package/src/web-components/input/gs-lineage-filter.tsx +1 -1
- package/src/web-components/input/gs-location-filter.tsx +1 -1
- package/src/web-components/input/gs-mutation-filter.tsx +1 -1
- package/src/web-components/input/gs-text-filter.tsx +1 -1
- package/src/web-components/visualization/gs-aggregate.tsx +2 -2
- package/src/web-components/visualization/gs-mutation-comparison.tsx +5 -2
- package/src/web-components/visualization/gs-mutations-over-time.tsx +5 -2
- package/src/web-components/visualization/gs-mutations.tsx +5 -2
- package/src/web-components/visualization/gs-number-sequences-over-time.tsx +2 -2
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -2
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +2 -2
- package/src/web-components/visualization/gs-sequences-by-location.tsx +2 -2
- package/src/web-components/visualization/gs-statistics.tsx +2 -2
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +2 -2
- package/standalone-bundle/dashboard-components.js +6624 -6538
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/LineageFilterChangedEvent-COWV-Y0k.js.map +0 -1
|
@@ -8,8 +8,9 @@ import { DateRangeFilter, type DateRangeFilterProps } from './date-range-filter'
|
|
|
8
8
|
import { previewHandles } from '../../../.storybook/preview';
|
|
9
9
|
import { LAPIS_URL } from '../../constants';
|
|
10
10
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
11
|
-
import { dateRangeOptionPresets } from './dateRangeOption';
|
|
11
|
+
import { dateRangeOptionPresets, type DateRangeValue } from './dateRangeOption';
|
|
12
12
|
import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
|
|
13
|
+
import { expectOptionSelected } from '../shared/stories/expectOptionSelected';
|
|
13
14
|
|
|
14
15
|
const earliestDate = '1970-01-01';
|
|
15
16
|
|
|
@@ -19,6 +20,8 @@ const customDateRange = {
|
|
|
19
20
|
dateTo: '2021-12-31',
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
const placeholder = 'Date range';
|
|
24
|
+
|
|
22
25
|
const meta: Meta<DateRangeFilterProps> = {
|
|
23
26
|
title: 'Input/DateRangeFilter',
|
|
24
27
|
component: DateRangeFilter,
|
|
@@ -56,6 +59,7 @@ const meta: Meta<DateRangeFilterProps> = {
|
|
|
56
59
|
value: undefined,
|
|
57
60
|
lapisDateField: 'aDateColumn',
|
|
58
61
|
width: '100%',
|
|
62
|
+
placeholder,
|
|
59
63
|
},
|
|
60
64
|
};
|
|
61
65
|
|
|
@@ -69,6 +73,22 @@ const Primary: StoryObj<DateRangeFilterProps> = {
|
|
|
69
73
|
),
|
|
70
74
|
};
|
|
71
75
|
|
|
76
|
+
export const WithUndefinedValue: StoryObj<DateRangeFilterProps> = {
|
|
77
|
+
...Primary,
|
|
78
|
+
args: {
|
|
79
|
+
...Primary.args,
|
|
80
|
+
},
|
|
81
|
+
play: async ({ canvasElement }) => {
|
|
82
|
+
const canvas = within(canvasElement);
|
|
83
|
+
|
|
84
|
+
await waitFor(async () => {
|
|
85
|
+
await expectOptionSelected(canvasElement, placeholder);
|
|
86
|
+
await expect(dateFromPicker(canvas)).toHaveValue('');
|
|
87
|
+
await expect(dateToPicker(canvas)).toHaveValue('');
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
72
92
|
export const SetCorrectInitialValues: StoryObj<DateRangeFilterProps> = {
|
|
73
93
|
...Primary,
|
|
74
94
|
args: {
|
|
@@ -79,6 +99,7 @@ export const SetCorrectInitialValues: StoryObj<DateRangeFilterProps> = {
|
|
|
79
99
|
const canvas = within(canvasElement);
|
|
80
100
|
|
|
81
101
|
await waitFor(async () => {
|
|
102
|
+
await expectOptionSelected(canvasElement, 'CustomDateRange');
|
|
82
103
|
await expect(selectField(canvas)).toHaveValue('CustomDateRange');
|
|
83
104
|
await expect(dateFromPicker(canvas)).toHaveValue('2021-01-01');
|
|
84
105
|
await expect(dateToPicker(canvas)).toHaveValue('2021-12-31');
|
|
@@ -98,7 +119,7 @@ export const SetCorrectInitialDateFrom: StoryObj<DateRangeFilterProps> = {
|
|
|
98
119
|
const canvas = within(canvasElement);
|
|
99
120
|
|
|
100
121
|
await waitFor(async () => {
|
|
101
|
-
await
|
|
122
|
+
await expectOptionSelected(canvasElement, 'Custom');
|
|
102
123
|
await expect(dateFromPicker(canvas)).toHaveValue(initialDateFrom);
|
|
103
124
|
await expect(dateToPicker(canvas)).toHaveValue(dayjs().format('YYYY-MM-DD'));
|
|
104
125
|
});
|
|
@@ -117,14 +138,14 @@ export const SetCorrectInitialDateTo: StoryObj<DateRangeFilterProps> = {
|
|
|
117
138
|
const canvas = within(canvasElement);
|
|
118
139
|
|
|
119
140
|
await waitFor(async () => {
|
|
120
|
-
await
|
|
141
|
+
await expectOptionSelected(canvasElement, 'Custom');
|
|
121
142
|
await expect(dateFromPicker(canvas)).toHaveValue(earliestDate);
|
|
122
143
|
await expect(dateToPicker(canvas)).toHaveValue(initialDateTo);
|
|
123
144
|
});
|
|
124
145
|
},
|
|
125
146
|
};
|
|
126
147
|
|
|
127
|
-
export const
|
|
148
|
+
export const SetsValueOnBlur: StoryObj<DateRangeFilterProps> = {
|
|
128
149
|
...Primary,
|
|
129
150
|
args: {
|
|
130
151
|
...Primary.args,
|
|
@@ -143,26 +164,28 @@ export const ChangingDateSetsOptionToCustom: StoryObj<DateRangeFilterProps> = {
|
|
|
143
164
|
await userEvent.click(dateToPicker(canvas));
|
|
144
165
|
|
|
145
166
|
await waitFor(async () => {
|
|
146
|
-
await
|
|
167
|
+
await expectOptionSelected(canvasElement, 'Custom');
|
|
147
168
|
});
|
|
148
169
|
|
|
149
|
-
await
|
|
150
|
-
expect.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
await waitFor(async () => {
|
|
171
|
+
await expect(filterChangedListenerMock).toHaveBeenCalledWith(
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
detail: {
|
|
174
|
+
aDateColumnFrom: '2000-01-01',
|
|
175
|
+
aDateColumnTo: dayjs().format('YYYY-MM-DD'),
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await expect(optionChangedListenerMock).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
detail: {
|
|
183
|
+
dateFrom: '2000-01-01',
|
|
184
|
+
dateTo: dayjs().format('YYYY-MM-DD'),
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
166
189
|
});
|
|
167
190
|
},
|
|
168
191
|
};
|
|
@@ -171,12 +194,13 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
|
|
|
171
194
|
...Primary,
|
|
172
195
|
render: (args) => {
|
|
173
196
|
const StatefulWrapper = () => {
|
|
174
|
-
const [value, setValue] = useState('Last month');
|
|
197
|
+
const [value, setValue] = useState<DateRangeValue | undefined>('Last month');
|
|
175
198
|
const ref = useRef<HTMLDivElement>(null);
|
|
176
199
|
|
|
177
200
|
useEffect(() => {
|
|
178
201
|
ref.current?.addEventListener('gs-date-range-option-changed', (event) => {
|
|
179
|
-
|
|
202
|
+
const newValue = (event as CustomEvent).detail;
|
|
203
|
+
setValue(newValue ?? undefined);
|
|
180
204
|
});
|
|
181
205
|
}, []);
|
|
182
206
|
|
|
@@ -207,12 +231,13 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
|
|
|
207
231
|
await step('Change the value of the component programmatically', async () => {
|
|
208
232
|
await userEvent.click(canvas.getByRole('button', { name: 'Set to Custom' }));
|
|
209
233
|
await waitFor(async () => {
|
|
234
|
+
await expectOptionSelected(canvasElement, customDateRange.label);
|
|
210
235
|
await expect(selectField(canvas)).toHaveValue(customDateRange.label);
|
|
211
236
|
});
|
|
212
237
|
|
|
213
238
|
await userEvent.click(canvas.getByRole('button', { name: 'Set to Last month' }));
|
|
214
239
|
await waitFor(async () => {
|
|
215
|
-
await
|
|
240
|
+
await expectOptionSelected(canvasElement, 'Last month');
|
|
216
241
|
});
|
|
217
242
|
|
|
218
243
|
await expect(filterChangedListenerMock).toHaveBeenCalledTimes(0);
|
|
@@ -220,12 +245,25 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
|
|
|
220
245
|
});
|
|
221
246
|
|
|
222
247
|
await step('Changing the value from within the component is still possible', async () => {
|
|
223
|
-
await userEvent.selectOptions(selectField(canvas), 'All times');
|
|
224
248
|
await waitFor(async () => {
|
|
225
|
-
await
|
|
249
|
+
await userEvent.selectOptions(selectField(canvas), 'All times');
|
|
250
|
+
await expectOptionSelected(canvasElement, 'All times');
|
|
251
|
+
});
|
|
252
|
+
await waitFor(async () => {
|
|
253
|
+
await expect(filterChangedListenerMock).toHaveBeenCalledTimes(1);
|
|
254
|
+
await expect(optionChangedListenerMock).toHaveBeenCalledTimes(1);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await step('Clearing the value from within the component is still possible', async () => {
|
|
259
|
+
await waitFor(async () => {
|
|
260
|
+
await userEvent.click(canvas.getByRole('button', { name: '×' }));
|
|
261
|
+
await expectOptionSelected(canvasElement, placeholder);
|
|
262
|
+
});
|
|
263
|
+
await waitFor(async () => {
|
|
264
|
+
await expect(filterChangedListenerMock).toHaveBeenCalledTimes(2);
|
|
265
|
+
await expect(optionChangedListenerMock).toHaveBeenCalledTimes(2);
|
|
226
266
|
});
|
|
227
|
-
await expect(filterChangedListenerMock).toHaveBeenCalledTimes(1);
|
|
228
|
-
await expect(optionChangedListenerMock).toHaveBeenCalledTimes(1);
|
|
229
267
|
});
|
|
230
268
|
},
|
|
231
269
|
};
|
|
@@ -236,13 +274,13 @@ export const ChangingDateOption: StoryObj<DateRangeFilterProps> = {
|
|
|
236
274
|
const { canvas, filterChangedListenerMock, optionChangedListenerMock } = await prepare(canvasElement, step);
|
|
237
275
|
|
|
238
276
|
await waitFor(async () => {
|
|
239
|
-
await
|
|
277
|
+
await expectOptionSelected(canvasElement, placeholder);
|
|
240
278
|
});
|
|
241
279
|
|
|
242
280
|
await step('Change date to custom', async () => {
|
|
243
281
|
await waitFor(async () => {
|
|
244
282
|
await userEvent.selectOptions(selectField(canvas), 'CustomDateRange');
|
|
245
|
-
await
|
|
283
|
+
await expectOptionSelected(canvasElement, 'CustomDateRange');
|
|
246
284
|
});
|
|
247
285
|
|
|
248
286
|
await expect(filterChangedListenerMock).toHaveBeenCalledWith(
|
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
import 'flatpickr/dist/flatpickr.min.css';
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
4
2
|
import z from 'zod';
|
|
5
3
|
|
|
6
4
|
import { computeInitialValues } from './computeInitialValues';
|
|
5
|
+
import { DatePicker } from './date-picker';
|
|
7
6
|
import { toYYYYMMDD } from './dateConversion';
|
|
8
7
|
import {
|
|
8
|
+
type DateRangeOption,
|
|
9
9
|
DateRangeOptionChangedEvent,
|
|
10
10
|
dateRangeOptionSchema,
|
|
11
|
-
type DateRangeSelectOption,
|
|
12
11
|
dateRangeValueSchema,
|
|
13
12
|
} from './dateRangeOption';
|
|
14
|
-
import {
|
|
13
|
+
import { ClearableSelect } from '../components/clearable-select';
|
|
15
14
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
16
|
-
import { Select } from '../components/select';
|
|
17
|
-
import type { ScaleType } from '../shared/charts/getYAxisScale';
|
|
18
15
|
|
|
19
16
|
const customOption = 'Custom';
|
|
20
17
|
|
|
21
18
|
const dateRangeFilterInnerPropsSchema = z.object({
|
|
22
19
|
dateRangeOptions: z.array(dateRangeOptionSchema),
|
|
23
20
|
earliestDate: z.string().date(),
|
|
24
|
-
value: dateRangeValueSchema
|
|
21
|
+
value: dateRangeValueSchema,
|
|
25
22
|
lapisDateField: z.string().min(1),
|
|
23
|
+
placeholder: z.string().optional(),
|
|
26
24
|
});
|
|
27
25
|
|
|
28
26
|
const dateRangeFilterPropsSchema = dateRangeFilterInnerPropsSchema.extend({
|
|
@@ -32,6 +30,12 @@ const dateRangeFilterPropsSchema = dateRangeFilterInnerPropsSchema.extend({
|
|
|
32
30
|
export type DateRangeFilterProps = z.infer<typeof dateRangeFilterPropsSchema>;
|
|
33
31
|
export type DateRangeFilterInnerProps = z.infer<typeof dateRangeFilterInnerPropsSchema>;
|
|
34
32
|
|
|
33
|
+
type DateRangeFilterState = {
|
|
34
|
+
label: string;
|
|
35
|
+
dateFrom?: Date;
|
|
36
|
+
dateTo?: Date;
|
|
37
|
+
} | null;
|
|
38
|
+
|
|
35
39
|
export const DateRangeFilter = (props: DateRangeFilterProps) => {
|
|
36
40
|
const { width, ...innerProps } = props;
|
|
37
41
|
const size = { width, height: '3rem' };
|
|
@@ -50,135 +54,116 @@ export const DateRangeFilterInner = ({
|
|
|
50
54
|
earliestDate = '1900-01-01',
|
|
51
55
|
value,
|
|
52
56
|
lapisDateField,
|
|
57
|
+
placeholder,
|
|
53
58
|
}: DateRangeFilterInnerProps) => {
|
|
54
59
|
const initialValues = useMemo(
|
|
55
60
|
() => computeInitialValues(value, earliestDate, dateRangeOptions),
|
|
56
61
|
[value, earliestDate, dateRangeOptions],
|
|
57
62
|
);
|
|
58
63
|
|
|
59
|
-
const fromDatePickerRef = useRef<HTMLInputElement>(null);
|
|
60
|
-
const toDatePickerRef = useRef<HTMLInputElement>(null);
|
|
61
64
|
const divRef = useRef<HTMLDivElement>(null);
|
|
62
|
-
const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
|
|
63
|
-
const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
|
|
64
65
|
|
|
65
|
-
const
|
|
66
|
-
initialValues
|
|
66
|
+
const getInitialState = useCallback(() => {
|
|
67
|
+
if (!initialValues) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return initialValues.initialSelectedDateRange
|
|
71
|
+
? {
|
|
72
|
+
label: initialValues.initialSelectedDateRange,
|
|
73
|
+
dateFrom: initialValues.initialSelectedDateFrom,
|
|
74
|
+
dateTo: initialValues.initialSelectedDateTo,
|
|
75
|
+
}
|
|
76
|
+
: {
|
|
77
|
+
label: customOption,
|
|
78
|
+
dateFrom: initialValues.initialSelectedDateFrom,
|
|
79
|
+
dateTo: initialValues.initialSelectedDateTo,
|
|
80
|
+
};
|
|
81
|
+
}, [initialValues]);
|
|
82
|
+
|
|
83
|
+
const customComboboxValue = { label: customOption };
|
|
84
|
+
const [options, setOptions] = useState(
|
|
85
|
+
getInitialState()?.label === customOption ? [...dateRangeOptions, customComboboxValue] : [...dateRangeOptions],
|
|
67
86
|
);
|
|
87
|
+
const [state, setState] = useState<DateRangeFilterState>(getInitialState());
|
|
68
88
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
dateTo:
|
|
72
|
-
|
|
89
|
+
function updateState(newState: DateRangeFilterState) {
|
|
90
|
+
setState(newState);
|
|
91
|
+
fireFilterChangedEvent({ dateFrom: newState?.dateFrom, dateTo: newState?.dateTo, lapisDateField });
|
|
92
|
+
fireOptionChangedEvent(newState);
|
|
93
|
+
}
|
|
73
94
|
|
|
74
95
|
useEffect(() => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
defaultDate: initialValues.initialSelectedDateFrom,
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
96
|
+
setState(getInitialState());
|
|
97
|
+
}, [getInitialState]);
|
|
98
|
+
|
|
99
|
+
const onSelectChange = (option: DateRangeOption | null) => {
|
|
100
|
+
updateState(
|
|
101
|
+
option !== null
|
|
102
|
+
? {
|
|
103
|
+
label: option?.label,
|
|
104
|
+
dateFrom: getFromDate(option, earliestDate),
|
|
105
|
+
dateTo: getToDate(option),
|
|
106
|
+
}
|
|
107
|
+
: null,
|
|
108
|
+
);
|
|
109
|
+
if (option?.label !== customOption) {
|
|
110
|
+
setOptions([...dateRangeOptions]);
|
|
93
111
|
}
|
|
112
|
+
};
|
|
94
113
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
...commonConfig,
|
|
99
|
-
defaultDate: initialValues.initialSelectedDateTo,
|
|
100
|
-
}),
|
|
101
|
-
);
|
|
114
|
+
function getFromDate(option: DateRangeOption | null, earliestDate: string) {
|
|
115
|
+
if (!option || option.label === customOption) {
|
|
116
|
+
return undefined;
|
|
102
117
|
}
|
|
118
|
+
return new Date(option?.dateFrom ?? earliestDate);
|
|
119
|
+
}
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return null;
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
}, [fromDatePickerRef, toDatePickerRef, initialValues]);
|
|
115
|
-
|
|
116
|
-
const onSelectChange = (value: string) => {
|
|
117
|
-
setSelectedDateRange(value);
|
|
118
|
-
|
|
119
|
-
const dateRange = getDatesForSelectorValue(value, dateRangeOptions, earliestDate);
|
|
120
|
-
|
|
121
|
-
dateToPicker?.set('minDate', dateRange.dateFrom);
|
|
122
|
-
dateFromPicker?.set('maxDate', dateRange.dateTo);
|
|
123
|
-
|
|
124
|
-
dateFromPicker?.setDate(dateRange.dateFrom);
|
|
125
|
-
dateToPicker?.setDate(dateRange.dateTo);
|
|
126
|
-
|
|
127
|
-
setSelectedDates({
|
|
128
|
-
dateFrom: dateRange.dateFrom,
|
|
129
|
-
dateTo: dateRange.dateTo,
|
|
130
|
-
});
|
|
121
|
+
function getToDate(option: DateRangeOption | null) {
|
|
122
|
+
if (!option || option.label === customOption) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (!option.dateTo) {
|
|
126
|
+
return new Date();
|
|
127
|
+
}
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
};
|
|
129
|
+
return new Date(option.dateTo);
|
|
130
|
+
}
|
|
135
131
|
|
|
136
|
-
const onChangeDateFrom = () => {
|
|
137
|
-
if (
|
|
132
|
+
const onChangeDateFrom = (date: Date | undefined) => {
|
|
133
|
+
if (date?.toDateString() === state?.dateFrom?.toDateString()) {
|
|
138
134
|
return;
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
dateToPicker?.set('minDate', dateFrom);
|
|
146
|
-
setSelectedDateRange(customOption);
|
|
147
|
-
|
|
148
|
-
fireFilterChangedEvent();
|
|
149
|
-
fireOptionChangedEvent({
|
|
150
|
-
dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
|
|
151
|
-
dateTo: toYYYYMMDD(dateTo || new Date()),
|
|
137
|
+
updateState({
|
|
138
|
+
label: customOption,
|
|
139
|
+
dateFrom: date,
|
|
140
|
+
dateTo: state?.dateTo,
|
|
152
141
|
});
|
|
142
|
+
setOptions([...dateRangeOptions, customComboboxValue]);
|
|
153
143
|
};
|
|
154
144
|
|
|
155
|
-
const onChangeDateTo = () => {
|
|
156
|
-
if (
|
|
145
|
+
const onChangeDateTo = (date: Date | undefined) => {
|
|
146
|
+
if (date?.toDateString() === state?.dateTo?.toDateString()) {
|
|
157
147
|
return;
|
|
158
148
|
}
|
|
159
149
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
dateFromPicker?.set('maxDate', dateTo);
|
|
165
|
-
setSelectedDateRange(customOption);
|
|
166
|
-
|
|
167
|
-
fireFilterChangedEvent();
|
|
168
|
-
fireOptionChangedEvent({
|
|
169
|
-
dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
|
|
170
|
-
dateTo: toYYYYMMDD(dateTo || new Date()),
|
|
150
|
+
updateState({
|
|
151
|
+
label: customOption,
|
|
152
|
+
dateFrom: state?.dateFrom,
|
|
153
|
+
dateTo: date,
|
|
171
154
|
});
|
|
155
|
+
setOptions([...dateRangeOptions, customComboboxValue]);
|
|
172
156
|
};
|
|
173
157
|
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
158
|
+
const fireFilterChangedEvent = ({
|
|
159
|
+
dateFrom,
|
|
160
|
+
dateTo,
|
|
161
|
+
lapisDateField,
|
|
162
|
+
}: {
|
|
163
|
+
dateFrom: Date | undefined;
|
|
164
|
+
dateTo: Date | undefined;
|
|
165
|
+
lapisDateField: string;
|
|
166
|
+
}) => {
|
|
182
167
|
const detail = {
|
|
183
168
|
...(dateFrom !== undefined && { [`${lapisDateField}From`]: toYYYYMMDD(dateFrom) }),
|
|
184
169
|
...(dateTo !== undefined && { [`${lapisDateField}To`]: toYYYYMMDD(dateTo) }),
|
|
@@ -193,39 +178,51 @@ export const DateRangeFilterInner = ({
|
|
|
193
178
|
);
|
|
194
179
|
};
|
|
195
180
|
|
|
181
|
+
const fireOptionChangedEvent = (state: DateRangeFilterState) => {
|
|
182
|
+
const eventDetail =
|
|
183
|
+
state?.label === customOption
|
|
184
|
+
? {
|
|
185
|
+
dateFrom: state.dateFrom !== undefined ? toYYYYMMDD(state.dateFrom) : undefined,
|
|
186
|
+
dateTo: state.dateTo !== undefined ? toYYYYMMDD(state.dateTo) : undefined,
|
|
187
|
+
}
|
|
188
|
+
: state?.label;
|
|
189
|
+
|
|
190
|
+
divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(eventDetail));
|
|
191
|
+
};
|
|
192
|
+
|
|
196
193
|
return (
|
|
197
|
-
<div
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
194
|
+
<div className={'@container'} ref={divRef}>
|
|
195
|
+
<div className='flex min-w-[7.5rem] flex-col @md:flex-row'>
|
|
196
|
+
<div className='flex-grow'>
|
|
197
|
+
<ClearableSelect
|
|
198
|
+
items={options.map((item) => item.label)}
|
|
199
|
+
placeholderText={placeholder}
|
|
200
|
+
onChange={(value) => {
|
|
201
|
+
const dateRangeOption = options.find((item) => item.label === value);
|
|
202
|
+
onSelectChange(dateRangeOption ?? null);
|
|
203
|
+
}}
|
|
204
|
+
value={state?.label ?? null}
|
|
205
|
+
selectClassName={'rounded-t-md rounded-b-none @md:rounded-l-md @md:rounded-r-none'}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
<div className={'flex flex-grow flex-col @4xs:flex-row'}>
|
|
209
|
+
<DatePicker
|
|
210
|
+
className={'flex-grow min-w-[7.5rem] @4xs:rounded-bl-md @md:rounded-l-none rounded-none'}
|
|
211
|
+
value={state?.dateFrom}
|
|
212
|
+
onChange={onChangeDateFrom}
|
|
213
|
+
maxDate={state?.dateTo}
|
|
214
|
+
placeholderText={'Date from'}
|
|
215
|
+
/>
|
|
216
|
+
<DatePicker
|
|
217
|
+
className={
|
|
218
|
+
'flex-grow min-w-[7.5rem] rounded-b-md rounded-t-none @4xs:rounded-tr-none @4xs:rounded-l-none @md:rounded-r-md '
|
|
219
|
+
}
|
|
220
|
+
value={state?.dateTo}
|
|
221
|
+
onChange={onChangeDateTo}
|
|
222
|
+
minDate={state?.dateFrom}
|
|
223
|
+
placeholderText={'Date to'}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
229
226
|
</div>
|
|
230
227
|
</div>
|
|
231
228
|
);
|
|
@@ -22,20 +22,20 @@ export const dateRangeOptionSchema = z.object({
|
|
|
22
22
|
|
|
23
23
|
export type DateRangeOption = z.infer<typeof dateRangeOptionSchema>;
|
|
24
24
|
|
|
25
|
-
export const dateRangeValueSchema = z
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
export const dateRangeValueSchema = z
|
|
26
|
+
.union([
|
|
27
|
+
z.string(),
|
|
28
|
+
z.object({
|
|
29
|
+
dateFrom: z.string().date().optional(),
|
|
30
|
+
dateTo: z.string().date().optional(),
|
|
31
|
+
}),
|
|
32
|
+
])
|
|
33
|
+
.optional();
|
|
32
34
|
|
|
33
35
|
export type DateRangeValue = z.infer<typeof dateRangeValueSchema>;
|
|
34
36
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
|
|
38
|
-
constructor(detail: DateRangeSelectOption) {
|
|
37
|
+
export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeValue> {
|
|
38
|
+
constructor(detail: DateRangeValue) {
|
|
39
39
|
super('gs-date-range-option-changed', {
|
|
40
40
|
detail,
|
|
41
41
|
bubbles: true,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type WithClassName<T = object> = T & { className?: string };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { expect, within } from '@storybook/test';
|
|
2
|
+
|
|
3
|
+
export const expectOptionSelected = async (canvasElement: HTMLElement, option: string) => {
|
|
4
|
+
const canvas = within(canvasElement);
|
|
5
|
+
const placeholderOption = canvas.getByRole('combobox').querySelector('option:checked');
|
|
6
|
+
await expect(placeholderOption).toHaveTextContent(option);
|
|
7
|
+
};
|
package/src/utilEntrypoint.ts
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title='Concepts/Mutation Annotations' />
|
|
4
|
+
|
|
5
|
+
# Mutation Annotations
|
|
6
|
+
|
|
7
|
+
Organism data usually has a lot of mutations.
|
|
8
|
+
It can be challenging for users to find out which mutations are relevant to investigate.
|
|
9
|
+
To guide the user to the most interesting mutations, we provide a mutation annotation feature.
|
|
10
|
+
|
|
11
|
+
The mutation annotations can be (optionally) supplied to `gs-app` as a JSON object:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<gs-app
|
|
15
|
+
lapis="https://your.lapis.url"
|
|
16
|
+
mutationAnnotations="[
|
|
17
|
+
{
|
|
18
|
+
name: 'I am an annotation!',
|
|
19
|
+
description: 'This describes what is special about these mutations.',
|
|
20
|
+
symbol: '+',
|
|
21
|
+
nucleotideMutations: ['C44T', 'C774T', 'G24872T', 'T23011-'],
|
|
22
|
+
aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
|
|
23
|
+
},
|
|
24
|
+
]"
|
|
25
|
+
>
|
|
26
|
+
{/* children... */}
|
|
27
|
+
</gs-app>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The mutation annotations are then distributed to child components.
|
|
31
|
+
Whenever we display a mutation (e.g. in the `gs-mutations` table view)
|
|
32
|
+
we will append the `symbol` of all matching annotations: <span>C44T<sup class='text-red-600'>+</sup></span>.
|
|
33
|
+
Users can click on the mutation to open a modal that shows the `name` and `description` of the annotation.
|