Skip to content

Commit

Permalink
Allow multiple therapies to be added for CDx biomarker asssociation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinlu3 authored Oct 31, 2024
1 parent 8bc7b4c commit 7df2e6d
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 54 deletions.
4 changes: 4 additions & 0 deletions src/main/webapp/app/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ Generic styles
margin-right: 0.5rem;
}

.error-message > svg {
flex-shrink: 0;
}

.warning-message {
display: flex;
align-items: center;
Expand Down
142 changes: 108 additions & 34 deletions src/main/webapp/app/components/panels/CompanionDiagnosticDevicePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Menu } from 'react-pro-sidebar';
import { Button, Container, Form } from 'reactstrap';
import { ENTITY_ACTION, ENTITY_TYPE, RULE_ENTITY, SearchOptionType } from 'app/config/constants/constants';
import {
DUPLICATE_THERAPY_ERROR_MESSAGE,
EMPTY_THERAPY_ERROR_MESSAGE,
ENTITY_ACTION,
ENTITY_TYPE,
RULE_ENTITY,
SearchOptionType,
} from 'app/config/constants/constants';
import { useHistory, useLocation } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus';
Expand All @@ -18,9 +25,21 @@ import { associationClient } from 'app/shared/api/clients';
import { notifyError, notifySuccess } from 'app/oncokb-commons/components/util/NotificationUtils';
import { IRootStore } from 'app/stores';
import { connect } from 'app/shared/util/typed-inject';
import classNames from 'classnames';
import { faTrashCan } from '@fortawesome/free-solid-svg-icons';
import _ from 'lodash';
import { ErrorMessage } from 'app/shared/error/ErrorMessage';

const SidebarMenuItem: React.FunctionComponent<{ style?: React.CSSProperties; children: React.ReactNode }> = ({ style, children }) => {
return <div style={{ padding: '0.5rem 1rem', ...style }}>{children}</div>;
const SidebarMenuItem: React.FunctionComponent<{ style?: React.CSSProperties; children: React.ReactNode; className?: string }> = ({
className,
children,
...props
}) => {
return (
<div className={classNames('py-2 px-1', className)} {...props}>
{children}
</div>
);
};

export const defaultAdditional = {
Expand All @@ -30,27 +49,27 @@ export const defaultAdditional = {

const CompanionDiagnosticDevicePanel: React.FunctionComponent<StoreProps> = ({ getEntity }: StoreProps) => {
const [geneValue, setGeneValue] = useState<GeneSelectOption | null>(null);
const [alterationValue, onAlterationChange] = useState<readonly AlterationSelectOption[]>();
const [cancerTypeValue, onCancerTypeChange] = useState<CancerTypeSelectOption | null>(null);
const [drugValue, onDrugChange] = useState<readonly DrugSelectOption[]>([]);
const [fdaSubmissionValue, onFdaSubmissionChange] = useState<readonly FdaSubmissionSelectOption[]>([]);
const [alterationValue, setAlterationValue] = useState<readonly AlterationSelectOption[]>();
const [cancerTypeValue, setCancerTypeValue] = useState<CancerTypeSelectOption | null>(null);
const [selectedTreatments, setSelectedTreatments] = useState<DrugSelectOption[][]>([[]]);
const [fdaSubmissionValue, setFdaSubmissionValue] = useState<readonly FdaSubmissionSelectOption[]>([]);

const history = useHistory();
const location = useLocation();
const id = parseInt(location.pathname.split('/')[2], 10);

useEffect(() => {
if (geneValue === null) {
onAlterationChange([]);
setAlterationValue([]);
}
}, [geneValue]);

const resetValues = () => {
onAlterationChange([]);
setAlterationValue([]);
setGeneValue(null);
onCancerTypeChange(null);
onDrugChange([]);
onFdaSubmissionChange([]);
setCancerTypeValue(null);
setSelectedTreatments([[]]);
setFdaSubmissionValue([]);
};

const createBiomarkerAssociation = (e: any) => {
Expand All @@ -75,18 +94,16 @@ const CompanionDiagnosticDevicePanel: React.FunctionComponent<StoreProps> = ({ g
proteinChange: '',
};
}),
drugs: drugValue?.map((drug): Drug => {
return {
id: drug.value,
uuid: '',
};
}),
drugs: _.uniqBy(
selectedTreatments.flat().map(option => ({ id: option.value, uuid: '' })),
'id',
),
};

if (association.drugs) {
if (!_.isEmpty(association.drugs)) {
const rule: Rule = {
entity: RULE_ENTITY.DRUG,
rule: association.drugs.map(drug => drug.id).join('+'),
rule: selectedTreatments.map(innerArray => innerArray.map(option => option.value).join('+')).join(','),
};
association.rules = [rule];
}
Expand All @@ -112,12 +129,27 @@ const CompanionDiagnosticDevicePanel: React.FunctionComponent<StoreProps> = ({ g
history.push(getEntityActionRoute(ENTITY_TYPE.FDA_SUBMISSION, ENTITY_ACTION.ADD));
};

const onTreatmentChange = (drugOptions: DrugSelectOption[], index: number) => {
setSelectedTreatments(prevItems => prevItems.map((item, i) => (i === index ? drugOptions : item)));
};

const isEmptyTreatments = selectedTreatments.some(drugs => drugs.length === 0);
const hasDuplicateTreatments = useMemo(() => {
return selectedTreatments.some((item, index) => selectedTreatments.slice(index + 1).some(otherItem => _.isEqual(item, otherItem)));
}, [selectedTreatments]);

const isSaveButtonDisabled =
isEmptyTreatments ||
hasDuplicateTreatments ||
[geneValue, alterationValue, cancerTypeValue, fdaSubmissionValue].some(v => _.isEmpty(v));

return (
<Container>
<h4 style={{ marginBottom: '2rem', marginLeft: '1rem' }}>Curation Panel</h4>
<Container className="ps-0">
<h4 style={{ marginBottom: '1rem' }}>Curation Panel</h4>
<div className="border-top py-3"></div>
<Menu>
<Form onSubmit={createBiomarkerAssociation}>
<SidebarMenuItem>Add Biomarker Association</SidebarMenuItem>
<h5 className="ms-1">Add Biomarker Association</h5>
<SidebarMenuItem>
<GeneSelect onChange={setGeneValue} value={geneValue} />
</SidebarMenuItem>
Expand All @@ -127,43 +159,85 @@ const CompanionDiagnosticDevicePanel: React.FunctionComponent<StoreProps> = ({ g
<AlterationSelect
isMulti
geneId={geneValue?.value?.toString() ?? ''}
onChange={onAlterationChange}
onChange={setAlterationValue}
value={alterationValue}
/>
</div>
<DefaultTooltip overlay={'Create new alteration'}>
<Button className="ms-1" color="primary" onClick={redirectToCreateAlteration}>
<Button className="ms-1" color="primary" onClick={redirectToCreateAlteration} outline>
<FontAwesomeIcon icon={faPlus} size="sm" />
</Button>
</DefaultTooltip>
</div>
</SidebarMenuItem>
<SidebarMenuItem>
<CancerTypeSelect onChange={onCancerTypeChange} value={cancerTypeValue} />
<CancerTypeSelect onChange={setCancerTypeValue} value={cancerTypeValue} />
</SidebarMenuItem>
<SidebarMenuItem>
<DrugSelect isMulti onChange={onDrugChange} value={drugValue} placeholder={'Select drug(s)'} />
<SidebarMenuItem className="border-top py-2">
<h6>Input Therapies</h6>
<>
{selectedTreatments.map((drugOptions, index) => {
return (
<div key={`cdx-drug-add-${index}`} className={classNames(index > 0 ? 'mt-2' : undefined, 'd-flex align-items-start')}>
<DrugSelect
className={'flex-grow-1'}
isMulti
onChange={options => onTreatmentChange(options as DrugSelectOption[], index)}
value={drugOptions}
placeholder={'Select drug(s)'}
/>
<Button
className="ms-1"
color="danger"
onClick={() => {
setSelectedTreatments(prevItems => prevItems.filter((val, i) => i !== index));
}}
outline
disabled={selectedTreatments.length === 1}
>
<FontAwesomeIcon icon={faTrashCan} size="sm" />
</Button>
</div>
);
})}
</>
<Button
outline
size="sm"
className="mt-2"
color="primary"
onClick={() => {
setSelectedTreatments(prevState => [...prevState, []]);
}}
disabled={isEmptyTreatments || hasDuplicateTreatments}
>
Add Therapy
</Button>
<div className="mt-1">
{selectedTreatments.length > 1 && isEmptyTreatments && <ErrorMessage message={EMPTY_THERAPY_ERROR_MESSAGE} />}
{hasDuplicateTreatments && <ErrorMessage message={DUPLICATE_THERAPY_ERROR_MESSAGE} />}
</div>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuItem className="border-top py-2">
<div className="d-flex align-items-start">
<div style={{ flex: 1 }}>
<FdaSubmissionSelect
cdxId={id}
isMulti
onChange={onFdaSubmissionChange}
onChange={setFdaSubmissionValue}
value={fdaSubmissionValue}
placeholder={'Select FDA submission(s)'}
/>
</div>
<DefaultTooltip overlay={'Create new FDA submission'}>
<Button className="ms-1" color="primary" onClick={redirectToCreateFdaSubmission}>
<Button className="ms-1" color="primary" onClick={redirectToCreateFdaSubmission} outline>
<FontAwesomeIcon icon={faPlus} size="sm" />
</Button>
</DefaultTooltip>
</div>
</SidebarMenuItem>
<SidebarMenuItem>
<SaveButton />
<SidebarMenuItem style={{ display: 'flex', justifyContent: 'end' }}>
<SaveButton disabled={isSaveButtonDisabled} />
</SidebarMenuItem>
</Form>
</Menu>
Expand Down
7 changes: 7 additions & 0 deletions src/main/webapp/app/config/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,10 @@ export const KEYCLOAK_UNAUTHORIZED_PARAM = 'unauthorized';
*/
export const PRIORITY_ENTITY_MENU_ITEM_KEY = 'oncokbCuration-entityMenuPriorityKey';
export const SOMATIC_GERMLINE_SETTING_KEY = 'oncokbCuration-somaticGermlineSettingKey';

/**
* Error Messages
*/
export const DUPLICATE_THERAPY_ERROR_MESSAGE = 'Each therapy must be unique';
export const EMPTY_THERAPY_ERROR_MESSAGE = 'You must include at least one drug for each therapy';
export const THERAPY_ALREADY_EXISTS_ERROR_MESSAGE = 'Therapy already exists';
16 changes: 16 additions & 0 deletions src/main/webapp/app/shared/error/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DEFAULT_ICON_SIZE } from 'app/config/constants/constants';
import React from 'react';
import { FaExclamationCircle } from 'react-icons/fa';

export interface IErrorMessage {
message: string;
}

export const ErrorMessage = ({ message }: IErrorMessage) => {
return (
<div className="error-message">
<FaExclamationCircle className="me-2" size={DEFAULT_ICON_SIZE} />
<span>{message}</span>
</div>
);
};
30 changes: 10 additions & 20 deletions src/main/webapp/app/shared/modal/ModifyTherapyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import { IRootStore } from 'app/stores';
import { componentInject } from '../util/typed-inject';
import { observer } from 'mobx-react';
import { IDrug } from '../model/drug.model';
import { FaExclamationCircle, FaRegTrashAlt } from 'react-icons/fa';
import { FaRegTrashAlt } from 'react-icons/fa';
import './modify-therapy-modal.scss';
import { Button } from 'reactstrap';
import { generateUuid } from '../util/utils';
import { parseNcitUniqId } from '../select/NcitCodeSelect';
import _ from 'lodash';
import { Treatment, Tumor } from '../model/firebase/firebase.model';
import { DEFAULT_ICON_SIZE } from 'app/config/constants/constants';
import { Unsubscribe, onValue, ref } from 'firebase/database';
import {
DUPLICATE_THERAPY_ERROR_MESSAGE,
EMPTY_THERAPY_ERROR_MESSAGE,
THERAPY_ALREADY_EXISTS_ERROR_MESSAGE,
} from 'app/config/constants/constants';
import { ErrorMessage } from '../error/ErrorMessage';

export interface IModifyTherapyModalProps extends StoreProps {
treatmentUuid: string;
Expand Down Expand Up @@ -162,24 +167,9 @@ const ModifyTherapyModalContent = observer(
if (isEmptyTherapy || isDuplicate || alreadyExists) {
return (
<div>
{isDuplicate && (
<div className="error-message">
<FaExclamationCircle className="me-2" size={DEFAULT_ICON_SIZE} />
<span>Each therapy must be unique</span>
</div>
)}
{isEmptyTherapy && (
<div className="error-message">
<FaExclamationCircle className="me-2" size={DEFAULT_ICON_SIZE} />
<span>You must include at least one drug for each therapy</span>
</div>
)}
{alreadyExists && (
<div className="error-message">
<FaExclamationCircle className="me-2" size={DEFAULT_ICON_SIZE} />
<span>Therapy already exists</span>
</div>
)}
{isDuplicate && <ErrorMessage message={DUPLICATE_THERAPY_ERROR_MESSAGE} />}
{isEmptyTherapy && <ErrorMessage message={EMPTY_THERAPY_ERROR_MESSAGE} />}
{alreadyExists && <ErrorMessage message={THERAPY_ALREADY_EXISTS_ERROR_MESSAGE} />}
</div>
);
} else {
Expand Down

0 comments on commit 7df2e6d

Please sign in to comment.