feat(DICOM Upload): Added new DICOM upload dialogue launched from the worklist page (#3326)

* feat(DICOM Upload)
OHIF #3297
- DicomWebDataSource.store.dicom now accepts an ArrayBuffer of data sets to store
- DicomWebDataSource.store.dicom now also accepts optional callbacks to track upload and an AbortSignal object to cancel upload
- Added DicomFileUploader class that performs and tracks the upload of a DICOM file
- Added various UI pieces for the upload: DicomUpload, DicomUploadProgress, DicomUploadProgressItem
- ProgressLoadingBar was extracted from LoadingIndicatorProgress so it can be reused
- Modal dialogues can now optionally prevent an outside click from closing the Modal

* Passing an XMLHttpRequest to the dataSource.store.dicom method instead of callbacks and AbortSignal.
Cleanup of various UI pieces to minimize code and increase readability.

* Made the DicomUpload component a customization module exported by the cornerstone extension.

* Exposed a copy of the data source configuration via the IWebApiDataSource interface.
Added dicomUploadEnabled to a data source's configuration.

* Code clean up.

* Upgraded the dicomweb-client version to one that provides the ability to
pass a custom HTTP request. DICOM upload uses that custom HTTP request
to track progress and cancel requests.

* Distinguished between failed and cancelled uploads.

* Allow no selection in the upload dialogue.
Fixed the styling of various progress information so that everything aligns.

* Switched from cornerstone wado image loader to cornerstone dicom image loader for DICOM upload.

* Added special cancelled icon to differentiate from failed.

* Added a bit of spacing between the upload progress bar and percentage.

* Fixed minor issue with upload rejection.

* Performance improvement for cancel all uploads:
- use React memo for each upload item progress (row)
- do not await each request of a cancel all

* Fixed various padding/spacing for the DICOM upload drop zone component.
Changed the border dashing for the DICOM upload drop zone to be a background image gradient.
Added hover and active effects to the 'Cancel All Uploads' text.
This commit is contained in:
Joe Boccanfuso authored and GitHub committed 2023-04-28 16:13:47 -04:00
1 parent f377153b60
commit 66f6e3eade
32 files changed
+1126 -350

No files matched your search

@@ -1,5 +1,9 @@
window.config = {
routerBasename: '/',
customizationService: {
dicomUploadComponent:
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
},
showStudyList: true,
extensions: [],
modes: [],
@@ -26,6 +30,7 @@ window.config = {
requestOptions: {
auth: 'admin:admin',
},
dicomUploadEnabled: true,
},
},
{
@@ -3,6 +3,10 @@ window.config = {
// whiteLabelling: {},
extensions: [],
modes: [],
customizationService: {
dicomUploadComponent:
'@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent',
},
showStudyList: true,
maxNumberOfWebWorkers: 3,
showLoadingIndicator: true,
@@ -28,6 +32,7 @@ window.config = {
useBulkDataURI: false,
supportsFuzzyMatching: true,
supportsWildcard: true,
dicomUploadEnabled: true,
},
},
{
@@ -56,13 +56,14 @@ function DataSourceWrapper(props) {
// But only for LayoutTemplate type of 'list'?
// Or no data fetching here, and just hand down my source
const STUDIES_LIMIT = 101;
const [data, setData] = useState({
const DEFAULT_DATA = {
studies: [],
total: 0,
resultsPerPage: 25,
pageNumber: 1,
location: 'Not a valid location, causes first load to occur',
});
};
const [data, setData] = useState(DEFAULT_DATA);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
@@ -125,6 +126,8 @@ function DataSourceWrapper(props) {
dataTotal={data.total}
dataSource={dataSource}
isLoadingData={isLoading}
// To refresh the data, simply reset it to DEFAULT_DATA which invalidates it and triggers a new query to fetch the data.
onRefresh={() => setData(DEFAULT_DATA)}
/>
);
}
@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
import filtersMeta from './filtersMeta.js';
import { useAppConfig } from '@state';
import { useDebounce, useSearchParams } from '@hooks';
import { utils, hotkeys } from '@ohif/core';
import { utils, hotkeys, ServicesManager } from '@ohif/core';
import {
Icon,
@@ -47,6 +47,8 @@ function WorkList({
dataSource,
hotkeysManager,
dataPath,
onRefresh,
servicesManager,
}) {
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
const { show, hide } = useModal();
@@ -429,6 +431,33 @@ function WorkList({
});
}
const { customizationService } = servicesManager.services;
const { component: dicomUploadComponent } =
customizationService.get('dicomUploadComponent') ?? {};
const uploadProps =
dicomUploadComponent && dataSource.getConfig().dicomUploadEnabled
? {
title: 'Upload files',
closeButton: true,
shouldCloseOnEsc: false,
shouldCloseOnOverlayClick: false,
content: dicomUploadComponent.bind(null, {
dataSource,
onComplete: () => {
hide();
onRefresh();
},
onStarted: () => {
show({
...uploadProps,
// when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself
closeButton: false,
});
},
}),
}
: undefined;
return (
<div className="bg-black h-screen flex flex-col ">
<Header
@@ -445,6 +474,7 @@ function WorkList({
onChange={setFilterValues}
clearFilters={() => setFilterValues(defaultFilterValues)}
isFiltering={isFiltering(filterValues, defaultFilterValues)}
onUploadClick={uploadProps ? () => show(uploadProps) : undefined}
/>
{hasStudies ? (
<>
@@ -478,8 +508,10 @@ WorkList.propTypes = {
data: PropTypes.array.isRequired,
dataSource: PropTypes.shape({
query: PropTypes.object.isRequired,
getConfig: PropTypes.func,
}).isRequired,
isLoadingData: PropTypes.bool.isRequired,
servicesManager: PropTypes.instanceOf(ServicesManager),
};
const defaultFilterValues = {
+7 -6
View File
@@ -25,12 +25,6 @@ const bakedInRoutes = [
// NOT FOUND (404)
const notFoundRoute = { component: NotFound };
const WorkListRoute = {
path: '/',
children: DataSourceWrapper,
private: true,
props: { children: WorkList },
};
const createRoutes = ({
modes,
@@ -54,6 +48,13 @@ const createRoutes = ({
const { customizationService } = servicesManager.services;
const WorkListRoute = {
path: '/',
children: DataSourceWrapper,
private: true,
props: { children: WorkList, servicesManager },
};
const customRoutes = customizationService.getGlobalCustomization(
'customRoutes'
);