@genspectrum/dashboard-components 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +45 -26
- package/dist/dashboard-components.js +518 -434
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +21 -12
- package/dist/style.css +150 -229
- package/package.json +3 -1
- package/src/preact/aggregatedData/aggregate.tsx +1 -1
- package/src/preact/components/SegmentSelector.tsx +0 -1
- package/src/preact/components/checkbox-selector.tsx +7 -9
- package/src/preact/components/dropdown.tsx +40 -0
- package/src/preact/components/info.stories.tsx +8 -8
- package/src/preact/components/info.tsx +38 -19
- package/src/preact/components/mutation-type-selector.tsx +0 -1
- package/src/preact/components/proportion-selector-dropdown.tsx +9 -18
- package/src/preact/components/tabs.tsx +12 -3
- package/src/preact/dateRangeSelector/computeInitialValues.spec.ts +99 -0
- package/src/preact/dateRangeSelector/computeInitialValues.ts +73 -0
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +93 -4
- package/src/preact/dateRangeSelector/date-range-selector.tsx +49 -106
- package/src/preact/dateRangeSelector/selectableOptions.ts +79 -0
- package/src/preact/locationFilter/location-filter.tsx +1 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +3 -3
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -6
- package/src/preact/mutationFilter/mutation-filter.tsx +48 -54
- package/src/preact/mutations/mutations.tsx +3 -4
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +3 -5
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +3 -3
- package/src/preact/shared/floating-ui/hooks.ts +83 -0
- package/src/web-components/input/gs-date-range-selector.stories.ts +11 -5
- package/src/web-components/input/gs-date-range-selector.tsx +22 -5
- package/src/web-components/input/gs-location-filter.stories.ts +6 -7
- package/src/web-components/input/gs-location-filter.tsx +3 -2
- package/src/web-components/input/gs-mutation-filter.stories.ts +1 -8
- package/src/web-components/input/gs-mutation-filter.tsx +1 -9
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +1 -1
|
@@ -1,31 +1,29 @@
|
|
|
1
|
+
import { Dropdown } from './dropdown';
|
|
2
|
+
|
|
1
3
|
export type CheckboxItem = {
|
|
2
4
|
label: string;
|
|
3
5
|
checked: boolean;
|
|
4
6
|
};
|
|
5
7
|
|
|
6
8
|
export interface CheckboxSelectorProps<Item extends CheckboxItem = CheckboxItem> {
|
|
7
|
-
className?: string;
|
|
8
9
|
items: Item[];
|
|
9
10
|
label: string;
|
|
10
11
|
setItems: (items: Item[]) => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export const CheckboxSelector = <Item extends CheckboxItem>({
|
|
14
|
-
className,
|
|
15
15
|
items,
|
|
16
16
|
label,
|
|
17
17
|
setItems,
|
|
18
18
|
}: CheckboxSelectorProps<Item>) => {
|
|
19
19
|
return (
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
{label}
|
|
23
|
-
</div>
|
|
24
|
-
<ul tabIndex={0} class='p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box'>
|
|
20
|
+
<Dropdown buttonTitle={label} placement={'bottom-start'}>
|
|
21
|
+
<ul>
|
|
25
22
|
{items.map((item, index) => (
|
|
26
|
-
<li
|
|
23
|
+
<li className='flex flex-row items-center' key={item.label}>
|
|
27
24
|
<label>
|
|
28
25
|
<input
|
|
26
|
+
className={'mr-2'}
|
|
29
27
|
type='checkbox'
|
|
30
28
|
id={`item-${index}`}
|
|
31
29
|
checked={item.checked}
|
|
@@ -41,6 +39,6 @@ export const CheckboxSelector = <Item extends CheckboxItem>({
|
|
|
41
39
|
</li>
|
|
42
40
|
))}
|
|
43
41
|
</ul>
|
|
44
|
-
</
|
|
42
|
+
</Dropdown>
|
|
45
43
|
);
|
|
46
44
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { flip, offset, shift } from '@floating-ui/dom';
|
|
2
|
+
import { type Placement } from '@floating-ui/utils';
|
|
3
|
+
import { type FunctionComponent } from 'preact';
|
|
4
|
+
import { useRef, useState } from 'preact/hooks';
|
|
5
|
+
|
|
6
|
+
import { useCloseOnClickOutside, useCloseOnEsc, useFloatingUi } from '../shared/floating-ui/hooks';
|
|
7
|
+
|
|
8
|
+
interface DropdownProps {
|
|
9
|
+
buttonTitle: string;
|
|
10
|
+
placement?: Placement;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const dropdownClass =
|
|
14
|
+
'z-10 absolute w-max top-0 left-0 bg-white p-4 border border-gray-200 shadow-lg rounded-md';
|
|
15
|
+
|
|
16
|
+
export const Dropdown: FunctionComponent<DropdownProps> = ({ children, buttonTitle, placement }) => {
|
|
17
|
+
const [showContent, setShowContent] = useState(false);
|
|
18
|
+
const referenceRef = useRef<HTMLButtonElement>(null);
|
|
19
|
+
const floatingRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
useFloatingUi(referenceRef, floatingRef, [offset(4), shift(), flip()], placement);
|
|
22
|
+
|
|
23
|
+
useCloseOnClickOutside(floatingRef, referenceRef, setShowContent);
|
|
24
|
+
useCloseOnEsc(setShowContent);
|
|
25
|
+
|
|
26
|
+
const toggle = () => {
|
|
27
|
+
setShowContent(!showContent);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
<button type='button' className='btn btn-xs whitespace-nowrap' onClick={toggle} ref={referenceRef}>
|
|
33
|
+
{buttonTitle}
|
|
34
|
+
</button>
|
|
35
|
+
<div ref={floatingRef} className={`${dropdownClass} ${showContent ? '' : 'hidden'}`}>
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect,
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import Info, { type InfoProps } from './info';
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ const meta: Meta<InfoProps> = {
|
|
|
8
8
|
component: Info,
|
|
9
9
|
parameters: { fetchMock: {} },
|
|
10
10
|
args: {
|
|
11
|
-
|
|
11
|
+
height: '100px',
|
|
12
12
|
},
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -28,16 +28,16 @@ export const ShowsInfoOnClick: StoryObj<InfoProps> = {
|
|
|
28
28
|
...InfoStory,
|
|
29
29
|
play: async ({ canvasElement }) => {
|
|
30
30
|
const canvas = within(canvasElement);
|
|
31
|
-
const
|
|
31
|
+
const openInfo = canvas.getByRole('button', { name: '?' });
|
|
32
32
|
|
|
33
|
-
await waitFor(() => expect(
|
|
33
|
+
await waitFor(() => expect(openInfo).toBeInTheDocument());
|
|
34
34
|
|
|
35
|
-
await
|
|
35
|
+
await userEvent.click(openInfo);
|
|
36
36
|
|
|
37
|
-
await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).
|
|
37
|
+
await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeVisible());
|
|
38
38
|
|
|
39
|
-
await
|
|
39
|
+
await userEvent.click(document.body);
|
|
40
40
|
|
|
41
|
-
await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.
|
|
41
|
+
await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeVisible());
|
|
42
42
|
},
|
|
43
43
|
};
|
|
@@ -1,38 +1,57 @@
|
|
|
1
|
+
import { offset, shift, size } from '@floating-ui/dom';
|
|
1
2
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { useState } from 'preact/hooks';
|
|
3
|
+
import { useRef, useState } from 'preact/hooks';
|
|
4
|
+
|
|
5
|
+
import { dropdownClass } from './dropdown';
|
|
6
|
+
import { useCloseOnClickOutside, useCloseOnEsc, useFloatingUi } from '../shared/floating-ui/hooks';
|
|
3
7
|
|
|
4
8
|
export interface InfoProps {
|
|
5
|
-
|
|
6
|
-
height?: string;
|
|
7
|
-
width?: string;
|
|
8
|
-
};
|
|
9
|
+
height?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const Info: FunctionComponent<InfoProps> = ({ children,
|
|
12
|
+
const Info: FunctionComponent<InfoProps> = ({ children, height }) => {
|
|
12
13
|
const [showHelp, setShowHelp] = useState(false);
|
|
14
|
+
const referenceRef = useRef<HTMLButtonElement>(null);
|
|
15
|
+
const floatingRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
|
|
17
|
+
useFloatingUi(referenceRef, floatingRef, [
|
|
18
|
+
offset(10),
|
|
19
|
+
shift(),
|
|
20
|
+
size({
|
|
21
|
+
apply() {
|
|
22
|
+
if (!floatingRef.current) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
floatingRef.current.style.width = '100vw';
|
|
26
|
+
floatingRef.current.style.height = height ? height : '50vh';
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
13
30
|
|
|
14
31
|
const toggleHelp = () => {
|
|
15
32
|
setShowHelp(!showHelp);
|
|
16
33
|
};
|
|
17
34
|
|
|
35
|
+
useCloseOnEsc(setShowHelp);
|
|
36
|
+
useCloseOnClickOutside(floatingRef, referenceRef, setShowHelp);
|
|
37
|
+
|
|
18
38
|
return (
|
|
19
39
|
<div className='relative'>
|
|
20
|
-
<button className='btn btn-xs' onClick={toggleHelp}>
|
|
40
|
+
<button type='button' className='btn btn-xs' onClick={toggleHelp} ref={referenceRef}>
|
|
21
41
|
?
|
|
22
42
|
</button>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
<div
|
|
44
|
+
ref={floatingRef}
|
|
45
|
+
className={`${dropdownClass} overflow-y-auto opacity-90 ${showHelp ? '' : 'hidden'}`}
|
|
46
|
+
>
|
|
47
|
+
<div className={'flex flex-col'}>{children}</div>
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => setShowHelp(false)}
|
|
50
|
+
className={'float-right underline text-sm hover:text-blue-700 mr-2'}
|
|
27
51
|
>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Close
|
|
32
|
-
</button>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
)}
|
|
52
|
+
Close
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
36
55
|
</div>
|
|
37
56
|
);
|
|
38
57
|
};
|
|
@@ -21,7 +21,6 @@ export const MutationTypeSelector: FunctionComponent<MutationTypeSelectorProps>
|
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
23
|
<CheckboxSelector
|
|
24
|
-
className='mx-1'
|
|
25
24
|
items={displayedMutationTypes}
|
|
26
25
|
label={mutationTypesSelectorLabel}
|
|
27
26
|
setItems={(items) => setDisplayedMutationTypes(items)}
|
|
@@ -1,33 +1,24 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
|
+
import { Dropdown } from './dropdown';
|
|
3
4
|
import { ProportionSelector, type ProportionSelectorProps } from './proportion-selector';
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
openDirection?: 'left' | 'right';
|
|
7
|
-
}
|
|
6
|
+
export type ProportionSelectorDropdownProps = ProportionSelectorProps;
|
|
8
7
|
|
|
9
8
|
export const ProportionSelectorDropdown: FunctionComponent<ProportionSelectorDropdownProps> = ({
|
|
10
9
|
proportionInterval,
|
|
11
10
|
setMinProportion,
|
|
12
11
|
setMaxProportion,
|
|
13
|
-
openDirection = 'right',
|
|
14
12
|
}) => {
|
|
15
13
|
const label = `${(proportionInterval.min * 100).toFixed(1)}% - ${(proportionInterval.max * 100).toFixed(1)}%`;
|
|
16
14
|
|
|
17
15
|
return (
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
proportionInterval={proportionInterval}
|
|
26
|
-
setMinProportion={setMinProportion}
|
|
27
|
-
setMaxProportion={setMaxProportion}
|
|
28
|
-
/>
|
|
29
|
-
</div>
|
|
30
|
-
</ul>
|
|
31
|
-
</div>
|
|
16
|
+
<Dropdown buttonTitle={`Proportion ${label}`} placement={'bottom-start'}>
|
|
17
|
+
<ProportionSelector
|
|
18
|
+
proportionInterval={proportionInterval}
|
|
19
|
+
setMinProportion={setMinProportion}
|
|
20
|
+
setMaxProportion={setMaxProportion}
|
|
21
|
+
/>
|
|
22
|
+
</Dropdown>
|
|
32
23
|
);
|
|
33
24
|
};
|
|
@@ -17,11 +17,20 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
|
17
17
|
const [heightOfTabs, setHeightOfTabs] = useState('3rem');
|
|
18
18
|
const tabRef = useRef<HTMLDivElement>(null);
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const updateHeightOfTabs = () => {
|
|
21
21
|
if (tabRef.current) {
|
|
22
22
|
const heightOfTabs = tabRef.current.getBoundingClientRect().height;
|
|
23
23
|
setHeightOfTabs(`${heightOfTabs}px`);
|
|
24
24
|
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
updateHeightOfTabs();
|
|
29
|
+
|
|
30
|
+
window.addEventListener('resize', updateHeightOfTabs);
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener('resize', updateHeightOfTabs);
|
|
33
|
+
};
|
|
25
34
|
}, []);
|
|
26
35
|
|
|
27
36
|
const tabElements = (
|
|
@@ -51,9 +60,9 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
|
51
60
|
|
|
52
61
|
return (
|
|
53
62
|
<div className='h-full w-full'>
|
|
54
|
-
<div ref={tabRef} className='flex flex-row justify-between'>
|
|
63
|
+
<div ref={tabRef} className='flex flex-row justify-between flex-wrap'>
|
|
55
64
|
{tabElements}
|
|
56
|
-
{toolbar && <div className='py-2'>{toolbarElement}</div>}
|
|
65
|
+
{toolbar && <div className='py-2 flex flex-wrap gap-y-1'>{toolbarElement}</div>}
|
|
57
66
|
</div>
|
|
58
67
|
<div
|
|
59
68
|
className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0].title ? '' : 'rounded-tl-md'}`}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { computeInitialValues } from './computeInitialValues';
|
|
4
|
+
import { PRESET_VALUE_CUSTOM, PRESET_VALUE_LAST_3_MONTHS, PRESET_VALUE_LAST_6_MONTHS } from './selectableOptions';
|
|
5
|
+
|
|
6
|
+
const today = new Date();
|
|
7
|
+
const earliestDate = '1900-01-01';
|
|
8
|
+
|
|
9
|
+
describe('computeInitialValues', () => {
|
|
10
|
+
it('should compute for initial value if initial "from" and "to" are unset', () => {
|
|
11
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, undefined, earliestDate, []);
|
|
12
|
+
|
|
13
|
+
const expectedFrom = new Date();
|
|
14
|
+
expectedFrom.setMonth(today.getMonth() - 3);
|
|
15
|
+
|
|
16
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_3_MONTHS);
|
|
17
|
+
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
|
|
18
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should fall back to default when initial value is unknown', () => {
|
|
22
|
+
const result = computeInitialValues('not a known value', undefined, undefined, earliestDate, []);
|
|
23
|
+
|
|
24
|
+
const expectedFrom = new Date();
|
|
25
|
+
expectedFrom.setMonth(today.getMonth() - 6);
|
|
26
|
+
|
|
27
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_6_MONTHS);
|
|
28
|
+
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
|
|
29
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should overwrite initial value if initial "from" is set', () => {
|
|
33
|
+
const initialDateFrom = '2020-01-01';
|
|
34
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, initialDateFrom, undefined, earliestDate, []);
|
|
35
|
+
|
|
36
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
37
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
38
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should overwrite initial value if initial "to" is set', () => {
|
|
42
|
+
const initialDateTo = '2020-01-01';
|
|
43
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, initialDateTo, earliestDate, []);
|
|
44
|
+
|
|
45
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
46
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
|
|
47
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should overwrite initial value if initial "to" and "from" are set', () => {
|
|
51
|
+
const initialDateFrom = '2020-01-01';
|
|
52
|
+
const initialDateTo = '2022-01-01';
|
|
53
|
+
const result = computeInitialValues(
|
|
54
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
55
|
+
initialDateFrom,
|
|
56
|
+
initialDateTo,
|
|
57
|
+
earliestDate,
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
62
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
63
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should set initial "to" to "from" if "from" is after "to"', () => {
|
|
67
|
+
const initialDateFrom = '2020-01-01';
|
|
68
|
+
const initialDateTo = '1900-01-01';
|
|
69
|
+
const result = computeInitialValues(
|
|
70
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
71
|
+
initialDateFrom,
|
|
72
|
+
initialDateTo,
|
|
73
|
+
earliestDate,
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
78
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
79
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateFrom));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw if initial "from" is not a valid date', () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, 'not a date', undefined, earliestDate, []),
|
|
85
|
+
).toThrowError('Invalid initialDateFrom');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should throw if initial "to" is not a valid date', () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, 'not a date', earliestDate, []),
|
|
91
|
+
).toThrowError('Invalid initialDateTo');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function expectDateMatches(actual: Date, expected: Date) {
|
|
95
|
+
expect(actual.getFullYear()).toEqual(expected.getFullYear());
|
|
96
|
+
expect(actual.getMonth()).toEqual(expected.getMonth());
|
|
97
|
+
expect(actual.getDate()).toEqual(expected.getDate());
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CustomSelectOption,
|
|
3
|
+
getDatesForSelectorValue,
|
|
4
|
+
getSelectableOptions,
|
|
5
|
+
PRESET_VALUE_CUSTOM,
|
|
6
|
+
PRESET_VALUE_LAST_6_MONTHS,
|
|
7
|
+
type PresetOptionValues,
|
|
8
|
+
} from './selectableOptions';
|
|
9
|
+
import { UserFacingError } from '../components/error-display';
|
|
10
|
+
|
|
11
|
+
export function computeInitialValues<CustomLabel extends string>(
|
|
12
|
+
initialValue: PresetOptionValues | CustomLabel | undefined,
|
|
13
|
+
initialDateFrom: string | undefined,
|
|
14
|
+
initialDateTo: string | undefined,
|
|
15
|
+
earliestDate: string,
|
|
16
|
+
customSelectOptions: CustomSelectOption<CustomLabel>[],
|
|
17
|
+
): {
|
|
18
|
+
initialSelectedDateRange: CustomLabel | PresetOptionValues;
|
|
19
|
+
initialSelectedDateFrom: Date;
|
|
20
|
+
initialSelectedDateTo: Date;
|
|
21
|
+
} {
|
|
22
|
+
if (isUndefinedOrEmpty(initialDateFrom) && isUndefinedOrEmpty(initialDateTo)) {
|
|
23
|
+
const selectableOptions = getSelectableOptions(customSelectOptions);
|
|
24
|
+
const initialSelectedDateRange =
|
|
25
|
+
initialValue !== undefined && selectableOptions.some((option) => option.value === initialValue)
|
|
26
|
+
? initialValue
|
|
27
|
+
: PRESET_VALUE_LAST_6_MONTHS;
|
|
28
|
+
|
|
29
|
+
const { dateFrom, dateTo } = getDatesForSelectorValue(
|
|
30
|
+
initialSelectedDateRange,
|
|
31
|
+
customSelectOptions,
|
|
32
|
+
earliestDate,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
initialSelectedDateRange,
|
|
37
|
+
initialSelectedDateFrom: dateFrom,
|
|
38
|
+
initialSelectedDateTo: dateTo,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const initialSelectedDateFrom = isUndefinedOrEmpty(initialDateFrom)
|
|
43
|
+
? new Date(earliestDate)
|
|
44
|
+
: new Date(initialDateFrom);
|
|
45
|
+
let initialSelectedDateTo = isUndefinedOrEmpty(initialDateTo) ? new Date() : new Date(initialDateTo);
|
|
46
|
+
|
|
47
|
+
if (isNaN(initialSelectedDateFrom.getTime())) {
|
|
48
|
+
throw new UserFacingError(
|
|
49
|
+
'Invalid initialDateFrom',
|
|
50
|
+
`Invalid initialDateFrom "${initialDateFrom}", It must be of the format YYYY-MM-DD`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (isNaN(initialSelectedDateTo.getTime())) {
|
|
54
|
+
throw new UserFacingError(
|
|
55
|
+
'Invalid initialDateTo',
|
|
56
|
+
`Invalid initialDateTo "${initialDateTo}", It must be of the format YYYY-MM-DD`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (initialSelectedDateFrom > initialSelectedDateTo) {
|
|
61
|
+
initialSelectedDateTo = initialSelectedDateFrom;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
initialSelectedDateRange: PRESET_VALUE_CUSTOM,
|
|
66
|
+
initialSelectedDateFrom,
|
|
67
|
+
initialSelectedDateTo,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isUndefinedOrEmpty(value: string | undefined): value is undefined | '' {
|
|
72
|
+
return value === undefined || value === '';
|
|
73
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
|
+
import { expect, waitFor, within } from '@storybook/test';
|
|
4
|
+
import dayjs from 'dayjs/esm';
|
|
3
5
|
|
|
6
|
+
import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
|
|
4
7
|
import {
|
|
5
|
-
DateRangeSelector,
|
|
6
|
-
type DateRangeSelectorProps,
|
|
7
8
|
PRESET_VALUE_ALL_TIMES,
|
|
8
9
|
PRESET_VALUE_CUSTOM,
|
|
9
10
|
PRESET_VALUE_LAST_2_MONTHS,
|
|
@@ -11,10 +12,12 @@ import {
|
|
|
11
12
|
PRESET_VALUE_LAST_3_MONTHS,
|
|
12
13
|
PRESET_VALUE_LAST_6_MONTHS,
|
|
13
14
|
PRESET_VALUE_LAST_MONTH,
|
|
14
|
-
} from './
|
|
15
|
+
} from './selectableOptions';
|
|
15
16
|
import { LAPIS_URL } from '../../constants';
|
|
16
17
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
17
18
|
|
|
19
|
+
const earliestDate = '1970-01-01';
|
|
20
|
+
|
|
18
21
|
const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
19
22
|
title: 'Input/DateRangeSelector',
|
|
20
23
|
component: DateRangeSelector,
|
|
@@ -58,10 +61,12 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
58
61
|
},
|
|
59
62
|
args: {
|
|
60
63
|
customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
|
|
61
|
-
earliestDate
|
|
64
|
+
earliestDate,
|
|
62
65
|
initialValue: PRESET_VALUE_LAST_3_MONTHS,
|
|
63
66
|
dateColumn: 'aDateColumn',
|
|
64
67
|
width: '100%',
|
|
68
|
+
initialDateFrom: '',
|
|
69
|
+
initialDateTo: '',
|
|
65
70
|
},
|
|
66
71
|
decorators: [withActions],
|
|
67
72
|
};
|
|
@@ -75,9 +80,93 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
75
80
|
customSelectOptions={args.customSelectOptions}
|
|
76
81
|
earliestDate={args.earliestDate}
|
|
77
82
|
initialValue={args.initialValue}
|
|
83
|
+
initialDateFrom={args.initialDateFrom}
|
|
84
|
+
initialDateTo={args.initialDateTo}
|
|
78
85
|
width={args.width}
|
|
79
86
|
dateColumn={args.dateColumn}
|
|
80
87
|
/>
|
|
81
88
|
</LapisUrlContext.Provider>
|
|
82
89
|
),
|
|
83
90
|
};
|
|
91
|
+
|
|
92
|
+
export const SetCorrectInitialValues: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
93
|
+
...Primary,
|
|
94
|
+
args: {
|
|
95
|
+
...Primary.args,
|
|
96
|
+
initialValue: 'CustomDateRange',
|
|
97
|
+
},
|
|
98
|
+
play: async ({ canvasElement }) => {
|
|
99
|
+
const canvas = within(canvasElement);
|
|
100
|
+
|
|
101
|
+
const dateFrom = () => canvas.getByPlaceholderText('Date from');
|
|
102
|
+
const dateTo = () => canvas.getByPlaceholderText('Date to');
|
|
103
|
+
const selectField = () => canvas.getByRole('combobox');
|
|
104
|
+
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(selectField()).toHaveValue('CustomDateRange');
|
|
107
|
+
expect(dateFrom()).toHaveValue('2021-01-01');
|
|
108
|
+
expect(dateTo()).toHaveValue('2021-12-31');
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const initialDateFrom = '2000-01-01';
|
|
114
|
+
|
|
115
|
+
export const SetCorrectInitialDateFrom: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
116
|
+
...Primary,
|
|
117
|
+
args: {
|
|
118
|
+
...Primary.args,
|
|
119
|
+
initialDateFrom,
|
|
120
|
+
},
|
|
121
|
+
play: async ({ canvasElement }) => {
|
|
122
|
+
const canvas = within(canvasElement);
|
|
123
|
+
|
|
124
|
+
const dateFrom = () => canvas.getByPlaceholderText('Date from');
|
|
125
|
+
const dateTo = () => canvas.getByPlaceholderText('Date to');
|
|
126
|
+
const selectField = () => canvas.getByRole('combobox');
|
|
127
|
+
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(selectField()).toHaveValue(PRESET_VALUE_CUSTOM);
|
|
130
|
+
expect(dateFrom()).toHaveValue(initialDateFrom);
|
|
131
|
+
expect(dateTo()).toHaveValue(dayjs().format('YYYY-MM-DD'));
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const initialDateTo = '2000-01-01';
|
|
137
|
+
|
|
138
|
+
export const SetCorrectInitialDateTo: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
139
|
+
...Primary,
|
|
140
|
+
args: {
|
|
141
|
+
...Primary.args,
|
|
142
|
+
initialDateTo,
|
|
143
|
+
},
|
|
144
|
+
play: async ({ canvasElement }) => {
|
|
145
|
+
const canvas = within(canvasElement);
|
|
146
|
+
|
|
147
|
+
const dateFrom = () => canvas.getByPlaceholderText('Date from');
|
|
148
|
+
const dateTo = () => canvas.getByPlaceholderText('Date to');
|
|
149
|
+
const selectField = () => canvas.getByRole('combobox');
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(selectField()).toHaveValue(PRESET_VALUE_CUSTOM);
|
|
153
|
+
expect(dateFrom()).toHaveValue(earliestDate);
|
|
154
|
+
expect(dateTo()).toHaveValue(initialDateTo);
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const HandlesInvalidInitialDateFrom: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
160
|
+
...Primary,
|
|
161
|
+
args: {
|
|
162
|
+
...Primary.args,
|
|
163
|
+
initialDateFrom: 'not a date',
|
|
164
|
+
},
|
|
165
|
+
play: async ({ canvasElement }) => {
|
|
166
|
+
const canvas = within(canvasElement);
|
|
167
|
+
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(canvas.getByText('Oops! Something went wrong.')).toBeVisible();
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
};
|