fix: 🐛 Fix ghost shadow on thumb (#1113)
* fix: 🐛 Fix ghost shadow on thumb Updated react-dnd package for the lates. Refactored thumbnail component Closes: 1087 * feat: 🎸 Code review Set flex instead of float. Add cypress tag to thumb container * feat: 🎸 Code review. Revised style when no description
This commit is contained in:
parent
22127a8f59
commit
caaa032c4b
@ -49,9 +49,9 @@
|
|||||||
"prop-types": "15.6.2",
|
"prop-types": "15.6.2",
|
||||||
"react-bootstrap-modal": "4.2.0",
|
"react-bootstrap-modal": "4.2.0",
|
||||||
"react-dates": "21.2.1",
|
"react-dates": "21.2.1",
|
||||||
"react-dnd": "9.3.4",
|
"react-dnd": "9.4.0",
|
||||||
"react-dnd-html5-backend": "^9.3.4",
|
"react-dnd-html5-backend": "^9.4.0",
|
||||||
"react-dnd-touch-backend": "^9.3.4",
|
"react-dnd-touch-backend": "^9.4.0",
|
||||||
"react-i18next": "^10.11.0",
|
"react-i18next": "^10.11.0",
|
||||||
"react-with-direction": "1.3.0"
|
"react-with-direction": "1.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { StudyBrowser, ThumbnailEntry } from './studyBrowser';
|
import { StudyBrowser, Thumbnail } from './studyBrowser';
|
||||||
import { LayoutButton, LayoutChooser } from './layoutButton';
|
import { LayoutButton, LayoutChooser } from './layoutButton';
|
||||||
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
|
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
|
||||||
import { Overlay, OverlayTrigger } from './overlayTrigger';
|
import { Overlay, OverlayTrigger } from './overlayTrigger';
|
||||||
@ -44,9 +44,9 @@ export {
|
|||||||
StudyList,
|
StudyList,
|
||||||
TableList,
|
TableList,
|
||||||
TableListItem,
|
TableListItem,
|
||||||
|
Thumbnail,
|
||||||
TableSearchFilter,
|
TableSearchFilter,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
ThumbnailEntry,
|
|
||||||
ToolbarSection,
|
ToolbarSection,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
AboutModal,
|
AboutModal,
|
||||||
|
|||||||
@ -45,7 +45,7 @@
|
|||||||
.thumbnails-wrapper
|
.thumbnails-wrapper
|
||||||
transform-origin: 0% 0%
|
transform-origin: 0% 0%
|
||||||
|
|
||||||
.ThumbnailEntry
|
.thumbnail
|
||||||
float: left
|
float: left
|
||||||
|
|
||||||
.series-item
|
.series-item
|
||||||
@ -98,7 +98,7 @@
|
|||||||
transition: transform 0.3s ease
|
transition: transform 0.3s ease
|
||||||
transform-origin: 100% 0%
|
transform-origin: 100% 0%
|
||||||
|
|
||||||
.ThumbnailEntry
|
.thumbnail
|
||||||
float: right
|
float: right
|
||||||
|
|
||||||
.series-details
|
.series-details
|
||||||
@ -179,7 +179,7 @@
|
|||||||
.thumbnails-wrapper
|
.thumbnails-wrapper
|
||||||
transform: scale(1) translateY(0)
|
transform: scale(1) translateY(0)
|
||||||
|
|
||||||
.ThumbnailEntry .series-details
|
.thumbnail .series-details
|
||||||
opacity: 1
|
opacity: 1
|
||||||
transform: translateY(0)
|
transform: translateY(0)
|
||||||
transition-delay: 0.3s
|
transition-delay: 0.3s
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { ThumbnailEntry } from './../studyBrowser';
|
import { Thumbnail } from './../studyBrowser';
|
||||||
import './SeriesList.styl';
|
import './SeriesList.styl';
|
||||||
|
|
||||||
export class SeriesList extends Component {
|
export class SeriesList extends Component {
|
||||||
@ -24,7 +24,7 @@ export class SeriesList extends Component {
|
|||||||
getSeriesItems = () => {
|
getSeriesItems = () => {
|
||||||
return this.props.seriesItems.map((seriesData, index) => {
|
return this.props.seriesItems.map((seriesData, index) => {
|
||||||
return (
|
return (
|
||||||
<ThumbnailEntry
|
<Thumbnail
|
||||||
key={seriesData.displaySetInstanceUid}
|
key={seriesData.displaySetInstanceUid}
|
||||||
id={`series_thumb_${index}`}
|
id={`series_thumb_${index}`}
|
||||||
{...seriesData}
|
{...seriesData}
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
import React, { PureComponent, memo } from 'react';
|
|
||||||
import { DragLayer } from 'react-dnd';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { ThumbnailEntry } from './ThumbnailEntry';
|
|
||||||
import './DragPreview.styl';
|
|
||||||
|
|
||||||
let subscribedToOffsetChange = false;
|
|
||||||
let dragPreviewRef = null;
|
|
||||||
|
|
||||||
const collector = monitor => {
|
|
||||||
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 (
|
|
||||||
<div className="DragPreview">
|
|
||||||
<div className="source-preview" ref={updateRef}>
|
|
||||||
<ThumbnailEntry {...this.props} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DragPreview.propTypes = {
|
|
||||||
isDragging: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragLayer(collector)(memo(DragPreview));
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,62 +1,103 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ThumbnailEntry } from './ThumbnailEntry';
|
import { Thumbnail } from './Thumbnail.js';
|
||||||
import ThumbnailEntryDragSource from './ThumbnailEntryDragSource.js';
|
|
||||||
import './StudyBrowser.styl';
|
import './StudyBrowser.styl';
|
||||||
|
|
||||||
class StudyBrowser extends Component {
|
function StudyBrowser(props) {
|
||||||
static defaultProps = {
|
const {
|
||||||
studies: [],
|
studies,
|
||||||
supportsDragAndDrop: true,
|
onThumbnailClick,
|
||||||
};
|
onThumbnailDoubleClick,
|
||||||
|
supportsDrag,
|
||||||
|
} = props;
|
||||||
|
|
||||||
static propTypes = {
|
return (
|
||||||
studies: PropTypes.array.isRequired,
|
<div className="study-browser">
|
||||||
supportsDragAndDrop: PropTypes.bool.isRequired,
|
<div className="scrollable-study-thumbnails">
|
||||||
onThumbnailClick: PropTypes.func,
|
{studies
|
||||||
onThumbnailDoubleClick: PropTypes.func,
|
.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() {
|
return (
|
||||||
const studies = this.props.studies;
|
<div
|
||||||
|
key={`container_${studyIndex}_${thumbIndex}`}
|
||||||
const thumbnails = studies.map((study, studyIndex) => {
|
className="thumbnail-container"
|
||||||
return study.thumbnails.map((thumb, thumbIndex) => {
|
data-cy="thumbnail-list"
|
||||||
if (this.props.supportsDragAndDrop) {
|
>
|
||||||
return (
|
<Thumbnail
|
||||||
<ThumbnailEntryDragSource
|
supportsDrag={supportsDrag}
|
||||||
key={thumb.displaySetInstanceUid}
|
key={`${studyIndex}_${thumbIndex}`}
|
||||||
{...study}
|
id={`${studyIndex}_${thumbIndex}`} // Unused?
|
||||||
{...thumb}
|
// Study
|
||||||
id={`${studyIndex}_${thumbIndex}`}
|
studyInstanceUid={studyInstanceUid} // used by drop
|
||||||
onClick={this.props.onThumbnailClick}
|
// Thumb
|
||||||
onDoubleClick={this.props.onThumbnailDoubleClick}
|
altImageText={altImageText}
|
||||||
/>
|
imageId={imageId}
|
||||||
);
|
instanceNumber={instanceNumber}
|
||||||
} else {
|
displaySetInstanceUid={displaySetInstanceUid} // used by drop
|
||||||
return (
|
numImageFrames={numImageFrames}
|
||||||
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
|
seriesDescription={seriesDescription}
|
||||||
<ThumbnailEntry
|
seriesNumber={seriesNumber}
|
||||||
key={thumb.displaySetInstanceUid}
|
stackPercentComplete={stackPercentComplete}
|
||||||
{...study}
|
// Events
|
||||||
{...thumb}
|
onClick={onThumbnailClick.bind(
|
||||||
id={`${studyIndex}_${thumbIndex}`}
|
undefined,
|
||||||
onClick={this.props.onThumbnailClick}
|
displaySetInstanceUid
|
||||||
onDoubleClick={this.props.onThumbnailDoubleClick}
|
)}
|
||||||
/>
|
onDoubleClick={onThumbnailDoubleClick}
|
||||||
</div>
|
/>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.flat()}
|
||||||
const components = thumbnails.flat();
|
|
||||||
return (
|
|
||||||
<div className="StudyBrowser">
|
|
||||||
<div className="scrollable-study-thumbnails">{components}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 };
|
export { StudyBrowser };
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.StudyBrowser
|
.study-browser
|
||||||
float: left
|
float: left
|
||||||
height: 100%
|
height: 100%
|
||||||
width: 100%
|
width: 100%
|
||||||
@ -22,7 +22,7 @@
|
|||||||
&::-webkit-scrollbar
|
&::-webkit-scrollbar
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
.ThumbnailEntryContainer
|
.thumbnail-container
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-bottom: 1.5rem;
|
padding-bottom: 1.5rem;
|
||||||
|
|
||||||
|
|||||||
152
platform/ui/src/components/studyBrowser/Thumbnail.js
Normal file
152
platform/ui/src/components/studyBrowser/Thumbnail.js
Normal file
@ -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 (
|
||||||
|
<div className={classNames('item item-series', className)}>
|
||||||
|
<div className="icon">{icon}</div>
|
||||||
|
<div className="value">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const getSeriesInformation = (
|
||||||
|
seriesNumber,
|
||||||
|
instanceNumber,
|
||||||
|
numImageFrames
|
||||||
|
) => {
|
||||||
|
if (!seriesNumber && !instanceNumber && !numImageFrames) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="series-information">
|
||||||
|
{getInfo(seriesNumber, 'S:')}
|
||||||
|
{getInfo(instanceNumber, 'I:')}
|
||||||
|
{getInfo(numImageFrames, '', 'image-frames')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
||||||
|
<div className="series-description">{seriesDescription}</div>
|
||||||
|
{getSeriesInformation(seriesNumber, instanceNumber, numImageFrames)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
ref={drag}
|
||||||
|
className={classNames('thumbnail', { active: active })}
|
||||||
|
onClick={onClick}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
>
|
||||||
|
{/* SHOW IMAGE */}
|
||||||
|
{hasImage && (
|
||||||
|
<ImageThumbnail
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
imageId={imageId}
|
||||||
|
error={error}
|
||||||
|
stackPercentComplete={stackPercentComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* SHOW TEXT ALTERNATIVE */}
|
||||||
|
{!hasImage && hasAltText && (
|
||||||
|
<div className={'alt-image-text p-x-1'}>
|
||||||
|
<h1>{altImageText}</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ThumbnailFooter(props)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <div>. 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 };
|
||||||
@ -4,12 +4,12 @@
|
|||||||
--series-count-background-color: #678696;
|
--series-count-background-color: #678696;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ThumbnailEntry
|
.thumbnail
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
display: table
|
display: table
|
||||||
|
margin: 0 auto;
|
||||||
.p-x-1
|
min-width: 90%;
|
||||||
padding: 0 1rem
|
padding-bottom: 1.5rem;
|
||||||
|
|
||||||
.alt-image-text
|
.alt-image-text
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -31,6 +31,8 @@
|
|||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
|
|
||||||
.series-details
|
.series-details
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
font-size: 14px
|
font-size: 14px
|
||||||
line-height: 1.3em
|
line-height: 1.3em
|
||||||
@ -38,64 +40,30 @@
|
|||||||
margin-left: 1rem
|
margin-left: 1rem
|
||||||
margin-right: 0.5rem
|
margin-right: 0.5rem
|
||||||
max-width: 217px
|
max-width: 217px
|
||||||
min-height: 36px
|
|
||||||
position: relative
|
position: relative
|
||||||
word-wrap: break-word
|
word-wrap: break-word
|
||||||
|
|
||||||
&.info-only
|
.series-description
|
||||||
.series-description
|
flex-grow: 1
|
||||||
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
|
|
||||||
|
|
||||||
|
&.info-only .series-description
|
||||||
|
display: none
|
||||||
|
|
||||||
.series-information
|
.series-information
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
align-items: center
|
justify-content: space-between
|
||||||
|
|
||||||
.item
|
|
||||||
flex: 1
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.item-frames .icon
|
.item-frames .icon
|
||||||
height: 18px
|
height: 18px
|
||||||
|
|
||||||
.value
|
.item-series.image-frames .icon
|
||||||
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
|
|
||||||
background-color: var(--series-count-background-color);
|
background-color: var(--series-count-background-color);
|
||||||
margin-top: 6px
|
margin-top: 6px
|
||||||
position: relative
|
position: relative
|
||||||
|
|
||||||
&:after
|
&:after
|
||||||
|
content: ''
|
||||||
background-color: var(--active-color);
|
background-color: var(--active-color);
|
||||||
box-shadow: 1px 1px rgba(0, 0, 0, .115)
|
box-shadow: 1px 1px rgba(0, 0, 0, .115)
|
||||||
left: -4px
|
left: -4px
|
||||||
@ -104,16 +72,27 @@
|
|||||||
|
|
||||||
&
|
&
|
||||||
&:after
|
&:after
|
||||||
border: 0px solid var(--series-count-background-color)
|
|
||||||
content: ''
|
content: ''
|
||||||
|
border: 0px solid var(--series-count-background-color)
|
||||||
display: inline-block
|
display: inline-block
|
||||||
height: 11px
|
height: 11px
|
||||||
width: 11px
|
width: 11px
|
||||||
|
|
||||||
.image-thumbnail-clone
|
.value
|
||||||
z-index: 100000
|
color: var(--text-secondary-color);
|
||||||
pointer-events: none
|
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
|
.icon
|
||||||
border-color: var(--hover-color);
|
color: var(--active-color);
|
||||||
box-shadow: none
|
display: inline-block
|
||||||
|
font-size: 10px
|
||||||
|
font-weight: 900
|
||||||
|
text-align: right
|
||||||
|
width: 11px
|
||||||
@ -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 <div>. 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 = (
|
|
||||||
<div className="p-x-1">
|
|
||||||
<ImageThumbnail
|
|
||||||
imageSrc={this.props.imageSrc}
|
|
||||||
imageId={this.props.imageId}
|
|
||||||
error={this.props.error}
|
|
||||||
stackPercentComplete={this.props.stackPercentComplete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (this.props.altImageText) {
|
|
||||||
contents = (
|
|
||||||
<div className="p-x-1">
|
|
||||||
<div className="alt-image-text">
|
|
||||||
<h1>{this.props.altImageText}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
onClick={this.onClick}
|
|
||||||
onDoubleClick={this.onDoubleClick}
|
|
||||||
onMouseDown={this.onMouseDown}
|
|
||||||
>
|
|
||||||
{contents}
|
|
||||||
<div
|
|
||||||
className="series-details"
|
|
||||||
>
|
|
||||||
<div className="series-description">
|
|
||||||
{this.props.seriesDescription}
|
|
||||||
</div>
|
|
||||||
<div className="series-information">
|
|
||||||
<div className="item item-series">
|
|
||||||
<div className="icon">S:</div>
|
|
||||||
<div className="value">{this.props.seriesNumber}</div>
|
|
||||||
</div>
|
|
||||||
{hasInstanceNumber && (
|
|
||||||
<div className="item item-series">
|
|
||||||
<div className="icon">I:</div>
|
|
||||||
<div className="value">{this.props.instanceNumber}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="item item-frames">
|
|
||||||
<div className="icon">
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
<div className="value">{this.props.numImageFrames}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick = () => {
|
|
||||||
if (this.props.onClick) {
|
|
||||||
this.props.onClick(this.props.displaySetInstanceUid);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onDoubleClick = () => {
|
|
||||||
if (this.props.onDoubleClick) {
|
|
||||||
this.props.onDoubleClick(this.props.displaySetInstanceUid);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ThumbnailEntry };
|
|
||||||
@ -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(
|
|
||||||
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
|
|
||||||
<DragPreview {...this.props} />
|
|
||||||
<ThumbnailEntry {...this.props} />
|
|
||||||
</div>,
|
|
||||||
{ dropEffect }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const collect = (connect, monitor) => ({
|
|
||||||
connectDragSource: connect.dragSource(),
|
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default DragSource(Types.THUMBNAIL, thumbnailSource, collect)(
|
|
||||||
ThumbnailEntryDragSource
|
|
||||||
);
|
|
||||||
@ -1,2 +1,2 @@
|
|||||||
export { StudyBrowser } from './StudyBrowser.js';
|
export { StudyBrowser } from './StudyBrowser.js';
|
||||||
export { ThumbnailEntry } from './ThumbnailEntry.js';
|
export { Thumbnail } from './Thumbnail.js';
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import {
|
|||||||
StudyList,
|
StudyList,
|
||||||
TableList,
|
TableList,
|
||||||
TableListItem,
|
TableListItem,
|
||||||
|
Thumbnail,
|
||||||
TableSearchFilter,
|
TableSearchFilter,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
ThumbnailEntry,
|
|
||||||
ToolbarSection,
|
ToolbarSection,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
AboutModal,
|
AboutModal,
|
||||||
@ -86,9 +86,9 @@ export {
|
|||||||
StudyList,
|
StudyList,
|
||||||
TableList,
|
TableList,
|
||||||
TableListItem,
|
TableListItem,
|
||||||
|
Thumbnail,
|
||||||
TableSearchFilter,
|
TableSearchFilter,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
ThumbnailEntry,
|
|
||||||
Toolbar,
|
Toolbar,
|
||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
ToolbarSection,
|
ToolbarSection,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ describe('ViewerRouting', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// it('thumbnails list has more than 2 items', () => {
|
// 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')
|
// .its('length')
|
||||||
// .should('be.gte', 2);
|
// .should('be.gte', 2);
|
||||||
// });
|
// });
|
||||||
|
|||||||
@ -107,7 +107,7 @@ const ViewportGrid = function(props) {
|
|||||||
|
|
||||||
ViewportGrid.propTypes = {
|
ViewportGrid.propTypes = {
|
||||||
viewportData: PropTypes.array.isRequired,
|
viewportData: PropTypes.array.isRequired,
|
||||||
supportsDragAndDrop: PropTypes.bool.isRequired,
|
supportsDrop: PropTypes.bool.isRequired,
|
||||||
activeViewportIndex: PropTypes.number.isRequired,
|
activeViewportIndex: PropTypes.number.isRequired,
|
||||||
layout: PropTypes.object.isRequired,
|
layout: PropTypes.object.isRequired,
|
||||||
availablePlugins: PropTypes.object.isRequired,
|
availablePlugins: PropTypes.object.isRequired,
|
||||||
@ -127,7 +127,7 @@ ViewportGrid.defaultProps = {
|
|||||||
viewports: [{}],
|
viewports: [{}],
|
||||||
},
|
},
|
||||||
activeViewportIndex: 0,
|
activeViewportIndex: 0,
|
||||||
supportsDragAndDrop: true,
|
supportsDrop: true,
|
||||||
availablePlugins: {
|
availablePlugins: {
|
||||||
DefaultViewport,
|
DefaultViewport,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user