fix: download tool fixes & improvements (#1235)
* fix: download tool fixes & improvements * fix filename error * Create variable to track erros * Fix small console error * Fix all conflicts and merge changes from latest master with this PR's improvements * Small improvement on select and textinput labels * Add new icon for unlink * Refactor on download image modal * Adding loading screen * Fix translation issue * Fixing aspect Ratio and E2E tests * Allow empty value without setting to 0 * Remove eslint comments * Fixing typos Co-authored-by: Gustavo André Lelis <galelis@gmail.com> Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
This commit is contained in:
parent
96a8e26786
commit
b9574b6efc
14
platform/i18n/src/locales/en-US/ViewportDownloadForm.json
Normal file
14
platform/i18n/src/locales/en-US/ViewportDownloadForm.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"emptyFilenameError": "The file name cannot be empty.",
|
||||||
|
"fileType": "File Type",
|
||||||
|
"filename": "File Name",
|
||||||
|
"formTitle": "Please specify the dimensions, filename, and desired type for the output image.",
|
||||||
|
"imageHeight": "Image height (px)",
|
||||||
|
"imagePreview": "Image Preview",
|
||||||
|
"imageWidth": "Image width (px)",
|
||||||
|
"keepAspectRatio": "Keep aspect ratio",
|
||||||
|
"loadingPreview": "Loading Image Preview...",
|
||||||
|
"minHeightError": "The minimum valid height is 100px.",
|
||||||
|
"minWidthError": "The minimum valid width is 100px.",
|
||||||
|
"showAnnotations": "Show Annotations"
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import Header from './Header.json';
|
|||||||
import MeasurementTable from './MeasurementTable.json';
|
import MeasurementTable from './MeasurementTable.json';
|
||||||
import StudyList from './StudyList.json';
|
import StudyList from './StudyList.json';
|
||||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'en-US': {
|
'en-US': {
|
||||||
@ -19,5 +20,6 @@ export default {
|
|||||||
MeasurementTable,
|
MeasurementTable,
|
||||||
StudyList,
|
StudyList,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
|
ViewportDownloadForm,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,16 @@
|
|||||||
import React, { useEffect, useState, createRef } from 'react';
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
createRef,
|
||||||
|
} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import './ViewportDownloadForm.styl';
|
import './ViewportDownloadForm.styl';
|
||||||
import { TextInput, Select } from '@ohif/ui';
|
import { TextInput, Select, Icon } from '@ohif/ui';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
const FILE_TYPE_OPTIONS = [
|
const FILE_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
@ -17,6 +24,7 @@ const FILE_TYPE_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_FILENAME = 'image';
|
const DEFAULT_FILENAME = 'image';
|
||||||
|
const REFRESH_VIEWPORT_TIMEOUT = 1000;
|
||||||
|
|
||||||
const ViewportDownloadForm = ({
|
const ViewportDownloadForm = ({
|
||||||
activeViewport,
|
activeViewport,
|
||||||
@ -32,7 +40,7 @@ const ViewportDownloadForm = ({
|
|||||||
maximumSize,
|
maximumSize,
|
||||||
canvasClass,
|
canvasClass,
|
||||||
}) => {
|
}) => {
|
||||||
const [t] = useTranslation('Buttons');
|
const [t] = useTranslation('ViewportDownloadForm');
|
||||||
|
|
||||||
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
||||||
const [fileType, setFileType] = useState('jpg');
|
const [fileType, setFileType] = useState('jpg');
|
||||||
@ -44,6 +52,12 @@ const ViewportDownloadForm = ({
|
|||||||
|
|
||||||
const [showAnnotations, setShowAnnotations] = useState(true);
|
const [showAnnotations, setShowAnnotations] = useState(true);
|
||||||
|
|
||||||
|
const [keepAspect, setKeepAspect] = useState(true);
|
||||||
|
const [aspectMultiplier, setAspectMultiplier] = useState({
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
});
|
||||||
|
|
||||||
const [viewportElement, setViewportElement] = useState();
|
const [viewportElement, setViewportElement] = useState();
|
||||||
const [viewportElementDimensions, setViewportElementDimensions] = useState({
|
const [viewportElementDimensions, setViewportElementDimensions] = useState({
|
||||||
width: defaultSize,
|
width: defaultSize,
|
||||||
@ -62,22 +76,113 @@ const ViewportDownloadForm = ({
|
|||||||
height: defaultSize,
|
height: defaultSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cornerstone's `enable/disable`
|
const [error, setError] = useState({
|
||||||
useEffect(() => {
|
width: false,
|
||||||
enableViewport(viewportElement);
|
height: false,
|
||||||
|
filename: false,
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
const hasError = Object.values(error).includes(true);
|
||||||
disableViewport(viewportElement);
|
|
||||||
|
const refreshViewport = useRef(null);
|
||||||
|
|
||||||
|
const downloadImage = () => {
|
||||||
|
downloadBlob(
|
||||||
|
filename || DEFAULT_FILENAME,
|
||||||
|
fileType,
|
||||||
|
viewportElement,
|
||||||
|
downloadCanvas.ref.current
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} event - Input change event
|
||||||
|
* @param {string} dimension - "height" | "width"
|
||||||
|
*/
|
||||||
|
const onDimensionsChange = (event, dimension) => {
|
||||||
|
const oppositeDimension = dimension === 'height' ? 'width' : 'height';
|
||||||
|
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
|
||||||
|
const isEmpty = sanitizedTargetValue === '';
|
||||||
|
const newDimensions = { ...dimensions };
|
||||||
|
const updatedDimension = isEmpty
|
||||||
|
? ''
|
||||||
|
: Math.min(sanitizedTargetValue, maximumSize);
|
||||||
|
|
||||||
|
if (updatedDimension === dimensions[dimension]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newDimensions[dimension] = updatedDimension;
|
||||||
|
|
||||||
|
if (keepAspect && newDimensions[oppositeDimension] !== '') {
|
||||||
|
newDimensions[oppositeDimension] = Math.round(
|
||||||
|
newDimensions[dimension] * aspectMultiplier[oppositeDimension]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In current code, keepAspect is always `true`
|
||||||
|
// And we always start w/ a square width/height
|
||||||
|
setDimensions(newDimensions);
|
||||||
|
|
||||||
|
// Only update if value is non-empty
|
||||||
|
if (!isEmpty) {
|
||||||
|
setViewportElementDimensions(newDimensions);
|
||||||
|
setDownloadCanvas(state => ({
|
||||||
|
...state,
|
||||||
|
...newDimensions,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const error_messages = {
|
||||||
|
width: t('minWidthError'),
|
||||||
|
height: t('minHeightError'),
|
||||||
|
filename: t('emptyFilenameError'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderErrorHandler = errorType => {
|
||||||
|
if (!error[errorType]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="input-error">{error_messages[errorType]}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeepAspectToggle = () => {
|
||||||
|
const { width, height } = dimensions;
|
||||||
|
const aspectMultiplier = { ...aspectMultiplier };
|
||||||
|
if (!keepAspect) {
|
||||||
|
const base = Math.min(width, height);
|
||||||
|
aspectMultiplier.width = width / base;
|
||||||
|
aspectMultiplier.height = height / base;
|
||||||
|
setAspectMultiplier(aspectMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeepAspect(!keepAspect);
|
||||||
};
|
};
|
||||||
}, [disableViewport, enableViewport, viewportElement]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { width, height } = viewportElementDimensions;
|
|
||||||
const validSize = value => (value >= minimumSize ? value : minimumSize);
|
const validSize = value => (value >= minimumSize ? value : minimumSize);
|
||||||
const loadAndUpdateViewports = async () => {
|
const loadAndUpdateViewports = useCallback(async () => {
|
||||||
await loadImage(activeViewport, viewportElement, width, height);
|
const { width: scaledWidth, height: scaledHeight } = await loadImage(
|
||||||
|
activeViewport,
|
||||||
|
viewportElement,
|
||||||
|
dimensions.width,
|
||||||
|
dimensions.height
|
||||||
|
);
|
||||||
|
|
||||||
toggleAnnotations(showAnnotations, viewportElement);
|
toggleAnnotations(showAnnotations, viewportElement);
|
||||||
|
|
||||||
|
const scaledDimensions = {
|
||||||
|
height: validSize(scaledHeight),
|
||||||
|
width: validSize(scaledWidth),
|
||||||
|
};
|
||||||
|
|
||||||
|
setViewportElementDimensions(scaledDimensions);
|
||||||
|
setDownloadCanvas(state => ({
|
||||||
|
...state,
|
||||||
|
...scaledDimensions,
|
||||||
|
}));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dataUrl,
|
dataUrl,
|
||||||
width: viewportElementWidth,
|
width: viewportElementWidth,
|
||||||
@ -94,9 +199,6 @@ const ViewportDownloadForm = ({
|
|||||||
width: validSize(viewportElementWidth),
|
width: validSize(viewportElementWidth),
|
||||||
height: validSize(viewportElementHeight),
|
height: validSize(viewportElementHeight),
|
||||||
}));
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
loadAndUpdateViewports();
|
|
||||||
}, [
|
}, [
|
||||||
activeViewport,
|
activeViewport,
|
||||||
viewportElement,
|
viewportElement,
|
||||||
@ -111,76 +213,96 @@ const ViewportDownloadForm = ({
|
|||||||
viewportElementDimensions,
|
viewportElementDimensions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
useEffect(() => {
|
||||||
* @param {object} event - Input change event
|
enableViewport(viewportElement);
|
||||||
* @param {string} dimension - "height" | "width"
|
|
||||||
*/
|
|
||||||
const onDimensionsChange = (event, dimension) => {
|
|
||||||
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
|
|
||||||
const isEmpty = sanitizedTargetValue === '';
|
|
||||||
const updatedDimension = isEmpty
|
|
||||||
? ''
|
|
||||||
: Math.min(sanitizedTargetValue, maximumSize);
|
|
||||||
|
|
||||||
if (updatedDimension === dimensions.width) {
|
return () => {
|
||||||
return;
|
disableViewport(viewportElement);
|
||||||
}
|
|
||||||
|
|
||||||
// In current code, keepAspect is always `true`
|
|
||||||
// And we always start w/ a square width/height
|
|
||||||
setDimensions({
|
|
||||||
width: updatedDimension,
|
|
||||||
height: updatedDimension,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only update if value is non-empty
|
|
||||||
if (!isEmpty) {
|
|
||||||
setViewportElementDimensions({
|
|
||||||
height: updatedDimension,
|
|
||||||
width: updatedDimension,
|
|
||||||
});
|
|
||||||
setDownloadCanvas(state => ({
|
|
||||||
...state,
|
|
||||||
height: updatedDimension,
|
|
||||||
width: updatedDimension,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}, [disableViewport, enableViewport, viewportElement]);
|
||||||
|
|
||||||
const downloadImage = () => {
|
useEffect(() => {
|
||||||
downloadBlob(
|
if (refreshViewport.current !== null) {
|
||||||
filename || DEFAULT_FILENAME,
|
clearTimeout(refreshViewport.current);
|
||||||
fileType,
|
}
|
||||||
|
|
||||||
|
refreshViewport.current = setTimeout(() => {
|
||||||
|
refreshViewport.current = null;
|
||||||
|
loadAndUpdateViewports();
|
||||||
|
}, REFRESH_VIEWPORT_TIMEOUT);
|
||||||
|
}, [
|
||||||
|
activeViewport,
|
||||||
viewportElement,
|
viewportElement,
|
||||||
downloadCanvas.ref.current
|
showAnnotations,
|
||||||
);
|
dimensions,
|
||||||
|
loadImage,
|
||||||
|
toggleAnnotations,
|
||||||
|
updateViewportPreview,
|
||||||
|
fileType,
|
||||||
|
downloadCanvas.ref,
|
||||||
|
minimumSize,
|
||||||
|
maximumSize,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { width, height } = dimensions;
|
||||||
|
const hasError = {
|
||||||
|
width: width < minimumSize,
|
||||||
|
height: height < minimumSize,
|
||||||
|
filename: !filename,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setError({ ...hasError });
|
||||||
|
}, [dimensions, filename, minimumSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ViewportDownloadForm">
|
<div className="ViewportDownloadForm">
|
||||||
<div className="title">
|
<div className="title">{t('formTitle')}</div>
|
||||||
{t(
|
|
||||||
'Please specify the dimensions, filename, and desired type for the output image.'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="file-info-container" data-cy="file-info-container">
|
<div className="file-info-container" data-cy="file-info-container">
|
||||||
<div className="col">
|
<div className="dimension-wrapper">
|
||||||
|
<div className="dimensions">
|
||||||
<div className="width">
|
<div className="width">
|
||||||
<TextInput
|
<TextInput
|
||||||
data-cy="image-width"
|
type="number"
|
||||||
|
min={minimumSize}
|
||||||
|
max={maximumSize}
|
||||||
value={dimensions.width}
|
value={dimensions.width}
|
||||||
label={t('Image width (px)')}
|
label={t('imageWidth')}
|
||||||
onChange={evt => onDimensionsChange(evt, 'height')}
|
onChange={evt => onDimensionsChange(evt, 'width')}
|
||||||
|
data-cy="image-width"
|
||||||
/>
|
/>
|
||||||
|
{renderErrorHandler('width')}
|
||||||
</div>
|
</div>
|
||||||
<div className="height">
|
<div className="height">
|
||||||
<TextInput
|
<TextInput
|
||||||
data-cy="image-height"
|
type="number"
|
||||||
|
min={minimumSize}
|
||||||
|
max={maximumSize}
|
||||||
value={dimensions.height}
|
value={dimensions.height}
|
||||||
label={t('Image height (px)')}
|
label={t('imageHeight')}
|
||||||
onChange={evt => onDimensionsChange(evt, 'width')}
|
onChange={evt => onDimensionsChange(evt, 'height')}
|
||||||
|
data-cy="image-height"
|
||||||
/>
|
/>
|
||||||
|
{renderErrorHandler('height')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="keep-aspect-wrapper">
|
||||||
|
<button
|
||||||
|
id="keep-aspect"
|
||||||
|
className={classnames(
|
||||||
|
'form-button btn',
|
||||||
|
keepAspect ? 'active' : ''
|
||||||
|
)}
|
||||||
|
data-cy="keep-aspect"
|
||||||
|
alt={t('keepAspectRatio')}
|
||||||
|
onClick={onKeepAspectToggle}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={keepAspect ? 'link' : 'unlink'}
|
||||||
|
alt={keepAspect ? 'Dismiss Aspect' : 'Keep Aspect'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -191,9 +313,10 @@ const ViewportDownloadForm = ({
|
|||||||
data-cy="file-name"
|
data-cy="file-name"
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={event => setFilename(event.target.value)}
|
onChange={event => setFilename(event.target.value)}
|
||||||
label={t('File name')}
|
label={t('filename')}
|
||||||
id="file-name"
|
id="file-name"
|
||||||
/>
|
/>
|
||||||
|
{renderErrorHandler('filename')}
|
||||||
</div>
|
</div>
|
||||||
<div className="file-type">
|
<div className="file-type">
|
||||||
<Select
|
<Select
|
||||||
@ -201,7 +324,7 @@ const ViewportDownloadForm = ({
|
|||||||
data-cy="file-type"
|
data-cy="file-type"
|
||||||
onChange={event => setFileType(event.target.value)}
|
onChange={event => setFileType(event.target.value)}
|
||||||
options={FILE_TYPE_OPTIONS}
|
options={FILE_TYPE_OPTIONS}
|
||||||
label={t('File type')}
|
label={t('fileType')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -217,7 +340,7 @@ const ViewportDownloadForm = ({
|
|||||||
checked={showAnnotations}
|
checked={showAnnotations}
|
||||||
onChange={event => setShowAnnotations(event.target.checked)}
|
onChange={event => setShowAnnotations(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
{t('Show Annotations')}
|
{t('showAnnotations')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -245,15 +368,23 @@ const ViewportDownloadForm = ({
|
|||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{viewportPreview.src ? (
|
||||||
<div className="preview" data-cy="image-preview">
|
<div className="preview" data-cy="image-preview">
|
||||||
<h4> {t('Image Preview')}</h4>
|
<div className="preview-header"> {t('imagePreview')}</div>
|
||||||
<img
|
<img
|
||||||
className="viewport-preview"
|
className="viewport-preview"
|
||||||
src={viewportPreview.src}
|
src={viewportPreview.src}
|
||||||
alt="Viewport Preview"
|
alt={t('imagePreview')}
|
||||||
|
data-cy="image-preview"
|
||||||
data-cy="viewport-preview-img"
|
data-cy="viewport-preview-img"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="loading-image">
|
||||||
|
<Icon name="circle-notch" className="icon-spin" />
|
||||||
|
{t('loadingPreview')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="action-cancel">
|
<div className="action-cancel">
|
||||||
@ -263,16 +394,17 @@ const ViewportDownloadForm = ({
|
|||||||
className="btn btn-danger"
|
className="btn btn-danger"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
{t('Cancel')}
|
{t('Buttons:Cancel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="action-save">
|
<div className="action-save">
|
||||||
<button
|
<button
|
||||||
|
disabled={hasError}
|
||||||
onClick={downloadImage}
|
onClick={downloadImage}
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
data-cy="download-btn"
|
data-cy="download-btn"
|
||||||
>
|
>
|
||||||
{t('Download')}
|
{t('Buttons:Download')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,6 +10,9 @@
|
|||||||
input, select
|
input, select
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
|
|
||||||
|
#keep-aspect svg
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
.title
|
.title
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -17,33 +20,58 @@
|
|||||||
.file-info-container
|
.file-info-container
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 20px 10px 0;
|
padding: 20px 10px;
|
||||||
background-color: #16202b;
|
background-color: var(--ui-gray-dark);
|
||||||
|
|
||||||
.form-control.input-ohif
|
|
||||||
padding: 6px 12px;
|
|
||||||
|
|
||||||
@media screen and (max-width: 1023px)
|
@media screen and (max-width: 1023px)
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
.col
|
.width,
|
||||||
flex-grow: 1;
|
.height,
|
||||||
|
.file-name,
|
||||||
|
.file-type
|
||||||
|
height: 56px;
|
||||||
|
|
||||||
.input-ohif
|
.input-ohif
|
||||||
margin-left: 15px;
|
margin: 0 5px;
|
||||||
@media screen and (max-width: 1023px)
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 5px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
|
.file-name,
|
||||||
.file-type
|
.file-type
|
||||||
.select-ohif
|
.select-ohif, .input-ohif
|
||||||
margin-left: 17px;
|
width: 170px;
|
||||||
|
|
||||||
|
.input-ohif-label, .select-ohif-label
|
||||||
|
width: 90px;
|
||||||
|
display: inline-block;
|
||||||
@media screen and (max-width: 1023px)
|
@media screen and (max-width: 1023px)
|
||||||
margin-left: 0;
|
width: 120px;
|
||||||
width: 100%;
|
|
||||||
|
.dimension-wrapper
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.dimensions
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.input-ohif-label
|
||||||
|
width: 120px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.input-ohif
|
||||||
|
@media screen and (max-width: 1023px)
|
||||||
|
width: 170px;
|
||||||
|
|
||||||
|
.keep-aspect-wrapper
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 86px;
|
||||||
|
|
||||||
.show-annotations
|
.show-annotations
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -56,46 +84,53 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.preview
|
.loading-image
|
||||||
|
height: 580px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction column;
|
|
||||||
height: fit-content;
|
|
||||||
background-color: #16202b;
|
|
||||||
width: fit-content;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
align-self: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
@media screen and (max-width: 1023px)
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: var(--active-color);
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
.icon-spin
|
||||||
|
margin-right: 15px;
|
||||||
|
|
||||||
|
.preview
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
height: 580px;
|
||||||
|
|
||||||
.viewport-preview
|
.viewport-preview
|
||||||
max-height: 512px;
|
max-height: 512px;
|
||||||
max-width: 512px;
|
max-width: 512px;
|
||||||
|
|
||||||
h4
|
.preview-header
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align center;
|
text-align center;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
|
|
||||||
.preview-container
|
|
||||||
width: auto;
|
|
||||||
height: 100%;
|
|
||||||
max-height: 400px;
|
|
||||||
object-fit contain;
|
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.action-cancel
|
margin-top: 20px;
|
||||||
margin: 0 20px;
|
|
||||||
.actions-save
|
.btn
|
||||||
margin: 0 0 0 10px;
|
margin: 0 10px;
|
||||||
|
|
||||||
|
.input-error
|
||||||
|
font-size: 12px;
|
||||||
|
color: red;
|
||||||
|
text-align: center;
|
||||||
|
margin: 3px 0;
|
||||||
|
|
||||||
|
|
||||||
.modal-dialog
|
.modal-dialog
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -10,7 +10,10 @@ export class TableListItem extends Component {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
itemClass: PropTypes.string,
|
itemClass: PropTypes.string,
|
||||||
itemIndex: PropTypes.number,
|
itemIndex: PropTypes.number,
|
||||||
itemKey: PropTypes.oneOfType(['number', 'string']),
|
itemKey: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
onItemClick: PropTypes.func.isRequired,
|
onItemClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
.icon-pulse
|
.icon-pulse
|
||||||
fa-spin 1s infinite steps(8)
|
fa-spin 1s infinite steps(8)
|
||||||
|
|
||||||
|
.icon-spin {
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fa-spin{
|
@keyframes fa-spin{
|
||||||
0%{ transform:rotate(0deg) }
|
0%{ transform:rotate(0deg) }
|
||||||
to{ transform:rotate(1turn) }
|
to{ transform:rotate(1turn) }
|
||||||
|
|||||||
@ -80,6 +80,7 @@ import thLarge from './icons/th-large.svg';
|
|||||||
import thList from './icons/th-list.svg';
|
import thList from './icons/th-list.svg';
|
||||||
import times from './icons/times.svg';
|
import times from './icons/times.svg';
|
||||||
import trash from './icons/trash.svg';
|
import trash from './icons/trash.svg';
|
||||||
|
import unlink from './icons/unlink.svg';
|
||||||
import user from './icons/user.svg';
|
import user from './icons/user.svg';
|
||||||
import youtube from './icons/youtube.svg';
|
import youtube from './icons/youtube.svg';
|
||||||
|
|
||||||
@ -158,6 +159,7 @@ const ICONS = {
|
|||||||
rotate,
|
rotate,
|
||||||
'rotate-right': rotateRight,
|
'rotate-right': rotateRight,
|
||||||
trash,
|
trash,
|
||||||
|
unlink,
|
||||||
'exclamation-circle': exclamationCircle,
|
'exclamation-circle': exclamationCircle,
|
||||||
link,
|
link,
|
||||||
'exclamation-triangle': exclamationTriangle,
|
'exclamation-triangle': exclamationTriangle,
|
||||||
|
|||||||
11
platform/ui/src/elements/Icon/icons/unlink.svg
Normal file
11
platform/ui/src/elements/Icon/icons/unlink.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-labelledby="unlink"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<title id="title">Unlink</title>
|
||||||
|
<path d="M304.083 388.936c4.686 4.686 4.686 12.284 0 16.971l-65.057 65.056c-54.709 54.711-143.27 54.721-197.989 0-54.713-54.713-54.719-143.27 0-197.989l65.056-65.057c4.686-4.686 12.284-4.686 16.971 0l22.627 22.627c4.686 4.686 4.686 12.284 0 16.971L81.386 311.82c-34.341 34.341-33.451 88.269.597 120.866 32.577 31.187 84.788 31.337 117.445-1.32l65.057-65.056c4.686-4.686 12.284-4.686 16.971 0l22.627 22.626zm-56.568-243.245l64.304-64.304c34.346-34.346 88.286-33.453 120.882.612 31.18 32.586 31.309 84.785-1.335 117.43l-65.056 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.686 4.686 12.284 4.686 16.971 0l65.056-65.057c54.711-54.709 54.721-143.271 0-197.99-54.71-54.711-143.27-54.72-197.989 0l-65.057 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.685 4.685 12.283 4.685 16.97-.001zm238.343 362.794l22.627-22.627c4.686-4.686 4.686-12.284 0-16.971L43.112 3.515c-4.686-4.686-12.284-4.686-16.971 0L3.515 26.142c-4.686 4.686-4.686 12.284 0 16.971l465.373 465.373c4.686 4.686 12.284 4.686 16.97-.001z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -29,8 +29,9 @@ class Select extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="select-ohif-container">
|
<div className="select-ohif-container">
|
||||||
<label className="select-ohif-label" htmlFor={this.id}>
|
{this.props.label && (
|
||||||
{this.props.label}
|
<label className="select-ohif-label" htmlFor={this.id}>{this.props.label}</label>
|
||||||
|
)}
|
||||||
<select className="form-control select-ohif" {...this.props}>
|
<select className="form-control select-ohif" {...this.props}>
|
||||||
{this.props.options.map(({ key, value }) => {
|
{this.props.options.map(({ key, value }) => {
|
||||||
return (
|
return (
|
||||||
@ -40,7 +41,6 @@ class Select extends Component {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,15 +28,15 @@ class TextInput extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="input-ohif-container">
|
<div className="input-ohif-container">
|
||||||
<label className="input-ohif-label" htmlFor={this.props.id}>
|
{this.props.label && (
|
||||||
{this.props.label}
|
<label className="input-ohif-label" htmlFor={this.props.id}>{this.props.label}</label>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type={this.props.type}
|
type={this.props.type}
|
||||||
id={this.props.id}
|
id={this.props.id}
|
||||||
className="form-control input-ohif"
|
className="form-control input-ohif"
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user