From 7df2e6d49009f34d5b0c1c6a0cd397d0b80a94af Mon Sep 17 00:00:00 2001 From: Calvin Lu <59149377+calvinlu3@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:52:29 -0400 Subject: [PATCH] Allow multiple therapies to be added for CDx biomarker asssociation (#465) --- src/main/webapp/app/app.scss | 4 + .../panels/CompanionDiagnosticDevicePanel.tsx | 142 +++++++++++++----- .../webapp/app/config/constants/constants.ts | 7 + .../webapp/app/shared/error/ErrorMessage.tsx | 16 ++ .../app/shared/modal/ModifyTherapyModal.tsx | 30 ++-- 5 files changed, 145 insertions(+), 54 deletions(-) create mode 100644 src/main/webapp/app/shared/error/ErrorMessage.tsx diff --git a/src/main/webapp/app/app.scss b/src/main/webapp/app/app.scss index cc193d79d..cfa78ecb8 100644 --- a/src/main/webapp/app/app.scss +++ b/src/main/webapp/app/app.scss @@ -101,6 +101,10 @@ Generic styles margin-right: 0.5rem; } +.error-message > svg { + flex-shrink: 0; +} + .warning-message { display: flex; align-items: center; diff --git a/src/main/webapp/app/components/panels/CompanionDiagnosticDevicePanel.tsx b/src/main/webapp/app/components/panels/CompanionDiagnosticDevicePanel.tsx index 2b7a8e54f..fffa43ce2 100644 --- a/src/main/webapp/app/components/panels/CompanionDiagnosticDevicePanel.tsx +++ b/src/main/webapp/app/components/panels/CompanionDiagnosticDevicePanel.tsx @@ -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'; @@ -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
{children}
; +const SidebarMenuItem: React.FunctionComponent<{ style?: React.CSSProperties; children: React.ReactNode; className?: string }> = ({ + className, + children, + ...props +}) => { + return ( +
+ {children} +
+ ); }; export const defaultAdditional = { @@ -30,10 +49,10 @@ export const defaultAdditional = { const CompanionDiagnosticDevicePanel: React.FunctionComponent = ({ getEntity }: StoreProps) => { const [geneValue, setGeneValue] = useState(null); - const [alterationValue, onAlterationChange] = useState(); - const [cancerTypeValue, onCancerTypeChange] = useState(null); - const [drugValue, onDrugChange] = useState([]); - const [fdaSubmissionValue, onFdaSubmissionChange] = useState([]); + const [alterationValue, setAlterationValue] = useState(); + const [cancerTypeValue, setCancerTypeValue] = useState(null); + const [selectedTreatments, setSelectedTreatments] = useState([[]]); + const [fdaSubmissionValue, setFdaSubmissionValue] = useState([]); const history = useHistory(); const location = useLocation(); @@ -41,16 +60,16 @@ const CompanionDiagnosticDevicePanel: React.FunctionComponent = ({ g 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) => { @@ -75,18 +94,16 @@ const CompanionDiagnosticDevicePanel: React.FunctionComponent = ({ 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]; } @@ -112,12 +129,27 @@ const CompanionDiagnosticDevicePanel: React.FunctionComponent = ({ 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 ( - -

Curation Panel

+ +

Curation Panel

+
- Add Biomarker Association +
Add Biomarker Association
@@ -127,43 +159,85 @@ const CompanionDiagnosticDevicePanel: React.FunctionComponent = ({ g - - + - - + +
Input Therapies
+ <> + {selectedTreatments.map((drugOptions, index) => { + return ( +
0 ? 'mt-2' : undefined, 'd-flex align-items-start')}> + onTreatmentChange(options as DrugSelectOption[], index)} + value={drugOptions} + placeholder={'Select drug(s)'} + /> + +
+ ); + })} + + +
+ {selectedTreatments.length > 1 && isEmptyTreatments && } + {hasDuplicateTreatments && } +
- +
-
- - + +
diff --git a/src/main/webapp/app/config/constants/constants.ts b/src/main/webapp/app/config/constants/constants.ts index 6fc96746f..a81b5e72f 100644 --- a/src/main/webapp/app/config/constants/constants.ts +++ b/src/main/webapp/app/config/constants/constants.ts @@ -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'; diff --git a/src/main/webapp/app/shared/error/ErrorMessage.tsx b/src/main/webapp/app/shared/error/ErrorMessage.tsx new file mode 100644 index 000000000..92ac191a0 --- /dev/null +++ b/src/main/webapp/app/shared/error/ErrorMessage.tsx @@ -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 ( +
+ + {message} +
+ ); +}; diff --git a/src/main/webapp/app/shared/modal/ModifyTherapyModal.tsx b/src/main/webapp/app/shared/modal/ModifyTherapyModal.tsx index 85dc08d45..ed15f65f7 100644 --- a/src/main/webapp/app/shared/modal/ModifyTherapyModal.tsx +++ b/src/main/webapp/app/shared/modal/ModifyTherapyModal.tsx @@ -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; @@ -162,24 +167,9 @@ const ModifyTherapyModalContent = observer( if (isEmptyTherapy || isDuplicate || alreadyExists) { return (
- {isDuplicate && ( -
- - Each therapy must be unique -
- )} - {isEmptyTherapy && ( -
- - You must include at least one drug for each therapy -
- )} - {alreadyExists && ( -
- - Therapy already exists -
- )} + {isDuplicate && } + {isEmptyTherapy && } + {alreadyExists && }
); } else {