feat: Issue 879 viewer route query param not filtering but promoting (#1141)
* feat: 🎸 Filter by url query param for seriesInstnaceUID * fix: Set SR viewport as active by interaction (#1118) * fix: Set SR viewport as active by interaction * quick fix * (eslint) add "before" as global variables * add data-cy * add data-cy * create E2E test * (E2E) create custom command to set layout size * remove .only e2e * remove throttle for onScroll * feat: 🎸 Code review in progress Code review. Move retrieveMEtadata load to separate folders. Some minor code clean up * feat: 🎸 Code review. Missing changes from previous commit * feat: 🎸 Code review missing changes from previous commit When sorting, Criteria for instance must use instanceNumber and not instancesNumber * feat: 🎸 Code review. Add more jsdoc info * feat: 🎸 Code review. Prettify changed code * feat: 🎸 Instead of filtering do promote. WIP * feat: 🎸 Fix minor issue. Allow promote or filter * feat: 🎸 Changing component to functional component * Merge from master Part1/2 Conflicts solved: both added: core/src/studies/services/wado/retrieveMetadataLoader.js both added: core/src/studies/services/wado/retrieveMetadataLoaderAsync.js both added: core/src/studies/services/wado/retrieveMetadataLoaderSync.js both modified: viewer/src/connectedComponents/ViewerRetrieveStudyData.js both modified: viewer/src/routes/ViewerRouting.js * Merge process from master Part 1/2 Missing files from previous commit * feat: 🎸 Add cancelable promises to cut async methods * feat: 🎸 Missing changes from previous merge process * feat: 🎸 Missing changes from previous merge process
This commit is contained in:
1 parent
c77a2ef5c9
commit
b17f753e62
7 files changed
+762
-182
No files matched your search
@@ -1,6 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
||||
import OHIF from "@ohif/core";
|
||||
|
||||
const {
|
||||
clearViewportSpecificData
|
||||
} = OHIF.redux.actions;
|
||||
const isActive = a => a.active === true;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
@@ -10,10 +14,17 @@ const mapStateToProps = state => {
|
||||
server: activeServer,
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
clearViewportSpecificData: () => {
|
||||
dispatch(clearViewportSpecificData());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewerRetrieveStudyData = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
mapDispatchToProps
|
||||
)(ViewerRetrieveStudyData);
|
||||
|
||||
export default ConnectedViewerRetrieveStudyData;
|
||||
@@ -1,191 +1,346 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { metadata, studies, utils, log } from '@ohif/core';
|
||||
|
||||
import ConnectedViewer from './ConnectedViewer.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { extensionManager } from './../App.js';
|
||||
import { withSnackbar } from '@ohif/ui';
|
||||
import { useSnackbarContext } from '@ohif/ui';
|
||||
|
||||
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
||||
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
|
||||
const { studyMetadataManager, updateMetaDataManager } = utils;
|
||||
const { studyMetadataManager, updateMetaDataManager, makeCancelable } = utils;
|
||||
|
||||
class ViewerRetrieveStudyData extends Component {
|
||||
static propTypes = {
|
||||
studyInstanceUids: PropTypes.array.isRequired,
|
||||
seriesInstanceUids: PropTypes.array,
|
||||
server: PropTypes.object,
|
||||
// Contexts
|
||||
import AppContext from '../context/AppContext';
|
||||
|
||||
const _promoteToFront = (list, value, searchMethod) => {
|
||||
let response = [...list];
|
||||
let promoted = false;
|
||||
const index = response.findIndex(searchMethod.bind(undefined, value));
|
||||
|
||||
if (index > 0) {
|
||||
const first = response.splice(index, 1);
|
||||
response = [...first, ...response];
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
promoted = true;
|
||||
}
|
||||
|
||||
return {
|
||||
promoted,
|
||||
data: response,
|
||||
};
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.abortSeriesLoad = false;
|
||||
this.seriesLoadStats = Object.create(null);
|
||||
this.state = {
|
||||
studies: null,
|
||||
error: null,
|
||||
};
|
||||
/**
|
||||
* Promote series to front if find found equivalent on filters object
|
||||
* @param {Object} study - study reference to promote series against
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
||||
*/
|
||||
const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => {
|
||||
let promoted = false;
|
||||
// Promote only if no filter should be applied
|
||||
if (!isFilterStrategy) {
|
||||
_sortStudyDisplaySet(study, studyMetadata);
|
||||
promoted = _promoteStudyDisplaySet(study, studyMetadata, filters);
|
||||
}
|
||||
|
||||
async loadStudies() {
|
||||
try {
|
||||
const { server, studyInstanceUids, seriesInstanceUids } = this.props;
|
||||
const filters = {};
|
||||
return promoted;
|
||||
};
|
||||
|
||||
// Use the first, discard others
|
||||
const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
|
||||
const _promoteStudyDisplaySet = (study, studyMetadata, filters) => {
|
||||
let promoted = false;
|
||||
const queryParamsLength = Object.keys(filters).length;
|
||||
const shouldPromoteToFront = queryParamsLength > 0;
|
||||
|
||||
if (seriesInstanceUID) {
|
||||
filters.seriesInstanceUID = seriesInstanceUID;
|
||||
}
|
||||
|
||||
const studies = await retrieveStudiesMetadata(
|
||||
server,
|
||||
studyInstanceUids,
|
||||
filters
|
||||
);
|
||||
this.validateFilters(studies, filters);
|
||||
this.setStudies(studies);
|
||||
} catch (e) {
|
||||
this.setState({ error: true });
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate filters and promp user a message in case filter is unsuccessfully applied.
|
||||
* In case of success, studies array contains, as the first element, the queried content (from filter)
|
||||
* @param {Array} studies array of studies to be evaluated
|
||||
* @param {Object} filters filters to test against
|
||||
*/
|
||||
validateFilters(studies = [], filters = {}) {
|
||||
if (shouldPromoteToFront) {
|
||||
const { seriesInstanceUID } = filters;
|
||||
|
||||
const { snackbarContext } = this.props;
|
||||
// skip in case no filter or no toast manager
|
||||
if (!seriesInstanceUID || !snackbarContext) {
|
||||
return;
|
||||
}
|
||||
const _seriesLookup = (valueToCompare, displaySet) => {
|
||||
return displaySet.seriesInstanceUid === valueToCompare;
|
||||
};
|
||||
const promotedResponse = _promoteToFront(
|
||||
studyMetadata.getDisplaySets(),
|
||||
seriesInstanceUID,
|
||||
_seriesLookup
|
||||
);
|
||||
|
||||
const firstStudy = studies[0] || {};
|
||||
const { seriesList = [] } = firstStudy;
|
||||
const firstSeries = seriesList[0];
|
||||
|
||||
if (!firstSeries || firstSeries.seriesInstanceUid !== seriesInstanceUID) {
|
||||
snackbarContext.show({
|
||||
message: 'No series for given filter: ' + seriesInstanceUID,
|
||||
});
|
||||
}
|
||||
study.displaySets = promotedResponse.data;
|
||||
promoted = promotedResponse.promoted;
|
||||
}
|
||||
|
||||
setStudies(givenStudies) {
|
||||
if (Array.isArray(givenStudies) && givenStudies.length > 0) {
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
return promoted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to identify if query param (from url) was applied to given list
|
||||
* @param {Object} study - study reference to promote series against
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
||||
*/
|
||||
const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => {
|
||||
const { seriesInstanceUID } = filters;
|
||||
let applied = true;
|
||||
// skip in case no filter or no toast manager
|
||||
if (!seriesInstanceUID) {
|
||||
return applied;
|
||||
}
|
||||
|
||||
const { seriesList = [], displaySets = [] } = study;
|
||||
const firstSeries = isFilterStrategy ? seriesList[0] : displaySets[0];
|
||||
|
||||
if (!firstSeries || firstSeries.seriesInstanceUid !== seriesInstanceUID) {
|
||||
applied = false;
|
||||
}
|
||||
|
||||
return applied;
|
||||
};
|
||||
const _showUserMessage = (queryParamApplied, message, dialog = {}) => {
|
||||
if (queryParamApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { show: showUserMessage = () => { } } = dialog;
|
||||
showUserMessage({
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
const _addSeriesToStudy = (studyMetadata, series) => {
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
const study = studyMetadata.getData();
|
||||
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
||||
studyMetadata.addSeries(seriesMetadata);
|
||||
studyMetadata.createAndAddDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
seriesMetadata,
|
||||
false
|
||||
);
|
||||
study.displaySets = studyMetadata.getDisplaySets();
|
||||
_updateMetaDataManager(study, series.seriesInstanceUid);
|
||||
};
|
||||
|
||||
const _updateMetaDataManager = (study, studyMetadata, series) => {
|
||||
updateMetaDataManager(study, series);
|
||||
|
||||
const { studyInstanceUID } = study;
|
||||
|
||||
if (!studyMetadataManager.get(studyInstanceUID)) {
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
}
|
||||
};
|
||||
|
||||
const _updateStudyDisplaySets = (study, studyMetadata) => {
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
|
||||
if (!study.displaySets) {
|
||||
study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||
}
|
||||
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
};
|
||||
|
||||
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
||||
studyMetadata.sortDisplaySets(study.displaySets);
|
||||
};
|
||||
const _loadRemainingSeries = studyMetadata => {
|
||||
const { seriesLoader } = studyMetadata.getData();
|
||||
if (!seriesLoader) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const promisesLoaders = [];
|
||||
while (seriesLoader.hasNext()) {
|
||||
promisesLoaders.push(
|
||||
seriesLoader
|
||||
.next()
|
||||
.then(
|
||||
series => void _addSeriesToStudy(studyMetadata, series),
|
||||
error => void log.error(error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promisesLoaders);
|
||||
};
|
||||
|
||||
function ViewerRetrieveStudyData({
|
||||
server,
|
||||
studyInstanceUids,
|
||||
seriesInstanceUids,
|
||||
clearViewportSpecificData,
|
||||
}) {
|
||||
// hooks
|
||||
const [error, setError] = useState(false);
|
||||
const [studies, setStudies] = useState([]);
|
||||
const snackbarContext = useSnackbarContext();
|
||||
const { appConfig = {} } = useContext(AppContext);
|
||||
const { filterQueryParam: isFilterStrategy = false } = appConfig;
|
||||
|
||||
let cancelableSeriesPromises;
|
||||
let cancelableStudiesPromises;
|
||||
/**
|
||||
* Callback method when study is totally loaded
|
||||
* @param {object} study study loaded
|
||||
* @param {object} studyMetadata studyMetadata for given study
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
*/
|
||||
const studyDidLoad = (study, studyMetadata, filters) => {
|
||||
// User message
|
||||
const promoted = _promoteList(
|
||||
study,
|
||||
studyMetadata,
|
||||
filters,
|
||||
isFilterStrategy
|
||||
);
|
||||
|
||||
// Clear viewport to allow new promoted one to be displayed
|
||||
if (promoted) {
|
||||
clearViewportSpecificData(0);
|
||||
}
|
||||
|
||||
const isQueryParamApplied = _isQueryParamApplied(
|
||||
study,
|
||||
filters,
|
||||
isFilterStrategy
|
||||
);
|
||||
// Show message in case not promoted neither filtered but should to
|
||||
_showUserMessage(
|
||||
isQueryParamApplied,
|
||||
'Query parameters were not applied. Using original series list for given study.',
|
||||
snackbarContext
|
||||
);
|
||||
|
||||
setStudies([...studies, study]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to process studies. It will update displaySet, studyMetadata, load remaining series, ...
|
||||
* @param {Array} studiesData Array of studies retrieved from server
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
*/
|
||||
const processStudies = (studiesData, filters) => {
|
||||
if (Array.isArray(studiesData) && studiesData.length > 0) {
|
||||
// Map studies to new format, update metadata manager?
|
||||
const studies = givenStudies.map(study => {
|
||||
const studies = studiesData.map(study => {
|
||||
const studyMetadata = new OHIFStudyMetadata(
|
||||
study,
|
||||
study.studyInstanceUid
|
||||
);
|
||||
if (!study.displaySets) {
|
||||
study.displaySets = studyMetadata.createDisplaySets(
|
||||
sopClassHandlerModules
|
||||
);
|
||||
}
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
// Updates WADO-RS metaDataManager
|
||||
updateMetaDataManager(study);
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
|
||||
_updateStudyDisplaySets(study, studyMetadata);
|
||||
_updateMetaDataManager(study, studyMetadata);
|
||||
|
||||
// Attempt to load remaning series if any
|
||||
this._attemptToLoadRemainingSeries(studyMetadata);
|
||||
cancelableSeriesPromises[study.studyInstanceUid] = makeCancelable(
|
||||
_loadRemainingSeries(studyMetadata)
|
||||
)
|
||||
.then(result => {
|
||||
if (result && !result.isCanceled) {
|
||||
studyDidLoad(study, studyMetadata, filters);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && !error.isCanceled) {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
|
||||
return study;
|
||||
});
|
||||
this.setState({ studies });
|
||||
}
|
||||
}
|
||||
|
||||
_addSeriesToStudy(studyMetadata, series) {
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
const study = studyMetadata.getData();
|
||||
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
||||
studyMetadata.addSeries(seriesMetadata);
|
||||
studyMetadata.createAndAddDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
seriesMetadata
|
||||
);
|
||||
study.displaySets = studyMetadata.getDisplaySets();
|
||||
updateMetaDataManager(study, series.seriesInstanceUid);
|
||||
this.setState(function(state) {
|
||||
return { studies: state.studies.slice() };
|
||||
});
|
||||
}
|
||||
|
||||
_handleSeriesLoadResult(error, studyMetadata, series) {
|
||||
if (this.abortSeriesLoad) return;
|
||||
const stats = this.seriesLoadStats[studyMetadata.getStudyInstanceUID()];
|
||||
if (!stats) return;
|
||||
stats.count--;
|
||||
if (error || !series) {
|
||||
stats.errors++;
|
||||
log.error(error || 'Bad Series');
|
||||
return;
|
||||
setStudies(studies);
|
||||
}
|
||||
this._addSeriesToStudy(studyMetadata, series);
|
||||
}
|
||||
};
|
||||
|
||||
_attemptToLoadRemainingSeries(studyMetadata) {
|
||||
const { seriesLoader } = studyMetadata.getData();
|
||||
if (!seriesLoader) {
|
||||
return;
|
||||
}
|
||||
const stats = (this.seriesLoadStats[studyMetadata.getStudyInstanceUID()] = {
|
||||
errors: 0,
|
||||
count: 0,
|
||||
});
|
||||
while (seriesLoader.hasNext()) {
|
||||
seriesLoader
|
||||
.next()
|
||||
.then(
|
||||
series =>
|
||||
void this._handleSeriesLoadResult(null, studyMetadata, series),
|
||||
error => void this._handleSeriesLoadResult({ error }, null, null)
|
||||
);
|
||||
stats.count++;
|
||||
}
|
||||
}
|
||||
const loadStudies = async () => {
|
||||
try {
|
||||
const filters = {};
|
||||
// Use the first, discard others
|
||||
const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
|
||||
|
||||
componentWillUnmount() {
|
||||
this.abortSeriesLoad = true;
|
||||
for (const studyInstanceUid in this.seriesLoadStats) {
|
||||
const stats = this.seriesLoadStats[studyInstanceUid];
|
||||
if (stats && (stats.count > 0 || stats.errors > 0)) {
|
||||
deleteStudyMetadataPromise(studyInstanceUid);
|
||||
studyMetadataManager.remove(studyInstanceUid);
|
||||
log.info(`Purging incomplete study data: ${studyInstanceUid}`);
|
||||
const retrieveParams = [server, studyInstanceUids];
|
||||
|
||||
if (seriesInstanceUID) {
|
||||
filters.seriesInstanceUID = seriesInstanceUID;
|
||||
// Query param filtering controlled by appConfig property
|
||||
if (isFilterStrategy) {
|
||||
retrieveParams.push(filters);
|
||||
}
|
||||
}
|
||||
|
||||
cancelableStudiesPromises[studyInstanceUids] = makeCancelable(
|
||||
retrieveStudiesMetadata(...retrieveParams)
|
||||
)
|
||||
.then(result => {
|
||||
if (result && !result.isCanceled) {
|
||||
processStudies(result, filters);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && !error.isCanceled) {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// TODO: CLEAR THIS SOMEWHERE ELSE
|
||||
studyMetadataManager.purge();
|
||||
this.loadStudies();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
const purgeCancellablePromises = () => {
|
||||
for (let studyInstanceUids in cancelableStudiesPromises) {
|
||||
if ('cancel' in cancelableStudiesPromises[studyInstanceUids]) {
|
||||
cancelableStudiesPromises[studyInstanceUids].cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectedViewer
|
||||
studies={this.state.studies}
|
||||
studyInstanceUids={this.props.studyInstanceUids}
|
||||
/>
|
||||
);
|
||||
for (let studyInstanceUids in cancelableSeriesPromises) {
|
||||
if ('cancel' in cancelableSeriesPromises[studyInstanceUids]) {
|
||||
cancelableSeriesPromises[studyInstanceUids].cancel();
|
||||
deleteStudyMetadataPromise(studyInstanceUids);
|
||||
studyMetadataManager.remove(studyInstanceUids);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
studyMetadataManager.purge();
|
||||
purgeCancellablePromises();
|
||||
}, [studyInstanceUids]);
|
||||
|
||||
useEffect(() => {
|
||||
cancelableSeriesPromises = {};
|
||||
cancelableStudiesPromises = {};
|
||||
loadStudies();
|
||||
|
||||
return () => {
|
||||
purgeCancellablePromises();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {JSON.stringify(error)}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectedViewer studies={studies} studyInstanceUids={studyInstanceUids} />
|
||||
);
|
||||
}
|
||||
|
||||
export default withSnackbar(ViewerRetrieveStudyData);
|
||||
ViewerRetrieveStudyData.propTypes = {
|
||||
studyInstanceUids: PropTypes.array.isRequired,
|
||||
seriesInstanceUids: PropTypes.array,
|
||||
server: PropTypes.object,
|
||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ViewerRetrieveStudyData;
|
||||
Reference in new issue
Block a user