From 9ea7aecb0112bf4bf0f03c93de174a2f2b4c7ce8 Mon Sep 17 00:00:00 2001 From: Jay Allen <107942890+jallentxbiomed@users.noreply.github.com> Date: Thu, 21 Mar 2024 18:17:31 -0500 Subject: [PATCH] Handle Widget Modal Closing on Submit or Cancel (#700) * added new functionality for closing procedure entry modal * Added snd.project.objectId column to query and fixed duplicate projectId column name by prefixing columns with snd/ehr. (#701) * added new functionality for closing procedure entry modal * grid changes * added error handling for widget * re-disabled New button * re-disabled New button * changed error alert to red * removed unnecessary call to handleCloseUpdateModal --------- Co-authored-by: Terry J Hawkins --- .../SndEventsWidget/SndEventsWidget.tsx | 50 ++++++++----- .../components/AdmissionInfoPopover.tsx | 32 ++++----- .../components/EventListingGridPanel.tsx | 71 ++++++++++++------- .../components/ProcedureEntryModal.tsx | 51 +++++++++---- 4 files changed, 132 insertions(+), 72 deletions(-) diff --git a/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx b/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx index 91b0e8cd..76ecad25 100644 --- a/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/SndEventsWidget.tsx @@ -1,7 +1,7 @@ import React, { FC, memo, useState, useEffect } from 'react'; import { EventListingGridPanel } from './components/EventListingGridPanel'; -import './styles/sndEventsWidget.scss' -import { FormGroup, ControlLabel, FormControl } from 'react-bootstrap' +import './styles/sndEventsWidget.scss'; +import { FormGroup, ControlLabel, FormControl } from 'react-bootstrap'; import { getMultiRow } from './actions'; import { Alert } from '@labkey/components'; @@ -13,6 +13,8 @@ interface Props { export const SndEventsWidget: FC = memo((props: Props) => { const {filterConfig, hasPermission} = props; const [subjectIds, setSubjectIds] = useState(['']); + const [message, setMessage] = useState(undefined); + const [status, setStatus] = useState(undefined); useEffect(() => { (async () => { @@ -20,15 +22,27 @@ export const SndEventsWidget: FC = memo((props: Props) => { await getSubjectIdsFromFilters(filterConfig, setSubjectIds); } })(); - }, []) + }, []); const handleEnter = (e) => { - setSubjectIds(e.target.value.split(";")) - } + setSubjectIds(e.target.value.split(';')); + }; + + const handleError = (message: string) => { + setMessage(message); + setStatus("danger"); + window.setTimeout(() => setMessage(undefined), 30000); + }; + + const handleUpdateResponse = (message: string, status: string) => { + setMessage(message); + setStatus(status); + window.setTimeout(() => setMessage(undefined), 30000); + }; const form = () => { - return( + return (
@@ -38,13 +52,13 @@ export const SndEventsWidget: FC = memo((props: Props) => { type={'text'} placeholder={`Enter Subject ID`} required={true} - onChange={(e: any) => setSubjectIds(e.target.value.split(";"))} + onChange={(e: any) => setSubjectIds(e.target.value.split(';'))} />
- ) - } + ); + }; return (
@@ -61,26 +75,30 @@ export const SndEventsWidget: FC = memo((props: Props) => { {hasPermission && subjectIds[0] === 'none' && ( No animals were found for filter selections )} + {message && ( + {message} + )} {hasPermission && subjectIds && ( - + )}
- ) -}) + ); +}); const getSubjectIdsFromFilters = async (filterConfig, handleSetSubjectIds) => { const subjectIds = []; const filters = filterConfig.filters; if (filters.inputType === 'roomCage' && filters.room !== null) { const rooms = filters.room.split(','); - let ids: string[] + let ids: string[]; try { ids = (await getMultiRow('study', 'demographicsCurLocation', 'room', rooms, []))['rows'].map(a => a.Id); - } catch (err) { + } + catch (err) { console.log(err); } - handleSetSubjectIds(ids.length ? ids : ['none']) + handleSetSubjectIds(ids.length ? ids : ['none']); } else if (filters.inputType === 'multiSubject') { const ids = filters.nonRemovable['0'] ? filters.nonRemovable['0'].value : ['none']; handleSetSubjectIds(Array.isArray(ids) ? ids : [ids]); @@ -89,4 +107,4 @@ const getSubjectIdsFromFilters = async (filterConfig, handleSetSubjectIds) => { subjectIds.push(ids); handleSetSubjectIds(subjectIds); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx b/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx index ed52b12c..b971e635 100644 --- a/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/components/AdmissionInfoPopover.tsx @@ -8,7 +8,7 @@ interface Props { } export const AdmissionInfoPopover: FC = memo((props: Props) => { - const { admitChargeId, eventId } = props; + const {admitChargeId, eventId} = props; const [admissionInfo, setAdmissionInfo] = useState([]); @@ -16,19 +16,19 @@ export const AdmissionInfoPopover: FC = memo((props: Props) => { const handlePopoverEnter = async () => { await getAdmitData(eventId, setAdmissionInfo); - } + }; const popover = ( - +

Admission Information

{admissionInfo.map((d, i) => { - return
{d}
+ return
{d}
; })}
- ) - return( + ); + return ( (ref = r)} container={ref.current} @@ -38,8 +38,8 @@ export const AdmissionInfoPopover: FC = memo((props: Props) => { shouldUpdatePosition={true}> {admitChargeId} - ) -}) + ); +}); const getAdmitData = async (admitEventId, handleSetAdmissionInfo) => { @@ -49,24 +49,24 @@ const getAdmitData = async (admitEventId, handleSetAdmissionInfo) => { display.push(Admit ID: {info['AdmitId']}, Diagnosis: {info['Diagnosis']}, Admitting complaint: {info['AdmittingComplaint']}, - Admission date: {info['AdmitDate'] === null ? 'Not recorded' : info['AdmitDate'].split(" ")[0]}) + Admission date: {info['AdmitDate'] === null ? 'Not recorded' : info['AdmitDate'].split(' ')[0]}); if (info['ReleaseDate'] != null) { - display.push(Release date: {info['ReleaseDate'].split(" ")[0]}) + display.push(Release date: {info['ReleaseDate'].split(' ')[0]}); } if (info['Resolution'] != null) { - display.push(Resolution: {info['Resolution']}) + display.push(Resolution: {info['Resolution']}); } } else { - display.push(Charge ID: {info['ChargeId']}) + display.push(Charge ID: {info['ChargeId']}); if (info['Protocol'] != null) { display.push(IACUC #: {info['Protocol']}, IACUC Description: {info['IacucDescription']}, - IACUC Assignment Date: {(info['AssignmentDate'] === null ? 'Not found' : info['AssignmentDate'].split(" ")[0])}, - Supervising Vet: {(info['Veterinarian'] === null ? 'Not recorded' : info['Veterinarian'])}) + IACUC Assignment Date: {(info['AssignmentDate'] === null ? 'Not found' : info['AssignmentDate'].split(' ')[0])}, + Supervising Vet: {(info['Veterinarian'] === null ? 'Not recorded' : info['Veterinarian'])}); } else { - display.push(Description: {info['Description']}) + display.push(Description: {info['Description']}); } } handleSetAdmissionInfo(display); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx b/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx index 5878bad2..5c6ff23a 100644 --- a/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/components/EventListingGridPanel.tsx @@ -14,11 +14,13 @@ import { ProcedureEntryModal } from './ProcedureEntryModal'; import { AdmissionInfoPopover } from './AdmissionInfoPopover'; interface EventListingProps { - subjectIDs: string[] + subjectIDs: string[], + onChange: (message: string, status: string) => any, + onError: (message: string) => any } export const EventListingGridPanelImpl: FC = memo((props: EventListingProps & InjectedQueryModels) => { - const { subjectIDs, actions, queryModels } = props; + const {subjectIDs, actions, queryModels, onChange, onError} = props; const [showDialog, setShowDialog] = useState(''); const [eventID, setEventID] = useState(''); @@ -30,22 +32,22 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve const {queryInfo} = queryModels[subjectIDs[0]]; if (queryInfo) { const queryCols = new ExtendedMap(); - queryInfo.columns.forEach( (column, key) => { + queryInfo.columns.forEach((column, key) => { if ((column.name === 'HTMLNarrative')) { - const htmlCol = new QueryColumn({...column, ...{"cell": htmlRenderer}}); + const htmlCol = new QueryColumn({...column, ...{'cell': htmlRenderer}}); queryCols.set(key, htmlCol); } else if ((column.name === 'SubjectId')) { - const editCol = new QueryColumn({...column, ...{"cell": editButtonRenderer}}); + const editCol = new QueryColumn({...column, ...{'cell': editButtonRenderer}}); queryCols.set(key, editCol); } else if ((column.name === 'AdmitChargeId')) { - const admitCol = new QueryColumn({...column, ...{"cell": admitChargeIdPopoverRenderer}}); + const admitCol = new QueryColumn({...column, ...{'cell': admitChargeIdPopoverRenderer}}); queryCols.set(key, admitCol); } else { queryCols.set(key, column); } }); - const newQueryInfo = new QueryInfo({...queryInfo, ...{"columns": queryCols}}); + const newQueryInfo = new QueryInfo({...queryInfo, ...{'columns': queryCols}}); // Update QueryModel with new QueryInfo setQueryModel( @@ -55,7 +57,7 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve ); } } - })() + })(); }, [queryModels[subjectIDs[0]]]); @@ -83,9 +85,9 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve const htmlRenderer = (data) => { return ( -
+
); - } + }; const handleClick = (value) => { // Code to run when the button is clicked @@ -93,24 +95,37 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve toggleDialog('edit'); }; + const handleCloseUpdateModal = (message?: string, status?: string) => { + if (message && status) { + actions.loadModel(subjectIDs[0]); + closeDialog(); + onChange(message, status); + } else if (message) { + closeDialog(); + onError(message); + } + }; + const editButtonRenderer = (data, row) => { return (
- {data.get('value')} - + {data.get('value')} +
); - } + }; const admitChargeIdPopoverRenderer = (data, row) => { const admitChargeId = data.get('value'); const eventId = row.get('EventId').get('value'); return ( - - ) - } + + ); + }; /** * Render the custom buttons that will be displayed on the grid @@ -118,8 +133,8 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve const renderButtons = () => { return (
- {/*{}
@@ -130,7 +145,7 @@ export const EventListingGridPanelImpl: FC = memo((props: Eve
{queryModel?.queryInfo && ( = memo((props: Eve )} {showDialog === 'edit' && ( )} {showDialog === 'create' && ( - + )}
- ) + ); }); const EventListingGridPanelWithModels = withQueryModels(EventListingGridPanelImpl); -export const EventListingGridPanel: FC = memo((props: EventListingProps ) => { - const { subjectIDs } = props; +export const EventListingGridPanel: FC = memo((props: EventListingProps) => { + const {subjectIDs} = props; const queryConfigs: QueryConfigMap = useMemo( () => ({ diff --git a/snprc_ehr/src/client/SndEventsWidget/components/ProcedureEntryModal.tsx b/snprc_ehr/src/client/SndEventsWidget/components/ProcedureEntryModal.tsx index b908d663..8cdae288 100644 --- a/snprc_ehr/src/client/SndEventsWidget/components/ProcedureEntryModal.tsx +++ b/snprc_ehr/src/client/SndEventsWidget/components/ProcedureEntryModal.tsx @@ -1,42 +1,63 @@ -import React, { FC, memo, useEffect, useState } from 'react'; +import React, { FC, memo, useEffect } from 'react'; import { Modal } from 'react-bootstrap'; -import { Alert } from '@labkey/components'; interface Props { show: boolean, onCancel: () => any, + onComplete: (message, status) => any, + onError: (message) => any, eventId?: string, subjectId?: string } + export const ProcedureEntryModal: FC = memo((props: Props) => { - const { show, onCancel, eventId, subjectId } = props; + const {show, onCancel, onComplete, onError, eventId, subjectId} = props; useEffect(() => { const script = document.createElement('script'); - script.src = '/labkey/snprc_mobile/app/widget.js' - script.type = 'application/javascript' + script.src = '/labkey/snprc_mobile/app/widget.js'; + script.type = 'application/javascript'; script.onload = () => { - const ProcedureEntryWidget = document.getElementById('procedure-entry-widget') + const ProcedureEntryWidget = document.getElementById('procedure-entry-widget'); if (eventId) { - ProcedureEntryWidget.setAttribute('data-event-id', eventId) + ProcedureEntryWidget.setAttribute('data-event-id', eventId); } else if (subjectId) { - ProcedureEntryWidget.setAttribute('data-subject-id', subjectId) + ProcedureEntryWidget.setAttribute('data-subject-id', subjectId); } - } + window.addEventListener('saveEventComplete', handleSaveEventComplete); + window.addEventListener('widgetDataError', handleWidgetDataError); + }; - document.getElementById("modal-body").insertBefore(script, document.getElementById("modal-body").firstChild) + const handleBodyClick = (event: Event) => { + let targetElement = event.target as HTMLElement; // Casting to HTMLElement + if (targetElement && (targetElement.className.includes('widget-cancel') || (targetElement.parentElement && targetElement.parentElement.className.includes('widget-cancel')))) { + onCancel(); + } + }; - return () => { - document.getElementById("modal-body").removeChild(script); + const handleSaveEventComplete = (event) => { + onComplete(event.detail.message, event.detail.status); }; - }, []) + const handleWidgetDataError = (event) => { + onError(event.detail.message); + }; + + document.body.addEventListener('click', handleBodyClick); + + document.getElementById('modal-body').insertBefore(script, document.getElementById('modal-body').firstChild); + + return () => { + document.getElementById('modal-body').removeChild(script); + }; + }, []); return ( - + {eventId && (
@@ -46,5 +67,5 @@ export const ProcedureEntryModal: FC = memo((props: Props) => { )}
- ) + ); }); \ No newline at end of file