@genspectrum/dashboard-components 0.13.7 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/custom-elements.json +24 -62
  2. package/dist/{LineageFilterChangedEvent-GedKNGFI.js → LineageFilterChangedEvent-C9dXOxt6.js} +11 -3
  3. package/dist/LineageFilterChangedEvent-C9dXOxt6.js.map +1 -0
  4. package/dist/assets/mutationOverTimeWorker-Dxnxrfe0.js.map +1 -1
  5. package/dist/components.d.ts +40 -50
  6. package/dist/components.js +112 -91
  7. package/dist/components.js.map +1 -1
  8. package/dist/util.d.ts +38 -28
  9. package/dist/util.js +1 -1
  10. package/package.json +2 -2
  11. package/src/lapisApi/lapisApi.ts +1 -1
  12. package/src/operator/FillMissingOperator.spec.ts +1 -1
  13. package/src/operator/GroupByAndSumOperator.spec.ts +1 -1
  14. package/src/operator/GroupByOperator.spec.ts +2 -2
  15. package/src/operator/MapOperator.spec.ts +1 -1
  16. package/src/operator/MockOperator.spec.ts +1 -1
  17. package/src/operator/MockOperator.ts +6 -4
  18. package/src/operator/SortOperator.spec.ts +1 -1
  19. package/src/preact/LapisUrlContext.ts +14 -1
  20. package/src/preact/aggregatedData/aggregate.stories.tsx +4 -4
  21. package/src/preact/aggregatedData/aggregate.tsx +3 -4
  22. package/src/preact/components/csv-download-button.stories.tsx +2 -2
  23. package/src/preact/components/csv-download-button.tsx +1 -1
  24. package/src/preact/components/error-boundary.stories.tsx +5 -5
  25. package/src/preact/components/error-boundary.tsx +14 -3
  26. package/src/preact/components/error-display.stories.tsx +9 -9
  27. package/src/preact/components/fullscreen.tsx +3 -3
  28. package/src/preact/components/info.tsx +1 -1
  29. package/src/preact/components/mutation-type-selector.stories.tsx +1 -1
  30. package/src/preact/components/table.stories.tsx +3 -3
  31. package/src/preact/components/table.tsx +1 -1
  32. package/src/preact/dateRangeSelector/computeInitialValues.spec.ts +34 -20
  33. package/src/preact/dateRangeSelector/computeInitialValues.ts +25 -21
  34. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +107 -46
  35. package/src/preact/dateRangeSelector/date-range-selector.tsx +31 -22
  36. package/src/preact/dateRangeSelector/dateRangeOption.ts +11 -1
  37. package/src/preact/lineageFilter/lineage-filter.stories.tsx +9 -9
  38. package/src/preact/lineageFilter/lineage-filter.tsx +3 -4
  39. package/src/preact/locationFilter/fetchAutocompletionList.ts +1 -1
  40. package/src/preact/locationFilter/location-filter.stories.tsx +9 -9
  41. package/src/preact/locationFilter/location-filter.tsx +4 -4
  42. package/src/preact/map/sequences-by-location.stories.tsx +4 -4
  43. package/src/preact/map/sequences-by-location.tsx +3 -4
  44. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +3 -3
  45. package/src/preact/mutationComparison/mutation-comparison.tsx +4 -4
  46. package/src/preact/mutationFilter/mutation-filter-info.tsx +3 -3
  47. package/src/preact/mutationFilter/mutation-filter.stories.tsx +7 -7
  48. package/src/preact/mutations/getMutationsGridData.ts +1 -1
  49. package/src/preact/mutations/mutations.stories.tsx +3 -3
  50. package/src/preact/mutations/mutations.tsx +4 -4
  51. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +3 -3
  52. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +4 -4
  53. package/src/preact/mutationsOverTime/mutations-over-time.tsx +5 -4
  54. package/src/preact/numberSequencesOverTime/number-sequences-over-time.stories.tsx +4 -4
  55. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +4 -4
  56. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +4 -4
  57. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -4
  58. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +4 -4
  59. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +4 -4
  60. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +4 -4
  61. package/src/preact/shared/floating-ui/hooks.ts +1 -1
  62. package/src/preact/statistic/statistics.stories.tsx +3 -3
  63. package/src/preact/statistic/statistics.tsx +2 -3
  64. package/src/preact/textInput/text-input.stories.tsx +7 -7
  65. package/src/preact/textInput/text-input.tsx +3 -4
  66. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +3 -3
  67. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +4 -4
  68. package/src/utils/map2d.ts +1 -0
  69. package/src/web-components/PreactLitAdapter.tsx +3 -3
  70. package/src/web-components/gs-app.stories.ts +7 -7
  71. package/src/web-components/gs-app.ts +3 -1
  72. package/src/web-components/input/gs-date-range-selector.stories.ts +10 -17
  73. package/src/web-components/input/gs-date-range-selector.tsx +15 -38
  74. package/src/web-components/input/gs-lineage-filter.stories.ts +1 -1
  75. package/src/web-components/input/gs-location-filter.stories.ts +1 -1
  76. package/src/web-components/input/gs-mutation-filter.stories.ts +7 -7
  77. package/src/web-components/input/gs-text-input.stories.ts +3 -3
  78. package/src/web-components/visualization/gs-aggregate.tsx +2 -2
  79. package/standalone-bundle/assets/mutationOverTimeWorker-CmSrq4SZ.js.map +1 -1
  80. package/standalone-bundle/dashboard-components.js +6068 -6055
  81. package/standalone-bundle/dashboard-components.js.map +1 -1
  82. package/dist/LineageFilterChangedEvent-GedKNGFI.js.map +0 -1
@@ -18,8 +18,8 @@ const dateRangeOptions = [
18
18
  ];
19
19
 
20
20
  describe('computeInitialValues', () => {
21
- it('should compute for initial value if initial "from" and "to" are unset', () => {
22
- const result = computeInitialValues(fromToOption, undefined, undefined, earliestDate, dateRangeOptions);
21
+ it('should compute initial value if value is dateRangeOption label', () => {
22
+ const result = computeInitialValues(fromToOption, earliestDate, dateRangeOptions);
23
23
 
24
24
  expect(result.initialSelectedDateRange).toEqual(fromToOption);
25
25
  expectDateMatches(result.initialSelectedDateFrom, new Date(dateFromOptionValue));
@@ -27,7 +27,7 @@ describe('computeInitialValues', () => {
27
27
  });
28
28
 
29
29
  it('should use today as "dateTo" if it is unset in selected option', () => {
30
- const result = computeInitialValues(fromOption, undefined, undefined, earliestDate, dateRangeOptions);
30
+ const result = computeInitialValues(fromOption, earliestDate, dateRangeOptions);
31
31
 
32
32
  expect(result.initialSelectedDateRange).toEqual(fromOption);
33
33
  expectDateMatches(result.initialSelectedDateFrom, new Date(dateFromOptionValue));
@@ -35,7 +35,7 @@ describe('computeInitialValues', () => {
35
35
  });
36
36
 
37
37
  it('should use earliest date as "dateFrom" if it is unset in selected option', () => {
38
- const result = computeInitialValues(toOption, undefined, undefined, earliestDate, dateRangeOptions);
38
+ const result = computeInitialValues(toOption, earliestDate, dateRangeOptions);
39
39
 
40
40
  expect(result.initialSelectedDateRange).toEqual(toOption);
41
41
  expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
@@ -43,7 +43,7 @@ describe('computeInitialValues', () => {
43
43
  });
44
44
 
45
45
  it('should fall back to full range if initial value is not set', () => {
46
- const result = computeInitialValues(undefined, undefined, undefined, earliestDate, dateRangeOptions);
46
+ const result = computeInitialValues(undefined, earliestDate, dateRangeOptions);
47
47
 
48
48
  expect(result.initialSelectedDateRange).toBeUndefined();
49
49
  expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
@@ -51,39 +51,46 @@ describe('computeInitialValues', () => {
51
51
  });
52
52
 
53
53
  it('should throw when initial value is unknown', () => {
54
- expect(() =>
55
- computeInitialValues('not a known value', undefined, undefined, earliestDate, dateRangeOptions),
56
- ).toThrowError(/Invalid initialValue "not a known value", It must be one of/);
54
+ expect(() => computeInitialValues('not a known value', earliestDate, dateRangeOptions)).toThrowError(
55
+ /Invalid value "not a known value", It must be one of/,
56
+ );
57
57
  });
58
58
 
59
59
  it('should throw when initial value is set but no options are provided', () => {
60
- expect(() => computeInitialValues('not a known value', undefined, undefined, earliestDate, [])).toThrowError(
60
+ expect(() => computeInitialValues('not a known value', earliestDate, [])).toThrowError(
61
61
  /There are no selectable options/,
62
62
  );
63
63
  });
64
64
 
65
- it('should overwrite initial value if initial "from" is set', () => {
65
+ it('should select from date until today if only dateFrom is given', () => {
66
66
  const initialDateFrom = '2020-01-01';
67
- const result = computeInitialValues(fromOption, initialDateFrom, undefined, earliestDate, dateRangeOptions);
67
+ const result = computeInitialValues({ dateFrom: initialDateFrom }, earliestDate, dateRangeOptions);
68
68
 
69
69
  expect(result.initialSelectedDateRange).toBeUndefined();
70
70
  expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
71
71
  expectDateMatches(result.initialSelectedDateTo, today);
72
72
  });
73
73
 
74
- it('should overwrite initial value if initial "to" is set', () => {
74
+ it('should select from earliest date until date if only dateTo is given', () => {
75
75
  const initialDateTo = '2020-01-01';
76
- const result = computeInitialValues(fromOption, undefined, initialDateTo, earliestDate, dateRangeOptions);
76
+ const result = computeInitialValues({ dateTo: initialDateTo }, earliestDate, dateRangeOptions);
77
77
 
78
78
  expect(result.initialSelectedDateRange).toBeUndefined();
79
79
  expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
80
80
  expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
81
81
  });
82
82
 
83
- it('should overwrite initial value if initial "to" and "from" are set', () => {
83
+ it('should select date range is dateFrom and dateTo are given', () => {
84
84
  const initialDateFrom = '2020-01-01';
85
85
  const initialDateTo = '2022-01-01';
86
- const result = computeInitialValues(fromOption, initialDateFrom, initialDateTo, earliestDate, dateRangeOptions);
86
+ const result = computeInitialValues(
87
+ {
88
+ dateFrom: initialDateFrom,
89
+ dateTo: initialDateTo,
90
+ },
91
+ earliestDate,
92
+ dateRangeOptions,
93
+ );
87
94
 
88
95
  expect(result.initialSelectedDateRange).toBeUndefined();
89
96
  expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
@@ -93,7 +100,14 @@ describe('computeInitialValues', () => {
93
100
  it('should set initial "to" to "from" if "from" is after "to"', () => {
94
101
  const initialDateFrom = '2020-01-01';
95
102
  const initialDateTo = '1900-01-01';
96
- const result = computeInitialValues(undefined, initialDateFrom, initialDateTo, earliestDate, dateRangeOptions);
103
+ const result = computeInitialValues(
104
+ {
105
+ dateFrom: initialDateFrom,
106
+ dateTo: initialDateTo,
107
+ },
108
+ earliestDate,
109
+ dateRangeOptions,
110
+ );
97
111
 
98
112
  expect(result.initialSelectedDateRange).toBeUndefined();
99
113
  expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
@@ -101,14 +115,14 @@ describe('computeInitialValues', () => {
101
115
  });
102
116
 
103
117
  it('should throw if initial "from" is not a valid date', () => {
104
- expect(() => computeInitialValues(undefined, 'not a date', undefined, earliestDate, [])).toThrowError(
105
- 'Invalid initialDateFrom',
118
+ expect(() => computeInitialValues({ dateFrom: 'not a date' }, earliestDate, [])).toThrowError(
119
+ 'Invalid value.dateFrom',
106
120
  );
107
121
  });
108
122
 
109
123
  it('should throw if initial "to" is not a valid date', () => {
110
- expect(() => computeInitialValues(undefined, undefined, 'not a date', earliestDate, [])).toThrowError(
111
- 'Invalid initialDateTo',
124
+ expect(() => computeInitialValues({ dateTo: 'not a date' }, earliestDate, [])).toThrowError(
125
+ 'Invalid value.dateTo',
112
126
  );
113
127
  });
114
128
 
@@ -1,11 +1,9 @@
1
- import { type DateRangeOption } from './dateRangeOption';
1
+ import { type DateRangeOption, type DateRangeValue } from './dateRangeOption';
2
2
  import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
3
3
  import { UserFacingError } from '../components/error-display';
4
4
 
5
5
  export function computeInitialValues(
6
- initialValue: string | undefined,
7
- initialDateFrom: string | undefined,
8
- initialDateTo: string | undefined,
6
+ value: DateRangeValue | undefined,
9
7
  earliestDate: string,
10
8
  dateRangeOptions: DateRangeOption[],
11
9
  ): {
@@ -13,20 +11,26 @@ export function computeInitialValues(
13
11
  initialSelectedDateFrom: Date;
14
12
  initialSelectedDateTo: Date;
15
13
  } {
16
- if (isUndefinedOrEmpty(initialDateFrom) && isUndefinedOrEmpty(initialDateTo)) {
14
+ if (value === undefined) {
15
+ const { dateFrom, dateTo } = getDatesForSelectorValue(undefined, dateRangeOptions, earliestDate);
16
+ return {
17
+ initialSelectedDateRange: undefined,
18
+ initialSelectedDateFrom: dateFrom,
19
+ initialSelectedDateTo: dateTo,
20
+ };
21
+ }
22
+
23
+ if (typeof value === 'string') {
17
24
  const selectableOptions = getSelectableOptions(dateRangeOptions);
18
- const initialSelectedDateRange = selectableOptions.find((option) => option.value === initialValue)?.value;
25
+ const initialSelectedDateRange = selectableOptions.find((option) => option.value === value)?.value;
19
26
 
20
- if (initialValue !== undefined && initialSelectedDateRange === undefined) {
27
+ if (initialSelectedDateRange === undefined) {
21
28
  if (selectableOptions.length === 0) {
22
- throw new UserFacingError(
23
- 'Invalid initialValue',
24
- 'There are no selectable options, but initialValue is set.',
25
- );
29
+ throw new UserFacingError('Invalid value', 'There are no selectable options, but value is set.');
26
30
  }
27
31
  throw new UserFacingError(
28
- 'Invalid initialValue',
29
- `Invalid initialValue "${initialValue}", It must be one of ${selectableOptions.map((option) => `'${option.value}'`).join(', ')}`,
32
+ 'Invalid value',
33
+ `Invalid value "${value}", It must be one of ${selectableOptions.map((option) => `'${option.value}'`).join(', ')}`,
30
34
  );
31
35
  }
32
36
 
@@ -39,21 +43,21 @@ export function computeInitialValues(
39
43
  };
40
44
  }
41
45
 
42
- const initialSelectedDateFrom = isUndefinedOrEmpty(initialDateFrom)
43
- ? new Date(earliestDate)
44
- : new Date(initialDateFrom);
45
- let initialSelectedDateTo = isUndefinedOrEmpty(initialDateTo) ? new Date() : new Date(initialDateTo);
46
+ const { dateFrom, dateTo } = value;
47
+
48
+ const initialSelectedDateFrom = isUndefinedOrEmpty(dateFrom) ? new Date(earliestDate) : new Date(dateFrom);
49
+ let initialSelectedDateTo = isUndefinedOrEmpty(dateTo) ? new Date() : new Date(dateTo);
46
50
 
47
51
  if (isNaN(initialSelectedDateFrom.getTime())) {
48
52
  throw new UserFacingError(
49
- 'Invalid initialDateFrom',
50
- `Invalid initialDateFrom "${initialDateFrom}", It must be of the format YYYY-MM-DD`,
53
+ 'Invalid value.dateFrom',
54
+ `Invalid value.dateFrom "${dateFrom}", It must be of the format YYYY-MM-DD`,
51
55
  );
52
56
  }
53
57
  if (isNaN(initialSelectedDateTo.getTime())) {
54
58
  throw new UserFacingError(
55
- 'Invalid initialDateTo',
56
- `Invalid initialDateTo "${initialDateTo}", It must be of the format YYYY-MM-DD`,
59
+ 'Invalid value.dateTo',
60
+ `Invalid value.dateTo "${dateTo}", It must be of the format YYYY-MM-DD`,
57
61
  );
58
62
  }
59
63
 
@@ -2,11 +2,12 @@ import { type Meta, type PreactRenderer, type StoryObj } from '@storybook/preact
2
2
  import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
3
  import type { StepFunction } from '@storybook/types';
4
4
  import dayjs from 'dayjs/esm';
5
+ import { useEffect, useRef, useState } from 'preact/hooks';
5
6
 
6
7
  import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
7
8
  import { previewHandles } from '../../../.storybook/preview';
8
9
  import { LAPIS_URL } from '../../constants';
9
- import { LapisUrlContext } from '../LapisUrlContext';
10
+ import { LapisUrlContextProvider } from '../LapisUrlContext';
10
11
  import { dateRangeOptionPresets } from './dateRangeOption';
11
12
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
12
13
 
@@ -28,11 +29,10 @@ const meta: Meta<DateRangeSelectorProps> = {
28
29
  fetchMock: {},
29
30
  },
30
31
  argTypes: {
31
- initialValue: {
32
+ value: {
32
33
  control: {
33
- type: 'select',
34
+ type: 'object',
34
35
  },
35
- options: [dateRangeOptionPresets.lastMonth.label, dateRangeOptionPresets.allTimes.label, 'CustomDateRange'],
36
36
  },
37
37
  dateRangeOptions: {
38
38
  control: {
@@ -53,21 +53,19 @@ const meta: Meta<DateRangeSelectorProps> = {
53
53
  args: {
54
54
  dateRangeOptions: [dateRangeOptionPresets.lastMonth, dateRangeOptionPresets.allTimes, customDateRange],
55
55
  earliestDate,
56
- initialValue: dateRangeOptionPresets.lastMonth.label,
56
+ value: undefined,
57
57
  lapisDateField: 'aDateColumn',
58
58
  width: '100%',
59
- initialDateFrom: undefined,
60
- initialDateTo: undefined,
61
59
  },
62
60
  };
63
61
 
64
62
  export default meta;
65
63
 
66
- export const Primary: StoryObj<DateRangeSelectorProps> = {
64
+ const Primary: StoryObj<DateRangeSelectorProps> = {
67
65
  render: (args) => (
68
- <LapisUrlContext.Provider value={LAPIS_URL}>
66
+ <LapisUrlContextProvider value={LAPIS_URL}>
69
67
  <DateRangeSelector {...args} />
70
- </LapisUrlContext.Provider>
68
+ </LapisUrlContextProvider>
71
69
  ),
72
70
  };
73
71
 
@@ -75,15 +73,15 @@ export const SetCorrectInitialValues: StoryObj<DateRangeSelectorProps> = {
75
73
  ...Primary,
76
74
  args: {
77
75
  ...Primary.args,
78
- initialValue: 'CustomDateRange',
76
+ value: 'CustomDateRange',
79
77
  },
80
78
  play: async ({ canvasElement }) => {
81
79
  const canvas = within(canvasElement);
82
80
 
83
- await waitFor(() => {
84
- expect(selectField(canvas)).toHaveValue('CustomDateRange');
85
- expect(dateFromPicker(canvas)).toHaveValue('2021-01-01');
86
- expect(dateToPicker(canvas)).toHaveValue('2021-12-31');
81
+ await waitFor(async () => {
82
+ await expect(selectField(canvas)).toHaveValue('CustomDateRange');
83
+ await expect(dateFromPicker(canvas)).toHaveValue('2021-01-01');
84
+ await expect(dateToPicker(canvas)).toHaveValue('2021-12-31');
87
85
  });
88
86
  },
89
87
  };
@@ -94,15 +92,15 @@ export const SetCorrectInitialDateFrom: StoryObj<DateRangeSelectorProps> = {
94
92
  ...Primary,
95
93
  args: {
96
94
  ...Primary.args,
97
- initialDateFrom,
95
+ value: { dateFrom: initialDateFrom },
98
96
  },
99
97
  play: async ({ canvasElement }) => {
100
98
  const canvas = within(canvasElement);
101
99
 
102
- await waitFor(() => {
103
- expect(selectField(canvas)).toHaveValue('Custom');
104
- expect(dateFromPicker(canvas)).toHaveValue(initialDateFrom);
105
- expect(dateToPicker(canvas)).toHaveValue(dayjs().format('YYYY-MM-DD'));
100
+ await waitFor(async () => {
101
+ await expect(selectField(canvas)).toHaveValue('Custom');
102
+ await expect(dateFromPicker(canvas)).toHaveValue(initialDateFrom);
103
+ await expect(dateToPicker(canvas)).toHaveValue(dayjs().format('YYYY-MM-DD'));
106
104
  });
107
105
  },
108
106
  };
@@ -113,35 +111,39 @@ export const SetCorrectInitialDateTo: StoryObj<DateRangeSelectorProps> = {
113
111
  ...Primary,
114
112
  args: {
115
113
  ...Primary.args,
116
- initialDateTo,
114
+ value: { dateTo: initialDateTo },
117
115
  },
118
116
  play: async ({ canvasElement }) => {
119
117
  const canvas = within(canvasElement);
120
118
 
121
- await waitFor(() => {
122
- expect(selectField(canvas)).toHaveValue('Custom');
123
- expect(dateFromPicker(canvas)).toHaveValue(earliestDate);
124
- expect(dateToPicker(canvas)).toHaveValue(initialDateTo);
119
+ await waitFor(async () => {
120
+ await expect(selectField(canvas)).toHaveValue('Custom');
121
+ await expect(dateFromPicker(canvas)).toHaveValue(earliestDate);
122
+ await expect(dateToPicker(canvas)).toHaveValue(initialDateTo);
125
123
  });
126
124
  },
127
125
  };
128
126
 
129
127
  export const ChangingDateSetsOptionToCustom: StoryObj<DateRangeSelectorProps> = {
130
128
  ...Primary,
129
+ args: {
130
+ ...Primary.args,
131
+ value: dateRangeOptionPresets.lastMonth.label,
132
+ },
131
133
  play: async ({ canvasElement, step }) => {
132
134
  const { canvas, filterChangedListenerMock, optionChangedListenerMock } = await prepare(canvasElement, step);
133
135
 
134
- await waitFor(() => {
135
- expect(selectField(canvas)).toHaveValue('Last month');
136
+ await waitFor(async () => {
137
+ await expect(selectField(canvas)).toHaveValue('Last month');
136
138
  });
137
139
 
138
- step('Change date to custom value', async () => {
140
+ await step('Change date to custom value', async () => {
139
141
  await userEvent.type(dateFromPicker(canvas), '{backspace>12}');
140
142
  await userEvent.type(dateFromPicker(canvas), '2000-01-01');
141
143
  await userEvent.click(dateToPicker(canvas));
142
144
 
143
- await waitFor(() => {
144
- expect(selectField(canvas)).toHaveValue('Custom');
145
+ await waitFor(async () => {
146
+ await expect(selectField(canvas)).toHaveValue('Custom');
145
147
  });
146
148
 
147
149
  await expect(filterChangedListenerMock).toHaveBeenCalledWith(
@@ -165,20 +167,82 @@ export const ChangingDateSetsOptionToCustom: StoryObj<DateRangeSelectorProps> =
165
167
  },
166
168
  };
167
169
 
168
- export const ChangingDateOption: StoryObj<DateRangeSelectorProps> = {
170
+ export const ChangingTheValueProgrammatically: StoryObj<DateRangeSelectorProps> = {
169
171
  ...Primary,
172
+ render: (args) => {
173
+ const StatefulWrapper = () => {
174
+ const [value, setValue] = useState('Last month');
175
+ const ref = useRef<HTMLDivElement>(null);
176
+
177
+ useEffect(() => {
178
+ ref.current?.addEventListener('gs-date-range-option-changed', (event) => {
179
+ setValue((event as CustomEvent).detail);
180
+ });
181
+ }, []);
182
+
183
+ return (
184
+ <div ref={ref}>
185
+ <LapisUrlContextProvider value={LAPIS_URL}>
186
+ <DateRangeSelector {...args} value={value} />
187
+ </LapisUrlContextProvider>
188
+ <button className='btn' onClick={() => setValue(customDateRange.label)}>
189
+ Set to Custom
190
+ </button>
191
+ <button className='btn' onClick={() => setValue(dateRangeOptionPresets.lastMonth.label)}>
192
+ Set to Last month
193
+ </button>
194
+ </div>
195
+ );
196
+ };
197
+
198
+ return <StatefulWrapper />;
199
+ },
170
200
  play: async ({ canvasElement, step }) => {
171
201
  const { canvas, filterChangedListenerMock, optionChangedListenerMock } = await prepare(canvasElement, step);
172
202
 
173
- await waitFor(() => {
174
- expect(selectField(canvas)).toHaveValue('Last month');
203
+ await waitFor(async () => {
204
+ await expect(selectField(canvas)).toHaveValue('Last month');
175
205
  });
176
206
 
177
- step('Change date to custom', async () => {
178
- await userEvent.selectOptions(selectField(canvas), 'CustomDateRange');
207
+ await step('Change the value of the component programmatically', async () => {
208
+ await userEvent.click(canvas.getByRole('button', { name: 'Set to Custom' }));
209
+ await waitFor(async () => {
210
+ await expect(selectField(canvas)).toHaveValue(customDateRange.label);
211
+ });
179
212
 
180
- await waitFor(() => {
181
- expect(selectField(canvas)).toHaveValue('CustomDateRange');
213
+ await userEvent.click(canvas.getByRole('button', { name: 'Set to Last month' }));
214
+ await waitFor(async () => {
215
+ await expect(selectField(canvas)).toHaveValue('Last month');
216
+ });
217
+
218
+ await expect(filterChangedListenerMock).toHaveBeenCalledTimes(0);
219
+ await expect(optionChangedListenerMock).toHaveBeenCalledTimes(0);
220
+ });
221
+
222
+ await step('Changing the value from within the component is still possible', async () => {
223
+ await userEvent.selectOptions(selectField(canvas), 'All times');
224
+ await waitFor(async () => {
225
+ await expect(selectField(canvas)).toHaveValue('All times');
226
+ });
227
+ await expect(filterChangedListenerMock).toHaveBeenCalledTimes(1);
228
+ await expect(optionChangedListenerMock).toHaveBeenCalledTimes(1);
229
+ });
230
+ },
231
+ };
232
+
233
+ export const ChangingDateOption: StoryObj<DateRangeSelectorProps> = {
234
+ ...Primary,
235
+ play: async ({ canvasElement, step }) => {
236
+ const { canvas, filterChangedListenerMock, optionChangedListenerMock } = await prepare(canvasElement, step);
237
+
238
+ await waitFor(async () => {
239
+ await expect(selectField(canvas)).toHaveValue('Custom');
240
+ });
241
+
242
+ await step('Change date to custom', async () => {
243
+ await waitFor(async () => {
244
+ await userEvent.selectOptions(selectField(canvas), 'CustomDateRange');
245
+ await expect(selectField(canvas)).toHaveValue('CustomDateRange');
182
246
  });
183
247
 
184
248
  await expect(filterChangedListenerMock).toHaveBeenCalledWith(
@@ -203,13 +267,13 @@ export const HandlesInvalidInitialDateFrom: StoryObj<DateRangeSelectorProps> = {
203
267
  ...Primary,
204
268
  args: {
205
269
  ...Primary.args,
206
- initialDateFrom: 'not a date',
270
+ value: { dateFrom: 'not a date' },
207
271
  },
208
272
  play: async ({ canvasElement }) => {
209
273
  const canvas = within(canvasElement);
210
274
 
211
- await waitFor(() => {
212
- expect(canvas.getByText('Oops! Something went wrong.')).toBeVisible();
275
+ await waitFor(async () => {
276
+ await expect(canvas.getByText('Oops! Something went wrong.')).toBeVisible();
213
277
  });
214
278
  },
215
279
  };
@@ -221,7 +285,7 @@ export const WithNoDateColumn: StoryObj<DateRangeSelectorProps> = {
221
285
  lapisDateField: '',
222
286
  },
223
287
  play: async ({ canvasElement, step }) => {
224
- step('expect error message', async () => {
288
+ await step('expect error message', async () => {
225
289
  await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
226
290
  });
227
291
  },
@@ -231,12 +295,9 @@ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRend
231
295
  const canvas = within(canvasElement);
232
296
 
233
297
  const filterChangedListenerMock = fn();
234
- await step('Setup event listener mock', async () => {
235
- canvasElement.addEventListener('gs-date-range-filter-changed', filterChangedListenerMock);
236
- });
237
-
238
298
  const optionChangedListenerMock = fn();
239
- await step('Setup event listener mock', async () => {
299
+ await step('Setup event listener mock', () => {
300
+ canvasElement.addEventListener('gs-date-range-filter-changed', filterChangedListenerMock);
240
301
  canvasElement.addEventListener('gs-date-range-option-changed', optionChangedListenerMock);
241
302
  });
242
303
 
@@ -1,11 +1,16 @@
1
1
  import flatpickr from 'flatpickr';
2
2
  import 'flatpickr/dist/flatpickr.min.css';
3
- import { useEffect, useRef, useState } from 'preact/hooks';
3
+ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
4
4
  import z from 'zod';
5
5
 
6
6
  import { computeInitialValues } from './computeInitialValues';
7
7
  import { toYYYYMMDD } from './dateConversion';
8
- import { DateRangeOptionChangedEvent, dateRangeOptionSchema, type DateRangeSelectOption } from './dateRangeOption';
8
+ import {
9
+ DateRangeOptionChangedEvent,
10
+ dateRangeOptionSchema,
11
+ type DateRangeSelectOption,
12
+ dateRangeValueSchema,
13
+ } from './dateRangeOption';
9
14
  import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
10
15
  import { ErrorBoundary } from '../components/error-boundary';
11
16
  import { Select } from '../components/select';
@@ -16,9 +21,7 @@ const customOption = 'Custom';
16
21
  const dateRangeSelectorInnerPropsSchema = z.object({
17
22
  dateRangeOptions: z.array(dateRangeOptionSchema),
18
23
  earliestDate: z.string().date(),
19
- initialValue: z.string().optional(),
20
- initialDateFrom: z.string().date().optional(),
21
- initialDateTo: z.string().date().optional(),
24
+ value: dateRangeValueSchema.optional(),
22
25
  lapisDateField: z.string().min(1),
23
26
  });
24
27
 
@@ -45,17 +48,12 @@ export const DateRangeSelector = (props: DateRangeSelectorProps) => {
45
48
  export const DateRangeSelectorInner = ({
46
49
  dateRangeOptions,
47
50
  earliestDate = '1900-01-01',
48
- initialValue,
51
+ value,
49
52
  lapisDateField,
50
- initialDateFrom,
51
- initialDateTo,
52
53
  }: DateRangeSelectorInnerProps) => {
53
- const initialValues = computeInitialValues(
54
- initialValue,
55
- initialDateFrom,
56
- initialDateTo,
57
- earliestDate,
58
- dateRangeOptions,
54
+ const initialValues = useMemo(
55
+ () => computeInitialValues(value, earliestDate, dateRangeOptions),
56
+ [value, earliestDate, dateRangeOptions],
59
57
  );
60
58
 
61
59
  const fromDatePickerRef = useRef<HTMLInputElement>(null);
@@ -74,6 +72,12 @@ export const DateRangeSelectorInner = ({
74
72
  });
75
73
 
76
74
  useEffect(() => {
75
+ setSelectedDateRange(initialValues.initialSelectedDateRange);
76
+ setSelectedDates({
77
+ dateFrom: initialValues.initialSelectedDateFrom,
78
+ dateTo: initialValues.initialSelectedDateTo,
79
+ });
80
+
77
81
  const commonConfig = {
78
82
  allowInput: true,
79
83
  dateFormat: 'Y-m-d',
@@ -83,7 +87,7 @@ export const DateRangeSelectorInner = ({
83
87
  setDateFromPicker(
84
88
  flatpickr(fromDatePickerRef.current, {
85
89
  ...commonConfig,
86
- defaultDate: selectedDates.dateFrom,
90
+ defaultDate: initialValues.initialSelectedDateFrom,
87
91
  }),
88
92
  );
89
93
  }
@@ -92,17 +96,22 @@ export const DateRangeSelectorInner = ({
92
96
  setDateToPicker(
93
97
  flatpickr(toDatePickerRef.current, {
94
98
  ...commonConfig,
95
- defaultDate: selectedDates.dateTo,
99
+ defaultDate: initialValues.initialSelectedDateTo,
96
100
  }),
97
101
  );
98
102
  }
99
103
 
100
104
  return () => {
101
- dateFromPicker?.destroy();
102
- dateToPicker?.destroy();
105
+ setDateFromPicker((prev) => {
106
+ prev?.destroy();
107
+ return null;
108
+ });
109
+ setDateToPicker((prev) => {
110
+ prev?.destroy();
111
+ return null;
112
+ });
103
113
  };
104
- // eslint-disable-next-line react-hooks/exhaustive-deps
105
- }, [fromDatePickerRef, toDatePickerRef]);
114
+ }, [fromDatePickerRef, toDatePickerRef, initialValues]);
106
115
 
107
116
  const onSelectChange = (value: string) => {
108
117
  setSelectedDateRange(value);
@@ -139,7 +148,7 @@ export const DateRangeSelectorInner = ({
139
148
  fireFilterChangedEvent();
140
149
  fireOptionChangedEvent({
141
150
  dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
142
- dateTo: toYYYYMMDD(dateTo || new Date())!,
151
+ dateTo: toYYYYMMDD(dateTo || new Date()),
143
152
  });
144
153
  };
145
154
 
@@ -158,7 +167,7 @@ export const DateRangeSelectorInner = ({
158
167
  fireFilterChangedEvent();
159
168
  fireOptionChangedEvent({
160
169
  dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
161
- dateTo: toYYYYMMDD(dateTo || new Date())!,
170
+ dateTo: toYYYYMMDD(dateTo || new Date()),
162
171
  });
163
172
  };
164
173
 
@@ -22,7 +22,17 @@ export const dateRangeOptionSchema = z.object({
22
22
 
23
23
  export type DateRangeOption = z.infer<typeof dateRangeOptionSchema>;
24
24
 
25
- export type DateRangeSelectOption = string | { dateFrom: string; dateTo: string };
25
+ export const dateRangeValueSchema = z.union([
26
+ z.string(),
27
+ z.object({
28
+ dateFrom: z.string().date().optional(),
29
+ dateTo: z.string().date().optional(),
30
+ }),
31
+ ]);
32
+
33
+ export type DateRangeValue = z.infer<typeof dateRangeValueSchema>;
34
+
35
+ export type DateRangeSelectOption = Required<DateRangeValue>;
26
36
 
27
37
  export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
28
38
  constructor(detail: DateRangeSelectOption) {