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
+
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 {