Merge branch 'feat/v2-main' of github.com:OHIF/Viewers into feat/ohif-125

This commit is contained in:
igoroctaviano 2020-06-26 12:38:18 -03:00
commit a197b887fb
53 changed files with 962 additions and 184 deletions

View File

@ -34,7 +34,7 @@
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",

View File

@ -145,8 +145,4 @@ CornerstoneViewportDownloadForm.propTypes = {
activeViewportIndex: PropTypes.number.isRequired,
};
// export default CornerstoneViewportDownloadForm;
export default function HelloWorld() {
return <div>Hello World</div>;
}
export default CornerstoneViewportDownloadForm;

View File

@ -1,5 +1,4 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import getPointsFromHandles from './utils/getPointsFromHandles';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';

View File

@ -32,7 +32,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"webpack": "^4.0.0",
"dcmjs": "^0.12.4"
"dcmjs": "0.14.0"
},
"dependencies": {
"@babel/runtime": "7.7.6"

View File

@ -12,9 +12,16 @@ import getImageId from './utils/getImageId';
import * as dcmjs from 'dcmjs';
import { retrieveStudyMetadata } from './retrieveStudyMetadata.js';
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
const { urlUtil } = utils;
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';
/**
*
* @param {string} name - Data source name
@ -128,6 +135,32 @@ function createDicomWebApi(dicomWebConfig) {
},
},
},
store: {
dicom: async dataset => {
const meta = {
FileMetaInformationVersion:
dataset._meta.FileMetaInformationVersion.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
ImplementationClassUID,
ImplementationVersionName,
};
const denaturalized = denaturalizeDataset(meta);
const dicomDict = new DicomDict(denaturalized);
dicomDict.dict = denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const options = {
datasets: [part10Buffer],
};
await wadoDicomWebClient.storeInstances(options);
},
},
retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => {
if (!StudyInstanceUID) {
throw new Error(

View File

@ -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:

View File

@ -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);

View File

@ -20,7 +20,7 @@ function WrappedPanelStudyBrowser({
}) {
// TODO: This should be made available a different way; route should have
// already determined our datasource
const dataSource = extensionManager.getDataSources('dicomweb')[0];
const dataSource = extensionManager.getDataSources()[0];
const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind(
null,
dataSource

View File

@ -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, dispatch] = 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 && (
<DropdownContent

View File

@ -28,7 +28,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.11.0",
"react-dom": "^16.11.0"

View File

@ -31,7 +31,7 @@
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"

View File

@ -31,7 +31,7 @@
"@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.15.1",
"dcmjs": "^0.12.2",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"

View File

@ -34,7 +34,7 @@
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.16.0",
"dcmjs": "^0.12.3",
"dcmjs": "0.14.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",

View File

@ -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 (
<>
<ViewportActionBar
onSeriesChange={onMeasurementChange}
showPatientInfo={viewportIndex === activeViewportIndex}
showNavArrows={viewportIndex === activeViewportIndex}
studyData={{
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
@ -250,13 +249,13 @@ function OHIFCornerstoneSRViewport({
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 || '',
},
}}
/>

View File

@ -28,11 +28,12 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.12.4",
"dcmjs": "0.14.0",
"prop-types": "^15.6.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"webpack": "^4.0.0"
"webpack": "^4.0.0",
"cornerstone-tools": "4.15.1"
},
"dependencies": {
"@babel/runtime": "7.7.6",

View File

@ -2,10 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
function ActionButtons() {
function ActionButtons({ onExportClick, onCreateReportClick }) {
return (
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<ButtonGroup onClick={onExportClick}>
<Button
className="px-2 py-2 text-base text-white bg-black border-primary-main"
size="initial"
@ -26,7 +26,7 @@ function ActionButtons() {
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
onClick={onCreateReportClick}
>
Create Report
</Button>
@ -34,4 +34,14 @@ function ActionButtons() {
);
}
ActionButtons.propTypes = {
onExportClick: PropTypes.func,
onCreateReportClick: PropTypes.func,
};
ActionButtons.defaultProps = {
onExportClick: () => alert('Export'),
onCreateReportClick: () => alert('Create Report'),
};
export default ActionButtons;

View File

@ -1,10 +1,13 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { StudySummary, MeasurementTable } from '@ohif/ui';
import { DicomMetadataStore } from '@ohif/core';
import { DicomMetadataStore, DICOMSR } from '@ohif/core';
import { useDebounce } from '@hooks';
import ActionButtons from './ActionButtons';
import { useTrackedMeasurements } from '../../getContextModule';
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import dcmjs from 'dcmjs';
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
key: undefined, //
@ -13,7 +16,7 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
description: undefined, // 'CHEST/ABD/PELVIS W CONTRAST',
};
function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(
Date.now().toString()
);
@ -102,6 +105,35 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
const activeMeasurementItem = 0;
const onExportClick = () => {
const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
// TODO -> local download.
DICOMSR.downloadReport(trackedMeasurements, dataSource);
};
const onCreateReportClick = () => {
const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
const dataSources = extensionManager.getDataSources();
// TODO -> Eventually deal with multiple dataSources.
// Would need some way of saying which one is the "push" dataSource
const dataSource = dataSources[0];
DICOMSR.storeMeasurements(trackedMeasurements, dataSource);
};
return (
<>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
@ -121,7 +153,10 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
/>
</div>
<div className="flex justify-center p-4">
<ActionButtons />
<ActionButtons
onExportClick={onExportClick}
onCreateReportClick={onCreateReportClick}
/>
</div>
</>
);

View File

@ -201,10 +201,10 @@ function PanelStudyBrowserTracking({
);
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
? [
...expandedStudyInstanceUIDs.filter(
stdyUid => stdyUid !== StudyInstanceUID
),
]
...expandedStudyInstanceUIDs.filter(
stdyUid => stdyUid !== StudyInstanceUID
),
]
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
@ -285,7 +285,6 @@ function _mapDisplaySets(
) {
const thumbnailDisplaySets = [];
const thumbnailNoImageDisplaySets = [];
displaySets.forEach(ds => {
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
const componentType = _getComponentType(ds.Modality);

View File

@ -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.
@ -43,7 +41,6 @@ function TrackedCornerstoneViewport({
const [
{ activeViewportIndex, viewports },
dispatchViewportGrid,
] = useViewportGrid();
// viewportIndex, onSubmit
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
@ -209,15 +206,17 @@ function TrackedCornerstoneViewport({
SeriesInstanceUID,
SeriesNumber,
} = displaySet;
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
PixelSpacing,
ManufacturerModelName
} = displaySet.images[0];
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
@ -226,7 +225,6 @@ function TrackedCornerstoneViewport({
<>
<ViewportActionBar
onSeriesChange={direction => alert(`Series ${direction}`)}
showPatientInfo={viewportIndex === activeViewportIndex}
showNavArrows={viewportIndex === activeViewportIndex}
studyData={{
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
@ -237,13 +235,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 || '',
},
}}
/>

View File

@ -33,7 +33,7 @@
"@ohif/ui": "^2.0.0",
"cornerstone-core": "^2.3.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.12.2",
"dcmjs": "0.14.0",
"dicom-parser": "^1.8.3",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",

View File

@ -38,7 +38,7 @@
"dependencies": {
"@babel/runtime": "7.7.6",
"ajv": "^6.10.0",
"dcmjs": "^0.12.4",
"dcmjs": "0.14.0",
"dicomweb-client": "^0.6.0",
"immer": "6.0.2",
"isomorphic-base64": "^1.0.2",

View File

@ -6,6 +6,10 @@ import {
stowSRFromMeasurements,
} from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
import cornerstoneTools from 'cornerstone-tools';
import dcmjs from 'dcmjs';
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
/**
*
@ -47,7 +51,7 @@ const retrieveMeasurements = server => {
* @param {serverType} server
* @returns {Object} With message to be displayed on success
*/
const storeMeasurements = async (measurementData, filter, server) => {
const storeMeasurementsOld = async (measurementData, filter, server) => {
log.info('[DICOMSR] storeMeasurements');
if (!server || server.type !== 'dicomWeb') {
@ -78,4 +82,128 @@ const storeMeasurements = async (measurementData, filter, server) => {
}
};
export { retrieveMeasurements, storeMeasurements };
/**
*
* @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);
//Create a URL for the binary.
var objectUrl = URL.createObjectURL(reportBlob);
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);
const report = MeasurementReport.generateReport(
filteredToolState,
cornerstone.metaData
);
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.
log.info('[DICOMSR] storeMeasurements');
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
return Promise.reject({});
}
const naturalizedReport = generateReport(measurementData);
const { StudyInstanceUID } = naturalizedReport;
try {
await dataSource.store.dicom(naturalizedReport);
if (StudyInstanceUID) {
studies.deleteStudyMetadataPromise(StudyInstanceUID);
}
return {
message: 'Measurements saved successfully',
};
} catch (error) {
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
throw new Error('Error while saving the measurements.');
}
};
function _getFilteredCornerstoneToolState(uidFilter) {
const globalToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
const filteredToolState = {};
function addToFilteredToolState(imageId, toolType, toolDataI) {
if (!filteredToolState[imageId]) {
filteredToolState[imageId] = {};
}
const imageIdSpecificToolState = filteredToolState[imageId];
if (!imageIdSpecificToolState[toolType]) {
imageIdSpecificToolState[toolType] = {
data: [],
};
}
const toolData = imageIdSpecificToolState[toolType].data;
toolData.push(toolDataI);
}
const uids = uidFilter.slice();
const imageIds = Object.keys(globalToolState);
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const imageIdSpecificToolState = globalToolState[imageId];
const toolTypes = Object.keys(imageIdSpecificToolState);
for (let j = 0; j < toolTypes.length; j++) {
const toolType = toolTypes[j];
const toolData = imageIdSpecificToolState[toolType].data;
if (toolData) {
for (let k = 0; k < toolData.length; k++) {
const toolDataK = toolData[k];
const uidIndex = uids.findIndex(uid => uid === toolDataK.id);
if (uidIndex !== -1) {
addToFilteredToolState(imageId, toolType, toolDataK);
uids.splice(uidIndex, 1);
if (!uids.length) {
return filteredToolState;
}
}
}
}
}
}
return filteredToolState;
}
export { retrieveMeasurements, storeMeasurements, downloadReport };

View File

@ -1,9 +1,14 @@
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
import {
retrieveMeasurements,
storeMeasurements,
downloadReport,
} from './dataExchange';
import isToolSupported from './utils/isToolSupported';
const DICOMSR = {
retrieveMeasurements,
storeMeasurements,
downloadReport,
isToolSupported,
};

View File

@ -15,6 +15,7 @@ import { DicomMetadataStore } from '@ohif/core';
function create({
query,
retrieve,
store,
retrieveSeriesMetadata,
getImageIdsForDisplaySet,
}) {
@ -44,13 +45,20 @@ function create({
series: {},
};
const defaultStore = {
dicom: async naturalizedDataset => {
throw new Error(
'store.dicom(naturalizedDicom, StudyInstanceUID) not implemented for dataSource.'
);
},
};
return {
query: query || defaultQuery,
retrieve: retrieve || defaultRetrieve,
store: store || defaultStore,
getImageIdsForDisplaySet,
retrieveSeriesMetadata,
// then go get all series level metadata.
// Store this in the DICOM MetadataStore.
};
}

View File

@ -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

View File

@ -33,7 +33,7 @@ const serviceImplementation = {
function _show({
content = null,
contentProps = null,
shouldCloseOnEsc = false,
shouldCloseOnEsc = true,
isOpen = true,
closeButton = true,
title = null,

View File

@ -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();
}

View File

@ -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,

View File

@ -76,6 +76,7 @@ export {
Typography,
Viewport,
ViewportActionBar,
ViewportDownloadForm,
ViewportGrid,
ViewportPane,
} from './src/components';

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="currentColor" fill-rule="evenodd">
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M.188.187L8.813 8.812M8.813.187L.188 8.812" transform="translate(5 5)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,11 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
aria-labelledby="title"
width="1em"
height="1em"
fill="currentColor"
>
<path d="m5.364 28.917c1.047 1.046 2.422 1.57 3.797 1.57 1.376 0 2.751-0.523 3.798-1.572l7.552-7.554c1.016-1.014 1.574-2.361 1.574-3.797 -0.001-1.432-0.558-2.781-1.574-3.796l-2.297-2.298 -2.009 2.009 2.297 2.298c0.478 0.478 0.741 1.113 0.741 1.788 0 0.674-0.264 1.309-0.741 1.786l-7.553 7.555c-0.987 0.985-2.59 0.985-3.576 0l-2.297-2.298c-0.987-0.985-0.987-2.589 0-3.576l3.775-3.776 -2.009-2.009 -3.776 3.776c-2.094 2.096-2.092 5.502 0 7.595l2.299 2.3zM26.695 2.992" />
<path d="m26.695 2.992c-1.014-1.016-2.362-1.575-3.797-1.575 -0.001 0-0.001 0-0.002 0 -1.435 0-2.784 0.56-3.798 1.573l-7.551 7.553c-1.017 1.016-1.576 2.363-1.576 3.799 0 1.434 0.558 2.784 1.574 3.797l2.297 2.297 2.01-2.009 -2.298-2.297c-0.477-0.477-0.741-1.113-0.741-1.788 0-0.676 0.265-1.311 0.742-1.788l7.553-7.555c0.477-0.477 1.111-0.74 1.789-0.74 0 0 0 0 0.001 0 0.674 0 1.309 0.264 1.786 0.74l2.297 2.299c0.477 0.477 0.74 1.111 0.74 1.788 0 0.674-0.264 1.311-0.74 1.788l-3.776 3.777 2.009 2.009 3.776-3.777c1.014-1.013 1.573-2.363 1.574-3.797 0-1.435-0.56-2.784-1.574-3.797l-2.295-2.296z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,11 @@
<svg
xmlns="http://www.w3.org/2000/svg"
aria-labelledby="unlink"
viewBox="0 0 512 512"
width="1em"
height="1em"
fill="currentColor"
>
<title id="title">Unlink</title>
<path d="M304.083 388.936c4.686 4.686 4.686 12.284 0 16.971l-65.057 65.056c-54.709 54.711-143.27 54.721-197.989 0-54.713-54.713-54.719-143.27 0-197.989l65.056-65.057c4.686-4.686 12.284-4.686 16.971 0l22.627 22.627c4.686 4.686 4.686 12.284 0 16.971L81.386 311.82c-34.341 34.341-33.451 88.269.597 120.866 32.577 31.187 84.788 31.337 117.445-1.32l65.057-65.056c4.686-4.686 12.284-4.686 16.971 0l22.627 22.626zm-56.568-243.245l64.304-64.304c34.346-34.346 88.286-33.453 120.882.612 31.18 32.586 31.309 84.785-1.335 117.43l-65.056 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.686 4.686 12.284 4.686 16.971 0l65.056-65.057c54.711-54.709 54.721-143.271 0-197.99-54.71-54.711-143.27-54.72-197.989 0l-65.057 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.685 4.685 12.283 4.685 16.97-.001zm238.343 362.794l22.627-22.627c4.686-4.686 4.686-12.284 0-16.971L43.112 3.515c-4.686-4.686-12.284-4.686-16.971 0L3.515 26.142c-4.686 4.686-4.686 12.284 0 16.971l465.373 465.373c4.686 4.686 12.284 4.686 16.97-.001z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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 (
<div className={classnames('flex-col inline-flex items-center', className)}>
<Icon name="magnifier" className="mb-4" />
<Typography className="text-primary-light" variant="h5">
No studies available
{!isLoading ? 'No studies available' : 'Loading...'}
</Typography>
</div>
);

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,
};

View File

@ -0,0 +1,3 @@
.modal-content {
max-height: calc(100vh - theme('spacing.250px'));
}

View File

@ -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 && (
<header>
<h4>{title}</h4>
<header className="mb-6 pb-4 border-b border-secondary-main">
<Typography variant="h4">{title}</Typography>
{closeButton && (
<button data-cy="close-button" onClick={onClose}>
×
</button>
<IconButton
className="absolute top-0 right-0 focus:outline-none flex -mr-3 -mt-3"
data-cy="close-button"
color="primary"
onClick={onClose}
rounded="full"
>
<Icon name="close" className="text-white w-8 h-8" />
</IconButton>
)}
</header>
)
@ -41,22 +45,26 @@ const Modal = ({
return (
<ReactModal
className={classNames(className)}
className="relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded"
overlayClassName="fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-start justify-center py-16"
shouldCloseOnEsc={shouldCloseOnEsc}
onRequestClose={handleClose}
isOpen={isOpen}
title={title}
style={customStyle}
>
<>
{renderHeader()}
<section>{children}</section>
</>
<div className="px-6">{renderHeader()}</div>
<section className="ohif-scrollbar modal-content overflow-y-auto px-6">
{children}
</section>
</ReactModal>
);
};
Modal.defaultProps = {
shouldCloseOnEsc: true,
};
Modal.propTypes = {
className: PropTypes.string,
closeButton: PropTypes.bool,
shouldCloseOnEsc: PropTypes.bool,
isOpen: PropTypes.bool,

View File

@ -76,10 +76,9 @@ const Select = ({
options={options}
value={selectedOptions}
onChange={(selectedOptions, { action }) => {
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);
}}
></ReactSelect>

View File

@ -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 = () => <div>Search Instructions</div>;
show({
content: modalContent,
title: 'Learn More',
});
};
return (
<React.Fragment>
@ -38,6 +48,7 @@ const StudyListFilter = ({
color="inherit"
className="text-primary-active"
startIcon={<Icon name="info-link" className="w-2" />}
onClick={showLearnMoreContent}
>
<span className="flex flex-col flex-1">
<span>Learn more</span>

View File

@ -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 ? (
<Icon name="dotted-circle" className="w-6 text-primary-light" />
) : (
<Tooltip
position="bottom-left"
content={
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex ml-4">
<span className="text-base text-common-light">
Series is
<Tooltip
position="bottom-left"
content={
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex ml-4">
<span className="text-base text-common-light">
Series is
<span className="font-bold text-white"> tracked</span> and
can be viewed <br /> in the measurement panel
</span>
</div>
</div>
</div>
}
>
<Icon name="tracked" className="w-6 text-primary-light" />
</Tooltip>
)}
}
>
<Icon name="tracked" className="w-6 text-primary-light" />
</Tooltip>
)}
</div>
);
};
return (
<div className="flex items-center p-2 border-b border-primary-light">
<div className="flex flex-grow">
@ -131,19 +136,18 @@ const ViewportActionBar = ({
</ButtonGroup>
</div>
)}
{showPatientInfo && (
<div className="flex ml-4 mr-2">
<PatientInfo
patientName={patientName}
patientSex={patientSex}
patientAge={patientAge}
MRN={MRN}
thickness={thickness}
spacing={spacing}
scanner={scanner}
/>
</div>
)}
<div className="flex ml-4 mr-2 cursor-pointer" onClick={onPatientInfoClick}>
<PatientInfo
isOpen={showPatientInfo}
patientName={patientName}
patientSex={patientSex}
patientAge={patientAge}
MRN={MRN}
thickness={thickness}
spacing={spacing}
scanner={scanner}
/>
</div>
</div>
);
};
@ -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 (
<Tooltip
isSticky
isDisabled={!isOpen}
position="bottom-right"
content={
content={isOpen && (
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
@ -236,7 +243,7 @@ function PatientInfo({
</div>
</div>
</div>
}
)}
>
<div className="relative flex justify-end">
<div className="relative">

View File

@ -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 (
<Typography className="mt-2 pl-1" color="error">
{error_messages[errorType]}
</Typography>
);
};
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 (
<div>
<Typography variant="h6">
Please specify the dimensions, filename, and desired type for the output
image.
</Typography>
<div className="mt-6 flex flex-col">
<div className="w-full mb-4">
<Input
data-cy="file-name"
value={filename}
onChange={value => setFilename(value)}
label="File Name"
/>
{renderErrorHandler('filename')}
</div>
<div className="flex">
<div className="flex w-1/3">
<div className="flex flex-col flex-grow">
<div className="w-full">
<Input
type="number"
min={minimumSize}
max={maximumSize}
label="Image width (px)"
value={dimensions.width}
onChange={value => onDimensionsChange(value, 'width')}
data-cy="image-width"
/>
{renderErrorHandler('width')}
</div>
<div className="w-full mt-4">
<Input
type="number"
min={minimumSize}
max={maximumSize}
label="Image height (px)"
value={dimensions.height}
onChange={value => onDimensionsChange(value, 'height')}
data-cy="image-height"
/>
{renderErrorHandler('height')}
</div>
</div>
<div className="flex items-center mt-8">
<Tooltip
position="right"
content={keepAspect ? 'Dismiss Aspect' : 'Keep Aspect'}
>
<IconButton
onClick={onKeepAspectToggle}
size="small"
rounded="full"
>
<Icon name={keepAspect ? 'link' : 'unlink'} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="ml-6 w-1/4 pl-6 border-l border-secondary-dark">
<div>
<InputLabelWrapper
sortDirection="none"
label="File Type"
isSortable={false}
onLabelClick={() => {}}
>
<Select
className="mt-2"
isClearable={false}
value={fileType}
data-cy="file-type"
onChange={value => {
setFileType([value.value]);
}}
hideSelectedOptions={false}
options={FILE_TYPE_OPTIONS}
placeholder="File Type"
/>
</InputLabelWrapper>
</div>
<div className="mt-4 ml-2">
<label htmlFor="show-annotations" className="flex items-center">
<input
id="show-annotations"
data-cy="show-annotations"
type="checkbox"
className="mr-2"
checked={showAnnotations}
onChange={event => setShowAnnotations(event.target.checked)}
/>
<Typography>Show Annotations</Typography>
</label>
</div>
</div>
</div>
</div>
<div className="mt-8">
<div
className="hidden"
style={{
height: viewportElementDimensions.height,
width: viewportElementDimensions.width,
}}
ref={ref => setViewportElement(ref)}
>
<canvas
className={classnames('block', canvasClass)}
style={{
height: downloadCanvas.height,
width: downloadCanvas.width,
}}
width={downloadCanvas.width}
height={downloadCanvas.height}
ref={downloadCanvas.ref}
></canvas>
</div>
{viewportPreview.src ? (
<div
className="p-4 rounded bg-secondary-dark border-secondary-primary"
data-cy="image-preview"
>
<Typography variant="h5">Image preview</Typography>
<img
className="mt-4"
src={viewportPreview.src}
alt="Preview"
data-cy="image-preview"
/>
</div>
) : (
<div className="text-center p-8">
<Typography>Loading Image Preview...</Typography>
</div>
)}
</div>
<div className="flex justify-end mt-4">
<Button data-cy="cancel-btn" variant="outlined" onClick={onClose}>
Cancel
</Button>
<Button
className="ml-2"
disabled={hasError}
onClick={downloadImage}
color="primary"
data-cy="download-btn"
>
Download
</Button>
</div>
</div>
);
};
export default ViewportDownloadForm;

View File

@ -0,0 +1 @@
export { default } from './ViewportDownloadForm';

View File

@ -46,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';
@ -99,6 +100,7 @@ export {
Typography,
Viewport,
ViewportActionBar,
ViewportDownloadForm,
ViewportGrid,
ViewportPane,
};

View File

@ -16,7 +16,7 @@ const ModalComponent = ({
ModalComponent.defaultProps = {
content: null,
contentProps: null,
shouldCloseOnEsc: false,
shouldCloseOnEsc: true,
isOpen: true,
closeButton: true,
title: null,

View File

@ -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,

View File

@ -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: {

View File

@ -69,7 +69,7 @@
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.16.0",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "^0.12.2",
"dcmjs": "0.14.0",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4",
"dotenv-webpack": "^1.7.0",

View File

@ -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;

View File

@ -46,14 +46,17 @@ function DataSourceWrapper(props) {
// studies.processResults --> <LayoutTemplate studies={} />
// 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 (
<React.Fragment>
{data && (
<LayoutTemplate
{...rest}
history={history}
data={data}
dataSource={dataSource}
/>
)}
</React.Fragment>
<LayoutTemplate
{...rest}
history={history}
data={data}
dataSource={dataSource}
isLoadingData={isLoading}
/>
);
}

View File

@ -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';
@ -28,15 +28,13 @@ 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 +78,7 @@ function WorkList({ history, data: studies, dataSource }) {
return 0;
});
// ~ Rows & Studies
const [expandedRows, setExpandedRows] = useState([]);
const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]);
@ -190,6 +189,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);
@ -211,8 +211,8 @@ function WorkList({ history, data: studies, dataSource }) {
content: patientName ? (
patientName
) : (
<span className="text-gray-700">(Empty)</span>
),
<span className="text-gray-700">(Empty)</span>
),
title: patientName,
gridCol: 4,
},
@ -299,13 +299,13 @@ function WorkList({ history, data: studies, dataSource }) {
seriesTableDataSource={
seriesInStudiesMap.has(studyInstanceUid)
? seriesInStudiesMap.get(studyInstanceUid).map(s => {
return {
description: s.description || '(empty)',
seriesNumber: s.seriesNumber || '',
modality: s.modality || '',
instances: s.numSeriesInstances || '',
};
})
return {
description: s.description || '(empty)',
seriesNumber: s.seriesNumber || '',
modality: s.modality || '',
instances: s.numSeriesInstances || '',
};
})
: []
}
>
@ -322,7 +322,7 @@ function WorkList({ history, data: studies, dataSource }) {
<Link
key={i}
to={`${mode.id}?StudyInstanceUIDs=${studyInstanceUid}`}
// to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
// to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
>
<Button
rounded="full"
@ -368,7 +368,7 @@ function WorkList({ history, data: studies, dataSource }) {
variant="text"
color="inherit"
className="text-primary-active"
onClick={() => {}}
onClick={() => { }}
>
<React.Fragment>
<Icon name="settings" />
@ -403,10 +403,10 @@ function WorkList({ history, data: studies, dataSource }) {
/>
</>
) : (
<div className="flex flex-col items-center justify-center pt-48">
<EmptyStudies />
</div>
)}
<div className="flex flex-col items-center justify-center pt-48">
<EmptyStudies isLoading={isLoadingData} />
</div>
)}
</div>
);
}

View File

@ -7353,10 +7353,10 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@^0.12.2, dcmjs@^0.12.4:
version "0.12.4"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.12.4.tgz#82c24abdc357ea5281b78eb2cae8b781f7392aa3"
integrity sha512-N1ZsXqZIysirqdytb7h572TyIjmxpvCjrzdjtQsuPN8gC2EpxsUHQ598CPzaJBpBy9i1kfKuq4h2Jwt99cr/QQ==
dcmjs@0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.0.tgz#0dc6cb2d15ddcff759bc9002f2a9704537735d1c"
integrity sha512-VL/Ibxe5RDsc5j5SEv3aEqdlKuBXz81/bBuW59Or0cos9vgK3XnVl3rr0ct6DWXJGK8vGIUMBOguyd/NRvlN0w==
dependencies:
"@babel/polyfill" "^7.8.3"
"@babel/runtime" "^7.8.4"