diff --git a/platform/ui/src/components/studyList/StudyList.js b/platform/ui/src/components/studyList/StudyList.js index d3a8d4751..a09623f85 100644 --- a/platform/ui/src/components/studyList/StudyList.js +++ b/platform/ui/src/components/studyList/StudyList.js @@ -11,11 +11,79 @@ import { StudylistToolbar } from './StudyListToolbar.js'; import { isInclusivelyBeforeDay } from 'react-dates'; import moment from 'moment'; import debounce from 'lodash.debounce'; +import isEqual from 'lodash.isequal'; import { withTranslation } from '../../utils/LanguageProvider'; const today = moment(); const lastWeek = moment().subtract(7, 'day'); const lastMonth = moment().subtract(1, 'month'); +function getPaginationFragment( + props, + searchData, + nextPageCb, + prevPageCb, + changeRowsPerPageCb +) { + return ( + + ); +} + +function getTableMeta(translate) { + return { + patientName: { + displayText: translate('PatientName'), + sort: 0, + }, + patientId: { + displayText: translate('MRN'), + sort: 0, + }, + accessionNumber: { + displayText: translate('AccessionNumber'), + sort: 0, + }, + studyDate: { + displayText: translate('StudyDate'), + inputType: 'date-range', + sort: 0, + }, + modalities: { + displayText: translate('Modality'), + sort: 0, + }, + studyDescription: { + displayText: translate('StudyDescription'), + sort: 0, + }, + }; +} + +function getNoListFragment(translate, studies, error, loading) { + if (loading) { + return ( +
+ +
+ ); + } else if (error) { + return ( +
+ {translate('There was an error fetching studies')} +
+ ); + } else if (!studies.length) { + return
{translate('No matching results')}
; + } +} class StudyList extends Component { static propTypes = { @@ -111,24 +179,27 @@ class StudyList extends Component { getBlurHandler(key) { return event => { this.delayedSearch.cancel(); - this.setSearchData(key, event.target.value, this.search); + this.setSearchData(key, event.target.value); }; } - setSearchData(key, value, callback) { - const searchData = this.state.searchData; + setSearchData(key, value) { + const searchData = { ...this.state.searchData }; searchData[key] = value; - this.setState({ searchData }, callback); + + if (!isEqual(searchData[key], this.state.searchData[key])) { + this.setState({ ...this.state, searchData }); + } } - setSearchDataBatch(keyValues, callback) { - const searchData = this.state.searchData; + setSearchDataBatch(keyValues) { + const searchData = { ...this.state.searchData }; Object.keys(keyValues).forEach(key => { searchData[key] = keyValues[key]; }); - this.setState({ searchData }, callback); + this.setState({ searchData }); } async onInputKeydown(event) { @@ -138,7 +209,7 @@ class StudyList extends Component { this.delayedSearch.cancel(); // reset the page because user is doing a new search - this.setSearchData('currentPage', 0, this.search); + this.setSearchData('currentPage', 0); } } @@ -154,45 +225,21 @@ class StudyList extends Component { } } - renderNoMachingResults() { - if (!this.props.studies.length && !this.state.error) { - return
No matching results
; - } - } - - renderHasError() { - if (this.state.error) { - return ( -
There was an error fetching studies
- ); - } - } - - renderIsLoading() { - if (this.state.loading) { - return ( -
- -
- ); - } - } - nextPage(currentPage) { currentPage = currentPage + 1; this.delayedSearch.cancel(); - this.setSearchData('currentPage', currentPage, this.search); + this.setSearchData('currentPage', currentPage); } prevPage(currentPage) { currentPage = currentPage - 1; this.delayedSearch.cancel(); - this.setSearchData('currentPage', currentPage, this.search); + this.setSearchData('currentPage', currentPage); } onRowsPerPageChange(rowsPerPage) { this.delayedSearch.cancel(); - this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }, this.search); + this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }); } onSortClick(field) { @@ -213,7 +260,7 @@ class StudyList extends Component { } this.delayedSearch.cancel(); - this.setSearchData('sortData', { field, order }, this.search); + this.setSearchData('sortData', { field, order }); }; } @@ -221,10 +268,39 @@ class StudyList extends Component { this.setState({ highlightedItem: studyItemUid }); } - renderTableRow(study) { + getTableRow(study, index) { + const trKey = `trStudy${index}${study.studyInstanceUid}`; + + if (!study) { + return; + } + + const getTableCell = ( + study, + studyKey, + emptyValue = '', + emptyClass = '' + ) => { + const componentKey = `td${studyKey}`; + const isValidValue = study && typeof study[studyKey] === 'string'; + let className = emptyClass; + let value = emptyValue; + + if (isValidValue) { + className = studyKey; + value = study[studyKey]; + } + + return ( + + {value} + + ); + }; + return ( - - {study.patientName || `(${this.props.t('Empty')})`} - - - {study.patientId} - {study.accessionNumber} - {study.studyDate} - {study.modalities} - {study.studyDescription} + {getTableCell( + study, + 'patientName', + `(${this.props.t('Empty')})`, + 'emptyCell' + )} + {getTableCell(study, 'patientId')} + {getTableCell(study, 'accessionNumber')} + {getTableCell(study, 'studyDate')} + {getTableCell(study, 'modalities')} + {getTableCell(study, 'studyDescription')} ); } + componentDidUpdate(previousProps, previousState) { + if (!isEqual(previousState.searchData, this.state.searchData)) { + this.search(); + } + } + + renderTableBody(noListFragment) { + return !noListFragment && this.props.studies + ? this.props.studies.map(this.getTableRow.bind(this)) + : null; + } + render() { - const tableMeta = { - patientName: { - displayText: this.props.t('PatientName'), - sort: 0, - }, - patientId: { - displayText: this.props.t('MRN'), - sort: 0, - }, - accessionNumber: { - displayText: this.props.t('AccessionNumber'), - sort: 0, - }, - studyDate: { - displayText: this.props.t('StudyDate'), - inputType: 'date-range', - sort: 0, - }, - modalities: { - displayText: this.props.t('Modality'), - sort: 0, - }, - studyDescription: { - displayText: this.props.t('StudyDescription'), - sort: 0, - }, - }; + const tableMeta = getTableMeta(this.props.t); // Apply sort const sortedFieldName = this.state.searchData.sortData.field; @@ -294,14 +358,20 @@ class StudyList extends Component { // Sort Icons const sortIcons = ['sort', 'sort-up', 'sort-down']; + const noListFragment = getNoListFragment( + this.props.t, + this.props.studies, + this.state.error, + this.props.loading || this.state.loading + ); + const tableBody = this.renderTableBody(noListFragment); + const studiesNum = (this.props.studies && this.props.studies.length) || 0; return (
{this.props.t('StudyList')}
-
- {this.props.studies.length} -
+
{studiesNum}
{this.props.studyListFunctionsEnabled ? ( @@ -367,22 +437,16 @@ class StudyList extends Component { (this.state.focusedInput === 'endDate' || preset) ) { - this.setSearchDataBatch( - { - studyDateFrom: startDate.toDate(), - studyDateTo: endDate.toDate(), - }, - this.search - ); + this.setSearchDataBatch({ + studyDateFrom: startDate.toDate(), + studyDateTo: endDate.toDate(), + }); this.setState({ focusedInput: false }); } else if (!startDate && !endDate) { - this.setSearchDataBatch( - { - studyDateFrom: null, - studyDateTo: null, - }, - this.search - ); + this.setSearchDataBatch({ + studyDateFrom: null, + studyDateTo: null, + }); } }} focusedInput={this.state.focusedInput} @@ -398,26 +462,18 @@ class StudyList extends Component { })} - - {this.props.studies.map(study => { - return this.renderTableRow(study); - })} - + {tableBody} - {this.renderIsLoading()} - {this.renderHasError()} - {this.renderNoMachingResults()} - - + {noListFragment + ? noListFragment + : getPaginationFragment( + this.props, + this.state.searchData, + this.nextPage, + this.prevPage, + this.onRowsPerPageChange + )}
); diff --git a/platform/ui/src/components/studyList/StudyList.styl b/platform/ui/src/components/studyList/StudyList.styl index 9cefa7a5d..c4cf6a767 100644 --- a/platform/ui/src/components/studyList/StudyList.styl +++ b/platform/ui/src/components/studyList/StudyList.styl @@ -82,9 +82,14 @@ placeholder-color(c) position: absolute z-index: 2 - .loading-text - color: var(--table-text-secondary-color) - font-size: 30px + .loading + display: flex; + justify-content: center; + + .loading-text + color: var(--table-text-secondary-color) + font-size: 30px + width: fit-content; .notFound color: var(--table-text-secondary-color) diff --git a/platform/ui/src/components/studyList/StudyListLoadingText.js b/platform/ui/src/components/studyList/StudyListLoadingText.js index deab2e331..22c0dad74 100644 --- a/platform/ui/src/components/studyList/StudyListLoadingText.js +++ b/platform/ui/src/components/studyList/StudyListLoadingText.js @@ -1,12 +1,17 @@ import { Icon } from './../../elements/Icon'; import React from 'react'; +import { withTranslation } from '../../utils/LanguageProvider'; -function StudyListLoadingText() { +function StudyListLoadingText({ t: translate }) { return (
- Loading... + {translate('Loading')}...
); } -export { StudyListLoadingText }; +const connectedComponent = withTranslation('StudyListLoadingText')( + StudyListLoadingText +); + +export { connectedComponent as StudyListLoadingText }; diff --git a/platform/viewer/cypress/integration/common/OHIFStandaloneViewer.spec.js b/platform/viewer/cypress/integration/common/OHIFStandaloneViewer.spec.js index b79a4598c..5d5f7671f 100644 --- a/platform/viewer/cypress/integration/common/OHIFStandaloneViewer.spec.js +++ b/platform/viewer/cypress/integration/common/OHIFStandaloneViewer.spec.js @@ -1,6 +1,6 @@ describe('OHIFStandaloneViewer', () => { beforeEach(() => { - cy.visit('/'); + cy.openStudyList(); }); it('loads route with at least 2 rows', () => { diff --git a/platform/viewer/cypress/support/aliases.js b/platform/viewer/cypress/support/aliases.js index c32aa728d..ec9a246eb 100644 --- a/platform/viewer/cypress/support/aliases.js +++ b/platform/viewer/cypress/support/aliases.js @@ -35,8 +35,8 @@ export function initCommonElementsAliases() { //Creating aliases for Routes export function initRouteAliases() { cy.server(); - cy.route('GET', '/**/series/**').as('getStudySeries'); - cy.route('GET', '/**/studies/**').as('getStudies'); + cy.route('GET', '**/series**').as('getStudySeries'); + cy.route('GET', '**/studies**').as('getStudies'); } //Creating aliases for VTK tools buttons diff --git a/platform/viewer/cypress/support/commands.js b/platform/viewer/cypress/support/commands.js index 5981b9cf4..05edbabfd 100644 --- a/platform/viewer/cypress/support/commands.js +++ b/platform/viewer/cypress/support/commands.js @@ -39,20 +39,12 @@ import { * @param {string} PatientName - Patient name that we would like to search for */ Cypress.Commands.add('openStudy', patientName => { - cy.initRouteAliases(); - cy.visit('/'); - cy.wrap(null).then(() => { - return new Cypress.Promise((resolve, reject) => { - cy.get('#patientName').type(patientName); - setTimeout(() => { - cy.get('#studyListData') - .contains(patientName) - .first() - .click(); - resolve(); - }, 2000); - }); - }); + cy.openStudyList(); + cy.get('#patientName').type(patientName); + cy.wait('@getStudies'); + cy.get('#studyListData .studylistStudy', { timeout: 5000 }) + .contains(patientName) + .first().click(); }); /** @@ -82,6 +74,12 @@ Cypress.Commands.add('isPageLoaded', (url = '/viewer/') => { return cy.location('pathname', { timeout: 60000 }).should('include', url); }); +Cypress.Commands.add('openStudyList', patientName => { + cy.initRouteAliases(); + cy.visit('/'); + cy.wait('@getStudies'); +}); + /** * Command to perform a drag and drop action. Before using this command, we must get the element that should be dragged first. * Example of usage: cy.get(element-to-be-dragged).drag(dropzone-element) diff --git a/platform/viewer/src/googleCloud/DicomStorePickerModal.js b/platform/viewer/src/googleCloud/DicomStorePickerModal.js index 023a5c842..d7ed14884 100644 --- a/platform/viewer/src/googleCloud/DicomStorePickerModal.js +++ b/platform/viewer/src/googleCloud/DicomStorePickerModal.js @@ -22,6 +22,8 @@ class DicomStorePickerModal extends Component { handleEvent = data => { const servers = GoogleCloudUtilServers.getServers(data, data.dicomstore); this.props.setServers(servers); + // Force auto close + this.props.onClose(); }; render() { diff --git a/platform/viewer/src/studylist/StudyListWithData.js b/platform/viewer/src/studylist/StudyListWithData.js index e7bf60681..cce0c0646 100644 --- a/platform/viewer/src/studylist/StudyListWithData.js +++ b/platform/viewer/src/studylist/StudyListWithData.js @@ -8,6 +8,7 @@ import { StudyList } from '@ohif/ui'; import ConnectedHeader from '../connectedComponents/ConnectedHeader.js'; import * as RoutesUtil from '../routes/routesUtil'; import moment from 'moment'; +import isEqual from 'lodash.isequal'; import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader'; import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker'; import filesToStudies from '../lib/filesToStudies.js'; @@ -20,8 +21,9 @@ import AppContext from '../context/AppContext'; class StudyListWithData extends Component { static contextType = AppContext; state = { - searchData: {}, + searchOutdated: true, studies: [], + searchingStudies: false, error: null, modalComponentId: null, }; @@ -62,6 +64,7 @@ class StudyListWithData extends Component { if (!this.props.server && appConfig.enableGoogleCloudAdapter) { this.setState({ modalComponentId: 'DicomStorePicker', + searchOutdated: false, }); } else { this.searchForStudies({ @@ -71,21 +74,33 @@ class StudyListWithData extends Component { } } - componentDidUpdate(prevProps) { - if (!this.state.searchData && !this.state.studies) { - this.searchForStudies(); - } - if (this.props.server !== prevProps.server) { - this.setState({ - modalComponentId: null, - searchData: null, - studies: null, - }); + componentDidUpdate(prevProps, prevState) { + const hasNewServer = !isEqual(this.props.server, prevProps.server); + + const { searchOutdated, searchingStudies } = this.state; + + if (!searchingStudies) { + if (hasNewServer) { + const { appConfig = {} } = this.context; + + const newState = { + searchOutdated: true, + studies: null, + }; + if (appConfig.enableGoogleCloudAdapter) { + newState.modalComponentId = null; + } + this.setState(newState); + } + + if (searchOutdated) { + this.searchForStudies(); + } } } searchForStudies = (searchData = StudyListWithData.defaultSearchData) => { - const { server } = this.props; + const { server = {} } = this.props; const filter = { patientId: searchData.patientId, patientName: searchData.patientName, @@ -105,7 +120,9 @@ class StudyListWithData extends Component { // TODO: add sorting const promise = OHIF.studies.searchStudies(server, filter); - // Render the viewer when the data is ready + this.setState({ + searchingStudies: true, + }); promise .then(studies => { if (!studies) { @@ -150,11 +167,15 @@ class StudyListWithData extends Component { this.setState({ studies: sortedStudies, + searchingStudies: false, + searchOutdated: false, }); }) .catch(error => { this.setState({ error: true, + searchingStudies: false, + searchOutdated: false, }); throw new Error(error); @@ -208,8 +229,6 @@ class StudyListWithData extends Component { if (this.state.error) { return
Error: {JSON.stringify(this.state.error)}
; - } else if (this.state.studies === null && !this.state.modalComponentId) { - return
Loading...
; } let healthCareApiButtons = null; @@ -240,8 +259,9 @@ class StudyListWithData extends Component { const studyList = (
- {this.state.studies ? ( + {this.state.studies || this.state.searchingStudies ? (