@genspectrum/dashboard-components 1.7.0 → 1.8.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 +81 -0
- package/dist/components.d.ts +52 -25
- package/dist/components.js +112 -25
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +25 -25
- package/package.json +1 -1
- package/src/preact/MutationLinkTemplateContext.tsx +56 -0
- package/src/preact/components/annotated-mutation.stories.tsx +69 -17
- package/src/preact/components/annotated-mutation.tsx +45 -19
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +3 -0
- package/src/preact/mutations/mutations-table.tsx +3 -0
- package/src/preact/shared/stories/expectMutationAnnotation.ts +3 -1
- package/src/web-components/gs-app.spec-d.ts +7 -0
- package/src/web-components/gs-app.stories.ts +32 -2
- package/src/web-components/gs-app.ts +17 -0
- package/src/web-components/mutation-link-template-context.ts +13 -0
- package/src/web-components/mutationLinks.mdx +27 -0
- package/src/web-components/visualization/gs-mutation-comparison.tsx +18 -8
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +12 -1
- package/src/web-components/visualization/gs-mutations-over-time.tsx +24 -14
- package/src/web-components/visualization/gs-mutations.tsx +19 -9
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +17 -7
- package/standalone-bundle/dashboard-components.js +6351 -6290
- package/standalone-bundle/dashboard-components.js.map +1 -1
package/dist/util.d.ts
CHANGED
|
@@ -989,7 +989,7 @@ declare global {
|
|
|
989
989
|
|
|
990
990
|
declare global {
|
|
991
991
|
interface HTMLElementTagNameMap {
|
|
992
|
-
'gs-
|
|
992
|
+
'gs-prevalence-over-time': PrevalenceOverTimeComponent;
|
|
993
993
|
}
|
|
994
994
|
}
|
|
995
995
|
|
|
@@ -997,7 +997,7 @@ declare global {
|
|
|
997
997
|
declare global {
|
|
998
998
|
namespace JSX {
|
|
999
999
|
interface IntrinsicElements {
|
|
1000
|
-
'gs-
|
|
1000
|
+
'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1001
1001
|
}
|
|
1002
1002
|
}
|
|
1003
1003
|
}
|
|
@@ -1005,7 +1005,7 @@ declare global {
|
|
|
1005
1005
|
|
|
1006
1006
|
declare global {
|
|
1007
1007
|
interface HTMLElementTagNameMap {
|
|
1008
|
-
'gs-
|
|
1008
|
+
'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
|
|
1009
1009
|
}
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
@@ -1013,7 +1013,7 @@ declare global {
|
|
|
1013
1013
|
declare global {
|
|
1014
1014
|
namespace JSX {
|
|
1015
1015
|
interface IntrinsicElements {
|
|
1016
|
-
'gs-
|
|
1016
|
+
'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1017
1017
|
}
|
|
1018
1018
|
}
|
|
1019
1019
|
}
|
|
@@ -1101,11 +1101,7 @@ declare global {
|
|
|
1101
1101
|
|
|
1102
1102
|
declare global {
|
|
1103
1103
|
interface HTMLElementTagNameMap {
|
|
1104
|
-
'gs-
|
|
1105
|
-
}
|
|
1106
|
-
interface HTMLElementEventMap {
|
|
1107
|
-
[gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
|
|
1108
|
-
[gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
|
|
1104
|
+
'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
|
|
1109
1105
|
}
|
|
1110
1106
|
}
|
|
1111
1107
|
|
|
@@ -1113,7 +1109,7 @@ declare global {
|
|
|
1113
1109
|
declare global {
|
|
1114
1110
|
namespace JSX {
|
|
1115
1111
|
interface IntrinsicElements {
|
|
1116
|
-
'gs-
|
|
1112
|
+
'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1117
1113
|
}
|
|
1118
1114
|
}
|
|
1119
1115
|
}
|
|
@@ -1121,10 +1117,11 @@ declare global {
|
|
|
1121
1117
|
|
|
1122
1118
|
declare global {
|
|
1123
1119
|
interface HTMLElementTagNameMap {
|
|
1124
|
-
'gs-
|
|
1120
|
+
'gs-date-range-filter': DateRangeFilterComponent;
|
|
1125
1121
|
}
|
|
1126
1122
|
interface HTMLElementEventMap {
|
|
1127
|
-
[gsEventNames.
|
|
1123
|
+
[gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
|
|
1124
|
+
[gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
|
|
1128
1125
|
}
|
|
1129
1126
|
}
|
|
1130
1127
|
|
|
@@ -1132,7 +1129,7 @@ declare global {
|
|
|
1132
1129
|
declare global {
|
|
1133
1130
|
namespace JSX {
|
|
1134
1131
|
interface IntrinsicElements {
|
|
1135
|
-
'gs-
|
|
1132
|
+
'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1136
1133
|
}
|
|
1137
1134
|
}
|
|
1138
1135
|
}
|
|
@@ -1159,10 +1156,10 @@ declare global {
|
|
|
1159
1156
|
|
|
1160
1157
|
declare global {
|
|
1161
1158
|
interface HTMLElementTagNameMap {
|
|
1162
|
-
'gs-
|
|
1159
|
+
'gs-location-filter': LocationFilterComponent;
|
|
1163
1160
|
}
|
|
1164
1161
|
interface HTMLElementEventMap {
|
|
1165
|
-
[gsEventNames.
|
|
1162
|
+
[gsEventNames.locationChanged]: LocationChangedEvent;
|
|
1166
1163
|
}
|
|
1167
1164
|
}
|
|
1168
1165
|
|
|
@@ -1170,7 +1167,7 @@ declare global {
|
|
|
1170
1167
|
declare global {
|
|
1171
1168
|
namespace JSX {
|
|
1172
1169
|
interface IntrinsicElements {
|
|
1173
|
-
'gs-
|
|
1170
|
+
'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1174
1171
|
}
|
|
1175
1172
|
}
|
|
1176
1173
|
}
|
|
@@ -1178,10 +1175,10 @@ declare global {
|
|
|
1178
1175
|
|
|
1179
1176
|
declare global {
|
|
1180
1177
|
interface HTMLElementTagNameMap {
|
|
1181
|
-
'gs-
|
|
1178
|
+
'gs-mutation-filter': MutationFilterComponent;
|
|
1182
1179
|
}
|
|
1183
1180
|
interface HTMLElementEventMap {
|
|
1184
|
-
[gsEventNames.
|
|
1181
|
+
[gsEventNames.mutationFilterChanged]: CustomEvent<MutationsFilter>;
|
|
1185
1182
|
}
|
|
1186
1183
|
}
|
|
1187
1184
|
|
|
@@ -1189,7 +1186,7 @@ declare global {
|
|
|
1189
1186
|
declare global {
|
|
1190
1187
|
namespace JSX {
|
|
1191
1188
|
interface IntrinsicElements {
|
|
1192
|
-
'gs-
|
|
1189
|
+
'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1193
1190
|
}
|
|
1194
1191
|
}
|
|
1195
1192
|
}
|
|
@@ -1197,11 +1194,10 @@ declare global {
|
|
|
1197
1194
|
|
|
1198
1195
|
declare global {
|
|
1199
1196
|
interface HTMLElementTagNameMap {
|
|
1200
|
-
'gs-
|
|
1197
|
+
'gs-lineage-filter': LineageFilterComponent;
|
|
1201
1198
|
}
|
|
1202
1199
|
interface HTMLElementEventMap {
|
|
1203
|
-
[gsEventNames.
|
|
1204
|
-
[gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
|
|
1200
|
+
[gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
|
|
1205
1201
|
}
|
|
1206
1202
|
}
|
|
1207
1203
|
|
|
@@ -1209,7 +1205,7 @@ declare global {
|
|
|
1209
1205
|
declare global {
|
|
1210
1206
|
namespace JSX {
|
|
1211
1207
|
interface IntrinsicElements {
|
|
1212
|
-
'gs-
|
|
1208
|
+
'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1213
1209
|
}
|
|
1214
1210
|
}
|
|
1215
1211
|
}
|
|
@@ -1217,7 +1213,11 @@ declare global {
|
|
|
1217
1213
|
|
|
1218
1214
|
declare global {
|
|
1219
1215
|
interface HTMLElementTagNameMap {
|
|
1220
|
-
'gs-
|
|
1216
|
+
'gs-number-range-filter': NumberRangeFilterComponent;
|
|
1217
|
+
}
|
|
1218
|
+
interface HTMLElementEventMap {
|
|
1219
|
+
[gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
|
|
1220
|
+
[gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
|
|
1221
1221
|
}
|
|
1222
1222
|
}
|
|
1223
1223
|
|
|
@@ -1225,7 +1225,7 @@ declare global {
|
|
|
1225
1225
|
declare global {
|
|
1226
1226
|
namespace JSX {
|
|
1227
1227
|
interface IntrinsicElements {
|
|
1228
|
-
'gs-
|
|
1228
|
+
'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1229
1229
|
}
|
|
1230
1230
|
}
|
|
1231
1231
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createContext, type Provider } from 'preact';
|
|
2
|
+
import { useContext, useMemo } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import type { SequenceType } from '../types';
|
|
5
|
+
import type { Deletion, Substitution } from '../utils/mutations';
|
|
6
|
+
import { mutationLinkTemplateSchema } from '../web-components/mutation-link-template-context';
|
|
7
|
+
import { ErrorDisplay } from './components/error-display';
|
|
8
|
+
import { ResizeContainer } from './components/resize-container';
|
|
9
|
+
|
|
10
|
+
type MutationLinkTemplate = {
|
|
11
|
+
nucleotideMutation?: string;
|
|
12
|
+
aminoAcidMutation?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const MutationLinkTemplateContext = createContext<MutationLinkTemplate>({
|
|
16
|
+
nucleotideMutation: undefined,
|
|
17
|
+
aminoAcidMutation: undefined,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const MutationLinkTemplateContextProvider: Provider<MutationLinkTemplate> = ({ value, children }) => {
|
|
21
|
+
const parseResult = useMemo(() => mutationLinkTemplateSchema.safeParse(value), [value]);
|
|
22
|
+
|
|
23
|
+
if (!parseResult.success) {
|
|
24
|
+
return (
|
|
25
|
+
<ResizeContainer size={{ width: '100%' }}>
|
|
26
|
+
<ErrorDisplay error={parseResult.error} layout='vertical' />
|
|
27
|
+
</ResizeContainer>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<MutationLinkTemplateContext.Provider value={parseResult.data}>{children}</MutationLinkTemplateContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function useMutationLinkProvider() {
|
|
37
|
+
const linkTemplate = useContext(MutationLinkTemplateContext);
|
|
38
|
+
|
|
39
|
+
return (mutation: Substitution | Deletion, sequenceType: SequenceType) => {
|
|
40
|
+
switch (sequenceType) {
|
|
41
|
+
case 'nucleotide': {
|
|
42
|
+
if (linkTemplate.nucleotideMutation !== undefined) {
|
|
43
|
+
return linkTemplate.nucleotideMutation.replace('{{mutation}}', encodeURIComponent(mutation.code));
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'amino acid': {
|
|
49
|
+
if (linkTemplate.aminoAcidMutation !== undefined) {
|
|
50
|
+
return linkTemplate.aminoAcidMutation.replace('{{mutation}}', encodeURIComponent(mutation.code));
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -2,15 +2,25 @@ import { type Meta, type StoryObj } from '@storybook/preact';
|
|
|
2
2
|
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import { AnnotatedMutation, type AnnotatedMutationProps } from './annotated-mutation';
|
|
5
|
-
import {
|
|
5
|
+
import type { MutationAnnotations } from '../../web-components/mutation-annotations-context';
|
|
6
|
+
import type { MutationLinkTemplate } from '../../web-components/mutation-link-template-context';
|
|
6
7
|
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
8
|
+
import { MutationLinkTemplateContextProvider } from '../MutationLinkTemplateContext';
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
type ContextProps = {
|
|
11
|
+
annotations: MutationAnnotations;
|
|
12
|
+
linkTemplate: MutationLinkTemplate;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type StoryProps = AnnotatedMutationProps & ContextProps;
|
|
16
|
+
|
|
17
|
+
const meta: Meta<StoryProps> = {
|
|
9
18
|
title: 'Component/Annotated Mutation',
|
|
10
19
|
component: AnnotatedMutation,
|
|
11
20
|
parameters: { fetchMock: {} },
|
|
12
21
|
argTypes: {
|
|
13
22
|
annotations: { control: { type: 'object' } },
|
|
23
|
+
linkTemplate: { control: { type: 'object' } },
|
|
14
24
|
mutation: { control: { type: 'object' } },
|
|
15
25
|
sequenceType: {
|
|
16
26
|
options: ['nucleotide', 'amino acid'],
|
|
@@ -21,14 +31,16 @@ const meta: Meta<AnnotatedMutationProps & { annotations: MutationAnnotations }>
|
|
|
21
31
|
|
|
22
32
|
export default meta;
|
|
23
33
|
|
|
24
|
-
export const MutationWithoutAnnotationEntry: StoryObj<
|
|
34
|
+
export const MutationWithoutAnnotationEntry: StoryObj<StoryProps> = {
|
|
25
35
|
render: (args) => {
|
|
26
|
-
const { annotations, ...annotatedMutationsArgs } = args;
|
|
36
|
+
const { annotations, linkTemplate, ...annotatedMutationsArgs } = args;
|
|
27
37
|
|
|
28
38
|
return (
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
39
|
+
<MutationLinkTemplateContextProvider value={linkTemplate}>
|
|
40
|
+
<MutationAnnotationsContextProvider value={annotations}>
|
|
41
|
+
<AnnotatedMutation {...annotatedMutationsArgs} />
|
|
42
|
+
</MutationAnnotationsContextProvider>
|
|
43
|
+
</MutationLinkTemplateContextProvider>
|
|
32
44
|
);
|
|
33
45
|
},
|
|
34
46
|
args: {
|
|
@@ -48,6 +60,7 @@ export const MutationWithoutAnnotationEntry: StoryObj<AnnotatedMutationProps & {
|
|
|
48
60
|
nucleotideMutations: ['123T'],
|
|
49
61
|
},
|
|
50
62
|
],
|
|
63
|
+
linkTemplate: {},
|
|
51
64
|
},
|
|
52
65
|
play: async ({ canvasElement }) => {
|
|
53
66
|
const canvas = within(canvasElement);
|
|
@@ -58,7 +71,7 @@ export const MutationWithoutAnnotationEntry: StoryObj<AnnotatedMutationProps & {
|
|
|
58
71
|
},
|
|
59
72
|
};
|
|
60
73
|
|
|
61
|
-
export const MutationWithAnnotationEntry: StoryObj<
|
|
74
|
+
export const MutationWithAnnotationEntry: StoryObj<StoryProps> = {
|
|
62
75
|
...MutationWithoutAnnotationEntry,
|
|
63
76
|
args: {
|
|
64
77
|
...MutationWithoutAnnotationEntry.args,
|
|
@@ -77,15 +90,13 @@ export const MutationWithAnnotationEntry: StoryObj<AnnotatedMutationProps & { an
|
|
|
77
90
|
await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
|
|
78
91
|
await expect(getAnnotationIndicator(canvas)).toBeVisible();
|
|
79
92
|
|
|
80
|
-
await userEvent.click(canvas.getByText('
|
|
93
|
+
await userEvent.click(canvas.getByText('*'));
|
|
81
94
|
await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
|
|
82
95
|
await expect(canvas.getByRole('link', { name: 'with a link.' })).toBeVisible();
|
|
83
96
|
},
|
|
84
97
|
};
|
|
85
98
|
|
|
86
|
-
export const MutationWithMultipleAnnotationEntries: StoryObj<
|
|
87
|
-
AnnotatedMutationProps & { annotations: MutationAnnotations }
|
|
88
|
-
> = {
|
|
99
|
+
export const MutationWithMultipleAnnotationEntries: StoryObj<StoryProps> = {
|
|
89
100
|
...MutationWithoutAnnotationEntry,
|
|
90
101
|
args: {
|
|
91
102
|
...MutationWithoutAnnotationEntry.args,
|
|
@@ -111,17 +122,16 @@ export const MutationWithMultipleAnnotationEntries: StoryObj<
|
|
|
111
122
|
await expect(getAnnotationIndicator(canvas)).toBeVisible();
|
|
112
123
|
await expect(canvas.queryByText('+')).toBeVisible();
|
|
113
124
|
|
|
114
|
-
await userEvent.click(canvas.getByText('
|
|
125
|
+
await userEvent.click(canvas.getByText('*'));
|
|
115
126
|
await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
|
|
116
127
|
await expect(canvas.queryByText('Another test annotation')).toBeVisible();
|
|
117
128
|
},
|
|
118
129
|
};
|
|
119
130
|
|
|
120
|
-
export const AminoAcidMutationWithAnnotationEntry: StoryObj<
|
|
121
|
-
AnnotatedMutationProps & { annotations: MutationAnnotations }
|
|
122
|
-
> = {
|
|
131
|
+
export const AminoAcidMutationWithAnnotationEntry: StoryObj<StoryProps> = {
|
|
123
132
|
...MutationWithoutAnnotationEntry,
|
|
124
133
|
args: {
|
|
134
|
+
...MutationWithoutAnnotationEntry.args,
|
|
125
135
|
mutation: {
|
|
126
136
|
type: 'substitution',
|
|
127
137
|
code: 'S:A501G',
|
|
@@ -145,7 +155,7 @@ export const AminoAcidMutationWithAnnotationEntry: StoryObj<
|
|
|
145
155
|
await waitFor(() => expect(canvas.getByText('S:A501G')).toBeVisible());
|
|
146
156
|
await expect(getAnnotationIndicator(canvas)).toBeVisible();
|
|
147
157
|
|
|
148
|
-
await userEvent.click(canvas.getByText('
|
|
158
|
+
await userEvent.click(canvas.getByText('*'));
|
|
149
159
|
await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
|
|
150
160
|
},
|
|
151
161
|
};
|
|
@@ -157,3 +167,45 @@ function getAnnotationIndicator(canvas: ReturnType<typeof within>) {
|
|
|
157
167
|
function getAnnotationName(canvas: ReturnType<typeof within>) {
|
|
158
168
|
return canvas.queryByText('Test annotation');
|
|
159
169
|
}
|
|
170
|
+
|
|
171
|
+
export const NucleotideMutationWithLink: StoryObj<StoryProps> = {
|
|
172
|
+
...MutationWithoutAnnotationEntry,
|
|
173
|
+
args: {
|
|
174
|
+
...MutationWithoutAnnotationEntry.args,
|
|
175
|
+
linkTemplate: {
|
|
176
|
+
nucleotideMutation: 'http://foo.com/query?nucMut={{mutation}}',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
play: async ({ canvasElement }) => {
|
|
180
|
+
const canvas = within(canvasElement);
|
|
181
|
+
|
|
182
|
+
await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
|
|
183
|
+
const link = canvas.getByText('A23403G').closest('a');
|
|
184
|
+
void expect(link).toHaveAttribute('href', 'http://foo.com/query?nucMut=A23403G');
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const AminoAcidMutationWithLink: StoryObj<StoryProps> = {
|
|
189
|
+
...MutationWithoutAnnotationEntry,
|
|
190
|
+
args: {
|
|
191
|
+
...MutationWithoutAnnotationEntry.args,
|
|
192
|
+
mutation: {
|
|
193
|
+
type: 'substitution',
|
|
194
|
+
code: 'S:A501G',
|
|
195
|
+
position: 501,
|
|
196
|
+
valueAtReference: 'A',
|
|
197
|
+
substitutionValue: 'G',
|
|
198
|
+
},
|
|
199
|
+
sequenceType: 'amino acid',
|
|
200
|
+
linkTemplate: {
|
|
201
|
+
aminoAcidMutation: 'http://foo.com/query?aaMut={{mutation}}',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
play: async ({ canvasElement }) => {
|
|
205
|
+
const canvas = within(canvasElement);
|
|
206
|
+
|
|
207
|
+
await waitFor(() => expect(canvas.getByText('S:A501G')).toBeVisible());
|
|
208
|
+
const link = canvas.getByText('S:A501G').closest('a');
|
|
209
|
+
void expect(link).toHaveAttribute('href', 'http://foo.com/query?aaMut=S%3AA501G');
|
|
210
|
+
},
|
|
211
|
+
};
|
|
@@ -7,6 +7,7 @@ import type { Deletion, Substitution } from '../../utils/mutations';
|
|
|
7
7
|
import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
|
|
8
8
|
import { InfoHeadline1, InfoHeadline2, InfoParagraph } from './info';
|
|
9
9
|
import { ButtonWithModalDialog, useModalRef } from './modal';
|
|
10
|
+
import { useMutationLinkProvider } from '../MutationLinkTemplateContext';
|
|
10
11
|
|
|
11
12
|
export type AnnotatedMutationProps = {
|
|
12
13
|
mutation: Substitution | Deletion;
|
|
@@ -15,13 +16,22 @@ export type AnnotatedMutationProps = {
|
|
|
15
16
|
|
|
16
17
|
export const AnnotatedMutation: FunctionComponent<AnnotatedMutationProps> = (props) => {
|
|
17
18
|
const annotationsProvider = useMutationAnnotationsProvider();
|
|
19
|
+
const linkProvider = useMutationLinkProvider();
|
|
18
20
|
const modalRef = useModalRef();
|
|
19
21
|
|
|
20
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
<AnnotatedMutationWithoutContext
|
|
24
|
+
{...props}
|
|
25
|
+
annotationsProvider={annotationsProvider}
|
|
26
|
+
linkProvider={linkProvider}
|
|
27
|
+
modalRef={modalRef}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
21
30
|
};
|
|
22
31
|
|
|
23
32
|
type GridJsAnnotatedMutationProps = AnnotatedMutationProps & {
|
|
24
33
|
annotationsProvider: ReturnType<typeof useMutationAnnotationsProvider>;
|
|
34
|
+
linkProvider: ReturnType<typeof useMutationLinkProvider>;
|
|
25
35
|
};
|
|
26
36
|
|
|
27
37
|
/**
|
|
@@ -43,12 +53,26 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
|
|
|
43
53
|
mutation,
|
|
44
54
|
sequenceType,
|
|
45
55
|
annotationsProvider,
|
|
56
|
+
linkProvider,
|
|
46
57
|
modalRef,
|
|
47
58
|
}) => {
|
|
59
|
+
const link = linkProvider(mutation, sequenceType);
|
|
60
|
+
let innerLabel = <>{mutation.code}</>;
|
|
61
|
+
if (link !== undefined) {
|
|
62
|
+
innerLabel = (
|
|
63
|
+
<a
|
|
64
|
+
className='hover:text-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-300 underline'
|
|
65
|
+
href={link}
|
|
66
|
+
>
|
|
67
|
+
{mutation.code}
|
|
68
|
+
</a>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
48
72
|
const mutationAnnotations = annotationsProvider(mutation, sequenceType);
|
|
49
73
|
|
|
50
74
|
if (mutationAnnotations === undefined || mutationAnnotations.length === 0) {
|
|
51
|
-
return
|
|
75
|
+
return innerLabel;
|
|
52
76
|
}
|
|
53
77
|
|
|
54
78
|
const modalContent = (
|
|
@@ -66,22 +90,24 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
|
|
|
66
90
|
);
|
|
67
91
|
|
|
68
92
|
return (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
<>
|
|
94
|
+
{innerLabel}
|
|
95
|
+
<ButtonWithModalDialog
|
|
96
|
+
buttonClassName={'select-text cursor-pointer'}
|
|
97
|
+
modalContent={modalContent}
|
|
98
|
+
modalRef={modalRef}
|
|
99
|
+
>
|
|
100
|
+
<sup className='hover:underline focus-visible:underline decoration-red-600'>
|
|
101
|
+
{mutationAnnotations
|
|
102
|
+
.map((annotation) => annotation.symbol)
|
|
103
|
+
.map((symbol, index) => (
|
|
104
|
+
<Fragment key={symbol}>
|
|
105
|
+
<span className='text-red-600'>{symbol}</span>
|
|
106
|
+
{index !== mutationAnnotations.length - 1 && ','}
|
|
107
|
+
</Fragment>
|
|
108
|
+
))}
|
|
109
|
+
</sup>
|
|
110
|
+
</ButtonWithModalDialog>
|
|
111
|
+
</>
|
|
86
112
|
);
|
|
87
113
|
};
|
|
@@ -6,6 +6,7 @@ import { type Dataset } from '../../operator/Dataset';
|
|
|
6
6
|
import type { SequenceType } from '../../types';
|
|
7
7
|
import { type DeletionClass, type SubstitutionClass } from '../../utils/mutations';
|
|
8
8
|
import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
|
|
9
|
+
import { useMutationLinkProvider } from '../MutationLinkTemplateContext';
|
|
9
10
|
import { GridJsAnnotatedMutation } from '../components/annotated-mutation';
|
|
10
11
|
import { type ProportionInterval } from '../components/proportion-selector';
|
|
11
12
|
import { Table } from '../components/table';
|
|
@@ -26,6 +27,7 @@ export const MutationComparisonTable: FunctionComponent<MutationsTableProps> = (
|
|
|
26
27
|
sequenceType,
|
|
27
28
|
}) => {
|
|
28
29
|
const annotationsProvider = useMutationAnnotationsProvider();
|
|
30
|
+
const linkProvider = useMutationLinkProvider();
|
|
29
31
|
|
|
30
32
|
const headers = [
|
|
31
33
|
{
|
|
@@ -38,6 +40,7 @@ export const MutationComparisonTable: FunctionComponent<MutationsTableProps> = (
|
|
|
38
40
|
mutation={cell}
|
|
39
41
|
sequenceType={sequenceType}
|
|
40
42
|
annotationsProvider={annotationsProvider}
|
|
43
|
+
linkProvider={linkProvider}
|
|
41
44
|
/>
|
|
42
45
|
),
|
|
43
46
|
},
|
|
@@ -5,6 +5,7 @@ import { getMutationsTableData } from './getMutationsTableData';
|
|
|
5
5
|
import { type SequenceType, type SubstitutionOrDeletionEntry } from '../../types';
|
|
6
6
|
import { type DeletionClass, type SubstitutionClass } from '../../utils/mutations';
|
|
7
7
|
import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
|
|
8
|
+
import { useMutationLinkProvider } from '../MutationLinkTemplateContext';
|
|
8
9
|
import { GridJsAnnotatedMutation } from '../components/annotated-mutation';
|
|
9
10
|
import type { ProportionInterval } from '../components/proportion-selector';
|
|
10
11
|
import { Table } from '../components/table';
|
|
@@ -29,6 +30,7 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({
|
|
|
29
30
|
sequenceType,
|
|
30
31
|
}) => {
|
|
31
32
|
const annotationsProvider = useMutationAnnotationsProvider();
|
|
33
|
+
const linkProvider = useMutationLinkProvider();
|
|
32
34
|
|
|
33
35
|
const headers = [
|
|
34
36
|
{
|
|
@@ -43,6 +45,7 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({
|
|
|
43
45
|
mutation={cell}
|
|
44
46
|
sequenceType={sequenceType}
|
|
45
47
|
annotationsProvider={annotationsProvider}
|
|
48
|
+
linkProvider={linkProvider}
|
|
46
49
|
/>
|
|
47
50
|
),
|
|
48
51
|
},
|
|
@@ -6,7 +6,9 @@ export async function expectMutationAnnotation(canvasElement: HTMLElement, mutat
|
|
|
6
6
|
await waitFor(async () => {
|
|
7
7
|
const annotatedMutation = canvas.getAllByText(mutation)[0];
|
|
8
8
|
await expect(annotatedMutation).toBeVisible();
|
|
9
|
-
|
|
9
|
+
const button = within(annotatedMutation).getByRole('button');
|
|
10
|
+
await expect(button).toBeVisible();
|
|
11
|
+
await userEvent.click(button);
|
|
10
12
|
});
|
|
11
13
|
|
|
12
14
|
await waitFor(() => expect(canvas.getByText(`Annotations for ${mutation}`)).toBeVisible());
|
|
@@ -2,9 +2,16 @@ import { describe, expectTypeOf, test } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import { AppComponent } from './gs-app';
|
|
4
4
|
import { type MutationAnnotations } from './mutation-annotations-context';
|
|
5
|
+
import { type MutationLinkTemplate } from './mutation-link-template-context';
|
|
5
6
|
|
|
6
7
|
describe('gs-app types', () => {
|
|
7
8
|
test('mutationAnnotations type should match', () => {
|
|
8
9
|
expectTypeOf(AppComponent.prototype).toHaveProperty('mutationAnnotations').toEqualTypeOf<MutationAnnotations>();
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
test('mutationLinkTemplate type should match', () => {
|
|
13
|
+
expectTypeOf(AppComponent.prototype)
|
|
14
|
+
.toHaveProperty('mutationLinkTemplate')
|
|
15
|
+
.toEqualTypeOf<MutationLinkTemplate>();
|
|
16
|
+
});
|
|
10
17
|
});
|
|
@@ -11,6 +11,7 @@ import { referenceGenomeContext } from './reference-genome-context';
|
|
|
11
11
|
import { withComponentDocs } from '../../.storybook/ComponentDocsBlock';
|
|
12
12
|
import { LAPIS_URL, REFERENCE_GENOME_ENDPOINT } from '../constants';
|
|
13
13
|
import { type MutationAnnotations, mutationAnnotationsContext } from './mutation-annotations-context';
|
|
14
|
+
import { type MutationLinkTemplate, mutationLinkTemplateContext } from './mutation-link-template-context';
|
|
14
15
|
import type { ReferenceGenome } from '../lapisApi/ReferenceGenome';
|
|
15
16
|
import referenceGenome from '../lapisApi/__mockData__/referenceGenome.json';
|
|
16
17
|
|
|
@@ -35,11 +36,19 @@ const meta: Meta = {
|
|
|
35
36
|
|
|
36
37
|
export default meta;
|
|
37
38
|
|
|
38
|
-
type StoryProps = {
|
|
39
|
+
type StoryProps = {
|
|
40
|
+
lapis: string;
|
|
41
|
+
mutationAnnotations: MutationAnnotations;
|
|
42
|
+
mutationLinkTemplate: MutationLinkTemplate;
|
|
43
|
+
};
|
|
39
44
|
|
|
40
45
|
const Template: StoryObj<StoryProps> = {
|
|
41
46
|
render: (args) => {
|
|
42
|
-
return html` <gs-app
|
|
47
|
+
return html` <gs-app
|
|
48
|
+
lapis="${args.lapis}"
|
|
49
|
+
.mutationAnnotations="${args.mutationAnnotations}"
|
|
50
|
+
.mutationLinkTemplate="${args.mutationLinkTemplate}"
|
|
51
|
+
>
|
|
43
52
|
<gs-app-display></gs-app-display>
|
|
44
53
|
</gs-app>`;
|
|
45
54
|
},
|
|
@@ -56,6 +65,10 @@ const Template: StoryObj<StoryProps> = {
|
|
|
56
65
|
aminoAcidPositions: ['S:123'],
|
|
57
66
|
},
|
|
58
67
|
],
|
|
68
|
+
mutationLinkTemplate: {
|
|
69
|
+
nucleotideMutation: 'http://foo.com/query?nucMut={{mutation}}',
|
|
70
|
+
aminoAcidMutation: 'http://foo.com/query?aaMut={{mutation}}',
|
|
71
|
+
},
|
|
59
72
|
},
|
|
60
73
|
};
|
|
61
74
|
|
|
@@ -128,6 +141,18 @@ export const FailsToFetchReferenceGenome: StoryObj<StoryProps> = {
|
|
|
128
141
|
},
|
|
129
142
|
};
|
|
130
143
|
|
|
144
|
+
export const ProvidesMutationLinkTemplateToChildren: StoryObj<StoryProps> = {
|
|
145
|
+
...Template,
|
|
146
|
+
play: async ({ canvasElement }) => {
|
|
147
|
+
const canvas = within(canvasElement);
|
|
148
|
+
|
|
149
|
+
await waitFor(async () => {
|
|
150
|
+
await expect(canvas.getByText('http://foo.com/query?nucMut={{mutation}}', { exact: false })).toBeVisible();
|
|
151
|
+
await expect(canvas.getByText('http://foo.com/query?aaMut={{mutation}}', { exact: false })).toBeVisible();
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
131
156
|
@customElement('gs-app-display')
|
|
132
157
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- it is used in the story above
|
|
133
158
|
class AppDisplay extends LitElement {
|
|
@@ -143,6 +168,9 @@ class AppDisplay extends LitElement {
|
|
|
143
168
|
@consume({ context: mutationAnnotationsContext, subscribe: true })
|
|
144
169
|
mutationAnnotations: MutationAnnotations = [];
|
|
145
170
|
|
|
171
|
+
@consume({ context: mutationLinkTemplateContext, subscribe: true })
|
|
172
|
+
mutationLinkTemplate: MutationLinkTemplate = {};
|
|
173
|
+
|
|
146
174
|
override render() {
|
|
147
175
|
return html`
|
|
148
176
|
<h1 class="text-xl font-bold">Dummy component</h1>
|
|
@@ -156,6 +184,8 @@ class AppDisplay extends LitElement {
|
|
|
156
184
|
<pre><code>${JSON.stringify(this.referenceGenome, null, 2)}</code></pre>
|
|
157
185
|
<h2 class="text-lg font-bold">Mutation annotations</h2>
|
|
158
186
|
<pre><code>${JSON.stringify(this.mutationAnnotations, null, 2)}</code></pre>
|
|
187
|
+
<h2 class="text-lg font-bold">Mutation link template</h2>
|
|
188
|
+
<pre><code>${JSON.stringify(this.mutationLinkTemplate, null, 2)}</code></pre>
|
|
159
189
|
`;
|
|
160
190
|
}
|
|
161
191
|
|