@genspectrum/dashboard-components 0.8.4 → 0.9.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.
Files changed (28) hide show
  1. package/README.md +14 -2
  2. package/custom-elements.json +30 -7
  3. package/dist/assets/{mutationOverTimeWorker-kjUXkRmn.js.map → mutationOverTimeWorker-DuWGESoO.js.map} +1 -1
  4. package/dist/{genspectrum-components.d.ts → components.d.ts} +25 -59
  5. package/dist/{dashboard-components.js → components.js} +43 -66
  6. package/dist/components.js.map +1 -0
  7. package/dist/util.d.ts +301 -0
  8. package/dist/util.js +6 -0
  9. package/dist/util.js.map +1 -0
  10. package/dist/utilEntrypoint-g4DsyhU7.js +61 -0
  11. package/dist/utilEntrypoint-g4DsyhU7.js.map +1 -0
  12. package/package.json +10 -5
  13. package/src/{index.ts → componentsEntrypoint.ts} +0 -1
  14. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +101 -44
  15. package/src/preact/dateRangeSelector/date-range-selector.tsx +33 -14
  16. package/src/preact/dateRangeSelector/dateConversion.ts +1 -5
  17. package/src/preact/dateRangeSelector/dateRangeOption.ts +18 -0
  18. package/src/preact/webWorkers/useWebWorker.ts +32 -10
  19. package/src/preact/webWorkers/workerFunction.ts +19 -3
  20. package/src/standaloneEntrypoint.ts +2 -0
  21. package/src/utilEntrypoint.ts +6 -0
  22. package/src/web-components/input/gs-date-range-selector.stories.ts +41 -10
  23. package/src/web-components/input/gs-date-range-selector.tsx +16 -2
  24. package/standalone-bundle/assets/mutationOverTimeWorker-MVSt1FVw.js.map +1 -0
  25. package/standalone-bundle/dashboard-components.js +12986 -16649
  26. package/standalone-bundle/dashboard-components.js.map +1 -1
  27. package/standalone-bundle/style.css +1 -0
  28. package/dist/dashboard-components.js.map +0 -1
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
4
4
 
5
5
  import { computeInitialValues } from './computeInitialValues';
6
6
  import { toYYYYMMDD } from './dateConversion';
7
- import { type DateRangeOption } from './dateRangeOption';
7
+ import { type DateRangeOption, DateRangeOptionChangedEvent, type DateRangeSelectOption } from './dateRangeOption';
8
8
  import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
9
9
  import { ErrorBoundary } from '../components/error-boundary';
10
10
  import { Select } from '../components/select';
@@ -115,7 +115,8 @@ export const DateRangeSelectorInner = ({
115
115
  dateTo: dateRange.dateTo,
116
116
  });
117
117
 
118
- submit();
118
+ fireFilterChangedEvent();
119
+ fireOptionChangedEvent(value);
119
120
  };
120
121
 
121
122
  const onChangeDateFrom = () => {
@@ -123,11 +124,18 @@ export const DateRangeSelectorInner = ({
123
124
  return;
124
125
  }
125
126
 
126
- selectedDates.dateFrom = dateFromPicker?.selectedDates[0] || new Date();
127
- dateToPicker?.set('minDate', dateFromPicker?.selectedDates[0]);
127
+ const dateTo = dateToPicker?.selectedDates[0];
128
+ const dateFrom = dateFromPicker?.selectedDates[0];
129
+
130
+ selectedDates.dateFrom = dateFrom || new Date();
131
+ dateToPicker?.set('minDate', dateFrom);
128
132
  setSelectedDateRange(customOption);
129
133
 
130
- submit();
134
+ fireFilterChangedEvent();
135
+ fireOptionChangedEvent({
136
+ dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
137
+ dateTo: toYYYYMMDD(dateTo || new Date())!,
138
+ });
131
139
  };
132
140
 
133
141
  const onChangeDateTo = () => {
@@ -135,24 +143,35 @@ export const DateRangeSelectorInner = ({
135
143
  return;
136
144
  }
137
145
 
138
- selectedDates.dateTo = dateToPicker?.selectedDates[0] || new Date();
139
- dateFromPicker?.set('maxDate', dateToPicker?.selectedDates[0]);
146
+ const dateTo = dateToPicker?.selectedDates[0];
147
+ const dateFrom = dateFromPicker?.selectedDates[0];
148
+
149
+ selectedDates.dateTo = dateTo || new Date();
150
+ dateFromPicker?.set('maxDate', dateTo);
140
151
  setSelectedDateRange(customOption);
141
152
 
142
- submit();
153
+ fireFilterChangedEvent();
154
+ fireOptionChangedEvent({
155
+ dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
156
+ dateTo: toYYYYMMDD(dateTo || new Date())!,
157
+ });
158
+ };
159
+
160
+ const fireOptionChangedEvent = (option: DateRangeSelectOption) => {
161
+ divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(option));
143
162
  };
144
163
 
145
- const submit = () => {
146
- const dateFrom = toYYYYMMDD(dateFromPicker?.selectedDates[0]);
147
- const dateTo = toYYYYMMDD(dateToPicker?.selectedDates[0]);
164
+ const fireFilterChangedEvent = () => {
165
+ const dateFrom = dateFromPicker?.selectedDates[0];
166
+ const dateTo = dateToPicker?.selectedDates[0];
148
167
 
149
168
  const detail = {
150
- ...(dateFrom !== undefined && { [`${dateColumn}From`]: dateFrom }),
151
- ...(dateTo !== undefined && { [`${dateColumn}To`]: dateTo }),
169
+ ...(dateFrom !== undefined && { [`${dateColumn}From`]: toYYYYMMDD(dateFrom) }),
170
+ ...(dateTo !== undefined && { [`${dateColumn}To`]: toYYYYMMDD(dateTo) }),
152
171
  };
153
172
 
154
173
  divRef.current?.dispatchEvent(
155
- new CustomEvent('gs-date-range-changed', {
174
+ new CustomEvent('gs-date-range-filter-changed', {
156
175
  detail,
157
176
  bubbles: true,
158
177
  composed: true,
@@ -1,8 +1,4 @@
1
- export const toYYYYMMDD = (date?: Date) => {
2
- if (!date) {
3
- return undefined;
4
- }
5
-
1
+ export const toYYYYMMDD = (date: Date) => {
6
2
  const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
7
3
  return date.toLocaleDateString('en-CA', options);
8
4
  };
@@ -18,6 +18,17 @@ export type DateRangeOption = {
18
18
  dateTo?: string;
19
19
  };
20
20
 
21
+ export type DateRangeSelectOption = string | { dateFrom: string; dateTo: string };
22
+ export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
23
+ constructor(detail: DateRangeSelectOption) {
24
+ super('gs-date-range-option-changed', {
25
+ detail,
26
+ bubbles: true,
27
+ composed: true,
28
+ });
29
+ }
30
+ }
31
+
21
32
  const today = new Date();
22
33
 
23
34
  const twoWeeksAgo = new Date();
@@ -35,6 +46,9 @@ last3Months.setMonth(today.getMonth() - 3);
35
46
  const last6Months = new Date(today);
36
47
  last6Months.setMonth(today.getMonth() - 6);
37
48
 
49
+ const lastYear = new Date(today);
50
+ lastYear.setFullYear(today.getFullYear() - 1);
51
+
38
52
  /**
39
53
  * Presets for the `gs-date-range-selector` component that can be used as `dateRangeOptions`.
40
54
  */
@@ -59,6 +73,10 @@ export const dateRangeOptionPresets = {
59
73
  label: 'Last 6 months',
60
74
  dateFrom: toYYYYMMDD(last6Months),
61
75
  },
76
+ lastYear: {
77
+ label: 'Last year',
78
+ dateFrom: toYYYYMMDD(lastYear),
79
+ },
62
80
  allTimes: {
63
81
  label: 'All times',
64
82
  },
@@ -1,31 +1,53 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
2
 
3
+ import { UserFacingError } from '../components/error-display';
4
+
5
+ export type LoadingWorkerStatus = {
6
+ status: 'loading';
7
+ };
8
+ export type SuccessWorkerStatus<Response> = {
9
+ status: 'success';
10
+ data: Response;
11
+ };
12
+ export type ErrorWorkerStatus =
13
+ | {
14
+ status: 'error';
15
+ userFacing: false;
16
+ error: Error;
17
+ }
18
+ | {
19
+ status: 'error';
20
+ userFacing: true;
21
+ headline: string;
22
+ error: Error;
23
+ };
24
+ export type WorkerStatus<Response> = LoadingWorkerStatus | SuccessWorkerStatus<Response> | ErrorWorkerStatus;
25
+
3
26
  export function useWebWorker<Request, Response>(messageToWorker: Request, worker: Worker) {
4
27
  const [data, setData] = useState<Response | undefined>(undefined);
5
28
  const [error, setError] = useState<Error | undefined>(undefined);
6
29
  const [isLoading, setIsLoading] = useState(true);
7
30
 
8
31
  useEffect(() => {
9
- worker.onmessage = (
10
- event: MessageEvent<{
11
- status: 'loading' | 'success' | 'error';
12
- data?: Response;
13
- error?: Error;
14
- }>,
15
- ) => {
16
- const { status, data, error } = event.data;
32
+ worker.onmessage = (event: MessageEvent<WorkerStatus<Response>>) => {
33
+ const eventData = event.data;
34
+ const status = eventData.status;
17
35
 
18
36
  switch (status) {
19
37
  case 'loading':
20
38
  setIsLoading(true);
21
39
  break;
22
40
  case 'success':
23
- setData(data);
41
+ setData(eventData.data);
24
42
  setError(undefined);
25
43
  setIsLoading(false);
26
44
  break;
27
45
  case 'error':
28
- setError(error);
46
+ setError(
47
+ eventData.userFacing
48
+ ? new UserFacingError(eventData.headline, eventData.error.message)
49
+ : eventData.error,
50
+ );
29
51
  setIsLoading(false);
30
52
  break;
31
53
  default:
@@ -1,14 +1,30 @@
1
+ import { type ErrorWorkerStatus, type LoadingWorkerStatus, type SuccessWorkerStatus } from './useWebWorker';
2
+ import { UserFacingError } from '../components/error-display';
3
+
1
4
  export async function workerFunction<R>(queryFunction: () => R) {
2
5
  try {
3
- postMessage({ status: 'loading' });
6
+ postMessage({ status: 'loading' } satisfies LoadingWorkerStatus);
4
7
 
5
8
  const workerResponse = await queryFunction();
6
9
 
7
10
  postMessage({
8
11
  status: 'success',
9
12
  data: workerResponse,
10
- });
13
+ } satisfies SuccessWorkerStatus<R>);
11
14
  } catch (error) {
12
- postMessage({ status: 'error', error });
15
+ postMessage(
16
+ (error instanceof UserFacingError
17
+ ? {
18
+ status: 'error',
19
+ userFacing: true,
20
+ headline: error.headline,
21
+ error,
22
+ }
23
+ : {
24
+ status: 'error',
25
+ userFacing: false,
26
+ error: error instanceof Error ? error : new Error(`${error}`),
27
+ }) satisfies ErrorWorkerStatus,
28
+ );
13
29
  }
14
30
  }
@@ -0,0 +1,2 @@
1
+ export * from './utilEntrypoint';
2
+ export * from './componentsEntrypoint';
@@ -0,0 +1,6 @@
1
+ export {
2
+ type DateRangeOption,
3
+ type DateRangeSelectOption,
4
+ dateRangeOptionPresets,
5
+ DateRangeOptionChangedEvent,
6
+ } from './preact/dateRangeSelector/dateRangeOption';
@@ -1,4 +1,4 @@
1
- import { expect, waitFor } from '@storybook/test';
1
+ import { expect, fn, userEvent, waitFor, type within } from '@storybook/test';
2
2
  import type { Meta, StoryObj } from '@storybook/web-components';
3
3
  import { html } from 'lit';
4
4
 
@@ -23,12 +23,14 @@ const codeExample = String.raw`
23
23
  dateColumn="myDateColumn"
24
24
  ></gs-date-range-selector>`;
25
25
 
26
+ const customDateRange = { label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' };
27
+
26
28
  const meta: Meta<Required<DateRangeSelectorProps>> = {
27
29
  title: 'Input/DateRangeSelector',
28
30
  component: 'gs-date-range-selector',
29
31
  parameters: withComponentDocs({
30
32
  actions: {
31
- handles: ['gs-date-range-changed', ...previewHandles],
33
+ handles: ['gs-date-range-filter-changed', 'gs-date-range-option-changed', ...previewHandles],
32
34
  },
33
35
  fetchMock: {},
34
36
  componentDocs: {
@@ -66,7 +68,7 @@ const meta: Meta<Required<DateRangeSelectorProps>> = {
66
68
  dateRangeOptionPresets.lastMonth,
67
69
  dateRangeOptionPresets.last3Months,
68
70
  dateRangeOptionPresets.allTimes,
69
- { label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' },
71
+ customDateRange,
70
72
  ],
71
73
  earliestDate: '1970-01-01',
72
74
  initialValue: dateRangeOptionPresets.lastMonth.label,
@@ -80,7 +82,7 @@ const meta: Meta<Required<DateRangeSelectorProps>> = {
80
82
 
81
83
  export default meta;
82
84
 
83
- export const DateRangeSelectorStory: StoryObj<Required<DateRangeSelectorProps>> = {
85
+ export const Default: StoryObj<Required<DateRangeSelectorProps>> = {
84
86
  render: (args) =>
85
87
  html` <gs-app lapis="${LAPIS_URL}">
86
88
  <div class="max-w-screen-lg">
@@ -95,19 +97,48 @@ export const DateRangeSelectorStory: StoryObj<Required<DateRangeSelectorProps>>
95
97
  ></gs-date-range-selector>
96
98
  </div>
97
99
  </gs-app>`,
100
+ };
101
+
102
+ export const FiresEvents: StoryObj<Required<DateRangeSelectorProps>> = {
103
+ ...Default,
98
104
  play: async ({ canvasElement, step }) => {
99
105
  const canvas = await withinShadowRoot(canvasElement, 'gs-date-range-selector');
100
- const dateTo = () => canvas.getByPlaceholderText('Date to');
106
+
107
+ const filterChangedListenerMock = fn();
108
+ const optionChangedListenerMock = fn();
109
+ await step('Setup event listener mock', async () => {
110
+ canvasElement.addEventListener('gs-date-range-filter-changed', filterChangedListenerMock);
111
+ canvasElement.addEventListener('gs-date-range-option-changed', optionChangedListenerMock);
112
+ });
101
113
 
102
114
  await step('Expect last 6 months to be selected', async () => {
103
- await expect(canvas.getByRole('combobox')).toHaveValue('Last month');
115
+ await expect(selectField(canvas)).toHaveValue('Last month');
104
116
  await waitFor(() => {
105
- expect(dateTo()).toHaveValue(toYYYYMMDD(new Date()));
117
+ expect(dateToPicker(canvas)).toHaveValue(toYYYYMMDD(new Date()));
106
118
  });
107
119
  });
108
120
 
109
- // Due to the limitations of storybook testing which does not fire an event,
110
- // when selecting a value from the dropdown we can't test the fired event here.
111
- // An e2e test (using playwright) for that can be found in tests/dateRangeSelector.spec.ts
121
+ await step('Expect event to be fired when selecting a different value', async () => {
122
+ await userEvent.selectOptions(selectField(canvas), 'CustomDateRange');
123
+ await expect(dateToPicker(canvas)).toHaveValue(customDateRange.dateTo);
124
+
125
+ await expect(filterChangedListenerMock).toHaveBeenCalledWith(
126
+ expect.objectContaining({
127
+ detail: {
128
+ aDateColumnFrom: customDateRange.dateFrom,
129
+ aDateColumnTo: customDateRange.dateTo,
130
+ },
131
+ }),
132
+ );
133
+
134
+ await expect(optionChangedListenerMock).toHaveBeenCalledWith(
135
+ expect.objectContaining({
136
+ detail: customDateRange.label,
137
+ }),
138
+ );
139
+ });
112
140
  },
113
141
  };
142
+
143
+ const dateToPicker = (canvas: ReturnType<typeof within>) => canvas.getByPlaceholderText('Date to');
144
+ const selectField = (canvas: ReturnType<typeof within>) => canvas.getByRole('combobox');
@@ -2,6 +2,7 @@ import { customElement, property } from 'lit/decorators.js';
2
2
  import { type DetailedHTMLProps, type HTMLAttributes } from 'react';
3
3
 
4
4
  import { DateRangeSelector, type DateRangeSelectorProps } from '../../preact/dateRangeSelector/date-range-selector';
5
+ import { type DateRangeOptionChangedEvent } from '../../preact/dateRangeSelector/dateRangeOption';
5
6
  import { type Equals, type Expect } from '../../utils/typeAssertions';
6
7
  import { PreactLitAdapter } from '../PreactLitAdapter';
7
8
 
@@ -18,7 +19,7 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
18
19
  * Setting a value in either of the date pickers will set the select field to "custom",
19
20
  * which represents an arbitrary date range.
20
21
  *
21
- * @fires {CustomEvent<{ `${dateColumn}From`: string; `${dateColumn}To`: string; }>} gs-date-range-changed
22
+ * @fires {CustomEvent<{ `${dateColumn}From`: string; `${dateColumn}To`: string; }>} gs-date-range-filter-changed
22
23
  * Fired when:
23
24
  * - The select field is changed,
24
25
  * - A date is selected in either of the date pickers,
@@ -33,6 +34,18 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
33
34
  * }
34
35
  * ```
35
36
  * will be fired.
37
+ *
38
+ * Use this event, when you want to use the filter directly as a LAPIS filter.
39
+ *
40
+ *
41
+ * @fires {CustomEvent<{ string | {dateFrom: string, dateTo: string}}>} gs-date-range-option-changed
42
+ * Fired when:
43
+ * - The select field is changed,
44
+ * - A date is selected in either of the date pickers,
45
+ * - A date was typed into either of the date input fields, and the input field loses focus ("on blur").
46
+ * Contains the selected dateRangeOption or when users select custom values it contains the selected dates.
47
+ *
48
+ * Use this event, when you want to control this component in your JS application.
36
49
  */
37
50
  @customElement('gs-date-range-selector')
38
51
  export class DateRangeSelectorComponent extends PreactLitAdapter {
@@ -116,7 +129,8 @@ declare global {
116
129
  }
117
130
 
118
131
  interface HTMLElementEventMap {
119
- 'gs-date-range-changed': CustomEvent<Record<string, string>>;
132
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
133
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
120
134
  }
121
135
  }
122
136