Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow curating mutation summary #433

Merged
merged 5 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { isSectionRemovableWithoutReview } from 'app/shared/util/firebase/fireba
import EditIcon from 'app/shared/icons/EditIcon';
import ModifyCancerTypeModal from 'app/shared/modal/ModifyCancerTypeModal';
import { notifyError } from 'app/oncokb-commons/components/util/NotificationUtils';
import _ from 'lodash';
import { getLevelDropdownOptions } from 'app/shared/util/firebase/firebase-level-utils';
import { DIAGNOSTIC_LEVELS_ORDERING, READABLE_FIELD, PROGNOSTIC_LEVELS_ORDERING } from 'app/config/constants/firebase';
import { RealtimeTextAreaInput } from 'app/shared/firebase/input/RealtimeInputs';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
getFirebaseGenePath,
getFirebaseVusPath,
getMutationName,
hasMultipleMutations,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hasMultipleMutations,

isMutationEffectCuratable,
isSectionRemovableWithoutReview,
} from 'app/shared/util/firebase/firebase-utils';
Expand All @@ -35,7 +36,7 @@ import { IRootStore } from 'app/stores';
import { get, onValue, ref } from 'firebase/database';
import _ from 'lodash';
import { observer } from 'mobx-react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button } from 'reactstrap';
import BadgeGroup from '../BadgeGroup';
import { DeleteSectionButton } from '../button/DeleteSectionButton';
Expand All @@ -49,6 +50,7 @@ import { NestLevelColor, NestLevelMapping, NestLevelType } from './NestLevel';
import { RemovableCollapsible } from './RemovableCollapsible';
import { Unsubscribe } from 'firebase/database';
import { getLocationIdentifier } from 'app/components/geneHistoryTooltip/gene-history-tooltip-utils';
import { SimpleConfirmModal } from 'app/shared/modal/SimpleConfirmModal';

export interface IMutationCollapsibleProps extends StoreProps {
mutationPath: string;
Expand Down Expand Up @@ -77,15 +79,21 @@ const MutationCollapsible = ({
annotatedAltsCache,
genomicIndicators,
showLastModified,
handleFirebaseUpdate,
}: IMutationCollapsibleProps) => {
const firebaseMutationsPath = `${getFirebaseGenePath(isGermline, hugoSymbol)}/mutations`;

const [mutationUuid, setMutationUuid] = useState<string>('');
const [mutationName, setMutationName] = useState<string>('');
const [mutationNameReview, setMutationNameReview] = useState<Review | null>(null);
const [mutationSummary, setMutationSummary] = useState<string>('');
const [mutationAlterations, setMutationAlterations] = useState<Alteration[] | null>(null);
const [isRemovableWithoutReview, setIsRemovableWithoutReview] = useState(false);
const [relatedAnnotationResult, setRelatedAnnotationResult] = useState<AlterationAnnotationStatus[]>([]);
const [oncogenicity, setOncogenicity] = useState<string>('');
const [showSimpleConfirmModal, setShowSimpleConfirmModal] = useState<boolean>(false);
const [simpleConfirmModalBody, setSimpleConfirmModalBody] = useState<string | undefined>(undefined);
const [mutationSummaryRef, setMutationSummaryRef] = useState<HTMLElement | null>(null);

useEffect(() => {
const arr = annotatedAltsCache?.get(hugoSymbol ?? '', [{ name: mutationName, alterations: mutationAlterations }]) ?? [];
Expand Down Expand Up @@ -157,6 +165,11 @@ const MutationCollapsible = ({
setMutationName(snapshot.val());
}),
);
callbacks.push(
onValue(ref(firebaseDb, `${mutationPath}/summary`), snapshot => {
setMutationSummary(snapshot.val());
}),
);
callbacks.push(
onValue(ref(firebaseDb, `${mutationPath}/alterations`), snapshot => {
setMutationAlterations(snapshot.val());
Expand All @@ -169,6 +182,11 @@ const MutationCollapsible = ({
setIsRemovableWithoutReview(isSectionRemovableWithoutReview(review));
}),
);
callbacks.push(
onValue(ref(firebaseDb, `${mutationPath}/mutation_effect/oncogenic`), snapshot => {
setOncogenicity(snapshot.val());
}),
);

onValue(
ref(firebaseDb, `${mutationPath}/name_uuid`),
Expand Down Expand Up @@ -197,6 +215,34 @@ const MutationCollapsible = ({
[mutationPath, mutationName, parsedHistoryList],
);

async function simpleConfirmModalOnConfirm() {
await handleFirebaseUpdate?.(mutationPath, { summary: '' });
if (mutationSummaryRef) {
mutationSummaryRef.click();
}
setShowSimpleConfirmModal(false);
setSimpleConfirmModalBody(undefined);
}

function oncogenicityRadioOnClick(
event: React.MouseEvent<HTMLInputElement> | React.MouseEvent<HTMLLabelElement> | React.MouseEvent<HTMLDivElement>,
) {
if (mutationSummary && event.target) {
let newOncogenicityVal;
if (event.target instanceof HTMLInputElement) {
newOncogenicityVal = event.target.value;
} else if (event.target instanceof HTMLDivElement || event.target instanceof HTMLLabelElement) {
newOncogenicityVal = event.target.innerText;
}
if (newOncogenicityVal === RADIO_OPTION_NONE) {
event.preventDefault();
setMutationSummaryRef(event.target as HTMLElement);
setShowSimpleConfirmModal(true);
setSimpleConfirmModalBody(`Mutation summary will be removed after removing oncogenicity.`);
}
}
}

async function handleDeleteMutation(toVus = false) {
if (!firebaseDb) {
return;
Expand Down Expand Up @@ -308,6 +354,25 @@ const MutationCollapsible = ({
}
isPendingDelete={isMutationPendingDelete}
>
<RealtimeTextAreaInput
firebasePath={`${mutationPath}/summary`}
inputClass={styles.summaryTextarea}
label="Mutation Summary (Optional)"
labelIcon={
<GeneHistoryTooltip
historyData={parsedHistoryList}
location={`${getMutationName(mutationName, mutationAlterations)}, ${READABLE_FIELD.SUMMARY}`}
locationIdentifier={getLocationIdentifier({
mutationUuid,
fields: [READABLE_FIELD.SUMMARY],
})}
/>
}
name="summary"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change to mutationSummary to prevent potential name clash with gene summary.

parseRefs
disabled={oncogenicity === ''}
Copy link
Contributor

@calvinlu3 calvinlu3 Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge case: What happens when oncogenicity is cleared after entering mutation summary already. Should the mutation summary get cleared back to empty string.

disabledMessage={'Not curatable: mutation summary is only curatable when oncogenicity is specified.'}
/>
<Collapsible
idPrefix={`${mutationName}-mutation-effect`}
title="Mutation Effect"
Expand Down Expand Up @@ -369,6 +434,10 @@ const MutationCollapsible = ({
}
</>
}
/** Radio a bit tricky. Have to use onMouseDown event to cancel the default event.
* The onclick event does not like to be overwritten **/
onMouseDown={oncogenicityRadioOnClick}
labelOnClick={oncogenicityRadioOnClick}
isRadio
options={[...ONCOGENICITY_OPTIONS, RADIO_OPTION_NONE].map(label => ({
label,
Expand Down Expand Up @@ -598,6 +667,12 @@ const MutationCollapsible = ({
}}
/>
) : undefined}
<SimpleConfirmModal
show={showSimpleConfirmModal}
body={simpleConfirmModalBody}
onConfirm={simpleConfirmModalOnConfirm}
onCancel={() => setShowSimpleConfirmModal(false)}
/>
</>
);
};
Expand All @@ -623,6 +698,7 @@ const mapStoreToProps = ({
firebaseDb: firebaseAppStore.firebaseDb,
annotatedAltsCache: curationPageStore.annotatedAltsCache,
genomicIndicators: firebaseGenomicIndicatorsStore.data,
handleFirebaseUpdate: firebaseGeneService.updateObject,
});

type StoreProps = Partial<ReturnType<typeof mapStoreToProps>>;
Expand Down
17 changes: 14 additions & 3 deletions src/main/webapp/app/shared/firebase/input/RealtimeBasicInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IRootStore } from 'app/stores';
import { default as classNames, default as classnames } from 'classnames';
import { onValue, ref } from 'firebase/database';
import { inject } from 'mobx-react';
import React, { useEffect, useRef, useState } from 'react';
import React, { MouseEventHandler, useEffect, useRef, useState } from 'react';
import { FormFeedback, Input, Label, LabelProps } from 'reactstrap';
import { InputType } from 'reactstrap/types/lib/Input';
import * as styles from './styles.module.scss';
Expand Down Expand Up @@ -51,13 +51,15 @@ export interface IRealtimeBasicInput extends React.InputHTMLAttributes<HTMLInput
firebasePath: string; // firebase path that component needs to listen to
type: RealtimeBasicInputType;
label: string;
labelOnClick?: MouseEventHandler<HTMLLabelElement>;
invalid?: boolean;
invalidMessage?: string;
labelClass?: string;
labelIcon?: JSX.Element;
inputClass?: string;
parseRefs?: boolean;
updateMetaData?: boolean;
disabledMessage?: string;
}

const RealtimeBasicInput: React.FunctionComponent<IRealtimeBasicInput> = (props: IRealtimeBasicInput) => {
Expand All @@ -79,6 +81,11 @@ const RealtimeBasicInput: React.FunctionComponent<IRealtimeBasicInput> = (props:
updateReviewableContent,
style,
updateMetaData,
placeholder,
disabled,
disabledMessage,
onMouseDown,
labelOnClick,
...otherProps
} = props;

Expand Down Expand Up @@ -139,7 +146,7 @@ const RealtimeBasicInput: React.FunctionComponent<IRealtimeBasicInput> = (props:
}, [inputValueLoaded]);

const labelComponent = label && (
<RealtimeBasicLabel label={label} labelIcon={labelIcon} id={id} labelClass={isCheckType ? 'mb-0' : 'fw-bold'} />
<RealtimeBasicLabel label={label} labelIcon={labelIcon} id={id} labelClass={isCheckType ? 'mb-0' : 'fw-bold'} onClick={labelOnClick} />
);

const inputChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -169,22 +176,26 @@ const RealtimeBasicInput: React.FunctionComponent<IRealtimeBasicInput> = (props:
<>
<Input
innerRef={inputRef}
className={classNames(inputClass, isCheckType && 'ms-1 position-relative', isTextType && styles.editableTextBox)}
className={classNames(inputClass, isCheckType && 'ms-1 position-relative', isTextType && !props.disabled && styles.editableTextBox)}
id={id}
name={`${id}-${label.toLowerCase()}`}
autoComplete="off"
onChange={e => {
inputChangeHandler(e);
}}
onMouseDown={onMouseDown}
type={props.type as InputType}
style={inputStyle}
value={inputValue}
invalid={invalid}
checked={isCheckType && isChecked()}
disabled={disabled}
placeholder={placeholder ? placeholder : disabled && disabledMessage ? disabledMessage : ''}
{...otherProps}
>
{children}
</Input>
{disabled && disabledMessage && inputValue && <div className={'text-danger'}>{disabledMessage}</div>}
{invalid && <FormFeedback>{invalidMessage || ''}</FormFeedback>}
</>
);
Expand Down
10 changes: 9 additions & 1 deletion src/main/webapp/app/shared/firebase/input/RealtimeInputs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { MouseEventHandler } from 'react';
import RealtimeBasicInput, { IRealtimeBasicInput, RealtimeInputType } from './RealtimeBasicInput';

/**
Expand Down Expand Up @@ -40,6 +40,9 @@ export interface IRealtimeCheckedInputGroup {
inlineHeader?: boolean;
options: RealtimeCheckedInputOption[];
disabled?: boolean;
onClick?: MouseEventHandler<HTMLInputElement>;
onMouseDown?: MouseEventHandler<HTMLInputElement>;
labelOnClick?: MouseEventHandler<HTMLLabelElement>;
}

export const RealtimeCheckedInputGroup = (props: IRealtimeCheckedInputGroup) => {
Expand All @@ -54,7 +57,10 @@ export const RealtimeCheckedInputGroup = (props: IRealtimeCheckedInputGroup) =>
key={option.label}
firebasePath={option.firebasePath}
className="me-2"
value={option.label}
label={option.label}
onMouseDown={props.onMouseDown}
labelOnClick={props.labelOnClick}
id={`${option.firebasePath}-${option.label}`}
/>
) : (
Expand All @@ -64,6 +70,8 @@ export const RealtimeCheckedInputGroup = (props: IRealtimeCheckedInputGroup) =>
firebasePath={option.firebasePath}
style={{ marginTop: '0.1rem' }}
className="me-2"
onMouseDown={props.onMouseDown}
labelOnClick={props.labelOnClick}
label={option.label}
/>
);
Expand Down
3 changes: 3 additions & 0 deletions src/main/webapp/app/shared/model/firebase/firebase.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ export class Mutation {
name_uuid: string = generateUuid();
tumors: Tumor[] = [];
tumors_uuid: string = generateUuid();
summary = '';
summary_review?: Review;
summary_uuid: string = generateUuid();

// Germline
mutation_specific_penetrance = new MutationSpecificPenetrance();
Expand Down
6 changes: 5 additions & 1 deletion src/main/webapp/app/shared/util/firebase/firebase-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,12 @@ const addDuplicateMutationInfo = (duplicates: DuplicateMutationInfo[], mutationN
}
};

export const hasMultipleMutations = (mutationName: string) => {
return mutationName.includes(',');
};
export const isMutationEffectCuratable = (mutationName: string) => {
if (mutationName.includes(',')) {
const multipleMuts = hasMultipleMutations(mutationName);
if (multipleMuts) {
Comment on lines +403 to +408
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not in the scope of this PR, but we should update so that grch37:L265P, grch38:L252P should not get matched.

return false;
}
const excludedMutations = ['Oncogenic Mutations'];
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.