diff --git a/platform/ui/index.js b/platform/ui/index.js index 407ac00f2..8a99ddcfa 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -1,3 +1,5 @@ +import * as utils from './src/utils/'; + export { Button, ButtonGroup, @@ -9,4 +11,6 @@ export { Typography, } from './src/components'; +export { utils }; + export { StudyList } from './src/views'; diff --git a/platform/ui/package.json b/platform/ui/package.json index 8cc957d2d..4622b78b5 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "classnames": "^2.2.6", + "date-fns": "^2.10.0", "docz": "^2.2.0", "theme-ui": "^0.2.38", "gatsby": "2.19.24", diff --git a/platform/ui/src/assets/icons/instances-active.svg b/platform/ui/src/assets/icons/instances-active.svg deleted file mode 100644 index 3be5c3622..000000000 --- a/platform/ui/src/assets/icons/instances-active.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/platform/ui/src/assets/icons/magnifier.svg b/platform/ui/src/assets/icons/magnifier.svg new file mode 100644 index 000000000..7d199db88 --- /dev/null +++ b/platform/ui/src/assets/icons/magnifier.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/active.svg b/platform/ui/src/assets/icons/series-active.svg similarity index 100% rename from platform/ui/src/assets/icons/active.svg rename to platform/ui/src/assets/icons/series-active.svg diff --git a/platform/ui/src/assets/icons/instances-inactive.svg b/platform/ui/src/assets/icons/series-inactive.svg similarity index 100% rename from platform/ui/src/assets/icons/instances-inactive.svg rename to platform/ui/src/assets/icons/series-inactive.svg diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 743a91df4..248e94550 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -1,6 +1,7 @@ import React from 'react'; // Icons -import active from './../../assets/icons/active.svg'; +import seriesActive from './../../assets/icons/series-active.svg'; +import seriesInactive from './../../assets/icons/series-inactive.svg'; import cancel from './../../assets/icons/cancel.svg'; import chevronDown from './../../assets/icons/chevron-down.svg'; import chevronRight from './../../assets/icons/chevron-right.svg'; @@ -8,6 +9,7 @@ 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 logoOhifSmall from './../../assets/icons/logo-ohif-small.svg'; +import magnifier from './../../assets/icons/magnifier.svg'; import notificationwarningDiamond from './../../assets/icons/notificationwarning-diamond.svg'; import settings from './../../assets/icons/settings.svg'; import sorting from './../../assets/icons/sorting.svg'; @@ -15,7 +17,8 @@ import sortingActiveDown from './../../assets/icons/sorting-active-down.svg'; import sortingActiveUp from './../../assets/icons/sorting-active-up.svg'; const ICONS = { - active: active, + 'series-active': seriesActive, + 'series-inactive': seriesInactive, cancel: cancel, 'chevron-down': chevronDown, 'chevron-right': chevronRight, @@ -23,6 +26,7 @@ const ICONS = { 'launch-arrow': launchArrow, 'launch-info': launchInfo, 'logo-ohif-small': logoOhifSmall, + magnifier: magnifier, 'notificationwarning-diamond': notificationwarningDiamond, settings: settings, 'sorting-active-down': sortingActiveDown, diff --git a/platform/ui/src/components/Icon/icon.mdx b/platform/ui/src/components/Icon/icon.mdx index a59554ab8..5670a57f4 100644 --- a/platform/ui/src/components/Icon/icon.mdx +++ b/platform/ui/src/components/Icon/icon.mdx @@ -14,7 +14,7 @@ import { ICONS } from './getIcon';
- +
diff --git a/platform/ui/src/mocks/studyList.json b/platform/ui/src/mocks/studyList.json index 6ecfc51ca..557986877 100644 --- a/platform/ui/src/mocks/studyList.json +++ b/platform/ui/src/mocks/studyList.json @@ -3,10 +3,13 @@ { "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78", "StudyDescription": "BRAIN SELLA", + "AccessionNumber": "11788761116031", "StudyDate": "20010108", "StudyTime": "120022", "PatientName": "MISTER^MR", "PatientId": "832040", + "Instances": 33, + "Modalities": "MR", "series": [ { "SeriesDescription": "SAG T-1", diff --git a/platform/ui/src/utils/colorManipulator.js b/platform/ui/src/utils/colorManipulator.js deleted file mode 100644 index 1b0a99d3b..000000000 --- a/platform/ui/src/utils/colorManipulator.js +++ /dev/null @@ -1,270 +0,0 @@ -/* eslint-disable no-use-before-define */ - -/** - * Returns a number whose value is limited to the given range. - * - * @param {number} value The value to be clamped - * @param {number} min The lower boundary of the output range - * @param {number} max The upper boundary of the output range - * @returns {number} A number in the range [min, max] - */ -function clamp(value, min = 0, max = 1) { - if (process.env.NODE_ENV !== 'production') { - if (value < min || value > max) { - console.error( - `@ohif/ui: the value provided ${value} is out of range [${min}, ${max}].` - ); - } - } - - return Math.min(Math.max(min, value), max); -} - -/** - * Converts a color from CSS hex format to CSS rgb format. - * - * @param {string} color - Hex color, i.e. #nnn or #nnnnnn - * @returns {string} A CSS rgb color string - */ -export function hexToRgb(color) { - color = color.substr(1); - - const re = new RegExp(`.{1,${color.length / 3}}`, 'g'); - let colors = color.match(re); - - if (colors && colors[0].length === 1) { - colors = colors.map(n => n + n); - } - - return colors ? `rgb(${colors.map(n => parseInt(n, 16)).join(', ')})` : ''; -} - -function intToHex(int) { - const hex = int.toString(16); - return hex.length === 1 ? `0${hex}` : hex; -} - -/** - * Converts a color from CSS rgb format to CSS hex format. - * - * @param {string} color - RGB color, i.e. rgb(n, n, n) - * @returns {string} A CSS rgb color string, i.e. #nnnnnn - */ -export function rgbToHex(color) { - // Idempotent - if (color.indexOf('#') === 0) { - return color; - } - - const { values } = decomposeColor(color); - return `#${values.map(n => intToHex(n)).join('')}`; -} - -/** - * Converts a color from hsl format to rgb format. - * - * @param {string} color - HSL color values - * @returns {string} rgb color values - */ -export function hslToRgb(color) { - color = decomposeColor(color); - const { values } = color; - const h = values[0]; - const s = values[1] / 100; - const l = values[2] / 100; - const a = s * Math.min(l, 1 - l); - const f = (n, k = (n + h / 30) % 12) => - l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); - - let type = 'rgb'; - const rgb = [ - Math.round(f(0) * 255), - Math.round(f(8) * 255), - Math.round(f(4) * 255), - ]; - - if (color.type === 'hsla') { - type += 'a'; - rgb.push(values[3]); - } - - return recomposeColor({ type, values: rgb }); -} - -/** - * Returns an object with the type and values of a color. - * - * Note: Does not support rgb % values. - * - * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @returns {object} - A MUI color object: {type: string, values: number[]} - */ -export function decomposeColor(color) { - // Idempotent - if (color.type) { - return color; - } - - if (color.charAt(0) === '#') { - return decomposeColor(hexToRgb(color)); - } - - const marker = color.indexOf('('); - const type = color.substring(0, marker); - - if (['rgb', 'rgba', 'hsl', 'hsla'].indexOf(type) === -1) { - throw new Error( - [ - `@ohif/ui: unsupported \`${color}\` color.`, - 'We support the following formats: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla().', - ].join('\n') - ); - } - - let values = color.substring(marker + 1, color.length - 1).split(','); - values = values.map(value => parseFloat(value)); - - return { type, values }; -} - -/** - * Converts a color object with type and values to a string. - * - * @param {object} color - Decomposed color - * @param {string} color.type - One of: 'rgb', 'rgba', 'hsl', 'hsla' - * @param {array} color.values - [n,n,n] or [n,n,n,n] - * @returns {string} A CSS color string - */ -export function recomposeColor(color) { - const { type } = color; - let { values } = color; - - if (type.indexOf('rgb') !== -1) { - // Only convert the first 3 values to int (i.e. not alpha) - values = values.map((n, i) => (i < 3 ? parseInt(n, 10) : n)); - } else if (type.indexOf('hsl') !== -1) { - values[1] = `${values[1]}%`; - values[2] = `${values[2]}%`; - } - - return `${type}(${values.join(', ')})`; -} - -/** - * Calculates the contrast ratio between two colors. - * - * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests - * - * @param {string} foreground - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @returns {number} A contrast ratio value in the range 0 - 21. - */ -export function getContrastRatio(foreground, background) { - const lumA = getLuminance(foreground); - const lumB = getLuminance(background); - return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05); -} - -/** - * The relative brightness of any point in a color space, - * normalized to 0 for darkest black and 1 for lightest white. - * - * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests - * - * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @returns {number} The relative brightness of the color in the range 0 - 1 - */ -export function getLuminance(color) { - color = decomposeColor(color); - - let rgb = - color.type === 'hsl' - ? decomposeColor(hslToRgb(color)).values - : color.values; - rgb = rgb.map(val => { - val /= 255; // normalized - return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4; - }); - - // Truncate at 3 digits - return Number( - (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3) - ); -} - -/** - * Darken or lighten a color, depending on its luminance. - * Light colors are darkened, dark colors are lightened. - * - * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @param {number} coefficient=0.15 - multiplier in the range 0 - 1 - * @returns {string} A CSS color string. Hex input values are returned as rgb - */ -export function emphasize(color, coefficient = 0.15) { - return getLuminance(color) > 0.5 - ? darken(color, coefficient) - : lighten(color, coefficient); -} - -/** - * Set the absolute transparency of a color. - * Any existing alpha values are overwritten. - * - * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @param {number} value - value to set the alpha channel to in the range 0 -1 - * @returns {string} A CSS color string. Hex input values are returned as rgb - */ -export function fade(color, value) { - color = decomposeColor(color); - value = clamp(value); - - if (color.type === 'rgb' || color.type === 'hsl') { - color.type += 'a'; - } - color.values[3] = value; - - return recomposeColor(color); -} - -/** - * Darkens a color. - * - * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @param {number} coefficient - multiplier in the range 0 - 1 - * @returns {string} A CSS color string. Hex input values are returned as rgb - */ -export function darken(color, coefficient) { - color = decomposeColor(color); - coefficient = clamp(coefficient); - - if (color.type.indexOf('hsl') !== -1) { - color.values[2] *= 1 - coefficient; - } else if (color.type.indexOf('rgb') !== -1) { - for (let i = 0; i < 3; i += 1) { - color.values[i] *= 1 - coefficient; - } - } - return recomposeColor(color); -} - -/** - * Lightens a color. - * - * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() - * @param {number} coefficient - multiplier in the range 0 - 1 - * @returns {string} A CSS color string. Hex input values are returned as rgb - */ -export function lighten(color, coefficient) { - color = decomposeColor(color); - coefficient = clamp(coefficient); - - if (color.type.indexOf('hsl') !== -1) { - color.values[2] += (100 - color.values[2]) * coefficient; - } else if (color.type.indexOf('rgb') !== -1) { - for (let i = 0; i < 3; i += 1) { - color.values[i] += (255 - color.values[i]) * coefficient; - } - } - - return recomposeColor(color); -} diff --git a/platform/ui/src/utils/getInstances.js b/platform/ui/src/utils/getInstances.js new file mode 100644 index 000000000..3ca9fc315 --- /dev/null +++ b/platform/ui/src/utils/getInstances.js @@ -0,0 +1,15 @@ +/** + * Calculates the number of instances in series + * @param {array} series + * @returns {number} Number of instances in series + */ + +const getInstances = series => { + const instances = series.reduce((acc, item) => { + return acc + item.instances.length; + }, 0); + + return instances; +}; + +export default getInstances; diff --git a/platform/ui/src/utils/getMockedStudies.js b/platform/ui/src/utils/getMockedStudies.js new file mode 100644 index 000000000..678d79bf8 --- /dev/null +++ b/platform/ui/src/utils/getMockedStudies.js @@ -0,0 +1,17 @@ +import studyListMock from '../mocks/studyList.json'; + +/** Values can be env vars */ +const DEFAULT_MOCKED_STUDIES_LIMIT = 1000; + +/** + * Method to get a mocked study list + * @param {number} items Number of studies to be loaded + * @returns {array} Study list + */ +const getMockedStudies = (items = 50) => { + const num = + items > DEFAULT_MOCKED_STUDIES_LIMIT ? DEFAULT_MOCKED_STUDIES_LIMIT : items; + return new Array(num).fill(studyListMock.studies[0]); +}; + +export default getMockedStudies; diff --git a/platform/ui/src/utils/getModalities.js b/platform/ui/src/utils/getModalities.js new file mode 100644 index 000000000..ae5cef90d --- /dev/null +++ b/platform/ui/src/utils/getModalities.js @@ -0,0 +1,20 @@ +/** + * Get formatted Modalities + * @param {array} series + * @returns {string} Formatted modalities + */ +const getModalities = series => { + const modalities = series.reduce((acc, item) => { + const { Modality } = item; + if (acc.includes(Modality)) { + return acc; + } + + acc.push(Modality); + return acc; + }, []); + + return modalities.join('/'); +}; + +export default getModalities; diff --git a/platform/ui/src/utils/index.js b/platform/ui/src/utils/index.js new file mode 100644 index 000000000..a7e441b76 --- /dev/null +++ b/platform/ui/src/utils/index.js @@ -0,0 +1,6 @@ +import capitalize from './capitalize'; +import getMockedStudies from './getMockedStudies'; +import getModalities from './getModalities'; +import getInstances from './getInstances'; + +export { capitalize, getMockedStudies, getModalities, getInstances }; diff --git a/platform/ui/src/views/StudyList/StudyList.js b/platform/ui/src/views/StudyList/StudyList.js index 1b96d1fcb..c92f24805 100644 --- a/platform/ui/src/views/StudyList/StudyList.js +++ b/platform/ui/src/views/StudyList/StudyList.js @@ -1,18 +1,38 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + import Header from './components/Header'; import StudyListFilter from './components/StudyListFilter'; import StudyListTable from './components/StudyListTable'; import StudyListPagination from './components/StudyListPagination'; -const StudyList = () => { +const StudyList = ({ studies, perPage }) => { + const studiesData = studies.slice(0, perPage); + const numOfStudies = studies.length; + const isEmptyStudies = numOfStudies === 0; return ( -
+
- - - + + + {!isEmptyStudies && }
); }; +StudyList.defaultProps = { + studies: [], + perPage: 25, +}; + +StudyList.propTypes = { + studies: PropTypes.array, + perPage: PropTypes.number, +}; + export default StudyList; diff --git a/platform/ui/src/views/StudyList/StudyList.mdx b/platform/ui/src/views/StudyList/StudyList.mdx index f18d36b5e..284020c26 100644 --- a/platform/ui/src/views/StudyList/StudyList.mdx +++ b/platform/ui/src/views/StudyList/StudyList.mdx @@ -4,8 +4,11 @@ menu: Views route: views/studyList --- +import { useState } from 'react' import { Playground, Props } from 'docz'; import StudyList from './'; +import Button from '../../components/Button'; +import { getMockedStudies } from '../../utils/' # StudyList @@ -20,9 +23,24 @@ import { StudyList } from '@ohfi/ui'; ## StudyList - <> - - + {() => { + const [studies, setStudies] = useState([]) + const handleStudyList = (number) => { + const studies = getMockedStudies(number) + setStudies(studies) + } + return ( +
+
+ + + + +
+ +
+ ); + }}
## Properties diff --git a/platform/ui/src/views/StudyList/components/StudyListTable.js b/platform/ui/src/views/StudyList/components/StudyListTable.js index e6d77c444..096b7f205 100644 --- a/platform/ui/src/views/StudyList/components/StudyListTable.js +++ b/platform/ui/src/views/StudyList/components/StudyListTable.js @@ -1,19 +1,24 @@ import React, { useState } from 'react'; +import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Button } from '@ohif/ui'; +import { format } from 'date-fns'; +import { Button, Icon, Typography } from '@ohif/ui'; -/** TODO: Icon component should be used instead of importing the icons directly */ -import ChevronRight from '../../../assets/icons/chevron-right.svg'; -import ChevronDown from '../../../assets/icons/chevron-down.svg'; -import InstancesActive from '../../../assets/icons/instances-active.svg'; -import InstancesInactive from '../../../assets/icons/instances-inactive.svg'; -import LaunchInfo from '../../../assets/icons/launch-info.svg'; +const TableRow = props => { + const { + AccessionNumber, + Modalities, + Instances, + StudyDescription, + PatientId, + PatientName, + StudyDate, + series, + } = props; -const TableRow = () => { const [isOpened, setIsOpened] = useState(false); const toggleRow = () => setIsOpened(!isOpened); - const ChevronIcon = isOpened ? ChevronDown : ChevronRight; - const InstancesIcon = isOpened ? InstancesActive : InstancesInactive; + const ChevronIconName = isOpened ? 'chevron-down' : 'chevron-right'; const tdClasses = [ 'px-4 py-2', { 'border-b border-custom-violetPale': !isOpened }, @@ -23,7 +28,6 @@ const TableRow = () => { small: 'px-2 flex-0.3', }; const seriesBodyClasses = 'border-r border-custom-violetPale'; - return ( <> @@ -50,31 +54,39 @@ const TableRow = () => { onClick={toggleRow} > - + - Patient name - 11000002 + {PatientName} + {PatientId} - Mar-29-2013 11:26 AM + {format(StudyDate, 'MMM-DD-YYYY')} - PET^1_PETCT_WB_AC (Adult) + {StudyDescription} - CT/OT/PT - 00000001 + {Modalities} - - 902 + {AccessionNumber} + + + + {Instances} {isOpened && ( -
+
- - {/* ADD ICON HERE */} +
+ Feedback text lorem ipsum dolor sit amet - +
@@ -124,7 +139,7 @@ const TableRow = () => {
- {new Array(30).fill('').map((el, i) => ( + {series.map((seriesItem, i) => (
{ seriesBodyClasses )} > - # + {seriesItem.SeriesNumber}
{ seriesBodyClasses )} > - CT + {seriesItem.Modality}
- 149 + {seriesItem.instances.length}
))} @@ -170,27 +185,85 @@ const TableRow = () => { ); }; -const StudyListTable = () => { +TableRow.propTypes = { + AccessionNumber: PropTypes.string.isRequired, + Modalities: PropTypes.string.isRequired, + Instances: PropTypes.number.isRequired, + PatientId: PropTypes.string.isRequired, + PatientName: PropTypes.string.isRequired, + StudyDescription: PropTypes.string.isRequired, + StudyDate: PropTypes.string.isRequired, + series: PropTypes.array.isRequired, +}; + +const StudyListTable = ({ studies, numOfStudies }) => { + const renderTable = () => { + return ( + + + {studies.map((study, i) => ( + + ))} + +
+ ); + }; + + const renderEmpty = () => { + return ( +
+ + + No studies available + +
+ ); + }; + return ( <>
-
-

- Filter list to 100 studies or less to enable sorting -

-
- - - {new Array(30).fill('').map((empty, i) => ( - - ))} - -
+ {numOfStudies > 100 && ( +
+

+ Filter list to 100 studies or less to enable sorting +

+
+ )} + + {numOfStudies > 0 && renderTable()} + {numOfStudies === 0 && renderEmpty()}
); }; +StudyListTable.propTypes = { + studies: PropTypes.arrayOf( + PropTypes.shape({ + AccessionNumber: PropTypes.string.isRequired, + Modalities: PropTypes.string.isRequired, + Instances: PropTypes.number.isRequired, + PatientId: PropTypes.string.isRequired, + PatientName: PropTypes.string.isRequired, + StudyDescription: PropTypes.string.isRequired, + StudyDate: PropTypes.string.isRequired, + series: PropTypes.array.isRequired, + }) + ).isRequired, + numOfStudies: PropTypes.number.isRequired, +}; + export default StudyListTable; diff --git a/platform/viewer/src/connectedComponents/ConnectedStudyList.js b/platform/viewer/src/connectedComponents/ConnectedStudyList.js index ea2373402..fca88ae25 100644 --- a/platform/viewer/src/connectedComponents/ConnectedStudyList.js +++ b/platform/viewer/src/connectedComponents/ConnectedStudyList.js @@ -1,8 +1,12 @@ import React from 'react'; import { StudyList } from '@ohif/ui'; +// TEMPORARY MOCKING DATA FOR VISUALIZATION PURPOSES +import { utils } from '@ohif/ui'; + const ConnectedStudyList = () => { - return ; + const studies = utils.getMockedStudies(); + return ; }; export default ConnectedStudyList; diff --git a/yarn.lock b/yarn.lock index 0a0332f1f..ffea1ed0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6280,7 +6280,7 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== -date-fns@^2.2.1: +date-fns@^2.10.0, date-fns@^2.2.1: version "2.10.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.10.0.tgz#abd10604d8bafb0bcbd2ba2e9b0563b922ae4b6b" integrity sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA==