@genspectrum/dashboard-components 1.4.0 → 1.6.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.
Files changed (41) hide show
  1. package/custom-elements.json +134 -7
  2. package/dist/assets/{mutationOverTimeWorker-CQxrFo53.js.map → mutationOverTimeWorker-BmB6BvVM.js.map} +1 -1
  3. package/dist/components.d.ts +52 -11
  4. package/dist/components.js +189 -45
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +11 -11
  7. package/package.json +1 -1
  8. package/src/preact/lineageFilter/lineage-filter.stories.tsx +24 -0
  9. package/src/preact/lineageFilter/lineage-filter.tsx +13 -2
  10. package/src/preact/locationFilter/location-filter.stories.tsx +24 -0
  11. package/src/preact/locationFilter/location-filter.tsx +19 -3
  12. package/src/preact/mutationFilter/mutation-filter-info.tsx +2 -2
  13. package/src/preact/mutationFilter/mutation-filter.stories.tsx +71 -1
  14. package/src/preact/mutationFilter/mutation-filter.tsx +65 -24
  15. package/src/preact/mutationsOverTime/__mockData__/withGaps.ts +352 -0
  16. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +38 -0
  17. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +10 -0
  18. package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +2 -0
  19. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +35 -0
  20. package/src/preact/mutationsOverTime/mutations-over-time.tsx +28 -4
  21. package/src/preact/textFilter/text-filter.stories.tsx +29 -4
  22. package/src/preact/textFilter/text-filter.tsx +13 -2
  23. package/src/query/queryMutationsOverTime.ts +37 -4
  24. package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +122 -0
  25. package/src/utils/map2d.spec.ts +30 -0
  26. package/src/utils/map2d.ts +14 -1
  27. package/src/utils/mutations.spec.ts +13 -0
  28. package/src/utils/mutations.ts +3 -3
  29. package/src/web-components/input/gs-lineage-filter.stories.ts +7 -0
  30. package/src/web-components/input/gs-lineage-filter.tsx +8 -0
  31. package/src/web-components/input/gs-location-filter.stories.ts +7 -0
  32. package/src/web-components/input/gs-location-filter.tsx +9 -1
  33. package/src/web-components/input/gs-mutation-filter.stories.ts +29 -1
  34. package/src/web-components/input/gs-mutation-filter.tsx +29 -2
  35. package/src/web-components/input/gs-text-filter.stories.ts +7 -0
  36. package/src/web-components/input/gs-text-filter.tsx +8 -0
  37. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +14 -1
  38. package/src/web-components/visualization/gs-mutations-over-time.tsx +8 -0
  39. package/standalone-bundle/assets/{mutationOverTimeWorker-CDACUs6w.js.map → mutationOverTimeWorker-B_xP8pIC.js.map} +1 -1
  40. package/standalone-bundle/dashboard-components.js +3666 -3553
  41. package/standalone-bundle/dashboard-components.js.map +1 -1
package/dist/util.d.ts CHANGED
@@ -933,7 +933,7 @@ declare global {
933
933
 
934
934
  declare global {
935
935
  interface HTMLElementTagNameMap {
936
- 'gs-genome-data-viewer': GenomeDataViewerComponent;
936
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
937
937
  }
938
938
  }
939
939
 
@@ -941,7 +941,7 @@ declare global {
941
941
  declare global {
942
942
  namespace JSX {
943
943
  interface IntrinsicElements {
944
- 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
944
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
945
945
  }
946
946
  }
947
947
  }
@@ -949,7 +949,7 @@ declare global {
949
949
 
950
950
  declare global {
951
951
  interface HTMLElementTagNameMap {
952
- 'gs-mutation-comparison-component': MutationComparisonComponent;
952
+ 'gs-genome-data-viewer': GenomeDataViewerComponent;
953
953
  }
954
954
  }
955
955
 
@@ -957,7 +957,7 @@ declare global {
957
957
  declare global {
958
958
  namespace JSX {
959
959
  interface IntrinsicElements {
960
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
960
+ 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
961
961
  }
962
962
  }
963
963
  }
@@ -1093,10 +1093,11 @@ declare global {
1093
1093
 
1094
1094
  declare global {
1095
1095
  interface HTMLElementTagNameMap {
1096
- 'gs-location-filter': LocationFilterComponent;
1096
+ 'gs-date-range-filter': DateRangeFilterComponent;
1097
1097
  }
1098
1098
  interface HTMLElementEventMap {
1099
- [gsEventNames.locationChanged]: LocationChangedEvent;
1099
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1100
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1100
1101
  }
1101
1102
  }
1102
1103
 
@@ -1104,7 +1105,7 @@ declare global {
1104
1105
  declare global {
1105
1106
  namespace JSX {
1106
1107
  interface IntrinsicElements {
1107
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1108
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1108
1109
  }
1109
1110
  }
1110
1111
  }
@@ -1112,11 +1113,10 @@ declare global {
1112
1113
 
1113
1114
  declare global {
1114
1115
  interface HTMLElementTagNameMap {
1115
- 'gs-date-range-filter': DateRangeFilterComponent;
1116
+ 'gs-location-filter': LocationFilterComponent;
1116
1117
  }
1117
1118
  interface HTMLElementEventMap {
1118
- [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1119
- [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1119
+ [gsEventNames.locationChanged]: LocationChangedEvent;
1120
1120
  }
1121
1121
  }
1122
1122
 
@@ -1124,7 +1124,7 @@ declare global {
1124
1124
  declare global {
1125
1125
  namespace JSX {
1126
1126
  interface IntrinsicElements {
1127
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1127
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1128
1128
  }
1129
1129
  }
1130
1130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -74,6 +74,11 @@ const meta: Meta = {
74
74
  type: 'object',
75
75
  },
76
76
  },
77
+ hideCounts: {
78
+ control: {
79
+ type: 'boolean',
80
+ },
81
+ },
77
82
  },
78
83
 
79
84
  args: {
@@ -84,6 +89,7 @@ const meta: Meta = {
84
89
  placeholderText: 'Enter a lineage',
85
90
  value: 'A.1',
86
91
  width: '100%',
92
+ hideCounts: false,
87
93
  },
88
94
  };
89
95
 
@@ -163,6 +169,24 @@ export const WithNoLapisField: StoryObj<LineageFilterProps> = {
163
169
  },
164
170
  };
165
171
 
172
+ export const WithHideCountsTrue: StoryObj<LineageFilterProps> = {
173
+ ...Default,
174
+ args: {
175
+ ...Default.args,
176
+ hideCounts: true,
177
+ },
178
+ play: async ({ canvasElement, step }) => {
179
+ const { canvas } = await prepare(canvasElement, step);
180
+
181
+ await step('visible without counts', async () => {
182
+ const input = await inputField(canvas);
183
+ await userEvent.clear(input);
184
+ await userEvent.type(input, 'B.1');
185
+ await expect(canvas.getByRole('option', { name: 'B.1' })).toBeVisible();
186
+ });
187
+ },
188
+ };
189
+
166
190
  async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
167
191
  const canvas = within(canvasElement);
168
192
 
@@ -16,6 +16,7 @@ const lineageSelectorPropsSchema = z.object({
16
16
  lapisField: z.string().min(1),
17
17
  placeholderText: z.string().optional(),
18
18
  value: z.string(),
19
+ hideCounts: z.boolean().optional(),
19
20
  });
20
21
  const lineageFilterInnerPropsSchema = lineageSelectorPropsSchema.extend({
21
22
  lapisFilter: lapisFilterSchema,
@@ -46,6 +47,7 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
46
47
  placeholderText,
47
48
  value,
48
49
  lapisFilter,
50
+ hideCounts,
49
51
  }) => {
50
52
  const lapisUrl = useLapisUrl();
51
53
 
@@ -62,7 +64,15 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
62
64
  throw error;
63
65
  }
64
66
 
65
- return <LineageSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
67
+ return (
68
+ <LineageSelector
69
+ lapisField={lapisField}
70
+ value={value}
71
+ placeholderText={placeholderText}
72
+ data={data}
73
+ hideCounts={hideCounts}
74
+ />
75
+ );
66
76
  };
67
77
 
68
78
  const LineageSelector = ({
@@ -70,6 +80,7 @@ const LineageSelector = ({
70
80
  value,
71
81
  placeholderText,
72
82
  data,
83
+ hideCounts = false,
73
84
  }: LineageSelectorProps & {
74
85
  data: LineageItem[];
75
86
  }) => {
@@ -88,7 +99,7 @@ const LineageSelector = ({
88
99
  formatItemInList={(item: LineageItem) => (
89
100
  <p>
90
101
  <span>{item.lineage}</span>
91
- <span className='ml-2 text-gray-500'>({item.count})</span>
102
+ {!hideCounts && <span className='ml-2 text-gray-500'>({item.count})</span>}
92
103
  </p>
93
104
  )}
94
105
  />
@@ -44,6 +44,7 @@ const meta: Meta<LocationFilterProps> = {
44
44
  lapisFilter: {
45
45
  age: 18,
46
46
  },
47
+ hideCounts: false,
47
48
  },
48
49
  argTypes: {
49
50
  fields: {
@@ -66,6 +67,11 @@ const meta: Meta<LocationFilterProps> = {
66
67
  type: 'text',
67
68
  },
68
69
  },
70
+ hideCounts: {
71
+ control: {
72
+ type: 'boolean',
73
+ },
74
+ },
69
75
  lapisFilter: {
70
76
  control: {
71
77
  type: 'object',
@@ -146,6 +152,24 @@ export const OnBlurInput: StoryObj<LocationFilterProps> = {
146
152
  },
147
153
  };
148
154
 
155
+ export const WithHideCountsTrue: StoryObj<LocationFilterProps> = {
156
+ ...Primary,
157
+ args: {
158
+ ...Primary.args,
159
+ hideCounts: true,
160
+ },
161
+ play: async ({ canvasElement, step }) => {
162
+ const { canvas } = await prepare(canvasElement, step);
163
+
164
+ await step('visible without counts', async () => {
165
+ const input = await inputField(canvas);
166
+ await userEvent.clear(input);
167
+ await userEvent.type(input, 'Adajan');
168
+ await expect(canvas.getByRole('option', { name: 'Adajan Asia / India / Gujarat / Adajan' })).toBeVisible();
169
+ });
170
+ },
171
+ };
172
+
149
173
  const inputField = (canvas: ReturnType<typeof within>) => canvas.findByPlaceholderText('Enter a location');
150
174
 
151
175
  async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
@@ -16,6 +16,7 @@ const locationSelectorPropsSchema = z.object({
16
16
  value: lapisLocationFilterSchema.optional(),
17
17
  placeholderText: z.string().optional(),
18
18
  fields: z.array(z.string()).min(1),
19
+ hideCounts: z.boolean().optional(),
19
20
  });
20
21
  const locationFilterInnerPropsSchema = locationSelectorPropsSchema.extend({ lapisFilter: lapisFilterSchema });
21
22
  const locationFilterPropsSchema = locationFilterInnerPropsSchema.extend({
@@ -39,7 +40,13 @@ export const LocationFilter: FunctionComponent<LocationFilterProps> = (props) =>
39
40
  );
40
41
  };
41
42
 
42
- export const LocationFilterInner = ({ value, fields, placeholderText, lapisFilter }: LocationFilterInnerProps) => {
43
+ export const LocationFilterInner = ({
44
+ value,
45
+ fields,
46
+ placeholderText,
47
+ lapisFilter,
48
+ hideCounts,
49
+ }: LocationFilterInnerProps) => {
43
50
  const lapis = useLapisUrl();
44
51
 
45
52
  const { data, error, isLoading } = useQuery(
@@ -54,7 +61,15 @@ export const LocationFilterInner = ({ value, fields, placeholderText, lapisFilte
54
61
  throw error;
55
62
  }
56
63
 
57
- return <LocationSelector fields={fields} value={value} placeholderText={placeholderText} locationData={data} />;
64
+ return (
65
+ <LocationSelector
66
+ fields={fields}
67
+ value={value}
68
+ placeholderText={placeholderText}
69
+ locationData={data}
70
+ hideCounts={hideCounts}
71
+ />
72
+ );
58
73
  };
59
74
 
60
75
  type SelectItem = {
@@ -69,6 +84,7 @@ const LocationSelector = ({
69
84
  value,
70
85
  placeholderText,
71
86
  locationData,
87
+ hideCounts = false,
72
88
  }: LocationSelectorProps & {
73
89
  locationData: LocationEntry[];
74
90
  }) => {
@@ -96,7 +112,7 @@ const LocationSelector = ({
96
112
  <>
97
113
  <p>
98
114
  <span>{item.label}</span>
99
- <span className='ml-2 text-gray-500'>({item.count})</span>
115
+ {!hideCounts && <span className='ml-2 text-gray-500'>({item.count})</span>}
100
116
  </p>
101
117
  <span className='text-sm text-gray-500'>{item.description}</span>
102
118
  </>
@@ -139,8 +139,8 @@ const AminoAcidMutationsInfo = () => {
139
139
  <InfoParagraph>
140
140
  An amino acid mutation has the format <b>&lt;gene&gt;:&lt;position&gt;&lt;base&gt;</b> or
141
141
  <b>&lt;gene&gt;:&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. A <b>&lt;base&gt;</b> can be one of
142
- the 20 amino acid codes. It can also be <b>-</b> for deletion and <b>X</b> for unknown. Example:{' '}
143
- <ExampleMutation mutationType='substitution' sequenceType='amino acid' />.
142
+ the 20 amino acid codes. It can also be <b>*</b> for a stop codon, <b>-</b> for deletion and <b>X</b>{' '}
143
+ for unknown. Example: <ExampleMutation mutationType='substitution' sequenceType='amino acid' />.
144
144
  </InfoParagraph>
145
145
  <InfoParagraph>
146
146
  Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
@@ -27,6 +27,11 @@ const meta: Meta<MutationFilterProps> = {
27
27
  type: 'object',
28
28
  },
29
29
  },
30
+ enabledMutationTypes: {
31
+ control: {
32
+ type: 'object',
33
+ },
34
+ },
30
35
  },
31
36
  };
32
37
 
@@ -36,7 +41,11 @@ export const Default: StoryObj<MutationFilterProps> = {
36
41
  render: (args) => (
37
42
  <LapisUrlContextProvider value={LAPIS_URL}>
38
43
  <ReferenceGenomeContext.Provider value={referenceGenome}>
39
- <MutationFilter width={args.width} initialValue={args.initialValue} />
44
+ <MutationFilter
45
+ width={args.width}
46
+ initialValue={args.initialValue}
47
+ enabledMutationTypes={args.enabledMutationTypes}
48
+ />
40
49
  </ReferenceGenomeContext.Provider>
41
50
  </LapisUrlContextProvider>
42
51
  ),
@@ -217,6 +226,67 @@ export const IgnoresDuplicatesOnPasteCommaSeparatedList: StoryObj<MutationFilter
217
226
  },
218
227
  };
219
228
 
229
+ export const FiltersOutDisabledMutationTypes: StoryObj<MutationFilterProps> = {
230
+ ...Default,
231
+ args: {
232
+ ...Default.args,
233
+ enabledMutationTypes: ['nucleotideMutations'],
234
+ },
235
+ play: async ({ canvasElement, step }) => {
236
+ const { canvas, changedListenerMock } = await prepare(canvasElement, step);
237
+
238
+ await step('Enters an invalid insertion mutation', async () => {
239
+ await testNoOptionsExist(canvas, 'ins_23:T');
240
+ await expect(changedListenerMock).not.toHaveBeenCalled();
241
+
242
+ await userEvent.type(inputField(canvas), '{backspace>12/}', INPUT_DELAY);
243
+ });
244
+
245
+ await step('Enters an invalid amino acid mutation', async () => {
246
+ await testNoOptionsExist(canvas, 'S:A1234T');
247
+ await expect(changedListenerMock).not.toHaveBeenCalled();
248
+
249
+ await userEvent.type(inputField(canvas), '{backspace>12/}', INPUT_DELAY);
250
+ });
251
+
252
+ await step('Enter a comma separated list of invalid mutations', async () => {
253
+ await pasteMutations(canvas, 'insX, ins_123:AA');
254
+
255
+ await waitFor(() =>
256
+ expect(changedListenerMock).toHaveBeenCalledWith(
257
+ expect.objectContaining({
258
+ detail: {
259
+ nucleotideMutations: [],
260
+ aminoAcidMutations: [],
261
+ nucleotideInsertions: [],
262
+ aminoAcidInsertions: [],
263
+ },
264
+ }),
265
+ ),
266
+ );
267
+
268
+ await userEvent.type(inputField(canvas), '{backspace>24/}', INPUT_DELAY);
269
+ });
270
+
271
+ await step('Enter a valid mutation', async () => {
272
+ await submitMutation(canvas, 'A123T');
273
+
274
+ await waitFor(() =>
275
+ expect(changedListenerMock).toHaveBeenCalledWith(
276
+ expect.objectContaining({
277
+ detail: {
278
+ nucleotideMutations: ['A123T'],
279
+ aminoAcidMutations: [],
280
+ nucleotideInsertions: [],
281
+ aminoAcidInsertions: [],
282
+ },
283
+ }),
284
+ ),
285
+ );
286
+ });
287
+ },
288
+ };
289
+
220
290
  export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
221
291
  ...Default,
222
292
  play: async ({ canvasElement, step }) => {
@@ -1,6 +1,6 @@
1
1
  import { useCombobox, useMultipleSelection } from 'downshift/preact';
2
2
  import { type FunctionComponent } from 'preact';
3
- import { useContext, useMemo, useRef, useState } from 'preact/hooks';
3
+ import { useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';
4
4
  import z from 'zod';
5
5
 
6
6
  import { getExampleMutation } from './ExampleMutation';
@@ -15,8 +15,18 @@ import { ErrorBoundary } from '../components/error-boundary';
15
15
  import { UserFacingError } from '../components/error-display';
16
16
  import { singleGraphColorRGBByName } from '../shared/charts/colors';
17
17
 
18
+ const mutationTypeSchema = z.enum([
19
+ 'nucleotideMutations',
20
+ 'aminoAcidMutations',
21
+ 'nucleotideInsertions',
22
+ 'aminoAcidInsertions',
23
+ ]);
24
+
25
+ export type MutationType = z.infer<typeof mutationTypeSchema>;
26
+
18
27
  const mutationFilterInnerPropsSchema = z.object({
19
28
  initialValue: z.union([mutationsFilterSchema.optional(), z.array(z.string()), z.undefined()]),
29
+ enabledMutationTypes: z.array(mutationTypeSchema).optional(),
20
30
  });
21
31
 
22
32
  const mutationFilterPropsSchema = mutationFilterInnerPropsSchema.extend({
@@ -53,7 +63,7 @@ export type MutationFilterItem =
53
63
  | SelectedAminoAcidInsertion;
54
64
 
55
65
  export const MutationFilter: FunctionComponent<MutationFilterProps> = (props) => {
56
- const { width, initialValue } = props;
66
+ const { width, initialValue, enabledMutationTypes } = props;
57
67
  return (
58
68
  <ErrorBoundary
59
69
  size={{ height: '40px', width }}
@@ -62,20 +72,23 @@ export const MutationFilter: FunctionComponent<MutationFilterProps> = (props) =>
62
72
  componentProps={props}
63
73
  >
64
74
  <div style={{ width }}>
65
- <MutationFilterInner initialValue={initialValue} />
75
+ <MutationFilterInner initialValue={initialValue} enabledMutationTypes={enabledMutationTypes} />
66
76
  </div>
67
77
  </ErrorBoundary>
68
78
  );
69
79
  };
70
80
 
71
- function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
81
+ function MutationFilterInner({
82
+ initialValue,
83
+ enabledMutationTypes = ['nucleotideMutations', 'nucleotideInsertions', 'aminoAcidMutations', 'aminoAcidInsertions'],
84
+ }: MutationFilterInnerProps) {
72
85
  const referenceGenome = useContext(ReferenceGenomeContext);
73
86
  const filterRef = useRef<HTMLDivElement>(null);
74
87
  const [inputValue, setInputValue] = useState('');
75
88
 
76
89
  const initialState = useMemo(() => {
77
- return getInitialState(initialValue, referenceGenome);
78
- }, [initialValue, referenceGenome]);
90
+ return getInitialState(initialValue, referenceGenome, enabledMutationTypes);
91
+ }, [initialValue, referenceGenome, enabledMutationTypes]);
79
92
 
80
93
  const [selectedItems, setSelectedItems] = useState<MutationFilterItem[]>(initialState);
81
94
  const [itemCandidate, setItemCandidate] = useState<MutationFilterItem | null>(null);
@@ -83,6 +96,12 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
83
96
 
84
97
  const items = itemCandidate ? [itemCandidate] : [];
85
98
 
99
+ useEffect(() => {
100
+ setSelectedItems((prevSelectedItems) =>
101
+ prevSelectedItems.filter((mutFilterItem) => enabledMutationTypes.includes(mutFilterItem.type)),
102
+ );
103
+ }, [enabledMutationTypes, selectedItems]);
104
+
86
105
  const fireChangeEvent = (selectedFilters: MutationFilterItem[]) => {
87
106
  const detail = mapToMutationFilterStrings(selectedFilters);
88
107
 
@@ -115,16 +134,24 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
115
134
  const values = newInputValue.split(',').map((value) => {
116
135
  return { value, parsedValue: parseAndValidateMutation(value.trim(), referenceGenome) };
117
136
  });
118
- const validEntries = values.map((value) => value.parsedValue).filter((value) => value !== null);
119
- const invalidInput = values
120
- .filter((value) => value.parsedValue === null)
121
- .map((value) => value.value.trim())
122
- .join(',');
137
+
138
+ const validEntries: MutationFilterItem[] = [];
139
+ const rejected: string[] = [];
140
+
141
+ for (const v of values) {
142
+ if (v.parsedValue === null) {
143
+ rejected.push(v.value.trim());
144
+ } else if (enabledMutationTypes.includes(v.parsedValue.type)) {
145
+ validEntries.push(v.parsedValue);
146
+ } else {
147
+ rejected.push(v.parsedValue.value.code);
148
+ }
149
+ }
123
150
 
124
151
  const selectedItemCandidates = [...selectedItems, ...validEntries];
125
152
 
126
153
  handleSelectedItemsChanged(extractUniqueValues(selectedItemCandidates));
127
- setInputValue(invalidInput);
154
+ setInputValue(rejected.join(','));
128
155
  setItemCandidate(null);
129
156
  } else {
130
157
  setInputValue(newInputValue ?? '');
@@ -133,7 +160,8 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
133
160
  const alreadyExists = selectedItems.find(
134
161
  (selectedItem) => selectedItem.value.code === candidate?.value.code,
135
162
  );
136
- if (!alreadyExists) {
163
+ const allowedType = candidate !== null && enabledMutationTypes.includes(candidate.type);
164
+ if (!alreadyExists && allowedType) {
137
165
  setItemCandidate(candidate);
138
166
  }
139
167
  }
@@ -216,7 +244,7 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
216
244
  })}
217
245
  <div className='flex gap-0.5 grow p-1'>
218
246
  <input
219
- placeholder={getPlaceholder(referenceGenome)}
247
+ placeholder={getPlaceholder(referenceGenome, enabledMutationTypes)}
220
248
  className='w-full focus:outline-none min-w-8'
221
249
  {...getInputProps(getDropdownProps({ preventKeyAction: isOpen }))}
222
250
  onBlur={() => {
@@ -261,7 +289,11 @@ function extractUniqueValues(newSelectedItems: MutationFilterItem[]) {
261
289
  return Array.from(uniqueMutationsMap.values());
262
290
  }
263
291
 
264
- function getInitialState(initialValue: MutationsFilter | string[] | undefined, referenceGenome: ReferenceGenome) {
292
+ function getInitialState(
293
+ initialValue: MutationsFilter | string[] | undefined,
294
+ referenceGenome: ReferenceGenome,
295
+ enabledMutationTypes: MutationType[],
296
+ ) {
265
297
  if (initialValue === undefined) {
266
298
  return [];
267
299
  }
@@ -270,18 +302,27 @@ function getInitialState(initialValue: MutationsFilter | string[] | undefined, r
270
302
 
271
303
  return values
272
304
  .map((value) => parseAndValidateMutation(value, referenceGenome))
273
- .filter((parsedMutation) => parsedMutation !== null);
305
+ .filter((parsedMutation): parsedMutation is MutationFilterItem => parsedMutation !== null)
306
+ .filter((mutation) => enabledMutationTypes.includes(mutation.type));
274
307
  }
275
308
 
276
- function getPlaceholder(referenceGenome: ReferenceGenome) {
277
- const nucleotideSubstitution = getExampleMutation(referenceGenome, 'nucleotide', 'substitution');
278
- const nucleotideInsertion = getExampleMutation(referenceGenome, 'nucleotide', 'insertion');
279
- const aminoAcidSubstitution = getExampleMutation(referenceGenome, 'amino acid', 'substitution');
280
- const aminoAcidInsertion = getExampleMutation(referenceGenome, 'amino acid', 'insertion');
309
+ function getPlaceholder(referenceGenome: ReferenceGenome, enabledMutationTypes: MutationType[]) {
310
+ const exampleMutationList = [];
311
+
312
+ if (enabledMutationTypes.includes('nucleotideMutations')) {
313
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'nucleotide', 'substitution'));
314
+ }
315
+ if (enabledMutationTypes.includes('nucleotideInsertions')) {
316
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'nucleotide', 'insertion'));
317
+ }
318
+ if (enabledMutationTypes.includes('aminoAcidMutations')) {
319
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'amino acid', 'substitution'));
320
+ }
321
+ if (enabledMutationTypes.includes('aminoAcidInsertions')) {
322
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'amino acid', 'insertion'));
323
+ }
281
324
 
282
- const exampleMutations = [nucleotideSubstitution, nucleotideInsertion, aminoAcidSubstitution, aminoAcidInsertion]
283
- .filter((example) => example !== '')
284
- .join(', ');
325
+ const exampleMutations = exampleMutationList.filter((example) => example !== '').join(', ');
285
326
 
286
327
  return `Enter a mutation (e.g. ${exampleMutations})`;
287
328
  }