@genspectrum/dashboard-components 1.5.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.
package/dist/util.d.ts CHANGED
@@ -917,7 +917,7 @@ declare global {
917
917
 
918
918
  declare global {
919
919
  interface HTMLElementTagNameMap {
920
- 'gs-genome-data-viewer': GenomeDataViewerComponent;
920
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
921
921
  }
922
922
  }
923
923
 
@@ -925,7 +925,7 @@ declare global {
925
925
  declare global {
926
926
  namespace JSX {
927
927
  interface IntrinsicElements {
928
- 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
928
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
929
929
  }
930
930
  }
931
931
  }
@@ -949,7 +949,7 @@ declare global {
949
949
 
950
950
  declare global {
951
951
  interface HTMLElementTagNameMap {
952
- 'gs-mutations': MutationsComponent;
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-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
960
+ 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
961
961
  }
962
962
  }
963
963
  }
@@ -965,7 +965,7 @@ declare global {
965
965
 
966
966
  declare global {
967
967
  interface HTMLElementTagNameMap {
968
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
968
+ 'gs-mutations': MutationsComponent;
969
969
  }
970
970
  }
971
971
 
@@ -973,7 +973,7 @@ declare global {
973
973
  declare global {
974
974
  namespace JSX {
975
975
  interface IntrinsicElements {
976
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
976
+ 'gs-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
977
977
  }
978
978
  }
979
979
  }
@@ -997,7 +997,7 @@ declare global {
997
997
 
998
998
  declare global {
999
999
  interface HTMLElementTagNameMap {
1000
- 'gs-aggregate': AggregateComponent;
1000
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1001
1001
  }
1002
1002
  }
1003
1003
 
@@ -1005,7 +1005,7 @@ declare global {
1005
1005
  declare global {
1006
1006
  namespace JSX {
1007
1007
  interface IntrinsicElements {
1008
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1008
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1009
1009
  }
1010
1010
  }
1011
1011
  }
@@ -1013,7 +1013,7 @@ declare global {
1013
1013
 
1014
1014
  declare global {
1015
1015
  interface HTMLElementTagNameMap {
1016
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1016
+ 'gs-aggregate': AggregateComponent;
1017
1017
  }
1018
1018
  }
1019
1019
 
@@ -1021,7 +1021,7 @@ declare global {
1021
1021
  declare global {
1022
1022
  namespace JSX {
1023
1023
  interface IntrinsicElements {
1024
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1024
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1025
1025
  }
1026
1026
  }
1027
1027
  }
@@ -1029,7 +1029,7 @@ declare global {
1029
1029
 
1030
1030
  declare global {
1031
1031
  interface HTMLElementTagNameMap {
1032
- 'gs-sequences-by-location': SequencesByLocationComponent;
1032
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1033
1033
  }
1034
1034
  }
1035
1035
 
@@ -1037,7 +1037,7 @@ declare global {
1037
1037
  declare global {
1038
1038
  namespace JSX {
1039
1039
  interface IntrinsicElements {
1040
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1040
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1041
1041
  }
1042
1042
  }
1043
1043
  }
@@ -1045,7 +1045,7 @@ declare global {
1045
1045
 
1046
1046
  declare global {
1047
1047
  interface HTMLElementTagNameMap {
1048
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1048
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1049
1049
  }
1050
1050
  }
1051
1051
 
@@ -1053,7 +1053,7 @@ declare global {
1053
1053
  declare global {
1054
1054
  namespace JSX {
1055
1055
  interface IntrinsicElements {
1056
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1056
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
1057
  }
1058
1058
  }
1059
1059
  }
@@ -1061,7 +1061,7 @@ declare global {
1061
1061
 
1062
1062
  declare global {
1063
1063
  interface HTMLElementTagNameMap {
1064
- 'gs-statistics': StatisticsComponent;
1064
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1065
1065
  }
1066
1066
  }
1067
1067
 
@@ -1069,7 +1069,7 @@ declare global {
1069
1069
  declare global {
1070
1070
  namespace JSX {
1071
1071
  interface IntrinsicElements {
1072
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1072
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1073
1073
  }
1074
1074
  }
1075
1075
  }
@@ -1077,10 +1077,7 @@ declare global {
1077
1077
 
1078
1078
  declare global {
1079
1079
  interface HTMLElementTagNameMap {
1080
- 'gs-location-filter': LocationFilterComponent;
1081
- }
1082
- interface HTMLElementEventMap {
1083
- [gsEventNames.locationChanged]: LocationChangedEvent;
1080
+ 'gs-statistics': StatisticsComponent;
1084
1081
  }
1085
1082
  }
1086
1083
 
@@ -1088,7 +1085,7 @@ declare global {
1088
1085
  declare global {
1089
1086
  namespace JSX {
1090
1087
  interface IntrinsicElements {
1091
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1088
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1092
1089
  }
1093
1090
  }
1094
1091
  }
@@ -1096,7 +1093,11 @@ declare global {
1096
1093
 
1097
1094
  declare global {
1098
1095
  interface HTMLElementTagNameMap {
1099
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1096
+ 'gs-date-range-filter': DateRangeFilterComponent;
1097
+ }
1098
+ interface HTMLElementEventMap {
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-wastewater-mutations-over-time': 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.5.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",
@@ -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
  }
@@ -326,7 +326,7 @@ function parseMutationCode(code: string): SubstitutionClass | DeletionClass {
326
326
  if (maybeSubstitution) {
327
327
  return maybeSubstitution;
328
328
  }
329
- throw Error('Given code is not valid');
329
+ throw Error(`Given code is not valid: ${code}`);
330
330
  }
331
331
 
332
332
  /**
@@ -8,6 +8,11 @@ describe('SubstitutionClass', () => {
8
8
  expect(SubstitutionClass.parse('seg1:A1T')).deep.equal(new SubstitutionClass('seg1', 'A', 'T', 1));
9
9
  });
10
10
 
11
+ it('should be parsed with stop codons', () => {
12
+ expect(SubstitutionClass.parse('S:*1247T')).deep.equal(new SubstitutionClass('S', '*', 'T', 1247));
13
+ expect(SubstitutionClass.parse('S:T1247*')).deep.equal(new SubstitutionClass('S', 'T', '*', 1247));
14
+ });
15
+
11
16
  it('should render to string correctly', () => {
12
17
  const substitutions = [
13
18
  {
@@ -30,6 +35,10 @@ describe('DeletionClass', () => {
30
35
  expect(DeletionClass.parse('seg1:A1-')).deep.equal(new DeletionClass('seg1', 'A', 1));
31
36
  });
32
37
 
38
+ it('should be parsed with stop codons', () => {
39
+ expect(DeletionClass.parse('seg1:*1-')).deep.equal(new DeletionClass('seg1', '*', 1));
40
+ });
41
+
33
42
  it('should render to string correctly', () => {
34
43
  const substitutions = [
35
44
  {
@@ -61,4 +70,8 @@ describe('InsertionClass', () => {
61
70
  expect(InsertionClass.parse('ins_geNe1:1:A')).deep.equal(new InsertionClass('geNe1', 1, 'A'));
62
71
  expect(InsertionClass.parse('ins_1:aA')).deep.equal(new InsertionClass(undefined, 1, 'aA'));
63
72
  });
73
+
74
+ it('should be parsed with stop codon insertion', () => {
75
+ expect(InsertionClass.parse('ins_134:*')).deep.equal(new InsertionClass(undefined, 134, '*'));
76
+ });
64
77
  });
@@ -14,7 +14,7 @@ export interface MutationClass extends Mutation {
14
14
  }
15
15
 
16
16
  export const substitutionRegex =
17
- /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z])?(?<position>\d+)(?<substitutionValue>[A-Z.*])?$/i;
17
+ /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(?<substitutionValue>[A-Z.*])?$/i;
18
18
 
19
19
  export interface Substitution extends Mutation {
20
20
  type: 'substitution';
@@ -68,7 +68,7 @@ export class SubstitutionClass implements MutationClass, Substitution {
68
68
  }
69
69
  }
70
70
 
71
- export const deletionRegex = /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z])?(?<position>\d+)(-)$/i;
71
+ export const deletionRegex = /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(-)$/i;
72
72
 
73
73
  export interface Deletion extends Mutation {
74
74
  type: 'deletion';
@@ -119,7 +119,7 @@ export class DeletionClass implements MutationClass, Deletion {
119
119
  }
120
120
 
121
121
  export const insertionRegexp =
122
- /^ins_((?<segment>[A-Z0-9_-]+)(?=:):)?(?<position>\d+):(?<insertedSymbols>(([A-Z?]|(\.\*))+))$/i;
122
+ /^ins_((?<segment>[A-Z0-9_-]+)(?=:):)?(?<position>\d+):(?<insertedSymbols>(([A-Z?*]|(\.\*))+))$/i;
123
123
 
124
124
  export interface Insertion extends Mutation {
125
125
  type: 'insertion';
@@ -38,6 +38,11 @@ const meta: Meta<MutationFilterProps> = {
38
38
  },
39
39
  },
40
40
  width: { control: 'text' },
41
+ enabledMutationTypes: {
42
+ control: {
43
+ type: 'object',
44
+ },
45
+ },
41
46
  },
42
47
  tags: ['autodocs'],
43
48
  };
@@ -48,7 +53,11 @@ const Template: StoryObj<MutationFilterProps> = {
48
53
  render: (args) => {
49
54
  return html` <gs-app lapis="${LAPIS_URL}">
50
55
  <div class="max-w-(--breakpoint-lg)">
51
- <gs-mutation-filter .initialValue=${args.initialValue} .width=${args.width}></gs-mutation-filter>
56
+ <gs-mutation-filter
57
+ .initialValue=${args.initialValue}
58
+ .width=${args.width}
59
+ .enabledMutationTypes=${args.enabledMutationTypes}
60
+ ></gs-mutation-filter>
52
61
  </div>
53
62
  </gs-app>`;
54
63
  },
@@ -104,6 +113,25 @@ export const FiresFilterChangedEvent: StoryObj<MutationFilterProps> = {
104
113
  },
105
114
  };
106
115
 
116
+ export const RestrictEnabledMutationTypes: StoryObj<MutationFilterProps> = {
117
+ ...Template,
118
+ args: {
119
+ ...Template.args,
120
+ enabledMutationTypes: ['nucleotideMutations', 'aminoAcidMutations'],
121
+ },
122
+ play: async ({ canvasElement }) => {
123
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-filter');
124
+
125
+ const inputField = () => canvas.getByPlaceholderText('Enter a mutation', { exact: false });
126
+
127
+ await waitFor(async () => {
128
+ const placeholderText = inputField().getAttribute('placeholder');
129
+
130
+ await expect(placeholderText).toEqual('Enter a mutation (e.g. 23T, E:57Q)');
131
+ });
132
+ },
133
+ };
134
+
107
135
  export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
108
136
  ...Template,
109
137
  args: {
@@ -2,7 +2,11 @@ import { customElement, property } from 'lit/decorators.js';
2
2
  import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
3
 
4
4
  import { ReferenceGenomesAwaiter } from '../../preact/components/ReferenceGenomesAwaiter';
5
- import { MutationFilter, type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter';
5
+ import {
6
+ MutationFilter,
7
+ type MutationType,
8
+ type MutationFilterProps,
9
+ } from '../../preact/mutationFilter/mutation-filter';
6
10
  import type { MutationsFilter } from '../../types';
7
11
  import { type gsEventNames } from '../../utils/gsEventNames';
8
12
  import type { Equals, Expect } from '../../utils/typeAssertions';
@@ -43,6 +47,11 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
43
47
  *
44
48
  * Examples: `ins_S:614:G`, `ins_614:G`
45
49
  *
50
+ * ### Enabled mutation types
51
+ *
52
+ * After parsing, the entered mutation/insertion also has to match the enabled mutation types,
53
+ * which are configured with the `enabledMutationTypes` attribute.
54
+ *
46
55
  * @fires {CustomEvent<{
47
56
  * nucleotideMutations: string[],
48
57
  * aminoAcidMutations: string[],
@@ -81,10 +90,28 @@ export class MutationFilterComponent extends PreactLitAdapter {
81
90
  @property({ type: String })
82
91
  width: string = '100%';
83
92
 
93
+ /**
94
+ * Which mutation types this input will accept.
95
+ * Any (or all) of the following can be given in a list:
96
+ *
97
+ * - `nucleotideMutations`
98
+ * - `nucleotideInsertions`
99
+ * - `aminoAcidMutations`
100
+ * - `aminoAcidInsertions`
101
+ *
102
+ * By default or if none are given, all types are accepted.
103
+ */
104
+ @property({ type: Object })
105
+ enabledMutationTypes: MutationType[] | undefined = undefined;
106
+
84
107
  override render() {
85
108
  return (
86
109
  <ReferenceGenomesAwaiter>
87
- <MutationFilter initialValue={this.initialValue} width={this.width} />
110
+ <MutationFilter
111
+ initialValue={this.initialValue}
112
+ width={this.width}
113
+ enabledMutationTypes={this.enabledMutationTypes}
114
+ />
88
115
  </ReferenceGenomesAwaiter>
89
116
  );
90
117
  }