fix/remove legacy ui (#5010)

This commit is contained in:
Alireza 2025-05-02 14:05:40 -04:00 committed by GitHub
parent 1cb8d328a4
commit 4be8495335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
321 changed files with 24 additions and 18605 deletions

2
.gitignore vendored
View File

@ -58,3 +58,5 @@ tests/test-results/
tests/playwright-report/
/blob-report/
/playwright/.cache/
**/.claude/settings.local.json

View File

@ -1,4 +1,3 @@
import { ToolbarButton as ToolbarButtonLegacy } from '@ohif/ui';
import { ToolButton, utils } from '@ohif/ui-next';
import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
@ -33,28 +32,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
name: 'ohif.toolBoxButton',
defaultComponent: ToolBoxButtonWrapper,
},
// legacy will get removed in the future
// legacy will get removed in the future
// legacy will get removed in the future
// legacy will get removed in the future
// legacy will get removed in the future
// legacy will get removed in the future
{
name: 'ohif.radioGroup',
defaultComponent: ToolbarButtonLegacy,
},
{
name: 'ohif.buttonGroup',
defaultComponent: ToolbarButtonGroupWithServicesLegacy,
},
{
name: 'ohif.divider',
defaultComponent: ToolbarDividerLegacy,
},
{
name: 'ohif.splitButton',
defaultComponent: ToolbarSplitButtonWithServicesLegacy,
},
// others
{
name: 'ohif.layoutSelector',

View File

@ -18,8 +18,6 @@ import {
StudyListTable,
StudyListPagination,
StudyListFilter,
useSessionStorage,
InvestigationalUseDialog,
Button,
ButtonEnums,
} from '@ohif/ui';
@ -32,8 +30,10 @@ import {
TooltipContent,
Clipboard,
useModal,
useSessionStorage,
Onboarding,
ScrollArea,
InvestigationalUseDialog,
} from '@ohif/ui-next';
import { Types } from '@ohif/ui';
@ -60,7 +60,6 @@ function WorkList({
onRefresh,
servicesManager,
}: withAppTypes) {
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
const { show, hide } = useModal();
const { t } = useTranslation();
// ~ Modes
@ -426,7 +425,7 @@ function WorkList({
}}
// to={`${mode.routeName}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`}
>
{/* TODO revisit the completely rounded style of buttons used for launching a mode from the worklist later - for now use LegacyButton*/}
{/* TODO revisit the completely rounded style of buttons used for launching a mode from the worklist later */}
<Button
type={ButtonEnums.type.primary}
size={ButtonEnums.size.medium}

View File

@ -0,0 +1,4 @@
import useSessionStorage from './useSessionStorage';
import useDynamicMaxHeight from './useDynamicMaxHeight';
export { useSessionStorage, useDynamicMaxHeight };

View File

@ -48,3 +48,5 @@ export function useDynamicMaxHeight(
return { ref, maxHeight };
}
export default useDynamicMaxHeight;

View File

@ -4,3 +4,4 @@ export * from './components';
export * from './contextProviders';
export * as Types from './types';
export { utils, cn };
export { useSessionStorage, useDynamicMaxHeight } from './hooks';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,150 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import detect from 'browser-detect';
import { useTranslation } from 'react-i18next';
import Typography from '../Typography';
import { Icons } from '@ohif/ui-next';
const Link = ({ href, children, showIcon = false }) => {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
>
<Typography
variant="subtitle"
component="p"
color="primaryActive"
className="flex items-center"
>
{children}
{!!showIcon && <Icons.ExternalLink className="ml-2 w-5 text-white" />}
</Typography>
</a>
);
};
const Row = ({ title, value, link }) => {
return (
<div className="mb-4 flex">
<Typography
variant="subtitle"
component="p"
className="w-48 text-white"
>
{title}
</Typography>
{link ? (
<Link href={link}>{value}</Link>
) : (
<Typography
variant="subtitle"
component="p"
className="w-48 text-white"
>
{value}
</Typography>
)}
</div>
);
};
const AboutModal = ({ buildNumber, versionNumber, commitHash }) => {
const { os, version, name } = detect();
const browser = `${name[0].toUpperCase()}${name.substr(1)} ${version}`;
const { t } = useTranslation('AboutModal');
const renderRowTitle = title => (
<div className="mb-3 border-b-2 border-black pb-3">
<Typography
variant="inherit"
color="primaryLight"
className="text-[16px] font-semibold !leading-[1.2]"
>
{title}
</Typography>
</div>
);
return (
<div>
{renderRowTitle(t('Important links'))}
<div className="mb-8 flex">
<Link
href="https://community.ohif.org/"
showIcon={true}
>
{t('Visit the forum')}
</Link>
<span className="ml-4">
<Link
href="https://github.com/OHIF/Viewers/issues/new/choose"
showIcon={true}
>
{t('Report an issue')}
</Link>
</span>
<span className="ml-4">
<Link
href="https://ohif.org/"
showIcon={true}
>
{t('More details')}
</Link>
</span>
</div>
{renderRowTitle(t('Version information'))}
<div className="flex flex-col">
<Row
title={t('Repository URL')}
value="https://github.com/OHIF/Viewers/"
link="https://github.com/OHIF/Viewers/"
/>
<Row
title={t('Data citation')}
value="https://github.com/OHIF/Viewers/blob/master/DATACITATION.md"
link="https://github.com/OHIF/Viewers/blob/master/DATACITATION.md"
/>
{/* <Row
title={t('Last master commits')}
value="https://github.com/OHIF/Viewers/"
link="https://github.com/OHIF/Viewers/"
/> */}
<Row
title={t('Version number')}
value={versionNumber}
/>
{buildNumber && (
<Row
title={t('Build number')}
value={buildNumber}
/>
)}
{commitHash && (
<Row
title={t('Commit hash')}
value={commitHash}
/>
)}
<Row
title={t('Browser')}
value={browser}
/>
<Row
title={t('OS')}
value={os}
/>
</div>
</div>
);
};
AboutModal.propTypes = {
buildNumber: PropTypes.string,
versionNumber: PropTypes.string,
};
export default AboutModal;

View File

@ -1,50 +0,0 @@
import AboutModal from '../../AboutModal';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
createStoryMetaSettings,
} from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: AboutModal,
title: 'Modals/About',
};
<Meta
title="Modals/About"
component={AboutModal}
/>
export const aboutTemplate = args => (
<div className="bg-primary-dark">
<AboutModal {...args} />
</div>
);
<Heading
title="About Modal"
componentRelativePath="AboutModal/AboutModal.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
OHIF about modal component provides information about the application version and build number
<Canvas>
<Story name="Overview">{aboutTemplate.bind({})}</Story>
</Canvas>
## Props
<ArgsTable of={AboutModal} />
## Usage
## Contribute
<Footer componentRelativePath="AboutModal/__stories__/aboutModal.stories.mdx" />

View File

@ -1,2 +0,0 @@
import AboutModal from './AboutModal';
export default AboutModal;

View File

@ -1,36 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonEnums } from '../../components';
function ActionButtons({ actions, disabled = false, t }) {
return (
<React.Fragment>
{actions.map((action, index) => (
<Button
key={index}
onClick={action.onClick}
disabled={disabled || action.disabled}
type={ButtonEnums.type.secondary}
size={ButtonEnums.size.small}
className={index > 0 ? 'ml-2' : ''}
>
{t ? t(action.label) : action.label}
</Button>
))}
</React.Fragment>
);
}
ActionButtons.propTypes = {
actions: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool,
})
).isRequired,
disabled: PropTypes.bool,
};
export default ActionButtons;

View File

@ -1,3 +0,0 @@
import ActionButtons from './ActionButtons';
export default ActionButtons;

View File

@ -1,72 +0,0 @@
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import { PanelSection, Tooltip } from '../../components';
import ToolSettings from './ToolSettings';
import { Icons } from '@ohif/ui-next';
/**
* Use Toolbox component instead of this although it doesn't have "Advanced" in its name
* it is better to use it instead of this one
*/
const AdvancedToolbox = ({ title, items }) => {
const [activeItemName, setActiveItemName] = useState(null);
useEffect(() => {
// see if any of the items are active from the outside
const activeItem = items?.find(item => item.active);
setActiveItemName(activeItem ? activeItem.name : null);
}, [items]);
const activeItemOptions = items?.find(item => item.name === activeItemName)?.options;
return (
<PanelSection
title={title}
childrenClassName="flex-shrink-0"
>
<div className="flex flex-col bg-black">
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
{items?.map(item => {
return (
<Tooltip
position="bottom"
content={<span className="text-white">{item.name}</span>}
key={item.name}
>
<div
className="ml-2 mb-2"
onClick={() => {
if (item.disabled) {
return;
}
setActiveItemName(item.name);
item.onClick(item.name);
}}
>
<div
className={classnames(
'text-primary-active grid h-[40px] w-[40px] place-items-center rounded-md bg-black',
activeItemName === item.name && 'bg-primary-light text-black',
item.disabled && 'opacity-50',
!item.disabled &&
'hover:bg-primary-light cursor-pointer hover:cursor-pointer hover:text-black'
)}
>
<Icons.ByName name={item.icon} />
</div>
</div>
</Tooltip>
);
})}
</div>
<div className="bg-primary-dark h-auto px-2">
<ToolSettings options={activeItemOptions} />
</div>
</div>
</PanelSection>
);
};
AdvancedToolbox.propTypes = {};
export default AdvancedToolbox;

View File

@ -1,128 +0,0 @@
import React from 'react';
import { ButtonGroup, InputDoubleRange, InputRange } from '../../components';
const SETTING_TYPES = {
RANGE: 'range',
RADIO: 'radio',
CUSTOM: 'custom',
DOUBLE_RANGE: 'double-range',
};
function ToolSettings({ options }) {
if (!options) {
return null;
}
if (typeof options === 'function') {
return options();
}
return (
<div className="space-y-2 py-2 text-white">
{options?.map(option => {
if (option.condition && option.condition?.({ options }) === false) {
return null;
}
switch (option.type) {
case SETTING_TYPES.RANGE:
return renderRangeSetting(option);
case SETTING_TYPES.RADIO:
return renderRadioSetting(option);
case SETTING_TYPES.DOUBLE_RANGE:
return renderDoubleRangeSetting(option);
case SETTING_TYPES.CUSTOM:
return renderCustomSetting(option);
default:
return null;
}
})}
</div>
);
}
const renderRangeSetting = option => {
return (
<div
className="flex items-center"
key={option.id}
>
<div className="w-1/3 text-[13px]">{option.name}</div>
<div className="w-2/3">
<InputRange
minValue={option.min}
maxValue={option.max}
step={option.step}
value={option.value}
onChange={value => option.commands?.(value)}
allowNumberEdit={true}
showAdjustmentArrows={false}
inputClassName="ml-1 w-4/5 cursor-pointer"
/>
</div>
</div>
);
};
const renderRadioSetting = option => {
const renderButtons = option => {
return option.values?.map(({ label, value: optionValue }, index) => (
<button
onClick={() => {
option.commands?.(optionValue);
}}
key={`button-${option.id}-${index}`}
>
{label}
</button>
));
};
return (
<div
className="flex items-center justify-between text-[13px]"
key={option.id}
>
<span>{option.name}</span>
<div className="max-w-1/2">
<ButtonGroup
className="border-secondary-light rounded-md border"
activeIndex={option.values.findIndex(({ value }) => value === option.value) || 0}
>
{renderButtons(option)}
</ButtonGroup>
</div>
</div>
);
};
const renderDoubleRangeSetting = option => {
return (
<div
className="flex w-full items-center"
key={option.id}
>
<InputDoubleRange
values={option.value}
onChange={option.commands}
minValue={option.min}
maxValue={option.max}
step={option.step}
showLabel={true}
allowNumberEdit={true}
showAdjustmentArrows={false}
containerClassName="w-full"
/>
</div>
);
};
const renderCustomSetting = option => {
return (
<div key={option.id}>
{typeof option.children === 'function' ? option.children() : option.children}
</div>
);
};
export default ToolSettings;

View File

@ -1,144 +0,0 @@
import AdvancedToolbox from '../../AdvancedToolbox';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
createStoryMetaSettings,
} from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: AdvancedToolbox,
title: 'Components/AdvancedToolbox',
};
<Meta
title="Components/AdvancedToolbox"
component={AdvancedToolbox}
/>
export const advancedToolboxTemplate = args => (
<div className="bg-primary-dark h-96 w-64">
<AdvancedToolbox {...args} />
</div>
);
<Heading
title="Advanced Toolbox"
componentRelativePath="AdvancedToolbox/AdvancedToolbox.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
OHIF advanced toolbox which can host set of tools that require more space for customization.
<Canvas>
<Story
name="Overview"
args={{
title: 'Segmentation Tools',
items: [
{
name: 'Brush',
icon: 'icon-tool-brush',
active: false,
onClick: () => console.log('Brush clicked'),
options: [
{
name: 'Radius (mm)',
type: 'range',
min: 1,
max: 10,
value: 5,
step: 1,
onChange: value => console.log('Brush size changed', value),
},
{
name: 'Mode',
type: 'radio',
value: 'Circle',
values: [
{ value: 'Circle', label: 'Circle' },
{ value: 'Sphere', label: 'Sphere' },
{ value: 'Rectangle', label: 'Rectangle' },
],
onChange: value => console.log('Brush mode changed', value),
},
],
},
{
name: 'Eraser',
icon: 'icon-tool-eraser',
onClick: () => console.log('eraser clicked'),
options: [
{
name: 'Mode',
type: 'radio',
value: 'EraserSphere',
values: [
{ value: 'EraserCircle', label: 'Circle' },
{ value: 'EraserSphere', label: 'Sphere' },
],
onChange: value => console.log('Brush mode changed', value),
},
],
},
{
name: 'Threshold',
icon: 'icon-tool-threshold',
active: true,
onClick: () => console.log('eraser clicked'),
options: [
{
name: 'Radius (mm)',
type: 'range',
min: 1,
max: 10,
value: 5,
step: 1,
onChange: value => console.log('Brush size changed', value),
},
{
name: 'Mode',
type: 'radio',
value: 'Circle',
values: [
{ value: 'Circle', label: 'Circle' },
{ value: 'Sphere', label: 'Sphere' },
{ value: 'Rectangle', label: 'Rectangle' },
],
onChange: value => console.log('Brush mode changed', value),
},
{
name: 'custom',
type: 'custom',
children: () => {
return (
<div>
<div>Custom</div>
<input type="text" />
</div>
);
},
},
],
},
],
}}
>
{advancedToolboxTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={AdvancedToolbox} />
## Usage
## Contribute
<Footer componentRelativePath="AdvancedToolbox/__stories__/AdvancedToolbox.stories.mdx" />

View File

@ -1,4 +0,0 @@
import AdvancedToolbox from './AdvancedToolbox';
import ToolSettings from './ToolSettings';
export default AdvancedToolbox;
export { ToolSettings };

View File

@ -1,27 +0,0 @@
import React from 'react';
import { Icons } from '@ohif/ui-next';
import DividerItem from './DividerItem';
type BackItemProps = {
backLabel?: string;
onBackClick: () => void;
};
const BackItem = ({ backLabel, onBackClick }: BackItemProps) => {
return (
<>
<div
className="all-in-one-menu-item all-in-one-menu-item-effects"
onClick={onBackClick}
>
<Icons.ByName name="content-prev"></Icons.ByName>
<div className="pl-2">{backLabel || 'Back to Display Options'}</div>
</div>
<DividerItem></DividerItem>
</>
);
};
export default BackItem;

View File

@ -1,11 +0,0 @@
import React from 'react';
const DividerItem = () => {
return (
<div className="flex h-3.5 shrink-0 items-center px-0">
<div className="bg-background h-px w-full"></div>
</div>
);
};
export default DividerItem;

View File

@ -1,13 +0,0 @@
import React, { ReactNode } from 'react';
type HeaderItemProps = {
children: ReactNode;
};
const HeaderItem = ({ children }: HeaderItemProps) => {
return (
<div className="text-aqua-pale mx-2 flex h-6 shrink-0 items-center text-[11px]">{children}</div>
);
};
export default HeaderItem;

View File

@ -1,83 +0,0 @@
import React, { useCallback, useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import { MenuProps } from './Menu';
import classNames from 'classnames';
import { AllInOneMenu } from '..';
import { Icons } from '@ohif/ui-next';
export interface IconMenuProps extends MenuProps {
icon: string;
iconClassName?: string;
horizontalDirection?: AllInOneMenu.HorizontalDirection;
verticalDirection?: AllInOneMenu.VerticalDirection;
menuKey?: number | string;
}
/**
* An IconMenu allows for a div wrapped icon to be clicked to show and hide
* an AllInOneMenu.Menu. Based on the direction(s) specified, the menu is
* positioned relative to the icon.
*
* HorizontalDirection.LeftToRight - the left edges of the icon and menu are aligned
* HorizontalDirection.RightRoLeft - the right edges of the icon and menu are aligned
* VerticalDirection.TopToBottom - the top edge of the menu appears directly below the bottom edge of the icon
* VerticalDirection.BottomToTop - the bottom edge of the menu appears directly above the top edge of the icon
*
* For example, if an IconMenu were situated in the bottom-left corner of a container,
* it would be best to use BottomToTop and LeftToRight directions for it.
*/
export default function IconMenu({
icon,
iconClassName,
horizontalDirection,
verticalDirection,
children,
backLabel,
menuClassName,
menuStyle,
onVisibilityChange,
menuKey,
}: IconMenuProps) {
const [isMenuVisible, setIsMenuVisible] = useState(false);
const toggleMenuVisibility = useCallback(() => setIsMenuVisible(isVisible => !isVisible), []);
return (
<OutsideClickHandler
onOutsideClick={toggleMenuVisibility}
disabled={!isMenuVisible}
>
<div className="relative">
<div
className={iconClassName}
onClick={toggleMenuVisibility}
>
<Icons.ByName name={icon} />
</div>
<AllInOneMenu.Menu
key={menuKey}
isVisible={isMenuVisible}
backLabel={backLabel}
menuClassName={classNames(
menuClassName,
'absolute',
verticalDirection === AllInOneMenu.VerticalDirection.TopToBottom
? 'top-[100%]'
: 'bottom-[100%]',
horizontalDirection === AllInOneMenu.HorizontalDirection.LeftToRight
? 'left-0'
: 'right-0'
)}
menuStyle={menuStyle}
onVisibilityChange={isVis => {
setIsMenuVisible(isVis);
onVisibilityChange?.(isVis);
}}
horizontalDirection={horizontalDirection}
>
{children}
</AllInOneMenu.Menu>
</div>
</OutsideClickHandler>
);
}

View File

@ -1,45 +0,0 @@
import React, { ReactNode, useCallback, useContext } from 'react';
import { MenuContext } from './Menu';
type ItemProps = {
label: string;
secondaryLabel?: string;
icon?: ReactNode;
onClick?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
rightIcon?: ReactNode;
};
const Item = ({
label,
secondaryLabel,
icon,
rightIcon,
onClick,
onMouseEnter,
onMouseLeave,
}: ItemProps) => {
const { hideMenu } = useContext(MenuContext);
const onClickHandler = useCallback(() => {
hideMenu();
onClick?.();
}, [hideMenu, onClick]);
return (
<div
className="all-in-one-menu-item all-in-one-menu-item-effects"
onClick={onClickHandler}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{icon && <div className="pr-2">{icon}</div>}
<span>{label}</span>
{secondaryLabel != null && <span className="text-aqua-pale ml-[1ch]">{secondaryLabel}</span>}
{rightIcon && <div className="ml-auto">{rightIcon}</div>}
</div>
);
};
export default Item;

View File

@ -1,29 +0,0 @@
import React, { ReactNode, useContext, useEffect } from 'react';
import { MenuContext } from './Menu';
type ItemPanelProps = {
label?: string;
index?: number;
children: ReactNode;
};
const ItemPanel = ({ label, index = 0, children }: ItemPanelProps) => {
const { addItemPanel, activePanelIndex } = useContext(MenuContext);
useEffect(() => {
addItemPanel(index, label);
}, []);
return (
activePanelIndex === index && (
<div
style={{ scrollbarGutter: 'auto' }}
className="ohif-scrollbar flex flex-col overflow-auto"
>
{children}
</div>
)
);
};
export default ItemPanel;

View File

@ -1,188 +0,0 @@
import React, { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
import './allInOneMenu.css';
import DividerItem from './DividerItem';
import PanelSelector from './PanelSelector';
import classNames from 'classnames';
import BackItem from './BackItem';
/**
* The vertical direction that the menu will be opened/used with.
*
* A TopToBottom menu would be used for cases where the menu is opened "near"
* the top edge of its container. Likewise a BottomToTop menu would be used
* for cases where the menu is opened "near" the bottom edge of its container.
*
* See IconMenu for more information.
*/
export enum VerticalDirection {
TopToBottom,
BottomToTop,
}
/**
* The horizontal direction that the menu is opened/used with.
* This direction dictates the general direction sub-menus and
* back-to-menus are opened with. For example, a RightToLeft menu
* will have sub-menu items indicated with a left pointing chevron
* and aligned with the left edge of the menu. Similarly back-to items of a
* RightToLeft menu are indicated with a right pointing chevron and
* aligned with the right edge of the menu.
*
* It is also worth noting that a LeftToRight menu would be used for
* cases where a menu is opened "near" the left edge of its container.
* Likewise, a RightToLeft menu would be used for cases where a menu is opened
* "near" the right edge of its container.
*
* See IconMenu for more information.
*/
export enum HorizontalDirection {
LeftToRight,
RightToLeft,
}
export interface MenuProps {
menuStyle?: unknown;
menuClassName?: string;
isVisible?: boolean;
preventHideMenu?: boolean;
backLabel?: string;
headerComponent?: ReactNode;
showHeaderDivider?: boolean;
activePanelIndex?: number;
onVisibilityChange?: (isVisible: boolean) => void;
horizontalDirection?: HorizontalDirection;
children: ReactNode;
}
type MenuContextProps = {
showSubMenu: (subMenuProps: MenuProps) => void;
hideMenu: () => void;
addItemPanel: (index: number, label: string) => void;
horizontalDirection: HorizontalDirection;
activePanelIndex: number;
};
type MenuPathState = {
props: MenuProps;
activePanelIndex: number;
};
export const MenuContext = createContext<MenuContextProps>(null);
const Menu = (props: MenuProps) => {
const {
isVisible,
onVisibilityChange,
activePanelIndex,
preventHideMenu,
menuClassName,
menuStyle,
horizontalDirection = HorizontalDirection.LeftToRight,
} = props;
const [isMenuVisible, setIsMenuVisible] = useState(isVisible);
// The menuPath is an array consisting of this top Menu and every SubMenu
// that has been traversed/opened by the user with the last item in the array
// being the current (sub)menu that is currently visible. This allows for the previously
// viewed menus to be returned to via the Back button at the top of the menu.
const [menuPath, setMenuPath] = useState<Array<MenuPathState>>([
{ props, activePanelIndex: activePanelIndex || 0 },
]);
const [itemPanelLabels, setItemPanelLabels] = useState<Array<string>>([]);
const hideMenu = useCallback(() => {
if (preventHideMenu) {
return;
}
setMenuPath(path => [path[0]]);
setItemPanelLabels([]);
setIsMenuVisible(false);
onVisibilityChange?.(false);
}, [preventHideMenu, onVisibilityChange]);
useEffect(() => {
if (isVisible) {
setIsMenuVisible(isVisible);
onVisibilityChange?.(isVisible);
} else {
hideMenu();
}
}, [hideMenu, isVisible, onVisibilityChange]);
const showSubMenu = useCallback((subMenuProps: MenuProps) => {
setMenuPath(path => {
return [
...path,
{ props: subMenuProps, activePanelIndex: subMenuProps.activePanelIndex || 0 },
];
});
setItemPanelLabels([]);
}, []);
const addItemPanel = useCallback((index, label) => {
setItemPanelLabels(labels => {
return [...labels.slice(0, index), label, ...labels.slice(index + 1, labels.length)];
});
}, []);
const onActivePanelIndexChange = useCallback(index => {
setMenuPath(path => {
return [
...path.slice(0, path.length - 1),
{ ...path[path.length - 1], activePanelIndex: index },
];
});
}, []);
const onBackClick = useCallback(() => {
setMenuPath(path => [...path.slice(0, path.length - 1)]);
setItemPanelLabels([]);
}, []);
const { props: currentMenuProps, activePanelIndex: currentMenuActivePanelIndex } =
menuPath[menuPath.length - 1];
return (
<>
<MenuContext.Provider
value={{
showSubMenu,
hideMenu,
addItemPanel,
activePanelIndex: currentMenuActivePanelIndex,
horizontalDirection,
}}
>
{isMenuVisible && (
<div
className={classNames(
'bg-secondary-dark flex select-none flex-col rounded px-1 py-1.5 text-white opacity-90',
menuClassName
)}
style={menuStyle}
>
{menuPath.length > 1 && (
<BackItem
backLabel={menuPath[menuPath.length - 2].props.backLabel}
onBackClick={onBackClick}
/>
)}
{itemPanelLabels.length > 1 && (
<PanelSelector
panelLabels={itemPanelLabels}
activeIndex={currentMenuActivePanelIndex}
onActiveIndexChange={onActivePanelIndexChange}
></PanelSelector>
)}
{currentMenuProps.headerComponent}
{currentMenuProps.showHeaderDivider && <DividerItem />}
{currentMenuProps.children}
</div>
)}
</MenuContext.Provider>
</>
);
};
export default Menu;

View File

@ -1,31 +0,0 @@
import React, { ReactNode } from 'react';
import { ButtonGroup } from '../../components';
type PanelSelectorProps = {
panelLabels: Array<ReactNode>;
onActiveIndexChange: (index: number) => void;
activeIndex: number;
};
const PanelSelector = ({ panelLabels, onActiveIndexChange, activeIndex }: PanelSelectorProps) => {
const getButtons = () => {
return panelLabels.map((panelLabel, index) => {
return {
children: panelLabel,
key: index,
};
});
};
return (
<div className="mx-2 my-1 flex justify-center">
<ButtonGroup
buttons={getButtons()}
onActiveIndexChange={onActiveIndexChange}
defaultActiveIndex={activeIndex}
></ButtonGroup>
</div>
);
};
export default PanelSelector;

View File

@ -1,35 +0,0 @@
import React, { useCallback, useContext } from 'react';
import { MenuContext, MenuProps } from './Menu';
import { Icons } from '@ohif/ui-next';
export interface SubMenuProps extends MenuProps {
itemLabel: string;
onClick?: () => void;
itemIcon?: string;
}
const SubMenu = (props: SubMenuProps) => {
const { showSubMenu } = useContext(MenuContext);
const onClickHandler = useCallback(() => {
showSubMenu(props);
props.onClick?.();
}, [showSubMenu, props]);
return (
<div
className="all-in-one-menu-item all-in-one-menu-item-effects flex items-center"
onClick={onClickHandler}
>
{props.itemIcon && (
<Icons.ByName
name={props.itemIcon}
className="mr-2"
></Icons.ByName>
)}
<div className="mr-auto">{props.itemLabel}</div>
<Icons.ByName name="content-next"></Icons.ByName>
</div>
);
};
export default SubMenu;

View File

@ -1,129 +0,0 @@
import { DividerItem, HeaderItem, Item, ItemPanel, Menu, SubMenu } from '..';
import InputRange from '../../InputRange/index.js';
import SwitchButton from '../../SwitchButton/index.js';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: Menu,
title: 'Components/AllInOneMenu',
};
<Meta
title="Components/AllInOneMenu"
component={Menu}
/>
export const AllInOneMenuTemplate = args => (
<div className="w-80">
<Menu {...args}>
<ItemPanel>
<Item
label="Item 1"
key="0"
onClick={() => console.info('Item 1 clicked.')}
></Item>
<Item
label="Item 2"
secondaryLabel="Alt item 2"
key="1"
></Item>
<div
className="all-in-one-menu-item py-1"
style={{ flexBasis: 'content' }}
>
<div>Arbitrary item component:</div>
<InputRange
minValue={1}
maxValue={10}
value={5}
onChange={() => {}}
></InputRange>
</div>
<DividerItem key="2"></DividerItem>
<SubMenu
key="3"
backLabel="Back to Level 2"
itemLabel="Item 3 opens a sub menu"
onClick={() => console.info('Sub menu item clicked.')}
showHeaderDivider={true}
headerComponent={
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton label="Switch item in header" />
</div>
}
>
<ItemPanel
index={0}
label="Panel A"
>
<Item label="Panel A item"></Item>
</ItemPanel>
<ItemPanel
index={1}
label="Panel B"
>
<Item label="Panel B item"></Item>
<SubMenu
itemLabel="Opens a sub, sub menu"
headerComponent={<HeaderItem>Header for scrolling list of items</HeaderItem>}
>
<ItemPanel label="Sub sub Panel">
<Item label="Sub sub menu item 1"></Item>
<Item label="Sub sub menu item 2"></Item>
<Item label="Sub sub menu item 3"></Item>
<Item label="Sub sub menu item 4"></Item>
<Item label="Sub sub menu item 5"></Item>
<Item label="Sub sub menu item 6"></Item>
</ItemPanel>
</SubMenu>
</ItemPanel>
</SubMenu>
</ItemPanel>
</Menu>
</div>
);
<Heading
title="AllInOneMenu"
componentRelativePath="AllInOneMenu/AllInOneMenu.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
AllInOneMenu is a component that renders a menu with various menu items, sub-menus and
sub-components. The particular feature of the AllInOneMenu is its ability to render a sub-menu by
replacing the parent menu on screen. It also provides the ability to return to the parent menu with
a back menu item from the sub-menu. Furthermore, each menu level can be split into item panes - that
is several panes of menu items that are switched in and out of view like a tabbed pane.
<Canvas>
<Story
name="Overview"
args={{
className: 'max-h-[210px]',
isVisible: true,
preventHideMenu: true,
backLabel: 'Back to Level 1',
onVisibilityChange: (isVisible) => console.info(`The menu visibility: ${isVisible}`),
}}
>
{AllInOneMenuTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={Menu} />
## Usage
## Contribute
<Footer componentRelativePath="AllInOneMenu/__stories__/AllInOneMenu.stories.mdx" />

View File

@ -1,11 +0,0 @@
.all-in-one-menu-item {
@apply h-8 px-2 text-[14px] w-full;
display: flex;
align-items: center;
flex-shrink: 0;
line-height: 18px;
}
.all-in-one-menu-item-effects {
@apply cursor-pointer hover:bg-primary-dark hover:rounded;
}

View File

@ -1,19 +0,0 @@
import Menu, { HorizontalDirection, VerticalDirection } from './Menu';
import DividerItem from './DividerItem';
import HeaderItem from './HeaderItem';
import IconMenu from './IconMenu';
import Item from './Item';
import ItemPanel from './ItemPanel';
import SubMenu from './SubMenu';
export {
Menu,
DividerItem,
HeaderItem,
IconMenu,
Item,
ItemPanel,
SubMenu,
HorizontalDirection,
VerticalDirection,
};

View File

@ -1,59 +0,0 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Typography } from '../../';
import { Icons } from '@ohif/ui-next';
/**
* REACT CheckBox component
* it has two props, checked and onChange
* checked is a boolean value
* onChange is a function that will be called when the checkbox is clicked
*
* CheckBox is a component that allows you to use as a boolean
*/
const CheckBox: React.FC<{
checked: boolean;
onChange: (state) => void;
className?: string;
label: string;
labelClassName?: string;
labelVariant?: string;
}> = ({ checked, onChange, label, labelClassName, labelVariant = 'body', className }) => {
const [isChecked, setIsChecked] = useState(checked);
const handleClick = useCallback(() => {
setIsChecked(!isChecked);
onChange(!isChecked);
}, [isChecked, onChange]);
return (
<div
className={`flex cursor-pointer items-center space-x-1 ${className ? className : ''}`}
onClick={handleClick}
>
{isChecked ? (
<Icons.ByName name="checkbox-checked" />
) : (
<Icons.ByName name="checkbox-unchecked" />
)}
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={labelClassName ?? 'text-white'}
>
{label}
</Typography>
</div>
);
};
CheckBox.propTypes = {
checked: PropTypes.bool,
onChange: PropTypes.func,
label: PropTypes.string,
labelClassName: PropTypes.string,
labelVariant: PropTypes.string,
};
export default CheckBox;

View File

@ -1,49 +0,0 @@
import CheckBox from '../CheckBox';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: CheckBox,
title: 'Components/CheckBox',
};
<Meta
title="Components/CheckBox"
component={CheckBox}
/>
export const CheckBoxTemplate = args => <CheckBox {...args} />;
<Heading
title="CheckBox"
componentRelativePath="CheckBox/CheckBox.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
CheckBox is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
checked: false,
onChange: () => console.log('on change callback'),
label: 'checkBoxLabel',
labelClassName: 'text-black text-sm',
}}
>
{CheckBoxTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={CheckBox} />
## Contribute
<Footer componentRelativePath="CheckBox/__stories__/CheckBox.stories.mdx" />

View File

@ -1,3 +0,0 @@
import CheckBox from './CheckBox';
export default CheckBox;

View File

@ -1,3 +0,0 @@
.cine-fps-range-tooltip .tooltip.tooltip-top {
bottom: 85% !important;
}

View File

@ -1,189 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import Tooltip from '../Tooltip';
import InputRange from '../InputRange';
import { Icons } from '@ohif/ui-next';
import './CinePlayer.css';
export type CinePlayerProps = {
className: string;
isPlaying: boolean;
minFrameRate?: number;
maxFrameRate?: number;
stepFrameRate?: number;
frameRate?: number;
onFrameRateChange: (value: number) => void;
onPlayPauseChange: (value: boolean) => void;
onClose: () => void;
updateDynamicInfo?: () => void;
dynamicInfo?: {
dimensionGroupNumber: number;
numDimensionGroups: number;
label?: string;
};
};
const fpsButtonClassNames =
'cursor-pointer text-primary-active active:text-primary-light hover:bg-customblue-300 w-4 flex items-center justify-center';
const CinePlayer: React.FC<CinePlayerProps> = ({
className,
isPlaying = false,
minFrameRate = 1,
maxFrameRate = 90,
stepFrameRate = 1,
frameRate: defaultFrameRate = 24,
onFrameRateChange = () => {},
onPlayPauseChange = () => {},
onClose = () => {},
dynamicInfo = {},
updateDynamicInfo,
}) => {
const isDynamic = !!dynamicInfo?.numDimensionGroups;
const [frameRate, setFrameRate] = useState(defaultFrameRate);
const debouncedSetFrameRate = useCallback(debounce(onFrameRateChange, 100), [onFrameRateChange]);
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
const handleSetFrameRate = (frameRate: number) => {
if (frameRate < minFrameRate || frameRate > maxFrameRate) {
return;
}
setFrameRate(frameRate);
debouncedSetFrameRate(frameRate);
};
useEffect(() => {
setFrameRate(defaultFrameRate);
}, [defaultFrameRate]);
const handleDimensionGroupNumberChange = useCallback(
(newGroupNumber: number) => {
if (isDynamic && dynamicInfo) {
// Here, you would update the component's state or context that controls the current time point index
// For demonstration, assuming a hypothetical function that updates the time point index
updateDynamicInfo({
...dynamicInfo,
dimensionGroupNumber: newGroupNumber,
});
}
},
[isDynamic, dynamicInfo]
);
return (
<div className={className}>
{isDynamic && dynamicInfo && (
<InputRange
value={dynamicInfo.dimensionGroupNumber}
onChange={handleDimensionGroupNumberChange}
minValue={1}
maxValue={dynamicInfo.numDimensionGroups}
step={1}
containerClassName="mb-3 w-full"
labelClassName="text-xs text-white"
leftColor="#3a3f99"
rightColor="#3a3f99"
trackHeight="4px"
thumbColor="#348cfd"
thumbColorOuter="#000000"
showLabel={false}
/>
)}
<div
className={
'border-secondary-light/60 bg-primary-dark inline-flex select-none items-center gap-2 rounded border px-2 py-2'
}
>
<Icons.ByName
name={getPlayPauseIconName()}
className="active:text-primary-light hover:bg-customblue-300 cursor-pointer text-white hover:rounded"
onClick={() => onPlayPauseChange(!isPlaying)}
data-cy={'cine-player-play-pause'}
/>
{isDynamic && dynamicInfo && (
<div className="min-w-16 max-w-44 flex flex-col text-white">
{/* Add Tailwind classes for monospace font and center alignment */}
<div className="text-[11px]">
<span className="w-2 text-white">{dynamicInfo.dimensionGroupNumber}</span>{' '}
<span className="text-aqua-pale">{`/${dynamicInfo.numDimensionGroups}`}</span>
</div>
<div className="text-aqua-pale text-xs">{dynamicInfo.label}</div>
</div>
)}
<div className="border-secondary-light ml-4 flex h-6 items-stretch gap-1 rounded border">
<div
className={`${fpsButtonClassNames} rounded-l`}
onClick={() => handleSetFrameRate(frameRate - 1)}
data-cy={'cine-player-left-arrow'}
>
<Icons.ChevronLeft />
</div>
<Tooltip
position="top"
className="group/fps cine-fps-range-tooltip"
tight={true}
content={
<InputRange
containerClassName="h-9 px-2"
inputClassName="w-40"
value={frameRate}
minValue={minFrameRate}
maxValue={maxFrameRate}
step={stepFrameRate}
onChange={handleSetFrameRate}
showLabel={false}
/>
}
>
<div className="flex items-center justify-center gap-1">
<div className="flex-shrink-0 text-center text-sm leading-[22px] text-white">
<span className="inline-block text-right">{`${frameRate} `}</span>
<span className="text-aqua-pale whitespace-nowrap text-xs">{' FPS'}</span>
</div>
</div>
</Tooltip>
<div
className={`${fpsButtonClassNames} rounded-r`}
onClick={() => handleSetFrameRate(frameRate + 1)}
data-cy={'cine-player-right-arrow'}
>
<Icons.ChevronRight />
</div>
</div>
<Icons.Close
className="text-primary-active active:text-primary-light hover:bg-customblue-300 cursor-pointer hover:rounded"
onClick={onClose}
data-cy={'cine-player-close'}
/>
</div>
</div>
);
};
CinePlayer.propTypes = {
/** Minimum value for range slider */
minFrameRate: PropTypes.number,
/** Maximum value for range slider */
maxFrameRate: PropTypes.number,
/** Increment range slider can "step" in either direction */
stepFrameRate: PropTypes.number,
frameRate: PropTypes.number,
/** 'true' if playing, 'false' if paused */
isPlaying: PropTypes.bool.isRequired,
onPlayPauseChange: PropTypes.func,
onFrameRateChange: PropTypes.func,
onClose: PropTypes.func,
isDynamic: PropTypes.bool,
dynamicInfo: PropTypes.shape({
dimensionGroupNumber: PropTypes.number,
numDimensionGroups: PropTypes.number,
label: PropTypes.string,
}),
};
export default CinePlayer;

View File

@ -1,57 +0,0 @@
import { CinePlayer } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: CinePlayer,
title: 'Components/CinePlayer',
};
<Meta
title="Components/CinePlayer"
component={CinePlayer}
/>
export const CinePlayerTemplate = args => (
<div className="relative h-96 w-96 bg-black">
<div className="absolute left-1/2 bottom-3 -translate-x-1/2">
<CinePlayer {...args} />
</div>
</div>
);
<Heading
title="CinePlayer"
componentRelativePath="CinePlayer/CinePlayer.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
CinePlayer is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
isDynamic: true,
dynamicInfo: {
timePointIndex: 5,
numTimePoints: 20,
label: 'TemporalPositionIdentifier',
},
}}
>
{CinePlayerTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={CinePlayer} />
## Contribute
<Footer componentRelativePath="CinePlayer/__stories__/CinePlayer.stories.mdx" />

View File

@ -1,2 +0,0 @@
import CinePlayer from './CinePlayer';
export default CinePlayer;

View File

@ -1,99 +0,0 @@
/** CONTAINER STYLES **/
.DateRangePickerInput {
@apply flex border-0 bg-transparent;
}
.DateRangePicker_picker {
@apply -mt-1;
}
/** INPUT DIV STYLES **/
.DateInput {
background: transparent;
@apply flex w-auto flex-1;
}
/** INPUT FIELD COMMON STYLES **/
.DateInput_input {
/* used data:image as background-image because svg import with relative url didn't work */
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="%236b6b6b" fill-rule="evenodd"><path d="M20 20h-4v-4h4v4zm-6-10h-4v4h4v-4zm6 0h-4v4h4v-4zm-12 6h-4v4h4v-4zm6 0h-4v4h4v-4zm-6-6h-4v4h4v-4zm16-8v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2z"/></g></svg>');
background-size: 14px;
background-position: 10px center;
@apply bg-no-repeat;
}
.DateInput_input {
@apply border-primary-main mt-2 w-full cursor-pointer appearance-none rounded border-t border-l border-r border-b border-solid bg-black py-2 px-3 pl-8 text-sm font-light leading-tight text-white shadow transition duration-300;
}
.DateInput_input:hover {
@apply border-gray-500;
}
.DateInput_input:focus {
@apply border-gray-500 outline-none;
}
/** FIRST INPUT STYLES **/
.DateInput:first-child .DateInput_input {
@apply rounded-r-none;
}
.DateInput:first-child .DateInput_input:hover,
.DateInput:first-child .DateInput_input:focus {
@apply relative z-10;
}
/** SECOND INPUT STYLES **/
.DateInput:last-child .DateInput_input {
@apply rounded-l-none;
margin-left: -1px;
}
/** ARROW STYLES **/
.DateRangePickerInput_arrow {
@apply hidden;
}
/* SELECT MONTH PICKER */
.DateRangePicker_select {
@apply text-secondary-active border-common-dark cursor-pointer appearance-none rounded border bg-white py-1 pl-2 pr-5 text-base; /* NEEDED FOR ARROW DOWN */
background-image: linear-gradient(45deg, transparent 50%, gray 50%),
linear-gradient(135deg, gray 50%, transparent 50%);
background-position:
calc(100% - 11px) 11px,
calc(100% - 6px) calc(11px);
background-size:
5px 5px,
5px 5px;
background-repeat: no-repeat;
}
/* CALENDAR DAYS */
.CalendarDay {
@apply rounded-full border-0;
}
.CalendarDay:hover,
.CalendarDay__selected,
.CalendarDay__selected:active,
.CalendarDay__selected:hover {
@apply bg-primary-main border-primary-main border-0 text-white;
}
.CalendarDay__blocked_out_of_range:hover {
@apply text-common-dark cursor-not-allowed border-0 bg-white;
}
.CalendarDay__selected_span {
@apply bg-primary-light border-0;
}
/* MONTH NAVIGATION BUTTONS */
.DayPickerNavigation_button__horizontalDefault,
.DayPickerNavigation_button__horizontalDefault:hover {
@apply border-common-dark text-common-dark;
top: 24px;
padding: 3px 9px;
}
.DayPickerNavigation_svg__horizontal {
@apply fill-current;
}

View File

@ -1,178 +0,0 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { useTranslation } from 'react-i18next';
/** REACT DATES */
import { DateRangePicker, isInclusivelyBeforeDay } from 'react-dates';
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import './DateRange.css';
const renderYearsOptions = () => {
const currentYear = moment().year();
const options = [];
for (let i = 0; i < 20; i++) {
const year = currentYear - i;
options.push(
<option
key={year}
value={year}
>
{year}
</option>
);
}
return options;
};
const DateRange = props => {
const { id = '', onChange, startDate = null, endDate = null } = props;
const [focusedInput, setFocusedInput] = useState(null);
const renderYearsOptionsCallback = useCallback(renderYearsOptions, []);
const { t } = useTranslation('DatePicker');
const today = moment();
const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month');
const studyDatePresets = [
{
text: t('Today'),
start: today,
end: today,
},
{
text: t('Last 7 days'),
start: lastWeek,
end: today,
},
{
text: t('Last 30 days'),
start: lastMonth,
end: today,
},
];
const renderDatePresets = () => {
return (
<div className="PresetDateRangePicker_panel flex justify-between">
{studyDatePresets.map(({ text, start, end }) => {
return (
<button
key={text}
type="button"
className={`bg-primary-main m-0 rounded border-0 py-2 px-3 text-base text-white transition duration-300 hover:opacity-80`}
onClick={() =>
onChange({
startDate: start ? start.format('YYYYMMDD') : undefined,
endDate: end ? end.format('YYYYMMDD') : undefined,
preset: true,
})
}
>
{text}
</button>
);
})}
</div>
);
};
const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => {
renderMonthElement.propTypes = {
month: PropTypes.object,
onMonthSelect: PropTypes.func,
onYearSelect: PropTypes.func,
};
const handleMonthChange = event => {
onMonthSelect(month, event.target.value);
};
const handleYearChange = event => {
onYearSelect(month, event.target.value);
};
const handleOnBlur = () => {};
return (
<div className="flex justify-center">
<div className="my-0 mx-1">
<select
className="DateRangePicker_select"
value={month.month()}
onChange={handleMonthChange}
onBlur={handleOnBlur}
>
{moment.months().map((label, value) => (
<option
key={value}
value={value}
>
{label}
</option>
))}
</select>
</div>
<div className="my-0 mx-1">
<select
className="DateRangePicker_select"
value={month.year()}
onChange={handleYearChange}
onBlur={handleOnBlur}
>
{renderYearsOptionsCallback()}
</select>
</div>
</div>
);
};
// Moment
const parsedStartDate = startDate ? moment(startDate, 'YYYYMMDD') : null;
const parsedEndDate = endDate ? moment(endDate, 'YYYYMMDD') : null;
return (
<DateRangePicker
/** REQUIRED */
startDate={parsedStartDate}
startDateId={`date-range-${id}-start-date`}
endDate={parsedEndDate}
endDateId={`date-range-${id}-end-date`}
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
onChange({
startDate: newStartDate ? newStartDate.format('YYYYMMDD') : undefined,
endDate: newEndDate ? newEndDate.format('YYYYMMDD') : undefined,
});
}}
focusedInput={focusedInput}
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
/** OPTIONAL */
renderCalendarInfo={renderDatePresets}
renderMonthElement={renderMonthElement}
startDatePlaceholderText={t('Start Date')}
endDatePlaceholderText={t('End Date')}
phrases={{
closeDatePicker: t('Close'),
clearDates: t('Clear dates'),
}}
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
hideKeyboardShortcutsPanel={true}
numberOfMonths={1}
showClearDates={false}
anchorDirection="left"
/>
);
};
DateRange.propTypes = {
id: PropTypes.string,
/** YYYYMMDD (19921022) */
startDate: PropTypes.string,
/** YYYYMMDD (19921022) */
endDate: PropTypes.string,
/** Callback that received { startDate: string(YYYYMMDD), endDate: string(YYYYMMDD)} */
onChange: PropTypes.func.isRequired,
};
export default DateRange;

View File

@ -1,52 +0,0 @@
import DateRange from '../DateRange';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: DateRange,
title: 'Components/DateRange',
};
<Meta
title="Components/DateRange"
component={DateRange}
/>
export const DateRangeTemplate = args => (
<div className="h-96">
<DateRange {...args} />
</div>
);
<Heading
title="DateRange"
componentRelativePath="DateRange/DateRange.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
DateRange is a component that allows you to select a range of dates.
<Canvas>
<Story
name="Overview"
args={{
id: 'date-range-1',
startDate: '1990-05-01',
endDate: '2022-01-01',
}}
>
{DateRangeTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={DateRange} />
## Contribute
<Footer componentRelativePath="DateRange/__stories__/dateRange.stories.mdx" />

View File

@ -1,3 +0,0 @@
import DateRange from './DateRange';
export default DateRange;

View File

@ -1,27 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Typography from '../Typography';
const Body = ({ text, className }) => {
const theme = 'bg-primary-dark';
return (
<div className={classNames('relative flex-auto', theme, className)}>
<Typography
variant="inherit"
color="initial"
className="text-[14px] !leading-[1.2]"
>
{text}
</Typography>
</div>
);
};
Body.propTypes = {
text: PropTypes.string,
className: PropTypes.string,
};
export default Body;

View File

@ -1,84 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Footer from './Footer';
import Body from './Body';
import Header from './Header';
import { useEffect } from 'react';
const Dialog = ({
title,
text,
onClose,
noCloseButton,
actions,
onShow,
onSubmit,
header: HeaderComponent = Header,
body: BodyComponent = Body,
footer: FooterComponent = Footer,
value: defaultValue = {},
}) => {
const [value, setValue] = useState(defaultValue);
const theme = 'bg-primary-dark';
const flex = 'flex flex-col';
const border = 'border-0 rounded';
const outline = 'outline-none focus:outline-none';
const position = 'relative';
const width = 'w-full';
const padding = 'px-[20px] pb-[20px] pt-[13px]';
useEffect(() => {
if (onShow) {
onShow();
}
}, [onShow]);
return (
<div className={classNames(theme, flex, border, outline, position, width, padding)}>
<HeaderComponent
title={title}
noCloseButton={noCloseButton}
onClose={onClose}
value={value}
setValue={setValue}
/>
<BodyComponent
text={text}
value={value}
setValue={setValue}
/>
<FooterComponent
actions={actions}
onSubmit={onSubmit}
value={value}
setValue={setValue}
/>
</div>
);
};
Dialog.propTypes = {
title: PropTypes.string,
text: PropTypes.string,
onClose: PropTypes.func,
noCloseButton: PropTypes.bool,
header: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
body: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
footer: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
onSubmit: PropTypes.func.isRequired,
value: PropTypes.object,
actions: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
value: PropTypes.any,
type: PropTypes.oneOf(['primary', 'secondary', 'cancel']).isRequired,
})
).isRequired,
onShow: PropTypes.func,
};
export default Dialog;

View File

@ -1,50 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Button, { ButtonEnums } from '../Button';
const Footer = ({ actions = [], className, onSubmit = () => {}, value }) => {
const flex = 'flex items-center justify-end';
const padding = 'pt-[20px]';
return (
<div className={classNames(flex, padding, className)}>
{actions?.map((action, index) => {
const isFirst = index === 0;
const onClickHandler = event => onSubmit({ action, value, event });
return (
<Button
key={index}
name={action.text}
className={classNames({ 'ml-2': !isFirst }, action.classes)}
type={action.type}
onClick={onClickHandler}
>
{action.text}
</Button>
);
})}
</div>
);
};
const noop = () => {};
Footer.propTypes = {
className: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
actions: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
value: PropTypes.any,
type: PropTypes.oneOf([ButtonEnums.type.primary, ButtonEnums.type.secondary]).isRequired,
classes: PropTypes.arrayOf(PropTypes.string),
})
).isRequired,
};
export default Footer;

View File

@ -1,48 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Typography from '../Typography';
import { Icons } from '@ohif/ui-next';
const CloseButton = ({ onClick }) => {
return (
<Icons.Close
data-cy="close-button"
onClick={onClick}
className="text-primary-active cursor-pointer"
/>
);
};
CloseButton.propTypes = {
onClick: PropTypes.func,
};
const Header = ({ title, noCloseButton = false, onClose }) => {
const theme = 'bg-primary-dark';
const flex = 'flex items-center justify-between';
const padding = 'pb-[20px]';
return (
<div className={classNames(theme, flex, padding)}>
<Typography
variant="h6"
color="primaryLight"
className="!leading-[1.2]"
>
{title}
</Typography>
{!noCloseButton && <CloseButton onClick={onClose} />}
</div>
);
};
Header.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
noCloseButton: PropTypes.bool,
onClose: PropTypes.func,
};
export default Header;

View File

@ -1,73 +0,0 @@
import Dialog from '../Dialog';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: Dialog,
title: 'Modals/Dialog',
};
<Meta
title="Modals/Dialog"
component={Dialog}
/>
export const DialogTemplate = createComponentTemplate(Dialog);
<Heading
title="Dialog"
componentRelativePath="Dialog/Dialog.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
Dialog is a modal dialog component which enabled to show a modal dialog.
<Canvas>
<Story name="Overview">{DialogTemplate.bind({})}</Story>
</Canvas>
## Props
<ArgsTable of={Dialog} />
## Usage
It can be used to show a modal dialog to submit a form or show a message.
<Canvas>
<Story
name="Submit"
args={{
title: 'Dialog Title',
text: 'Dialog Text',
onClose: () => {
window.alert('Dialog closed');
},
noCloseButton: false,
actions: [
{
id: 'cancel',
text: 'Cancel',
type: 'cancel',
},
{
id: 'submit',
text: 'Submit',
type: 'primary',
},
],
}}
>
{DialogTemplate.bind({})}
</Story>
</Canvas>
## Contribute
<Footer componentRelativePath="Dialog/__stories__/dialog.stories.mdx" />

View File

@ -1,2 +0,0 @@
import Dialog from './Dialog';
export default Dialog;

View File

@ -1,71 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import PortalTooltip from '../Tooltip/PortalTooltip';
import { useTranslation } from 'react-i18next';
import { Icons } from '@ohif/ui-next';
/**
* Displays a tooltip with a list of messages of a displaySet
* @param param0
* @returns
*/
const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => {
const { t } = useTranslation('Messages');
const [isOpen, setIsOpen] = useState(false);
if (messages?.size()) {
return (
<>
<Icons.ByName
id={id}
onMouseOver={() => setIsOpen(true)}
onFocus={() => setIsOpen(true)}
onMouseOut={() => setIsOpen(false)}
onBlur={() => setIsOpen(false)}
name="status-alert-warning"
/>
<PortalTooltip
active={isOpen}
position="right"
arrow="center"
parent={`#${id}`}
>
<div className="bg-primary-dark border-secondary-light max-w-64 rounded border text-left text-base text-white">
<div
className="break-normal text-base font-bold text-blue-300"
style={{
marginLeft: '12px',
marginTop: '12px',
}}
>
{t('Display Set Messages')}
</div>
<ol
style={{
marginLeft: '12px',
marginRight: '12px',
}}
>
{messages.messages.map((message, index) => (
<li
style={{
marginTop: '6px',
marginBottom: '6px',
}}
key={index}
>
{index + 1}. {t(message.id)}
</li>
))}
</ol>
</div>
</PortalTooltip>
</>
);
}
return <></>;
};
DisplaySetMessageListTooltip.propTypes = {
messages: PropTypes.object,
};
export default DisplaySetMessageListTooltip;

View File

@ -1,2 +0,0 @@
import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip';
export default DisplaySetMessageListTooltip;

View File

@ -1,221 +0,0 @@
import React, { useEffect, useCallback, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ReactDOM from 'react-dom';
import Typography from '../Typography';
import { Icons } from '@ohif/ui-next';
const borderStyle = 'border-b last:border-b-0 border-secondary-main';
const Dropdown = ({
id,
children,
showDropdownIcon = true,
list,
itemsClassName,
titleClassName,
showBorders = true,
alignment,
// By default the max characters per line is the longest title
// if you wish to override this, you can pass in a number
maxCharactersPerLine = 20,
}) => {
const [open, setOpen] = useState(false);
const elementRef = useRef(null);
const dropdownRef = useRef(null);
const [coords, setCoords] = useState({ x: 0, y: 0 });
// choose the max characters per line based on the longest title
const longestTitle = list.reduce((acc, item) => {
if (item.title.length > acc) {
return item.title.length;
}
return acc;
}, 0);
maxCharactersPerLine = maxCharactersPerLine ?? longestTitle;
const DropdownItem = useCallback(
({ id, title, icon, onClick }) => {
// Split the title into lines of length maxCharactersPerLine
const lines = [];
for (let i = 0; i < title.length; i += maxCharactersPerLine) {
lines.push(title.substring(i, i + maxCharactersPerLine));
}
return (
<div
key={title}
className={classnames(
'hover:bg-secondary-main flex cursor-pointer items-center px-4 py-2 transition duration-300',
titleClassName,
showBorders && borderStyle
)}
onClick={() => {
setOpen(false);
onClick();
}}
data-cy={id}
>
{!!icon && (
<Icons.ByName
name={icon}
className="mr-2 w-4 text-white"
/>
)}
<div
style={{
whiteSpace: 'nowrap',
}}
>
{title.length > maxCharactersPerLine && (
<div>
{lines.map((line, index) => (
<Typography
key={index}
className={itemsClassName}
>
{line}
</Typography>
))}
</div>
)}
{title.length <= maxCharactersPerLine && (
<Typography className={itemsClassName}>{title}</Typography>
)}
</div>
</div>
);
},
[maxCharactersPerLine, itemsClassName, titleClassName, showBorders]
);
const renderTitleElement = () => {
return (
<div className="flex items-center">
{children}
{showDropdownIcon && (
<Icons.ByName
name="chevron-down"
className="ml-1"
/>
)}
</div>
);
};
const toggleList = () => {
setOpen(s => !s);
};
const handleClick = e => {
if (elementRef.current && !elementRef.current.contains(e.target)) {
setOpen(false);
}
};
useEffect(() => {
if (elementRef.current && dropdownRef.current) {
const triggerRect = elementRef.current.getBoundingClientRect();
const dropdownRect = dropdownRef.current.getBoundingClientRect();
let x, y;
switch (alignment) {
case 'right':
x = triggerRect.right + window.scrollX - dropdownRect.width;
y = triggerRect.bottom + window.scrollY;
break;
case 'left':
x = triggerRect.left + window.scrollX;
y = triggerRect.bottom + window.scrollY;
break;
default:
x = triggerRect.left + window.scrollX;
y = triggerRect.bottom + window.scrollY;
break;
}
setCoords({ x, y });
}
}, [open, alignment, elementRef.current, dropdownRef.current]);
const renderList = () => {
const portalElement = document.getElementById('react-portal');
const listElement = (
<div
className={classnames(
'top-100 border-secondary-main w-max-content absolute mt-2 transform rounded border bg-black shadow transition duration-300',
{
'right-0 origin-top-right': alignment === 'right',
'left-0 origin-top-left': alignment === 'left',
}
)}
ref={dropdownRef}
style={{
position: 'absolute',
top: `${coords.y}px`,
left: open ? `${coords.x}px` : -999999,
zIndex: 9999,
}}
data-cy={`${id}-dropdown`}
>
{list.map((item, idx) => (
<DropdownItem
id={item.id}
title={item.title}
icon={item.icon}
onClick={item.onClick}
key={idx}
/>
))}
</div>
);
return ReactDOM.createPortal(listElement, portalElement);
};
useEffect(() => {
document.addEventListener('click', handleClick);
if (!open) {
document.removeEventListener('click', handleClick);
}
}, [open]);
return (
<div
data-cy="dropdown"
ref={elementRef}
className="relative"
>
<div
className="flex cursor-pointer items-center"
onClick={toggleList}
>
{renderTitleElement()}
</div>
{renderList()}
</div>
);
};
Dropdown.propTypes = {
id: PropTypes.string,
children: PropTypes.node.isRequired,
showDropdownIcon: PropTypes.bool,
titleClassName: PropTypes.string,
/** 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,
alignment: PropTypes.oneOf(['left', 'right']),
maxCharactersPerLine: PropTypes.number,
showBorders: PropTypes.bool,
};
export default Dropdown;

View File

@ -1,84 +0,0 @@
import Dropdown from '../Dropdown';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: Dropdown,
title: 'Components/Dropdown',
};
<Meta
title="Components/Dropdown"
component={Dropdown}
/>
export const DropdownTemplate = args => (
// Todo: this should not set a background
<div className="flex h-32">
<Dropdown {...args} />
</div>
);
<Heading
title="Dropdown"
componentRelativePath="Dropdown/Dropdown.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
Dropdown is a modal Dropdown component which enabled to show a modal Dropdown.
<Canvas>
<Story
name="Overview"
args={{ id: 'dropdown-d', list: [] }}
>
{DropdownTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={Dropdown} />
## Usage
You can list the items in the dropdown.
<Canvas>
<Story
name="List"
args={{
id: 'dropdown-1',
children: <div className="text-black">Drop Down</div>,
showDropdownIcon: true,
list: [
{
title: 'Item 1',
icon: 'clipboard',
onClick: () => {
alert('Item 1 clicked');
},
},
{
title: 'Item 2',
icon: 'tracked',
onClick: () => {
alert('Item 2 clicked');
},
},
],
}}
>
{DropdownTemplate.bind({})}
</Story>
</Canvas>
## Contribute
<Footer componentRelativePath="Dropdown/__stories__/dropdown.stories.mdx" />

View File

@ -1 +0,0 @@
export { default } from './Dropdown';

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import Typography from '../Typography';
import { Icons } from '@ohif/ui-next';
// TODO: Add loading spinner to OHIF + use it here.
const EmptyStudies = ({ className = '' }) => {
const { t } = useTranslation('StudyList');
return (

View File

@ -1,124 +0,0 @@
import React, { useState } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import i18n from 'i18next';
import Modal from '../Modal';
import IconButton from '../IconButton';
import { Icons } from '@ohif/ui-next';
const isProduction = process.env.NODE_ENV === 'production';
const DefaultFallback = ({ error, context, resetErrorBoundary = () => {}, fallbackRoute }) => {
const { t } = useTranslation('ErrorBoundary');
const [showDetails, setShowDetails] = useState(false);
const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`;
const subtitle = t('Sorry, something went wrong there. Try again.');
return (
<div
className="ErrorFallback bg-primary-dark h-full w-full"
role="alert"
>
<p className="text-primary-light text-xl">{title}</p>
<p className="text-primary-light text-base">{subtitle}</p>
{!isProduction && (
<div className="bg-secondary-dark mt-5 space-y-2 rounded-md p-5 font-mono">
<p className="text-primary-light">
{t('Context')}: {context}
</p>
<p className="text-primary-light">
{t('Error Message')}: {error.message}
</p>
<IconButton
variant="contained"
color="inherit"
size="initial"
className="text-primary-active"
onClick={() => setShowDetails(!showDetails)}
>
<React.Fragment>
<div>{t('Stack Trace')}</div>
<Icons.ChevronOpen />
</React.Fragment>
</IconButton>
{showDetails && (
<pre className="text-primary-light whitespace-pre-wrap px-4">Stack: {error.stack}</pre>
)}
</div>
)}
</div>
);
};
DefaultFallback.propTypes = {
error: PropTypes.object.isRequired,
resetErrorBoundary: PropTypes.func,
componentStack: PropTypes.string,
};
const ErrorBoundary = ({
context = 'OHIF',
onReset = () => {},
onError = () => {},
fallbackComponent: FallbackComponent = DefaultFallback,
children,
fallbackRoute = null,
isPage,
}) => {
const [isOpen, setIsOpen] = useState(true);
const onErrorHandler = (error, componentStack) => {
console.error(`${context} Error Boundary`, error, componentStack, context);
onError(error, componentStack, context);
};
const onResetHandler = (...args) => onReset(...args);
const withModal = Component => props => (
<Modal
closeButton
shouldCloseOnEsc
isOpen={isOpen}
title={i18n.t('ErrorBoundary:Something went wrong')}
onClose={() => {
setIsOpen(false);
if (fallbackRoute && typeof window !== 'undefined') {
window.location = fallbackRoute;
}
}}
>
<Component {...props} />
</Modal>
);
const Fallback = isPage ? FallbackComponent : withModal(FallbackComponent);
return (
<ReactErrorBoundary
fallbackRender={props => (
<Fallback
{...props}
context={context}
fallbackRoute={fallbackRoute}
/>
)}
onReset={onResetHandler}
onError={onErrorHandler}
>
{children}
</ReactErrorBoundary>
);
};
ErrorBoundary.propTypes = {
context: PropTypes.string,
onReset: PropTypes.func,
onError: PropTypes.func,
fallbackComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
children: PropTypes.node.isRequired,
fallbackRoute: PropTypes.string,
};
export default ErrorBoundary;

View File

@ -1,2 +0,0 @@
import ErrorBoundary from './ErrorBoundary';
export default ErrorBoundary;

View File

@ -1,16 +0,0 @@
.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after {
content: '';
position: absolute;
bottom: -10px;
border-width: 10px 10px 0;
border-style: solid;
border-color: #5acce6 transparent;
}
.ExpandableToolbarButton .ExpandableToolbarButton__content {
display: none;
}
.ExpandableToolbarButton:hover .ExpandableToolbarButton__content {
display: block;
}

View File

@ -1,85 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import IconButton from '../IconButton';
import { Icons } from '@ohif/ui-next';
import './ExpandableToolbarButton.css';
const ExpandableToolbarButton = ({
type = 'primary',
id = '',
isActive = false,
onClick = () => {},
icon = 'clipboard',
className,
content: Content = null,
contentProps = {},
}) => {
const classes = {
type: {
primary: isActive
? 'text-black'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
secondary: isActive
? 'text-black'
: 'text-white hover:bg-secondary-dark focus:bg-secondary-dark',
},
};
const onChildClickHandler = (...args) => {
onClick(...args);
if (contentProps.onClick) {
contentProps.onClick(...args);
}
};
const onClickHandler = (...args) => {
onClick(...args);
};
return (
<div
key={id}
className="ExpandableToolbarButton"
>
<IconButton
variant={isActive ? 'contained' : 'text'}
className={classnames(
'mx-1',
classes.type[type],
isActive && 'ExpandableToolbarButton__arrow'
)}
onClick={onClickHandler}
key={id}
>
<Icons.ByName name={icon} />
</IconButton>
<div className="absolute z-10 pt-4">
<div className={classnames('ExpandableToolbarButton__content w-48', className)}>
<Content
{...contentProps}
onClick={onChildClickHandler}
/>
</div>
</div>
</div>
);
};
const noop = () => {};
ExpandableToolbarButton.propTypes = {
/* Influences background/hover styling */
type: PropTypes.oneOf(['primary', 'secondary']),
id: PropTypes.string.isRequired,
isActive: PropTypes.bool,
onClick: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired,
/** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
contentProps: PropTypes.object,
};
export default ExpandableToolbarButton;

View File

@ -1,2 +0,0 @@
import ExpandableToolbarButton from './ExpandableToolbarButton';
export default ExpandableToolbarButton;

View File

@ -1,109 +0,0 @@
import React, { ReactNode } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import NavBar from '../NavBar';
import IconButton from '../IconButton';
import Dropdown from '../Dropdown';
import HeaderPatientInfo from '../HeaderPatientInfo';
import { PatientInfoVisibility } from '../../types/PatientInfoVisibility';
import { Icons } from '@ohif/ui-next';
function Header({
children,
menuOptions,
isReturnEnabled = true,
onClickReturnButton,
isSticky = false,
WhiteLabeling,
showPatientInfo = PatientInfoVisibility.VISIBLE_COLLAPSED,
servicesManager,
Secondary,
appConfig,
...props
}: withAppTypes): ReactNode {
const { t } = useTranslation('Header');
// TODO: this should be passed in as a prop instead and the react-router-dom
// dependency should be dropped
const onClickReturn = () => {
if (isReturnEnabled && onClickReturnButton) {
onClickReturnButton();
}
};
return (
<NavBar
isSticky={isSticky}
{...props}
>
<div className="relative h-[48px] items-center">
<div className="absolute left-0 top-1/2 flex -translate-y-1/2 items-center">
<div
className={classNames(
'mr-3 inline-flex items-center',
isReturnEnabled && 'cursor-pointer'
)}
onClick={onClickReturn}
data-cy="return-to-work-list"
>
{isReturnEnabled && <Icons.ChevronClosed className="text-primary-active w-8" />}
<div className="ml-1">
{WhiteLabeling?.createLogoComponentFn?.(React, props) || <Icons.OHIFLogo />}
</div>
</div>
</div>
<div className="absolute top-1/2 left-[250px] h-8 -translate-y-1/2">{Secondary}</div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<div className="flex items-center justify-center space-x-2">{children}</div>
</div>
<div className="absolute right-0 top-1/2 flex -translate-y-1/2 select-none items-center">
{showPatientInfo !== PatientInfoVisibility.DISABLED && (
<HeaderPatientInfo
servicesManager={servicesManager}
appConfig={appConfig}
/>
)}
<div className="border-primary-dark mx-1.5 h-[25px] border-r"></div>
<div className="flex-shrink-0">
<Dropdown
id="options"
showDropdownIcon={false}
list={menuOptions}
alignment="right"
>
<IconButton
id={'options-settings-icon'}
variant="text"
color="inherit"
size="initial"
className="text-primary-active hover:bg-primary-dark h-full w-full"
>
<Icons.ByName name="icon-settings" />
</IconButton>
</Dropdown>
</div>
</div>
</div>
</NavBar>
);
}
Header.propTypes = {
menuOptions: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
icon: PropTypes.string,
onClick: PropTypes.func.isRequired,
})
),
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
isReturnEnabled: PropTypes.bool,
isSticky: PropTypes.bool,
onClickReturnButton: PropTypes.func,
WhiteLabeling: PropTypes.object,
showPatientInfo: PropTypes.string,
servicesManager: PropTypes.object,
};
export default Header;

View File

@ -1,57 +0,0 @@
import Header from '../Header';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: Header,
title: 'Components/Header',
};
<Meta
title="Components/Header"
component={Header}
/>
export const HeaderTemplate = createComponentTemplate(Header);
<Heading
title="Header"
componentRelativePath="Header/Header.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
Header is a component that renders the header of the application.
<Canvas>
<Story
name="Overview"
args={{
menuOptions: [
{
title: 'About',
icon: 'info',
onClick: () => window.alert('About clicked'),
},
],
isReturnEnabled: true,
isSticky: false,
onClickReturnButton: () => window.alert('Return clicked'),
}}
>
{HeaderTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={Header} />
## Contribute
<Footer componentRelativePath="Header/__stories__/header.stories.mdx" />

View File

@ -1,2 +0,0 @@
import Header from './Header';
export default Header;

View File

@ -1,133 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { utils } from '@ohif/core';
import { Icons } from '@ohif/ui-next';
import { PatientInfoVisibility } from '../../types';
const { formatDate, formatPN } = utils;
const formatWithEllipsis = (str, maxLength) => {
if (str?.length > maxLength) {
return str.substring(0, maxLength) + '...';
}
return str;
};
function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
const { displaySetService } = servicesManager.services;
const [patientInfo, setPatientInfo] = useState({
PatientName: '',
PatientID: '',
PatientSex: '',
PatientDOB: '',
});
const [isMixedPatients, setIsMixedPatients] = useState(false);
const displaySets = displaySetService.getActiveDisplaySets();
const checkMixedPatients = PatientID => {
const displaySets = displaySetService.getActiveDisplaySets();
let isMixedPatients = false;
displaySets.forEach(displaySet => {
const instance = displaySet?.instances?.[0] || displaySet?.instance;
if (!instance) {
return;
}
if (instance.PatientID !== PatientID) {
isMixedPatients = true;
}
});
setIsMixedPatients(isMixedPatients);
};
const updatePatientInfo = () => {
const displaySet = displaySets[0];
const instance = displaySet?.instances?.[0] || displaySet?.instance;
if (!instance) {
return;
}
setPatientInfo({
PatientID: instance.PatientID || null,
PatientName: instance.PatientName ? formatPN(instance.PatientName) : null,
PatientSex: instance.PatientSex || null,
PatientDOB: formatDate(instance.PatientBirthDate) || null,
});
checkMixedPatients(instance.PatientID || null);
};
useEffect(() => {
const subscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
() => updatePatientInfo()
);
return () => subscription.unsubscribe();
}, []);
useEffect(() => {
updatePatientInfo();
}, [displaySets]);
return { patientInfo, isMixedPatients };
}
function HeaderPatientInfo({ servicesManager, appConfig }: withAppTypes) {
const initialExpandedState =
appConfig.showPatientInfo === PatientInfoVisibility.VISIBLE ||
appConfig.showPatientInfo === PatientInfoVisibility.VISIBLE_READONLY;
const [expanded, setExpanded] = useState(initialExpandedState);
const { patientInfo, isMixedPatients } = usePatientInfo(servicesManager);
useEffect(() => {
if (isMixedPatients && expanded) {
setExpanded(false);
}
}, [isMixedPatients, expanded]);
const handleOnClick = () => {
if (!isMixedPatients && appConfig.showPatientInfo !== PatientInfoVisibility.VISIBLE_READONLY) {
setExpanded(!expanded);
}
};
const formattedPatientName = formatWithEllipsis(patientInfo.PatientName, 27);
const formattedPatientID = formatWithEllipsis(patientInfo.PatientID, 15);
return (
<div
className="hover:bg-primary-dark flex cursor-pointer items-center justify-center gap-1 rounded-lg"
onClick={handleOnClick}
>
<Icons.ByName
name={isMixedPatients ? 'icon-multiple-patients' : 'icon-patient'}
className="text-primary-active"
/>
<div className="flex flex-col justify-center">
{expanded ? (
<>
<div className="self-start text-[13px] font-bold text-white">
{formattedPatientName}
</div>
<div className="text-aqua-pale flex gap-2 text-[11px]">
<div>{formattedPatientID}</div>
<div>{patientInfo.PatientSex}</div>
<div>{patientInfo.PatientDOB}</div>
</div>
</>
) : (
<div className="text-primary-active self-center text-[13px]">
{' '}
{isMixedPatients ? 'Multiple Patients' : 'Patient'}
</div>
)}
</div>
<Icons.ArrowLeft className={`text-primary-active ${expanded ? 'rotate-180' : ''}`} />
</div>
);
}
HeaderPatientInfo.propTypes = {
servicesManager: PropTypes.object.isRequired,
};
export default HeaderPatientInfo;

View File

@ -1,3 +0,0 @@
import HeaderPatientInfo from './HeaderPatientInfo';
export default HeaderPatientInfo;

View File

@ -1,61 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Input from '../Input';
import { getKeys, formatKeysForInput } from './utils';
/**
* HotkeyField
* Renders a hotkey input that records keys
*
* @param {object} props component props
* @param {Array[]} props.keys keys to be controlled by this field
* @param {boolean} props.disabled disables the field
* @param {function} props.onChange callback with changed values
* @param {string} props.className input classes
* @param {Array[]} props.modifierKeys
*/
const HotkeyField = ({ disabled = false, keys, onChange, className, modifierKeys, hotkeys }) => {
const inputValue = formatKeysForInput(keys);
const onInputKeyDown = event => {
hotkeys.record(sequence => {
const keys = getKeys({ sequence, modifierKeys });
hotkeys.unpause();
onChange(keys);
});
};
const onFocus = () => {
hotkeys.pause();
hotkeys.startRecording();
};
return (
<Input
readOnly
disabled={disabled}
value={inputValue}
onKeyDown={onInputKeyDown}
onFocus={onFocus}
className={className}
/>
);
};
HotkeyField.propTypes = {
keys: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
className: PropTypes.string,
modifierKeys: PropTypes.array,
disabled: PropTypes.bool,
hotkeys: PropTypes.shape({
initialize: PropTypes.func.isRequired,
pause: PropTypes.func.isRequired,
unpause: PropTypes.func.isRequired,
startRecording: PropTypes.func.isRequired,
record: PropTypes.func.isRequired,
}).isRequired,
};
export default HotkeyField;

View File

@ -1,3 +0,0 @@
import HotkeyField from './HotkeyField.tsx';
export default HotkeyField;

View File

@ -1,28 +0,0 @@
/**
* Take the pressed key array and return the readable string for the keys
*
* @param {Array} [keys=[]]
* @returns {string} string representation of an array of keys
*/
const formatKeysForInput = (keys = []) => keys.join('+');
/**
* formats given keys sequence to insert the modifier keys in the first index of the array
* @param {string} sequence keys sequence from MouseTrap Record -> "shift+left"
* @returns {Array} keys in array-format -> ['shift','left']
*/
const getKeys = ({ sequence, modifierKeys }) => {
const keysArray = sequence.join(' ').split('+');
let keys = [];
let modifiers = [];
keysArray.forEach(key => {
if (modifierKeys && modifierKeys.includes(key)) {
modifiers.push(key);
} else {
keys.push(key);
}
});
return [...modifiers, ...keys];
};
export { getKeys, formatKeysForInput };

View File

@ -1,143 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import HotkeyField from '../HotkeyField';
import Typography from '../Typography';
/* TODO: Move these configs and utils to core? */
import { MODIFIER_KEYS } from './hotkeysConfig';
import { validate, splitHotkeyDefinitionsAndCreateTuples } from './utils';
const HotkeysPreferences = ({
disabled = false,
hotkeyDefinitions,
errors: controlledErrors,
onChange = () => {},
hotkeysModule,
}) => {
const { t } = useTranslation('UserPreferencesModal');
const visibleHotkeys = Object.keys(hotkeyDefinitions)
.filter(key => hotkeyDefinitions[key].isEditable)
.reduce((obj, key) => {
obj[key] = hotkeyDefinitions[key];
return obj;
}, {});
const [errors, setErrors] = useState(controlledErrors);
const splitedHotkeys = splitHotkeyDefinitionsAndCreateTuples(visibleHotkeys);
if (!Object.keys(hotkeyDefinitions).length) {
return t('No hotkeys found');
}
const onHotkeyChangeHandler = (id, definition) => {
const { error } = validate({
commandName: id,
pressedKeys: definition.keys,
hotkeys: hotkeyDefinitions,
});
setErrors(prevState => {
const errors = { ...prevState, [id]: error };
return errors;
});
onChange(id, definition, { ...errors, [id]: error });
};
return (
<div className="flex flex-row justify-center">
<div className="flex w-full flex-row justify-evenly">
{splitedHotkeys.map((hotkeys, index) => {
return (
<div
key={`HotkeyGroup@${index}`}
className="flex flex-row"
>
<div className="flex flex-col p-2 text-right">
{hotkeys.map((hotkey, hotkeyIndex) => {
const [id, definition] = hotkey;
const isFirst = hotkeyIndex === 0;
const error = errors[id];
const onChangeHandler = keys =>
onHotkeyChangeHandler(id, { ...definition, keys });
return (
<div
key={`HotkeyItem@${hotkeyIndex}`}
className="mb-2 flex flex-row justify-end"
>
<div className="flex flex-col items-center">
<Typography
variant="subtitle"
className={classNames(
'text-primary-light w-full pr-6 text-right',
!isFirst && 'hidden'
)}
>
{t('Function')}
</Typography>
<Typography
variant="subtitle"
className={classNames(
'flex h-full flex-row items-center whitespace-nowrap pr-6',
isFirst && 'mt-5'
)}
>
{definition.label}
</Typography>
</div>
<div className="flex flex-col">
<Typography
variant="subtitle"
className={classNames(
'text-primary-light pr-6 pl-0 text-left',
!isFirst && 'hidden'
)}
>
{t('Shortcut')}
</Typography>
<div className={classNames('flex w-32 flex-col', isFirst && 'mt-5')}>
<HotkeyField
disabled={disabled}
keys={definition.keys}
modifierKeys={MODIFIER_KEYS}
onChange={onChangeHandler}
hotkeys={hotkeysModule}
className="h-8 text-lg"
/>
{error && (
<span className="p-2 text-left text-sm text-red-600">{error}</span>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
};
HotkeysPreferences.propTypes = {
onChange: PropTypes.func,
disabled: PropTypes.bool,
hotkeyDefinitions: PropTypes.object.isRequired,
hotkeysModule: PropTypes.shape({
initialize: PropTypes.func.isRequired,
pause: PropTypes.func.isRequired,
unpause: PropTypes.func.isRequired,
startRecording: PropTypes.func.isRequired,
record: PropTypes.func.isRequired,
}).isRequired,
};
export default HotkeysPreferences;

View File

@ -1,87 +0,0 @@
export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
export const DISALLOWED_COMBINATIONS = {
'': [],
alt: ['space'],
shift: [],
ctrl: [
'f4',
'f5',
'f11',
'w',
'r',
't',
'o',
'p',
'a',
'd',
'f',
'g',
'h',
'j',
'l',
'z',
'x',
'c',
'v',
'b',
'n',
'pagedown',
'pageup',
],
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
};
export const SPECIAL_KEYS = {
8: 'backspace',
9: 'tab',
13: 'return',
16: 'shift',
17: 'ctrl',
18: 'alt',
19: 'pause',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'insert',
46: 'del',
96: '0',
97: '1',
98: '2',
99: '3',
100: '4',
101: '5',
102: '6',
103: '7',
104: '8',
105: '9',
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
144: 'numlock',
145: 'scroll',
191: '/',
224: 'meta',
};

View File

@ -1,78 +0,0 @@
import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
import i18n from 'i18next';
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
let firstConflictingCommand = undefined;
const formatedPressedHotkeys = formatPressedKeys(pressedKeys);
for (const commandName in hotkeys) {
const toolHotkeys = hotkeys[commandName].keys;
const formatedToolHotkeys = formatPressedKeys(toolHotkeys);
if (formatedPressedHotkeys === formatedToolHotkeys && commandName !== currentCommandName) {
firstConflictingCommand = hotkeys[commandName];
break;
}
}
return firstConflictingCommand;
};
const ERROR_MESSAGES = {
MODIFIER: i18n.t('HotkeysValidators:It\'s not possible to define only modifier keys (ctrl, alt and shift) as a shortcut'),
EMPTY: i18n.t('HotkeysValidators:Field can\'t be empty'),
};
// VALIDATORS
const modifierValidator = ({ pressedKeys }) => {
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
// Check if it has a valid modifier
const isModifier = MODIFIER_KEYS.includes(lastPressedKey);
if (isModifier) {
return { error: ERROR_MESSAGES.MODIFIER };
}
};
const emptyValidator = ({ pressedKeys = [] }) => {
if (!pressedKeys.length) {
return { error: ERROR_MESSAGES.EMPTY };
}
};
const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
const conflictingCommand = findConflictingCommand(hotkeys, commandName, pressedKeys);
if (conflictingCommand) {
return {
error: i18n.t('HotkeysValidators:Hotkey is already in use', {action: conflictingCommand.label, pressedKeys: pressedKeys }),
};
}
};
const disallowedValidator = ({ pressedKeys = [] }) => {
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
const modifierCommand = formatPressedKeys(pressedKeys.slice(0, pressedKeys.length - 1));
const disallowedCombination = DISALLOWED_COMBINATIONS[modifierCommand];
const hasDisallowedCombinations = disallowedCombination
? disallowedCombination.includes(lastPressedKey)
: false;
if (hasDisallowedCombinations) {
return {
error: i18n.t('HotkeysValidators:Shortcut combination is not allowed', {pressedKeys: formatPressedKeys(pressedKeys)}),
};
}
};
const hotkeysValidators = [
emptyValidator,
modifierValidator,
conflictingValidator,
disallowedValidator,
];
export { hotkeysValidators };

View File

@ -1,3 +0,0 @@
import HotkeysPreferences from './HotkeysPreferences.tsx';
export default HotkeysPreferences;

View File

@ -1,47 +0,0 @@
import { hotkeysValidators } from './hotkeysValidators';
/**
* Split hotkeys definitions and create hotkey related tuples
*
* @param {array} hotkeyDefinitions
* @returns {array} array of tuples consisted of command name and hotkey definition
*/
const splitHotkeyDefinitionsAndCreateTuples = hotkeyDefinitions => {
const splitedHotkeys = [];
const arrayHotkeys = Object.entries(hotkeyDefinitions);
if (arrayHotkeys.length) {
const halfwayThrough = Math.ceil(arrayHotkeys.length / 2);
splitedHotkeys.push(arrayHotkeys.slice(0, halfwayThrough));
splitedHotkeys.push(arrayHotkeys.slice(halfwayThrough, arrayHotkeys.length));
}
return splitedHotkeys;
};
/**
* Validate a hotkey change
*
* @param {Object} arguments
* @param {string} arguments.commandName command name or id
* @param {array} arguments.pressedKeys new keys
* @param {array} arguments.hotkeys current hotkeys
* @returns {Object} {error} validation error
*/
const validate = ({ commandName, pressedKeys, hotkeys }) => {
for (const validator of hotkeysValidators) {
const validation = validator({
commandName,
pressedKeys,
hotkeys,
});
if (validation && validation.error) {
return validation;
}
}
return { error: undefined };
};
export { validate, splitHotkeyDefinitionsAndCreateTuples };

View File

@ -1,26 +0,0 @@
import React from 'react';
import IconButton from './IconButton';
import Icon from '../Icon/Icon';
import { ICONS } from '../Icon/getIcon';
export default {
component: IconButton,
title: 'Icons/IconButton',
argTypes: {
iconName: {
control: { type: 'select', options: Object.keys(ICONS) },
},
},
};
const Template = ({ iconName, ...args }) => (
<IconButton {...args}>
<Icon name={iconName} />
</IconButton>
);
export const Default = Template.bind({});
Default.args = {
iconName: 'clipboard',
};

Some files were not shown because too many files have changed in this diff Show More