@genspectrum/dashboard-components 1.9.2 → 1.10.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 +55 -7
- package/dist/{NumberRangeFilterChangedEvent-BnPI-Asz.js → NumberRangeFilterChangedEvent-Cdtcp9YL.js} +13 -2
- package/dist/NumberRangeFilterChangedEvent-Cdtcp9YL.js.map +1 -0
- package/dist/components.d.ts +45 -25
- package/dist/components.js +301 -62
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +22 -20
- package/dist/util.js +1 -1
- package/package.json +1 -1
- package/src/preact/components/downshift-combobox.tsx +277 -47
- package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +11 -0
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +2 -2
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +181 -2
- package/src/preact/lineageFilter/lineage-filter.tsx +65 -14
- package/src/utils/gsEventNames.ts +1 -0
- package/src/web-components/input/gs-lineage-filter.spec.ts +30 -0
- package/src/web-components/input/gs-lineage-filter.stories.ts +25 -2
- package/src/web-components/input/gs-lineage-filter.tsx +34 -23
- package/standalone-bundle/dashboard-components.js +6750 -6538
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/dist/NumberRangeFilterChangedEvent-BnPI-Asz.js.map +0 -1
|
@@ -17,7 +17,7 @@ const meta: Meta = {
|
|
|
17
17
|
component: LineageFilter,
|
|
18
18
|
parameters: {
|
|
19
19
|
actions: {
|
|
20
|
-
handles: [gsEventNames.lineageFilterChanged, ...previewHandles],
|
|
20
|
+
handles: [gsEventNames.lineageFilterChanged, gsEventNames.lineageFilterMultiChanged, ...previewHandles],
|
|
21
21
|
},
|
|
22
22
|
fetchMock: {
|
|
23
23
|
mocks: [
|
|
@@ -61,7 +61,7 @@ const meta: Meta = {
|
|
|
61
61
|
},
|
|
62
62
|
value: {
|
|
63
63
|
control: {
|
|
64
|
-
type: '
|
|
64
|
+
type: 'object',
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
width: {
|
|
@@ -79,6 +79,11 @@ const meta: Meta = {
|
|
|
79
79
|
type: 'boolean',
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
|
+
multiSelect: {
|
|
83
|
+
control: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
82
87
|
},
|
|
83
88
|
|
|
84
89
|
args: {
|
|
@@ -90,6 +95,7 @@ const meta: Meta = {
|
|
|
90
95
|
value: 'A.1',
|
|
91
96
|
width: '100%',
|
|
92
97
|
hideCounts: false,
|
|
98
|
+
multiSelect: false,
|
|
93
99
|
},
|
|
94
100
|
};
|
|
95
101
|
|
|
@@ -169,6 +175,40 @@ export const WithNoLapisField: StoryObj<LineageFilterProps> = {
|
|
|
169
175
|
},
|
|
170
176
|
};
|
|
171
177
|
|
|
178
|
+
export const WithStringValueInMultiSelectMode: StoryObj<LineageFilterProps> = {
|
|
179
|
+
...Default,
|
|
180
|
+
args: {
|
|
181
|
+
...Default.args,
|
|
182
|
+
multiSelect: true,
|
|
183
|
+
value: 'A.1',
|
|
184
|
+
},
|
|
185
|
+
play: async ({ canvasElement, step }) => {
|
|
186
|
+
await step('expect error message', async () => {
|
|
187
|
+
await expectInvalidAttributesErrorMessage(
|
|
188
|
+
canvasElement,
|
|
189
|
+
'When multiSelect is true, value must be an array of strings',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const WithArrayValueInSingleSelectMode: StoryObj<LineageFilterProps> = {
|
|
196
|
+
...Default,
|
|
197
|
+
args: {
|
|
198
|
+
...Default.args,
|
|
199
|
+
multiSelect: false,
|
|
200
|
+
value: ['A.1', 'B.1'],
|
|
201
|
+
},
|
|
202
|
+
play: async ({ canvasElement, step }) => {
|
|
203
|
+
await step('expect error message', async () => {
|
|
204
|
+
await expectInvalidAttributesErrorMessage(
|
|
205
|
+
canvasElement,
|
|
206
|
+
'When multiSelect is false or undefined, value must be a string',
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
172
212
|
export const WithHideCountsTrue: StoryObj<LineageFilterProps> = {
|
|
173
213
|
...Default,
|
|
174
214
|
args: {
|
|
@@ -243,6 +283,145 @@ export const EnterAndClearMultipleTimes: StoryObj<LineageFilterProps> = {
|
|
|
243
283
|
},
|
|
244
284
|
};
|
|
245
285
|
|
|
286
|
+
export const MultiSelectDefault: StoryObj<LineageFilterProps> = {
|
|
287
|
+
render: (args) => (
|
|
288
|
+
<LapisUrlContextProvider value={LAPIS_URL}>
|
|
289
|
+
<LineageFilter {...args} />
|
|
290
|
+
</LapisUrlContextProvider>
|
|
291
|
+
),
|
|
292
|
+
args: {
|
|
293
|
+
...Default.args,
|
|
294
|
+
multiSelect: true,
|
|
295
|
+
value: ['A.1', 'B.1'],
|
|
296
|
+
placeholderText: 'Select lineages',
|
|
297
|
+
},
|
|
298
|
+
play: async ({ canvasElement, step }) => {
|
|
299
|
+
const canvas = within(canvasElement);
|
|
300
|
+
const lineageChangedListenerMock = fn();
|
|
301
|
+
|
|
302
|
+
await step('Setup event listener mock', () => {
|
|
303
|
+
canvasElement.addEventListener(gsEventNames.lineageFilterMultiChanged, lineageChangedListenerMock);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await step('multi-select filter is rendered with initial values', async () => {
|
|
307
|
+
await waitFor(async () => {
|
|
308
|
+
await expect(canvas.getByText('A.1')).toBeVisible();
|
|
309
|
+
await expect(canvas.getByText('B.1')).toBeVisible();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await step('add another lineage', async () => {
|
|
314
|
+
const input = await canvas.findByPlaceholderText('Select lineages');
|
|
315
|
+
await userEvent.type(input, 'C.1');
|
|
316
|
+
await userEvent.click(canvas.getByRole('option', { name: 'C.1(23)' }));
|
|
317
|
+
|
|
318
|
+
await waitFor(() => {
|
|
319
|
+
return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
320
|
+
pangoLineage: ['A.1', 'B.1', 'C.1'],
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await step('verify all three lineages are displayed', async () => {
|
|
326
|
+
await expect(canvas.getByText('A.1')).toBeVisible();
|
|
327
|
+
await expect(canvas.getByText('B.1')).toBeVisible();
|
|
328
|
+
await expect(canvas.getByText('C.1')).toBeVisible();
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export const MultiSelectRemoveItem: StoryObj<LineageFilterProps> = {
|
|
334
|
+
render: (args) => (
|
|
335
|
+
<LapisUrlContextProvider value={LAPIS_URL}>
|
|
336
|
+
<LineageFilter {...args} />
|
|
337
|
+
</LapisUrlContextProvider>
|
|
338
|
+
),
|
|
339
|
+
args: {
|
|
340
|
+
...Default.args,
|
|
341
|
+
multiSelect: true,
|
|
342
|
+
value: ['A.1', 'B.1', 'C.1'],
|
|
343
|
+
placeholderText: 'Select lineages',
|
|
344
|
+
},
|
|
345
|
+
play: async ({ canvasElement, step }) => {
|
|
346
|
+
const canvas = within(canvasElement);
|
|
347
|
+
const lineageChangedListenerMock = fn();
|
|
348
|
+
|
|
349
|
+
await step('Setup event listener mock', () => {
|
|
350
|
+
canvasElement.addEventListener(gsEventNames.lineageFilterMultiChanged, lineageChangedListenerMock);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await step('multi-select filter is rendered with three values', async () => {
|
|
354
|
+
await waitFor(async () => {
|
|
355
|
+
await expect(canvas.getByText('A.1')).toBeVisible();
|
|
356
|
+
await expect(canvas.getByText('B.1')).toBeVisible();
|
|
357
|
+
await expect(canvas.getByText('C.1')).toBeVisible();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await step('remove B.1 lineage', async () => {
|
|
362
|
+
const removeButton = canvas.getByLabelText('remove B.1');
|
|
363
|
+
await userEvent.click(removeButton);
|
|
364
|
+
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
367
|
+
pangoLineage: ['A.1', 'C.1'],
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await step('verify B.1 is removed', async () => {
|
|
373
|
+
await expect(canvas.queryByText('B.1')).not.toBeVisible();
|
|
374
|
+
await expect(canvas.getByText('A.1')).toBeVisible();
|
|
375
|
+
await expect(canvas.getByText('C.1')).toBeVisible();
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export const MultiSelectClearAll: StoryObj<LineageFilterProps> = {
|
|
381
|
+
render: (args) => (
|
|
382
|
+
<LapisUrlContextProvider value={LAPIS_URL}>
|
|
383
|
+
<LineageFilter {...args} />
|
|
384
|
+
</LapisUrlContextProvider>
|
|
385
|
+
),
|
|
386
|
+
args: {
|
|
387
|
+
...Default.args,
|
|
388
|
+
multiSelect: true,
|
|
389
|
+
value: ['A.1', 'B.1'],
|
|
390
|
+
placeholderText: 'Select lineages',
|
|
391
|
+
},
|
|
392
|
+
play: async ({ canvasElement, step }) => {
|
|
393
|
+
const canvas = within(canvasElement);
|
|
394
|
+
const lineageChangedListenerMock = fn();
|
|
395
|
+
|
|
396
|
+
await step('Setup event listener mock', () => {
|
|
397
|
+
canvasElement.addEventListener(gsEventNames.lineageFilterMultiChanged, lineageChangedListenerMock);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await step('multi-select filter is rendered with values', async () => {
|
|
401
|
+
await waitFor(async () => {
|
|
402
|
+
await expect(canvas.getByText('A.1')).toBeVisible();
|
|
403
|
+
await expect(canvas.getByText('B.1')).toBeVisible();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await step('clear all selections', async () => {
|
|
408
|
+
const clearButton = canvas.getByLabelText('clear selection');
|
|
409
|
+
await userEvent.click(clearButton);
|
|
410
|
+
|
|
411
|
+
await waitFor(() => {
|
|
412
|
+
return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
413
|
+
pangoLineage: undefined,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await step('verify all chips are removed', async () => {
|
|
419
|
+
await expect(canvas.queryByText('A.1')).not.toBeVisible();
|
|
420
|
+
await expect(canvas.queryByText('B.1')).not.toBeVisible();
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
246
425
|
async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
|
|
247
426
|
const canvas = within(canvasElement);
|
|
248
427
|
|
|
@@ -3,10 +3,10 @@ import { useMemo } from 'preact/hooks';
|
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
|
|
5
5
|
import { useLapisUrl } from '../LapisUrlContext';
|
|
6
|
-
import { LineageFilterChangedEvent } from './LineageFilterChangedEvent';
|
|
7
|
-
import {
|
|
6
|
+
import { LineageFilterChangedEvent, LineageMultiFilterChangedEvent } from './LineageFilterChangedEvent';
|
|
7
|
+
import { type LineageItem, fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
|
|
8
8
|
import { lapisFilterSchema } from '../../types';
|
|
9
|
-
import { DownshiftCombobox } from '../components/downshift-combobox';
|
|
9
|
+
import { DownshiftCombobox, DownshiftMultiCombobox } from '../components/downshift-combobox';
|
|
10
10
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
11
11
|
import { LoadingDisplay } from '../components/loading-display';
|
|
12
12
|
import { ResizeContainer } from '../components/resize-container';
|
|
@@ -15,15 +15,36 @@ import { useQuery } from '../useQuery';
|
|
|
15
15
|
const lineageSelectorPropsSchema = z.object({
|
|
16
16
|
lapisField: z.string().min(1),
|
|
17
17
|
placeholderText: z.string().optional(),
|
|
18
|
-
value: z.string(),
|
|
18
|
+
value: z.union([z.string(), z.array(z.string())]),
|
|
19
19
|
hideCounts: z.boolean().optional(),
|
|
20
|
+
multiSelect: z.boolean().optional(),
|
|
20
21
|
});
|
|
22
|
+
|
|
21
23
|
const lineageFilterInnerPropsSchema = lineageSelectorPropsSchema.extend({
|
|
22
24
|
lapisFilter: lapisFilterSchema,
|
|
23
25
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
const lineageFilterPropsSchema = lineageFilterInnerPropsSchema
|
|
28
|
+
.extend({
|
|
29
|
+
width: z.string(),
|
|
30
|
+
})
|
|
31
|
+
.refine(
|
|
32
|
+
(data) => {
|
|
33
|
+
if (data.multiSelect && typeof data.value === 'string') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (!data.multiSelect && Array.isArray(data.value)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
(data) => ({
|
|
42
|
+
message: data.multiSelect
|
|
43
|
+
? 'When multiSelect is true, value must be an array of strings'
|
|
44
|
+
: 'When multiSelect is false or undefined, value must be a string',
|
|
45
|
+
path: ['value'],
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
27
48
|
|
|
28
49
|
export type LineageFilterInnerProps = z.infer<typeof lineageFilterInnerPropsSchema>;
|
|
29
50
|
export type LineageFilterProps = z.infer<typeof lineageFilterPropsSchema>;
|
|
@@ -48,6 +69,7 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
|
|
|
48
69
|
value,
|
|
49
70
|
lapisFilter,
|
|
50
71
|
hideCounts,
|
|
72
|
+
multiSelect = false,
|
|
51
73
|
}) => {
|
|
52
74
|
const lapisUrl = useLapisUrl();
|
|
53
75
|
|
|
@@ -71,6 +93,7 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
|
|
|
71
93
|
placeholderText={placeholderText}
|
|
72
94
|
data={data}
|
|
73
95
|
hideCounts={hideCounts}
|
|
96
|
+
multiSelect={multiSelect}
|
|
74
97
|
/>
|
|
75
98
|
);
|
|
76
99
|
};
|
|
@@ -81,13 +104,46 @@ const LineageSelector = ({
|
|
|
81
104
|
placeholderText,
|
|
82
105
|
data,
|
|
83
106
|
hideCounts = false,
|
|
107
|
+
multiSelect = false,
|
|
84
108
|
}: LineageSelectorProps & {
|
|
85
109
|
data: LineageItem[];
|
|
86
110
|
}) => {
|
|
111
|
+
const formatItemInList = (item: LineageItem) => (
|
|
112
|
+
<p>
|
|
113
|
+
<span>{item.lineage}</span>
|
|
114
|
+
{!hideCounts && <span className='ml-2 text-gray-500'>({item.count.toLocaleString('en-US')})</span>}
|
|
115
|
+
</p>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const selectedItems = useMemo(() => {
|
|
119
|
+
const valueArray = Array.isArray(value) ? value : [];
|
|
120
|
+
return valueArray
|
|
121
|
+
.map((lineageValue) => data.find((item) => item.lineage === lineageValue))
|
|
122
|
+
.filter((item): item is LineageItem => item !== undefined);
|
|
123
|
+
}, [data, value]);
|
|
124
|
+
|
|
87
125
|
const selectedItem = useMemo(() => {
|
|
88
|
-
|
|
126
|
+
const valueString = typeof value === 'string' ? value : '';
|
|
127
|
+
return data.find((item) => item.lineage === valueString) ?? null;
|
|
89
128
|
}, [data, value]);
|
|
90
129
|
|
|
130
|
+
if (multiSelect) {
|
|
131
|
+
return (
|
|
132
|
+
<DownshiftMultiCombobox
|
|
133
|
+
allItems={data}
|
|
134
|
+
value={selectedItems}
|
|
135
|
+
filterItemsByInputValue={filterByInputValue}
|
|
136
|
+
createEvent={(items) => {
|
|
137
|
+
const lineages = items.length > 0 ? items.map((item) => item.lineage) : undefined;
|
|
138
|
+
return new LineageMultiFilterChangedEvent({ [lapisField]: lineages });
|
|
139
|
+
}}
|
|
140
|
+
itemToString={(item) => item?.lineage ?? ''}
|
|
141
|
+
placeholderText={placeholderText ?? 'Select lineages'}
|
|
142
|
+
formatItemInList={formatItemInList}
|
|
143
|
+
formatSelectedItem={(item: LineageItem) => <span>{item.lineage}</span>}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
91
147
|
return (
|
|
92
148
|
<DownshiftCombobox
|
|
93
149
|
allItems={data}
|
|
@@ -96,12 +152,7 @@ const LineageSelector = ({
|
|
|
96
152
|
createEvent={(item) => new LineageFilterChangedEvent({ [lapisField]: item?.lineage ?? undefined })}
|
|
97
153
|
itemToString={(item) => item?.lineage ?? ''}
|
|
98
154
|
placeholderText={placeholderText}
|
|
99
|
-
formatItemInList={
|
|
100
|
-
<p>
|
|
101
|
-
<span>{item.lineage}</span>
|
|
102
|
-
{!hideCounts && <span className='ml-2 text-gray-500'>({item.count.toLocaleString('en-US')})</span>}
|
|
103
|
-
</p>
|
|
104
|
-
)}
|
|
155
|
+
formatItemInList={formatItemInList}
|
|
105
156
|
/>
|
|
106
157
|
);
|
|
107
158
|
};
|
|
@@ -5,6 +5,7 @@ export const gsEventNames = {
|
|
|
5
5
|
dateRangeOptionChanged: 'gs-date-range-option-changed',
|
|
6
6
|
mutationFilterChanged: 'gs-mutation-filter-changed',
|
|
7
7
|
lineageFilterChanged: 'gs-lineage-filter-changed',
|
|
8
|
+
lineageFilterMultiChanged: 'gs-lineage-filter-multi-changed',
|
|
8
9
|
locationChanged: 'gs-location-changed',
|
|
9
10
|
textFilterChanged: 'gs-text-filter-changed',
|
|
10
11
|
numberRangeFilterChanged: 'gs-number-range-filter-changed',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expectTypeOf, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LineageFilterComponent } from './gs-lineage-filter';
|
|
4
|
+
import { type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
|
|
5
|
+
|
|
6
|
+
describe('gs-lineage-filter types', () => {
|
|
7
|
+
test('should match', () => {
|
|
8
|
+
expectTypeOf(LineageFilterComponent.prototype)
|
|
9
|
+
.toHaveProperty('value')
|
|
10
|
+
.toEqualTypeOf<LineageFilterProps['value']>();
|
|
11
|
+
expectTypeOf(LineageFilterComponent.prototype)
|
|
12
|
+
.toHaveProperty('lapisField')
|
|
13
|
+
.toEqualTypeOf<LineageFilterProps['lapisField']>();
|
|
14
|
+
expectTypeOf(LineageFilterComponent.prototype)
|
|
15
|
+
.toHaveProperty('lapisFilter')
|
|
16
|
+
.toEqualTypeOf<LineageFilterProps['lapisFilter']>();
|
|
17
|
+
expectTypeOf(LineageFilterComponent.prototype)
|
|
18
|
+
.toHaveProperty('placeholderText')
|
|
19
|
+
.toEqualTypeOf<LineageFilterProps['placeholderText']>();
|
|
20
|
+
expectTypeOf(LineageFilterComponent.prototype)
|
|
21
|
+
.toHaveProperty('width')
|
|
22
|
+
.toEqualTypeOf<LineageFilterProps['width']>();
|
|
23
|
+
expectTypeOf(LineageFilterComponent.prototype)
|
|
24
|
+
.toHaveProperty('multiSelect')
|
|
25
|
+
.toEqualTypeOf<LineageFilterProps['multiSelect']>();
|
|
26
|
+
expectTypeOf(LineageFilterComponent.prototype)
|
|
27
|
+
.toHaveProperty('hideCounts')
|
|
28
|
+
.toEqualTypeOf<LineageFilterProps['hideCounts']>();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -28,7 +28,7 @@ const meta: Meta<Required<LineageFilterProps>> = {
|
|
|
28
28
|
component: 'gs-lineage-filter',
|
|
29
29
|
parameters: withComponentDocs({
|
|
30
30
|
actions: {
|
|
31
|
-
handles: [gsEventNames.lineageFilterChanged, ...previewHandles],
|
|
31
|
+
handles: [gsEventNames.lineageFilterChanged, gsEventNames.lineageFilterMultiChanged, ...previewHandles],
|
|
32
32
|
},
|
|
33
33
|
fetchMock: {
|
|
34
34
|
mocks: [
|
|
@@ -79,7 +79,7 @@ const meta: Meta<Required<LineageFilterProps>> = {
|
|
|
79
79
|
},
|
|
80
80
|
value: {
|
|
81
81
|
control: {
|
|
82
|
-
type: '
|
|
82
|
+
type: 'object',
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
85
|
width: {
|
|
@@ -97,6 +97,11 @@ const meta: Meta<Required<LineageFilterProps>> = {
|
|
|
97
97
|
type: 'boolean',
|
|
98
98
|
},
|
|
99
99
|
},
|
|
100
|
+
multiSelect: {
|
|
101
|
+
control: {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
100
105
|
},
|
|
101
106
|
};
|
|
102
107
|
|
|
@@ -113,6 +118,7 @@ const Template: StoryObj<Required<LineageFilterProps>> = {
|
|
|
113
118
|
.hideCounts=${args.hideCounts}
|
|
114
119
|
.value=${args.value}
|
|
115
120
|
.width=${args.width}
|
|
121
|
+
.multiSelect=${args.multiSelect}
|
|
116
122
|
></gs-lineage-filter>
|
|
117
123
|
</div>
|
|
118
124
|
</gs-app>`;
|
|
@@ -126,6 +132,7 @@ const Template: StoryObj<Required<LineageFilterProps>> = {
|
|
|
126
132
|
value: 'B.1.1.7',
|
|
127
133
|
width: '100%',
|
|
128
134
|
hideCounts: false,
|
|
135
|
+
multiSelect: false,
|
|
129
136
|
},
|
|
130
137
|
};
|
|
131
138
|
|
|
@@ -243,3 +250,19 @@ export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
|
|
|
243
250
|
value: '',
|
|
244
251
|
},
|
|
245
252
|
};
|
|
253
|
+
|
|
254
|
+
export const MultiSelectMode: StoryObj<Required<LineageFilterProps>> = {
|
|
255
|
+
...Template,
|
|
256
|
+
args: {
|
|
257
|
+
...Template.args,
|
|
258
|
+
multiSelect: true,
|
|
259
|
+
value: ['B.1.1.7', 'BA.5'],
|
|
260
|
+
placeholderText: 'Select lineages',
|
|
261
|
+
},
|
|
262
|
+
play: async ({ canvasElement }) => {
|
|
263
|
+
const canvas = await withinShadowRoot(canvasElement, 'gs-lineage-filter');
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
return expect(canvas.getByPlaceholderText('Select lineages')).toBeVisible();
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { customElement, property } from 'lit/decorators.js';
|
|
2
2
|
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
type LineageFilterChangedEvent,
|
|
6
|
+
type LineageMultiFilterChangedEvent,
|
|
7
|
+
} from '../../preact/lineageFilter/LineageFilterChangedEvent';
|
|
8
|
+
import { LineageFilter } from '../../preact/lineageFilter/lineage-filter';
|
|
6
9
|
import { type gsEventNames } from '../../utils/gsEventNames';
|
|
7
|
-
import type { Equals, Expect } from '../../utils/typeAssertions';
|
|
8
10
|
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
13
|
* ## Context
|
|
13
14
|
*
|
|
14
15
|
* This component provides a text input field to filter by lineages.
|
|
@@ -19,23 +20,37 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
|
19
20
|
* and provides an autocomplete list with the available values of the lineage and sublineage queries
|
|
20
21
|
* (a `*` appended to the lineage value).
|
|
21
22
|
*
|
|
23
|
+
* When `multiSelect` is true, it allows selecting multiple lineages and displays them as removable chips.
|
|
24
|
+
*
|
|
22
25
|
* @fires {CustomEvent<Record<string, string | undefined>>} gs-lineage-filter-changed
|
|
23
|
-
* Fired when the
|
|
24
|
-
* The `details` of this event contain an object with the `lapisField` as key and the
|
|
26
|
+
* Fired when the selection changes in single-select mode.
|
|
27
|
+
* The `details` of this event contain an object with the `lapisField` as key and the selected value as value.
|
|
25
28
|
* Example:
|
|
26
29
|
* ```
|
|
27
30
|
* {
|
|
28
31
|
* "pangoLineage": "B.1.1.7"
|
|
29
32
|
* }
|
|
30
|
-
*
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @fires {CustomEvent<Record<string, string[] | undefined>>} gs-lineage-filter-multi-changed
|
|
36
|
+
* Fired when the selection changes in multi-select mode.
|
|
37
|
+
* The `details` of this event contain an object with the `lapisField` as key and an array of selected values.
|
|
38
|
+
* Example:
|
|
39
|
+
* ```
|
|
40
|
+
* {
|
|
41
|
+
* "pangoLineage": ["B.1.1.7", "BA.5"]
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
31
44
|
*/
|
|
32
45
|
@customElement('gs-lineage-filter')
|
|
33
46
|
export class LineageFilterComponent extends PreactLitAdapter {
|
|
34
47
|
/**
|
|
35
48
|
* The initial value to use for this lineage filter.
|
|
49
|
+
* Can be a string for single select mode or an array of strings (or comma-separated string) for multi-select mode.
|
|
50
|
+
* Examples: "B.1.1.7" or ["B.1.1.7", "BA.5"] or "B.1.1.7,BA.5"
|
|
36
51
|
*/
|
|
37
|
-
@property()
|
|
38
|
-
value: string = '';
|
|
52
|
+
@property({ type: Array })
|
|
53
|
+
value: string | string[] = '';
|
|
39
54
|
|
|
40
55
|
/**
|
|
41
56
|
* Required.
|
|
@@ -46,6 +61,14 @@ export class LineageFilterComponent extends PreactLitAdapter {
|
|
|
46
61
|
@property()
|
|
47
62
|
lapisField = '';
|
|
48
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Whether to enable multi-select mode.
|
|
66
|
+
* When true, allows selecting multiple lineages displayed as removable chips.
|
|
67
|
+
* Defaults to false.
|
|
68
|
+
*/
|
|
69
|
+
@property({ type: Boolean })
|
|
70
|
+
multiSelect: boolean | undefined = false;
|
|
71
|
+
|
|
49
72
|
/**
|
|
50
73
|
* The filter that is used to fetch the available the autocomplete options.
|
|
51
74
|
* If not set it fetches all available options.
|
|
@@ -89,6 +112,7 @@ export class LineageFilterComponent extends PreactLitAdapter {
|
|
|
89
112
|
value={this.value}
|
|
90
113
|
width={this.width}
|
|
91
114
|
hideCounts={this.hideCounts}
|
|
115
|
+
multiSelect={this.multiSelect}
|
|
92
116
|
/>
|
|
93
117
|
);
|
|
94
118
|
}
|
|
@@ -101,6 +125,7 @@ declare global {
|
|
|
101
125
|
|
|
102
126
|
interface HTMLElementEventMap {
|
|
103
127
|
[gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
|
|
128
|
+
[gsEventNames.lineageFilterMultiChanged]: LineageMultiFilterChangedEvent;
|
|
104
129
|
}
|
|
105
130
|
}
|
|
106
131
|
|
|
@@ -112,17 +137,3 @@ declare global {
|
|
|
112
137
|
}
|
|
113
138
|
}
|
|
114
139
|
}
|
|
115
|
-
|
|
116
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
117
|
-
type InitialValueMatches = Expect<Equals<typeof LineageFilterComponent.prototype.value, LineageFilterProps['value']>>;
|
|
118
|
-
type LapisFieldMatches = Expect<
|
|
119
|
-
Equals<typeof LineageFilterComponent.prototype.lapisField, LineageFilterProps['lapisField']>
|
|
120
|
-
>;
|
|
121
|
-
type LapisFilterMatches = Expect<
|
|
122
|
-
Equals<typeof LineageFilterComponent.prototype.lapisFilter, LineageFilterProps['lapisFilter']>
|
|
123
|
-
>;
|
|
124
|
-
type PlaceholderTextMatches = Expect<
|
|
125
|
-
Equals<typeof LineageFilterComponent.prototype.placeholderText, LineageFilterProps['placeholderText']>
|
|
126
|
-
>;
|
|
127
|
-
type WidthMatches = Expect<Equals<typeof LineageFilterComponent.prototype.width, LineageFilterProps['width']>>;
|
|
128
|
-
/* eslint-enable @typescript-eslint/no-unused-vars */
|