@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,5 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect, waitFor, within } from '@storybook/test';
|
|
2
|
+
import { expect, fireEvent, fn, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import data from './__mockData__/aggregated_hosts.json';
|
|
5
5
|
import { TextInput, type TextInputProps } from './text-input';
|
|
@@ -23,6 +23,7 @@ const meta: Meta<TextInputProps> = {
|
|
|
23
23
|
url: AGGREGATED_ENDPOINT,
|
|
24
24
|
body: {
|
|
25
25
|
fields: ['host'],
|
|
26
|
+
country: 'Germany',
|
|
26
27
|
},
|
|
27
28
|
},
|
|
28
29
|
response: {
|
|
@@ -36,16 +37,15 @@ const meta: Meta<TextInputProps> = {
|
|
|
36
37
|
argTypes: {
|
|
37
38
|
lapisField: {
|
|
38
39
|
control: {
|
|
39
|
-
type: '
|
|
40
|
+
type: 'text',
|
|
40
41
|
},
|
|
41
|
-
options: ['host'],
|
|
42
42
|
},
|
|
43
43
|
placeholderText: {
|
|
44
44
|
control: {
|
|
45
45
|
type: 'text',
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
|
-
|
|
48
|
+
value: {
|
|
49
49
|
control: {
|
|
50
50
|
type: 'text',
|
|
51
51
|
},
|
|
@@ -55,6 +55,11 @@ const meta: Meta<TextInputProps> = {
|
|
|
55
55
|
type: 'text',
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
|
+
lapisFilter: {
|
|
59
|
+
control: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
58
63
|
},
|
|
59
64
|
};
|
|
60
65
|
|
|
@@ -63,35 +68,50 @@ export default meta;
|
|
|
63
68
|
export const Default: StoryObj<TextInputProps> = {
|
|
64
69
|
render: (args) => (
|
|
65
70
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
66
|
-
<TextInput
|
|
67
|
-
lapisField={args.lapisField}
|
|
68
|
-
placeholderText={args.placeholderText}
|
|
69
|
-
initialValue={args.initialValue}
|
|
70
|
-
width={args.width}
|
|
71
|
-
/>
|
|
71
|
+
<TextInput {...args} />
|
|
72
72
|
</LapisUrlContext.Provider>
|
|
73
73
|
),
|
|
74
74
|
args: {
|
|
75
75
|
lapisField: 'host',
|
|
76
76
|
placeholderText: 'Enter a host name',
|
|
77
|
-
|
|
77
|
+
value: '',
|
|
78
78
|
width: '100%',
|
|
79
|
+
lapisFilter: {
|
|
80
|
+
country: 'Germany',
|
|
81
|
+
},
|
|
79
82
|
},
|
|
80
83
|
};
|
|
81
84
|
|
|
82
|
-
export const
|
|
85
|
+
export const RemoveInitialValue: StoryObj<TextInputProps> = {
|
|
83
86
|
...Default,
|
|
84
87
|
args: {
|
|
85
88
|
...Default.args,
|
|
86
|
-
|
|
89
|
+
value: 'Homo sapiens',
|
|
87
90
|
},
|
|
88
|
-
play: async ({ canvasElement }) => {
|
|
91
|
+
play: async ({ canvasElement, step }) => {
|
|
89
92
|
const canvas = within(canvasElement);
|
|
90
93
|
|
|
94
|
+
const changedListenerMock = fn();
|
|
95
|
+
await step('Setup event listener mock', async () => {
|
|
96
|
+
canvasElement.addEventListener('gs-text-input-changed', changedListenerMock);
|
|
97
|
+
});
|
|
98
|
+
|
|
91
99
|
await waitFor(() => {
|
|
92
100
|
const input = canvas.getByPlaceholderText('Enter a host name', { exact: false });
|
|
93
101
|
expect(input).toHaveValue('Homo sapiens');
|
|
94
102
|
});
|
|
103
|
+
|
|
104
|
+
await step('Remove initial value', async () => {
|
|
105
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'clear selection' }));
|
|
106
|
+
|
|
107
|
+
await expect(changedListenerMock).toHaveBeenCalledWith(
|
|
108
|
+
expect.objectContaining({
|
|
109
|
+
detail: {
|
|
110
|
+
host: undefined,
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
95
115
|
},
|
|
96
116
|
};
|
|
97
117
|
|
|
@@ -1,27 +1,31 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { fetchStringAutocompleteList } from './fetchStringAutocompleteList';
|
|
6
6
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
7
|
+
import { TextInputChangedEvent } from './TextInputChangedEvent';
|
|
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
12
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
10
13
|
import { ResizeContainer } from '../components/resize-container';
|
|
11
14
|
import { useQuery } from '../useQuery';
|
|
12
15
|
|
|
13
|
-
const
|
|
16
|
+
const textSelectorPropsSchema = z.object({
|
|
14
17
|
lapisField: z.string().min(1),
|
|
15
18
|
placeholderText: z.string().optional(),
|
|
16
|
-
|
|
19
|
+
value: z.string().optional(),
|
|
17
20
|
});
|
|
18
|
-
|
|
21
|
+
const textInputInnerPropsSchema = textSelectorPropsSchema.extend({ lapisFilter: lapisFilterSchema });
|
|
19
22
|
const textInputPropsSchema = textInputInnerPropsSchema.extend({
|
|
20
23
|
width: z.string(),
|
|
21
24
|
});
|
|
22
25
|
|
|
23
26
|
export type TextInputInnerProps = z.infer<typeof textInputInnerPropsSchema>;
|
|
24
27
|
export type TextInputProps = z.infer<typeof textInputPropsSchema>;
|
|
28
|
+
type TextSelectorProps = z.infer<typeof textSelectorPropsSchema>;
|
|
25
29
|
|
|
26
30
|
export const TextInput: FunctionComponent<TextInputProps> = (props) => {
|
|
27
31
|
const { width, ...innerProps } = props;
|
|
@@ -36,12 +40,18 @@ export const TextInput: FunctionComponent<TextInputProps> = (props) => {
|
|
|
36
40
|
);
|
|
37
41
|
};
|
|
38
42
|
|
|
39
|
-
const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
|
|
43
|
+
const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
|
|
44
|
+
value,
|
|
45
|
+
lapisField,
|
|
46
|
+
placeholderText,
|
|
47
|
+
lapisFilter,
|
|
48
|
+
}) => {
|
|
40
49
|
const lapis = useContext(LapisUrlContext);
|
|
41
50
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
const { data, error, isLoading } = useQuery(
|
|
52
|
+
() => fetchStringAutocompleteList({ lapis, field: lapisField, lapisFilter }),
|
|
53
|
+
[lapisField, lapis, lapisFilter],
|
|
54
|
+
);
|
|
45
55
|
|
|
46
56
|
if (isLoading) {
|
|
47
57
|
return <LoadingDisplay />;
|
|
@@ -55,43 +65,35 @@ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ lapisField, pl
|
|
|
55
65
|
return <NoDataDisplay />;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (isValidValue(value)) {
|
|
62
|
-
inputRef.current?.dispatchEvent(
|
|
63
|
-
new CustomEvent('gs-text-input-changed', {
|
|
64
|
-
detail: { [lapisField]: value },
|
|
65
|
-
bubbles: true,
|
|
66
|
-
composed: true,
|
|
67
|
-
}),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const isValidValue = (value: string | undefined) => {
|
|
73
|
-
if (value === undefined) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
return data.includes(value);
|
|
77
|
-
};
|
|
68
|
+
return <TextSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
|
|
69
|
+
};
|
|
78
70
|
|
|
71
|
+
const TextSelector = ({
|
|
72
|
+
lapisField,
|
|
73
|
+
value,
|
|
74
|
+
placeholderText,
|
|
75
|
+
data,
|
|
76
|
+
}: TextSelectorProps & {
|
|
77
|
+
data: string[];
|
|
78
|
+
}) => {
|
|
79
79
|
return (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{data.map((item) => (
|
|
92
|
-
<option value={item} key={item} />
|
|
93
|
-
))}
|
|
94
|
-
</datalist>
|
|
95
|
-
</>
|
|
80
|
+
<DownshiftCombobox
|
|
81
|
+
allItems={data}
|
|
82
|
+
value={value}
|
|
83
|
+
filterItemsByInputValue={filterByInputValue}
|
|
84
|
+
createEvent={(item: string | null) => new TextInputChangedEvent({ [lapisField]: item ?? undefined })}
|
|
85
|
+
itemToString={(item: string | undefined | null) => item ?? ''}
|
|
86
|
+
placeholderText={placeholderText}
|
|
87
|
+
formatItemInList={(item: string) => {
|
|
88
|
+
return <span>{item}</span>;
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
96
91
|
);
|
|
97
92
|
};
|
|
93
|
+
|
|
94
|
+
function filterByInputValue(item: string, inputValue: string | undefined | null) {
|
|
95
|
+
if (inputValue === undefined || inputValue === null || inputValue === '') {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
|
|
99
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -28,6 +28,9 @@ export const namedLapisFilterSchema = z.object({
|
|
|
28
28
|
});
|
|
29
29
|
export type NamedLapisFilter = z.infer<typeof namedLapisFilterSchema>;
|
|
30
30
|
|
|
31
|
+
export const lapisLocationFilterSchema = z.record(z.union([z.string(), z.undefined()]));
|
|
32
|
+
export type LapisLocationFilter = z.infer<typeof lapisLocationFilterSchema>;
|
|
33
|
+
|
|
31
34
|
export const temporalGranularitySchema = z.union([
|
|
32
35
|
z.literal('day'),
|
|
33
36
|
z.literal('week'),
|
package/src/utilEntrypoint.ts
CHANGED
|
@@ -34,3 +34,5 @@ export type { ConfidenceIntervalMethod } from './preact/shared/charts/confideceI
|
|
|
34
34
|
export type { AxisMax, YAxisMaxConfig } from './preact/shared/charts/getYAxisMax';
|
|
35
35
|
|
|
36
36
|
export { LocationChangedEvent } from './preact/locationFilter/LocationChangedEvent';
|
|
37
|
+
export { LineageFilterChangedEvent } from './preact/lineageFilter/LineageFilterChangedEvent';
|
|
38
|
+
export { TextInputChangedEvent } from './preact/textInput/TextInputChangedEvent';
|
|
@@ -14,8 +14,9 @@ import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
|
14
14
|
const codeExample = String.raw`
|
|
15
15
|
<gs-lineage-filter
|
|
16
16
|
lapisField="pangoLineage"
|
|
17
|
+
lapisFilter='{"counrty": "Germany"}'
|
|
17
18
|
placeholderText="Enter lineage"
|
|
18
|
-
|
|
19
|
+
value="B.1.1.7"
|
|
19
20
|
width="50%">
|
|
20
21
|
</gs-lineage-filter>`;
|
|
21
22
|
|
|
@@ -34,6 +35,7 @@ const meta: Meta<Required<LineageFilterProps>> = {
|
|
|
34
35
|
url: AGGREGATED_ENDPOINT,
|
|
35
36
|
body: {
|
|
36
37
|
fields: ['pangoLineage'],
|
|
38
|
+
country: 'Germany',
|
|
37
39
|
},
|
|
38
40
|
},
|
|
39
41
|
response: {
|
|
@@ -50,18 +52,47 @@ const meta: Meta<Required<LineageFilterProps>> = {
|
|
|
50
52
|
},
|
|
51
53
|
}),
|
|
52
54
|
tags: ['autodocs'],
|
|
55
|
+
argTypes: {
|
|
56
|
+
lapisField: {
|
|
57
|
+
control: {
|
|
58
|
+
type: 'select',
|
|
59
|
+
},
|
|
60
|
+
options: ['host'],
|
|
61
|
+
},
|
|
62
|
+
placeholderText: {
|
|
63
|
+
control: {
|
|
64
|
+
type: 'text',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
value: {
|
|
68
|
+
control: {
|
|
69
|
+
type: 'text',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
width: {
|
|
73
|
+
control: {
|
|
74
|
+
type: 'text',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
lapisFilter: {
|
|
78
|
+
control: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
53
83
|
};
|
|
54
84
|
|
|
55
85
|
export default meta;
|
|
56
86
|
|
|
57
|
-
|
|
87
|
+
const Template: StoryObj<Required<LineageFilterProps>> = {
|
|
58
88
|
render: (args) => {
|
|
59
89
|
return html` <gs-app lapis="${LAPIS_URL}">
|
|
60
90
|
<div class="max-w-screen-lg">
|
|
61
91
|
<gs-lineage-filter
|
|
62
92
|
.lapisField=${args.lapisField}
|
|
93
|
+
.lapisFilter=${args.lapisFilter}
|
|
63
94
|
.placeholderText=${args.placeholderText}
|
|
64
|
-
.
|
|
95
|
+
.value=${args.value}
|
|
65
96
|
.width=${args.width}
|
|
66
97
|
></gs-lineage-filter>
|
|
67
98
|
</div>
|
|
@@ -69,18 +100,86 @@ export const Default: StoryObj<Required<LineageFilterProps>> = {
|
|
|
69
100
|
},
|
|
70
101
|
args: {
|
|
71
102
|
lapisField: 'pangoLineage',
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
lapisFilter: {
|
|
104
|
+
country: 'Germany',
|
|
105
|
+
},
|
|
106
|
+
placeholderText: 'Enter a lineage',
|
|
107
|
+
value: 'B.1.1.7',
|
|
74
108
|
width: '100%',
|
|
75
109
|
},
|
|
76
110
|
};
|
|
77
111
|
|
|
112
|
+
const aggregatedEndpointMatcher = {
|
|
113
|
+
name: 'pangoLineage',
|
|
114
|
+
url: AGGREGATED_ENDPOINT,
|
|
115
|
+
body: {
|
|
116
|
+
fields: ['pangoLineage'],
|
|
117
|
+
country: 'Germany',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const LineageFilter: StoryObj<Required<LineageFilterProps>> = {
|
|
122
|
+
...Template,
|
|
123
|
+
play: async ({ canvasElement }) => {
|
|
124
|
+
const canvas = await withinShadowRoot(canvasElement, 'gs-lineage-filter');
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
return expect(canvas.getByPlaceholderText('Enter a lineage')).toBeVisible();
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const DelayToShowLoadingState: StoryObj<Required<LineageFilterProps>> = {
|
|
132
|
+
...Template,
|
|
133
|
+
parameters: {
|
|
134
|
+
fetchMock: {
|
|
135
|
+
mocks: [
|
|
136
|
+
{
|
|
137
|
+
matcher: aggregatedEndpointMatcher,
|
|
138
|
+
response: {
|
|
139
|
+
status: 200,
|
|
140
|
+
body: aggregatedData,
|
|
141
|
+
},
|
|
142
|
+
options: {
|
|
143
|
+
delay: 5000,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const FetchingLocationsFails: StoryObj<Required<LineageFilterProps>> = {
|
|
152
|
+
...Template,
|
|
153
|
+
parameters: {
|
|
154
|
+
fetchMock: {
|
|
155
|
+
mocks: [
|
|
156
|
+
{
|
|
157
|
+
matcher: aggregatedEndpointMatcher,
|
|
158
|
+
response: {
|
|
159
|
+
status: 400,
|
|
160
|
+
body: {
|
|
161
|
+
error: { status: 400, detail: 'Dummy error message from mock LAPIS', type: 'about:blank' },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
play: async ({ canvasElement }) => {
|
|
169
|
+
const canvas = await withinShadowRoot(canvasElement, 'gs-lineage-filter');
|
|
170
|
+
|
|
171
|
+
await waitFor(() =>
|
|
172
|
+
expect(canvas.getByText('Oops! Something went wrong.', { exact: false })).toBeInTheDocument(),
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
78
177
|
export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
|
|
79
|
-
...
|
|
178
|
+
...LineageFilter,
|
|
80
179
|
play: async ({ canvasElement, step }) => {
|
|
81
180
|
const canvas = await withinShadowRoot(canvasElement, 'gs-lineage-filter');
|
|
82
181
|
|
|
83
|
-
const inputField = () => canvas.getByPlaceholderText('Enter lineage');
|
|
182
|
+
const inputField = () => canvas.getByPlaceholderText('Enter a lineage');
|
|
84
183
|
const listenerMock = fn();
|
|
85
184
|
await step('Setup event listener mock', async () => {
|
|
86
185
|
canvasElement.addEventListener('gs-lineage-filter-changed', listenerMock);
|
|
@@ -99,38 +198,28 @@ export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
|
|
|
99
198
|
|
|
100
199
|
await step('Empty input', async () => {
|
|
101
200
|
await userEvent.type(inputField(), '{backspace>9/}');
|
|
102
|
-
await
|
|
103
|
-
pangoLineage: undefined,
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await step('Enter a valid lineage value', async () => {
|
|
108
|
-
await userEvent.type(inputField(), 'B.1.1.7');
|
|
201
|
+
await userEvent.click(canvas.getByLabelText('toggle menu'));
|
|
109
202
|
|
|
110
|
-
await
|
|
111
|
-
expect.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}),
|
|
116
|
-
);
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
return expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
205
|
+
pangoLineage: undefined,
|
|
206
|
+
});
|
|
207
|
+
});
|
|
117
208
|
});
|
|
118
209
|
|
|
119
210
|
await step('Enter a valid lineage value', async () => {
|
|
120
|
-
await userEvent.type(inputField(), '{backspace>9/}');
|
|
121
211
|
await userEvent.type(inputField(), 'B.1.1.7*');
|
|
212
|
+
await userEvent.click(canvas.getByRole('option', { name: 'B.1.1.7*' }));
|
|
122
213
|
|
|
123
|
-
await
|
|
124
|
-
expect.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
214
|
+
await waitFor(() => {
|
|
215
|
+
return expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
216
|
+
pangoLineage: 'B.1.1.7*',
|
|
217
|
+
});
|
|
218
|
+
});
|
|
130
219
|
});
|
|
131
220
|
},
|
|
132
221
|
args: {
|
|
133
|
-
...
|
|
134
|
-
|
|
222
|
+
...LineageFilter.args,
|
|
223
|
+
value: '',
|
|
135
224
|
},
|
|
136
225
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { customElement, property } from 'lit/decorators.js';
|
|
2
2
|
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
3
3
|
|
|
4
|
+
import { type LineageFilterChangedEvent } from '../../preact/lineageFilter/LineageFilterChangedEvent';
|
|
4
5
|
import { LineageFilter, type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
|
|
5
6
|
import type { Equals, Expect } from '../../utils/typeAssertions';
|
|
6
7
|
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
@@ -13,11 +14,11 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
|
13
14
|
* Currently, it is designed to work well with Pango Lineages,
|
|
14
15
|
* but it may also be used for other lineage types, if suitable.
|
|
15
16
|
*
|
|
16
|
-
* It fetches all available values of the `lapisField` from the LAPIS instance
|
|
17
|
+
* It fetches all available values of the `lapisField` from the LAPIS instance within the given `lapisFilter`
|
|
17
18
|
* and provides an autocomplete list with the available values of the lineage and sublineage queries
|
|
18
19
|
* (a `*` appended to the lineage value).
|
|
19
20
|
*
|
|
20
|
-
* @fires {CustomEvent<Record<string, string>>} gs-lineage-filter-changed
|
|
21
|
+
* @fires {CustomEvent<Record<string, string | undefined>>} gs-lineage-filter-changed
|
|
21
22
|
* Fired when the input field is changed.
|
|
22
23
|
* The `details` of this event contain an object with the `lapisField` as key and the input value as value.
|
|
23
24
|
* Example:
|
|
@@ -33,7 +34,7 @@ export class LineageFilterComponent extends PreactLitAdapter {
|
|
|
33
34
|
* The initial value to use for this lineage filter.
|
|
34
35
|
*/
|
|
35
36
|
@property()
|
|
36
|
-
|
|
37
|
+
value: string = '';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Required.
|
|
@@ -44,6 +45,19 @@ export class LineageFilterComponent extends PreactLitAdapter {
|
|
|
44
45
|
@property()
|
|
45
46
|
lapisField = '';
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The filter that is used to fetch the available the autocomplete options.
|
|
50
|
+
* If not set it fetches all available options.
|
|
51
|
+
* It must be a valid LAPIS filter object.
|
|
52
|
+
*/
|
|
53
|
+
@property({ type: Object })
|
|
54
|
+
lapisFilter: Record<string, string | string[] | number | null | boolean | undefined> & {
|
|
55
|
+
nucleotideMutations?: string[];
|
|
56
|
+
aminoAcidMutations?: string[];
|
|
57
|
+
nucleotideInsertions?: string[];
|
|
58
|
+
aminoAcidInsertions?: string[];
|
|
59
|
+
} = {};
|
|
60
|
+
|
|
47
61
|
/**
|
|
48
62
|
* The placeholder text to display in the input field.
|
|
49
63
|
*/
|
|
@@ -62,8 +76,9 @@ export class LineageFilterComponent extends PreactLitAdapter {
|
|
|
62
76
|
return (
|
|
63
77
|
<LineageFilter
|
|
64
78
|
lapisField={this.lapisField}
|
|
79
|
+
lapisFilter={this.lapisFilter}
|
|
65
80
|
placeholderText={this.placeholderText}
|
|
66
|
-
|
|
81
|
+
value={this.value}
|
|
67
82
|
width={this.width}
|
|
68
83
|
/>
|
|
69
84
|
);
|
|
@@ -76,7 +91,7 @@ declare global {
|
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
interface HTMLElementEventMap {
|
|
79
|
-
'gs-lineage-filter-changed':
|
|
94
|
+
'gs-lineage-filter-changed': LineageFilterChangedEvent;
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
97
|
|
|
@@ -90,12 +105,13 @@ declare global {
|
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */
|
|
93
|
-
type InitialValueMatches = Expect<
|
|
94
|
-
Equals<typeof LineageFilterComponent.prototype.initialValue, LineageFilterProps['initialValue']>
|
|
95
|
-
>;
|
|
108
|
+
type InitialValueMatches = Expect<Equals<typeof LineageFilterComponent.prototype.value, LineageFilterProps['value']>>;
|
|
96
109
|
type LapisFieldMatches = Expect<
|
|
97
110
|
Equals<typeof LineageFilterComponent.prototype.lapisField, LineageFilterProps['lapisField']>
|
|
98
111
|
>;
|
|
112
|
+
type LapisFilterMatches = Expect<
|
|
113
|
+
Equals<typeof LineageFilterComponent.prototype.lapisFilter, LineageFilterProps['lapisFilter']>
|
|
114
|
+
>;
|
|
99
115
|
type PlaceholderTextMatches = Expect<
|
|
100
116
|
Equals<typeof LineageFilterComponent.prototype.placeholderText, LineageFilterProps['placeholderText']>
|
|
101
117
|
>;
|
|
@@ -15,6 +15,7 @@ import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
|
15
15
|
const codeExample = String.raw`
|
|
16
16
|
<gs-location-filter
|
|
17
17
|
fields='["region", "country"]'
|
|
18
|
+
lapisFilter='{"age": 10}'
|
|
18
19
|
value='{ "region": "Europe", "country": null}'
|
|
19
20
|
width="100%"
|
|
20
21
|
placeholderText="Enter a location"
|
|
@@ -54,6 +55,9 @@ const meta: Meta = {
|
|
|
54
55
|
type: 'text',
|
|
55
56
|
},
|
|
56
57
|
},
|
|
58
|
+
lapisFilter: {
|
|
59
|
+
age: 18,
|
|
60
|
+
},
|
|
57
61
|
},
|
|
58
62
|
tags: ['autodocs'],
|
|
59
63
|
};
|
|
@@ -66,6 +70,7 @@ const Template: StoryObj<LocationFilterProps> = {
|
|
|
66
70
|
<div class="max-w-screen-lg">
|
|
67
71
|
<gs-location-filter
|
|
68
72
|
.fields=${args.fields}
|
|
73
|
+
.lapisFilter=${args.lapisFilter}
|
|
69
74
|
.value=${args.value}
|
|
70
75
|
.width=${args.width}
|
|
71
76
|
placeholderText=${ifDefined(args.placeholderText)}
|
|
@@ -75,6 +80,9 @@ const Template: StoryObj<LocationFilterProps> = {
|
|
|
75
80
|
},
|
|
76
81
|
args: {
|
|
77
82
|
fields: ['region', 'country', 'division', 'location'],
|
|
83
|
+
lapisFilter: {
|
|
84
|
+
age: 18,
|
|
85
|
+
},
|
|
78
86
|
value: undefined,
|
|
79
87
|
width: '100%',
|
|
80
88
|
placeholderText: 'Enter a location',
|
|
@@ -86,6 +94,7 @@ const aggregatedEndpointMatcher = {
|
|
|
86
94
|
url: AGGREGATED_ENDPOINT,
|
|
87
95
|
body: {
|
|
88
96
|
fields: ['region', 'country', 'division', 'location'],
|
|
97
|
+
age: 18,
|
|
89
98
|
},
|
|
90
99
|
};
|
|
91
100
|
|
|
@@ -12,10 +12,11 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
|
12
12
|
* This component provides an input field to specify filters for locations.
|
|
13
13
|
*
|
|
14
14
|
* It expects a list of fields that form a strict hierarchical order, such as continent, country, and city.
|
|
15
|
-
* The component retrieves a list of all possible values for these fields from the Lapis instance
|
|
15
|
+
* The component retrieves a list of all possible values for these fields from the Lapis instance,
|
|
16
|
+
* within the `lapisFilter`.
|
|
16
17
|
* This list is then utilized to display autocomplete suggestions and to validate the input.
|
|
17
18
|
*
|
|
18
|
-
* @fires {CustomEvent<Record<string, string>>} gs-location-changed
|
|
19
|
+
* @fires {CustomEvent<Record<string, string | undefined>>} gs-location-changed
|
|
19
20
|
* Fired when a value from the datalist is selected or when a valid value is typed into the field.
|
|
20
21
|
* The `details` of this event contain an object with all `fields` as keys
|
|
21
22
|
* and the corresponding values as values, even if they are `undefined`.
|
|
@@ -35,7 +36,7 @@ export class LocationFilterComponent extends PreactLitAdapter {
|
|
|
35
36
|
* The initial value to use for this location filter.
|
|
36
37
|
*/
|
|
37
38
|
@property({ type: Object })
|
|
38
|
-
value: Record<string, string |
|
|
39
|
+
value: Record<string, string | undefined> | undefined = undefined;
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Required.
|
|
@@ -48,6 +49,19 @@ export class LocationFilterComponent extends PreactLitAdapter {
|
|
|
48
49
|
@property({ type: Array })
|
|
49
50
|
fields: string[] = [];
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The filter that is used to fetch the available the autocomplete options.
|
|
54
|
+
* If not set it fetches all available options.
|
|
55
|
+
* It must be a valid LAPIS filter object.
|
|
56
|
+
*/
|
|
57
|
+
@property({ type: Object })
|
|
58
|
+
lapisFilter: Record<string, string | string[] | number | null | boolean | undefined> & {
|
|
59
|
+
nucleotideMutations?: string[];
|
|
60
|
+
aminoAcidMutations?: string[];
|
|
61
|
+
nucleotideInsertions?: string[];
|
|
62
|
+
aminoAcidInsertions?: string[];
|
|
63
|
+
} = {};
|
|
64
|
+
|
|
51
65
|
/**
|
|
52
66
|
* The width of the component.
|
|
53
67
|
*
|
|
@@ -67,6 +81,7 @@ export class LocationFilterComponent extends PreactLitAdapter {
|
|
|
67
81
|
<LocationFilter
|
|
68
82
|
value={this.value}
|
|
69
83
|
fields={this.fields}
|
|
84
|
+
lapisFilter={this.lapisFilter}
|
|
70
85
|
width={this.width}
|
|
71
86
|
placeholderText={this.placeholderText}
|
|
72
87
|
/>
|
|
@@ -96,6 +111,9 @@ declare global {
|
|
|
96
111
|
/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */
|
|
97
112
|
type InitialValueMatches = Expect<Equals<typeof LocationFilterComponent.prototype.value, LocationFilterProps['value']>>;
|
|
98
113
|
type FieldsMatches = Expect<Equals<typeof LocationFilterComponent.prototype.fields, LocationFilterProps['fields']>>;
|
|
114
|
+
type LapisFilterMatches = Expect<
|
|
115
|
+
Equals<typeof LocationFilterComponent.prototype.lapisFilter, LocationFilterProps['lapisFilter']>
|
|
116
|
+
>;
|
|
99
117
|
type PlaceholderTextMatches = Expect<
|
|
100
118
|
Equals<typeof LocationFilterComponent.prototype.placeholderText, LocationFilterProps['placeholderText']>
|
|
101
119
|
>;
|