Merge pull request #1803 from OHIF/feat/ui-v2-Dropdown
Feat/ui v2 dropdown
This commit is contained in:
commit
2c0f7bb9f9
@ -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",
|
||||
|
||||
@ -1,22 +1,37 @@
|
||||
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
|
||||
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 { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
// 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' },
|
||||
// ]
|
||||
const { show } = useModal();
|
||||
|
||||
// TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
|
||||
const showAboutModal = useCallback(() => {
|
||||
const modalComponent = () => (
|
||||
<div>{t('AboutModal:OHIF Viewer - About')}</div>
|
||||
);
|
||||
show({
|
||||
title: t('AboutModal:OHIF Viewer - About'),
|
||||
content: modalComponent,
|
||||
});
|
||||
}, [show, t]);
|
||||
|
||||
// TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
|
||||
const showPreferencesModal = useCallback(() => {
|
||||
const modalComponent = () => (
|
||||
<div>{t('UserPreferencesModal:User Preferences')}</div>
|
||||
);
|
||||
show({
|
||||
title: t('UserPreferencesModal:User Preferences'),
|
||||
content: modalComponent,
|
||||
});
|
||||
}, [show, t]);
|
||||
|
||||
return (
|
||||
<NavBar className="justify-between border-b-4 border-black">
|
||||
@ -40,18 +55,40 @@ function Header({ children }) {
|
||||
<div className="flex items-center">{children}</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-lg text-common-light">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
{t('Header:INVESTIGATIONAL USE ONLY')}
|
||||
</span>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
className="text-primary-active"
|
||||
onClick={() => {}}
|
||||
<Dropdown
|
||||
showDropdownIcon={false}
|
||||
list={[
|
||||
{
|
||||
title: t('Header:About'),
|
||||
icon: 'info',
|
||||
onClick: showAboutModal,
|
||||
},
|
||||
{
|
||||
title: t('Header:Preferences'),
|
||||
icon: 'settings',
|
||||
onClick: showPreferencesModal,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Icon name="settings" /> <Icon name="chevron-down" />
|
||||
</React.Fragment>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="chevron-down" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</NavBar>
|
||||
|
||||
@ -31,6 +31,7 @@ export {
|
||||
ButtonGroup,
|
||||
DateRange,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
EmptyStudies,
|
||||
ExpandableToolbarButton,
|
||||
ListMenu,
|
||||
|
||||
@ -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"
|
||||
>
|
||||
<path d="m5.364 28.917c1.047 1.046 2.422 1.57 3.797 1.57 1.376 0 2.751-0.523 3.798-1.572l7.552-7.554c1.016-1.014 1.574-2.361 1.574-3.797 -0.001-1.432-0.558-2.781-1.574-3.796l-2.297-2.298 -2.009 2.009 2.297 2.298c0.478 0.478 0.741 1.113 0.741 1.788 0 0.674-0.264 1.309-0.741 1.786l-7.553 7.555c-0.987 0.985-2.59 0.985-3.576 0l-2.297-2.298c-0.987-0.985-0.987-2.589 0-3.576l3.775-3.776 -2.009-2.009 -3.776 3.776c-2.094 2.096-2.092 5.502 0 7.595l2.299 2.3zM26.695 2.992" />
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@ -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"
|
||||
>
|
||||
<title id="title">Unlink</title>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
119
platform/ui/src/components/Dropdown/Dropdown.jsx
Normal file
119
platform/ui/src/components/Dropdown/Dropdown.jsx
Normal file
@ -0,0 +1,119 @@
|
||||
import React, { useEffect, useCallback, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Icon, Typography } from '@ohif/ui';
|
||||
|
||||
const Dropdown = ({ children, showDropdownIcon, list }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const element = useRef(null);
|
||||
|
||||
const DropdownItem = useCallback(({ title, icon, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={classnames(
|
||||
'flex px-4 py-2 cursor-pointer items-center transition duration-300 hover:bg-secondary-main border-b last:border-b-0 border-secondary-main'
|
||||
)}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{!!icon && <Icon name={icon} className="text-white w-4 mr-2" />}
|
||||
<Typography>{title}</Typography>
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
||||
DropdownItem.defaultProps = {
|
||||
icon: '',
|
||||
};
|
||||
|
||||
DropdownItem.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const renderTitleElement = () => {
|
||||
return (
|
||||
<div className="flex text-white items-center">
|
||||
{children}
|
||||
{showDropdownIcon && (
|
||||
<Icon name="chevron-down" className="text-white ml-1" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleList = () => {
|
||||
setOpen(s => !s);
|
||||
};
|
||||
|
||||
const handleClick = e => {
|
||||
if (element.current && !element.current.contains(e.target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderList = () => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'absolute top-100 right-0 mt-2 z-10 origin-top-right transition duration-300 transform bg-primary-dark border border-secondary-main rounded shadow',
|
||||
{
|
||||
'scale-0': !open,
|
||||
'scale-100': open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{list.map((item, idx) => (
|
||||
<DropdownItem
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
onClick={item.onClick}
|
||||
key={idx}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClick);
|
||||
|
||||
if (!open) {
|
||||
document.removeEventListener('click', handleClick);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div data-cy="dropdown" ref={element} className="relative">
|
||||
<div className="cursor-pointer flex items-center" onClick={toggleList}>
|
||||
{renderTitleElement()}
|
||||
</div>
|
||||
|
||||
{renderList()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Dropdown.defaultProps = {
|
||||
showDropdownIcon: true,
|
||||
};
|
||||
|
||||
Dropdown.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
showDropdownIcon: PropTypes.bool,
|
||||
/** Items to render in the select's drop down */
|
||||
list: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
68
platform/ui/src/components/Dropdown/Dropdown.mdx
Normal file
68
platform/ui/src/components/Dropdown/Dropdown.mdx
Normal file
@ -0,0 +1,68 @@
|
||||
---
|
||||
name: Dropdown
|
||||
menu: General
|
||||
route: components/dropdown
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz';
|
||||
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
|
||||
|
||||
```javascript
|
||||
import { Dropdown } from '@ohif/ui';
|
||||
```
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
const handleClick = () => {
|
||||
alert("Clicked");
|
||||
}
|
||||
return (
|
||||
<div className="py-4">
|
||||
<Dropdown
|
||||
showDropdownIcon={false}
|
||||
list={[
|
||||
{
|
||||
title: 'Option 1',
|
||||
icon: 'info',
|
||||
onClick: handleClick,
|
||||
},
|
||||
{
|
||||
title: 'Option 2',
|
||||
icon: 'settings',
|
||||
onClick: handleClick,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="chevron-down" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={Dropdown} />
|
||||
1
platform/ui/src/components/Dropdown/index.js
Normal file
1
platform/ui/src/components/Dropdown/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Dropdown';
|
||||
@ -8,7 +8,7 @@ const NavBar = ({ className, children, isSticky }) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'flex flex-row items-center bg-secondary-dark px-3 py-1 border-b-4 border-black',
|
||||
'flex flex-row items-center bg-secondary-dark px-3 py-1 min-h-16 border-b-4 border-black',
|
||||
isSticky && stickyClasses,
|
||||
className
|
||||
)}
|
||||
|
||||
@ -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';
|
||||
@ -54,6 +55,7 @@ export {
|
||||
ButtonGroup,
|
||||
DateRange,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
EmptyStudies,
|
||||
ExpandableToolbarButton,
|
||||
ListMenu,
|
||||
|
||||
@ -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 (
|
||||
<AppConfigProvider value={appConfigState}>
|
||||
<Router basename={routerBasename}>
|
||||
<ThemeWrapper>
|
||||
<ViewportGridProvider
|
||||
initialState={{
|
||||
numRows: 1,
|
||||
numCols: 1,
|
||||
viewports: [],
|
||||
activeViewportIndex: 0,
|
||||
}}
|
||||
reducer={viewportGridReducer}
|
||||
>
|
||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<DialogProvider service={UIDialogService}>
|
||||
<ModalProvider modal={Modal} service={UIModalService}>
|
||||
{appRoutes}
|
||||
</ModalProvider>
|
||||
</DialogProvider>
|
||||
</SnackbarProvider>
|
||||
</ViewportDialogProvider>
|
||||
</ViewportGridProvider>
|
||||
</ThemeWrapper>
|
||||
</Router>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Router basename={routerBasename}>
|
||||
<ThemeWrapper>
|
||||
<ViewportGridProvider
|
||||
initialState={{
|
||||
numRows: 1,
|
||||
numCols: 1,
|
||||
viewports: [],
|
||||
activeViewportIndex: 0,
|
||||
}}
|
||||
reducer={viewportGridReducer}
|
||||
>
|
||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<DialogProvider service={UIDialogService}>
|
||||
<ModalProvider modal={Modal} service={UIModalService}>
|
||||
{appRoutes}
|
||||
</ModalProvider>
|
||||
</DialogProvider>
|
||||
</SnackbarProvider>
|
||||
</ViewportDialogProvider>
|
||||
</ViewportGridProvider>
|
||||
</ThemeWrapper>
|
||||
</Router>
|
||||
</I18nextProvider>
|
||||
</AppConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Dropdown, IconButton, Icon, useModal } from '@ohif/ui';
|
||||
|
||||
const PreferencesDropdown = () => {
|
||||
const { show } = useModal();
|
||||
|
||||
const showAboutModal = () => {
|
||||
const modalComponent = () => <div>About modal</div>;
|
||||
show({
|
||||
content: modalComponent,
|
||||
title: 'About',
|
||||
});
|
||||
};
|
||||
|
||||
const showPreferencesModal = () => {
|
||||
const modalComponent = () => <div>Preferences modal</div>;
|
||||
show({
|
||||
content: modalComponent,
|
||||
title: 'Preferences',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
showDropdownIcon={false}
|
||||
list={[
|
||||
{ title: 'About', icon: 'info', onClick: showAboutModal },
|
||||
{
|
||||
title: 'Preferences',
|
||||
icon: 'settings',
|
||||
onClick: showPreferencesModal,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Icon name="chevron-down" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesDropdown;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './PreferencesDropdown';
|
||||
@ -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,
|
||||
@ -34,7 +35,10 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
// ~ Filters
|
||||
const query = useQuery();
|
||||
const queryFilterValues = _getQueryFilterValues(query);
|
||||
const [filterValues, _setFilterValues] = useState({ ...defaultFilterValues, ...queryFilterValues });
|
||||
const [filterValues, _setFilterValues] = useState({
|
||||
...defaultFilterValues,
|
||||
...queryFilterValues,
|
||||
});
|
||||
const debouncedFilterValues = useDebounce(filterValues, 200);
|
||||
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
|
||||
|
||||
@ -211,8 +215,8 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
content: patientName ? (
|
||||
patientName
|
||||
) : (
|
||||
<span className="text-gray-700">(Empty)</span>
|
||||
),
|
||||
<span className="text-gray-700">(Empty)</span>
|
||||
),
|
||||
title: patientName,
|
||||
gridCol: 4,
|
||||
},
|
||||
@ -299,13 +303,13 @@ function WorkList({ history, data: studies, isLoadingData, 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 +326,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
<Link
|
||||
key={i}
|
||||
to={`${mode.id}?StudyInstanceUIDs=${studyInstanceUid}`}
|
||||
// to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
|
||||
// to={`${mode.id}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
|
||||
>
|
||||
<Button
|
||||
rounded="full"
|
||||
@ -364,17 +368,7 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
<span className="mr-3 text-lg text-common-light">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
className="text-primary-active"
|
||||
onClick={() => { }}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Icon name="settings" />
|
||||
<Icon name="chevron-down" />
|
||||
</React.Fragment>
|
||||
</IconButton>
|
||||
<PreferencesDropdown />
|
||||
</div>
|
||||
</NavBar>
|
||||
<StudyListFilter
|
||||
@ -403,10 +397,10 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
<EmptyStudies isLoading={isLoadingData} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
<EmptyStudies isLoading={isLoadingData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user