diff --git a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js index babf3bf69..7582c0960 100644 --- a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js +++ b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js @@ -145,8 +145,4 @@ CornerstoneViewportDownloadForm.propTypes = { activeViewportIndex: PropTypes.number.isRequired, }; -// export default CornerstoneViewportDownloadForm; - -export default function HelloWorld() { - return
Hello World
; -} +export default CornerstoneViewportDownloadForm; diff --git a/extensions/default/package.json b/extensions/default/package.json index 09342a611..74e36c504 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -28,6 +28,8 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", + "@ohif/i18n": "^0.52.8", + "react-i18next": "^10.11.0", "prop-types": "^15.6.2", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 33bce395d..93a7b1b2f 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -17,8 +17,10 @@ const { DicomMetaDictionary, DicomDict } = dcmjs.data; const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; const { urlUtil } = utils; -const VERSION_NAME = 'OHIF-0.1'; -const SR_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1'; +const ImplementationClassUID = + '2.25.270695996825855179949881587723571202391.2.0.0'; +const ImplementationVersionName = 'OHIF-VIEWER-2.0.0'; +const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; /** * @@ -140,9 +142,9 @@ function createDicomWebApi(dicomWebConfig) { dataset._meta.FileMetaInformationVersion.Value, MediaStorageSOPClassUID: dataset.SOPClassUID, MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, - TransferSyntaxUID: SR_TRANSFER_SYNTAX_UID, - ImplementationClassUID: DicomMetaDictionary.uid(), - ImplementationVersionName: VERSION_NAME, + TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + ImplementationClassUID, + ImplementationVersionName, }; const denaturalized = denaturalizeDataset(meta); diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js index 83613dc0c..00a390424 100644 --- a/extensions/default/src/DicomWebDataSource/qido.js +++ b/extensions/default/src/DicomWebDataSource/qido.js @@ -22,7 +22,7 @@ * | limit | {number} | * | offset | {number} | */ -import { DICOMWeb } from '@ohif/core'; +import { DICOMWeb, utils } from '@ohif/core'; const { getString, getName, getModalities } = DICOMWeb; @@ -50,7 +50,7 @@ function processResults(qidoStudies) { time: getString(qidoStudy['00080030']), // HHmmss.SSS (24-hour, minutes, seconds, fractional seconds) accession: getString(qidoStudy['00080050']) || '', // short string, probably a number? mrn: getString(qidoStudy['00100020']) || '', // medicalRecordNumber - patientName: getName(qidoStudy['00100010']) || '', + patientName: utils.formatPN(getName(qidoStudy['00100010'])) || '', instances: Number(getString(qidoStudy['00201208'])) || 0, // number description: getString(qidoStudy['00081030']) || '', modalities: diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index e2647aca6..221395926 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -127,7 +127,6 @@ function PanelStudyBrowser({ changedDisplaySets, thumbnailImageSrcMap ); - setDisplaySets(mappedDisplaySets); } ); @@ -152,11 +151,11 @@ function PanelStudyBrowser({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + [ + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx index aac3b1151..ca7bc92c6 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx @@ -5,22 +5,40 @@ import { useViewportGrid, } from '@ohif/ui'; +const DEFAULT_LAYOUT = { + type: 'SET_LAYOUT', + payload: { + numCols: 1, + numRows: 1, + }, +}; + function LayoutSelector() { const [isOpen, setIsOpen] = useState(false); const [viewportGridState, viewportGridService] = useViewportGrid(); - useEffect(() => { - function closeOnOutsideClick() { - if (isOpen) { - setIsOpen(false); - } + const closeOnOutsideClick = () => { + if (isOpen) { + setIsOpen(false); } + }; + + useEffect(() => { window.addEventListener('click', closeOnOutsideClick); return () => { window.removeEventListener('click', closeOnOutsideClick); }; }, [isOpen]); + useEffect(() => { + /* Reset to default layout when component unmounts */ + return () => { + dispatch(DEFAULT_LAYOUT); + }; + }, []); + + const onClickHandler = () => setIsOpen(!isOpen); + const DropdownContent = isOpen ? OHIFLayoutSelector : null; return ( @@ -28,9 +46,7 @@ function LayoutSelector() { id="Layout" label="Grid Layout" icon="tool-layout" - onClick={() => { - setIsOpen(!isOpen); - }} + onClick={onClickHandler} dropdownContent={ DropdownContent !== null && ( { + const modalComponent = () => ( +
{t('AboutModal:OHIF Viewer - About')}
+ ); + show({ + title: t('AboutModal:OHIF Viewer - About'), + content: modalComponent, + }); + }, [show, t]); + + // TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED + const showPreferencesModal = useCallback(() => { + const modalComponent = () => ( +
{t('UserPreferencesModal:User Preferences')}
+ ); + show({ + title: t('UserPreferencesModal:User Preferences'), + content: modalComponent, + }); + }, [show, t]); return ( @@ -40,18 +55,40 @@ function Header({ children }) {
{children}
- FOR INVESTIGATIONAL USE ONLY + {t('Header:INVESTIGATIONAL USE ONLY')} - {}} + - - - - + + + + + + +
diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index d17ec1469..35fe5d621 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -203,9 +203,11 @@ function OHIFCornerstoneSRViewport({ PatientSex, PatientAge, SliceThickness, + ManufacturerModelName, StudyDate, SeriesDescription, SeriesInstanceUID, + PixelSpacing, SeriesNumber, } = activeDisplaySetData; @@ -233,13 +235,10 @@ function OHIFCornerstoneSRViewport({ updateViewport(newMeasurementSelected); }; - console.log(currentImageIdIndex); - return ( <> diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index e13e9d504..2f4b279ce 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -198,10 +198,10 @@ function PanelStudyBrowserTracking({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -282,7 +282,6 @@ function _mapDisplaySets( ) { const thumbnailDisplaySets = []; const thumbnailNoImageDisplaySets = []; - displaySets.forEach(ds => { const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID]; const componentType = _getComponentType(ds.Modality); diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index c76efba79..79138339a 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -10,8 +10,6 @@ import { useViewportGrid, useViewportDialog, } from '@ohif/ui'; -import debounce from 'lodash.debounce'; -import throttle from 'lodash.throttle'; import { useTrackedMeasurements } from './../getContextModule'; // TODO -> Get this list from the list of tracked measurements. @@ -40,7 +38,6 @@ function TrackedCornerstoneViewport({ viewportIndex, }) { const [trackedMeasurements] = useTrackedMeasurements(); - const [{ activeViewportIndex, viewports }] = useViewportGrid(); // viewportIndex, onSubmit const [viewportDialogState, viewportDialogApi] = useViewportDialog(); @@ -218,15 +215,17 @@ function TrackedCornerstoneViewport({ SeriesInstanceUID, SeriesNumber, } = displaySet; + const { PatientID, PatientName, PatientSex, PatientAge, SliceThickness, + PixelSpacing, + ManufacturerModelName } = displaySet.images[0]; - if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { setIsTracked(!isTracked); } @@ -235,7 +234,6 @@ function TrackedCornerstoneViewport({ <> alert(`Series ${direction}`)} - showPatientInfo={viewportIndex === activeViewportIndex} showNavArrows={viewportIndex === activeViewportIndex} studyData={{ label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid], @@ -246,13 +244,13 @@ function TrackedCornerstoneViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? PatientName.Alphabetic || '' : '', + patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', thickness: `${SliceThickness}mm`, - spacing: '', - scanner: '', + spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '', + scanner: ManufacturerModelName || '', }, }} /> diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index dac463a12..2b3cfe172 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -82,6 +82,11 @@ const storeMeasurementsOld = async (measurementData, filter, server) => { } }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + */ const downloadReport = measurementData => { const srDataset = generateReport(measurementData); const reportBlob = dcmjs.data.datasetToBlob(srDataset); @@ -91,6 +96,11 @@ const downloadReport = measurementData => { window.location.assign(objectUrl); }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + */ const generateReport = measurementData => { const ids = measurementData.map(md => md.id); const filteredToolState = _getFilteredCornerstoneToolState(ids); @@ -103,6 +113,12 @@ const generateReport = measurementData => { return report.dataset; }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + * @param {object} dataSource The dataSource that you wish to use to persist the data. + */ const storeMeasurements = async (measurementData, dataSource) => { // TODO -> Eventually use the measurements directly and not the dcmjs adapter, // But it is good enough for now whilst we only have cornerstone as a datasource. diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 14dfde227..0ce25d6b6 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -137,6 +137,7 @@ export default class ExtensionManager { getDataSources = dataSourceName => { if (dataSourceName === undefined) { + // Default to the activeDataSource dataSourceName = this.activeDataSource; } @@ -144,6 +145,10 @@ export default class ExtensionManager { return this.dataSourceMap[dataSourceName]; }; + getActiveDataSource = () => { + return this.activeDataSource; + }; + /** * @private * @param {string} moduleType diff --git a/platform/core/src/services/UIModalService/index.js b/platform/core/src/services/UIModalService/index.js index ed2eab96a..884e53500 100644 --- a/platform/core/src/services/UIModalService/index.js +++ b/platform/core/src/services/UIModalService/index.js @@ -33,7 +33,7 @@ const serviceImplementation = { function _show({ content = null, contentProps = null, - shouldCloseOnEsc = false, + shouldCloseOnEsc = true, isOpen = true, closeButton = true, title = null, diff --git a/platform/core/src/utils/formatPN.js b/platform/core/src/utils/formatPN.js new file mode 100644 index 000000000..fb019ab5e --- /dev/null +++ b/platform/core/src/utils/formatPN.js @@ -0,0 +1,18 @@ +/** + * Formats a patient name for display purposes + */ +export default function formatPN(name) { + if (!name) { + return; + } + + // Convert the first ^ to a ', '. String.replace() only affects + // the first appearance of the character. + const commaBetweenFirstAndLast = name.replace('^', ', '); + + // Replace any remaining '^' characters with spaces + const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + + // Trim any extraneous whitespace + return cleaned.trim(); +} diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 6e9ea3140..a69889262 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -15,6 +15,7 @@ import makeCancelable from './makeCancelable'; import hotkeys from './hotkeys'; import Queue from './Queue'; import isDicomUid from './isDicomUid'; +import formatPN from './formatPN'; import resolveObjectPath from './resolveObjectPath'; import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as progressTrackingUtils from './progressTrackingUtils'; @@ -26,6 +27,7 @@ const utils = { addServers, sortBy, writeScript, + formatPN, b64toBlob, StackManager, studyMetadataManager, diff --git a/platform/ui/index.js b/platform/ui/index.js index 2025cfb3b..e3c8a8f25 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -31,6 +31,7 @@ export { ButtonGroup, DateRange, Dialog, + Dropdown, EmptyStudies, ExpandableToolbarButton, ListMenu, @@ -75,6 +76,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, } from './src/components'; diff --git a/platform/ui/src/assets/icons/close.svg b/platform/ui/src/assets/icons/close.svg new file mode 100644 index 000000000..83b259fca --- /dev/null +++ b/platform/ui/src/assets/icons/close.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/link.svg b/platform/ui/src/assets/icons/link.svg new file mode 100644 index 000000000..1f7f94009 --- /dev/null +++ b/platform/ui/src/assets/icons/link.svg @@ -0,0 +1,9 @@ + + + + diff --git a/platform/ui/src/assets/icons/unlink.svg b/platform/ui/src/assets/icons/unlink.svg new file mode 100644 index 000000000..ed9526b7f --- /dev/null +++ b/platform/ui/src/assets/icons/unlink.svg @@ -0,0 +1,9 @@ + + Unlink + + diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx new file mode 100644 index 000000000..b0b58497f --- /dev/null +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -0,0 +1,119 @@ +import React, { useEffect, useCallback, useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { Icon, Typography } from '@ohif/ui'; + +const Dropdown = ({ children, showDropdownIcon, list }) => { + const [open, setOpen] = useState(false); + const element = useRef(null); + + const DropdownItem = useCallback(({ title, icon, onClick }) => { + return ( +
{ + setOpen(false); + onClick(); + }} + > + {!!icon && } + {title} +
+ ); + }, []); + + DropdownItem.defaultProps = { + icon: '', + }; + + DropdownItem.propTypes = { + title: PropTypes.string.isRequired, + icon: PropTypes.string, + onClick: PropTypes.func.isRequired, + }; + + const renderTitleElement = () => { + return ( +
+ {children} + {showDropdownIcon && ( + + )} +
+ ); + }; + + const toggleList = () => { + setOpen(s => !s); + }; + + const handleClick = e => { + if (element.current && !element.current.contains(e.target)) { + setOpen(false); + } + }; + + const renderList = () => { + return ( +
+ {list.map((item, idx) => ( + + ))} +
+ ); + }; + + useEffect(() => { + document.addEventListener('click', handleClick); + + if (!open) { + document.removeEventListener('click', handleClick); + } + }, [open]); + + return ( +
+
+ {renderTitleElement()} +
+ + {renderList()} +
+ ); +}; + +Dropdown.defaultProps = { + showDropdownIcon: true, +}; + +Dropdown.propTypes = { + children: PropTypes.node.isRequired, + showDropdownIcon: PropTypes.bool, + /** Items to render in the select's drop down */ + list: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + icon: PropTypes.string, + onClick: PropTypes.func.isRequired, + }) + ).isRequired, +}; + +export default Dropdown; diff --git a/platform/ui/src/components/Dropdown/Dropdown.mdx b/platform/ui/src/components/Dropdown/Dropdown.mdx new file mode 100644 index 000000000..dc4b02555 --- /dev/null +++ b/platform/ui/src/components/Dropdown/Dropdown.mdx @@ -0,0 +1,68 @@ +--- +name: Dropdown +menu: General +route: components/dropdown +--- + +import { Playground, Props } from 'docz'; +import { Dropdown, IconButton, Icon } from '@ohif/ui'; + +# Dropdown + +This component is used when there are more than a few options to choose from. By +hovering or clicking on the trigger, a dropdown menu will appear, which allows +you to choose an option and execute the relevant action. + +## Import + +```javascript +import { Dropdown } from '@ohif/ui'; +``` + + + {() => { + const handleClick = () => { + alert("Clicked"); + } + return ( +
+ + + + + + + + +
+ ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/Dropdown/index.js b/platform/ui/src/components/Dropdown/index.js new file mode 100644 index 000000000..453771730 --- /dev/null +++ b/platform/ui/src/components/Dropdown/index.js @@ -0,0 +1 @@ +export { default } from './Dropdown'; diff --git a/platform/ui/src/components/EmptyStudies/EmptyStudies.js b/platform/ui/src/components/EmptyStudies/EmptyStudies.js index 48e3baaa6..893ecf3e3 100644 --- a/platform/ui/src/components/EmptyStudies/EmptyStudies.js +++ b/platform/ui/src/components/EmptyStudies/EmptyStudies.js @@ -3,12 +3,13 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Icon, Typography } from '@ohif/ui'; -const EmptyStudies = ({ className }) => { +// TODO: Add loading spinner to OHIF + use it here. +const EmptyStudies = ({ className, isLoading }) => { return (
- No studies available + {!isLoading ? 'No studies available' : 'Loading...'}
); diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 145539060..8f0a7a534 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -4,6 +4,7 @@ import React from 'react'; import arrowDown from './../../assets/icons/arrow-down.svg'; import calendar from './../../assets/icons/calendar.svg'; import cancel from './../../assets/icons/cancel.svg'; +import close from './../../assets/icons/close.svg'; import dottedCircle from './../../assets/icons/dotted-circle.svg'; import circledCheckmark from './../../assets/icons/circled-checkmark.svg'; import chevronDown from './../../assets/icons/chevron-down.svg'; @@ -16,6 +17,7 @@ import info from './../../assets/icons/info.svg'; import infoLink from './../../assets/icons/info-link.svg'; import launchArrow from './../../assets/icons/launch-arrow.svg'; import launchInfo from './../../assets/icons/launch-info.svg'; +import link from './../../assets/icons/link.svg'; import listBullets from './../../assets/icons/list-bullets.svg'; import lock from './../../assets/icons/lock.svg'; import logoOhifSmall from './../../assets/icons/logo-ohif-small.svg'; @@ -30,6 +32,7 @@ import sorting from './../../assets/icons/sorting.svg'; import sortingActiveDown from './../../assets/icons/sorting-active-down.svg'; import sortingActiveUp from './../../assets/icons/sorting-active-up.svg'; import tracked from './../../assets/icons/tracked.svg'; +import unlink from './../../assets/icons/unlink.svg'; /** Tools */ import toolZoom from './../../assets/icons/tool-zoom.svg'; @@ -47,6 +50,7 @@ const ICONS = { 'arrow-down': arrowDown, calendar: calendar, cancel: cancel, + close: close, 'dotted-circle': dottedCircle, 'circled-checkmark': circledCheckmark, 'chevron-down': chevronDown, @@ -59,6 +63,7 @@ const ICONS = { 'info-link': infoLink, 'launch-arrow': launchArrow, 'launch-info': launchInfo, + link: link, 'list-bullets': listBullets, lock: lock, 'logo-ohif-small': logoOhifSmall, @@ -73,6 +78,7 @@ const ICONS = { 'sorting-active-up': sortingActiveUp, sorting: sorting, tracked: tracked, + unlink: unlink, /** Tools */ 'tool-zoom': toolZoom, diff --git a/platform/ui/src/components/IconButton/IconButton.jsx b/platform/ui/src/components/IconButton/IconButton.jsx index d92a6b9ca..290bfa273 100644 --- a/platform/ui/src/components/IconButton/IconButton.jsx +++ b/platform/ui/src/components/IconButton/IconButton.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; const baseClasses = - 'text-center items-center justify-center outline-none font-bold focus:outline-none'; + 'text-center items-center justify-center transition duration-300 ease-in-out outline-none font-bold focus:outline-none'; const roundedClasses = { none: '', @@ -84,7 +84,7 @@ const IconButton = ({ }) => { const buttonElement = useRef(null); - const handleOnClick = (e) => { + const handleOnClick = e => { buttonElement.current.blur(); onClick(e); }; @@ -113,7 +113,7 @@ const IconButton = ({ }; IconButton.defaultProps = { - onClick: () => { }, + onClick: () => {}, color: 'default', disabled: false, fullWidth: false, diff --git a/platform/ui/src/components/Input/Input.jsx b/platform/ui/src/components/Input/Input.jsx index 51b40f0e5..8f26ad4e7 100644 --- a/platform/ui/src/components/Input/Input.jsx +++ b/platform/ui/src/components/Input/Input.jsx @@ -4,11 +4,11 @@ import Label from '../Label'; import classnames from 'classnames'; const baseInputClasses = - 'shadow transition duration-300 appearance-none border rounded w-full py-2 px-3 text-sm text-white hover:border-gray-500 leading-tight focus:border-gray-500 focus:outline-none'; + 'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none'; const transparentClasses = { true: 'bg-transparent', - false: '', + false: 'bg-black', }; const Input = ({ @@ -16,7 +16,7 @@ const Input = ({ containerClassName = '', labelClassName = '', className = '', - transparent = true, + transparent = false, type = 'text', value, onChange, diff --git a/platform/ui/src/components/InputText/InputText.jsx b/platform/ui/src/components/InputText/InputText.jsx index 2a244d7e8..2f6fbe70b 100644 --- a/platform/ui/src/components/InputText/InputText.jsx +++ b/platform/ui/src/components/InputText/InputText.jsx @@ -23,7 +23,7 @@ const InputText = ({ type="text" containerClassName="mr-2" value={value} - onChange={(event) => { + onChange={event => { onChange(event.target.value); }} /> @@ -33,15 +33,17 @@ const InputText = ({ InputText.defaultProps = { value: '', + isSortable: false, + onLabelClick: () => {}, + sortDirection: 'none', }; InputText.propTypes = { label: PropTypes.string.isRequired, - isSortable: PropTypes.bool.isRequired, - sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']) - .isRequired, - onLabelClick: PropTypes.func.isRequired, - value: PropTypes.string, + isSortable: PropTypes.bool, + sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']), + onLabelClick: PropTypes.func, + value: PropTypes.any, onChange: PropTypes.func.isRequired, }; diff --git a/platform/ui/src/components/Modal/Modal.css b/platform/ui/src/components/Modal/Modal.css new file mode 100644 index 000000000..f67d31616 --- /dev/null +++ b/platform/ui/src/components/Modal/Modal.css @@ -0,0 +1,3 @@ +.modal-content { + max-height: calc(100vh - theme('spacing.250px')); +} diff --git a/platform/ui/src/components/Modal/Modal.jsx b/platform/ui/src/components/Modal/Modal.jsx index 4ef669cbf..3a245f8aa 100644 --- a/platform/ui/src/components/Modal/Modal.jsx +++ b/platform/ui/src/components/Modal/Modal.jsx @@ -1,22 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import ReactModal from 'react-modal'; -import classNames from 'classnames'; -const customStyle = { - overlay: { - zIndex: 1071, - backgroundColor: 'rgb(0, 0, 0, 0.5)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, -}; +import './Modal.css'; + +import { Typography, useModal, IconButton, Icon } from '@ohif/ui'; ReactModal.setAppElement(document.getElementById('root')); const Modal = ({ - className, closeButton, shouldCloseOnEsc, isOpen, @@ -24,15 +16,27 @@ const Modal = ({ onClose, children, }) => { + const { hide } = useModal(); + + const handleClose = () => { + hide(); + }; + const renderHeader = () => { return ( title && ( -
-

{title}

+
+ {title} {closeButton && ( - + + + )}
) @@ -41,22 +45,26 @@ const Modal = ({ return ( - <> - {renderHeader()} -
{children}
- +
{renderHeader()}
+
+ {children} +
); }; +Modal.defaultProps = { + shouldCloseOnEsc: true, +}; + Modal.propTypes = { - className: PropTypes.string, closeButton: PropTypes.bool, shouldCloseOnEsc: PropTypes.bool, isOpen: PropTypes.bool, diff --git a/platform/ui/src/components/NavBar/NavBar.jsx b/platform/ui/src/components/NavBar/NavBar.jsx index 5ab60f626..8adc0ce3b 100644 --- a/platform/ui/src/components/NavBar/NavBar.jsx +++ b/platform/ui/src/components/NavBar/NavBar.jsx @@ -8,7 +8,7 @@ const NavBar = ({ className, children, isSticky }) => { return (
{ - const newSelection = !selectedOptions.length ? selectedOptions : selectedOptions.reduce( - (acc, curr) => acc.concat([curr.value]), - [] - ); + const newSelection = !selectedOptions.length + ? selectedOptions + : selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []); onChange(newSelection, action); }} > diff --git a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx index 2d409a555..0d06bb2e3 100644 --- a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx +++ b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Icon, Typography, InputGroup } from '@ohif/ui'; +import { Button, Icon, Typography, InputGroup, useModal } from '@ohif/ui'; const StudyListFilter = ({ filtersMeta, @@ -20,6 +20,16 @@ const StudyListFilter = ({ }); }; const isSortingEnable = numOfStudies > 0 && numOfStudies <= 100; + const { show } = useModal(); + + const showLearnMoreContent = () => { + const modalContent = () =>
Search Instructions
; + + show({ + content: modalContent, + title: 'Learn More', + }); + }; return ( @@ -38,6 +48,7 @@ const StudyListFilter = ({ color="inherit" className="text-primary-active" startIcon={} + onClick={showLearnMoreContent} > Learn more diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index 9133b522c..9129d871c 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Icon, ButtonGroup, Button, Tooltip } from '@ohif/ui'; @@ -13,9 +13,11 @@ const classes = { const ViewportActionBar = ({ studyData, showNavArrows, - showPatientInfo, + showPatientInfo: patientInfoVisibility, onSeriesChange, }) => { + const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); + const { label, isTracked, @@ -37,6 +39,8 @@ const ViewportActionBar = ({ scanner, } = patientInformation; + const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo) + const renderIconStatus = () => { if (modality === 'SR') { return ( @@ -60,29 +64,30 @@ const ViewportActionBar = ({ {!isTracked ? ( ) : ( - -
- -
-
- - Series is + +
+ +
+
+ + Series is tracked and can be viewed
in the measurement panel
+
-
- } - > - - - )} + } + > + + + )} ); }; + return (
@@ -131,19 +136,18 @@ const ViewportActionBar = ({
)} - {showPatientInfo && ( -
- -
- )} +
+ +
); }; @@ -174,7 +178,7 @@ ViewportActionBar.propTypes = { ViewportActionBar.defaultProps = { showNavArrows: true, - showPatientInfo: true, + showPatientInfo: false, }; function PatientInfo({ @@ -185,11 +189,14 @@ function PatientInfo({ thickness, spacing, scanner, + isOpen, }) { return (
@@ -236,7 +243,7 @@ function PatientInfo({
- } + )} >
diff --git a/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx new file mode 100644 index 000000000..06c682502 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx @@ -0,0 +1,432 @@ +import React, { + useCallback, + useEffect, + useState, + createRef, + useRef, +} from 'react'; + +import classnames from 'classnames'; + +import { + Typography, + Input, + Tooltip, + IconButton, + Icon, + Select, + InputLabelWrapper, + Button, +} from '@ohif/ui'; + +const FILE_TYPE_OPTIONS = [ + { + value: 'jpg', + label: 'jpg', + }, + { + value: 'png', + label: 'png', + }, +]; + +const DEFAULT_FILENAME = 'image'; +const REFRESH_VIEWPORT_TIMEOUT = 1000; + +const ViewportDownloadForm = ({ + activeViewport, + onClose, + updateViewportPreview, + enableViewport, + disableViewport, + toggleAnnotations, + loadImage, + downloadBlob, + defaultSize, + minimumSize, + maximumSize, + canvasClass, +}) => { + const [filename, setFilename] = useState(DEFAULT_FILENAME); + const [fileType, setFileType] = useState(['jpg']); + + const [dimensions, setDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [showAnnotations, setShowAnnotations] = useState(true); + + const [keepAspect, setKeepAspect] = useState(true); + const [aspectMultiplier, setAspectMultiplier] = useState({ + width: 1, + height: 1, + }); + + const [viewportElement, setViewportElement] = useState(); + const [viewportElementDimensions, setViewportElementDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [downloadCanvas, setDownloadCanvas] = useState({ + ref: createRef(), + width: defaultSize, + height: defaultSize, + }); + + const [viewportPreview, setViewportPreview] = useState({ + src: null, + width: defaultSize, + height: defaultSize, + }); + + const [error, setError] = useState({ + width: false, + height: false, + filename: false, + }); + + const hasError = Object.values(error).includes(true); + + const refreshViewport = useRef(null); + + const onKeepAspectToggle = () => { + const { width, height } = dimensions; + const aspectMultiplier = { ...aspectMultiplier }; + if (!keepAspect) { + const base = Math.min(width, height); + aspectMultiplier.width = width / base; + aspectMultiplier.height = height / base; + setAspectMultiplier(aspectMultiplier); + } + + setKeepAspect(!keepAspect); + }; + + const downloadImage = () => { + downloadBlob( + filename || DEFAULT_FILENAME, + fileType, + viewportElement, + downloadCanvas.ref.current + ); + }; + + /** + * @param {object} value - Input value + * @param {string} dimension - "height" | "width" + */ + const onDimensionsChange = (value, dimension) => { + const oppositeDimension = dimension === 'height' ? 'width' : 'height'; + const sanitizedTargetValue = value.replace(/\D/, ''); + const isEmpty = sanitizedTargetValue === ''; + const newDimensions = { ...dimensions }; + const updatedDimension = isEmpty + ? '' + : Math.min(sanitizedTargetValue, maximumSize); + + if (updatedDimension === dimensions[dimension]) { + return; + } + + newDimensions[dimension] = updatedDimension; + + if (keepAspect && newDimensions[oppositeDimension] !== '') { + newDimensions[oppositeDimension] = Math.round( + newDimensions[dimension] * aspectMultiplier[oppositeDimension] + ); + } + + // In current code, keepAspect is always `true` + // And we always start w/ a square width/height + setDimensions(newDimensions); + + // Only update if value is non-empty + if (!isEmpty) { + setViewportElementDimensions(newDimensions); + setDownloadCanvas(state => ({ + ...state, + ...newDimensions, + })); + } + }; + + const error_messages = { + width: 'The minimum valid width is 100px.', + height: 'The minimum valid height is 100px.', + filename: 'The file name cannot be empty.', + }; + + const renderErrorHandler = errorType => { + if (!error[errorType]) { + return null; + } + + return ( + + {error_messages[errorType]} + + ); + }; + + const validSize = useCallback( + value => (value >= minimumSize ? value : minimumSize), + [minimumSize] + ); + + const loadAndUpdateViewports = useCallback(async () => { + const { width: scaledWidth, height: scaledHeight } = await loadImage( + activeViewport, + viewportElement, + dimensions.width, + dimensions.height + ); + + toggleAnnotations(showAnnotations, viewportElement); + + const scaledDimensions = { + height: validSize(scaledHeight), + width: validSize(scaledWidth), + }; + + setViewportElementDimensions(scaledDimensions); + setDownloadCanvas(state => ({ + ...state, + ...scaledDimensions, + })); + + const { + dataUrl, + width: viewportElementWidth, + height: viewportElementHeight, + } = await updateViewportPreview( + viewportElement, + downloadCanvas.ref.current, + fileType + ); + + setViewportPreview(state => ({ + ...state, + src: dataUrl, + width: validSize(viewportElementWidth), + height: validSize(viewportElementHeight), + })); + }, [ + loadImage, + activeViewport, + viewportElement, + dimensions.width, + dimensions.height, + toggleAnnotations, + showAnnotations, + validSize, + updateViewportPreview, + downloadCanvas.ref, + fileType, + ]); + + useEffect(() => { + enableViewport(viewportElement); + + return () => { + disableViewport(viewportElement); + }; + }, [disableViewport, enableViewport, viewportElement]); + + useEffect(() => { + if (refreshViewport.current !== null) { + clearTimeout(refreshViewport.current); + } + + refreshViewport.current = setTimeout(() => { + refreshViewport.current = null; + loadAndUpdateViewports(); + }, REFRESH_VIEWPORT_TIMEOUT); + }, [ + activeViewport, + viewportElement, + showAnnotations, + dimensions, + loadImage, + toggleAnnotations, + updateViewportPreview, + fileType, + downloadCanvas.ref, + minimumSize, + maximumSize, + loadAndUpdateViewports, + ]); + + useEffect(() => { + const { width, height } = dimensions; + const hasError = { + width: width < minimumSize, + height: height < minimumSize, + filename: !filename, + }; + + setError({ ...hasError }); + }, [dimensions, filename, minimumSize]); + + return ( +
+ + Please specify the dimensions, filename, and desired type for the output + image. + + +
+
+ setFilename(value)} + label="File Name" + /> + {renderErrorHandler('filename')} +
+
+
+
+
+ onDimensionsChange(value, 'width')} + data-cy="image-width" + /> + {renderErrorHandler('width')} +
+
+ onDimensionsChange(value, 'height')} + data-cy="image-height" + /> + {renderErrorHandler('height')} +
+
+ +
+ + + + + +
+
+ +
+
+ {}} + > + setShowAnnotations(event.target.checked)} + /> + Show Annotations + +
+
+
+
+ +
+
setViewportElement(ref)} + > + +
+ + {viewportPreview.src ? ( +
+ Image preview + Preview +
+ ) : ( +
+ Loading Image Preview... +
+ )} +
+ +
+ + +
+
+ ); +}; + +export default ViewportDownloadForm; diff --git a/platform/ui/src/components/ViewportDownloadForm/index.js b/platform/ui/src/components/ViewportDownloadForm/index.js new file mode 100644 index 000000000..d630f3a19 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/index.js @@ -0,0 +1 @@ +export { default } from './ViewportDownloadForm'; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 04b5270cc..deb140bbb 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -2,6 +2,7 @@ import Button from './Button'; import ButtonGroup from './ButtonGroup'; import DateRange from './DateRange'; import Dialog from './Dialog'; +import Dropdown from './Dropdown'; import EmptyStudies from './EmptyStudies'; import Icon from './Icon'; import IconButton from './IconButton'; @@ -45,6 +46,7 @@ import Tooltip from './Tooltip'; import Typography from './Typography'; import Viewport from './Viewport'; import ViewportActionBar from './ViewportActionBar'; +import ViewportDownloadForm from './ViewportDownloadForm'; import ViewportGrid from './ViewportGrid'; import ViewportPane from './ViewportPane'; @@ -53,6 +55,7 @@ export { ButtonGroup, DateRange, Dialog, + Dropdown, EmptyStudies, ExpandableToolbarButton, ListMenu, @@ -97,6 +100,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, }; diff --git a/platform/ui/src/contextProviders/ModalComponent.jsx b/platform/ui/src/contextProviders/ModalComponent.jsx index b3b6602a7..cf7965812 100644 --- a/platform/ui/src/contextProviders/ModalComponent.jsx +++ b/platform/ui/src/contextProviders/ModalComponent.jsx @@ -16,7 +16,7 @@ const ModalComponent = ({ ModalComponent.defaultProps = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, diff --git a/platform/ui/src/contextProviders/ModalProvider.jsx b/platform/ui/src/contextProviders/ModalProvider.jsx index e8d65635d..ff9224bca 100644 --- a/platform/ui/src/contextProviders/ModalProvider.jsx +++ b/platform/ui/src/contextProviders/ModalProvider.jsx @@ -19,7 +19,7 @@ export const useModal = () => useContext(ModalContext); * @typedef {Object} ModalProps * @property {ReactElement|HTMLElement} [content=null] Modal content. * @property {Object} [contentProps=null] Modal content props. - * @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key. + * @property {boolean} [shouldCloseOnEsc=true] Modal is dismissible via the esc key. * @property {boolean} [isOpen=true] Make the Modal visible or hidden. * @property {boolean} [closeButton=true] Should the modal body render the close button. * @property {string} [title=null] Should the modal render the title independently of the body content. @@ -30,7 +30,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => { const DEFAULT_OPTIONS = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index 5cdbcdf50..3e32e4955 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -10,6 +10,7 @@ module.exports = { xl: '1280px', }, colors: { + overlay: 'rgba(0, 0, 0, 0.8)', transparent: 'transparent', black: '#000', white: '#fff', @@ -17,10 +18,10 @@ module.exports = { inherit: 'inherit', indigo: { - dark: '#0b1a42' + dark: '#0b1a42', }, aqua: { - pale: '#7bb2ce' + pale: '#7bb2ce', }, primary: { diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 01f1e3a3d..5ab382d4f 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -1,12 +1,13 @@ // External import React from 'react'; import PropTypes from 'prop-types'; +import i18n from '@ohif/i18n'; +import { I18nextProvider } from 'react-i18next'; import { BrowserRouter, HashRouter } from 'react-router-dom'; import { DialogProvider, Modal, ModalProvider, - Notification, SnackbarProvider, ThemeWrapper, ViewportDialogProvider, @@ -59,21 +60,23 @@ function App({ config, defaultExtensions }) { return ( - - - - - - - - {appRoutes} - - - - - - - + + + + + + + + + {appRoutes} + + + + + + + + ); } diff --git a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx new file mode 100644 index 000000000..791905819 --- /dev/null +++ b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { Dropdown, IconButton, Icon, useModal } from '@ohif/ui'; + +const PreferencesDropdown = () => { + const { show } = useModal(); + + const showAboutModal = () => { + const modalComponent = () =>
About modal
; + show({ + content: modalComponent, + title: 'About', + }); + }; + + const showPreferencesModal = () => { + const modalComponent = () =>
Preferences modal
; + show({ + content: modalComponent, + title: 'Preferences', + }); + }; + + return ( + + + + + {}} + > + + + + ); +}; + +export default PreferencesDropdown; diff --git a/platform/viewer/src/components/PreferencesDropdown/index.js b/platform/viewer/src/components/PreferencesDropdown/index.js new file mode 100644 index 000000000..edf8641a2 --- /dev/null +++ b/platform/viewer/src/components/PreferencesDropdown/index.js @@ -0,0 +1 @@ +export { default } from './PreferencesDropdown'; diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index c9300381f..f82fc5f18 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -1,10 +1,12 @@ /** * CSS Grid Reference: http://grid.malven.co/ */ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import EmptyViewport from './EmptyViewport'; +import { classes } from '@ohif/core'; +const { ImageSet } = classes; function ViewerViewportGrid(props) { const { servicesManager, viewportComponents, dataSource } = props; @@ -20,6 +22,29 @@ function ViewerViewportGrid(props) { // TODO -> Need some way of selecting which displaySets hit the viewports. const { DisplaySetService } = servicesManager.services; + useEffect(() => { + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED, + displaySets => { + displaySets.sort((a, b) => { + const isImageSet = x => x instanceof ImageSet; + return (isImageSet(a) === isImageSet(b)) ? 0 : isImageSet(a) ? -1 : 1; + }); + dispatch({ + type: 'SET_DISPLAYSET_FOR_VIEWPORT', + payload: { + viewportIndex: 0, + displaySetInstanceUID: displaySets[0].displaySetInstanceUID, + }, + }); + }, + ); + + return () => { + unsubscribe(); + }; + }, []); + // TODO -> Make a HangingProtocolService const HangingProtocolService = displaySets => { let displaySetInstanceUID; diff --git a/platform/viewer/src/routes/DataSourceWrapper.jsx b/platform/viewer/src/routes/DataSourceWrapper.jsx index d0a0711e3..158594128 100644 --- a/platform/viewer/src/routes/DataSourceWrapper.jsx +++ b/platform/viewer/src/routes/DataSourceWrapper.jsx @@ -46,14 +46,17 @@ function DataSourceWrapper(props) { // studies.processResults --> // But only for LayoutTemplate type of 'list'? // Or no data fetching here, and just hand down my source - const [data, setData] = useState(); + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { // 204: no content async function getData() { + setIsLoading(true); const searchResults = await dataSource.query.studies.search( queryFilterValues ); setData(searchResults); + setIsLoading(false); } try { @@ -61,23 +64,19 @@ function DataSourceWrapper(props) { } catch (ex) { console.warn(ex); } - console.log('DataSourceWrapper: useEffect'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [history.location.search]); // queryFilterValues // TODO: Better way to pass DataSource? return ( - - {data && ( - - )} - + ); } diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx index 264bc87e2..f7edc664d 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.jsx +++ b/platform/viewer/src/routes/WorkList/WorkList.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { Suspense, useState, useEffect } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; @@ -9,13 +9,14 @@ import filtersMeta from './filtersMeta.js'; import { useAppConfig } from '@state'; import { useDebounce, useQuery } from '@hooks'; +import PreferencesDropdown from '../../components/PreferencesDropdown'; + import { Icon, StudyListExpandedRow, Button, NavBar, Svg, - IconButton, EmptyStudies, StudyListTable, StudyListPagination, @@ -28,15 +29,16 @@ const seriesInStudiesMap = new Map(); * TODO: * - debounce `setFilterValues` (150ms?) */ -function WorkList({ history, data: studies, dataSource }) { +function WorkList({ history, data: studies, isLoadingData, dataSource }) { // ~ Modes const [appConfig] = useAppConfig(); // ~ Filters const query = useQuery(); const queryFilterValues = _getQueryFilterValues(query); - const [filterValues, _setFilterValues] = useState( - Object.assign({}, defaultFilterValues, queryFilterValues) - ); + const [filterValues, _setFilterValues] = useState({ + ...defaultFilterValues, + ...queryFilterValues, + }); const debouncedFilterValues = useDebounce(filterValues, 200); const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues; @@ -80,6 +82,7 @@ function WorkList({ history, data: studies, dataSource }) { return 0; }); + // ~ Rows & Studies const [expandedRows, setExpandedRows] = useState([]); const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]); @@ -190,6 +193,7 @@ function WorkList({ history, data: studies, dataSource }) { return filterValues[name] !== defaultFilterValues[name]; }); }; + const tableDataSource = sortedStudies.map((study, key) => { const rowKey = key + 1; const isExpanded = expandedRows.some(k => k === rowKey); @@ -364,17 +368,7 @@ function WorkList({ history, data: studies, dataSource }) { FOR INVESTIGATIONAL USE ONLY - {}} - > - - - - - +
) : (
- +
)}