@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.
- package/custom-elements.json +134 -7
- package/dist/assets/{mutationOverTimeWorker-CQxrFo53.js.map → mutationOverTimeWorker-BmB6BvVM.js.map} +1 -1
- package/dist/components.d.ts +52 -11
- package/dist/components.js +189 -45
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +11 -11
- package/package.json +1 -1
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +24 -0
- package/src/preact/lineageFilter/lineage-filter.tsx +13 -2
- package/src/preact/locationFilter/location-filter.stories.tsx +24 -0
- package/src/preact/locationFilter/location-filter.tsx +19 -3
- 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/preact/mutationsOverTime/__mockData__/withGaps.ts +352 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +38 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +10 -0
- package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +2 -0
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +35 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +28 -4
- package/src/preact/textFilter/text-filter.stories.tsx +29 -4
- package/src/preact/textFilter/text-filter.tsx +13 -2
- package/src/query/queryMutationsOverTime.ts +37 -4
- package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +122 -0
- package/src/utils/map2d.spec.ts +30 -0
- package/src/utils/map2d.ts +14 -1
- package/src/utils/mutations.spec.ts +13 -0
- package/src/utils/mutations.ts +3 -3
- package/src/web-components/input/gs-lineage-filter.stories.ts +7 -0
- package/src/web-components/input/gs-lineage-filter.tsx +8 -0
- package/src/web-components/input/gs-location-filter.stories.ts +7 -0
- package/src/web-components/input/gs-location-filter.tsx +9 -1
- package/src/web-components/input/gs-mutation-filter.stories.ts +29 -1
- package/src/web-components/input/gs-mutation-filter.tsx +29 -2
- package/src/web-components/input/gs-text-filter.stories.ts +7 -0
- package/src/web-components/input/gs-text-filter.tsx +8 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +14 -1
- package/src/web-components/visualization/gs-mutations-over-time.tsx +8 -0
- package/standalone-bundle/assets/{mutationOverTimeWorker-CDACUs6w.js.map → mutationOverTimeWorker-B_xP8pIC.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +3666 -3553
- 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-
|
|
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-
|
|
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-
|
|
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
|
}
|
|
@@ -1093,10 +1093,11 @@ declare global {
|
|
|
1093
1093
|
|
|
1094
1094
|
declare global {
|
|
1095
1095
|
interface HTMLElementTagNameMap {
|
|
1096
|
-
'gs-
|
|
1096
|
+
'gs-date-range-filter': DateRangeFilterComponent;
|
|
1097
1097
|
}
|
|
1098
1098
|
interface HTMLElementEventMap {
|
|
1099
|
-
[gsEventNames.
|
|
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
|
@@ -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
|
|
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 = ({
|
|
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
|
|
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><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
|
}
|