@genspectrum/dashboard-components 1.9.2 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +88 -8
- package/dist/{NumberRangeFilterChangedEvent-BnPI-Asz.js → NumberRangeFilterChangedEvent-Cdtcp9YL.js} +13 -2
- package/dist/NumberRangeFilterChangedEvent-Cdtcp9YL.js.map +1 -0
- package/dist/components.d.ts +56 -35
- package/dist/components.js +323 -61
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +32 -30
- package/dist/util.js +1 -1
- package/package.json +1 -1
- package/src/preact/components/downshift-combobox.tsx +277 -47
- package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +2 -0
- 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 +73 -2
- package/src/web-components/input/gs-lineage-filter.tsx +60 -22
- package/src/web-components/input/gs-text-filter.stories.ts +9 -7
- package/standalone-bundle/dashboard-components.js +6765 -6538
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/dist/NumberRangeFilterChangedEvent-BnPI-Asz.js.map +0 -1
package/dist/util.d.ts
CHANGED
|
@@ -135,6 +135,7 @@ export declare const gsEventNames: {
|
|
|
135
135
|
readonly dateRangeOptionChanged: "gs-date-range-option-changed";
|
|
136
136
|
readonly mutationFilterChanged: "gs-mutation-filter-changed";
|
|
137
137
|
readonly lineageFilterChanged: "gs-lineage-filter-changed";
|
|
138
|
+
readonly lineageFilterMultiChanged: "gs-lineage-filter-multi-changed";
|
|
138
139
|
readonly locationChanged: "gs-location-changed";
|
|
139
140
|
readonly textFilterChanged: "gs-text-filter-changed";
|
|
140
141
|
readonly numberRangeFilterChanged: "gs-number-range-filter-changed";
|
|
@@ -939,22 +940,6 @@ declare global {
|
|
|
939
940
|
}
|
|
940
941
|
|
|
941
942
|
|
|
942
|
-
declare global {
|
|
943
|
-
interface HTMLElementTagNameMap {
|
|
944
|
-
'gs-genome-data-viewer': GenomeDataViewerComponent;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
declare global {
|
|
950
|
-
namespace JSX {
|
|
951
|
-
interface IntrinsicElements {
|
|
952
|
-
'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
|
|
958
943
|
declare global {
|
|
959
944
|
interface HTMLElementTagNameMap {
|
|
960
945
|
'gs-mutation-comparison-component': MutationComparisonComponent;
|
|
@@ -1053,7 +1038,7 @@ declare global {
|
|
|
1053
1038
|
|
|
1054
1039
|
declare global {
|
|
1055
1040
|
interface HTMLElementTagNameMap {
|
|
1056
|
-
'gs-
|
|
1041
|
+
'gs-sequences-by-location': SequencesByLocationComponent;
|
|
1057
1042
|
}
|
|
1058
1043
|
}
|
|
1059
1044
|
|
|
@@ -1061,7 +1046,7 @@ declare global {
|
|
|
1061
1046
|
declare global {
|
|
1062
1047
|
namespace JSX {
|
|
1063
1048
|
interface IntrinsicElements {
|
|
1064
|
-
'gs-
|
|
1049
|
+
'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1065
1050
|
}
|
|
1066
1051
|
}
|
|
1067
1052
|
}
|
|
@@ -1069,7 +1054,7 @@ declare global {
|
|
|
1069
1054
|
|
|
1070
1055
|
declare global {
|
|
1071
1056
|
interface HTMLElementTagNameMap {
|
|
1072
|
-
'gs-
|
|
1057
|
+
'gs-mutations-over-time': MutationsOverTimeComponent;
|
|
1073
1058
|
}
|
|
1074
1059
|
}
|
|
1075
1060
|
|
|
@@ -1077,7 +1062,7 @@ declare global {
|
|
|
1077
1062
|
declare global {
|
|
1078
1063
|
namespace JSX {
|
|
1079
1064
|
interface IntrinsicElements {
|
|
1080
|
-
'gs-
|
|
1065
|
+
'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1081
1066
|
}
|
|
1082
1067
|
}
|
|
1083
1068
|
}
|
|
@@ -1085,7 +1070,7 @@ declare global {
|
|
|
1085
1070
|
|
|
1086
1071
|
declare global {
|
|
1087
1072
|
interface HTMLElementTagNameMap {
|
|
1088
|
-
'gs-
|
|
1073
|
+
'gs-genome-data-viewer': GenomeDataViewerComponent;
|
|
1089
1074
|
}
|
|
1090
1075
|
}
|
|
1091
1076
|
|
|
@@ -1093,7 +1078,7 @@ declare global {
|
|
|
1093
1078
|
declare global {
|
|
1094
1079
|
namespace JSX {
|
|
1095
1080
|
interface IntrinsicElements {
|
|
1096
|
-
'gs-
|
|
1081
|
+
'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1097
1082
|
}
|
|
1098
1083
|
}
|
|
1099
1084
|
}
|
|
@@ -1101,7 +1086,7 @@ declare global {
|
|
|
1101
1086
|
|
|
1102
1087
|
declare global {
|
|
1103
1088
|
interface HTMLElementTagNameMap {
|
|
1104
|
-
'gs-
|
|
1089
|
+
'gs-statistics': StatisticsComponent;
|
|
1105
1090
|
}
|
|
1106
1091
|
}
|
|
1107
1092
|
|
|
@@ -1109,7 +1094,7 @@ declare global {
|
|
|
1109
1094
|
declare global {
|
|
1110
1095
|
namespace JSX {
|
|
1111
1096
|
interface IntrinsicElements {
|
|
1112
|
-
'gs-
|
|
1097
|
+
'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1113
1098
|
}
|
|
1114
1099
|
}
|
|
1115
1100
|
}
|
|
@@ -1192,12 +1177,33 @@ declare global {
|
|
|
1192
1177
|
}
|
|
1193
1178
|
|
|
1194
1179
|
|
|
1180
|
+
declare global {
|
|
1181
|
+
interface HTMLElementTagNameMap {
|
|
1182
|
+
'gs-number-range-filter': NumberRangeFilterComponent;
|
|
1183
|
+
}
|
|
1184
|
+
interface HTMLElementEventMap {
|
|
1185
|
+
[gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
|
|
1186
|
+
[gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
declare global {
|
|
1192
|
+
namespace JSX {
|
|
1193
|
+
interface IntrinsicElements {
|
|
1194
|
+
'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
|
|
1195
1200
|
declare global {
|
|
1196
1201
|
interface HTMLElementTagNameMap {
|
|
1197
1202
|
'gs-lineage-filter': LineageFilterComponent;
|
|
1198
1203
|
}
|
|
1199
1204
|
interface HTMLElementEventMap {
|
|
1200
1205
|
[gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
|
|
1206
|
+
[gsEventNames.lineageFilterMultiChanged]: LineageMultiFilterChangedEvent;
|
|
1201
1207
|
}
|
|
1202
1208
|
}
|
|
1203
1209
|
|
|
@@ -1213,11 +1219,7 @@ declare global {
|
|
|
1213
1219
|
|
|
1214
1220
|
declare global {
|
|
1215
1221
|
interface HTMLElementTagNameMap {
|
|
1216
|
-
'gs-
|
|
1217
|
-
}
|
|
1218
|
-
interface HTMLElementEventMap {
|
|
1219
|
-
[gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
|
|
1220
|
-
[gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
|
|
1222
|
+
'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
|
|
1221
1223
|
}
|
|
1222
1224
|
}
|
|
1223
1225
|
|
|
@@ -1225,7 +1227,7 @@ declare global {
|
|
|
1225
1227
|
declare global {
|
|
1226
1228
|
namespace JSX {
|
|
1227
1229
|
interface IntrinsicElements {
|
|
1228
|
-
'gs-
|
|
1230
|
+
'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1229
1231
|
}
|
|
1230
1232
|
}
|
|
1231
1233
|
}
|
package/dist/util.js
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCombobox } from 'downshift/preact';
|
|
1
|
+
import { useCombobox, useMultipleSelection } from 'downshift/preact';
|
|
2
2
|
import { type ComponentChild } from 'preact';
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
4
4
|
|
|
@@ -43,17 +43,7 @@ export function DownshiftCombobox<Item>({
|
|
|
43
43
|
divRef.current?.dispatchEvent(createEvent(item));
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const environment =
|
|
49
|
-
shadowRoot !== undefined
|
|
50
|
-
? {
|
|
51
|
-
addEventListener: window.addEventListener.bind(window),
|
|
52
|
-
removeEventListener: window.removeEventListener.bind(window),
|
|
53
|
-
document: shadowRoot.ownerDocument,
|
|
54
|
-
Node: window.Node,
|
|
55
|
-
}
|
|
56
|
-
: undefined;
|
|
46
|
+
const environment = useShadowEnvironment(divRef);
|
|
57
47
|
|
|
58
48
|
const {
|
|
59
49
|
isOpen,
|
|
@@ -120,44 +110,284 @@ export function DownshiftCombobox<Item>({
|
|
|
120
110
|
{...getInputProps()}
|
|
121
111
|
onBlur={onInputBlur}
|
|
122
112
|
/>
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
className={`px-2 ${inputValue === '' && 'hidden'}`}
|
|
126
|
-
type='button'
|
|
127
|
-
onClick={clearInput}
|
|
128
|
-
tabIndex={-1}
|
|
129
|
-
>
|
|
130
|
-
<DeleteIcon />
|
|
131
|
-
</button>
|
|
132
|
-
<button
|
|
133
|
-
aria-label='toggle menu'
|
|
134
|
-
className='px-2'
|
|
135
|
-
type='button'
|
|
136
|
-
{...getToggleButtonProps()}
|
|
137
|
-
ref={buttonRef}
|
|
138
|
-
>
|
|
139
|
-
{isOpen ? <>↑</> : <>↓</>}
|
|
140
|
-
</button>
|
|
113
|
+
<ClearButton onClick={clearInput} isHidden={inputValue === ''} />
|
|
114
|
+
<ToggleButton isOpen={isOpen} buttonRef={buttonRef} getToggleButtonProps={getToggleButtonProps} />
|
|
141
115
|
</div>
|
|
142
116
|
</div>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
117
|
+
<DropdownMenu
|
|
118
|
+
isOpen={isOpen}
|
|
119
|
+
getMenuProps={getMenuProps}
|
|
120
|
+
items={items}
|
|
121
|
+
highlightedIndex={highlightedIndex}
|
|
122
|
+
getItemProps={getItemProps}
|
|
123
|
+
formatItemInList={formatItemInList}
|
|
124
|
+
itemToString={itemToString}
|
|
125
|
+
selectedItem={selectedItem}
|
|
126
|
+
emptyMessage='No elements to select.'
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function DownshiftMultiCombobox<Item>({
|
|
133
|
+
allItems,
|
|
134
|
+
value,
|
|
135
|
+
filterItemsByInputValue,
|
|
136
|
+
createEvent,
|
|
137
|
+
itemToString,
|
|
138
|
+
placeholderText,
|
|
139
|
+
formatItemInList,
|
|
140
|
+
formatSelectedItem,
|
|
141
|
+
inputClassName = '',
|
|
142
|
+
}: {
|
|
143
|
+
allItems: Item[];
|
|
144
|
+
value: Item[];
|
|
145
|
+
filterItemsByInputValue: (item: Item, value: string) => boolean;
|
|
146
|
+
createEvent: (items: Item[]) => CustomEvent;
|
|
147
|
+
itemToString: (item: Item | undefined | null) => string;
|
|
148
|
+
placeholderText?: string;
|
|
149
|
+
formatItemInList: (item: Item) => ComponentChild;
|
|
150
|
+
formatSelectedItem?: (item: Item) => ComponentChild;
|
|
151
|
+
inputClassName?: string;
|
|
152
|
+
}) {
|
|
153
|
+
const [selectedItems, setSelectedItems] = useState<Item[]>(() => value);
|
|
154
|
+
const [itemsFilter, setItemsFilter] = useState('');
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
setSelectedItems(value);
|
|
158
|
+
}, [value]);
|
|
159
|
+
|
|
160
|
+
const availableItems = useMemo(() => {
|
|
161
|
+
return allItems.filter((item) => {
|
|
162
|
+
const notAlreadySelected = !selectedItems.find(
|
|
163
|
+
(selectedItem) => itemToString(selectedItem) === itemToString(item),
|
|
164
|
+
);
|
|
165
|
+
const matchesFilter = filterItemsByInputValue(item, itemsFilter);
|
|
166
|
+
return notAlreadySelected && matchesFilter;
|
|
167
|
+
});
|
|
168
|
+
}, [allItems, selectedItems, filterItemsByInputValue, itemsFilter, itemToString]);
|
|
169
|
+
|
|
170
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
171
|
+
|
|
172
|
+
const dispatchEvent = (items: Item[]) => {
|
|
173
|
+
setSelectedItems(items);
|
|
174
|
+
divRef.current?.dispatchEvent(createEvent(items));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const environment = useShadowEnvironment(divRef);
|
|
178
|
+
|
|
179
|
+
const { getDropdownProps, removeSelectedItem } = useMultipleSelection({
|
|
180
|
+
selectedItems,
|
|
181
|
+
onStateChange({ selectedItems: newSelectedItems, type }) {
|
|
182
|
+
switch (type) {
|
|
183
|
+
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
|
184
|
+
dispatchEvent(newSelectedItems ?? []);
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
environment,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, closeMenu } =
|
|
194
|
+
useCombobox({
|
|
195
|
+
items: availableItems,
|
|
196
|
+
itemToString(item) {
|
|
197
|
+
return itemToString(item);
|
|
198
|
+
},
|
|
199
|
+
inputValue: itemsFilter,
|
|
200
|
+
onStateChange({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) {
|
|
201
|
+
switch (type) {
|
|
202
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
203
|
+
case useCombobox.stateChangeTypes.ItemClick:
|
|
204
|
+
if (newSelectedItem) {
|
|
205
|
+
dispatchEvent([...selectedItems, newSelectedItem]);
|
|
206
|
+
setItemsFilter('');
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
case useCombobox.stateChangeTypes.InputChange:
|
|
210
|
+
setItemsFilter(newInputValue?.trim() ?? '');
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
stateReducer(state, actionAndChanges) {
|
|
217
|
+
const { changes, type } = actionAndChanges;
|
|
218
|
+
switch (type) {
|
|
219
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
220
|
+
case useCombobox.stateChangeTypes.ItemClick:
|
|
221
|
+
return {
|
|
222
|
+
...changes,
|
|
223
|
+
isOpen: true,
|
|
224
|
+
highlightedIndex: state.highlightedIndex,
|
|
225
|
+
inputValue: '',
|
|
226
|
+
};
|
|
227
|
+
default:
|
|
228
|
+
return changes;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
environment,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const clearAll = () => {
|
|
235
|
+
dispatchEvent([]);
|
|
236
|
+
setItemsFilter('');
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const buttonRef = useRef(null);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div ref={divRef} className={'relative w-full'}>
|
|
243
|
+
<div className='w-full flex flex-col gap-1'>
|
|
244
|
+
<div
|
|
245
|
+
className={`flex gap-1 flex-wrap p-1.5 input min-w-24 h-fit w-full ${inputClassName}`}
|
|
246
|
+
onBlur={(event) => {
|
|
247
|
+
if (event.relatedTarget != buttonRef.current) {
|
|
248
|
+
closeMenu();
|
|
249
|
+
}
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
{selectedItems.map((selectedItem, index) => (
|
|
253
|
+
<span
|
|
254
|
+
key={`${itemToString(selectedItem)}-${index}`}
|
|
255
|
+
className='inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-black rounded'
|
|
153
256
|
>
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
257
|
+
{formatSelectedItem ? formatSelectedItem(selectedItem) : itemToString(selectedItem)}
|
|
258
|
+
<button
|
|
259
|
+
aria-label={`remove ${itemToString(selectedItem)}`}
|
|
260
|
+
className='cursor-pointer hover:text-red-600'
|
|
261
|
+
type='button'
|
|
262
|
+
onClick={() => removeSelectedItem(selectedItem)}
|
|
263
|
+
tabIndex={-1}
|
|
264
|
+
>
|
|
265
|
+
×
|
|
266
|
+
</button>
|
|
267
|
+
</span>
|
|
268
|
+
))}
|
|
269
|
+
<div className='flex gap-0.5 grow min-w-32'>
|
|
270
|
+
<input
|
|
271
|
+
placeholder={placeholderText}
|
|
272
|
+
className='w-full px-1 py-0.5 focus:outline-none min-w-24'
|
|
273
|
+
{...getInputProps(getDropdownProps({ preventKeyAction: isOpen }))}
|
|
274
|
+
/>
|
|
275
|
+
<ClearButton onClick={clearAll} isHidden={selectedItems.length === 0} />
|
|
276
|
+
<ToggleButton
|
|
277
|
+
isOpen={isOpen}
|
|
278
|
+
buttonRef={buttonRef}
|
|
279
|
+
getToggleButtonProps={getToggleButtonProps}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<DropdownMenu
|
|
285
|
+
isOpen={isOpen}
|
|
286
|
+
getMenuProps={getMenuProps}
|
|
287
|
+
items={availableItems}
|
|
288
|
+
highlightedIndex={highlightedIndex}
|
|
289
|
+
getItemProps={getItemProps}
|
|
290
|
+
formatItemInList={formatItemInList}
|
|
291
|
+
itemToString={itemToString}
|
|
292
|
+
selectedItem={selectedItems}
|
|
293
|
+
emptyMessage={selectedItems.length > 0 ? 'No more elements to select.' : 'No elements to select.'}
|
|
294
|
+
/>
|
|
161
295
|
</div>
|
|
162
296
|
);
|
|
163
297
|
}
|
|
298
|
+
|
|
299
|
+
function useShadowEnvironment(divRef: React.RefObject<HTMLDivElement>) {
|
|
300
|
+
const shadowRoot = divRef.current?.shadowRoot ?? undefined;
|
|
301
|
+
|
|
302
|
+
return shadowRoot !== undefined
|
|
303
|
+
? {
|
|
304
|
+
addEventListener: window.addEventListener.bind(window),
|
|
305
|
+
removeEventListener: window.removeEventListener.bind(window),
|
|
306
|
+
document: shadowRoot.ownerDocument,
|
|
307
|
+
Node: window.Node,
|
|
308
|
+
}
|
|
309
|
+
: undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function ToggleButton({
|
|
313
|
+
isOpen,
|
|
314
|
+
buttonRef,
|
|
315
|
+
getToggleButtonProps,
|
|
316
|
+
onClick,
|
|
317
|
+
}: {
|
|
318
|
+
isOpen: boolean;
|
|
319
|
+
buttonRef?: React.Ref<HTMLButtonElement>;
|
|
320
|
+
getToggleButtonProps?: () => Record<string, unknown>;
|
|
321
|
+
onClick?: () => void;
|
|
322
|
+
}) {
|
|
323
|
+
const props = getToggleButtonProps ? getToggleButtonProps() : { onClick };
|
|
324
|
+
return (
|
|
325
|
+
<button aria-label='toggle menu' className='px-2' type='button' {...props} ref={buttonRef}>
|
|
326
|
+
{isOpen ? <>↑</> : <>↓</>}
|
|
327
|
+
</button>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function ClearButton({ onClick, isHidden }: { onClick: () => void; isHidden: boolean }) {
|
|
332
|
+
return (
|
|
333
|
+
<button
|
|
334
|
+
aria-label='clear selection'
|
|
335
|
+
className={`px-2 ${isHidden ? 'hidden' : ''}`}
|
|
336
|
+
type='button'
|
|
337
|
+
onClick={onClick}
|
|
338
|
+
tabIndex={-1}
|
|
339
|
+
>
|
|
340
|
+
<DeleteIcon />
|
|
341
|
+
</button>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function DropdownMenu<Item>({
|
|
346
|
+
isOpen,
|
|
347
|
+
getMenuProps,
|
|
348
|
+
items,
|
|
349
|
+
highlightedIndex,
|
|
350
|
+
getItemProps,
|
|
351
|
+
formatItemInList,
|
|
352
|
+
itemToString,
|
|
353
|
+
selectedItem,
|
|
354
|
+
emptyMessage,
|
|
355
|
+
}: {
|
|
356
|
+
isOpen: boolean;
|
|
357
|
+
getMenuProps: () => Record<string, unknown>;
|
|
358
|
+
items: Item[];
|
|
359
|
+
highlightedIndex: number;
|
|
360
|
+
getItemProps: (options: { item: Item; index: number }) => Record<string, unknown>;
|
|
361
|
+
formatItemInList: (item: Item) => ComponentChild;
|
|
362
|
+
itemToString: (item: Item) => string;
|
|
363
|
+
selectedItem?: Item | Item[] | null;
|
|
364
|
+
emptyMessage: string;
|
|
365
|
+
}) {
|
|
366
|
+
const isItemSelected = (item: Item) => {
|
|
367
|
+
if (Array.isArray(selectedItem)) {
|
|
368
|
+
return selectedItem.some((selected) => itemToString(selected) === itemToString(item));
|
|
369
|
+
}
|
|
370
|
+
return selectedItem !== null && selectedItem !== undefined && itemToString(selectedItem) === itemToString(item);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<ul
|
|
375
|
+
className={`absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32 ${isOpen ? '' : 'hidden'}`}
|
|
376
|
+
{...getMenuProps()}
|
|
377
|
+
>
|
|
378
|
+
{items.length > 0 ? (
|
|
379
|
+
items.map((item, index) => (
|
|
380
|
+
<li
|
|
381
|
+
className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${isItemSelected(item) ? 'font-bold' : ''} py-2 px-3 shadow-xs cursor-pointer`}
|
|
382
|
+
key={itemToString(item)}
|
|
383
|
+
{...getItemProps({ item, index })}
|
|
384
|
+
>
|
|
385
|
+
{formatItemInList(item)}
|
|
386
|
+
</li>
|
|
387
|
+
))
|
|
388
|
+
) : (
|
|
389
|
+
<li className='py-2 px-3 shadow-xs'>{emptyMessage}</li>
|
|
390
|
+
)}
|
|
391
|
+
</ul>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { gsEventNames } from '../../utils/gsEventNames';
|
|
2
2
|
|
|
3
3
|
type LapisLineageFilter = Record<string, string | undefined>;
|
|
4
|
+
type LapisLineageMultiFilter = Record<string, string[] | undefined>;
|
|
4
5
|
|
|
5
6
|
export class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
|
|
6
7
|
constructor(detail: LapisLineageFilter) {
|
|
@@ -11,3 +12,13 @@ export class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
|
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
|
|
16
|
+
export class LineageMultiFilterChangedEvent extends CustomEvent<LapisLineageMultiFilter> {
|
|
17
|
+
constructor(detail: LapisLineageMultiFilter) {
|
|
18
|
+
super(gsEventNames.lineageFilterMultiChanged, {
|
|
19
|
+
detail,
|
|
20
|
+
bubbles: true,
|
|
21
|
+
composed: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -2,6 +2,8 @@ import { fetchLineageDefinition } from '../../lapisApi/lapisApi';
|
|
|
2
2
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
3
3
|
import type { LapisFilter } from '../../types';
|
|
4
4
|
|
|
5
|
+
export type LineageItem = { lineage: string; count: number };
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Generates the autocomplete list for lineage search. It includes lineages with wild cards
|
|
7
9
|
* (i.e. "BA.3.2.1" and "BA.3.2.1*") as well as all prefixes of lineages with an asterisk ("BA.3.2*").
|
|
@@ -56,8 +58,6 @@ export async function fetchLineageAutocompleteList({
|
|
|
56
58
|
});
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
export type LineageItem = { lineage: string; count: number };
|
|
60
|
-
|
|
61
61
|
async function getCountsByLineage({
|
|
62
62
|
lapisUrl,
|
|
63
63
|
lapisField,
|