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:
ladeirarodolfo 2019-10-30 16:53:58 -03:00 committed by Danny Brown
parent 22127a8f59
commit caaa032c4b
16 changed files with 295 additions and 400 deletions

View File

@ -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"
},

View File

@ -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,

View File

@ -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

View File

@ -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 (
<ThumbnailEntry
<Thumbnail
key={seriesData.displaySetInstanceUid}
id={`series_thumb_${index}`}
{...seriesData}

View File

@ -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));

View File

@ -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;
}

View File

@ -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 (
<div className="study-browser">
<div className="scrollable-study-thumbnails">
{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 (
<ThumbnailEntryDragSource
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
onClick={this.props.onThumbnailClick}
onDoubleClick={this.props.onThumbnailDoubleClick}
/>
);
} else {
return (
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
<ThumbnailEntry
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
onClick={this.props.onThumbnailClick}
onDoubleClick={this.props.onThumbnailDoubleClick}
/>
</div>
);
}
});
});
const components = thumbnails.flat();
return (
<div className="StudyBrowser">
<div className="scrollable-study-thumbnails">{components}</div>
return (
<div
key={`container_${studyIndex}_${thumbIndex}`}
className="thumbnail-container"
data-cy="thumbnail-list"
>
<Thumbnail
supportsDrag={supportsDrag}
key={`${studyIndex}_${thumbIndex}`}
id={`${studyIndex}_${thumbIndex}`} // Unused?
// Study
studyInstanceUid={studyInstanceUid} // used by drop
// Thumb
altImageText={altImageText}
imageId={imageId}
instanceNumber={instanceNumber}
displaySetInstanceUid={displaySetInstanceUid} // used by drop
numImageFrames={numImageFrames}
seriesDescription={seriesDescription}
seriesNumber={seriesNumber}
stackPercentComplete={stackPercentComplete}
// Events
onClick={onThumbnailClick.bind(
undefined,
displaySetInstanceUid
)}
onDoubleClick={onThumbnailDoubleClick}
/>
</div>
);
});
})
.flat()}
</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 };

View File

@ -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;

View 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 };

View File

@ -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

View File

@ -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 };

View File

@ -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
);

View File

@ -1,2 +1,2 @@
export { StudyBrowser } from './StudyBrowser.js';
export { ThumbnailEntry } from './ThumbnailEntry.js';
export { Thumbnail } from './Thumbnail.js';

View File

@ -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,

View File

@ -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);
// });

View File

@ -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,
},