@genspectrum/dashboard-components 0.12.1 → 0.13.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 +114 -25
- package/dist/{LocationChangedEvent-CORvQvXv.js → LineageFilterChangedEvent-GedKNGFI.js} +25 -3
- package/dist/LineageFilterChangedEvent-GedKNGFI.js.map +1 -0
- package/dist/components.d.ts +55 -21
- package/dist/components.js +229 -286
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +28 -14
- package/dist/util.js +3 -1
- package/package.json +1 -1
- package/src/preact/components/downshift-combobox.tsx +145 -0
- package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +11 -0
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +16 -2
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +13 -2
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +110 -9
- package/src/preact/lineageFilter/lineage-filter.tsx +40 -50
- package/src/preact/locationFilter/LocationChangedEvent.ts +1 -1
- package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +6 -2
- package/src/preact/locationFilter/fetchAutocompletionList.ts +16 -6
- package/src/preact/locationFilter/location-filter.stories.tsx +33 -30
- package/src/preact/locationFilter/location-filter.tsx +47 -144
- package/src/preact/textInput/TextInputChangedEvent.ts +1 -1
- package/src/preact/textInput/fetchStringAutocompleteList.ts +20 -0
- package/src/preact/textInput/text-input.stories.tsx +14 -11
- package/src/preact/textInput/text-input.tsx +39 -140
- package/src/types.ts +3 -0
- package/src/utilEntrypoint.ts +2 -0
- package/src/web-components/input/gs-lineage-filter.stories.ts +120 -31
- package/src/web-components/input/gs-lineage-filter.tsx +24 -8
- package/src/web-components/input/gs-location-filter.stories.ts +9 -0
- package/src/web-components/input/gs-location-filter.tsx +21 -3
- package/src/web-components/input/gs-text-input.stories.ts +14 -5
- package/src/web-components/input/gs-text-input.tsx +23 -7
- package/standalone-bundle/dashboard-components.js +5574 -5605
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/dist/LocationChangedEvent-CORvQvXv.js.map +0 -1
- package/src/preact/textInput/fetchAutocompleteList.ts +0 -9
package/dist/util.d.ts
CHANGED
|
@@ -160,7 +160,17 @@ declare const lapisFilterSchema: default_2.ZodIntersection<default_2.ZodRecord<d
|
|
|
160
160
|
aminoAcidInsertions?: string[] | undefined;
|
|
161
161
|
}>>;
|
|
162
162
|
|
|
163
|
-
declare type
|
|
163
|
+
declare type LapisLineageFilter = Record<string, string | undefined>;
|
|
164
|
+
|
|
165
|
+
declare type LapisLocationFilter = default_2.infer<typeof lapisLocationFilterSchema>;
|
|
166
|
+
|
|
167
|
+
declare const lapisLocationFilterSchema: default_2.ZodRecord<default_2.ZodString, default_2.ZodUnion<[default_2.ZodString, default_2.ZodUndefined]>>;
|
|
168
|
+
|
|
169
|
+
declare type LapisTextFilter = Record<string, string | undefined>;
|
|
170
|
+
|
|
171
|
+
export declare class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
|
|
172
|
+
constructor(detail: LapisLineageFilter);
|
|
173
|
+
}
|
|
164
174
|
|
|
165
175
|
export declare class LocationChangedEvent extends CustomEvent<LapisLocationFilter> {
|
|
166
176
|
constructor(detail: LapisLocationFilter);
|
|
@@ -772,6 +782,10 @@ export declare type TemporalGranularity = default_2.infer<typeof temporalGranula
|
|
|
772
782
|
|
|
773
783
|
declare const temporalGranularitySchema: default_2.ZodUnion<[default_2.ZodLiteral<"day">, default_2.ZodLiteral<"week">, default_2.ZodLiteral<"month">, default_2.ZodLiteral<"year">]>;
|
|
774
784
|
|
|
785
|
+
export declare class TextInputChangedEvent extends CustomEvent<LapisTextFilter> {
|
|
786
|
+
constructor(detail: LapisTextFilter);
|
|
787
|
+
}
|
|
788
|
+
|
|
775
789
|
export declare const views: {
|
|
776
790
|
readonly table: "table";
|
|
777
791
|
readonly venn: "venn";
|
|
@@ -888,7 +902,7 @@ declare global {
|
|
|
888
902
|
|
|
889
903
|
declare global {
|
|
890
904
|
interface HTMLElementTagNameMap {
|
|
891
|
-
'gs-
|
|
905
|
+
'gs-aggregate': AggregateComponent;
|
|
892
906
|
}
|
|
893
907
|
}
|
|
894
908
|
|
|
@@ -896,7 +910,7 @@ declare global {
|
|
|
896
910
|
declare global {
|
|
897
911
|
namespace JSX {
|
|
898
912
|
interface IntrinsicElements {
|
|
899
|
-
'gs-
|
|
913
|
+
'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
900
914
|
}
|
|
901
915
|
}
|
|
902
916
|
}
|
|
@@ -904,7 +918,7 @@ declare global {
|
|
|
904
918
|
|
|
905
919
|
declare global {
|
|
906
920
|
interface HTMLElementTagNameMap {
|
|
907
|
-
'gs-
|
|
921
|
+
'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
|
|
908
922
|
}
|
|
909
923
|
}
|
|
910
924
|
|
|
@@ -912,7 +926,7 @@ declare global {
|
|
|
912
926
|
declare global {
|
|
913
927
|
namespace JSX {
|
|
914
928
|
interface IntrinsicElements {
|
|
915
|
-
'gs-
|
|
929
|
+
'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
916
930
|
}
|
|
917
931
|
}
|
|
918
932
|
}
|
|
@@ -1007,10 +1021,10 @@ declare global {
|
|
|
1007
1021
|
|
|
1008
1022
|
declare global {
|
|
1009
1023
|
interface HTMLElementTagNameMap {
|
|
1010
|
-
'gs-
|
|
1024
|
+
'gs-text-input': TextInputComponent;
|
|
1011
1025
|
}
|
|
1012
1026
|
interface HTMLElementEventMap {
|
|
1013
|
-
'gs-
|
|
1027
|
+
'gs-text-input-changed': TextInputChangedEvent;
|
|
1014
1028
|
}
|
|
1015
1029
|
}
|
|
1016
1030
|
|
|
@@ -1018,7 +1032,7 @@ declare global {
|
|
|
1018
1032
|
declare global {
|
|
1019
1033
|
namespace JSX {
|
|
1020
1034
|
interface IntrinsicElements {
|
|
1021
|
-
'gs-
|
|
1035
|
+
'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1022
1036
|
}
|
|
1023
1037
|
}
|
|
1024
1038
|
}
|
|
@@ -1026,10 +1040,10 @@ declare global {
|
|
|
1026
1040
|
|
|
1027
1041
|
declare global {
|
|
1028
1042
|
interface HTMLElementTagNameMap {
|
|
1029
|
-
'gs-
|
|
1043
|
+
'gs-mutation-filter': MutationFilterComponent;
|
|
1030
1044
|
}
|
|
1031
1045
|
interface HTMLElementEventMap {
|
|
1032
|
-
'gs-
|
|
1046
|
+
'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
|
|
1033
1047
|
}
|
|
1034
1048
|
}
|
|
1035
1049
|
|
|
@@ -1037,7 +1051,7 @@ declare global {
|
|
|
1037
1051
|
declare global {
|
|
1038
1052
|
namespace JSX {
|
|
1039
1053
|
interface IntrinsicElements {
|
|
1040
|
-
'gs-
|
|
1054
|
+
'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1041
1055
|
}
|
|
1042
1056
|
}
|
|
1043
1057
|
}
|
|
@@ -1045,10 +1059,10 @@ declare global {
|
|
|
1045
1059
|
|
|
1046
1060
|
declare global {
|
|
1047
1061
|
interface HTMLElementTagNameMap {
|
|
1048
|
-
'gs-
|
|
1062
|
+
'gs-lineage-filter': LineageFilterComponent;
|
|
1049
1063
|
}
|
|
1050
1064
|
interface HTMLElementEventMap {
|
|
1051
|
-
'gs-
|
|
1065
|
+
'gs-lineage-filter-changed': LineageFilterChangedEvent;
|
|
1052
1066
|
}
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
@@ -1056,7 +1070,7 @@ declare global {
|
|
|
1056
1070
|
declare global {
|
|
1057
1071
|
namespace JSX {
|
|
1058
1072
|
interface IntrinsicElements {
|
|
1059
|
-
'gs-
|
|
1073
|
+
'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1060
1074
|
}
|
|
1061
1075
|
}
|
|
1062
1076
|
}
|
package/dist/util.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { D, L, d, v } from "./
|
|
1
|
+
import { D, a, L, T, d, v } from "./LineageFilterChangedEvent-GedKNGFI.js";
|
|
2
2
|
export {
|
|
3
3
|
D as DateRangeOptionChangedEvent,
|
|
4
|
+
a as LineageFilterChangedEvent,
|
|
4
5
|
L as LocationChangedEvent,
|
|
6
|
+
T as TextInputChangedEvent,
|
|
5
7
|
d as dateRangeOptionPresets,
|
|
6
8
|
v as views
|
|
7
9
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCombobox } from 'downshift/preact';
|
|
2
|
+
import { type ComponentChild } from 'preact';
|
|
3
|
+
import { useRef, useState } from 'preact/hooks';
|
|
4
|
+
|
|
5
|
+
export function DownshiftCombobox<Item>({
|
|
6
|
+
allItems,
|
|
7
|
+
value,
|
|
8
|
+
filterItemsByInputValue,
|
|
9
|
+
createEvent,
|
|
10
|
+
itemToString,
|
|
11
|
+
placeholderText,
|
|
12
|
+
formatItemInList,
|
|
13
|
+
}: {
|
|
14
|
+
allItems: Item[];
|
|
15
|
+
value?: Item;
|
|
16
|
+
filterItemsByInputValue: (item: Item, value: string) => boolean;
|
|
17
|
+
createEvent: (item: Item | null) => CustomEvent;
|
|
18
|
+
itemToString: (item: Item | undefined | null) => string;
|
|
19
|
+
placeholderText?: string;
|
|
20
|
+
formatItemInList: (item: Item) => ComponentChild;
|
|
21
|
+
}) {
|
|
22
|
+
const initialSelectedItem = value ?? null;
|
|
23
|
+
|
|
24
|
+
const [items, setItems] = useState(
|
|
25
|
+
allItems.filter((item) => filterItemsByInputValue(item, itemToString(initialSelectedItem))),
|
|
26
|
+
);
|
|
27
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
const shadowRoot = divRef.current?.shadowRoot ?? undefined;
|
|
30
|
+
|
|
31
|
+
const environment =
|
|
32
|
+
shadowRoot !== undefined
|
|
33
|
+
? {
|
|
34
|
+
addEventListener: window.addEventListener.bind(window),
|
|
35
|
+
removeEventListener: window.removeEventListener.bind(window),
|
|
36
|
+
document: shadowRoot.ownerDocument,
|
|
37
|
+
Node: window.Node,
|
|
38
|
+
}
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
isOpen,
|
|
43
|
+
getToggleButtonProps,
|
|
44
|
+
getMenuProps,
|
|
45
|
+
getInputProps,
|
|
46
|
+
highlightedIndex,
|
|
47
|
+
getItemProps,
|
|
48
|
+
selectedItem,
|
|
49
|
+
inputValue,
|
|
50
|
+
selectItem,
|
|
51
|
+
setInputValue,
|
|
52
|
+
closeMenu,
|
|
53
|
+
} = useCombobox({
|
|
54
|
+
onInputValueChange({ inputValue }) {
|
|
55
|
+
setItems(allItems.filter((item) => filterItemsByInputValue(item, inputValue)));
|
|
56
|
+
},
|
|
57
|
+
onSelectedItemChange({ selectedItem }) {
|
|
58
|
+
if (selectedItem !== null) {
|
|
59
|
+
divRef.current?.dispatchEvent(createEvent(selectedItem));
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
items,
|
|
63
|
+
itemToString(item) {
|
|
64
|
+
return itemToString(item);
|
|
65
|
+
},
|
|
66
|
+
initialSelectedItem,
|
|
67
|
+
environment,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const onInputBlur = () => {
|
|
71
|
+
if (inputValue === '') {
|
|
72
|
+
divRef.current?.dispatchEvent(createEvent(null));
|
|
73
|
+
selectItem(null);
|
|
74
|
+
} else if (inputValue !== itemToString(selectedItem)) {
|
|
75
|
+
setInputValue(itemToString(selectedItem) || '');
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const clearInput = () => {
|
|
80
|
+
divRef.current?.dispatchEvent(createEvent(null));
|
|
81
|
+
selectItem(null);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const buttonRef = useRef(null);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div ref={divRef} className={'relative w-full'}>
|
|
88
|
+
<div className='w-full flex flex-col gap-1'>
|
|
89
|
+
<div
|
|
90
|
+
className='flex gap-0.5 input input-bordered min-w-32'
|
|
91
|
+
onBlur={(event) => {
|
|
92
|
+
if (event.relatedTarget != buttonRef.current) {
|
|
93
|
+
closeMenu();
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<input
|
|
98
|
+
placeholder={placeholderText}
|
|
99
|
+
className='w-full p-1.5'
|
|
100
|
+
{...getInputProps()}
|
|
101
|
+
onBlur={onInputBlur}
|
|
102
|
+
/>
|
|
103
|
+
<button
|
|
104
|
+
aria-label='clear selection'
|
|
105
|
+
className={`px-2 ${inputValue === '' && 'hidden'}`}
|
|
106
|
+
type='button'
|
|
107
|
+
onClick={clearInput}
|
|
108
|
+
tabIndex={-1}
|
|
109
|
+
>
|
|
110
|
+
×
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
aria-label='toggle menu'
|
|
114
|
+
className='px-2'
|
|
115
|
+
type='button'
|
|
116
|
+
{...getToggleButtonProps()}
|
|
117
|
+
ref={buttonRef}
|
|
118
|
+
>
|
|
119
|
+
{isOpen ? <>↑</> : <>↓</>}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
{isOpen && (
|
|
124
|
+
<ul
|
|
125
|
+
className='absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32'
|
|
126
|
+
{...getMenuProps()}
|
|
127
|
+
>
|
|
128
|
+
{items.length > 0 ? (
|
|
129
|
+
items.map((item, index) => (
|
|
130
|
+
<li
|
|
131
|
+
className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm flex flex-col`}
|
|
132
|
+
key={itemToString(item)}
|
|
133
|
+
{...getItemProps({ item, index })}
|
|
134
|
+
>
|
|
135
|
+
{formatItemInList(item)}
|
|
136
|
+
</li>
|
|
137
|
+
))
|
|
138
|
+
) : (
|
|
139
|
+
<li className='py-2 px-3 shadow-sm flex flex-col'>No elements to select.</li>
|
|
140
|
+
)}
|
|
141
|
+
</ul>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type LapisLineageFilter = Record<string, string | undefined>;
|
|
2
|
+
|
|
3
|
+
export class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
|
|
4
|
+
constructor(detail: LapisLineageFilter) {
|
|
5
|
+
super('gs-lineage-filter-changed', {
|
|
6
|
+
detail,
|
|
7
|
+
bubbles: true,
|
|
8
|
+
composed: true,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -5,9 +5,23 @@ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../../vitest.setup';
|
|
|
5
5
|
|
|
6
6
|
describe('fetchLineageAutocompleteList', () => {
|
|
7
7
|
test('should add sublineage values', async () => {
|
|
8
|
-
lapisRequestMocks.aggregated(
|
|
8
|
+
lapisRequestMocks.aggregated(
|
|
9
|
+
{ fields: ['lineageField'], country: 'Germany' },
|
|
10
|
+
{
|
|
11
|
+
data: [
|
|
12
|
+
{
|
|
13
|
+
lineageField: 'B.1.1.7',
|
|
14
|
+
count: 1,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
);
|
|
9
19
|
|
|
10
|
-
const result = await fetchLineageAutocompleteList(
|
|
20
|
+
const result = await fetchLineageAutocompleteList({
|
|
21
|
+
lapis: DUMMY_LAPIS_URL,
|
|
22
|
+
field: 'lineageField',
|
|
23
|
+
lapisFilter: { country: 'Germany' },
|
|
24
|
+
});
|
|
11
25
|
|
|
12
26
|
expect(result).to.deep.equal(['B.1.1.7', 'B.1.1.7*']);
|
|
13
27
|
});
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
|
+
import type { LapisFilter } from '../../types';
|
|
2
3
|
|
|
3
|
-
export async function fetchLineageAutocompleteList(
|
|
4
|
-
|
|
4
|
+
export async function fetchLineageAutocompleteList({
|
|
5
|
+
lapis,
|
|
6
|
+
field,
|
|
7
|
+
signal,
|
|
8
|
+
lapisFilter,
|
|
9
|
+
}: {
|
|
10
|
+
lapis: string;
|
|
11
|
+
field: string;
|
|
12
|
+
lapisFilter?: LapisFilter;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}) {
|
|
15
|
+
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [field]);
|
|
5
16
|
|
|
6
17
|
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
7
18
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
1
|
+
import { type Meta, type PreactRenderer, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
|
3
|
+
import type { StepFunction } from '@storybook/types';
|
|
2
4
|
|
|
3
5
|
import { LineageFilter, type LineageFilterProps } from './lineage-filter';
|
|
4
6
|
import { previewHandles } from '../../../.storybook/preview';
|
|
@@ -22,6 +24,7 @@ const meta: Meta = {
|
|
|
22
24
|
url: AGGREGATED_ENDPOINT,
|
|
23
25
|
body: {
|
|
24
26
|
fields: ['pangoLineage'],
|
|
27
|
+
country: 'Germany',
|
|
25
28
|
},
|
|
26
29
|
},
|
|
27
30
|
response: {
|
|
@@ -32,10 +35,41 @@ const meta: Meta = {
|
|
|
32
35
|
],
|
|
33
36
|
},
|
|
34
37
|
},
|
|
38
|
+
argTypes: {
|
|
39
|
+
lapisField: {
|
|
40
|
+
control: {
|
|
41
|
+
type: 'text',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
placeholderText: {
|
|
45
|
+
control: {
|
|
46
|
+
type: 'text',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
value: {
|
|
50
|
+
control: {
|
|
51
|
+
type: 'text',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
width: {
|
|
55
|
+
control: {
|
|
56
|
+
type: 'text',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
lapisFilter: {
|
|
60
|
+
control: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
35
66
|
args: {
|
|
36
67
|
lapisField: 'pangoLineage',
|
|
37
|
-
|
|
38
|
-
|
|
68
|
+
lapisFilter: {
|
|
69
|
+
country: 'Germany',
|
|
70
|
+
},
|
|
71
|
+
placeholderText: 'Enter a lineage',
|
|
72
|
+
value: 'A.1',
|
|
39
73
|
width: '100%',
|
|
40
74
|
},
|
|
41
75
|
};
|
|
@@ -45,14 +79,62 @@ export default meta;
|
|
|
45
79
|
export const Default: StoryObj<LineageFilterProps> = {
|
|
46
80
|
render: (args) => (
|
|
47
81
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
48
|
-
<LineageFilter
|
|
49
|
-
lapisField={args.lapisField}
|
|
50
|
-
placeholderText={args.placeholderText}
|
|
51
|
-
initialValue={args.initialValue}
|
|
52
|
-
width={args.width}
|
|
53
|
-
/>
|
|
82
|
+
<LineageFilter {...args} />
|
|
54
83
|
</LapisUrlContext.Provider>
|
|
55
84
|
),
|
|
85
|
+
play: async ({ canvasElement, step }) => {
|
|
86
|
+
const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
|
|
87
|
+
|
|
88
|
+
step('change lineage filter value fires event', async () => {
|
|
89
|
+
const input = await inputField(canvas);
|
|
90
|
+
await userEvent.clear(input);
|
|
91
|
+
await userEvent.type(input, 'B.1');
|
|
92
|
+
await userEvent.click(canvas.getByRole('option', { name: 'B.1' }));
|
|
93
|
+
|
|
94
|
+
await waitFor(() => {
|
|
95
|
+
return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
96
|
+
pangoLineage: 'B.1',
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const ClearSelection: StoryObj<LineageFilterProps> = {
|
|
104
|
+
...Default,
|
|
105
|
+
play: async ({ canvasElement, step }) => {
|
|
106
|
+
const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
|
|
107
|
+
|
|
108
|
+
step('clear selection fires event with empty filter', async () => {
|
|
109
|
+
const clearSelectionButton = await canvas.findByLabelText('clear selection');
|
|
110
|
+
await userEvent.click(clearSelectionButton);
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
114
|
+
pangoLineage: undefined,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const OnBlurInput: StoryObj<LineageFilterProps> = {
|
|
122
|
+
...Default,
|
|
123
|
+
play: async ({ canvasElement, step }) => {
|
|
124
|
+
const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
|
|
125
|
+
|
|
126
|
+
step('after cleared selection by hand and then blur fires event with empty filter', async () => {
|
|
127
|
+
const input = await inputField(canvas);
|
|
128
|
+
await userEvent.clear(input);
|
|
129
|
+
await userEvent.click(canvas.getByLabelText('toggle menu'));
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
133
|
+
pangoLineage: undefined,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
},
|
|
56
138
|
};
|
|
57
139
|
|
|
58
140
|
export const WithNoLapisField: StoryObj<LineageFilterProps> = {
|
|
@@ -67,3 +149,22 @@ export const WithNoLapisField: StoryObj<LineageFilterProps> = {
|
|
|
67
149
|
});
|
|
68
150
|
},
|
|
69
151
|
};
|
|
152
|
+
|
|
153
|
+
async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
|
|
154
|
+
const canvas = within(canvasElement);
|
|
155
|
+
|
|
156
|
+
const lineageChangedListenerMock = fn();
|
|
157
|
+
step('Setup event listener mock', () => {
|
|
158
|
+
canvasElement.addEventListener('gs-lineage-filter-changed', lineageChangedListenerMock);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
step('location filter is rendered with value', async () => {
|
|
162
|
+
await waitFor(async () => {
|
|
163
|
+
return expect(await inputField(canvas)).toHaveValue('A.1');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return { canvas, lineageChangedListenerMock };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const inputField = (canvas: ReturnType<typeof within>) => canvas.findByPlaceholderText('Enter a lineage');
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { useContext
|
|
2
|
+
import { useContext } from 'preact/hooks';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
|
|
5
5
|
import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
|
|
6
6
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
7
|
+
import { LineageFilterChangedEvent } from './LineageFilterChangedEvent';
|
|
8
|
+
import { lapisFilterSchema } from '../../types';
|
|
9
|
+
import { DownshiftCombobox } from '../components/downshift-combobox';
|
|
7
10
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
8
11
|
import { LoadingDisplay } from '../components/loading-display';
|
|
9
|
-
import { NoDataDisplay } from '../components/no-data-display';
|
|
10
12
|
import { ResizeContainer } from '../components/resize-container';
|
|
11
13
|
import { useQuery } from '../useQuery';
|
|
12
14
|
|
|
13
|
-
const
|
|
15
|
+
const lineageSelectorPropsSchema = z.object({
|
|
14
16
|
lapisField: z.string().min(1),
|
|
15
17
|
placeholderText: z.string().optional(),
|
|
16
|
-
|
|
18
|
+
value: z.string(),
|
|
19
|
+
});
|
|
20
|
+
const lineageFilterInnerPropsSchema = lineageSelectorPropsSchema.extend({
|
|
21
|
+
lapisFilter: lapisFilterSchema,
|
|
17
22
|
});
|
|
18
|
-
|
|
19
23
|
const lineageFilterPropsSchema = lineageFilterInnerPropsSchema.extend({
|
|
20
24
|
width: z.string(),
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
export type LineageFilterInnerProps = z.infer<typeof lineageFilterInnerPropsSchema>;
|
|
24
28
|
export type LineageFilterProps = z.infer<typeof lineageFilterPropsSchema>;
|
|
29
|
+
type LineageSelectorProps = z.infer<typeof lineageSelectorPropsSchema>;
|
|
25
30
|
|
|
26
31
|
export const LineageFilter: FunctionComponent<LineageFilterProps> = (props) => {
|
|
27
32
|
const { width, ...innerProps } = props;
|
|
@@ -39,15 +44,14 @@ export const LineageFilter: FunctionComponent<LineageFilterProps> = (props) => {
|
|
|
39
44
|
const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
|
|
40
45
|
lapisField,
|
|
41
46
|
placeholderText,
|
|
42
|
-
|
|
47
|
+
value,
|
|
48
|
+
lapisFilter,
|
|
43
49
|
}) => {
|
|
44
50
|
const lapis = useContext(LapisUrlContext);
|
|
45
51
|
|
|
46
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
47
|
-
|
|
48
52
|
const { data, error, isLoading } = useQuery(
|
|
49
|
-
() => fetchLineageAutocompleteList(lapis, lapisField),
|
|
50
|
-
[lapisField, lapis],
|
|
53
|
+
() => fetchLineageAutocompleteList({ lapis, field: lapisField, lapisFilter }),
|
|
54
|
+
[lapisField, lapis, lapisFilter],
|
|
51
55
|
);
|
|
52
56
|
|
|
53
57
|
if (isLoading) {
|
|
@@ -58,47 +62,33 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
|
|
|
58
62
|
throw error;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const onInput = () => {
|
|
66
|
-
const value = inputRef.current?.value === '' ? undefined : inputRef.current?.value;
|
|
67
|
-
|
|
68
|
-
if (isValidValue(value)) {
|
|
69
|
-
inputRef.current?.dispatchEvent(
|
|
70
|
-
new CustomEvent('gs-lineage-filter-changed', {
|
|
71
|
-
detail: { [lapisField]: value },
|
|
72
|
-
bubbles: true,
|
|
73
|
-
composed: true,
|
|
74
|
-
}),
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const isValidValue = (value: string | undefined) => {
|
|
80
|
-
if (value === undefined) {
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
return data.includes(value);
|
|
84
|
-
};
|
|
65
|
+
return <LineageSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
|
|
66
|
+
};
|
|
85
67
|
|
|
68
|
+
const LineageSelector = ({
|
|
69
|
+
lapisField,
|
|
70
|
+
value,
|
|
71
|
+
placeholderText,
|
|
72
|
+
data,
|
|
73
|
+
}: LineageSelectorProps & {
|
|
74
|
+
data: string[];
|
|
75
|
+
}) => {
|
|
86
76
|
return (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/>
|
|
97
|
-
<datalist id={lapisField}>
|
|
98
|
-
{data.map((item) => (
|
|
99
|
-
<option value={item} key={item} />
|
|
100
|
-
))}
|
|
101
|
-
</datalist>
|
|
102
|
-
</>
|
|
77
|
+
<DownshiftCombobox
|
|
78
|
+
allItems={data}
|
|
79
|
+
value={value}
|
|
80
|
+
filterItemsByInputValue={filterByInputValue}
|
|
81
|
+
createEvent={(item: string | null) => new LineageFilterChangedEvent({ [lapisField]: item ?? undefined })}
|
|
82
|
+
itemToString={(item: string | undefined | null) => item ?? ''}
|
|
83
|
+
placeholderText={placeholderText}
|
|
84
|
+
formatItemInList={(item: string) => item}
|
|
85
|
+
/>
|
|
103
86
|
);
|
|
104
87
|
};
|
|
88
|
+
|
|
89
|
+
function filterByInputValue(item: string, inputValue: string | undefined | null) {
|
|
90
|
+
if (inputValue === undefined || inputValue === null || inputValue === '') {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
|
|
94
|
+
}
|
|
@@ -8,7 +8,7 @@ describe('fetchAutocompletionList', () => {
|
|
|
8
8
|
const fields = ['region', 'country', 'division'];
|
|
9
9
|
|
|
10
10
|
lapisRequestMocks.aggregated(
|
|
11
|
-
{ fields },
|
|
11
|
+
{ fields, country: 'Germany' },
|
|
12
12
|
{
|
|
13
13
|
data: [
|
|
14
14
|
{ count: 0, region: 'region1', country: 'country1_1', division: 'division1_1_1' },
|
|
@@ -20,7 +20,11 @@ describe('fetchAutocompletionList', () => {
|
|
|
20
20
|
},
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const result = await fetchAutocompletionList(
|
|
23
|
+
const result = await fetchAutocompletionList({
|
|
24
|
+
fields,
|
|
25
|
+
lapis: DUMMY_LAPIS_URL,
|
|
26
|
+
lapisFilter: { country: 'Germany' },
|
|
27
|
+
});
|
|
24
28
|
|
|
25
29
|
expect(result).to.deep.equal([
|
|
26
30
|
{ region: 'region1', country: undefined, division: undefined },
|