Skip to content

Commit 60e0171

Browse files
committed
implement add range modal
1 parent 8c21047 commit 60e0171

File tree

8 files changed

+295
-1
lines changed

8 files changed

+295
-1
lines changed

src/main/webapp/app/config/constants/firebase.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export enum FB_COLLECTION {
4646
SETTING = 'Setting',
4747
VUS = 'VUS',
4848
GERMLINE_VUS = 'Germline_VUS',
49+
RANGES = 'Ranges',
50+
GERMLINE_RANGES = 'Germline_Ranges',
4951
}
5052

5153
export const FB_COLLECTION_PATH = {
@@ -65,6 +67,8 @@ export const FB_COLLECTION_PATH = {
6567
GERMLINE_VUS: `${FB_COLLECTION.GERMLINE_VUS}/:hugoSymbol`,
6668
HISTORY: `${FB_COLLECTION.HISTORY}/:hugoSymbol/api`,
6769
GERMLINE_HISTORY: `${FB_COLLECTION.GERMLINE_HISTORY}/:hugoSymbol/api`,
70+
RANGE: `${FB_COLLECTION.RANGES}/:hugoSymbol`,
71+
GERMLINE_RANGE: `${FB_COLLECTION.GERMLINE_RANGES}/:hugoSymbol`,
6872
};
6973

7074
export enum ONCOKB_STYLES_ONCOGENICITY_CLASSNAMES {

src/main/webapp/app/pages/curation/header/MutationsSectionHeader.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IRootStore } from 'app/stores';
77
import { DataSnapshot, onValue, ref } from 'firebase/database';
88
import _ from 'lodash';
99
import React, { useEffect, useMemo, useRef, useState } from 'react';
10-
import { FaFilter, FaSort } from 'react-icons/fa';
10+
import { FaFilter, FaPlus, FaSort } from 'react-icons/fa';
1111
import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
1212
import AddMutationButton from '../button/AddMutationButton';
1313
import ReactSelect from 'react-select';
@@ -23,6 +23,7 @@ export interface IMutationsSectionHeaderProps extends StoreProps {
2323
setFilteredMutationKeys: React.Dispatch<React.SetStateAction<string[]>>;
2424
showAddMutationModal: boolean;
2525
setShowAddMutationModal: React.Dispatch<React.SetStateAction<boolean>>;
26+
setShowAddRangeModal: React.Dispatch<React.SetStateAction<boolean>>;
2627
setSortMethod: React.Dispatch<React.SetStateAction<SortOptions>>;
2728
}
2829

@@ -61,6 +62,7 @@ function MutationsSectionHeader({
6162
setFilteredMutationKeys,
6263
showAddMutationModal,
6364
setShowAddMutationModal,
65+
setShowAddRangeModal,
6466
setSortMethod,
6567
firebaseDb,
6668
annotatedAltsCache,
@@ -326,6 +328,16 @@ function MutationsSectionHeader({
326328
<div className={'d-flex align-items-center mb-2'}>
327329
<h5 className="mb-0 me-2">Mutations:</h5>
328330
<AddMutationButton showAddMutationModal={showAddMutationModal} onClickHandler={(show: boolean) => setShowAddMutationModal(!show)} />
331+
<Button
332+
size="sm"
333+
outline
334+
color="primary"
335+
className="d-flex align-items-center text-nowrap"
336+
onClick={() => setShowAddRangeModal(true)}
337+
>
338+
<FaPlus className="me-2" />
339+
Add Range
340+
</Button>
329341
</div>
330342
<div style={{ width: '100%' }} className="d-flex align-items-center justify-content-between mb-2">
331343
{mutationsAreFiltered ? (

src/main/webapp/app/pages/curation/mutation/MutationsSection.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { extractPositionFromSingleNucleotideAlteration } from 'app/shared/util/u
2626
import { MUTATION_LIST_ID, SINGLE_MUTATION_VIEW_ID } from 'app/config/constants/html-id';
2727
import { SentryError } from 'app/config/sentry-error';
2828
import { MutationQuery } from 'app/stores/curation-page.store';
29+
import AddRangeModal from 'app/shared/modal/AddRangeModal';
2930

3031
export interface IMutationsSectionProps extends StoreProps {
3132
mutationsPath: string;
@@ -42,6 +43,7 @@ function MutationsSection({
4243
isGermline,
4344
parsedHistoryList,
4445
addMutation,
46+
addRange,
4547
openMutationCollapsibleListKey,
4648
setOpenMutationCollapsibleListKey,
4749
firebaseDb,
@@ -50,6 +52,7 @@ function MutationsSection({
5052
setIsMutationListRendered,
5153
}: IMutationsSectionProps) {
5254
const [showAddMutationModal, setShowAddMutationModal] = useState(false);
55+
const [showAddRangeModal, setShowAddRangeModal] = useState(false);
5356
const [filteredMutationKeys, setFilteredMutationKeys] = useState<string[]>([]);
5457
const [sortMethod, setSortMethod] = useState<SortOptions>(SortOptions.DEFAULT);
5558

@@ -213,6 +216,7 @@ function MutationsSection({
213216
setFilteredMutationKeys={setFilteredMutationKeys}
214217
showAddMutationModal={showAddMutationModal}
215218
setShowAddMutationModal={setShowAddMutationModal}
219+
setShowAddRangeModal={setShowAddRangeModal}
216220
setSortMethod={setSortMethod}
217221
/>
218222
</Col>
@@ -240,18 +244,31 @@ function MutationsSection({
240244
}}
241245
/>
242246
)}
247+
{showAddRangeModal && (
248+
<AddRangeModal
249+
hugoSymbol={hugoSymbol}
250+
isGermline={isGermline}
251+
onCancel={() => setShowAddRangeModal(false)}
252+
onConfirm={(alias, start, end, oncogencities, mutationTypes) => {
253+
addRange?.(hugoSymbol, alias, start, end, oncogencities, mutationTypes, false);
254+
setShowAddRangeModal(false);
255+
}}
256+
/>
257+
)}
243258
</>
244259
);
245260
}
246261

247262
const mapStoreToProps = ({
248263
firebaseGeneService,
264+
firebaseRangeService,
249265
openMutationCollapsibleStore,
250266
firebaseAppStore,
251267
curationPageStore,
252268
firebaseMutationConvertIconStore,
253269
}: IRootStore) => ({
254270
addMutation: firebaseGeneService.addMutation,
271+
addRange: firebaseRangeService.addRange,
255272
openMutationCollapsibleListKey: openMutationCollapsibleStore.listKey,
256273
setOpenMutationCollapsibleListKey: openMutationCollapsibleStore.setOpenMutationCollapsibleListKey,
257274
firebaseDb: firebaseAppStore.firebaseDb,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { getFirebaseRangesPath } from 'app/shared/util/firebase/firebase-utils';
2+
import { FirebaseRepository } from 'app/stores/firebase/firebase-repository';
3+
4+
/* eslint-disable no-console */
5+
export class FirebaseRangeService {
6+
firebaseRepository: FirebaseRepository;
7+
8+
constructor(firebaseRepository: FirebaseRepository) {
9+
this.firebaseRepository = firebaseRepository;
10+
}
11+
12+
addRange = async (
13+
hugoSymbol: string,
14+
alias: string,
15+
start: number,
16+
end: number,
17+
oncogenicities: string[],
18+
mutationTypes: string[],
19+
isGermline: boolean,
20+
) => {
21+
await this.firebaseRepository.push(getFirebaseRangesPath(isGermline, hugoSymbol), {
22+
alias,
23+
start,
24+
end,
25+
oncogencities: oncogenicities.join(','),
26+
mutationTypes: mutationTypes.join(','),
27+
});
28+
};
29+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { Button, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
3+
import { ONCOGENICITY_OPTIONS } from 'app/config/constants/firebase';
4+
import { IRootStore } from 'app/stores';
5+
import { observer } from 'mobx-react';
6+
import { componentInject } from '../util/typed-inject';
7+
import { onValue, ref } from 'firebase/database';
8+
import { getFirebaseRangesPath } from '../util/firebase/firebase-utils';
9+
import { MutationRange, RangeList } from '../model/firebase/firebase.model';
10+
11+
enum AddRangeStep {
12+
POSITION = 1,
13+
CRITERIA,
14+
ALIAS,
15+
}
16+
const addRangeStepLength = Object.keys(AddRangeStep).length / 2;
17+
const mutationTypes = ['Missense', 'Insertion', 'Deletion'];
18+
19+
export interface IAddRangeModalProps extends StoreProps {
20+
hugoSymbol: string;
21+
isGermline: boolean;
22+
onCancel: () => void;
23+
onConfirm: (alias: string, start: number, end: number, oncogencities: string[], mutationTypes: string[]) => void;
24+
}
25+
26+
function AddRangeModal({ hugoSymbol, isGermline, onCancel, onConfirm, firebaseDb }: IAddRangeModalProps) {
27+
const [currentStep, setCurrentStep] = useState(AddRangeStep.POSITION);
28+
const [position, setPosition] = useState<[number | undefined, number | undefined]>([undefined, undefined]);
29+
const [selectedOncogenicity, setSelectedOncogenicity] = useState<string[]>([]);
30+
const [selectedMutationTypes, setSelectedMutationTypes] = useState<string[]>([]);
31+
const [alias, setAlias] = useState('');
32+
const [existingAliases, setExistingAliases] = useState<string[]>([]);
33+
34+
useEffect(() => {
35+
if (!firebaseDb) {
36+
return;
37+
}
38+
const callback = onValue(ref(firebaseDb, getFirebaseRangesPath(isGermline, hugoSymbol)), snapshot => {
39+
const ranges: RangeList | undefined = snapshot.val();
40+
if (ranges) {
41+
setExistingAliases(Object.values(ranges).map(range => range.alias));
42+
}
43+
});
44+
45+
return () => {
46+
callback();
47+
};
48+
}, [firebaseDb]);
49+
50+
let errorMessage: string | undefined = undefined;
51+
switch (currentStep) {
52+
case AddRangeStep.POSITION:
53+
errorMessage = validateRange(position[0], position[1]);
54+
break;
55+
case AddRangeStep.CRITERIA:
56+
break;
57+
case AddRangeStep.ALIAS:
58+
errorMessage = validateAlias(alias, existingAliases);
59+
break;
60+
default:
61+
}
62+
63+
function nextStep() {
64+
setCurrentStep(step => step.valueOf() + 1);
65+
}
66+
67+
function prevStep() {
68+
setCurrentStep(step => step.valueOf() - 1);
69+
}
70+
71+
function toggleOncogenicity(value: string) {
72+
setSelectedOncogenicity(prev => (prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value]));
73+
}
74+
75+
function toggleMutationType(value: string) {
76+
setSelectedMutationTypes(prev => (prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value]));
77+
}
78+
79+
return (
80+
<Modal isOpen>
81+
<ModalHeader toggle={() => onCancel()}>{`Add Range (${currentStep} / ${addRangeStepLength})`}</ModalHeader>
82+
<ModalBody>
83+
<Label>Position</Label>
84+
<div className="d-flex align-items-center gap-2 mb-2">
85+
<Input
86+
type="number"
87+
disabled={currentStep !== AddRangeStep.POSITION}
88+
placeholder="Start"
89+
onChange={event => setPosition(pos => [event.target.value ? Number(event.target.value) : undefined, pos[1]])}
90+
/>
91+
<span>&mdash;</span>
92+
<Input
93+
type="number"
94+
disabled={currentStep !== AddRangeStep.POSITION}
95+
placeholder="End"
96+
onChange={event => setPosition(pos => [pos[0], event.target.value ? Number(event.target.value) : undefined])}
97+
/>
98+
</div>
99+
{currentStep.valueOf() >= AddRangeStep.CRITERIA.valueOf() && (
100+
<>
101+
<Label>Oncogenicity</Label>
102+
<div className="mb-2">
103+
{ONCOGENICITY_OPTIONS.map(option => (
104+
<FormGroup inline check key={option}>
105+
<Input
106+
disabled={currentStep !== AddRangeStep.CRITERIA}
107+
type="checkbox"
108+
id={`oncogenicity-${option}`}
109+
checked={selectedOncogenicity.includes(option)}
110+
onChange={() => toggleOncogenicity(option)}
111+
/>
112+
<Label check for={`oncogenicity-${option}`}>
113+
{option}
114+
</Label>
115+
</FormGroup>
116+
))}
117+
</div>
118+
<Label>Mutation Type</Label>
119+
<div className="mb-2">
120+
{mutationTypes.map(option => (
121+
<FormGroup inline check key={option}>
122+
<Input
123+
disabled={currentStep !== AddRangeStep.CRITERIA}
124+
type="checkbox"
125+
id={`mutation-type-${option}`}
126+
checked={selectedMutationTypes.includes(option)}
127+
onChange={() => toggleMutationType(option)}
128+
/>
129+
<Label check for={`mutation-type-${option}`}>
130+
{option}
131+
</Label>
132+
</FormGroup>
133+
))}
134+
</div>
135+
</>
136+
)}
137+
{currentStep.valueOf() >= AddRangeStep.ALIAS.valueOf() && (
138+
<>
139+
<Label>Alias</Label>
140+
<Input value={alias} placeholder="Enter an alias for the range" onChange={event => setAlias(event.target.value)} />
141+
</>
142+
)}
143+
</ModalBody>
144+
<ModalFooter className="d-flex justify-content-between">
145+
{currentStep.valueOf() === 1 ? (
146+
<Button outline color="danger" onClick={onCancel}>
147+
Cancel
148+
</Button>
149+
) : (
150+
<Button outline color="danger" onClick={prevStep}>
151+
Previous
152+
</Button>
153+
)}
154+
<span className="text-danger">{errorMessage}</span>
155+
{currentStep.valueOf() < addRangeStepLength ? (
156+
<Button disabled={!!errorMessage} outline color="primary" onClick={nextStep}>
157+
Next
158+
</Button>
159+
) : (
160+
<Button
161+
disabled={!!errorMessage}
162+
color="primary"
163+
onClick={() => {
164+
onConfirm(alias, position[0]!, position[1]!, selectedOncogenicity, selectedMutationTypes);
165+
}}
166+
>
167+
Confirm
168+
</Button>
169+
)}
170+
</ModalFooter>
171+
</Modal>
172+
);
173+
}
174+
175+
const mapStoreToProps = ({ firebaseAppStore }: IRootStore) => ({
176+
firebaseDb: firebaseAppStore.firebaseDb,
177+
});
178+
179+
type StoreProps = Partial<ReturnType<typeof mapStoreToProps>>;
180+
181+
export default componentInject(mapStoreToProps)(observer(AddRangeModal));
182+
183+
function validateRange(start: number | undefined, end: number | undefined): string | undefined {
184+
if (start === undefined || end === undefined) {
185+
return 'Start and end must both be specified';
186+
}
187+
if (start >= end) {
188+
return 'Start must be less than end.';
189+
}
190+
return undefined;
191+
// TODO: transcript validation
192+
}
193+
194+
function validateAlias(alias: string, existingAliases: string[]) {
195+
alias = alias.trim();
196+
if (!alias) {
197+
return 'Alias must be specified.';
198+
}
199+
if (existingAliases.some(existing => alias.toLowerCase() === existing.toLowerCase())) {
200+
return 'Alias already exists.';
201+
}
202+
return undefined;
203+
}

src/main/webapp/app/shared/model/firebase/firebase.model.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ export type HistoryList = {
3333
[uuid: string]: History;
3434
};
3535

36+
export type RangeCollection = {
37+
[hugoSymbol: string]: RangeList;
38+
};
39+
40+
export type RangeList = {
41+
[uuid: string]: MutationRange;
42+
};
43+
3644
export enum FIREBASE_ONCOGENICITY {
3745
YES = 'Yes',
3846
LIKELY = 'Likely',
@@ -515,3 +523,11 @@ export class HistoryInfo {
515523
};
516524
fields?: READABLE_FIELD[];
517525
}
526+
527+
export type MutationRange = {
528+
alias: string;
529+
start: number;
530+
end: number;
531+
oncogencities: string;
532+
mutationTypes: string;
533+
};

0 commit comments

Comments
 (0)