@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/dist/util.d.ts CHANGED
@@ -989,7 +989,7 @@ declare global {
989
989
 
990
990
  declare global {
991
991
  interface HTMLElementTagNameMap {
992
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
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-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-prevalence-over-time': PrevalenceOverTimeComponent;
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-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-date-range-filter': DateRangeFilterComponent;
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-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-location-filter': LocationFilterComponent;
1120
+ 'gs-date-range-filter': DateRangeFilterComponent;
1125
1121
  }
1126
1122
  interface HTMLElementEventMap {
1127
- [gsEventNames.locationChanged]: LocationChangedEvent;
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-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-mutation-filter': MutationFilterComponent;
1159
+ 'gs-location-filter': LocationFilterComponent;
1163
1160
  }
1164
1161
  interface HTMLElementEventMap {
1165
- [gsEventNames.mutationFilterChanged]: CustomEvent<MutationsFilter>;
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-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-lineage-filter': LineageFilterComponent;
1178
+ 'gs-mutation-filter': MutationFilterComponent;
1182
1179
  }
1183
1180
  interface HTMLElementEventMap {
1184
- [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
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-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-number-range-filter': NumberRangeFilterComponent;
1197
+ 'gs-lineage-filter': LineageFilterComponent;
1201
1198
  }
1202
1199
  interface HTMLElementEventMap {
1203
- [gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
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-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
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-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1228
+ 'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1229
1229
  }
1230
1230
  }
1231
1231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -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 { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
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
- const meta: Meta<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
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<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
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
- <MutationAnnotationsContextProvider value={annotations}>
30
- <AnnotatedMutation {...annotatedMutationsArgs} />
31
- </MutationAnnotationsContextProvider>
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<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
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('A23403G'));
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('A23403G'));
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('S:A501G'));
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 <AnnotatedMutationWithoutContext {...props} annotationsProvider={annotationsProvider} modalRef={modalRef} />;
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 mutation.code;
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
- <ButtonWithModalDialog
70
- buttonClassName={'select-text cursor-pointer'}
71
- modalContent={modalContent}
72
- modalRef={modalRef}
73
- >
74
- {mutation.code}
75
- <sup>
76
- {mutationAnnotations
77
- .map((annotation) => annotation.symbol)
78
- .map((symbol, index) => (
79
- <Fragment key={symbol}>
80
- <span className='text-red-600'>{symbol}</span>
81
- {index !== mutationAnnotations.length - 1 && ','}
82
- </Fragment>
83
- ))}
84
- </sup>
85
- </ButtonWithModalDialog>
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
- await userEvent.click(annotatedMutation);
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 = { lapis: string; mutationAnnotations: MutationAnnotations };
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 lapis="${args.lapis}" .mutationAnnotations="${args.mutationAnnotations}">
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