@genspectrum/dashboard-components 0.12.0 → 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 +117 -28
- package/dist/{LocationChangedEvent-CORvQvXv.js → LineageFilterChangedEvent-GedKNGFI.js} +25 -3
- package/dist/LineageFilterChangedEvent-GedKNGFI.js.map +1 -0
- package/dist/components.d.ts +86 -52
- package/dist/components.js +251 -196
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +59 -45
- 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/map/sequences-by-location-map.tsx +3 -3
- package/src/preact/textInput/TextInputChangedEvent.ts +11 -0
- package/src/preact/textInput/fetchStringAutocompleteList.ts +20 -0
- package/src/preact/textInput/text-input.stories.tsx +34 -14
- package/src/preact/textInput/text-input.tsx +47 -45
- 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 +44 -12
- package/src/web-components/input/gs-text-input.tsx +23 -7
- package/standalone-bundle/dashboard-components.js +4931 -4863
- 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
|
@@ -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 },
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
|
+
import type { LapisFilter } from '../../types';
|
|
2
3
|
|
|
3
|
-
export async function fetchAutocompletionList(
|
|
4
|
-
fields
|
|
5
|
-
lapis
|
|
6
|
-
signal
|
|
7
|
-
|
|
4
|
+
export async function fetchAutocompletionList({
|
|
5
|
+
fields,
|
|
6
|
+
lapis,
|
|
7
|
+
signal,
|
|
8
|
+
lapisFilter,
|
|
9
|
+
}: {
|
|
10
|
+
fields: string[];
|
|
11
|
+
lapis: string;
|
|
12
|
+
lapisFilter?: LapisFilter;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}): Promise<Record<string, string | undefined>[]> {
|
|
8
15
|
const toAncestorInHierarchyOverwriteValues = Array(fields.length - 1)
|
|
9
16
|
.fill(0)
|
|
10
17
|
.map((_, i) => i + 1)
|
|
11
18
|
.map((i) => fields.slice(i).reduce((acc, field) => ({ ...acc, [field]: null }), {}));
|
|
12
19
|
|
|
13
|
-
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>(
|
|
20
|
+
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>(
|
|
21
|
+
lapisFilter ?? {},
|
|
22
|
+
fields,
|
|
23
|
+
);
|
|
14
24
|
|
|
15
25
|
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
16
26
|
|
|
@@ -21,6 +21,7 @@ const meta: Meta<LocationFilterProps> = {
|
|
|
21
21
|
url: AGGREGATED_ENDPOINT,
|
|
22
22
|
body: {
|
|
23
23
|
fields: ['region', 'country', 'division', 'location'],
|
|
24
|
+
age: 18,
|
|
24
25
|
},
|
|
25
26
|
},
|
|
26
27
|
response: {
|
|
@@ -39,6 +40,9 @@ const meta: Meta<LocationFilterProps> = {
|
|
|
39
40
|
fields: ['region', 'country', 'division', 'location'],
|
|
40
41
|
value: { region: 'Europe', country: undefined, division: undefined, location: undefined },
|
|
41
42
|
placeholderText: 'Enter a location',
|
|
43
|
+
lapisFilter: {
|
|
44
|
+
age: 18,
|
|
45
|
+
},
|
|
42
46
|
},
|
|
43
47
|
argTypes: {
|
|
44
48
|
fields: {
|
|
@@ -61,6 +65,11 @@ const meta: Meta<LocationFilterProps> = {
|
|
|
61
65
|
type: 'text',
|
|
62
66
|
},
|
|
63
67
|
},
|
|
68
|
+
lapisFilter: {
|
|
69
|
+
control: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
64
73
|
},
|
|
65
74
|
};
|
|
66
75
|
|
|
@@ -81,16 +90,14 @@ export const Primary: StoryObj<LocationFilterProps> = {
|
|
|
81
90
|
await userEvent.type(input, 'Germany');
|
|
82
91
|
await userEvent.click(canvas.getByRole('option', { name: 'Germany Europe / Germany' }));
|
|
83
92
|
|
|
84
|
-
await
|
|
85
|
-
expect.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}),
|
|
93
|
-
);
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
95
|
+
country: 'Germany',
|
|
96
|
+
region: 'Europe',
|
|
97
|
+
division: undefined,
|
|
98
|
+
location: undefined,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
94
101
|
});
|
|
95
102
|
},
|
|
96
103
|
};
|
|
@@ -104,16 +111,14 @@ export const ClearSelection: StoryObj<LocationFilterProps> = {
|
|
|
104
111
|
const clearSelectionButton = await canvas.findByLabelText('clear selection');
|
|
105
112
|
await userEvent.click(clearSelectionButton);
|
|
106
113
|
|
|
107
|
-
await
|
|
108
|
-
expect.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}),
|
|
116
|
-
);
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
116
|
+
country: undefined,
|
|
117
|
+
region: undefined,
|
|
118
|
+
division: undefined,
|
|
119
|
+
location: undefined,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
117
122
|
});
|
|
118
123
|
},
|
|
119
124
|
};
|
|
@@ -128,16 +133,14 @@ export const OnBlurInput: StoryObj<LocationFilterProps> = {
|
|
|
128
133
|
await userEvent.clear(input);
|
|
129
134
|
await userEvent.click(canvas.getByLabelText('toggle menu'));
|
|
130
135
|
|
|
131
|
-
await
|
|
132
|
-
expect.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}),
|
|
140
|
-
);
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
138
|
+
country: undefined,
|
|
139
|
+
region: undefined,
|
|
140
|
+
division: undefined,
|
|
141
|
+
location: undefined,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
141
144
|
});
|
|
142
145
|
},
|
|
143
146
|
};
|
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
import { useCombobox } from 'downshift/preact';
|
|
2
1
|
import { type FunctionComponent } from 'preact';
|
|
3
|
-
import { useContext, useMemo
|
|
2
|
+
import { useContext, useMemo } from 'preact/hooks';
|
|
4
3
|
import z from 'zod';
|
|
5
4
|
|
|
6
5
|
import { fetchAutocompletionList } from './fetchAutocompletionList';
|
|
7
6
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
|
-
import {
|
|
7
|
+
import { LocationChangedEvent } from './LocationChangedEvent';
|
|
8
|
+
import { lapisFilterSchema, type LapisLocationFilter, lapisLocationFilterSchema } from '../../types';
|
|
9
|
+
import { DownshiftCombobox } from '../components/downshift-combobox';
|
|
9
10
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
10
11
|
import { LoadingDisplay } from '../components/loading-display';
|
|
11
12
|
import { ResizeContainer } from '../components/resize-container';
|
|
12
13
|
import { useQuery } from '../useQuery';
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
value:
|
|
15
|
+
const locationSelectorPropsSchema = z.object({
|
|
16
|
+
value: lapisLocationFilterSchema.optional(),
|
|
16
17
|
placeholderText: z.string().optional(),
|
|
17
18
|
fields: z.array(z.string()).min(1),
|
|
18
19
|
});
|
|
19
|
-
|
|
20
|
-
const
|
|
20
|
+
const locationFilterInnerPropsSchema = locationSelectorPropsSchema.extend({ lapisFilter: lapisFilterSchema });
|
|
21
|
+
const locationFilterPropsSchema = locationFilterInnerPropsSchema.extend({
|
|
21
22
|
width: z.string(),
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
export type LocationFilterInnerProps = z.infer<typeof
|
|
25
|
-
export type LocationFilterProps = z.infer<typeof
|
|
25
|
+
export type LocationFilterInnerProps = z.infer<typeof locationFilterInnerPropsSchema>;
|
|
26
|
+
export type LocationFilterProps = z.infer<typeof locationFilterPropsSchema>;
|
|
27
|
+
type LocationSelectorProps = z.infer<typeof locationSelectorPropsSchema>;
|
|
26
28
|
|
|
27
29
|
export const LocationFilter: FunctionComponent<LocationFilterProps> = (props) => {
|
|
28
30
|
const { width, ...innerProps } = props;
|
|
29
31
|
const size = { width, height: '3rem' };
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
|
-
<ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={
|
|
34
|
+
<ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={locationFilterPropsSchema}>
|
|
33
35
|
<ResizeContainer size={size}>
|
|
34
36
|
<LocationFilterInner {...innerProps} />
|
|
35
37
|
</ResizeContainer>
|
|
@@ -37,10 +39,13 @@ export const LocationFilter: FunctionComponent<LocationFilterProps> = (props) =>
|
|
|
37
39
|
);
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
export const LocationFilterInner = ({ value, fields, placeholderText }: LocationFilterInnerProps) => {
|
|
42
|
+
export const LocationFilterInner = ({ value, fields, placeholderText, lapisFilter }: LocationFilterInnerProps) => {
|
|
41
43
|
const lapis = useContext(LapisUrlContext);
|
|
42
44
|
|
|
43
|
-
const { data, error, isLoading } = useQuery(
|
|
45
|
+
const { data, error, isLoading } = useQuery(
|
|
46
|
+
() => fetchAutocompletionList({ fields, lapis, lapisFilter }),
|
|
47
|
+
[fields, lapis, lapisFilter],
|
|
48
|
+
);
|
|
44
49
|
|
|
45
50
|
if (isLoading) {
|
|
46
51
|
return <LoadingDisplay />;
|
|
@@ -54,7 +59,7 @@ export const LocationFilterInner = ({ value, fields, placeholderText }: Location
|
|
|
54
59
|
|
|
55
60
|
type SelectItem = {
|
|
56
61
|
lapisFilter: LapisLocationFilter;
|
|
57
|
-
label: string;
|
|
62
|
+
label: string | null | undefined;
|
|
58
63
|
description: string;
|
|
59
64
|
};
|
|
60
65
|
|
|
@@ -63,140 +68,38 @@ const LocationSelector = ({
|
|
|
63
68
|
value,
|
|
64
69
|
placeholderText,
|
|
65
70
|
locationData,
|
|
66
|
-
}:
|
|
71
|
+
}: LocationSelectorProps & {
|
|
67
72
|
locationData: LapisLocationFilter[];
|
|
68
73
|
}) => {
|
|
69
|
-
const allItems = useMemo(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
75
|
-
.filter((item): item is SelectItem => item !== undefined),
|
|
76
|
-
[locationData, fields],
|
|
77
|
-
);
|
|
74
|
+
const allItems = useMemo(() => {
|
|
75
|
+
return locationData
|
|
76
|
+
.map((location) => toSelectItem(location, fields))
|
|
77
|
+
.filter((item): item is SelectItem => item !== undefined);
|
|
78
|
+
}, [fields, locationData]);
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const [items, setItems] = useState(allItems.filter((item) => filterByInputValue(item, initialSelectedItem?.label)));
|
|
85
|
-
const divRef = useRef<HTMLDivElement>(null);
|
|
86
|
-
|
|
87
|
-
const shadowRoot = divRef.current?.shadowRoot ?? undefined;
|
|
88
|
-
|
|
89
|
-
const environment =
|
|
90
|
-
shadowRoot !== undefined
|
|
91
|
-
? {
|
|
92
|
-
addEventListener: window.addEventListener.bind(window),
|
|
93
|
-
removeEventListener: window.removeEventListener.bind(window),
|
|
94
|
-
document: shadowRoot.ownerDocument,
|
|
95
|
-
Node: window.Node,
|
|
96
|
-
}
|
|
97
|
-
: undefined;
|
|
98
|
-
|
|
99
|
-
const {
|
|
100
|
-
isOpen,
|
|
101
|
-
getToggleButtonProps,
|
|
102
|
-
getMenuProps,
|
|
103
|
-
getInputProps,
|
|
104
|
-
highlightedIndex,
|
|
105
|
-
getItemProps,
|
|
106
|
-
selectedItem,
|
|
107
|
-
inputValue,
|
|
108
|
-
selectItem,
|
|
109
|
-
setInputValue,
|
|
110
|
-
closeMenu,
|
|
111
|
-
} = useCombobox({
|
|
112
|
-
onInputValueChange({ inputValue }) {
|
|
113
|
-
setItems(allItems.filter((item) => filterByInputValue(item, inputValue)));
|
|
114
|
-
},
|
|
115
|
-
onSelectedItemChange({ selectedItem }) {
|
|
116
|
-
if (selectedItem !== null) {
|
|
117
|
-
divRef.current?.dispatchEvent(new LocationChangedEvent(selectedItem.lapisFilter));
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
items,
|
|
121
|
-
itemToString(item) {
|
|
122
|
-
return item?.label ?? '';
|
|
123
|
-
},
|
|
124
|
-
initialSelectedItem,
|
|
125
|
-
environment,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const onInputBlur = () => {
|
|
129
|
-
if (inputValue === '') {
|
|
130
|
-
divRef.current?.dispatchEvent(new LocationChangedEvent(emptyLocationFilter(fields)));
|
|
131
|
-
selectItem(null);
|
|
132
|
-
} else if (inputValue !== selectedItem?.label) {
|
|
133
|
-
setInputValue(selectedItem?.label || '');
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const clearInput = () => {
|
|
138
|
-
divRef.current?.dispatchEvent(new LocationChangedEvent(emptyLocationFilter(fields)));
|
|
139
|
-
selectItem(null);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const buttonRef = useRef(null);
|
|
80
|
+
const selectedItem = useMemo(() => {
|
|
81
|
+
return value !== undefined ? toSelectItem(value, fields) : undefined;
|
|
82
|
+
}, [fields, value]);
|
|
143
83
|
|
|
144
84
|
return (
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
className={`px-2 ${inputValue === '' && 'hidden'}`}
|
|
164
|
-
type='button'
|
|
165
|
-
onClick={clearInput}
|
|
166
|
-
tabIndex={-1}
|
|
167
|
-
>
|
|
168
|
-
×
|
|
169
|
-
</button>
|
|
170
|
-
<button
|
|
171
|
-
aria-label='toggle menu'
|
|
172
|
-
className='px-2'
|
|
173
|
-
type='button'
|
|
174
|
-
{...getToggleButtonProps()}
|
|
175
|
-
ref={buttonRef}
|
|
176
|
-
>
|
|
177
|
-
{isOpen ? <>↑</> : <>↓</>}
|
|
178
|
-
</button>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
<ul
|
|
182
|
-
className={`absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32 ${
|
|
183
|
-
!(isOpen && items.length > 0) && 'hidden'
|
|
184
|
-
}`}
|
|
185
|
-
{...getMenuProps()}
|
|
186
|
-
>
|
|
187
|
-
{isOpen &&
|
|
188
|
-
items.map((item, index) => (
|
|
189
|
-
<li
|
|
190
|
-
className={`${highlightedIndex === index && 'bg-blue-300'} ${selectedItem !== null && selectedItem.description === item.description && 'font-bold'} py-2 px-3 shadow-sm flex flex-col`}
|
|
191
|
-
key={item.description}
|
|
192
|
-
{...getItemProps({ item, index })}
|
|
193
|
-
>
|
|
194
|
-
<span>{item.label}</span>
|
|
195
|
-
<span className='text-sm text-gray-500'>{item.description}</span>
|
|
196
|
-
</li>
|
|
197
|
-
))}
|
|
198
|
-
</ul>
|
|
199
|
-
</div>
|
|
85
|
+
<DownshiftCombobox
|
|
86
|
+
allItems={allItems}
|
|
87
|
+
value={selectedItem}
|
|
88
|
+
filterItemsByInputValue={filterByInputValue}
|
|
89
|
+
createEvent={(item: SelectItem | null) =>
|
|
90
|
+
new LocationChangedEvent(item?.lapisFilter ?? emptyLocationFilter(fields))
|
|
91
|
+
}
|
|
92
|
+
itemToString={(item: SelectItem | undefined | null) => item?.label ?? ''}
|
|
93
|
+
placeholderText={placeholderText}
|
|
94
|
+
formatItemInList={(item: SelectItem) => {
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<span>{item.label}</span>
|
|
98
|
+
<span className='text-sm text-gray-500'>{item.description}</span>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
200
103
|
);
|
|
201
104
|
};
|
|
202
105
|
|
|
@@ -205,12 +108,12 @@ function filterByInputValue(item: SelectItem, inputValue: string | undefined | n
|
|
|
205
108
|
return true;
|
|
206
109
|
}
|
|
207
110
|
return (
|
|
208
|
-
item?.label
|
|
111
|
+
item?.label?.toLowerCase().includes(inputValue.toLowerCase()) ||
|
|
209
112
|
item?.description.toLowerCase().includes(inputValue.toLowerCase())
|
|
210
113
|
);
|
|
211
114
|
}
|
|
212
115
|
|
|
213
|
-
function
|
|
116
|
+
function toSelectItem(locationFilter: LapisLocationFilter, fields: string[]): SelectItem | undefined {
|
|
214
117
|
const concatenatedLocation = concatenateLocation(locationFilter, fields);
|
|
215
118
|
|
|
216
119
|
const lastNonUndefinedField = [...fields]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Feature, Geometry, GeometryObject } from 'geojson';
|
|
2
|
-
import
|
|
2
|
+
import { geoJson, type Layer, type LayerGroup, map } from 'leaflet';
|
|
3
3
|
import type { FunctionComponent } from 'preact';
|
|
4
4
|
import { useEffect, useRef } from 'preact/hooks';
|
|
5
5
|
|
|
@@ -42,7 +42,7 @@ export const SequencesByLocationMap: FunctionComponent<SequencesByLocationMapPro
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const leafletMap =
|
|
45
|
+
const leafletMap = map(ref.current, {
|
|
46
46
|
scrollWheelZoom: enableMapNavigation,
|
|
47
47
|
zoomControl: enableMapNavigation,
|
|
48
48
|
keyboard: enableMapNavigation,
|
|
@@ -52,7 +52,7 @@ export const SequencesByLocationMap: FunctionComponent<SequencesByLocationMapPro
|
|
|
52
52
|
center: [offsetY, offsetX],
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
geoJson(locations, {
|
|
56
56
|
style: (feature: Feature<GeometryObject, EnhancedGeoJsonFeatureProperties> | undefined) => ({
|
|
57
57
|
fillColor: getColor(feature?.properties.data?.proportion),
|
|
58
58
|
fillOpacity: 1,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type LapisTextFilter = Record<string, string | undefined>;
|
|
2
|
+
|
|
3
|
+
export class TextInputChangedEvent extends CustomEvent<LapisTextFilter> {
|
|
4
|
+
constructor(detail: LapisTextFilter) {
|
|
5
|
+
super('gs-text-input-changed', {
|
|
6
|
+
detail,
|
|
7
|
+
bubbles: true,
|
|
8
|
+
composed: true,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
|
+
import { type LapisFilter } from '../../types';
|
|
3
|
+
|
|
4
|
+
export async function fetchStringAutocompleteList({
|
|
5
|
+
lapis,
|
|
6
|
+
field,
|
|
7
|
+
lapisFilter,
|
|
8
|
+
signal,
|
|
9
|
+
}: {
|
|
10
|
+
lapis: string;
|
|
11
|
+
field: string;
|
|
12
|
+
lapisFilter?: LapisFilter;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}) {
|
|
15
|
+
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [field]);
|
|
16
|
+
|
|
17
|
+
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
18
|
+
|
|
19
|
+
return data.map((item) => item[field]).sort();
|
|
20
|
+
}
|