diff --git a/platform/ui/package.json b/platform/ui/package.json index 7c89b1935..676e102cb 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -49,9 +49,9 @@ "prop-types": "15.6.2", "react-bootstrap-modal": "4.2.0", "react-dates": "21.2.1", - "react-dnd": "9.3.4", - "react-dnd-html5-backend": "^9.3.4", - "react-dnd-touch-backend": "^9.3.4", + "react-dnd": "9.4.0", + "react-dnd-html5-backend": "^9.4.0", + "react-dnd-touch-backend": "^9.4.0", "react-i18next": "^10.11.0", "react-with-direction": "1.3.0" }, diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 56a3c34a8..99d573cdd 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -1,4 +1,4 @@ -import { StudyBrowser, ThumbnailEntry } from './studyBrowser'; +import { StudyBrowser, Thumbnail } from './studyBrowser'; import { LayoutButton, LayoutChooser } from './layoutButton'; import { MeasurementTable, MeasurementTableItem } from './measurementTable'; import { Overlay, OverlayTrigger } from './overlayTrigger'; @@ -44,9 +44,9 @@ export { StudyList, TableList, TableListItem, + Thumbnail, TableSearchFilter, TablePagination, - ThumbnailEntry, ToolbarSection, Tooltip, AboutModal, diff --git a/platform/ui/src/components/quickSwitch/QuickSwitch.styl b/platform/ui/src/components/quickSwitch/QuickSwitch.styl index b24314b14..bb98b9674 100644 --- a/platform/ui/src/components/quickSwitch/QuickSwitch.styl +++ b/platform/ui/src/components/quickSwitch/QuickSwitch.styl @@ -45,7 +45,7 @@ .thumbnails-wrapper transform-origin: 0% 0% - .ThumbnailEntry + .thumbnail float: left .series-item @@ -98,7 +98,7 @@ transition: transform 0.3s ease transform-origin: 100% 0% - .ThumbnailEntry + .thumbnail float: right .series-details @@ -179,7 +179,7 @@ .thumbnails-wrapper transform: scale(1) translateY(0) - .ThumbnailEntry .series-details + .thumbnail .series-details opacity: 1 transform: translateY(0) transition-delay: 0.3s diff --git a/platform/ui/src/components/quickSwitch/SeriesList.js b/platform/ui/src/components/quickSwitch/SeriesList.js index f38d95d63..2108c25d5 100644 --- a/platform/ui/src/components/quickSwitch/SeriesList.js +++ b/platform/ui/src/components/quickSwitch/SeriesList.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ThumbnailEntry } from './../studyBrowser'; +import { Thumbnail } from './../studyBrowser'; import './SeriesList.styl'; export class SeriesList extends Component { @@ -24,7 +24,7 @@ export class SeriesList extends Component { getSeriesItems = () => { return this.props.seriesItems.map((seriesData, index) => { return ( - { - if (!subscribedToOffsetChange) { - monitor.subscribeToOffsetChange(onOffsetChange(monitor)); - subscribedToOffsetChange = true; - } - - if (dragPreviewRef) { - const offset = - monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset(); - - if (offset) { - const transform = `translate(${offset.x}px, ${offset.y}px)`; - dragPreviewRef.style['transform'] = transform; - dragPreviewRef.style['-webkit-transform'] = transform; - } - } - - const item = monitor.getItem(); - let newItem = {}; - if (item) { - newItem = { - active: item.active, - altImageText: item.altImageText, - id: item.id, - imageSrc: item.imageSrc, - imageId: item.imageId, - instanceNumber: item.instanceNumber, - error: item.error, - numImageFrames: item.numImageFrames, - seriesDescription: item.seriesDescription, - seriesNumber: item.seriesNumber, - stackPercentComplete: item.stackPercentComplete, - }; - } - - return { - ...newItem, - isDragging: monitor.isDragging(), - }; -}; - -const onOffsetChange = monitor => () => { - if (!dragPreviewRef) return; - - const offset = - monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset(); - if (!offset) return; - - const transform = `translate(${offset.x}px, ${offset.y}px)`; - dragPreviewRef.style['transform'] = transform; - dragPreviewRef.style['-webkit-transform'] = transform; -}; - -const updateRef = ref => { - dragPreviewRef = ref; -}; - -class DragPreview extends PureComponent { - render() { - const { isDragging } = this.props; - if (!isDragging) return null; - return ( -
-
- -
-
- ); - } -} - -DragPreview.propTypes = { - isDragging: PropTypes.bool, -}; - -export default DragLayer(collector)(memo(DragPreview)); diff --git a/platform/ui/src/components/studyBrowser/DragPreview.styl b/platform/ui/src/components/studyBrowser/DragPreview.styl deleted file mode 100644 index 4866733d4..000000000 --- a/platform/ui/src/components/studyBrowser/DragPreview.styl +++ /dev/null @@ -1,14 +0,0 @@ -.DragPreview - - .source-preview { - border-radius: 5px; - background-color: rgba(0, 0, 0, 0.5); - position: fixed; - opacity: 0.5; - z-index: 9999; - left: 0; - top: 0; - transition: none; - pointer-events: none; - -webkit-touch-callout: none; - } diff --git a/platform/ui/src/components/studyBrowser/StudyBrowser.js b/platform/ui/src/components/studyBrowser/StudyBrowser.js index 3dad6d438..d5b38ba6c 100644 --- a/platform/ui/src/components/studyBrowser/StudyBrowser.js +++ b/platform/ui/src/components/studyBrowser/StudyBrowser.js @@ -1,62 +1,103 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { ThumbnailEntry } from './ThumbnailEntry'; -import ThumbnailEntryDragSource from './ThumbnailEntryDragSource.js'; +import { Thumbnail } from './Thumbnail.js'; import './StudyBrowser.styl'; -class StudyBrowser extends Component { - static defaultProps = { - studies: [], - supportsDragAndDrop: true, - }; +function StudyBrowser(props) { + const { + studies, + onThumbnailClick, + onThumbnailDoubleClick, + supportsDrag, + } = props; - static propTypes = { - studies: PropTypes.array.isRequired, - supportsDragAndDrop: PropTypes.bool.isRequired, - onThumbnailClick: PropTypes.func, - onThumbnailDoubleClick: PropTypes.func, - }; + return ( +
+
+ {studies + .map((study, studyIndex) => { + const { studyInstanceUid } = study; + return study.thumbnails.map((thumb, thumbIndex) => { + // TODO: Thumb has more props than we care about? + const { + altImageText, + displaySetInstanceUid, + imageId, + instanceNumber, + numImageFrames, + seriesDescription, + seriesNumber, + stackPercentComplete, + } = thumb; - render() { - const studies = this.props.studies; - - const thumbnails = studies.map((study, studyIndex) => { - return study.thumbnails.map((thumb, thumbIndex) => { - if (this.props.supportsDragAndDrop) { - return ( - - ); - } else { - return ( -
- -
- ); - } - }); - }); - - const components = thumbnails.flat(); - return ( -
-
{components}
+ return ( +
+ +
+ ); + }); + }) + .flat()}
- ); - } +
+ ); } +const noop = () => {}; + +StudyBrowser.propTypes = { + studies: PropTypes.arrayOf( + PropTypes.shape({ + studyInstanceUid: PropTypes.string.isRequired, + thumbnails: PropTypes.arrayOf( + PropTypes.shape({ + altImageText: PropTypes.string, + displaySetInstanceUid: PropTypes.string.isRequired, + imageId: PropTypes.string, + instanceNumber: PropTypes.number, + numImageFrames: PropTypes.number, + seriesDescription: PropTypes.string, + seriesNumber: PropTypes.number, + stackPercentComplete: PropTypes.number, + }) + ), + }) + ).isRequired, + supportsDrag: PropTypes.bool, + onThumbnailClick: PropTypes.func, + onThumbnailDoubleClick: PropTypes.func, +}; + +StudyBrowser.defaultProps = { + studies: [], + supportsDrag: true, + onThumbnailClick: noop, + onThumbnailDoubleClick: noop, +}; + export { StudyBrowser }; diff --git a/platform/ui/src/components/studyBrowser/StudyBrowser.styl b/platform/ui/src/components/studyBrowser/StudyBrowser.styl index 7fe1a0f6c..5a105afeb 100644 --- a/platform/ui/src/components/studyBrowser/StudyBrowser.styl +++ b/platform/ui/src/components/studyBrowser/StudyBrowser.styl @@ -1,4 +1,4 @@ -.StudyBrowser +.study-browser float: left height: 100% width: 100% @@ -22,7 +22,7 @@ &::-webkit-scrollbar display: none - .ThumbnailEntryContainer + .thumbnail-container margin: 0 auto; padding-bottom: 1.5rem; diff --git a/platform/ui/src/components/studyBrowser/Thumbnail.js b/platform/ui/src/components/studyBrowser/Thumbnail.js new file mode 100644 index 000000000..2f93a56cf --- /dev/null +++ b/platform/ui/src/components/studyBrowser/Thumbnail.js @@ -0,0 +1,152 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useDrag } from 'react-dnd'; +import ImageThumbnail from './ImageThumbnail'; +import classNames from 'classnames'; + +import './Thumbnail.styl'; + +function ThumbnailFooter({ + seriesDescription, + seriesNumber, + instanceNumber, + numImageFrames, +}) { + const infoOnly = !seriesDescription; + + const getInfo = (value, icon, className = '') => { + return ( +
+
{icon}
+
{value}
+
+ ); + }; + const getSeriesInformation = ( + seriesNumber, + instanceNumber, + numImageFrames + ) => { + if (!seriesNumber && !instanceNumber && !numImageFrames) { + return; + } + + return ( +
+ {getInfo(seriesNumber, 'S:')} + {getInfo(instanceNumber, 'I:')} + {getInfo(numImageFrames, '', 'image-frames')} +
+ ); + }; + + return ( +
+
{seriesDescription}
+ {getSeriesInformation(seriesNumber, instanceNumber, numImageFrames)} +
+ ); +} + +function Thumbnail(props) { + const { + active, + altImageText, + error, + displaySetInstanceUid, + imageId, + imageSrc, + instanceNumber, + numImageFrames, + seriesDescription, + seriesNumber, + stackPercentComplete, + studyInstanceUid, + onClick, + onDoubleClick, + onMouseDown, + supportsDrag, + } = props; + + const [collectedProps, drag, dragPreview] = useDrag({ + // `droppedItem` in `dropTarget` + // The only data it will have access to + item: { + studyInstanceUid, + displaySetInstanceUid, + type: 'thumbnail', // Has to match `dropTarget`'s type + }, + canDrag: function(monitor) { + return supportsDrag; + }, + }); + + const hasImage = imageSrc || imageId; + const hasAltText = altImageText !== undefined; + + return ( +
+ {/* SHOW IMAGE */} + {hasImage && ( + + )} + {/* SHOW TEXT ALTERNATIVE */} + {!hasImage && hasAltText && ( +
+

{altImageText}

+
+ )} + {ThumbnailFooter(props)} +
+ ); +} + +const noop = () => {}; + +Thumbnail.propTypes = { + supportsDrag: PropTypes.bool, + id: PropTypes.string.isRequired, + displaySetInstanceUid: PropTypes.string.isRequired, + studyInstanceUid: PropTypes.string.isRequired, + imageSrc: PropTypes.string, + imageId: PropTypes.string, + error: PropTypes.bool, + active: PropTypes.bool, + stackPercentComplete: PropTypes.number, + /** + altImageText will be used when no imageId or imageSrc is provided. +It will be displayed inside the
. This is useful when it is difficult + to make a preview for a type of DICOM series (e.g. DICOM-SR) + */ + altImageText: PropTypes.string, + seriesDescription: PropTypes.string, + seriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + instanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + numImageFrames: PropTypes.number, + onDoubleClick: PropTypes.func, + onClick: PropTypes.func, + onMouseDown: PropTypes.func, +}; + +Thumbnail.defaultProps = { + supportsDrag: false, + active: false, + error: false, + stackPercentComplete: 0, + onDoubleClick: noop, + onClick: noop, + onMouseDown: noop, +}; + +export { Thumbnail }; diff --git a/platform/ui/src/components/studyBrowser/ThumbnailEntry.styl b/platform/ui/src/components/studyBrowser/Thumbnail.styl similarity index 71% rename from platform/ui/src/components/studyBrowser/ThumbnailEntry.styl rename to platform/ui/src/components/studyBrowser/Thumbnail.styl index 3ccc4a59e..47e6ecb8e 100644 --- a/platform/ui/src/components/studyBrowser/ThumbnailEntry.styl +++ b/platform/ui/src/components/studyBrowser/Thumbnail.styl @@ -4,12 +4,12 @@ --series-count-background-color: #678696; } -.ThumbnailEntry +.thumbnail cursor: pointer display: table - - .p-x-1 - padding: 0 1rem + margin: 0 auto; + min-width: 90%; + padding-bottom: 1.5rem; .alt-image-text align-items: center; @@ -31,6 +31,8 @@ color: var(--text-primary-color); .series-details + display: flex; + flex-direction: column color: var(--text-primary-color); font-size: 14px line-height: 1.3em @@ -38,64 +40,30 @@ margin-left: 1rem margin-right: 0.5rem max-width: 217px - min-height: 36px position: relative word-wrap: break-word - &.info-only - .series-description - display: none - - .series-information - display: flex - flex-grow: 1 - float: none - max-width: none - padding-right: 0 - - .item - flex: 1 - text-align: center - - .icon, .value - display: inline - float: none - line-height: 25px + .series-description + flex-grow: 1 + &.info-only .series-description + display: none .series-information display: flex flex-direction: row - align-items: center - - .item - flex: 1 - text-align: center + justify-content: space-between .item-frames .icon height: 18px - .value - color: var(--text-secondary-color); - display: inline - font-size: 12px - margin-left: 4px - text-overflow: ellipsis - white-space: nowrap - - .icon - color: var(--active-color); - display: inline - font-size: 10px - font-weight: 900 - text-align: right - - div + .item-series.image-frames .icon background-color: var(--series-count-background-color); margin-top: 6px position: relative &:after + content: '' background-color: var(--active-color); box-shadow: 1px 1px rgba(0, 0, 0, .115) left: -4px @@ -104,16 +72,27 @@ & &:after - border: 0px solid var(--series-count-background-color) content: '' + border: 0px solid var(--series-count-background-color) display: inline-block height: 11px width: 11px -.image-thumbnail-clone - z-index: 100000 - pointer-events: none + .value + color: var(--text-secondary-color); + display: inline-block + font-size: 12px + margin-left: 4px + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + width: calc(100% - 15px) + vertical-align: middle - &:hover - border-color: var(--hover-color); - box-shadow: none + .icon + color: var(--active-color); + display: inline-block + font-size: 10px + font-weight: 900 + text-align: right + width: 11px diff --git a/platform/ui/src/components/studyBrowser/ThumbnailEntry.js b/platform/ui/src/components/studyBrowser/ThumbnailEntry.js deleted file mode 100644 index 30f15e6c9..000000000 --- a/platform/ui/src/components/studyBrowser/ThumbnailEntry.js +++ /dev/null @@ -1,114 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ImageThumbnail from './ImageThumbnail'; -import './ThumbnailEntry.styl'; -import classnames from 'classnames'; - -class ThumbnailEntry extends Component { - static defaultProps = { - active: false, - error: false, - stackPercentComplete: 0, - }; - - static propTypes = { - id: PropTypes.string.isRequired, - imageSrc: PropTypes.string, - imageId: PropTypes.string, - error: PropTypes.bool.isRequired, - active: PropTypes.bool.isRequired, - stackPercentComplete: PropTypes.number, - /** - altImageText will be used when no imageId or imageSrc is provided. - It will be displayed inside the
. This is useful when it is difficult - to make a preview for a type of DICOM series (e.g. DICOM-SR) - */ - altImageText: PropTypes.string, - seriesDescription: PropTypes.string, - seriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - instanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - numImageFrames: PropTypes.number, - onDoubleClick: PropTypes.func, - onClick: PropTypes.func, - }; - - render() { - const hasInstanceNumber = this.props.instanceNumber !== undefined; - - let className = classnames('ThumbnailEntry noselect', { - active: this.props.active, - }); - - let contents = null; - if (this.props.imageSrc || this.props.imageId) { - contents = ( -
- -
- ); - } else if (this.props.altImageText) { - contents = ( -
-
-

{this.props.altImageText}

-
-
- ); - } - - return ( -
- {contents} -
-
- {this.props.seriesDescription} -
-
-
-
S:
-
{this.props.seriesNumber}
-
- {hasInstanceNumber && ( -
-
I:
-
{this.props.instanceNumber}
-
- )} -
-
-
-
-
{this.props.numImageFrames}
-
-
-
-
- ); - } - - onClick = () => { - if (this.props.onClick) { - this.props.onClick(this.props.displaySetInstanceUid); - } - }; - - onDoubleClick = () => { - if (this.props.onDoubleClick) { - this.props.onDoubleClick(this.props.displaySetInstanceUid); - } - }; -} - -export { ThumbnailEntry }; diff --git a/platform/ui/src/components/studyBrowser/ThumbnailEntryDragSource.js b/platform/ui/src/components/studyBrowser/ThumbnailEntryDragSource.js deleted file mode 100644 index 8c2b0162c..000000000 --- a/platform/ui/src/components/studyBrowser/ThumbnailEntryDragSource.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { DragSource } from 'react-dnd'; -import DragPreview from './DragPreview'; -import { ThumbnailEntry } from './ThumbnailEntry.js'; - -// Drag sources and drop targets only interact -// if they have the same string type. -const Types = { - THUMBNAIL: 'thumbnail', -}; - -const thumbnailSource = { - /*canDrag(props) { - return props.error === false; - },*/ - - beginDrag(props) { - return props; - }, - - endDrag(props, monitor) { - //const item = monitor.getItem(); - const dropResult = monitor.getDropResult(); - - if (dropResult) { - //console.log(`You dropped ${item.id} into ${dropResult.id}!`); - //console.log(item); - } - }, -}; - -class ThumbnailEntryDragSource extends Component { - static propTypes = { - connectDragSource: PropTypes.func.isRequired, - isDragging: PropTypes.bool.isRequired, - }; - - static defaultProps = { - isDragging: false, - }; - - render() { - const { connectDragSource } = this.props; - const dropEffect = 'copy'; - - return connectDragSource( -
- - -
, - { dropEffect } - ); - } -} - -const collect = (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging(), -}); - -export default DragSource(Types.THUMBNAIL, thumbnailSource, collect)( - ThumbnailEntryDragSource -); diff --git a/platform/ui/src/components/studyBrowser/index.js b/platform/ui/src/components/studyBrowser/index.js index cf0d9d68a..fd9b43275 100644 --- a/platform/ui/src/components/studyBrowser/index.js +++ b/platform/ui/src/components/studyBrowser/index.js @@ -1,2 +1,2 @@ export { StudyBrowser } from './StudyBrowser.js'; -export { ThumbnailEntry } from './ThumbnailEntry.js'; +export { Thumbnail } from './Thumbnail.js'; diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index f184f3eab..1588da218 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -17,9 +17,9 @@ import { StudyList, TableList, TableListItem, + Thumbnail, TableSearchFilter, TablePagination, - ThumbnailEntry, ToolbarSection, Tooltip, AboutModal, @@ -86,9 +86,9 @@ export { StudyList, TableList, TableListItem, + Thumbnail, TableSearchFilter, TablePagination, - ThumbnailEntry, Toolbar, ToolbarButton, ToolbarSection, diff --git a/platform/viewer/cypress/integration/common/ViewerRouting.spec.js b/platform/viewer/cypress/integration/common/ViewerRouting.spec.js index b63d8d31b..d4e1a7494 100644 --- a/platform/viewer/cypress/integration/common/ViewerRouting.spec.js +++ b/platform/viewer/cypress/integration/common/ViewerRouting.spec.js @@ -8,7 +8,7 @@ describe('ViewerRouting', () => { }); // it('thumbnails list has more than 2 items', () => { - // cy.get('.scrollable-study-thumbnails div.ThumbnailEntryContainer') + // cy.get('.scrollable-study-thumbnails div.thumbnail-container') // .its('length') // .should('be.gte', 2); // }); diff --git a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js index ede4a1588..34d4b1bba 100644 --- a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js +++ b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js @@ -107,7 +107,7 @@ const ViewportGrid = function(props) { ViewportGrid.propTypes = { viewportData: PropTypes.array.isRequired, - supportsDragAndDrop: PropTypes.bool.isRequired, + supportsDrop: PropTypes.bool.isRequired, activeViewportIndex: PropTypes.number.isRequired, layout: PropTypes.object.isRequired, availablePlugins: PropTypes.object.isRequired, @@ -127,7 +127,7 @@ ViewportGrid.defaultProps = { viewports: [{}], }, activeViewportIndex: 0, - supportsDragAndDrop: true, + supportsDrop: true, availablePlugins: { DefaultViewport, },