@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/custom-elements.json +37 -2
- package/dist/assets/{mutationOverTimeWorker-BJ_P2T8Y.js.map → mutationOverTimeWorker-BmB6BvVM.js.map} +1 -1
- package/dist/components.d.ts +48 -27
- package/dist/components.js +73 -26
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +27 -27
- package/package.json +1 -1
- package/src/preact/mutationFilter/mutation-filter-info.tsx +2 -2
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +71 -1
- package/src/preact/mutationFilter/mutation-filter.tsx +65 -24
- package/src/query/queryMutationsOverTime.ts +1 -1
- package/src/utils/mutations.spec.ts +13 -0
- package/src/utils/mutations.ts +3 -3
- package/src/web-components/input/gs-mutation-filter.stories.ts +29 -1
- package/src/web-components/input/gs-mutation-filter.tsx +29 -2
- package/standalone-bundle/assets/{mutationOverTimeWorker-CkeGpKWp.js.map → mutationOverTimeWorker-B_xP8pIC.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +2622 -2592
- package/standalone-bundle/dashboard-components.js.map +1 -1
package/dist/util.d.ts
CHANGED
|
@@ -917,7 +917,7 @@ declare global {
|
|
|
917
917
|
|
|
918
918
|
declare global {
|
|
919
919
|
interface HTMLElementTagNameMap {
|
|
920
|
-
'gs-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1116
|
+
'gs-location-filter': LocationFilterComponent;
|
|
1116
1117
|
}
|
|
1117
1118
|
interface HTMLElementEventMap {
|
|
1118
|
-
[gsEventNames.
|
|
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-
|
|
1127
|
+
'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1128
1128
|
}
|
|
1129
1129
|
}
|
|
1130
1130
|
}
|
package/package.json
CHANGED
|
@@ -139,8 +139,8 @@ const AminoAcidMutationsInfo = () => {
|
|
|
139
139
|
<InfoParagraph>
|
|
140
140
|
An amino acid mutation has the format <b><gene>:<position><base></b> or
|
|
141
141
|
<b><gene>:<base_ref><position><base></b>. A <b><base></b> can be one of
|
|
142
|
-
the 20 amino acid codes. It can also be <b>-</b> for deletion and <b>X</b>
|
|
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
|
|
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({
|
|
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
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 =
|
|
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(
|
|
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
|
});
|
package/src/utils/mutations.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
110
|
+
<MutationFilter
|
|
111
|
+
initialValue={this.initialValue}
|
|
112
|
+
width={this.width}
|
|
113
|
+
enabledMutationTypes={this.enabledMutationTypes}
|
|
114
|
+
/>
|
|
88
115
|
</ReferenceGenomesAwaiter>
|
|
89
116
|
);
|
|
90
117
|
}
|