From a538824cc715a18b8c205ecbd0292daf3f7834bf Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Fri, 19 Jun 2020 20:59:09 -0300 Subject: [PATCH 01/32] feat: Modal + ViewportDownloadForm --- .../src/CornerstoneViewportDownloadForm.js | 6 +- platform/ui/index.js | 2 + platform/ui/src/assets/icons/link.svg | 11 + platform/ui/src/assets/icons/unlink.svg | 11 + platform/ui/src/components/Icon/getIcon.jsx | 4 + .../components/InputNumber/InputNumber.jsx | 50 +++ .../components/InputNumber/InputNumber.mdx | 44 ++ .../ui/src/components/InputNumber/index.js | 2 + .../ui/src/components/InputText/InputText.jsx | 12 +- platform/ui/src/components/Modal/Modal.jsx | 32 +- platform/ui/src/components/Select/Select.jsx | 5 + .../ViewportDownloadForm.jsx | 425 ++++++++++++++++++ .../components/ViewportDownloadForm/index.js | 1 + platform/ui/src/components/index.js | 4 + yarn.lock | 10 - 15 files changed, 592 insertions(+), 27 deletions(-) create mode 100644 platform/ui/src/assets/icons/link.svg create mode 100644 platform/ui/src/assets/icons/unlink.svg create mode 100644 platform/ui/src/components/InputNumber/InputNumber.jsx create mode 100644 platform/ui/src/components/InputNumber/InputNumber.mdx create mode 100644 platform/ui/src/components/InputNumber/index.js create mode 100644 platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx create mode 100644 platform/ui/src/components/ViewportDownloadForm/index.js diff --git a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js index babf3bf69..7582c0960 100644 --- a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js +++ b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js @@ -145,8 +145,4 @@ CornerstoneViewportDownloadForm.propTypes = { activeViewportIndex: PropTypes.number.isRequired, }; -// export default CornerstoneViewportDownloadForm; - -export default function HelloWorld() { - return
Hello World
; -} +export default CornerstoneViewportDownloadForm; diff --git a/platform/ui/index.js b/platform/ui/index.js index db85039d9..4e819e60a 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -39,6 +39,7 @@ export { InputGroup, InputLabelWrapper, InputMultiSelect, + InputNumber, InputText, Label, LayoutSelector, @@ -73,6 +74,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, } from './src/components'; diff --git a/platform/ui/src/assets/icons/link.svg b/platform/ui/src/assets/icons/link.svg new file mode 100644 index 000000000..7c99fc27c --- /dev/null +++ b/platform/ui/src/assets/icons/link.svg @@ -0,0 +1,11 @@ + + + + diff --git a/platform/ui/src/assets/icons/unlink.svg b/platform/ui/src/assets/icons/unlink.svg new file mode 100644 index 000000000..37c53bbb9 --- /dev/null +++ b/platform/ui/src/assets/icons/unlink.svg @@ -0,0 +1,11 @@ + + Unlink + + diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 145539060..7b1fc4dc2 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -16,6 +16,7 @@ import info from './../../assets/icons/info.svg'; import infoLink from './../../assets/icons/info-link.svg'; import launchArrow from './../../assets/icons/launch-arrow.svg'; import launchInfo from './../../assets/icons/launch-info.svg'; +import link from './../../assets/icons/link.svg'; import listBullets from './../../assets/icons/list-bullets.svg'; import lock from './../../assets/icons/lock.svg'; import logoOhifSmall from './../../assets/icons/logo-ohif-small.svg'; @@ -30,6 +31,7 @@ import sorting from './../../assets/icons/sorting.svg'; import sortingActiveDown from './../../assets/icons/sorting-active-down.svg'; import sortingActiveUp from './../../assets/icons/sorting-active-up.svg'; import tracked from './../../assets/icons/tracked.svg'; +import unlink from './../../assets/icons/unlink.svg'; /** Tools */ import toolZoom from './../../assets/icons/tool-zoom.svg'; @@ -59,6 +61,7 @@ const ICONS = { 'info-link': infoLink, 'launch-arrow': launchArrow, 'launch-info': launchInfo, + link: link, 'list-bullets': listBullets, lock: lock, 'logo-ohif-small': logoOhifSmall, @@ -73,6 +76,7 @@ const ICONS = { 'sorting-active-up': sortingActiveUp, sorting: sorting, tracked: tracked, + unlink: unlink, /** Tools */ 'tool-zoom': toolZoom, diff --git a/platform/ui/src/components/InputNumber/InputNumber.jsx b/platform/ui/src/components/InputNumber/InputNumber.jsx new file mode 100644 index 000000000..c6fca474f --- /dev/null +++ b/platform/ui/src/components/InputNumber/InputNumber.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Input, InputLabelWrapper } from '@ohif/ui'; + +const InputNumber = ({ + label, + isSortable, + sortDirection, + onLabelClick, + value, + onChange, +}) => { + return ( + + { + onChange(event.target.value); + }} + /> + + ); +}; + +InputNumber.defaultProps = { + value: '', + isSortable: false, + onLabelClick: () => {}, + sortDirection: 'none', +}; + +InputNumber.propTypes = { + label: PropTypes.string.isRequired, + isSortable: PropTypes.bool, + sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']), + onLabelClick: PropTypes.func, + value: PropTypes.number, + onChange: PropTypes.func.isRequired, +}; + +export default InputNumber; diff --git a/platform/ui/src/components/InputNumber/InputNumber.mdx b/platform/ui/src/components/InputNumber/InputNumber.mdx new file mode 100644 index 000000000..6435c32ab --- /dev/null +++ b/platform/ui/src/components/InputNumber/InputNumber.mdx @@ -0,0 +1,44 @@ +--- +name: InputNumber +menu: Form +route: components/InputNumber +--- + +import { useState } from 'react'; +import { Playground, Props } from 'docz'; +import { InputNumber } from '@ohif/ui'; + +# Input Text + +## Import + +```javascript +import { InputNumber } from '@ohif/ui'; +``` + +## Basic usage + + + {() => { + const [number, setNumber] = useState(10); + return ( +
+
+ { + setText(value); + }} + /> +
+
+ ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/InputNumber/index.js b/platform/ui/src/components/InputNumber/index.js new file mode 100644 index 000000000..566112449 --- /dev/null +++ b/platform/ui/src/components/InputNumber/index.js @@ -0,0 +1,2 @@ +import InputNumber from './InputNumber'; +export default InputNumber; diff --git a/platform/ui/src/components/InputText/InputText.jsx b/platform/ui/src/components/InputText/InputText.jsx index 2a244d7e8..fc62e3cdd 100644 --- a/platform/ui/src/components/InputText/InputText.jsx +++ b/platform/ui/src/components/InputText/InputText.jsx @@ -23,7 +23,7 @@ const InputText = ({ type="text" containerClassName="mr-2" value={value} - onChange={(event) => { + onChange={event => { onChange(event.target.value); }} /> @@ -33,14 +33,16 @@ const InputText = ({ InputText.defaultProps = { value: '', + isSortable: false, + onLabelClick: () => {}, + sortDirection: 'none', }; InputText.propTypes = { label: PropTypes.string.isRequired, - isSortable: PropTypes.bool.isRequired, - sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']) - .isRequired, - onLabelClick: PropTypes.func.isRequired, + isSortable: PropTypes.bool, + sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']), + onLabelClick: PropTypes.func, value: PropTypes.string, onChange: PropTypes.func.isRequired, }; diff --git a/platform/ui/src/components/Modal/Modal.jsx b/platform/ui/src/components/Modal/Modal.jsx index 4ef669cbf..79fa9709e 100644 --- a/platform/ui/src/components/Modal/Modal.jsx +++ b/platform/ui/src/components/Modal/Modal.jsx @@ -3,13 +3,16 @@ import PropTypes from 'prop-types'; import ReactModal from 'react-modal'; import classNames from 'classnames'; +import { Typography } from '@ohif/ui'; + const customStyle = { overlay: { zIndex: 1071, - backgroundColor: 'rgb(0, 0, 0, 0.5)', + backgroundColor: 'rgb(0, 0, 0, 0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', + padding: '40px 0', }, }; @@ -27,10 +30,14 @@ const Modal = ({ const renderHeader = () => { return ( title && ( -
-

{title}

+
+ {title} {closeButton && ( - )} @@ -41,20 +48,31 @@ const Modal = ({ return ( <> - {renderHeader()} -
{children}
+
{renderHeader()}
+
+ {children} +
); }; +Modal.defaultProps = { + shouldCloseOnEsc: true, +}; + Modal.propTypes = { className: PropTypes.string, closeButton: PropTypes.bool, diff --git a/platform/ui/src/components/Select/Select.jsx b/platform/ui/src/components/Select/Select.jsx index 3a47d8a6a..b51b64056 100644 --- a/platform/ui/src/components/Select/Select.jsx +++ b/platform/ui/src/components/Select/Select.jsx @@ -76,6 +76,11 @@ const Select = ({ options={options} value={selectedOptions} onChange={(selectedOptions, { action }) => { + if (!isMulti) { + onChange(selectedOptions, action); + return; + } + const newSelection = selectedOptions.reduce( (acc, curr) => acc.concat([curr.value]), [] diff --git a/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx new file mode 100644 index 000000000..6939ae0f7 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx @@ -0,0 +1,425 @@ +import React, { + useCallback, + useEffect, + useState, + createRef, + useRef, +} from 'react'; + +import { + Typography, + InputText, + InputNumber, + Tooltip, + IconButton, + Icon, + Select, + InputLabelWrapper, + Button, +} from '@ohif/ui'; + +const FILE_TYPE_OPTIONS = [ + { + value: 'jpg', + label: 'jpg', + }, + { + value: 'png', + label: 'png', + }, +]; + +const DEFAULT_FILENAME = 'image'; +const REFRESH_VIEWPORT_TIMEOUT = 1000; + +const ViewportDownloadForm = ({ + activeViewport, + onClose, + updateViewportPreview, + enableViewport, + disableViewport, + toggleAnnotations, + loadImage, + downloadBlob, + defaultSize, + minimumSize, + maximumSize, + canvasClass, +}) => { + const [filename, setFilename] = useState(DEFAULT_FILENAME); + const [fileType, setFileType] = useState(['jpg']); + + const [dimensions, setDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [showAnnotations, setShowAnnotations] = useState(true); + + const [keepAspect, setKeepAspect] = useState(true); + const [aspectMultiplier, setAspectMultiplier] = useState({ + width: 1, + height: 1, + }); + + const [viewportElement, setViewportElement] = useState(); + const [viewportElementDimensions, setViewportElementDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [downloadCanvas, setDownloadCanvas] = useState({ + ref: createRef(), + width: defaultSize, + height: defaultSize, + }); + + const [viewportPreview, setViewportPreview] = useState({ + src: null, + width: defaultSize, + height: defaultSize, + }); + + const [error, setError] = useState({ + width: false, + height: false, + filename: false, + }); + + const hasError = Object.values(error).includes(true); + + const refreshViewport = useRef(null); + + 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); + }; + + const downloadImage = () => { + downloadBlob( + filename || DEFAULT_FILENAME, + fileType, + viewportElement, + downloadCanvas.ref.current + ); + }; + + /** + * @param {object} value - Input value + * @param {string} dimension - "height" | "width" + */ + const onDimensionsChange = (value, dimension) => { + const oppositeDimension = dimension === 'height' ? 'width' : 'height'; + const sanitizedTargetValue = 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: 'The minimum valid width is 100px.', + height: 'The minimum valid height is 100px.', + filename: 'The file name cannot be empty.', + }; + + const renderErrorHandler = errorType => { + if (!error[errorType]) { + return null; + } + + return ( + + {error_messages[errorType]} + + ); + }; + + const validSize = value => (value >= minimumSize ? value : minimumSize); + + const loadAndUpdateViewports = useCallback(async () => { + const { width: scaledWidth, height: scaledHeight } = await loadImage( + activeViewport, + viewportElement, + dimensions.width, + dimensions.height + ); + + toggleAnnotations(showAnnotations, viewportElement); + + const scaledDimensions = { + height: validSize(scaledHeight), + width: validSize(scaledWidth), + }; + + setViewportElementDimensions(scaledDimensions); + setDownloadCanvas(state => ({ + ...state, + ...scaledDimensions, + })); + + const { + dataUrl, + width: viewportElementWidth, + height: viewportElementHeight, + } = await updateViewportPreview( + viewportElement, + downloadCanvas.ref.current, + fileType + ); + + setViewportPreview(state => ({ + ...state, + src: dataUrl, + width: validSize(viewportElementWidth), + height: validSize(viewportElementHeight), + })); + }, [ + loadImage, + activeViewport, + viewportElement, + dimensions.width, + dimensions.height, + toggleAnnotations, + showAnnotations, + validSize, + updateViewportPreview, + downloadCanvas.ref, + fileType, + ]); + + useEffect(() => { + enableViewport(viewportElement); + + return () => { + disableViewport(viewportElement); + }; + }, [disableViewport, enableViewport, viewportElement]); + + useEffect(() => { + if (refreshViewport.current !== null) { + clearTimeout(refreshViewport.current); + } + + refreshViewport.current = setTimeout(() => { + refreshViewport.current = null; + loadAndUpdateViewports(); + }, REFRESH_VIEWPORT_TIMEOUT); + }, [ + activeViewport, + viewportElement, + showAnnotations, + dimensions, + loadImage, + toggleAnnotations, + updateViewportPreview, + fileType, + downloadCanvas.ref, + minimumSize, + maximumSize, + loadAndUpdateViewports, + ]); + + useEffect(() => { + const { width, height } = dimensions; + const hasError = { + width: width < minimumSize, + height: height < minimumSize, + filename: !filename, + }; + + setError({ ...hasError }); + }, [dimensions, filename, minimumSize]); + + return ( +
+ + Please specify the dimensions, filename, and desired type for the output + image. + + +
+
+ setFilename(value)} + label="File Name" + /> + {renderErrorHandler('filename')} +
+
+
+
+
+ onDimensionsChange(value, 'width')} + data-cy="image-width" + /> + {renderErrorHandler('width')} +
+
+ onDimensionsChange(value, 'height')} + data-cy="image-height" + /> + {renderErrorHandler('height')} +
+
+ +
+ + + + + +
+
+ +
+
+ {}} + > + setShowAnnotations(event.target.checked)} + /> + Show Annotations + +
+
+
+
+ +
+
setViewportElement(ref)} + > + +
+ + {viewportPreview.src ? ( +
+
Image preview
+ Preview +
+ ) : ( +
+ Loading Image Preview... +
+ )} +
+ +
+ + +
+
+ ); +}; + +export default ViewportDownloadForm; diff --git a/platform/ui/src/components/ViewportDownloadForm/index.js b/platform/ui/src/components/ViewportDownloadForm/index.js new file mode 100644 index 000000000..d630f3a19 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/index.js @@ -0,0 +1 @@ +export { default } from './ViewportDownloadForm'; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 564522ab0..d67bbb9ad 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -10,6 +10,7 @@ import InputDateRange from './InputDateRange'; import InputGroup from './InputGroup'; import InputLabelWrapper from './InputLabelWrapper'; import InputMultiSelect from './InputMultiSelect'; +import InputNumber from './InputNumber'; import InputText from './InputText'; import Label from './Label'; import LayoutSelector from './LayoutSelector'; @@ -43,6 +44,7 @@ import Tooltip from './Tooltip'; import Typography from './Typography'; import Viewport from './Viewport'; import ViewportActionBar from './ViewportActionBar'; +import ViewportDownloadForm from './ViewportDownloadForm'; import ViewportGrid from './ViewportGrid'; import ViewportPane from './ViewportPane'; @@ -59,6 +61,7 @@ export { InputGroup, InputLabelWrapper, InputMultiSelect, + InputNumber, InputText, Label, LayoutSelector, @@ -93,6 +96,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, }; diff --git a/yarn.lock b/yarn.lock index b909ed6c5..eb88b95ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17889,16 +17889,6 @@ react-cornerstone-viewport@2.3.8: prop-types "^15.7.2" react-resize-detector "^4.2.1" -react-cornerstone-viewport@2.3.9: - version "2.3.9" - resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.9.tgz#f9761da8e536f0a217137c6ca1a983f5882249f9" - integrity sha512-qrhq8CbX/jq6b93cQjV2qC/mhHOvFBpxzxcHlvHzEQZt/rmRMcaYxCqjpNaNbUmmBu61wNkxesUVsggkPTTcqg== - dependencies: - classnames "^2.2.6" - date-fns "^2.2.1" - prop-types "^15.7.2" - react-resize-detector "^4.2.1" - react-dates@21.2.1: version "21.2.1" resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.2.1.tgz#a979ed6876326ccfbf754a019bc95458cc061ad8" From c89f3682cc9a31f855b6b6812675abac552f87ef Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Mon, 22 Jun 2020 20:14:59 -0300 Subject: [PATCH 02/32] styles for image preview box --- .../ViewportDownloadForm/ViewportDownloadForm.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx index 6939ae0f7..3f1283441 100644 --- a/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx +++ b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx @@ -388,10 +388,13 @@ const ViewportDownloadForm = ({ {viewportPreview.src ? ( -
-
Image preview
+
+ Image preview Preview Date: Mon, 22 Jun 2020 22:38:39 -0300 Subject: [PATCH 03/32] modal update & close handler --- platform/core/src/services/UIModalService/index.js | 2 +- platform/ui/src/components/Modal/Modal.jsx | 9 ++++++++- platform/ui/src/contextProviders/ModalComponent.jsx | 2 +- platform/ui/src/contextProviders/ModalProvider.jsx | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/platform/core/src/services/UIModalService/index.js b/platform/core/src/services/UIModalService/index.js index ed2eab96a..884e53500 100644 --- a/platform/core/src/services/UIModalService/index.js +++ b/platform/core/src/services/UIModalService/index.js @@ -33,7 +33,7 @@ const serviceImplementation = { function _show({ content = null, contentProps = null, - shouldCloseOnEsc = false, + shouldCloseOnEsc = true, isOpen = true, closeButton = true, title = null, diff --git a/platform/ui/src/components/Modal/Modal.jsx b/platform/ui/src/components/Modal/Modal.jsx index 79fa9709e..8e3418829 100644 --- a/platform/ui/src/components/Modal/Modal.jsx +++ b/platform/ui/src/components/Modal/Modal.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ReactModal from 'react-modal'; import classNames from 'classnames'; -import { Typography } from '@ohif/ui'; +import { Typography, useModal } from '@ohif/ui'; const customStyle = { overlay: { @@ -27,6 +27,12 @@ const Modal = ({ onClose, children, }) => { + const { hide } = useModal(); + + const handleClose = () => { + hide(); + }; + const renderHeader = () => { return ( title && ( @@ -52,6 +58,7 @@ const Modal = ({ `relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded ${className}` )} shouldCloseOnEsc={shouldCloseOnEsc} + onRequestClose={handleClose} isOpen={isOpen} title={title} style={customStyle} diff --git a/platform/ui/src/contextProviders/ModalComponent.jsx b/platform/ui/src/contextProviders/ModalComponent.jsx index b3b6602a7..cf7965812 100644 --- a/platform/ui/src/contextProviders/ModalComponent.jsx +++ b/platform/ui/src/contextProviders/ModalComponent.jsx @@ -16,7 +16,7 @@ const ModalComponent = ({ ModalComponent.defaultProps = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, diff --git a/platform/ui/src/contextProviders/ModalProvider.jsx b/platform/ui/src/contextProviders/ModalProvider.jsx index e8d65635d..ff9224bca 100644 --- a/platform/ui/src/contextProviders/ModalProvider.jsx +++ b/platform/ui/src/contextProviders/ModalProvider.jsx @@ -19,7 +19,7 @@ export const useModal = () => useContext(ModalContext); * @typedef {Object} ModalProps * @property {ReactElement|HTMLElement} [content=null] Modal content. * @property {Object} [contentProps=null] Modal content props. - * @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key. + * @property {boolean} [shouldCloseOnEsc=true] Modal is dismissible via the esc key. * @property {boolean} [isOpen=true] Make the Modal visible or hidden. * @property {boolean} [closeButton=true] Should the modal body render the close button. * @property {string} [title=null] Should the modal render the title independently of the body content. @@ -30,7 +30,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => { const DEFAULT_OPTIONS = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, From 92a2071a041c295aca92d12b4808aa40af97adb6 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Mon, 22 Jun 2020 22:38:52 -0300 Subject: [PATCH 04/32] open modal for Learn More (search info) --- .../components/StudyListFilter/StudyListFilter.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx index 2d409a555..7c7a777b1 100644 --- a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx +++ b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Icon, Typography, InputGroup } from '@ohif/ui'; +import { Button, Icon, Typography, InputGroup, useModal } from '@ohif/ui'; const StudyListFilter = ({ filtersMeta, @@ -20,6 +20,16 @@ const StudyListFilter = ({ }); }; const isSortingEnable = numOfStudies > 0 && numOfStudies <= 100; + const { show } = useModal(); + + const showLearnMoreContent = () => { + const modalContent = () =>
Search Instructions
; + + show({ + content: modalContent, + title: 'Learn More', + }); + }; return ( @@ -40,7 +50,7 @@ const StudyListFilter = ({ startIcon={} > - Learn more + Learn more From 487bcf043b7a68589fa4e78bc574638b2e081170 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 00:09:31 -0300 Subject: [PATCH 05/32] feat: Dropdown component --- platform/ui/index.js | 1 + .../ui/src/components/Dropdown/Dropdown.jsx | 107 ++++++++++++++++++ .../ui/src/components/Dropdown/Dropdown.mdx | 22 ++++ platform/ui/src/components/Dropdown/index.js | 1 + platform/ui/src/components/index.js | 2 + 5 files changed, 133 insertions(+) create mode 100644 platform/ui/src/components/Dropdown/Dropdown.jsx create mode 100644 platform/ui/src/components/Dropdown/Dropdown.mdx create mode 100644 platform/ui/src/components/Dropdown/index.js diff --git a/platform/ui/index.js b/platform/ui/index.js index 4e819e60a..0d7fe423e 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -31,6 +31,7 @@ export { ButtonGroup, DateRange, Dialog, + Dropdown, EmptyStudies, Icon, IconButton, diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx new file mode 100644 index 000000000..752e2b6f9 --- /dev/null +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { Icon, Typography } from '@ohif/ui'; + +const Dropdown = ({ titleElement, title, list }) => { + const [open, setOpen] = useState(false); + const element = useRef(null); + + const renderTitleElement = () => { + if (titleElement) { + return titleElement; + } + + return ( +
+ {title} + +
+ ); + }; + + const toggleList = () => { + setOpen(s => !s); + }; + + const handleClick = e => { + if (element.current && !element.current.contains(e.target)) { + setOpen(false); + } + }; + + const renderList = () => { + const itemsAmount = list.length; + + return ( +
+ {list.map((item, idx) => ( +
{ + setOpen(false); + + if (item.onClick) { + item.onClick(); + } + }} + > + {!!item.icon && ( + + )} + {item.title} +
+ ))} +
+ ); + }; + + useEffect(() => { + document.addEventListener('click', handleClick); + + return () => { + document.removeEventListener('click', handleClick); + }; + }, []); + + return ( +
+
+ {renderTitleElement()} +
+ + {renderList()} +
+ ); +}; + +Dropdown.propTypes = { + titleElement: PropTypes.node, + title: PropTypes.string.isRequired, + /** Items to render in the select's drop down */ + list: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + icon: PropTypes.object, + onClick: PropTypes.func, + link: PropTypes.string, + }) + ), +}; + +export default Dropdown; diff --git a/platform/ui/src/components/Dropdown/Dropdown.mdx b/platform/ui/src/components/Dropdown/Dropdown.mdx new file mode 100644 index 000000000..087877f56 --- /dev/null +++ b/platform/ui/src/components/Dropdown/Dropdown.mdx @@ -0,0 +1,22 @@ +--- +name: Dropdown +menu: General +route: components/dropdown +--- + +import { Playground, Props } from 'docz'; +import { Dropdown } from '@ohif/ui'; + +# Dropdown + +... + +## Import + +```javascript +import { Dropdown } from '@ohif/ui'; +``` + +## Properties + + diff --git a/platform/ui/src/components/Dropdown/index.js b/platform/ui/src/components/Dropdown/index.js new file mode 100644 index 000000000..453771730 --- /dev/null +++ b/platform/ui/src/components/Dropdown/index.js @@ -0,0 +1 @@ +export { default } from './Dropdown'; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index d67bbb9ad..be5d75e1e 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -2,6 +2,7 @@ import Button from './Button'; import ButtonGroup from './ButtonGroup'; import DateRange from './DateRange'; import Dialog from './Dialog'; +import Dropdown from './Dropdown'; import EmptyStudies from './EmptyStudies'; import Icon from './Icon'; import IconButton from './IconButton'; @@ -53,6 +54,7 @@ export { ButtonGroup, DateRange, Dialog, + Dropdown, EmptyStudies, Icon, IconButton, From aecd3d907156b1b9e0cf8ac8aeefbce62858eecc Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 00:09:49 -0300 Subject: [PATCH 06/32] change modal default alignment --- platform/ui/src/components/Modal/Modal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/Modal/Modal.jsx b/platform/ui/src/components/Modal/Modal.jsx index 8e3418829..1ce7e3cd7 100644 --- a/platform/ui/src/components/Modal/Modal.jsx +++ b/platform/ui/src/components/Modal/Modal.jsx @@ -10,7 +10,7 @@ const customStyle = { zIndex: 1071, backgroundColor: 'rgb(0, 0, 0, 0.8)', display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', justifyContent: 'center', padding: '40px 0', }, From 70cf31b44900cf8916fffc96d45b961465ba337e Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 00:10:44 -0300 Subject: [PATCH 07/32] fix navbar height --- platform/ui/src/components/NavBar/NavBar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/NavBar/NavBar.jsx b/platform/ui/src/components/NavBar/NavBar.jsx index 5ab60f626..8adc0ce3b 100644 --- a/platform/ui/src/components/NavBar/NavBar.jsx +++ b/platform/ui/src/components/NavBar/NavBar.jsx @@ -8,7 +8,7 @@ const NavBar = ({ className, children, isSticky }) => { return (
Date: Tue, 23 Jun 2020 00:10:58 -0300 Subject: [PATCH 08/32] using dropdown and opening modal for preferences --- .../default/src/ViewerLayout/Header.jsx | 53 +++++++++++++++---- .../PreferencesDropdown.jsx | 51 ++++++++++++++++++ .../components/PreferencesDropdown/index.js | 1 + .../viewer/src/routes/WorkList/WorkList.jsx | 15 ++---- 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx create mode 100644 platform/viewer/src/components/PreferencesDropdown/index.js diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index b5f8010c0..3fefdbd60 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -3,10 +3,28 @@ import PropTypes from 'prop-types'; // TODO: This may fail if package is split from PWA build import { useHistory } from 'react-router-dom'; // -import { NavBar, Svg, Icon, IconButton } from '@ohif/ui'; +import { NavBar, Svg, Icon, IconButton, Dropdown, useModal } from '@ohif/ui'; function Header({ children }) { const history = useHistory(); + const { show } = useModal(); + + const showAboutModal = () => { + const modalComponent = () =>
About modal
; + show({ + title: 'About', + content: modalComponent, + }); + }; + + const showPreferencesModal = () => { + const modalComponent = () =>
Preferences modal
; + show({ + title: 'Preferences', + content: modalComponent, + }); + }; + // const dropdownContent = [ // { // name: 'Soft tissue', @@ -42,16 +60,29 @@ function Header({ children }) { FOR INVESTIGATIONAL USE ONLY - {}} - > - - - - + {}} + > + + + + + } + list={[ + { title: 'About', icon: 'info', onClick: () => showAboutModal() }, + { + title: 'Preferences', + icon: 'settings', + onClick: () => showPreferencesModal(), + }, + ]} + />
diff --git a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx new file mode 100644 index 000000000..1167aa04f --- /dev/null +++ b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import { Dropdown, IconButton, Icon, useModal } from '@ohif/ui'; + +const PreferencesDropdown = () => { + const { show } = useModal(); + + const showAboutModal = () => { + const modalComponent = () =>
About modal
; + show({ + content: modalComponent, + title: 'About', + }); + }; + + const showPreferencesModal = () => { + const modalComponent = () =>
Preferences modal
; + show({ + content: modalComponent, + title: 'Preferences', + }); + }; + + return ( + {}} + > + + + + + } + list={[ + { title: 'About', icon: 'info', onClick: () => showAboutModal() }, + { + title: 'Preferences', + icon: 'settings', + onClick: () => showPreferencesModal(), + }, + ]} + /> + ); +}; + +export default PreferencesDropdown; diff --git a/platform/viewer/src/components/PreferencesDropdown/index.js b/platform/viewer/src/components/PreferencesDropdown/index.js new file mode 100644 index 000000000..edf8641a2 --- /dev/null +++ b/platform/viewer/src/components/PreferencesDropdown/index.js @@ -0,0 +1 @@ +export { default } from './PreferencesDropdown'; diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx index 264bc87e2..d32ef7a73 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.jsx +++ b/platform/viewer/src/routes/WorkList/WorkList.jsx @@ -9,13 +9,14 @@ import filtersMeta from './filtersMeta.js'; import { useAppConfig } from '@state'; import { useDebounce, useQuery } from '@hooks'; +import PreferencesDropdown from '../../components/PreferencesDropdown'; + import { Icon, StudyListExpandedRow, Button, NavBar, Svg, - IconButton, EmptyStudies, StudyListTable, StudyListPagination, @@ -364,17 +365,7 @@ function WorkList({ history, data: studies, dataSource }) { FOR INVESTIGATIONAL USE ONLY - {}} - > - - - - - +
Date: Tue, 23 Jun 2020 00:14:55 -0300 Subject: [PATCH 09/32] change modal default alignment --- platform/ui/src/components/Modal/Modal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/Modal/Modal.jsx b/platform/ui/src/components/Modal/Modal.jsx index 8e3418829..1ce7e3cd7 100644 --- a/platform/ui/src/components/Modal/Modal.jsx +++ b/platform/ui/src/components/Modal/Modal.jsx @@ -10,7 +10,7 @@ const customStyle = { zIndex: 1071, backgroundColor: 'rgb(0, 0, 0, 0.8)', display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', justifyContent: 'center', padding: '40px 0', }, From ef0eb53d25679b8ce5cab0289a5455d551aa9a95 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 23:05:37 -0300 Subject: [PATCH 10/32] add i18n provider in the viewer --- platform/viewer/src/App.jsx | 51 ++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 969977107..420695713 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -1,12 +1,13 @@ // External import React from 'react'; import PropTypes from 'prop-types'; +import i18n from '@ohif/i18n'; +import { I18nextProvider } from 'react-i18next'; import { BrowserRouter, HashRouter } from 'react-router-dom'; import { DialogProvider, Modal, ModalProvider, - Notification, SnackbarProvider, ThemeWrapper, ViewportDialogProvider, @@ -97,29 +98,31 @@ function App({ config, defaultExtensions }) { return ( - - - - - - - - {appRoutes} - - - - - - - + + + + + + + + + {appRoutes} + + + + + + + + ); } From 8b2c68d1bfe637684a550b3001669d03f7217d6c Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 23:05:45 -0300 Subject: [PATCH 11/32] add i18n to header --- extensions/default/package.json | 2 ++ .../default/src/ViewerLayout/Header.jsx | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/extensions/default/package.json b/extensions/default/package.json index e9633cdc1..f47b06d1d 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -28,6 +28,8 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", + "@ohif/i18n": "^0.52.8", + "react-i18next": "^10.11.0", "prop-types": "^15.6.2", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index 3fefdbd60..1593ba893 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -1,26 +1,32 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; // TODO: This may fail if package is split from PWA build import { useHistory } from 'react-router-dom'; // import { NavBar, Svg, Icon, IconButton, Dropdown, useModal } from '@ohif/ui'; function Header({ children }) { + const { t } = useTranslation(); const history = useHistory(); const { show } = useModal(); const showAboutModal = () => { - const modalComponent = () =>
About modal
; + const modalComponent = () => ( +
{t('AboutModal:OHIF Viewer - About')}
+ ); show({ - title: 'About', + title: t('AboutModal:OHIF Viewer - About'), content: modalComponent, }); }; const showPreferencesModal = () => { - const modalComponent = () =>
Preferences modal
; + const modalComponent = () => ( +
{t('UserPreferencesModal:User Preferences')}
+ ); show({ - title: 'Preferences', + title: t('UserPreferencesModal:User Preferences'), content: modalComponent, }); }; @@ -58,7 +64,7 @@ function Header({ children }) {
{children}
- FOR INVESTIGATIONAL USE ONLY + {t('Header:INVESTIGATIONAL USE ONLY')} } list={[ - { title: 'About', icon: 'info', onClick: () => showAboutModal() }, { - title: 'Preferences', + title: t('Header:About'), + icon: 'info', + onClick: () => showAboutModal(), + }, + { + title: t('Header:Preferences'), icon: 'settings', onClick: () => showPreferencesModal(), }, From 9dbbc709f42e07fb4121874f09e7e356a289fc27 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 23:22:57 -0300 Subject: [PATCH 12/32] remove fragment --- extensions/default/src/ViewerLayout/Header.jsx | 5 ++--- .../components/PreferencesDropdown/PreferencesDropdown.jsx | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index 1593ba893..a23d8e986 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -75,9 +75,8 @@ function Header({ children }) { className="text-primary-active" onClick={() => {}} > - - - + + } list={[ diff --git a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx index 1167aa04f..77813684c 100644 --- a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx +++ b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx @@ -31,9 +31,8 @@ const PreferencesDropdown = () => { className="text-primary-active" onClick={() => {}} > - - - + + } list={[ From 9745f06c4c54ddc357c1d53c84f6a50db7de3f76 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 23:40:35 -0300 Subject: [PATCH 13/32] fix eventListener --- platform/ui/src/components/Dropdown/Dropdown.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx index 752e2b6f9..8d298dc86 100644 --- a/platform/ui/src/components/Dropdown/Dropdown.jsx +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -10,7 +10,7 @@ const Dropdown = ({ titleElement, title, list }) => { const renderTitleElement = () => { if (titleElement) { - return titleElement; + return <>{titleElement}; } return ( @@ -74,10 +74,10 @@ const Dropdown = ({ titleElement, title, list }) => { useEffect(() => { document.addEventListener('click', handleClick); - return () => { + if (!open) { document.removeEventListener('click', handleClick); - }; - }, []); + } + }, [open]); return (
@@ -92,12 +92,12 @@ const Dropdown = ({ titleElement, title, list }) => { Dropdown.propTypes = { titleElement: PropTypes.node, - title: PropTypes.string.isRequired, + title: PropTypes.string, /** Items to render in the select's drop down */ list: PropTypes.arrayOf( PropTypes.shape({ title: PropTypes.string.isRequired, - icon: PropTypes.object, + icon: PropTypes.string, onClick: PropTypes.func, link: PropTypes.string, }) From 6ce531a6d29f56c430fbeda153eba8a523cb7973 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 23 Jun 2020 23:40:45 -0300 Subject: [PATCH 14/32] fix icon dropdown --- .../default/src/ViewerLayout/Header.jsx | 28 +++++++++++------- .../PreferencesDropdown.jsx | 29 ++++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index a23d8e986..09b802c50 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -68,16 +68,24 @@ function Header({ children }) { {}} - > - - - + <> + + + + + + + } list={[ { diff --git a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx index 77813684c..fe591bf35 100644 --- a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx +++ b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx @@ -24,16 +24,25 @@ const PreferencesDropdown = () => { return ( {}} - > - - - + <> + + + + {}} + > + + + } list={[ { title: 'About', icon: 'info', onClick: () => showAboutModal() }, From 987dae9b2dae55b23febbec7928cb826c70a5721 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Wed, 24 Jun 2020 01:16:39 -0300 Subject: [PATCH 15/32] fix dropdown --- .../ui/src/components/Dropdown/Dropdown.jsx | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx index 8d298dc86..f13852aad 100644 --- a/platform/ui/src/components/Dropdown/Dropdown.jsx +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -10,7 +10,7 @@ const Dropdown = ({ titleElement, title, list }) => { const renderTitleElement = () => { if (titleElement) { - return <>{titleElement}; + return titleElement; } return ( @@ -44,29 +44,35 @@ const Dropdown = ({ titleElement, title, list }) => { } )} > - {list.map((item, idx) => ( -
{ - setOpen(false); - - if (item.onClick) { - item.onClick(); - } - }} - > - {!!item.icon && ( - - )} - {item.title} -
- ))} + {list.map( + ( + { + title: itemTitle, + icon: itemIcon, + onClick: itemOnClick = () => {}, + }, + idx + ) => ( +
{ + setOpen(false); + itemOnClick(); + }} + > + {!!itemIcon && ( + + )} + {itemTitle} +
+ ) + )}
); }; From db0ce2f6f7780511b4b3b9b2de8662ab1a0b05a0 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Wed, 24 Jun 2020 01:16:56 -0300 Subject: [PATCH 16/32] minor fixes --- extensions/default/src/ViewerLayout/Header.jsx | 4 ++-- .../components/PreferencesDropdown/PreferencesDropdown.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index 09b802c50..b795f2fcb 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -91,12 +91,12 @@ function Header({ children }) { { title: t('Header:About'), icon: 'info', - onClick: () => showAboutModal(), + onClick: showAboutModal, }, { title: t('Header:Preferences'), icon: 'settings', - onClick: () => showPreferencesModal(), + onClick: showPreferencesModal, }, ]} /> diff --git a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx index fe591bf35..3df73650a 100644 --- a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx +++ b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx @@ -45,11 +45,11 @@ const PreferencesDropdown = () => { } list={[ - { title: 'About', icon: 'info', onClick: () => showAboutModal() }, + { title: 'About', icon: 'info', onClick: showAboutModal }, { title: 'Preferences', icon: 'settings', - onClick: () => showPreferencesModal(), + onClick: showPreferencesModal, }, ]} /> From a19c1acd7ba8d7e6ce92f8827f10b41b8b5f9416 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Wed, 24 Jun 2020 01:27:02 -0300 Subject: [PATCH 17/32] fix dropdown title --- .../default/src/ViewerLayout/Header.jsx | 40 +++++++++--------- .../ui/src/components/Dropdown/Dropdown.jsx | 22 +++++----- .../PreferencesDropdown.jsx | 42 +++++++++---------- 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index b795f2fcb..132fd7916 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -67,26 +67,7 @@ function Header({ children }) { {t('Header:INVESTIGATIONAL USE ONLY')} - - - - - - - - } + showDropdownIcon={false} list={[ { title: t('Header:About'), @@ -99,7 +80,24 @@ function Header({ children }) { onClick: showPreferencesModal, }, ]} - /> + > + + + + + + +
diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx index f13852aad..149c55ece 100644 --- a/platform/ui/src/components/Dropdown/Dropdown.jsx +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -4,19 +4,17 @@ import classnames from 'classnames'; import { Icon, Typography } from '@ohif/ui'; -const Dropdown = ({ titleElement, title, list }) => { +const Dropdown = ({ children, showDropdownIcon, list }) => { const [open, setOpen] = useState(false); const element = useRef(null); const renderTitleElement = () => { - if (titleElement) { - return titleElement; - } - return ( -
- {title} - +
+ {children} + {showDropdownIcon && ( + + )}
); }; @@ -96,9 +94,13 @@ const Dropdown = ({ titleElement, title, list }) => { ); }; +Dropdown.defaultProps = { + showDropdownIcon: true, +}; + Dropdown.propTypes = { - titleElement: PropTypes.node, - title: PropTypes.string, + children: PropTypes.node.isRequired, + showDropdownIcon: PropTypes.bool, /** Items to render in the select's drop down */ list: PropTypes.arrayOf( PropTypes.shape({ diff --git a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx index 3df73650a..791905819 100644 --- a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx +++ b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx @@ -23,27 +23,7 @@ const PreferencesDropdown = () => { return ( - - - - {}} - > - - - - } + showDropdownIcon={false} list={[ { title: 'About', icon: 'info', onClick: showAboutModal }, { @@ -52,7 +32,25 @@ const PreferencesDropdown = () => { onClick: showPreferencesModal, }, ]} - /> + > + + + + {}} + > + + + ); }; From e70c7a455a9f2ad6138b56bbfaaf3c638e356fe4 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Thu, 25 Jun 2020 13:46:24 -0300 Subject: [PATCH 18/32] split dropdown item into a separated render component --- .../ui/src/components/Dropdown/Dropdown.jsx | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx index 149c55ece..b25240458 100644 --- a/platform/ui/src/components/Dropdown/Dropdown.jsx +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -8,6 +8,42 @@ const Dropdown = ({ children, showDropdownIcon, list }) => { const [open, setOpen] = useState(false); const element = useRef(null); + const DropdownItem = ({ title, icon, onClick, index }) => { + const itemsAmount = list.length; + const isLastItem = itemsAmount === index + 1; + + return ( +
{ + setOpen(false); + onClick(); + }} + > + {!!icon && } + {title} +
+ ); + }; + + DropdownItem.defaultProps = { + icon: '', + onClick: () => {}, + }; + + DropdownItem.propTypes = { + title: PropTypes.string.isRequired, + icon: PropTypes.string, + onClick: PropTypes.func, + index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + }; + const renderTitleElement = () => { return (
@@ -30,8 +66,6 @@ const Dropdown = ({ children, showDropdownIcon, list }) => { }; const renderList = () => { - const itemsAmount = list.length; - return (
{ } )} > - {list.map( - ( - { - title: itemTitle, - icon: itemIcon, - onClick: itemOnClick = () => {}, - }, - idx - ) => ( -
{ - setOpen(false); - itemOnClick(); - }} - > - {!!itemIcon && ( - - )} - {itemTitle} -
- ) - )} + {list.map((item, idx) => ( + + ))}
); }; @@ -107,7 +121,6 @@ Dropdown.propTypes = { title: PropTypes.string.isRequired, icon: PropTypes.string, onClick: PropTypes.func, - link: PropTypes.string, }) ), }; From 547e11d877b3bc9e205380647cfd1fdb1e897dd7 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Thu, 25 Jun 2020 13:52:40 -0300 Subject: [PATCH 19/32] minor fix class order --- platform/ui/src/components/Dropdown/Dropdown.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx index b25240458..ef4ae250a 100644 --- a/platform/ui/src/components/Dropdown/Dropdown.jsx +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -69,7 +69,7 @@ const Dropdown = ({ children, showDropdownIcon, list }) => { return (
Date: Thu, 25 Jun 2020 14:08:09 -0300 Subject: [PATCH 20/32] useCallback to avoid redeclaring functions --- .../default/src/ViewerLayout/Header.jsx | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx index 132fd7916..6000e5c22 100644 --- a/extensions/default/src/ViewerLayout/Header.jsx +++ b/extensions/default/src/ViewerLayout/Header.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; // TODO: This may fail if package is split from PWA build @@ -11,7 +11,8 @@ function Header({ children }) { const history = useHistory(); const { show } = useModal(); - const showAboutModal = () => { + // TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED + const showAboutModal = useCallback(() => { const modalComponent = () => (
{t('AboutModal:OHIF Viewer - About')}
); @@ -19,9 +20,10 @@ function Header({ children }) { title: t('AboutModal:OHIF Viewer - About'), content: modalComponent, }); - }; + }, [show, t]); - const showPreferencesModal = () => { + // TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED + const showPreferencesModal = useCallback(() => { const modalComponent = () => (
{t('UserPreferencesModal:User Preferences')}
); @@ -29,18 +31,7 @@ function Header({ children }) { title: t('UserPreferencesModal:User Preferences'), content: modalComponent, }); - }; - - // const dropdownContent = [ - // { - // name: 'Soft tissue', - // value: '400/40', - // }, - // { name: 'Lung', value: '1500 / -600' }, - // { name: 'Liver', value: '150 / 90' }, - // { name: 'Bone', value: '2500 / 480' }, - // { name: 'Brain', value: '80 / 40' }, - // ] + }, [show, t]); return ( From b0935e4fb673e8fb79327b646df89b75d0435aaa Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Thu, 25 Jun 2020 15:42:36 -0300 Subject: [PATCH 21/32] fix icon size --- platform/ui/src/assets/icons/link.svg | 2 -- platform/ui/src/assets/icons/unlink.svg | 2 -- 2 files changed, 4 deletions(-) diff --git a/platform/ui/src/assets/icons/link.svg b/platform/ui/src/assets/icons/link.svg index 7c99fc27c..1f7f94009 100644 --- a/platform/ui/src/assets/icons/link.svg +++ b/platform/ui/src/assets/icons/link.svg @@ -2,8 +2,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-labelledby="title" - width="1em" - height="1em" fill="currentColor" > diff --git a/platform/ui/src/assets/icons/unlink.svg b/platform/ui/src/assets/icons/unlink.svg index 37c53bbb9..ed9526b7f 100644 --- a/platform/ui/src/assets/icons/unlink.svg +++ b/platform/ui/src/assets/icons/unlink.svg @@ -2,8 +2,6 @@ xmlns="http://www.w3.org/2000/svg" aria-labelledby="unlink" viewBox="0 0 512 512" - width="1em" - height="1em" fill="currentColor" > Unlink From eeeaa8d1dabe865e7bb2f27c72f969551958fb48 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Thu, 25 Jun 2020 18:45:28 -0300 Subject: [PATCH 22/32] fix tailwind class --- .../ui/src/components/Dropdown/Dropdown.jsx | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/platform/ui/src/components/Dropdown/Dropdown.jsx b/platform/ui/src/components/Dropdown/Dropdown.jsx index ef4ae250a..b0b58497f 100644 --- a/platform/ui/src/components/Dropdown/Dropdown.jsx +++ b/platform/ui/src/components/Dropdown/Dropdown.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useCallback, useState, useRef } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -8,18 +8,12 @@ const Dropdown = ({ children, showDropdownIcon, list }) => { const [open, setOpen] = useState(false); const element = useRef(null); - const DropdownItem = ({ title, icon, onClick, index }) => { - const itemsAmount = list.length; - const isLastItem = itemsAmount === index + 1; - + const DropdownItem = useCallback(({ title, icon, onClick }) => { return (
{ setOpen(false); @@ -30,18 +24,16 @@ const Dropdown = ({ children, showDropdownIcon, list }) => { {title}
); - }; + }, []); DropdownItem.defaultProps = { icon: '', - onClick: () => {}, }; DropdownItem.propTypes = { title: PropTypes.string.isRequired, icon: PropTypes.string, - onClick: PropTypes.func, - index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + onClick: PropTypes.func.isRequired, }; const renderTitleElement = () => { @@ -82,7 +74,6 @@ const Dropdown = ({ children, showDropdownIcon, list }) => { icon={item.icon} onClick={item.onClick} key={idx} - index={idx} /> ))}
@@ -120,9 +111,9 @@ Dropdown.propTypes = { PropTypes.shape({ title: PropTypes.string.isRequired, icon: PropTypes.string, - onClick: PropTypes.func, + onClick: PropTypes.func.isRequired, }) - ), + ).isRequired, }; export default Dropdown; From 81a764a78f2cc7f3d68c57e82907bf8d8530f32d Mon Sep 17 00:00:00 2001 From: James Petts Date: Fri, 26 Jun 2020 10:01:13 +0100 Subject: [PATCH 23/32] [OHIF-177] - JSDocs (#1818) * OHIF-177 JSDocs. * Add proxy for activeDataSource to extensionManager. --- platform/core/src/DICOMSR/dataExchange.js | 16 ++++++++++++++++ platform/core/src/extensions/ExtensionManager.js | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index dac463a12..2b3cfe172 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -82,6 +82,11 @@ const storeMeasurementsOld = async (measurementData, filter, server) => { } }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + */ const downloadReport = measurementData => { const srDataset = generateReport(measurementData); const reportBlob = dcmjs.data.datasetToBlob(srDataset); @@ -91,6 +96,11 @@ const downloadReport = measurementData => { window.location.assign(objectUrl); }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + */ const generateReport = measurementData => { const ids = measurementData.map(md => md.id); const filteredToolState = _getFilteredCornerstoneToolState(ids); @@ -103,6 +113,12 @@ const generateReport = measurementData => { return report.dataset; }; +/** + * + * @param {object[]} measurementData An array of measurements from the measurements service + * that you wish to serialize. + * @param {object} dataSource The dataSource that you wish to use to persist the data. + */ const storeMeasurements = async (measurementData, dataSource) => { // TODO -> Eventually use the measurements directly and not the dcmjs adapter, // But it is good enough for now whilst we only have cornerstone as a datasource. diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 14dfde227..0ce25d6b6 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -137,6 +137,7 @@ export default class ExtensionManager { getDataSources = dataSourceName => { if (dataSourceName === undefined) { + // Default to the activeDataSource dataSourceName = this.activeDataSource; } @@ -144,6 +145,10 @@ export default class ExtensionManager { return this.dataSourceMap[dataSourceName]; }; + getActiveDataSource = () => { + return this.activeDataSource; + }; + /** * @private * @param {string} moduleType From 8f05cf73c76bf7c7d0ef8c52cf1f85955aa1d960 Mon Sep 17 00:00:00 2001 From: James Petts Date: Fri, 26 Jun 2020 10:24:19 +0100 Subject: [PATCH 24/32] Add authoring information to exported DICOMs using the DICOMWeb DataSource (#1819) * OHIF-177 JSDocs. * Add proxy for activeDataSource to extensionManager. * Add app authoring information. --- extensions/default/src/DicomWebDataSource/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 33bce395d..93a7b1b2f 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -17,8 +17,10 @@ const { DicomMetaDictionary, DicomDict } = dcmjs.data; const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; const { urlUtil } = utils; -const VERSION_NAME = 'OHIF-0.1'; -const SR_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1'; +const ImplementationClassUID = + '2.25.270695996825855179949881587723571202391.2.0.0'; +const ImplementationVersionName = 'OHIF-VIEWER-2.0.0'; +const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; /** * @@ -140,9 +142,9 @@ function createDicomWebApi(dicomWebConfig) { dataset._meta.FileMetaInformationVersion.Value, MediaStorageSOPClassUID: dataset.SOPClassUID, MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, - TransferSyntaxUID: SR_TRANSFER_SYNTAX_UID, - ImplementationClassUID: DicomMetaDictionary.uid(), - ImplementationVersionName: VERSION_NAME, + TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + ImplementationClassUID, + ImplementationVersionName, }; const denaturalized = denaturalizeDataset(meta); From 1d8f5cfc4280c8f34672a1e823485ba60992ac89 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 10:40:03 -0300 Subject: [PATCH 25/32] Viewport grid load default as 1x1 and load first image of series by default (#1817) --- .../default/src/Panels/PanelStudyBrowser.jsx | 11 +++---- .../src/Toolbar/ToolbarLayoutSelector.jsx | 32 ++++++++++++++----- .../PanelStudyBrowserTracking.jsx | 9 +++--- .../viewports/TrackedCornerstoneViewport.js | 4 --- .../viewer/src/components/ViewportGrid.jsx | 27 +++++++++++++++- 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index e2647aca6..221395926 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -127,7 +127,6 @@ function PanelStudyBrowser({ changedDisplaySets, thumbnailImageSrcMap ); - setDisplaySets(mappedDisplaySets); } ); @@ -152,11 +151,11 @@ function PanelStudyBrowser({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + [ + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx index 36236b93b..b5a4253ab 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx @@ -5,22 +5,40 @@ import { useViewportGrid, } from '@ohif/ui'; +const DEFAULT_LAYOUT = { + type: 'SET_LAYOUT', + payload: { + numCols: 1, + numRows: 1, + }, +}; + function LayoutSelector() { const [isOpen, setIsOpen] = useState(false); const [viewportGridState, dispatch] = useViewportGrid(); - useEffect(() => { - function closeOnOutsideClick() { - if (isOpen) { - setIsOpen(false); - } + const closeOnOutsideClick = () => { + if (isOpen) { + setIsOpen(false); } + }; + + useEffect(() => { window.addEventListener('click', closeOnOutsideClick); return () => { window.removeEventListener('click', closeOnOutsideClick); }; }, [isOpen]); + useEffect(() => { + /* Reset to default layout when component unmounts */ + return () => { + dispatch(DEFAULT_LAYOUT); + }; + }, []); + + const onClickHandler = () => setIsOpen(!isOpen); + const DropdownContent = isOpen ? OHIFLayoutSelector : null; return ( @@ -28,9 +46,7 @@ function LayoutSelector() { id="Layout" label="Grid Layout" icon="tool-layout" - onClick={() => { - setIsOpen(!isOpen); - }} + onClick={onClickHandler} dropdownContent={ DropdownContent !== null && ( stdyUid !== StudyInstanceUID - ), - ] + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -285,7 +285,6 @@ function _mapDisplaySets( ) { const thumbnailDisplaySets = []; const thumbnailNoImageDisplaySets = []; - displaySets.forEach(ds => { const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID]; const componentType = _getComponentType(ds.Modality); diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index c582bd929..dff5d940f 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -10,8 +10,6 @@ import { useViewportGrid, useViewportDialog, } from '@ohif/ui'; -import debounce from 'lodash.debounce'; -import throttle from 'lodash.throttle'; import { useTrackedMeasurements } from './../getContextModule'; // TODO -> Get this list from the list of tracked measurements. @@ -43,7 +41,6 @@ function TrackedCornerstoneViewport({ const [ { activeViewportIndex, viewports }, - dispatchViewportGrid, ] = useViewportGrid(); // viewportIndex, onSubmit const [viewportDialogState, viewportDialogApi] = useViewportDialog(); @@ -217,7 +214,6 @@ function TrackedCornerstoneViewport({ SliceThickness, } = displaySet.images[0]; - if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { setIsTracked(!isTracked); } diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index fa7a6c7e4..2f99f7b48 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -1,10 +1,12 @@ /** * CSS Grid Reference: http://grid.malven.co/ */ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import EmptyViewport from './EmptyViewport'; +import { classes } from '@ohif/core'; +const { ImageSet } = classes; function ViewerViewportGrid(props) { const { servicesManager, viewportComponents, dataSource } = props; @@ -20,6 +22,29 @@ function ViewerViewportGrid(props) { // TODO -> Need some way of selecting which displaySets hit the viewports. const { DisplaySetService } = servicesManager.services; + useEffect(() => { + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED, + displaySets => { + displaySets.sort((a, b) => { + const isImageSet = x => x instanceof ImageSet; + return (isImageSet(a) === isImageSet(b)) ? 0 : isImageSet(a) ? -1 : 1; + }); + dispatch({ + type: 'SET_DISPLAYSET_FOR_VIEWPORT', + payload: { + viewportIndex: 0, + displaySetInstanceUID: displaySets[0].displaySetInstanceUID, + }, + }); + }, + ); + + return () => { + unsubscribe(); + }; + }, []); + // TODO -> Make a HangingProtocolService const HangingProtocolService = displaySets => { let displaySetInstanceUID; From 101efa4cf4309afb0883d07e30de7b0011defc2b Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 11:08:16 -0300 Subject: [PATCH 26/32] Add simple loading and default state to datasourcewrapper (#1815) --- .../components/EmptyStudies/EmptyStudies.js | 5 ++- .../viewer/src/routes/DataSourceWrapper.jsx | 23 +++++------ .../viewer/src/routes/WorkList/WorkList.jsx | 40 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/platform/ui/src/components/EmptyStudies/EmptyStudies.js b/platform/ui/src/components/EmptyStudies/EmptyStudies.js index 48e3baaa6..893ecf3e3 100644 --- a/platform/ui/src/components/EmptyStudies/EmptyStudies.js +++ b/platform/ui/src/components/EmptyStudies/EmptyStudies.js @@ -3,12 +3,13 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Icon, Typography } from '@ohif/ui'; -const EmptyStudies = ({ className }) => { +// TODO: Add loading spinner to OHIF + use it here. +const EmptyStudies = ({ className, isLoading }) => { return (
- No studies available + {!isLoading ? 'No studies available' : 'Loading...'}
); diff --git a/platform/viewer/src/routes/DataSourceWrapper.jsx b/platform/viewer/src/routes/DataSourceWrapper.jsx index d0a0711e3..158594128 100644 --- a/platform/viewer/src/routes/DataSourceWrapper.jsx +++ b/platform/viewer/src/routes/DataSourceWrapper.jsx @@ -46,14 +46,17 @@ function DataSourceWrapper(props) { // studies.processResults --> // But only for LayoutTemplate type of 'list'? // Or no data fetching here, and just hand down my source - const [data, setData] = useState(); + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { // 204: no content async function getData() { + setIsLoading(true); const searchResults = await dataSource.query.studies.search( queryFilterValues ); setData(searchResults); + setIsLoading(false); } try { @@ -61,23 +64,19 @@ function DataSourceWrapper(props) { } catch (ex) { console.warn(ex); } - console.log('DataSourceWrapper: useEffect'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [history.location.search]); // queryFilterValues // TODO: Better way to pass DataSource? return ( - - {data && ( - - )} - + ); } diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx index 264bc87e2..e3610cf90 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.jsx +++ b/platform/viewer/src/routes/WorkList/WorkList.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { Suspense, useState, useEffect } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; @@ -28,15 +28,13 @@ const seriesInStudiesMap = new Map(); * TODO: * - debounce `setFilterValues` (150ms?) */ -function WorkList({ history, data: studies, dataSource }) { +function WorkList({ history, data: studies, isLoadingData, dataSource }) { // ~ Modes const [appConfig] = useAppConfig(); // ~ Filters const query = useQuery(); const queryFilterValues = _getQueryFilterValues(query); - const [filterValues, _setFilterValues] = useState( - Object.assign({}, defaultFilterValues, queryFilterValues) - ); + const [filterValues, _setFilterValues] = useState({ ...defaultFilterValues, ...queryFilterValues }); const debouncedFilterValues = useDebounce(filterValues, 200); const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues; @@ -80,6 +78,7 @@ function WorkList({ history, data: studies, dataSource }) { return 0; }); + // ~ Rows & Studies const [expandedRows, setExpandedRows] = useState([]); const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]); @@ -190,6 +189,7 @@ function WorkList({ history, data: studies, dataSource }) { return filterValues[name] !== defaultFilterValues[name]; }); }; + const tableDataSource = sortedStudies.map((study, key) => { const rowKey = key + 1; const isExpanded = expandedRows.some(k => k === rowKey); @@ -211,8 +211,8 @@ function WorkList({ history, data: studies, dataSource }) { content: patientName ? ( patientName ) : ( - (Empty) - ), + (Empty) + ), title: patientName, gridCol: 4, }, @@ -299,13 +299,13 @@ function WorkList({ history, data: studies, dataSource }) { seriesTableDataSource={ seriesInStudiesMap.has(studyInstanceUid) ? seriesInStudiesMap.get(studyInstanceUid).map(s => { - return { - description: s.description || '(empty)', - seriesNumber: s.seriesNumber || '', - modality: s.modality || '', - instances: s.numSeriesInstances || '', - }; - }) + return { + description: s.description || '(empty)', + seriesNumber: s.seriesNumber || '', + modality: s.modality || '', + instances: s.numSeriesInstances || '', + }; + }) : [] } > @@ -322,7 +322,7 @@ function WorkList({ history, data: studies, dataSource }) {
); } From 5180eb127dcefcbd618a28717e9e838fa8b93552 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 11:11:00 -0300 Subject: [PATCH 27/32] OHIF-163: Patient Name is displayed in correct format "Last Name, First Name" (#1820) * Add util to formatpn * Format patient name in patient information tab --- .../default/src/DicomWebDataSource/qido.js | 4 ++-- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 2 +- .../viewports/TrackedCornerstoneViewport.js | 2 +- platform/core/src/utils/formatPN.js | 18 ++++++++++++++++++ platform/core/src/utils/index.js | 2 ++ 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 platform/core/src/utils/formatPN.js diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js index 83613dc0c..00a390424 100644 --- a/extensions/default/src/DicomWebDataSource/qido.js +++ b/extensions/default/src/DicomWebDataSource/qido.js @@ -22,7 +22,7 @@ * | limit | {number} | * | offset | {number} | */ -import { DICOMWeb } from '@ohif/core'; +import { DICOMWeb, utils } from '@ohif/core'; const { getString, getName, getModalities } = DICOMWeb; @@ -50,7 +50,7 @@ function processResults(qidoStudies) { time: getString(qidoStudy['00080030']), // HHmmss.SSS (24-hour, minutes, seconds, fractional seconds) accession: getString(qidoStudy['00080050']) || '', // short string, probably a number? mrn: getString(qidoStudy['00100020']) || '', // medicalRecordNumber - patientName: getName(qidoStudy['00100010']) || '', + patientName: utils.formatPN(getName(qidoStudy['00100010'])) || '', instances: Number(getString(qidoStudy['00201208'])) || 0, // number description: getString(qidoStudy['00081030']) || '', modalities: diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index dfc685e43..22f343112 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -250,7 +250,7 @@ function OHIFCornerstoneSRViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? PatientName.Alphabetic || '' : '', + patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index dff5d940f..91e28bb84 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -233,7 +233,7 @@ function TrackedCornerstoneViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? PatientName.Alphabetic || '' : '', + patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', diff --git a/platform/core/src/utils/formatPN.js b/platform/core/src/utils/formatPN.js new file mode 100644 index 000000000..fb019ab5e --- /dev/null +++ b/platform/core/src/utils/formatPN.js @@ -0,0 +1,18 @@ +/** + * Formats a patient name for display purposes + */ +export default function formatPN(name) { + if (!name) { + return; + } + + // Convert the first ^ to a ', '. String.replace() only affects + // the first appearance of the character. + const commaBetweenFirstAndLast = name.replace('^', ', '); + + // Replace any remaining '^' characters with spaces + const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + + // Trim any extraneous whitespace + return cleaned.trim(); +} diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 6e9ea3140..a69889262 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -15,6 +15,7 @@ import makeCancelable from './makeCancelable'; import hotkeys from './hotkeys'; import Queue from './Queue'; import isDicomUid from './isDicomUid'; +import formatPN from './formatPN'; import resolveObjectPath from './resolveObjectPath'; import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as progressTrackingUtils from './progressTrackingUtils'; @@ -26,6 +27,7 @@ const utils = { addServers, sortBy, writeScript, + formatPN, b64toBlob, StackManager, studyMetadataManager, From 554b9a0db4956647930130521e8a8b5db99dba5b Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Fri, 26 Jun 2020 12:05:29 -0300 Subject: [PATCH 28/32] OHIF-174: Patient Info Icon should be clickable and show the correct details (#1816) * Update patient info to be clickable and update fields in viewers * Add spacing * Remove log * round number * Add default --- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 9 +-- .../viewports/TrackedCornerstoneViewport.js | 8 +- .../ViewportActionBar/ViewportActionBar.jsx | 75 ++++++++++--------- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 22f343112..c54203427 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -203,9 +203,11 @@ function OHIFCornerstoneSRViewport({ PatientSex, PatientAge, SliceThickness, + ManufacturerModelName, StudyDate, SeriesDescription, SeriesInstanceUID, + PixelSpacing, SeriesNumber, } = activeDisplaySetData; @@ -233,13 +235,10 @@ function OHIFCornerstoneSRViewport({ updateViewport(newMeasurementSelected); }; - console.log(currentImageIdIndex); - return ( <> diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 91e28bb84..eae34e279 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -206,12 +206,15 @@ function TrackedCornerstoneViewport({ SeriesInstanceUID, SeriesNumber, } = displaySet; + const { PatientID, PatientName, PatientSex, PatientAge, SliceThickness, + PixelSpacing, + ManufacturerModelName } = displaySet.images[0]; if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { @@ -222,7 +225,6 @@ function TrackedCornerstoneViewport({ <> alert(`Series ${direction}`)} - showPatientInfo={viewportIndex === activeViewportIndex} showNavArrows={viewportIndex === activeViewportIndex} studyData={{ label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid], @@ -238,8 +240,8 @@ function TrackedCornerstoneViewport({ patientAge: PatientAge || '', MRN: PatientID || '', thickness: `${SliceThickness}mm`, - spacing: '', - scanner: '', + spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '', + scanner: ManufacturerModelName || '', }, }} /> diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index 9133b522c..9129d871c 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Icon, ButtonGroup, Button, Tooltip } from '@ohif/ui'; @@ -13,9 +13,11 @@ const classes = { const ViewportActionBar = ({ studyData, showNavArrows, - showPatientInfo, + showPatientInfo: patientInfoVisibility, onSeriesChange, }) => { + const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); + const { label, isTracked, @@ -37,6 +39,8 @@ const ViewportActionBar = ({ scanner, } = patientInformation; + const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo) + const renderIconStatus = () => { if (modality === 'SR') { return ( @@ -60,29 +64,30 @@ const ViewportActionBar = ({ {!isTracked ? ( ) : ( - -
- -
-
- - Series is + +
+ +
+
+ + Series is tracked and can be viewed
in the measurement panel
+
-
- } - > - - - )} + } + > + + + )} ); }; + return (
@@ -131,19 +136,18 @@ const ViewportActionBar = ({
)} - {showPatientInfo && ( -
- -
- )} +
+ +
); }; @@ -174,7 +178,7 @@ ViewportActionBar.propTypes = { ViewportActionBar.defaultProps = { showNavArrows: true, - showPatientInfo: true, + showPatientInfo: false, }; function PatientInfo({ @@ -185,11 +189,14 @@ function PatientInfo({ thickness, spacing, scanner, + isOpen, }) { return (
@@ -236,7 +243,7 @@ function PatientInfo({
- } + )} >
From d220dd24899868531643149eb7160873cd7a7117 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Fri, 26 Jun 2020 12:34:10 -0300 Subject: [PATCH 29/32] create dropdown documentation --- .../ui/src/components/Dropdown/Dropdown.mdx | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/platform/ui/src/components/Dropdown/Dropdown.mdx b/platform/ui/src/components/Dropdown/Dropdown.mdx index 087877f56..dc4b02555 100644 --- a/platform/ui/src/components/Dropdown/Dropdown.mdx +++ b/platform/ui/src/components/Dropdown/Dropdown.mdx @@ -5,11 +5,13 @@ route: components/dropdown --- import { Playground, Props } from 'docz'; -import { Dropdown } from '@ohif/ui'; +import { Dropdown, IconButton, Icon } from '@ohif/ui'; # Dropdown -... +This component is used when there are more than a few options to choose from. By +hovering or clicking on the trigger, a dropdown menu will appear, which allows +you to choose an option and execute the relevant action. ## Import @@ -17,6 +19,50 @@ import { Dropdown } from '@ohif/ui'; import { Dropdown } from '@ohif/ui'; ``` + + {() => { + const handleClick = () => { + alert("Clicked"); + } + return ( +
+ + + + + + + + +
+ ); + }} +
+ ## Properties - + From edd4a4118ceab6c205b41216533634e0907e9b15 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Fri, 26 Jun 2020 12:34:43 -0300 Subject: [PATCH 30/32] feat: Modal + ViewportDownloadForm (#1799) * feat: Modal + ViewportDownloadForm * styles for image preview box * modal update & close handler * open modal for Learn More (search info) * change modal default alignment * Update platform/ui/src/components/InputNumber/InputNumber.mdx * nuke inputNumber and use inputText with refactor * make input more reusable * revert inputText "number" stuff * replace inputText to Input * fix input spacing + label * fix modal overlay classes * fix inline styles * fix onclick learn more * fix close icon * revert transition * remove inline styles * remove unecessary stuff --- .../src/CornerstoneViewportDownloadForm.js | 6 +- .../core/src/services/UIModalService/index.js | 2 +- platform/ui/index.js | 1 + platform/ui/src/assets/icons/close.svg | 7 + platform/ui/src/assets/icons/link.svg | 11 + platform/ui/src/assets/icons/unlink.svg | 11 + platform/ui/src/components/Icon/getIcon.jsx | 6 + .../src/components/IconButton/IconButton.jsx | 6 +- platform/ui/src/components/Input/Input.jsx | 6 +- .../ui/src/components/InputText/InputText.jsx | 14 +- platform/ui/src/components/Modal/Modal.css | 3 + platform/ui/src/components/Modal/Modal.jsx | 54 ++- platform/ui/src/components/Select/Select.jsx | 7 +- .../StudyListFilter/StudyListFilter.jsx | 13 +- .../ViewportDownloadForm.jsx | 432 ++++++++++++++++++ .../components/ViewportDownloadForm/index.js | 1 + platform/ui/src/components/index.js | 2 + .../src/contextProviders/ModalComponent.jsx | 2 +- .../ui/src/contextProviders/ModalProvider.jsx | 4 +- platform/ui/tailwind.config.js | 5 +- 20 files changed, 542 insertions(+), 51 deletions(-) create mode 100644 platform/ui/src/assets/icons/close.svg create mode 100644 platform/ui/src/assets/icons/link.svg create mode 100644 platform/ui/src/assets/icons/unlink.svg create mode 100644 platform/ui/src/components/Modal/Modal.css create mode 100644 platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx create mode 100644 platform/ui/src/components/ViewportDownloadForm/index.js diff --git a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js index babf3bf69..7582c0960 100644 --- a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js +++ b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js @@ -145,8 +145,4 @@ CornerstoneViewportDownloadForm.propTypes = { activeViewportIndex: PropTypes.number.isRequired, }; -// export default CornerstoneViewportDownloadForm; - -export default function HelloWorld() { - return
Hello World
; -} +export default CornerstoneViewportDownloadForm; diff --git a/platform/core/src/services/UIModalService/index.js b/platform/core/src/services/UIModalService/index.js index ed2eab96a..884e53500 100644 --- a/platform/core/src/services/UIModalService/index.js +++ b/platform/core/src/services/UIModalService/index.js @@ -33,7 +33,7 @@ const serviceImplementation = { function _show({ content = null, contentProps = null, - shouldCloseOnEsc = false, + shouldCloseOnEsc = true, isOpen = true, closeButton = true, title = null, diff --git a/platform/ui/index.js b/platform/ui/index.js index 2025cfb3b..12b086abd 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -75,6 +75,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, } from './src/components'; diff --git a/platform/ui/src/assets/icons/close.svg b/platform/ui/src/assets/icons/close.svg new file mode 100644 index 000000000..83b259fca --- /dev/null +++ b/platform/ui/src/assets/icons/close.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/link.svg b/platform/ui/src/assets/icons/link.svg new file mode 100644 index 000000000..7c99fc27c --- /dev/null +++ b/platform/ui/src/assets/icons/link.svg @@ -0,0 +1,11 @@ + + + + diff --git a/platform/ui/src/assets/icons/unlink.svg b/platform/ui/src/assets/icons/unlink.svg new file mode 100644 index 000000000..37c53bbb9 --- /dev/null +++ b/platform/ui/src/assets/icons/unlink.svg @@ -0,0 +1,11 @@ + + Unlink + + diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 145539060..8f0a7a534 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -4,6 +4,7 @@ import React from 'react'; import arrowDown from './../../assets/icons/arrow-down.svg'; import calendar from './../../assets/icons/calendar.svg'; import cancel from './../../assets/icons/cancel.svg'; +import close from './../../assets/icons/close.svg'; import dottedCircle from './../../assets/icons/dotted-circle.svg'; import circledCheckmark from './../../assets/icons/circled-checkmark.svg'; import chevronDown from './../../assets/icons/chevron-down.svg'; @@ -16,6 +17,7 @@ import info from './../../assets/icons/info.svg'; import infoLink from './../../assets/icons/info-link.svg'; import launchArrow from './../../assets/icons/launch-arrow.svg'; import launchInfo from './../../assets/icons/launch-info.svg'; +import link from './../../assets/icons/link.svg'; import listBullets from './../../assets/icons/list-bullets.svg'; import lock from './../../assets/icons/lock.svg'; import logoOhifSmall from './../../assets/icons/logo-ohif-small.svg'; @@ -30,6 +32,7 @@ import sorting from './../../assets/icons/sorting.svg'; import sortingActiveDown from './../../assets/icons/sorting-active-down.svg'; import sortingActiveUp from './../../assets/icons/sorting-active-up.svg'; import tracked from './../../assets/icons/tracked.svg'; +import unlink from './../../assets/icons/unlink.svg'; /** Tools */ import toolZoom from './../../assets/icons/tool-zoom.svg'; @@ -47,6 +50,7 @@ const ICONS = { 'arrow-down': arrowDown, calendar: calendar, cancel: cancel, + close: close, 'dotted-circle': dottedCircle, 'circled-checkmark': circledCheckmark, 'chevron-down': chevronDown, @@ -59,6 +63,7 @@ const ICONS = { 'info-link': infoLink, 'launch-arrow': launchArrow, 'launch-info': launchInfo, + link: link, 'list-bullets': listBullets, lock: lock, 'logo-ohif-small': logoOhifSmall, @@ -73,6 +78,7 @@ const ICONS = { 'sorting-active-up': sortingActiveUp, sorting: sorting, tracked: tracked, + unlink: unlink, /** Tools */ 'tool-zoom': toolZoom, diff --git a/platform/ui/src/components/IconButton/IconButton.jsx b/platform/ui/src/components/IconButton/IconButton.jsx index d92a6b9ca..290bfa273 100644 --- a/platform/ui/src/components/IconButton/IconButton.jsx +++ b/platform/ui/src/components/IconButton/IconButton.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; const baseClasses = - 'text-center items-center justify-center outline-none font-bold focus:outline-none'; + 'text-center items-center justify-center transition duration-300 ease-in-out outline-none font-bold focus:outline-none'; const roundedClasses = { none: '', @@ -84,7 +84,7 @@ const IconButton = ({ }) => { const buttonElement = useRef(null); - const handleOnClick = (e) => { + const handleOnClick = e => { buttonElement.current.blur(); onClick(e); }; @@ -113,7 +113,7 @@ const IconButton = ({ }; IconButton.defaultProps = { - onClick: () => { }, + onClick: () => {}, color: 'default', disabled: false, fullWidth: false, diff --git a/platform/ui/src/components/Input/Input.jsx b/platform/ui/src/components/Input/Input.jsx index 51b40f0e5..8f26ad4e7 100644 --- a/platform/ui/src/components/Input/Input.jsx +++ b/platform/ui/src/components/Input/Input.jsx @@ -4,11 +4,11 @@ import Label from '../Label'; import classnames from 'classnames'; const baseInputClasses = - 'shadow transition duration-300 appearance-none border rounded w-full py-2 px-3 text-sm text-white hover:border-gray-500 leading-tight focus:border-gray-500 focus:outline-none'; + 'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none'; const transparentClasses = { true: 'bg-transparent', - false: '', + false: 'bg-black', }; const Input = ({ @@ -16,7 +16,7 @@ const Input = ({ containerClassName = '', labelClassName = '', className = '', - transparent = true, + transparent = false, type = 'text', value, onChange, diff --git a/platform/ui/src/components/InputText/InputText.jsx b/platform/ui/src/components/InputText/InputText.jsx index 2a244d7e8..2f6fbe70b 100644 --- a/platform/ui/src/components/InputText/InputText.jsx +++ b/platform/ui/src/components/InputText/InputText.jsx @@ -23,7 +23,7 @@ const InputText = ({ type="text" containerClassName="mr-2" value={value} - onChange={(event) => { + onChange={event => { onChange(event.target.value); }} /> @@ -33,15 +33,17 @@ const InputText = ({ InputText.defaultProps = { value: '', + isSortable: false, + onLabelClick: () => {}, + sortDirection: 'none', }; InputText.propTypes = { label: PropTypes.string.isRequired, - isSortable: PropTypes.bool.isRequired, - sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']) - .isRequired, - onLabelClick: PropTypes.func.isRequired, - value: PropTypes.string, + isSortable: PropTypes.bool, + sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']), + onLabelClick: PropTypes.func, + value: PropTypes.any, onChange: PropTypes.func.isRequired, }; diff --git a/platform/ui/src/components/Modal/Modal.css b/platform/ui/src/components/Modal/Modal.css new file mode 100644 index 000000000..f67d31616 --- /dev/null +++ b/platform/ui/src/components/Modal/Modal.css @@ -0,0 +1,3 @@ +.modal-content { + max-height: calc(100vh - theme('spacing.250px')); +} diff --git a/platform/ui/src/components/Modal/Modal.jsx b/platform/ui/src/components/Modal/Modal.jsx index 4ef669cbf..3a245f8aa 100644 --- a/platform/ui/src/components/Modal/Modal.jsx +++ b/platform/ui/src/components/Modal/Modal.jsx @@ -1,22 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import ReactModal from 'react-modal'; -import classNames from 'classnames'; -const customStyle = { - overlay: { - zIndex: 1071, - backgroundColor: 'rgb(0, 0, 0, 0.5)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, -}; +import './Modal.css'; + +import { Typography, useModal, IconButton, Icon } from '@ohif/ui'; ReactModal.setAppElement(document.getElementById('root')); const Modal = ({ - className, closeButton, shouldCloseOnEsc, isOpen, @@ -24,15 +16,27 @@ const Modal = ({ onClose, children, }) => { + const { hide } = useModal(); + + const handleClose = () => { + hide(); + }; + const renderHeader = () => { return ( title && ( -
-

{title}

+
+ {title} {closeButton && ( - + + + )}
) @@ -41,22 +45,26 @@ const Modal = ({ return ( - <> - {renderHeader()} -
{children}
- +
{renderHeader()}
+
+ {children} +
); }; +Modal.defaultProps = { + shouldCloseOnEsc: true, +}; + Modal.propTypes = { - className: PropTypes.string, closeButton: PropTypes.bool, shouldCloseOnEsc: PropTypes.bool, isOpen: PropTypes.bool, diff --git a/platform/ui/src/components/Select/Select.jsx b/platform/ui/src/components/Select/Select.jsx index 399edd541..0cc410a46 100644 --- a/platform/ui/src/components/Select/Select.jsx +++ b/platform/ui/src/components/Select/Select.jsx @@ -76,10 +76,9 @@ const Select = ({ options={options} value={selectedOptions} onChange={(selectedOptions, { action }) => { - const newSelection = !selectedOptions.length ? selectedOptions : selectedOptions.reduce( - (acc, curr) => acc.concat([curr.value]), - [] - ); + const newSelection = !selectedOptions.length + ? selectedOptions + : selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []); onChange(newSelection, action); }} > diff --git a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx index 2d409a555..0d06bb2e3 100644 --- a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx +++ b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Icon, Typography, InputGroup } from '@ohif/ui'; +import { Button, Icon, Typography, InputGroup, useModal } from '@ohif/ui'; const StudyListFilter = ({ filtersMeta, @@ -20,6 +20,16 @@ const StudyListFilter = ({ }); }; const isSortingEnable = numOfStudies > 0 && numOfStudies <= 100; + const { show } = useModal(); + + const showLearnMoreContent = () => { + const modalContent = () =>
Search Instructions
; + + show({ + content: modalContent, + title: 'Learn More', + }); + }; return ( @@ -38,6 +48,7 @@ const StudyListFilter = ({ color="inherit" className="text-primary-active" startIcon={} + onClick={showLearnMoreContent} > Learn more diff --git a/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx new file mode 100644 index 000000000..06c682502 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/ViewportDownloadForm.jsx @@ -0,0 +1,432 @@ +import React, { + useCallback, + useEffect, + useState, + createRef, + useRef, +} from 'react'; + +import classnames from 'classnames'; + +import { + Typography, + Input, + Tooltip, + IconButton, + Icon, + Select, + InputLabelWrapper, + Button, +} from '@ohif/ui'; + +const FILE_TYPE_OPTIONS = [ + { + value: 'jpg', + label: 'jpg', + }, + { + value: 'png', + label: 'png', + }, +]; + +const DEFAULT_FILENAME = 'image'; +const REFRESH_VIEWPORT_TIMEOUT = 1000; + +const ViewportDownloadForm = ({ + activeViewport, + onClose, + updateViewportPreview, + enableViewport, + disableViewport, + toggleAnnotations, + loadImage, + downloadBlob, + defaultSize, + minimumSize, + maximumSize, + canvasClass, +}) => { + const [filename, setFilename] = useState(DEFAULT_FILENAME); + const [fileType, setFileType] = useState(['jpg']); + + const [dimensions, setDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [showAnnotations, setShowAnnotations] = useState(true); + + const [keepAspect, setKeepAspect] = useState(true); + const [aspectMultiplier, setAspectMultiplier] = useState({ + width: 1, + height: 1, + }); + + const [viewportElement, setViewportElement] = useState(); + const [viewportElementDimensions, setViewportElementDimensions] = useState({ + width: defaultSize, + height: defaultSize, + }); + + const [downloadCanvas, setDownloadCanvas] = useState({ + ref: createRef(), + width: defaultSize, + height: defaultSize, + }); + + const [viewportPreview, setViewportPreview] = useState({ + src: null, + width: defaultSize, + height: defaultSize, + }); + + const [error, setError] = useState({ + width: false, + height: false, + filename: false, + }); + + const hasError = Object.values(error).includes(true); + + const refreshViewport = useRef(null); + + 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); + }; + + const downloadImage = () => { + downloadBlob( + filename || DEFAULT_FILENAME, + fileType, + viewportElement, + downloadCanvas.ref.current + ); + }; + + /** + * @param {object} value - Input value + * @param {string} dimension - "height" | "width" + */ + const onDimensionsChange = (value, dimension) => { + const oppositeDimension = dimension === 'height' ? 'width' : 'height'; + const sanitizedTargetValue = 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: 'The minimum valid width is 100px.', + height: 'The minimum valid height is 100px.', + filename: 'The file name cannot be empty.', + }; + + const renderErrorHandler = errorType => { + if (!error[errorType]) { + return null; + } + + return ( + + {error_messages[errorType]} + + ); + }; + + const validSize = useCallback( + value => (value >= minimumSize ? value : minimumSize), + [minimumSize] + ); + + const loadAndUpdateViewports = useCallback(async () => { + const { width: scaledWidth, height: scaledHeight } = await loadImage( + activeViewport, + viewportElement, + dimensions.width, + dimensions.height + ); + + toggleAnnotations(showAnnotations, viewportElement); + + const scaledDimensions = { + height: validSize(scaledHeight), + width: validSize(scaledWidth), + }; + + setViewportElementDimensions(scaledDimensions); + setDownloadCanvas(state => ({ + ...state, + ...scaledDimensions, + })); + + const { + dataUrl, + width: viewportElementWidth, + height: viewportElementHeight, + } = await updateViewportPreview( + viewportElement, + downloadCanvas.ref.current, + fileType + ); + + setViewportPreview(state => ({ + ...state, + src: dataUrl, + width: validSize(viewportElementWidth), + height: validSize(viewportElementHeight), + })); + }, [ + loadImage, + activeViewport, + viewportElement, + dimensions.width, + dimensions.height, + toggleAnnotations, + showAnnotations, + validSize, + updateViewportPreview, + downloadCanvas.ref, + fileType, + ]); + + useEffect(() => { + enableViewport(viewportElement); + + return () => { + disableViewport(viewportElement); + }; + }, [disableViewport, enableViewport, viewportElement]); + + useEffect(() => { + if (refreshViewport.current !== null) { + clearTimeout(refreshViewport.current); + } + + refreshViewport.current = setTimeout(() => { + refreshViewport.current = null; + loadAndUpdateViewports(); + }, REFRESH_VIEWPORT_TIMEOUT); + }, [ + activeViewport, + viewportElement, + showAnnotations, + dimensions, + loadImage, + toggleAnnotations, + updateViewportPreview, + fileType, + downloadCanvas.ref, + minimumSize, + maximumSize, + loadAndUpdateViewports, + ]); + + useEffect(() => { + const { width, height } = dimensions; + const hasError = { + width: width < minimumSize, + height: height < minimumSize, + filename: !filename, + }; + + setError({ ...hasError }); + }, [dimensions, filename, minimumSize]); + + return ( +
+ + Please specify the dimensions, filename, and desired type for the output + image. + + +
+
+ setFilename(value)} + label="File Name" + /> + {renderErrorHandler('filename')} +
+
+
+
+
+ onDimensionsChange(value, 'width')} + data-cy="image-width" + /> + {renderErrorHandler('width')} +
+
+ onDimensionsChange(value, 'height')} + data-cy="image-height" + /> + {renderErrorHandler('height')} +
+
+ +
+ + + + + +
+
+ +
+
+ {}} + > + setShowAnnotations(event.target.checked)} + /> + Show Annotations + +
+
+
+
+ +
+
setViewportElement(ref)} + > + +
+ + {viewportPreview.src ? ( +
+ Image preview + Preview +
+ ) : ( +
+ Loading Image Preview... +
+ )} +
+ +
+ + +
+
+ ); +}; + +export default ViewportDownloadForm; diff --git a/platform/ui/src/components/ViewportDownloadForm/index.js b/platform/ui/src/components/ViewportDownloadForm/index.js new file mode 100644 index 000000000..d630f3a19 --- /dev/null +++ b/platform/ui/src/components/ViewportDownloadForm/index.js @@ -0,0 +1 @@ +export { default } from './ViewportDownloadForm'; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 04b5270cc..77247ac4d 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -45,6 +45,7 @@ import Tooltip from './Tooltip'; import Typography from './Typography'; import Viewport from './Viewport'; import ViewportActionBar from './ViewportActionBar'; +import ViewportDownloadForm from './ViewportDownloadForm'; import ViewportGrid from './ViewportGrid'; import ViewportPane from './ViewportPane'; @@ -97,6 +98,7 @@ export { Typography, Viewport, ViewportActionBar, + ViewportDownloadForm, ViewportGrid, ViewportPane, }; diff --git a/platform/ui/src/contextProviders/ModalComponent.jsx b/platform/ui/src/contextProviders/ModalComponent.jsx index b3b6602a7..cf7965812 100644 --- a/platform/ui/src/contextProviders/ModalComponent.jsx +++ b/platform/ui/src/contextProviders/ModalComponent.jsx @@ -16,7 +16,7 @@ const ModalComponent = ({ ModalComponent.defaultProps = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, diff --git a/platform/ui/src/contextProviders/ModalProvider.jsx b/platform/ui/src/contextProviders/ModalProvider.jsx index e8d65635d..ff9224bca 100644 --- a/platform/ui/src/contextProviders/ModalProvider.jsx +++ b/platform/ui/src/contextProviders/ModalProvider.jsx @@ -19,7 +19,7 @@ export const useModal = () => useContext(ModalContext); * @typedef {Object} ModalProps * @property {ReactElement|HTMLElement} [content=null] Modal content. * @property {Object} [contentProps=null] Modal content props. - * @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key. + * @property {boolean} [shouldCloseOnEsc=true] Modal is dismissible via the esc key. * @property {boolean} [isOpen=true] Make the Modal visible or hidden. * @property {boolean} [closeButton=true] Should the modal body render the close button. * @property {string} [title=null] Should the modal render the title independently of the body content. @@ -30,7 +30,7 @@ const ModalProvider = ({ children, modal: Modal, service }) => { const DEFAULT_OPTIONS = { content: null, contentProps: null, - shouldCloseOnEsc: false, + shouldCloseOnEsc: true, isOpen: true, closeButton: true, title: null, diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index 5cdbcdf50..3e32e4955 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -10,6 +10,7 @@ module.exports = { xl: '1280px', }, colors: { + overlay: 'rgba(0, 0, 0, 0.8)', transparent: 'transparent', black: '#000', white: '#fff', @@ -17,10 +18,10 @@ module.exports = { inherit: 'inherit', indigo: { - dark: '#0b1a42' + dark: '#0b1a42', }, aqua: { - pale: '#7bb2ce' + pale: '#7bb2ce', }, primary: { From c93faf628b7c64d102e2f9b306fc55466f9b7134 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Fri, 26 Jun 2020 12:44:30 -0300 Subject: [PATCH 31/32] nuke inputnumber --- platform/ui/index.js | 1 - .../components/InputNumber/InputNumber.jsx | 50 ------------------- .../components/InputNumber/InputNumber.mdx | 44 ---------------- .../ui/src/components/InputNumber/index.js | 2 - platform/ui/src/components/index.js | 2 - 5 files changed, 99 deletions(-) delete mode 100644 platform/ui/src/components/InputNumber/InputNumber.jsx delete mode 100644 platform/ui/src/components/InputNumber/InputNumber.mdx delete mode 100644 platform/ui/src/components/InputNumber/index.js diff --git a/platform/ui/index.js b/platform/ui/index.js index 5032f0271..e3c8a8f25 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -42,7 +42,6 @@ export { InputGroup, InputLabelWrapper, InputMultiSelect, - InputNumber, InputText, Label, LayoutSelector, diff --git a/platform/ui/src/components/InputNumber/InputNumber.jsx b/platform/ui/src/components/InputNumber/InputNumber.jsx deleted file mode 100644 index c6fca474f..000000000 --- a/platform/ui/src/components/InputNumber/InputNumber.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { Input, InputLabelWrapper } from '@ohif/ui'; - -const InputNumber = ({ - label, - isSortable, - sortDirection, - onLabelClick, - value, - onChange, -}) => { - return ( - - { - onChange(event.target.value); - }} - /> - - ); -}; - -InputNumber.defaultProps = { - value: '', - isSortable: false, - onLabelClick: () => {}, - sortDirection: 'none', -}; - -InputNumber.propTypes = { - label: PropTypes.string.isRequired, - isSortable: PropTypes.bool, - sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']), - onLabelClick: PropTypes.func, - value: PropTypes.number, - onChange: PropTypes.func.isRequired, -}; - -export default InputNumber; diff --git a/platform/ui/src/components/InputNumber/InputNumber.mdx b/platform/ui/src/components/InputNumber/InputNumber.mdx deleted file mode 100644 index 6435c32ab..000000000 --- a/platform/ui/src/components/InputNumber/InputNumber.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: InputNumber -menu: Form -route: components/InputNumber ---- - -import { useState } from 'react'; -import { Playground, Props } from 'docz'; -import { InputNumber } from '@ohif/ui'; - -# Input Text - -## Import - -```javascript -import { InputNumber } from '@ohif/ui'; -``` - -## Basic usage - - - {() => { - const [number, setNumber] = useState(10); - return ( -
-
- { - setText(value); - }} - /> -
-
- ); - }} -
- -## Properties - - diff --git a/platform/ui/src/components/InputNumber/index.js b/platform/ui/src/components/InputNumber/index.js deleted file mode 100644 index 566112449..000000000 --- a/platform/ui/src/components/InputNumber/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import InputNumber from './InputNumber'; -export default InputNumber; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 8ba67e4ab..deb140bbb 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -11,7 +11,6 @@ import InputDateRange from './InputDateRange'; import InputGroup from './InputGroup'; import InputLabelWrapper from './InputLabelWrapper'; import InputMultiSelect from './InputMultiSelect'; -import InputNumber from './InputNumber'; import InputText from './InputText'; import Label from './Label'; import LayoutSelector from './LayoutSelector'; @@ -67,7 +66,6 @@ export { InputGroup, InputLabelWrapper, InputMultiSelect, - InputNumber, InputText, Label, LayoutSelector, From cdc88ff55273b7df67d42627e5eac89d2077ed8b Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Fri, 26 Jun 2020 12:59:06 -0300 Subject: [PATCH 32/32] fix learn more --- platform/ui/src/components/StudyListFilter/StudyListFilter.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx index a4d5c0589..0d06bb2e3 100644 --- a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx +++ b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx @@ -51,7 +51,7 @@ const StudyListFilter = ({ onClick={showLearnMoreContent} > - Learn more + Learn more