Chore/kill yalc with fire (#629)
* init * more shifting * Move @babel dependencies up to workspace root * Begin updating dependent libraries to use sync'd webpack builds w/ watches * Commit changes before a long weekend * Split packages into platform and extensions * Consolidate how/where we specify file/module type entrypoints * Make it possible to pass in extensions as App props * Resolve viewer's modules * Target for UMD bundle * Add Webpack Stylus loader * Provide WebPack build options for microscopy, vtk, and ui * Better entrypoint for extensions * Push changes up to switch PCs * Fix ESM symlink build for Viewers * Shift docs to root * Specify additional lerna config props * Begin providing basic readme info * Rename example extension folder * Shift links to bottom of doc * First attempt at a netlify deploy preview * Attempt to fix deploy preview build * push workspace enabled to initial command; remove second yarn install; use npx to call gitbook cli commands * Try to use global gitbook-cli * Export node_modules path * More agressive with modifying PATH * Follow deploy log output advice * try again to set bin path * Try swapping bash with a new shell * Try bash -l instead of exec bash * Annd let's try again * and again * We may have figured it out johnny, boy * We need to pull cornerstone-wado-image-loader files from hoisted node_modules * Attempt to fix ticks/escapes * Change copy syntax; try running tests on viewers from root for circleci * Lower version to match circleci image * Try alternative jest-canvas-mock location and version file syntax * Run version command instead of calling node directly * Try alternative jest transform * Set path and enable workspaces * Add codecov flags * Use cpx so our copying finishes? * Get all platform unit tests to run * Run and report individually and in parallel * Try once relying on codecov yaml to split w/ flags * Add projects to split by flags * Generate separate reports * Trigger codecov after everything has finished running; these may not support flags * Use recommended flags from issue comments for codecov * Fix typo * Remove clear flag * Shift back to calling codecov from root * Generate example for codecov issue * Try fixing paths * Simplify * Try using codecov orb * Bump circleci version * Upload core and viewer * Use full string paths * Remove individual codecov calls * Fix dir * Combine files before upload * Escape anchors * tryfix syntax * Escape string literal * Combine lines to reduce path * long paths * Use home alias * Fix filename * Try to see the contents of our cat'd file * Also upload `core` * Try running with aliased folder * Fix path; split PR and Merge unit tests into separate jobs * Set default threshold * Fix typo * Changing to scoped package names * Adding note * Catch more updates * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.0 - @ohif/extension-dicom-html@0.0.4-alpha.0 - @ohif/extension-dicom-microscopy@0.0.9-alpha.0 - @ohif/extension-dicom-pdf@0.0.8-alpha.0 - @ohif/extension-vtk@0.1.4-alpha.0 - @ohif/core@0.11.1-alpha.0 - @ohif/i18n@0.2.3-alpha.0 - @ohif/ui@0.2.18-alpha.0 - @ohif/viewer@0.0.22-alpha.0 * Clean up primary readme * Support for dev and dev:* commands * Shift build command; satisfy default PWA build * Remove old scripts * Clean up UI to set webpack scripts * Tidy up project links * Clean duplicate code in UI project * Update UI project's docs * Clean up ui and i18n config * Reduce duplicate code in `@ohif/core` * Fix @ohif/ui versions * bump @ohif/core versions * Redux testkit dep * Remove unused/duplicate config files for projects/packages * ohif-core --> @ohif/core * Safer publish command * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.1 - @ohif/extension-dicom-html@0.0.4-alpha.1 - @ohif/extension-dicom-microscopy@0.0.9-alpha.1 - @ohif/extension-dicom-pdf@0.0.8-alpha.1 - @ohif/extension-vtk@0.1.4-alpha.1 - @ohif/core@0.11.1-alpha.1 - @ohif/i18n@0.2.3-alpha.1 - @ohif/ui@0.2.18-alpha.1 - @ohif/viewer@0.0.22-alpha.1 * test version bump * Add global prettier config * chore(release): publish %s [skip ci] - @ohif/viewer@0.0.22-alpha.2 * test lerna publish * Update package.jsons * Fix build scripts * catch changes * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.2 - @ohif/extension-dicom-html@0.0.4-alpha.2 - @ohif/extension-dicom-microscopy@0.0.9-alpha.2 - @ohif/extension-dicom-pdf@0.0.8-alpha.2 - @ohif/extension-vtk@0.1.4-alpha.2 - @ohif/core@0.11.1-alpha.2 - @ohif/i18n@0.2.3-alpha.2 - @ohif/ui@0.2.18-alpha.2 - @ohif/viewer@0.0.22-alpha.3 * fix eslint issues caused by eslint 6 and our react-app config * Publish with canary tag * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.3 - @ohif/extension-dicom-html@0.0.4-alpha.3 - @ohif/extension-dicom-microscopy@0.0.9-alpha.3 - @ohif/extension-dicom-pdf@0.0.8-alpha.3 - @ohif/extension-vtk@0.1.4-alpha.3 - @ohif/core@0.11.1-alpha.3 - @ohif/i18n@0.2.3-alpha.3 - @ohif/ui@0.2.18-alpha.3 - @ohif/viewer@0.0.22-alpha.4 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.4 - @ohif/extension-dicom-html@0.0.4-alpha.4 - @ohif/extension-dicom-microscopy@0.0.9-alpha.4 - @ohif/extension-dicom-pdf@0.0.8-alpha.4 - @ohif/extension-vtk@0.1.4-alpha.4 - @ohif/core@0.11.1-alpha.4 - @ohif/i18n@0.2.3-alpha.4 - @ohif/ui@0.2.18-alpha.4 - @ohif/viewer@0.0.22-alpha.5 * Simplify build * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.5 - @ohif/extension-dicom-html@0.0.4-alpha.5 - @ohif/extension-dicom-microscopy@0.0.9-alpha.5 - @ohif/extension-dicom-pdf@0.0.8-alpha.5 - @ohif/extension-vtk@0.1.4-alpha.5 - @ohif/core@0.11.1-alpha.5 - @ohif/i18n@0.2.3-alpha.5 - @ohif/ui@0.2.18-alpha.5 - @ohif/viewer@0.0.22-alpha.6 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.6 - @ohif/extension-dicom-html@0.0.4-alpha.6 - @ohif/extension-dicom-microscopy@0.0.9-alpha.6 - @ohif/extension-dicom-pdf@0.0.8-alpha.6 - @ohif/extension-vtk@0.1.4-alpha.6 - @ohif/core@0.11.1-alpha.6 - @ohif/i18n@0.2.3-alpha.6 - @ohif/ui@0.2.18-alpha.6 - @ohif/viewer@0.0.22-alpha.7 * Split version and publish * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.7 - @ohif/extension-dicom-html@0.0.4-alpha.7 - @ohif/extension-dicom-microscopy@0.0.9-alpha.7 - @ohif/extension-dicom-pdf@0.0.8-alpha.7 - @ohif/extension-vtk@0.1.4-alpha.7 - @ohif/core@0.11.1-alpha.7 - @ohif/i18n@0.2.3-alpha.7 - @ohif/ui@0.2.18-alpha.7 - @ohif/viewer@0.0.22-alpha.8 * Try simplify netlify build * react-viewerbase --> @ohif/ui * add test for stylus loader * Try new way to specify publish path * Use an output folder that won't be ignored * Try move command * fiddle with paths * Dig for some information * Make sure we're in root for our commands * Remove discovery * Setup script-tag example * Fix typo * Move config folder * Update cornerstone extension to use shared/root webpack configs * capture changes * Update extension webpack configs * Don't check in docz output * Fix babel runtime resolution issue * Simplify ui build steps * Push in progress * Shift directories * Get viewer dev path to work * Fix viewer commonjs build * Fix build pwa config for viewers * Make sure our package specific command can be called from root * Update notes * Fix imports and misc. merge notes * Bump wado-image-loader version * Switch up commands to remove webworkers copy * Fix CI config * Fix more broken package references * Add react-dropzone * Persist so e2e test can use * Fix lockfile * Use cypress orb to simplify CI * Fix config * Try again * Install yarn latest for orb * Install yarn a different way * Wait on correct port * Run tests in viewer project * Fix cypress config * Make Cypress happier - use default location * Simplify e2e testing * Capture UI changes since fork * Catch core up * Grab latest deps after merge * Fix broken import * Make sure cornerstone-wado-image-loader resolves to 3+ * Note regarding orbs * Fix refresh on 404 routes * Fix odd error for cornerstone-wado-image-loader * Different target for UMD * Simplify dev script * Try to get fancy with netlify output * Let's do it again * Try to fix measurementTools issue * See if we can get our lerna magic to work on merge to monorepo branch
This commit is contained in:
1 parent
cac911f08e
commit
08ff214ea6
919 files changed
+45652
-52097
No files matched your search
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Checkbox
|
||||
menu: Elements
|
||||
route: /components/checkbox
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { Checkbox } from './../index'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
|
||||
# Checkbox
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State
|
||||
initial={{
|
||||
checked : false,
|
||||
label : 'Sample check'
|
||||
}}
|
||||
>
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<pre>{JSON.stringify(state, null, 2)}</pre>
|
||||
</div>
|
||||
<div style={{ maxWidth: '300px', margin: '0 auto' }}>
|
||||
<Checkbox {...state} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</State>
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={Checkbox} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="Checkbox" />
|
||||
@@ -0,0 +1,56 @@
|
||||
.ohif-check-container {
|
||||
--check-button-dim: 15px;
|
||||
}
|
||||
|
||||
.ohif-check-container input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ohif-check-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 35px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ohif-checkbox {
|
||||
width: var(--check-button-dim);
|
||||
height: var(--check-button-dim);
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 5%;
|
||||
cursor: pointer;
|
||||
background-color: var(--ui-gray-lighter);
|
||||
}
|
||||
|
||||
.ohif-checkbox:hover {
|
||||
background-color: var(--default-color);
|
||||
}
|
||||
|
||||
.ohif-checkbox:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.ohif-checkbox.ohif-checked:after {
|
||||
display: block;
|
||||
top: 14%;
|
||||
left: 14%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.ohif-check-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './checkbox.css';
|
||||
|
||||
export class Checkbox extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { checked: props.checked, label: props.label };
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({ checked: e.target.checked });
|
||||
}
|
||||
|
||||
render() {
|
||||
let checkbox;
|
||||
if (this.state.checked) {
|
||||
checkbox = <span className="ohif-checkbox ohif-checked" />;
|
||||
} else {
|
||||
checkbox = <span className="ohif-checkbox" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ohif-check-container">
|
||||
<form>
|
||||
<label className="ohif-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={this.state.checked}
|
||||
onChange={e => {
|
||||
this.handleChange(e);
|
||||
}}
|
||||
/>
|
||||
{checkbox}
|
||||
{this.state.label}
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Checkbox } from './checkbox.js';
|
||||
@@ -0,0 +1,202 @@
|
||||
import './CineDialog.styl';
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class CineDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
cineFrameRate: props.cineFrameRate,
|
||||
isPlaying: props.isPlaying,
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
/** Minimum value for range slider */
|
||||
cineMinFrameRate: PropTypes.number.isRequired,
|
||||
/** Maximum value for range slider */
|
||||
cineMaxFrameRate: PropTypes.number.isRequired,
|
||||
/** Increment range slider can "step" in either direction. */
|
||||
cineStepFrameRate: PropTypes.number.isRequired,
|
||||
cineFrameRate: PropTypes.number.isRequired,
|
||||
/** 'True' if playing, 'False' if paused. */
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
onPlayPauseChanged: PropTypes.func,
|
||||
onFrameRateChanged: PropTypes.func,
|
||||
onClickNextButton: PropTypes.func,
|
||||
onClickBackButton: PropTypes.func,
|
||||
onClickSkipToStart: PropTypes.func,
|
||||
onClickSkipToEnd: PropTypes.func,
|
||||
/** i18next translation function */
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
cineMinFrameRate: 1,
|
||||
cineMaxFrameRate: 90,
|
||||
cineStepFrameRate: 1,
|
||||
cineFrameRate: 24,
|
||||
isPlaying: false,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// TODO: Not sure if we should just switch this to a stateless
|
||||
// fully-controlled component instead
|
||||
if (
|
||||
this.props.isPlaying !== prevProps.isPlaying ||
|
||||
this.props.isPlaying !== this.state.isPlaying
|
||||
) {
|
||||
this.setState({
|
||||
isPlaying: this.props.isPlaying,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.cineFrameRate !== prevProps.cineFrameRate ||
|
||||
this.props.cineFrameRate !== this.state.cineFrameRate
|
||||
) {
|
||||
this.setState({
|
||||
cineFrameRate: this.props.cineFrameRate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange = event => {
|
||||
const target = event.target;
|
||||
|
||||
let value = target.value;
|
||||
|
||||
if (target.type === 'range') {
|
||||
value = parseFloat(target.value);
|
||||
}
|
||||
|
||||
const name = target.name;
|
||||
|
||||
this.setState({
|
||||
[name]: value,
|
||||
});
|
||||
|
||||
if (name === 'cineFrameRate' && this.props.onFrameRateChanged) {
|
||||
this.props.onFrameRateChanged(parseFloat(value));
|
||||
}
|
||||
};
|
||||
|
||||
onClickPlayPause = () => {
|
||||
const value = !this.state.isPlaying;
|
||||
|
||||
this.setState({
|
||||
isPlaying: value,
|
||||
});
|
||||
|
||||
if (this.props.onPlayPauseChanged) {
|
||||
this.props.onPlayPauseChanged(value);
|
||||
}
|
||||
};
|
||||
|
||||
onClickNextButton = event => {
|
||||
if (this.props.onClickNextButton) {
|
||||
this.props.onClickNextButton(event);
|
||||
}
|
||||
};
|
||||
|
||||
onClickBackButton = event => {
|
||||
if (this.props.onClickBackButton) {
|
||||
this.props.onClickBackButton(event);
|
||||
}
|
||||
};
|
||||
|
||||
onClickSkipToStart = event => {
|
||||
if (this.props.onClickSkipToStart) {
|
||||
this.props.onClickSkipToStart(event);
|
||||
}
|
||||
};
|
||||
|
||||
onClickSkipToEnd = event => {
|
||||
if (this.props.onClickSkipToEnd) {
|
||||
this.props.onClickSkipToEnd(event);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// - Add next / previous display set buttons which just call
|
||||
// onClickNextDisplaySet and onClickPreviousDisplaySet which are passed in as props.
|
||||
// See https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.html#L38
|
||||
// - Add 'isEnabled' prop: https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.js#L301
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<div className="CineDialog">
|
||||
<div className="noselect double-row-style">
|
||||
<div className="cine-controls">
|
||||
<div className="btn-group">
|
||||
<button
|
||||
title={t('Skip to first image')}
|
||||
className="btn"
|
||||
data-toggle="tooltip"
|
||||
onClick={this.onClickSkipToStart}
|
||||
>
|
||||
<Icon name="fast-backward" />
|
||||
</button>
|
||||
<button
|
||||
title={t('Previous image')}
|
||||
className="btn"
|
||||
data-toggle="tooltip"
|
||||
onClick={this.onClickBackButton}
|
||||
>
|
||||
<Icon name="step-backward" />
|
||||
</button>
|
||||
<button
|
||||
title={t('Play / Stop')}
|
||||
className="btn"
|
||||
data-toggle="tooltip"
|
||||
onClick={this.onClickPlayPause}
|
||||
>
|
||||
<Icon name={this.state.isPlaying ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button
|
||||
title={t('Next image')}
|
||||
className="btn"
|
||||
data-toggle="tooltip"
|
||||
onClick={this.onClickNextButton}
|
||||
>
|
||||
<Icon name="step-forward" />
|
||||
</button>
|
||||
<button
|
||||
title={t('Skip to last image')}
|
||||
className="btn"
|
||||
data-toggle="tooltip"
|
||||
onClick={this.onClickSkipToEnd}
|
||||
>
|
||||
<Icon name="fast-forward" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cine-options">
|
||||
<div className="fps-section">
|
||||
<input
|
||||
type="range"
|
||||
name="cineFrameRate"
|
||||
min={this.props.cineMinFrameRate}
|
||||
max={this.props.cineMaxFrameRate}
|
||||
step={this.props.cineStepFrameRate}
|
||||
value={this.state.cineFrameRate}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<span className="fps">
|
||||
{this.state.cineFrameRate.toFixed(1)} {t('fps')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('CineDialog')(CineDialog);
|
||||
export { connectedComponent as CineDialog };
|
||||
export default connectedComponent;
|
||||
@@ -0,0 +1,172 @@
|
||||
@import './../../design/styles/common/global.styl'
|
||||
@import './../../design/styles/common/form.styl'
|
||||
|
||||
.CineDialog
|
||||
background: var(--ui-gray-darkest);
|
||||
color: var(--text-secondary-color);
|
||||
filter: drop-shadow(0 0 3px var(--ui-gray-darkest))
|
||||
border: none
|
||||
border-radius: 8px
|
||||
width: inherit;
|
||||
|
||||
h5
|
||||
font-size: 20px
|
||||
line-height: 35px
|
||||
margin: 0
|
||||
|
||||
h5, label
|
||||
font-weight: 400
|
||||
|
||||
.cine-navigation, .cine-controls, .cine-options
|
||||
cursor: default
|
||||
|
||||
.fps-section
|
||||
input[type="range"]
|
||||
background-color: transparent
|
||||
border: 0 none
|
||||
outline: 0 none
|
||||
|
||||
&::-ms-tooltip
|
||||
display: none
|
||||
|
||||
.double-row-style
|
||||
box-sizing: border-box
|
||||
width: 290px
|
||||
height: 70px
|
||||
padding: 10px
|
||||
top: 2%
|
||||
left: 35%
|
||||
|
||||
.cine-navigation
|
||||
position: absolute
|
||||
right: 16px
|
||||
top: 10px
|
||||
|
||||
.btn
|
||||
padding: 0 4px
|
||||
|
||||
i
|
||||
font-size: 32px
|
||||
line-height: 32px
|
||||
|
||||
.cine-controls
|
||||
left: 0px
|
||||
|
||||
.cine-options
|
||||
padding: 0px 0
|
||||
|
||||
.fps-section
|
||||
width: 175px
|
||||
float: left
|
||||
|
||||
.fps
|
||||
float: right
|
||||
|
||||
|
||||
.single-row-style
|
||||
box-sizing: content-box
|
||||
width: 425px
|
||||
height: 45px
|
||||
padding: 4px 8px
|
||||
top: 2%
|
||||
left: 35%
|
||||
|
||||
.cine-navigation
|
||||
float: right
|
||||
overflow: hidden
|
||||
padding-right: 12px
|
||||
position: relative
|
||||
|
||||
.btn
|
||||
padding: 0 2px
|
||||
margin: 0 0 0 2px
|
||||
border: 0 none
|
||||
&:first-of-type
|
||||
margin-left: 0
|
||||
|
||||
i
|
||||
font-size: 32px
|
||||
line-height: 45px
|
||||
|
||||
.cine-controls
|
||||
position: relative
|
||||
float: left
|
||||
overflow: hidden
|
||||
|
||||
.btn
|
||||
font-size: 22px
|
||||
line-height: 45px
|
||||
min-width: 28px
|
||||
padding: 0 0px
|
||||
margin: 0 4px
|
||||
border: 0 none
|
||||
|
||||
.cine-options
|
||||
display: block
|
||||
position: relative
|
||||
width: 150px
|
||||
float: left
|
||||
overflow: hidden
|
||||
padding: 0
|
||||
margin: 0 0 0 10px
|
||||
|
||||
.fps-section
|
||||
display: block
|
||||
float: left
|
||||
width: 80px
|
||||
input[type="range"]
|
||||
line-height: 45px
|
||||
height: 45px
|
||||
|
||||
.fps
|
||||
display: block
|
||||
width: 68px
|
||||
float: left
|
||||
padding: 0
|
||||
margin: 0 0 0 2px
|
||||
text-align: center
|
||||
line-height: 45px
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
|
||||
.btn
|
||||
color: var(--text-secondary-color);
|
||||
background-color: transparent
|
||||
border: 0
|
||||
border-radius: 4px
|
||||
font-size: 15px
|
||||
width: 45px
|
||||
font-weight: normal
|
||||
height: 37px
|
||||
line-height: 37px
|
||||
padding: 0 12px
|
||||
|
||||
&:hover, &:active, &.active
|
||||
color: var(--hover-color);
|
||||
|
||||
&[disabled]
|
||||
&:hover
|
||||
color: inherit
|
||||
&:active
|
||||
color: var(--text-secondary-color);
|
||||
|
||||
&.btn-primary, &.btn-secondary, &.btn-danger
|
||||
transition: background-color 0.3s ease
|
||||
|
||||
&:hover, &:active, &:focus
|
||||
opacity: 0.8
|
||||
|
||||
&.btn-primary
|
||||
background-color: var(--active-color);
|
||||
color: var(--primary-background-color)
|
||||
|
||||
&.btn-secondary
|
||||
background-color: var(--text-secondary-color);
|
||||
color: var(--primary-background-color)
|
||||
|
||||
&.btn-danger
|
||||
background-color: var(--text-destructive-color);
|
||||
color: var(--text-primary-color);
|
||||
|
||||
&.active
|
||||
box-shadow: inset 1px 1px 2px var(--ui-gray-darkest)
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: CINE Dialog
|
||||
menu: Components
|
||||
route: /components/cine-dialog
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { CineDialog } from './../index'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
|
||||
# CINE Dialog
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State
|
||||
initial={{
|
||||
isPlaying: false,
|
||||
cineFrameRate: 24,
|
||||
isLoopEnabled: true,
|
||||
lastChange: '',
|
||||
}}
|
||||
>
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<pre>{JSON.stringify(state, null, 2)}</pre>
|
||||
</div>
|
||||
<div style={{ maxWidth: '300px', margin: '0 auto' }}>
|
||||
<CineDialog
|
||||
{...state}
|
||||
onClickSkipToStart={() =>
|
||||
setState({ lastChange: 'Clicked SkipToStart' })
|
||||
}
|
||||
onClickSkipToEnd={() =>
|
||||
setState({ lastChange: 'Clicked SkipToEnd' })
|
||||
}
|
||||
onClickNextButton={() => setState({ lastChange: 'Clicked Next' })}
|
||||
onClickBackButton={() => setState({ lastChange: 'Clicked Back' })}
|
||||
onLoopChanged={value => setState({ isLoopEnabled: value })}
|
||||
onFrameRateChanged={value => setState({ cineFrameRate: value })}
|
||||
onPlayPauseChanged={() => setState({ isPlaying: !state.isPlaying })}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</State>
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={CineDialog} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="CineDialog" />
|
||||
@@ -0,0 +1,2 @@
|
||||
import CineDialog from './CineDialog';
|
||||
export { CineDialog };
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ExampleDropTarget,
|
||||
StudyBrowser,
|
||||
ThumbnailEntry,
|
||||
} from './studyBrowser';
|
||||
import { LayoutButton, LayoutChooser } from './layoutButton';
|
||||
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
|
||||
import { Overlay, OverlayTrigger } from './overlayTrigger';
|
||||
import { TableList, TableListItem } from './tableList';
|
||||
import {
|
||||
AboutModal,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
} from './userPreferencesModal';
|
||||
|
||||
import { CineDialog } from './cineDialog';
|
||||
import { QuickSwitch } from './quickSwitch';
|
||||
import { RoundedButtonGroup } from './roundedButtonGroup';
|
||||
import { SelectTree } from './selectTree';
|
||||
import { SimpleDialog } from './simpleDialog';
|
||||
import { StudyList } from './studyList';
|
||||
import { ToolbarSection } from './toolbarSection';
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
export {
|
||||
CineDialog,
|
||||
ExampleDropTarget,
|
||||
LayoutButton,
|
||||
LayoutChooser,
|
||||
MeasurementTable,
|
||||
MeasurementTableItem,
|
||||
Overlay,
|
||||
OverlayTrigger,
|
||||
QuickSwitch,
|
||||
RoundedButtonGroup,
|
||||
SelectTree,
|
||||
SimpleDialog,
|
||||
StudyBrowser,
|
||||
StudyList,
|
||||
TableList,
|
||||
TableListItem,
|
||||
ThumbnailEntry,
|
||||
ToolbarSection,
|
||||
Tooltip,
|
||||
AboutModal,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
import './LanguageSwitcher.styl';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const getCurrentLanguage = (language = i18n.language) =>
|
||||
language.split('-')[0];
|
||||
|
||||
const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage());
|
||||
const languages = [
|
||||
// TODO: list of available languages should come from i18n.options.resources
|
||||
{
|
||||
value: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
value: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = () => {
|
||||
const { value } = event.target;
|
||||
const language = getCurrentLanguage(value);
|
||||
setCurrentLanguage(language);
|
||||
|
||||
i18n.init({
|
||||
fallbackLng: language,
|
||||
lng: language,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
i18n.on('languageChanged', () => {
|
||||
if (mounted) {
|
||||
setCurrentLanguage(getCurrentLanguage());
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<select
|
||||
name="language-select"
|
||||
id="language-select"
|
||||
className="language-select"
|
||||
value={currentLanguage}
|
||||
onChange={onChange}
|
||||
>
|
||||
{languages.map(language => (
|
||||
<option key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
|
||||
@@ -0,0 +1,4 @@
|
||||
.language-select
|
||||
color: var(--primary-background-color)
|
||||
display: block
|
||||
min-width: 150px
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LanguageSwitcher';
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { LayoutChooser } from './LayoutChooser.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import ToolbarButton from '../../viewer/ToolbarButton';
|
||||
|
||||
export class LayoutButton extends PureComponent {
|
||||
static defaultProps = {
|
||||
dropdownVisible: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dropdownVisible: PropTypes.bool.isRequired,
|
||||
/** Called with the selectedCell number when grid sell is selected */
|
||||
onChange: PropTypes.func,
|
||||
/** The cell to show as selected */
|
||||
selectedCell: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
dropdownVisible: this.props.dropdownVisible,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.dropdownVisible !== prevProps.dropdownVisible) {
|
||||
this.setState({
|
||||
dropdownVisible: this.props.dropdownVisible,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
this.setState({
|
||||
dropdownVisible: !this.state.dropdownVisible,
|
||||
});
|
||||
};
|
||||
|
||||
onChange = selectedCell => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(selectedCell);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="btn-group">
|
||||
<ToolbarButton
|
||||
isActive={this.state.dropdownVisible}
|
||||
label={'Layout'}
|
||||
icon="th"
|
||||
onClick={this.onClick}
|
||||
/>
|
||||
<LayoutChooser
|
||||
visible={this.state.dropdownVisible}
|
||||
onChange={this.onChange}
|
||||
onClick={this.onClick}
|
||||
selectedCell={this.props.selectedCell}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LayoutButton;
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './LayoutChooser.styl';
|
||||
|
||||
/**
|
||||
* Adds the 'hover' class to cells above and to the left of the current cell
|
||||
* This is used to "fill in" the grid that the user will change the layout to,
|
||||
* if they click on a specific table cell.
|
||||
**/
|
||||
|
||||
class LayoutChooser extends PureComponent {
|
||||
static propTypes = {
|
||||
rows: PropTypes.number.isRequired,
|
||||
columns: PropTypes.number.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
selectedCell: PropTypes.object,
|
||||
boxSize: PropTypes.number.isRequired,
|
||||
cellBorder: PropTypes.number.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
visible: true,
|
||||
boxSize: 20,
|
||||
cellBorder: 1,
|
||||
selectedCell: {
|
||||
row: -1,
|
||||
col: -1,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.emptyCell = {
|
||||
row: -1,
|
||||
column: -1,
|
||||
};
|
||||
this.state = {
|
||||
table: [[]],
|
||||
selectedCell: this.props.selectedCell,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.highlightCells(this.emptyCell);
|
||||
}
|
||||
onClick(currentCell) {
|
||||
this.setState({
|
||||
selectedCell: currentCell,
|
||||
});
|
||||
this.highlightCells(currentCell);
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(currentCell);
|
||||
}
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(currentCell);
|
||||
}
|
||||
}
|
||||
isRange = (cell, parentCell) => {
|
||||
return cell.row <= parentCell.row && cell.col <= parentCell.col;
|
||||
};
|
||||
highlightCells = currentCell => {
|
||||
let table = [];
|
||||
for (let row = 0; row < this.props.rows; row++) {
|
||||
let newRow = [];
|
||||
for (let col = 0; col < this.props.columns; col++) {
|
||||
let cell = { row: row, col: col };
|
||||
if (this.isRange(cell, currentCell)) {
|
||||
cell.className = 'hover';
|
||||
} else if (
|
||||
this.isRange(currentCell, this.emptyCell) &&
|
||||
this.isRange(cell, this.state.selectedCell)
|
||||
) {
|
||||
cell.className = 'selectedBefore';
|
||||
}
|
||||
newRow.push(cell);
|
||||
}
|
||||
table.push(newRow);
|
||||
}
|
||||
this.setState({ table: table });
|
||||
};
|
||||
|
||||
render() {
|
||||
let columns = this.props.columns;
|
||||
const style = {
|
||||
display: this.props.visible ? 'block' : 'none',
|
||||
minWidth:
|
||||
columns * this.props.boxSize + (columns + 5) * this.props.cellBorder,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="layoutChooser layoutChooser-dropdown-menu"
|
||||
role="menu"
|
||||
style={style}
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
{this.state.table.map((row, i) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
{row.map((cell, j) => {
|
||||
return (
|
||||
<td
|
||||
className={cell.className}
|
||||
style={{
|
||||
width: this.props.boxSize,
|
||||
height: this.props.boxSize,
|
||||
border: 'solid 1px black',
|
||||
}}
|
||||
key={j}
|
||||
onMouseEnter={() => this.highlightCells(cell)}
|
||||
onMouseLeave={() => this.highlightCells(this.emptyCell)}
|
||||
onClick={() => this.onClick(cell)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { LayoutChooser };
|
||||
@@ -0,0 +1,47 @@
|
||||
@import './../../design/styles/common/button.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
$borderColor = rgba(77, 99, 110, 0.81)
|
||||
|
||||
.layoutChooser-dropdown-menu
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
background-color: #fff;
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0,0,0,.15);
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,.175);
|
||||
|
||||
.layoutChooser
|
||||
.selectedBefore
|
||||
background-color: #5cc3eb
|
||||
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 8px
|
||||
padding: 5px 0;
|
||||
position: absolute;
|
||||
z-index: 5000
|
||||
|
||||
table
|
||||
margin: 0 auto
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
td
|
||||
cursor: pointer
|
||||
transition(background-color 0.1s ease)
|
||||
|
||||
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
|
||||
background-color: #209ac9
|
||||
@@ -0,0 +1,10 @@
|
||||
.LayoutExample {
|
||||
background-color: var(--primary-background-color);
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.GridLayout td {
|
||||
padding: 5px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Layout Button
|
||||
menu: Components
|
||||
route: /components/layout-button
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { LayoutButton } from './../index.js'
|
||||
import './layoutButton.css'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
|
||||
# Layout Button
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
selectedCell: {
|
||||
className: 'hover',
|
||||
col: 1,
|
||||
row: 1
|
||||
},
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
{/* STATE */}
|
||||
<div><pre>{JSON.stringify(state, null, 2) }</pre></div>
|
||||
|
||||
{/* COMPONENT */}
|
||||
<div className="LayoutExample">
|
||||
<LayoutButton
|
||||
{...state}
|
||||
onChange={cell => setState({ selectedCell: cell })}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={LayoutButton} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="Buttons" />
|
||||
@@ -0,0 +1,2 @@
|
||||
export { LayoutButton } from './LayoutButton.js';
|
||||
export { LayoutChooser } from './LayoutChooser.js';
|
||||
@@ -0,0 +1,178 @@
|
||||
import './MeasurementTable.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { MeasurementTableItem } from './MeasurementTableItem.js';
|
||||
import { OverlayTrigger } from './../overlayTrigger';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js';
|
||||
import { TableList } from './../tableList';
|
||||
import { Tooltip } from './../tooltip';
|
||||
|
||||
class MeasurementTable extends Component {
|
||||
static propTypes = {
|
||||
measurementCollection: PropTypes.array.isRequired,
|
||||
timepoints: PropTypes.array.isRequired,
|
||||
overallWarnings: PropTypes.object.isRequired,
|
||||
readOnly: PropTypes.bool,
|
||||
onItemClick: PropTypes.func,
|
||||
onRelabelClick: PropTypes.func,
|
||||
onDeleteClick: PropTypes.func,
|
||||
onEditDescriptionClick: PropTypes.func,
|
||||
selectedMeasurementNumber: PropTypes.number,
|
||||
overwallWarnings: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
overallWarnings: {
|
||||
warningList: [],
|
||||
},
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedKey: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const hasOverallWarnings =
|
||||
this.props.overallWarnings.warningList.length > 0;
|
||||
return (
|
||||
<div className="measurementTable">
|
||||
<div className="measurementTableHeader">
|
||||
{hasOverallWarnings && (
|
||||
<OverlayTrigger
|
||||
key={'overwall-warning'}
|
||||
placement="left"
|
||||
overlay={
|
||||
<Tooltip
|
||||
placement="left"
|
||||
className="in tooltip-warning"
|
||||
id="tooltip-left"
|
||||
style={{}}
|
||||
>
|
||||
<div className="warningTitle">
|
||||
{this.props.t('Criteria nonconformities')}
|
||||
</div>
|
||||
<div className="warningContent">
|
||||
{this.getWarningContent()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<span className="warning-status">
|
||||
<span className="warning-border">
|
||||
<Icon name="exclamation-triangle" />
|
||||
</span>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{this.getTimepointsHeader()}
|
||||
</div>
|
||||
<ScrollableArea>
|
||||
<div>{this.getMeasurementsGroups()}</div>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getMeasurementsGroups = () => {
|
||||
return this.props.measurementCollection.map((measureGroup, index) => {
|
||||
return (
|
||||
<TableList
|
||||
key={index}
|
||||
customHeader={this.getCustomHeader(measureGroup)}
|
||||
>
|
||||
{this.getMeasurements(measureGroup)}
|
||||
</TableList>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
getMeasurements = measureGroup => {
|
||||
const selectedKey = this.props.selectedMeasurementNumber
|
||||
? this.props.selectedMeasurementNumber
|
||||
: this.state.selectedKey;
|
||||
return measureGroup.measurements.map((measurement, index) => {
|
||||
const key = measurement.measurementNumber;
|
||||
const itemIndex = measurement.itemNumber || index + 1;
|
||||
const itemClass =
|
||||
selectedKey === key && !this.props.readOnly ? 'selected' : '';
|
||||
|
||||
return (
|
||||
<MeasurementTableItem
|
||||
key={key}
|
||||
itemIndex={itemIndex}
|
||||
itemClass={itemClass}
|
||||
measurementData={measurement}
|
||||
onItemClick={this.onItemClick}
|
||||
onRelabel={this.props.onRelabelClick}
|
||||
onDelete={this.props.onDeleteClick}
|
||||
onEditDescription={this.props.onEditDescriptionClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
onItemClick = (event, measurementData) => {
|
||||
if (this.props.readOnly) return;
|
||||
|
||||
this.setState({
|
||||
selectedKey: measurementData.measurementNumber,
|
||||
});
|
||||
|
||||
if (this.props.onItemClick) {
|
||||
this.props.onItemClick(event, measurementData);
|
||||
}
|
||||
};
|
||||
|
||||
getCustomHeader = measureGroup => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="tableListHeaderTitle">
|
||||
{this.props.t(measureGroup.groupName)}
|
||||
</div>
|
||||
{measureGroup.maxMeasurements && (
|
||||
<div className="maxMeasurements">
|
||||
{this.props.t('MAX')} {measureGroup.maxMeasurements}
|
||||
</div>
|
||||
)}
|
||||
<div className="numberOfItems">{measureGroup.measurements.length}</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
getTimepointsHeader = () => {
|
||||
return this.props.timepoints.map((timepoint, index) => {
|
||||
return (
|
||||
<div key={index} className="measurementTableHeaderItem">
|
||||
<div className="timepointLabel">{this.props.t(timepoint.key)}</div>
|
||||
<div className="timepointDate">{timepoint.date}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
getWarningContent = () => {
|
||||
const { warningList = '' } = this.props.overwallWarnings;
|
||||
|
||||
if (Array.isArray(warningList)) {
|
||||
const listedWarnings = warningList.map((warn, index) => {
|
||||
return <li key={index}>{warn}</li>;
|
||||
});
|
||||
|
||||
return <ol>{listedWarnings}</ol>;
|
||||
} else {
|
||||
return <React.Fragment>{warningList}</React.Fragment>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation(['MeasurementTable', 'Common'])(
|
||||
MeasurementTable
|
||||
);
|
||||
export { connectedComponent as MeasurementTable };
|
||||
export default connectedComponent;
|
||||
@@ -0,0 +1,92 @@
|
||||
.measurementTable
|
||||
background-color: var(--primary-background-color)
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.maxMeasurements
|
||||
border-radius: 3px;
|
||||
color: var(--primary-background-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
height: 19px;
|
||||
line-height: 17px;
|
||||
margin-top: 22px;
|
||||
margin-right: 15px;
|
||||
padding: 2px 6px 0;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--default-color);
|
||||
|
||||
.measurementTableHeader
|
||||
padding-left: 45px;
|
||||
|
||||
.measurementTableHeaderItem
|
||||
display: inline-block;
|
||||
margin-top: 9px;
|
||||
margin-left: 9px;
|
||||
padding-left: 9px;
|
||||
width: 90px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
border-left: 1px solid var(--text-secondary-color);
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.timepointLabel
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 12px;
|
||||
|
||||
.warning-status
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
padding-left: 7px;
|
||||
left: 15px;
|
||||
top: 7px;
|
||||
|
||||
.warning-border
|
||||
display: block;
|
||||
border: 2px solid #e29e4a;
|
||||
border-radius: 36px;
|
||||
padding: 6px 7px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
svg
|
||||
color: #e29e4a;
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
pointer-events: inherit;
|
||||
|
||||
.tooltip-warning
|
||||
&.in
|
||||
opacity 1;
|
||||
|
||||
.tooltip-inner
|
||||
padding: 0
|
||||
|
||||
.warningTitle
|
||||
text-align: left
|
||||
background-color: var(--ui-gray-darkest);
|
||||
padding: 7px 15px;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
|
||||
.warningContent
|
||||
text-align center
|
||||
background-color: var(--ui-gray);
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
|
||||
ol
|
||||
margin-top: auto;
|
||||
padding-inline-start: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 25px;
|
||||
text-align: left;
|
||||
|
||||
li
|
||||
padding-right: 10px;
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { OverlayTrigger } from './../overlayTrigger';
|
||||
import { Tooltip } from './../tooltip';
|
||||
import { TableListItem } from './../tableList/TableListItem.js';
|
||||
|
||||
import './MeasurementTableItem.styl';
|
||||
|
||||
class MeasurementTableItem extends Component {
|
||||
static propTypes = {
|
||||
measurementData: PropTypes.object.isRequired,
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
onRelabel: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onEditDescription: PropTypes.func,
|
||||
itemClass: PropTypes.string,
|
||||
itemIndex: PropTypes.number,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { warningTitle = '', hasWarnings } = this.props.measurementData;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{hasWarnings ? (
|
||||
<OverlayTrigger
|
||||
key={this.props.itemIndex}
|
||||
placement="left"
|
||||
overlay={
|
||||
<Tooltip
|
||||
placement="left"
|
||||
className="in tooltip-warning"
|
||||
id="tooltip-left"
|
||||
>
|
||||
<div className="warningTitle">{this.props.t(warningTitle)}</div>
|
||||
<div className="warningContent">{this.getWarningContent()}</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div>{this.getTableListItem()}</div>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
getActionButton = (btnLabel, onClickCallback) => {
|
||||
return (
|
||||
<button key={btnLabel} className="btnAction" onClick={onClickCallback}>
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
{this.props.t(btnLabel)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
getTableListItem = () => {
|
||||
const hasWarningClass = this.props.measurementData.hasWarnings
|
||||
? 'hasWarnings'
|
||||
: '';
|
||||
|
||||
const actionButtons = [];
|
||||
|
||||
if (typeof this.props.onRelabel === 'function') {
|
||||
const relabelButton = this.getActionButton(
|
||||
'Relabel',
|
||||
this.onRelabelClick
|
||||
);
|
||||
actionButtons.push(relabelButton);
|
||||
}
|
||||
if (typeof this.props.onEditDescription === 'function') {
|
||||
const descriptionButton = this.getActionButton(
|
||||
'Description',
|
||||
this.onEditDescriptionClick
|
||||
);
|
||||
actionButtons.push(descriptionButton);
|
||||
}
|
||||
if (typeof this.props.onDelete === 'function') {
|
||||
const deleteButton = this.getActionButton('Delete', this.onDeleteClick);
|
||||
actionButtons.push(deleteButton);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableListItem
|
||||
itemKey={this.props.measurementData.measurementNumber}
|
||||
itemClass={`measurementItem ${this.props.itemClass} ${hasWarningClass}`}
|
||||
itemIndex={this.props.itemIndex}
|
||||
onItemClick={this.onItemClick}
|
||||
>
|
||||
<div>
|
||||
<div className="measurementLocation">
|
||||
{this.props.t(this.props.measurementData.label, {
|
||||
keySeparator: '>',
|
||||
nsSeparator: '|',
|
||||
})}
|
||||
</div>
|
||||
<div>{this.getDataDisplayText()}</div>
|
||||
<div className="rowActions">{actionButtons}</div>
|
||||
</div>
|
||||
</TableListItem>
|
||||
);
|
||||
};
|
||||
|
||||
onItemClick = event => {
|
||||
this.props.onItemClick(event, this.props.measurementData);
|
||||
};
|
||||
|
||||
onRelabelClick = event => {
|
||||
// Prevent onItemClick from firing
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.onRelabel(event, this.props.measurementData);
|
||||
};
|
||||
|
||||
onEditDescriptionClick = event => {
|
||||
// Prevent onItemClick from firing
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.onEditDescription(event, this.props.measurementData);
|
||||
};
|
||||
|
||||
onDeleteClick = event => {
|
||||
// Prevent onItemClick from firing
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.onDelete(event, this.props.measurementData);
|
||||
};
|
||||
|
||||
getDataDisplayText = () => {
|
||||
return this.props.measurementData.data.map((data, index) => {
|
||||
return (
|
||||
<div key={`displayText_${index}`} className="measurementDisplayText">
|
||||
{data.displayText ? data.displayText : '...'}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
getWarningContent = () => {
|
||||
const { warningList = '' } = this.props.measurementData;
|
||||
|
||||
if (Array.isArray(warningList)) {
|
||||
const listedWarnings = warningList.map((warn, index) => {
|
||||
return <li key={index}>{warn}</li>;
|
||||
});
|
||||
|
||||
return <ol>{listedWarnings}</ol>;
|
||||
} else {
|
||||
return <React.Fragment>{warningList}</React.Fragment>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('MeasurementTable')(
|
||||
MeasurementTableItem
|
||||
);
|
||||
export { connectedComponent as MeasurementTableItem };
|
||||
export default connectedComponent;
|
||||
@@ -0,0 +1,61 @@
|
||||
.measurementItem
|
||||
.rowActions
|
||||
background-color: var(--ui-gray-darker)
|
||||
height: 0
|
||||
overflow: hidden
|
||||
transition: all 0.3s ease
|
||||
visibility: hidden
|
||||
padding-left: 14px
|
||||
|
||||
.btnAction
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--default-color)
|
||||
cursor: pointer
|
||||
line-height: 35px
|
||||
height: 35px
|
||||
transition: all 0.3s ease
|
||||
|
||||
&:hover, &:active
|
||||
color: var(--text-primary-color)
|
||||
|
||||
i
|
||||
margin-right: 4px;
|
||||
|
||||
&.selected
|
||||
.rowActions
|
||||
height: 35px
|
||||
visibility: visible
|
||||
|
||||
.measurementLocation
|
||||
margin-left: 9px;
|
||||
margin-top: 9px;
|
||||
|
||||
.measurementDisplayText
|
||||
display: inline-block;
|
||||
margin-top: 9px;
|
||||
margin-left: 9px;
|
||||
padding-left: 9px;
|
||||
width: 90px;
|
||||
border-left: 1px solid var(--text-secondary-color);
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.itemIndex
|
||||
-webkit-box-sizing: initial;
|
||||
-moz-box-sizing: initial;
|
||||
box-sizing: initial;
|
||||
|
||||
&.hasWarnings
|
||||
.itemIndex
|
||||
opacity: 1
|
||||
background-color: #e29e4a;
|
||||
color: #fff;
|
||||
|
||||
.warning-icon
|
||||
display: block;
|
||||
margin: 7px auto 0;
|
||||
|
||||
svg
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
pointer-events: inherit;
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Measurement Table
|
||||
menu: Components
|
||||
route: /components/measurement-table
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { MeasurementTable } from './../index.js'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
//
|
||||
import measurements from './measurements.js'
|
||||
import timepoints from './timepoints.js'
|
||||
import warnings from './warnings.js'
|
||||
|
||||
# Measurement Table
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State
|
||||
initial={{
|
||||
// Spelling error: overwallWarnings
|
||||
overwallWarnings: warnings,
|
||||
timepoints,
|
||||
measurementCollection: measurements,
|
||||
}}
|
||||
>
|
||||
{({ state, setState }) => (
|
||||
<div style={{ height: '340px' }}>
|
||||
<MeasurementTable
|
||||
{...state}
|
||||
onItemClick={() => alert('onItemClick')}
|
||||
onRelabelClick={() => alert('onRelabelClick')}
|
||||
onDeleteClick={() => alert('onDeleteClick')}
|
||||
onEditDescriptionClick={() => alert('onEditDescriptionClick')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</State>
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={MeasurementTable} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="MeasurementTable" />
|
||||
@@ -0,0 +1,93 @@
|
||||
export default [
|
||||
{
|
||||
maxMeasurements: 5,
|
||||
groupName: 'Targets',
|
||||
measurements: [
|
||||
{
|
||||
measurementId: '123',
|
||||
measurementNumber: '123',
|
||||
itemNumber: 1,
|
||||
label: 'Chest Wall Posterior',
|
||||
hasWarnings: true,
|
||||
warningTitle: 'Criteria nonconformities',
|
||||
isSplitLesion: false,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
data: [
|
||||
{
|
||||
displayText: '25.7 x 12.9',
|
||||
},
|
||||
{
|
||||
displayText: '24.7 x 11.5',
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{
|
||||
measurementId: '124',
|
||||
measurementNumber: '124',
|
||||
itemNumber: 2,
|
||||
label: 'Bone Extremity',
|
||||
data: [
|
||||
{
|
||||
displayText: '24.7 x 11.1',
|
||||
},
|
||||
{
|
||||
displayText: '21.2 x 10.9',
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
maxMeasurements: 3,
|
||||
groupName: 'NonTargets',
|
||||
measurements: [
|
||||
{
|
||||
measurementId: '125',
|
||||
measurementNumber: '125',
|
||||
itemNumber: 1,
|
||||
label: 'Chest Wall Single Site',
|
||||
data: [
|
||||
{
|
||||
displayText: 'MD',
|
||||
},
|
||||
{
|
||||
displayText: 'NM',
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{
|
||||
measurementId: '126',
|
||||
measurementNumber: '126',
|
||||
itemNumber: 2,
|
||||
label: 'Extremity Multiple Sites',
|
||||
data: [
|
||||
{
|
||||
displayText: 'CR',
|
||||
},
|
||||
{},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{
|
||||
measurementId: '127',
|
||||
measurementNumber: '127',
|
||||
label: 'Extremity Site',
|
||||
data: [
|
||||
{
|
||||
displayText: 'CR',
|
||||
},
|
||||
{},
|
||||
{
|
||||
displayText: 'NM',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,14 @@
|
||||
export default [
|
||||
{
|
||||
label: 'Follow-up 2',
|
||||
date: '20-Dec-18',
|
||||
},
|
||||
{
|
||||
label: 'Follow-up 1',
|
||||
date: '15-Jun-18',
|
||||
},
|
||||
{
|
||||
label: 'Baseline',
|
||||
date: '10-Apr-18',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { MeasurementTable } from './MeasurementTable.js';
|
||||
export { MeasurementTableItem } from './MeasurementTableItem.js';
|
||||
@@ -0,0 +1,102 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Transition, {
|
||||
ENTERED,
|
||||
ENTERING,
|
||||
} from 'react-transition-group/Transition';
|
||||
|
||||
const propTypes = {
|
||||
/**
|
||||
* Show the component; triggers the fade in or fade out animation
|
||||
*/
|
||||
in: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Wait until the first "enter" transition to mount the component (add it to the DOM)
|
||||
*/
|
||||
mountOnEnter: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Unmount the component (remove it from the DOM) when it is faded out
|
||||
*/
|
||||
unmountOnExit: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Run the fade in animation when the component mounts, if it is initially
|
||||
* shown
|
||||
*/
|
||||
appear: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Duration of the fade animation in milliseconds, to ensure that finishing
|
||||
* callbacks are fired even if the original browser transition end events are
|
||||
* canceled
|
||||
*/
|
||||
timeout: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Callback fired before the component fades in
|
||||
*/
|
||||
onEnter: PropTypes.func,
|
||||
/**
|
||||
* Callback fired after the component starts to fade in
|
||||
*/
|
||||
onEntering: PropTypes.func,
|
||||
/**
|
||||
* Callback fired after the has component faded in
|
||||
*/
|
||||
onEntered: PropTypes.func,
|
||||
/**
|
||||
* Callback fired before the component fades out
|
||||
*/
|
||||
onExit: PropTypes.func,
|
||||
/**
|
||||
* Callback fired after the component starts to fade out
|
||||
*/
|
||||
onExiting: PropTypes.func,
|
||||
/**
|
||||
* Callback fired after the component has faded out
|
||||
*/
|
||||
onExited: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
in: false,
|
||||
timeout: 300,
|
||||
mountOnEnter: false,
|
||||
unmountOnExit: false,
|
||||
appear: false,
|
||||
};
|
||||
|
||||
const fadeStyles = {
|
||||
[ENTERING]: 'in',
|
||||
[ENTERED]: 'in',
|
||||
};
|
||||
|
||||
class Fade extends React.Component {
|
||||
render() {
|
||||
const { className, children, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<Transition {...props}>
|
||||
{(status, innerProps) =>
|
||||
React.cloneElement(children, {
|
||||
...innerProps,
|
||||
className: classNames(
|
||||
'fade',
|
||||
className,
|
||||
children.props.className,
|
||||
fadeStyles[status]
|
||||
),
|
||||
})
|
||||
}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Fade.propTypes = propTypes;
|
||||
Fade.defaultProps = defaultProps;
|
||||
|
||||
export default Fade;
|
||||
@@ -0,0 +1,102 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { cloneElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Overlay as BaseOverlay } from 'react-overlays';
|
||||
import elementType from 'prop-types-extra/lib/elementType';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import Fade from './Fade';
|
||||
|
||||
const propTypes = {
|
||||
/**
|
||||
* Set the visibility of the Overlay
|
||||
*/
|
||||
show: PropTypes.bool,
|
||||
/**
|
||||
* Specify whether the overlay should trigger onHide when the user clicks outside the overlay
|
||||
*/
|
||||
rootClose: PropTypes.bool,
|
||||
/**
|
||||
* A callback invoked by the overlay when it wishes to be hidden. Required if
|
||||
* `rootClose` is specified.
|
||||
*/
|
||||
onHide: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Use animation
|
||||
*/
|
||||
animation: PropTypes.oneOfType([PropTypes.bool, elementType]),
|
||||
|
||||
/**
|
||||
* Callback fired before the Overlay transitions in
|
||||
*/
|
||||
onEnter: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback fired as the Overlay begins to transition in
|
||||
*/
|
||||
onEntering: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback fired after the Overlay finishes transitioning in
|
||||
*/
|
||||
onEntered: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback fired right before the Overlay transitions out
|
||||
*/
|
||||
onExit: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback fired as the Overlay begins to transition out
|
||||
*/
|
||||
onExiting: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback fired after the Overlay finishes transitioning out
|
||||
*/
|
||||
onExited: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Sets the direction of the Overlay.
|
||||
*/
|
||||
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
animation: Fade,
|
||||
rootClose: false,
|
||||
show: false,
|
||||
placement: 'right',
|
||||
};
|
||||
|
||||
class Overlay extends React.Component {
|
||||
render() {
|
||||
const { animation, children, ...props } = this.props;
|
||||
|
||||
const transition = animation === true ? Fade : animation || null;
|
||||
|
||||
let child;
|
||||
|
||||
if (!transition) {
|
||||
child = cloneElement(children, {
|
||||
className: classNames(children.props.className, 'in'),
|
||||
});
|
||||
} else {
|
||||
child = children;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseOverlay {...props} transition={transition}>
|
||||
{child}
|
||||
</BaseOverlay>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Overlay.propTypes = propTypes;
|
||||
Overlay.defaultProps = defaultProps;
|
||||
|
||||
const connectedComponent = withTranslation()(Overlay);
|
||||
export { connectedComponent as Overlay };
|
||||
export default connectedComponent;
|
||||
@@ -0,0 +1,330 @@
|
||||
import contains from 'dom-helpers/query/contains';
|
||||
import React, { cloneElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import warning from 'warning';
|
||||
|
||||
import { Overlay } from './Overlay';
|
||||
|
||||
import createChainedFunction from './createChainedFunction';
|
||||
|
||||
/**
|
||||
* Check if value one is inside or equal to the of value
|
||||
*
|
||||
* @param {string} one
|
||||
* @param {string|array} of
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isOneOf(one, of) {
|
||||
if (Array.isArray(of)) {
|
||||
return of.indexOf(one) >= 0;
|
||||
}
|
||||
return one === of;
|
||||
}
|
||||
|
||||
const triggerType = PropTypes.oneOf(['click', 'hover', 'focus']);
|
||||
|
||||
const propTypes = {
|
||||
/**
|
||||
* Specify which action or actions trigger Overlay visibility
|
||||
*/
|
||||
trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]),
|
||||
|
||||
/**
|
||||
* A millisecond delay amount to show and hide the Overlay once triggered
|
||||
*/
|
||||
delay: PropTypes.number,
|
||||
/**
|
||||
* A millisecond delay amount before showing the Overlay once triggered.
|
||||
*/
|
||||
delayShow: PropTypes.number,
|
||||
/**
|
||||
* A millisecond delay amount before hiding the Overlay once triggered.
|
||||
*/
|
||||
delayHide: PropTypes.number,
|
||||
|
||||
// FIXME: This should be `defaultShow`.
|
||||
/**
|
||||
* The initial visibility state of the Overlay. For more nuanced visibility
|
||||
* control, consider using the Overlay component directly.
|
||||
*/
|
||||
defaultOverlayShown: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* An element or text to overlay next to the target.
|
||||
*/
|
||||
overlay: PropTypes.node.isRequired,
|
||||
|
||||
/**
|
||||
* A function to be called once the hide is triggered
|
||||
*/
|
||||
handleHide: PropTypes.func,
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onBlur: PropTypes.func,
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onFocus: PropTypes.func,
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onMouseOut: PropTypes.func,
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onMouseOver: PropTypes.func,
|
||||
|
||||
// Overridden props from `<Overlay>`.
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
target: PropTypes.oneOf([null]),
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onHide: PropTypes.oneOf([null]),
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
show: PropTypes.oneOf([null]),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
defaultOverlayShown: false,
|
||||
trigger: ['hover', 'focus'],
|
||||
};
|
||||
|
||||
class OverlayTrigger extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.handleDelayedShow = this.handleDelayedShow.bind(this);
|
||||
this.handleDelayedHide = this.handleDelayedHide.bind(this);
|
||||
this.handleHide = createChainedFunction(
|
||||
this.handleHide.bind(this),
|
||||
props.handleHide
|
||||
);
|
||||
|
||||
this.handleMouseOver = e =>
|
||||
this.handleMouseOverOut(this.handleDelayedShow, e, 'fromElement');
|
||||
this.handleMouseOut = e =>
|
||||
this.handleMouseOverOut(this.handleDelayedHide, e, 'toElement');
|
||||
|
||||
this._mountNode = null;
|
||||
|
||||
this.state = {
|
||||
show: props.defaultOverlayShown,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._mountNode = document.createElement('div');
|
||||
this.renderOverlay();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderOverlay();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ReactDOM.unmountComponentAtNode(this._mountNode);
|
||||
this._mountNode = null;
|
||||
|
||||
clearTimeout(this._hoverShowDelay);
|
||||
clearTimeout(this._hoverHideDelay);
|
||||
}
|
||||
|
||||
handleDelayedHide() {
|
||||
if (this._hoverShowDelay != null) {
|
||||
clearTimeout(this._hoverShowDelay);
|
||||
this._hoverShowDelay = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.show || this._hoverHideDelay != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delay =
|
||||
this.props.delayHide != null ? this.props.delayHide : this.props.delay;
|
||||
|
||||
if (!delay) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this._hoverHideDelay = setTimeout(() => {
|
||||
this._hoverHideDelay = null;
|
||||
this.hide();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
handleDelayedShow() {
|
||||
if (this._hoverHideDelay != null) {
|
||||
clearTimeout(this._hoverHideDelay);
|
||||
this._hoverHideDelay = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.show || this._hoverShowDelay != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delay =
|
||||
this.props.delayShow != null ? this.props.delayShow : this.props.delay;
|
||||
|
||||
if (!delay) {
|
||||
this.show();
|
||||
return;
|
||||
}
|
||||
|
||||
this._hoverShowDelay = setTimeout(() => {
|
||||
this._hoverShowDelay = null;
|
||||
this.show();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
handleHide() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
// Simple implementation of mouseEnter and mouseLeave.
|
||||
// React's built version is broken: https://github.com/facebook/react/issues/4251
|
||||
// for cases when the trigger is disabled and mouseOut/Over can cause flicker
|
||||
// moving from one child element to another.
|
||||
handleMouseOverOut(handler, e, relatedNative) {
|
||||
const target = e.currentTarget;
|
||||
const related = e.relatedTarget || e.nativeEvent[relatedNative];
|
||||
|
||||
if ((!related || related !== target) && !contains(target, related)) {
|
||||
handler(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleToggle() {
|
||||
if (this.state.show) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.setState({ show: false });
|
||||
}
|
||||
|
||||
makeOverlay(overlay, props) {
|
||||
return (
|
||||
<Overlay
|
||||
{...props}
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
target={this}
|
||||
>
|
||||
{overlay}
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.setState({ show: true });
|
||||
}
|
||||
|
||||
renderOverlay() {
|
||||
ReactDOM.unstable_renderSubtreeIntoContainer(
|
||||
this,
|
||||
this._overlay,
|
||||
this._mountNode
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
trigger,
|
||||
overlay,
|
||||
children,
|
||||
onBlur,
|
||||
onClick,
|
||||
onFocus,
|
||||
onMouseOut,
|
||||
onMouseOver,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
delete props.delay;
|
||||
delete props.delayShow;
|
||||
delete props.delayHide;
|
||||
delete props.defaultOverlayShown;
|
||||
|
||||
const child = React.Children.only(children);
|
||||
const childProps = child.props;
|
||||
const triggerProps = {};
|
||||
|
||||
if (this.state.show) {
|
||||
triggerProps['aria-describedby'] = overlay.props.id;
|
||||
}
|
||||
|
||||
// FIXME: The logic here for passing through handlers on this component is
|
||||
// inconsistent. We shouldn't be passing any of these props through.
|
||||
|
||||
triggerProps.onClick = createChainedFunction(childProps.onClick, onClick);
|
||||
|
||||
if (isOneOf('click', trigger)) {
|
||||
triggerProps.onClick = createChainedFunction(
|
||||
triggerProps.onClick,
|
||||
this.handleToggle
|
||||
);
|
||||
}
|
||||
|
||||
if (isOneOf('hover', trigger)) {
|
||||
warning(
|
||||
!(trigger === 'hover'),
|
||||
'[react-bootstrap] Specifying only the `"hover"` trigger limits the ' +
|
||||
'visibility of the overlay to just mouse users. Consider also ' +
|
||||
'including the `"focus"` trigger so that touch and keyboard only ' +
|
||||
'users can see the overlay as well.'
|
||||
);
|
||||
|
||||
triggerProps.onMouseOver = createChainedFunction(
|
||||
childProps.onMouseOver,
|
||||
onMouseOver,
|
||||
this.handleMouseOver
|
||||
);
|
||||
triggerProps.onMouseOut = createChainedFunction(
|
||||
childProps.onMouseOut,
|
||||
onMouseOut,
|
||||
this.handleMouseOut
|
||||
);
|
||||
}
|
||||
|
||||
if (isOneOf('focus', trigger)) {
|
||||
triggerProps.onFocus = createChainedFunction(
|
||||
childProps.onFocus,
|
||||
onFocus,
|
||||
this.handleDelayedShow
|
||||
);
|
||||
triggerProps.onBlur = createChainedFunction(
|
||||
childProps.onBlur,
|
||||
onBlur,
|
||||
this.handleDelayedHide
|
||||
);
|
||||
}
|
||||
|
||||
this._overlay = this.makeOverlay(overlay, props);
|
||||
|
||||
return cloneElement(child, triggerProps);
|
||||
}
|
||||
}
|
||||
|
||||
OverlayTrigger.propTypes = propTypes;
|
||||
OverlayTrigger.defaultProps = defaultProps;
|
||||
|
||||
export { OverlayTrigger };
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: Overlay Trigger
|
||||
menu: Components
|
||||
route: /components/overlay-trigger
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { Tooltip } from './../../tooltip'
|
||||
import { OverlayTrigger } from './../index.js'
|
||||
|
||||
# Overlay Trigger
|
||||
|
||||
## Basic usage
|
||||
|
||||
### On Hover
|
||||
|
||||
<Playground>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
{
|
||||
['right', 'left', 'bottom', 'top'].map((placement) => {
|
||||
return (
|
||||
<React.Fragment key={`example-${placement}`}>
|
||||
<OverlayTrigger
|
||||
key={`example-${placement}`}
|
||||
placement={placement}
|
||||
overlay={
|
||||
<Tooltip
|
||||
placement={placement}
|
||||
className='in'
|
||||
id={`tooltip-${placement}`}
|
||||
>
|
||||
Here I am!
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<button> Tooltip on {placement}</button>
|
||||
</OverlayTrigger>
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
</Playground>
|
||||
|
||||
### On Click
|
||||
|
||||
<Playground>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{['right', 'left', 'bottom', 'top'].map(placement => {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={`example-${placement}`}
|
||||
placement={placement}
|
||||
trigger="click"
|
||||
overlay={
|
||||
<Tooltip
|
||||
placement={placement}
|
||||
className="in"
|
||||
id={`tooltip-${placement}`}
|
||||
>
|
||||
Here I am!
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<button> Tooltip on {placement}</button>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={OverlayTrigger} />
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Safe chained function
|
||||
*
|
||||
* Will only create a new function if needed,
|
||||
* otherwise will pass back existing functions or null.
|
||||
*
|
||||
* @param {function} functions to chain
|
||||
* @returns {function|null}
|
||||
*/
|
||||
function createChainedFunction(...funcs) {
|
||||
return funcs
|
||||
.filter(f => f != null)
|
||||
.reduce((acc, f) => {
|
||||
if (typeof f !== 'function') {
|
||||
throw new Error(
|
||||
'Invalid Argument Type, must only provide functions, undefined, or null.'
|
||||
);
|
||||
}
|
||||
|
||||
if (acc === null) {
|
||||
return f;
|
||||
}
|
||||
|
||||
return function chainedFunction(...args) {
|
||||
acc.apply(this, args);
|
||||
f.apply(this, args);
|
||||
};
|
||||
}, null);
|
||||
}
|
||||
|
||||
export default createChainedFunction;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Overlay } from './Overlay.js';
|
||||
export { OverlayTrigger } from './OverlayTrigger.js';
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StudiesList } from './StudiesList.js';
|
||||
import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js';
|
||||
import { SeriesList } from './SeriesList.js';
|
||||
|
||||
import './QuickSwitch.styl';
|
||||
|
||||
export class QuickSwitch extends Component {
|
||||
static propTypes = {
|
||||
side: PropTypes.string,
|
||||
studyListData: PropTypes.array.isRequired,
|
||||
onSeriesSelected: PropTypes.func.isRequired,
|
||||
seriesListData: PropTypes.array,
|
||||
onStudySelected: PropTypes.func,
|
||||
activeStudyInstanceUid: PropTypes.string,
|
||||
activeDisplaySetInstanceUid: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
seriesQuickSwitchOpen: false,
|
||||
sideClass: this.props.side || '',
|
||||
activeStudyInstanceUid: this.props.activeStudyInstanceUid,
|
||||
activeDisplaySetInstanceUid: this.props.activeDisplaySetInstanceUid,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (props.activeStudyInstanceUid !== prevProps.activeStudyInstanceUid) {
|
||||
this.setState({
|
||||
activeStudyInstanceUid: props.activeStudyInstanceUid,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
props.activeDisplaySetInstanceUid !==
|
||||
prevProps.activeDisplaySetInstanceUid
|
||||
) {
|
||||
this.setState({
|
||||
activeDisplaySetInstanceUid: props.activeDisplaySetInstanceUid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const quickSwitchClass = this.state.seriesQuickSwitchOpen
|
||||
? 'series-triggered'
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`series-quick-switch clearfix noselect ${
|
||||
this.state.sideClass
|
||||
} ${quickSwitchClass}`}
|
||||
onMouseLeave={this.hideSeriesSwitch}
|
||||
>
|
||||
<div className="series-switch" onMouseEnter={this.showSeriesSwitch}>
|
||||
<div className="title-label">Series</div>
|
||||
<div className="series-box">
|
||||
{this.getSmallListItems()}
|
||||
<ScrollableArea scrollStep={201} class="series-browser">
|
||||
<SeriesList
|
||||
seriesItems={this.getSeriesItems()}
|
||||
onClick={this.onSeriesClick}
|
||||
activeDisplaySetInstanceUid={
|
||||
this.state.activeDisplaySetInstanceUid
|
||||
}
|
||||
/>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="study-switch">
|
||||
<div className="title-label">Study</div>
|
||||
<div className="study-box">
|
||||
<ScrollableArea scrollStep={91} class="study-browser">
|
||||
<StudiesList
|
||||
studyListData={this.props.studyListData}
|
||||
onClick={this.onStudyClick}
|
||||
activeStudyInstanceUid={this.state.activeStudyInstanceUid}
|
||||
/>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getSeriesItems = () => {
|
||||
let seriesListData;
|
||||
|
||||
if (this.props.seriesListData) {
|
||||
seriesListData = this.props.seriesListData;
|
||||
} else if (this.state.activeStudyInstanceUid) {
|
||||
const study = this.props.studyListData.find(
|
||||
study => study.studyInstanceUid === this.state.activeStudyInstanceUid
|
||||
);
|
||||
|
||||
seriesListData = study.thumbnails;
|
||||
} else {
|
||||
seriesListData = this.props.studyListData[0].thumbnails;
|
||||
}
|
||||
|
||||
return seriesListData || [];
|
||||
};
|
||||
|
||||
getSmallListItems = () => {
|
||||
const seriesItems = this.getSeriesItems() || [];
|
||||
return seriesItems.map((seriesData, index) => {
|
||||
const active =
|
||||
seriesData.displaySetInstanceUid ===
|
||||
this.state.activeDisplaySetInstanceUid;
|
||||
return (
|
||||
<div key={index} className={`series-item ${active ? 'active' : ''}`} />
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
onStudyClick = studyDataSelected => {
|
||||
if (this.props.onStudySelected) {
|
||||
this.props.onStudySelected(studyDataSelected);
|
||||
}
|
||||
this.setState({
|
||||
activeStudyInstanceUid: studyDataSelected.studyInstanceUid,
|
||||
seriesQuickSwitchOpen: true,
|
||||
});
|
||||
};
|
||||
|
||||
onSeriesClick = seriesDataSelected => {
|
||||
this.setState({
|
||||
activeDisplaySetInstanceUid: seriesDataSelected.displaySetInstanceUid,
|
||||
});
|
||||
|
||||
this.props.onSeriesSelected(seriesDataSelected);
|
||||
};
|
||||
|
||||
showSeriesSwitch = () => {
|
||||
this.setState({
|
||||
seriesQuickSwitchOpen: true,
|
||||
});
|
||||
};
|
||||
|
||||
hideSeriesSwitch = () => {
|
||||
this.setState({
|
||||
seriesQuickSwitchOpen: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
.series-quick-switch
|
||||
position: relative;
|
||||
|
||||
&:before
|
||||
box-shadow: 0 0 0 5000px rgba(0, 0, 0, 0.8)
|
||||
transition: opacity 0.3s ease
|
||||
opacity: 0
|
||||
content: ''
|
||||
|
||||
&:hover
|
||||
z-index: 50
|
||||
|
||||
&:before
|
||||
display: table;
|
||||
opacity: 1
|
||||
content: ''
|
||||
|
||||
&:not(.left):not(.right)
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
|
||||
&.left
|
||||
border-right: 1px solid var(--ui-border-color)
|
||||
right: 0
|
||||
|
||||
&.right
|
||||
left: 0
|
||||
|
||||
.series-switch
|
||||
float: right
|
||||
|
||||
.study-switch
|
||||
float: left
|
||||
|
||||
.study-browser
|
||||
left: auto
|
||||
right: 0
|
||||
transform-origin: calc(100% - 1em) calc(1em + 17px)
|
||||
|
||||
.series-browser
|
||||
left: 0
|
||||
right: auto
|
||||
transform-origin: 1em calc(1em + 15px)
|
||||
|
||||
.thumbnails-wrapper
|
||||
transform-origin: 0% 0%
|
||||
|
||||
.ThumbnailEntry
|
||||
float: left
|
||||
|
||||
.series-item
|
||||
float: left
|
||||
|
||||
.title-label
|
||||
color: var(--text-secondary-color)
|
||||
font-size: 12px
|
||||
font-weight: 500
|
||||
line-height: 12px
|
||||
opacity: 1
|
||||
padding-bottom: 3px
|
||||
text-align: center
|
||||
transition: opacity 0.3s ease
|
||||
|
||||
.series-switch
|
||||
padding-right: 10px;
|
||||
|
||||
.series-switch,
|
||||
.study-switch
|
||||
float: left
|
||||
position: relative
|
||||
|
||||
.study-switch:hover:after
|
||||
content: ''
|
||||
display: block
|
||||
height: 100px
|
||||
left: 0
|
||||
position: absolute
|
||||
top: 0
|
||||
width: 300px
|
||||
|
||||
.study-browser
|
||||
left: 0
|
||||
padding-top: 60px;
|
||||
max-height: calc(100vh - 60px);
|
||||
transform-origin: 1em calc(1em + 17px)
|
||||
width: 300px
|
||||
|
||||
.study-browser-list
|
||||
padding: 0 10px
|
||||
|
||||
.series-browser
|
||||
max-height: 100vh
|
||||
max-width: 720px
|
||||
right: 0
|
||||
transform-origin: calc(100% - 1em) calc(1em + 15px)
|
||||
|
||||
.thumbnails-wrapper
|
||||
transition: transform 0.3s ease
|
||||
transform-origin: 100% 0%
|
||||
|
||||
.ThumbnailEntry
|
||||
float: right
|
||||
|
||||
.series-details
|
||||
opacity: 0
|
||||
transform: translateY(-100%)
|
||||
transition: transform 0.3s ease, opacity 0.3s ease
|
||||
|
||||
.scrollable
|
||||
padding-top: 15px
|
||||
transition: padding-bottom 0.3s ease
|
||||
|
||||
.study-browser-series {
|
||||
overflow hidden
|
||||
}
|
||||
|
||||
.series-browser,
|
||||
.study-browser
|
||||
min-height: 120px
|
||||
opacity: 0
|
||||
position: absolute
|
||||
top: 0
|
||||
transform: scale(0)
|
||||
transition: transform 0.3s ease , opacity 0.3s ease
|
||||
z-index: 3
|
||||
|
||||
.series-box,
|
||||
.study-box
|
||||
height: 57px
|
||||
width: 57px
|
||||
|
||||
.study-box
|
||||
background-color: var(--ui-gray-dark)
|
||||
border: solid 2px var(--ui-border-color-dark)
|
||||
border-radius: 11px
|
||||
|
||||
.series-item
|
||||
background-color: var(--box-background-color)
|
||||
border-radius: 3px
|
||||
float: right
|
||||
height: 15px
|
||||
margin: 2px
|
||||
transition: opacity 0.3s ease
|
||||
width: 15px
|
||||
|
||||
&.count
|
||||
color: var(--text-primary-color)
|
||||
background-color: transparent
|
||||
font-size: 12px
|
||||
font-weight: 500
|
||||
line-height: 17px
|
||||
text-align: center
|
||||
|
||||
&.active
|
||||
background-color: var(--active-color)
|
||||
|
||||
&.series-triggered .series-browser,
|
||||
.series-switch:hover .series-browser,
|
||||
.study-switch:hover .study-browser
|
||||
opacity: 1
|
||||
transform: scale(1)
|
||||
|
||||
&.series-triggered .series-item,
|
||||
.series-switch:hover .title-label
|
||||
opacity: 0
|
||||
|
||||
&.series-triggered
|
||||
.thumbnails-wrapper
|
||||
transform: scale(0.9) translateY(80px)
|
||||
|
||||
.series-browser:not(:hover)
|
||||
&>.scrollable
|
||||
padding-bottom: 80px
|
||||
|
||||
&>.scrollNav
|
||||
opacity: 0
|
||||
|
||||
.series-switch:hover .series-browser
|
||||
.thumbnails-wrapper
|
||||
transform: scale(1) translateY(0)
|
||||
|
||||
.ThumbnailEntry .series-details
|
||||
opacity: 1
|
||||
transform: translateY(0)
|
||||
transition-delay: 0.3s
|
||||
|
||||
.study-browser-item.active .study-item-box
|
||||
box-shadow: inset 0 0 0 3px var(--active-color)
|
||||
|
||||
@media screen and (max-width: 1599px)
|
||||
.series-quick-switch
|
||||
|
||||
.series-box,
|
||||
.study-box
|
||||
width: 36px
|
||||
height: 36px
|
||||
|
||||
.study-box
|
||||
border-radius: 7px
|
||||
|
||||
.series-item
|
||||
height: 10px
|
||||
margin: 1px
|
||||
width: 10px
|
||||
|
||||
&.count
|
||||
font-size: 10px
|
||||
font-weight: 300
|
||||
line-height: 10px
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ThumbnailEntry } from './../studyBrowser';
|
||||
import './SeriesList.styl';
|
||||
|
||||
export class SeriesList extends Component {
|
||||
static propTypes = {
|
||||
seriesItems: PropTypes.array.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
activeDisplaySetInstanceUid: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="study-browser-series clearfix thumbnails-wrapper">
|
||||
<div className="study-series-container">{this.getSeriesItems()}</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
getSeriesItems = () => {
|
||||
return this.props.seriesItems.map((seriesData, index) => {
|
||||
return (
|
||||
<ThumbnailEntry
|
||||
key={seriesData.displaySetInstanceUid}
|
||||
id={`series_thumb_${index}`}
|
||||
{...seriesData}
|
||||
active={
|
||||
seriesData.displaySetInstanceUid ===
|
||||
this.props.activeDisplaySetInstanceUid
|
||||
}
|
||||
onClick={() => this.props.onClick(seriesData)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,60 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './StudiesItem.styl';
|
||||
|
||||
export class StudiesItem extends Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
studyData: PropTypes.object.isRequired,
|
||||
active: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
studyDate,
|
||||
studyDescription,
|
||||
modalities,
|
||||
studyAvailable,
|
||||
} = this.props.studyData;
|
||||
const activeClass = this.props.active ? ' active' : '';
|
||||
const hasDescriptionAndDate = studyDate && studyDescription;
|
||||
return (
|
||||
<div
|
||||
className={`studyBrowseItem${activeClass}`}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<div className="studyItemBox">
|
||||
<div className="studyModality">
|
||||
<div
|
||||
className="studyModalityText"
|
||||
style={this.getModalitiesStyle()}
|
||||
>
|
||||
{modalities}
|
||||
</div>
|
||||
</div>
|
||||
<div className="studyText">
|
||||
{hasDescriptionAndDate ? (
|
||||
<React.Fragment>
|
||||
<div className="studyDate">{studyDate}</div>
|
||||
<div className="studyDescription">{studyDescription}</div>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className="studyAvailability">
|
||||
{studyAvailable ? (
|
||||
<React.Fragment>N/A</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>Click to load</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getModalitiesStyle = () => {
|
||||
return {};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
$boxHoverBackgroundColor = #14191E
|
||||
$boxHoverBorderColor = #2d4660
|
||||
$boxActiveBorderColor = #131D29
|
||||
$boxWidth = 50px
|
||||
$borderThickness = 2px
|
||||
$boxBorderRadius = 8px
|
||||
$nestingMargin = 6px
|
||||
$spacerX = 7px
|
||||
$spacerY = 12px
|
||||
|
||||
.studyBrowseItem
|
||||
margin-bottom: 10px;
|
||||
position: relative
|
||||
// required transformation to make inner fixed elements relative to this one
|
||||
transform: scale(1)
|
||||
|
||||
&.active
|
||||
.studyItemBox
|
||||
border-color: var(--active-color)
|
||||
|
||||
.studyModality
|
||||
color: var(--primary-background-color)
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
background-color: var(--active-color)
|
||||
border-color: $boxActiveBorderColor
|
||||
|
||||
&:not(.active) .studyBrowserSeries
|
||||
max-height: 0 !important
|
||||
|
||||
.studySeriesContainer
|
||||
opacity: 0
|
||||
transform: translateY(-100%)
|
||||
|
||||
.studyBrowserSeries
|
||||
overflow: hidden
|
||||
transition: max-height 0.3s ease
|
||||
|
||||
.studySeriesContainer
|
||||
opacity: 1
|
||||
transition: opacity 0.3s ease, transform 0.3s ease
|
||||
transform: translateY(0)
|
||||
transform-origin: 50% 0%
|
||||
|
||||
.studyItemBox
|
||||
border: 3px solid var(--ui-border-color-dark)
|
||||
border-radius: 12px
|
||||
cursor: pointer
|
||||
padding: $spacerY $spacerX ($spacerY - 1)
|
||||
position: relative
|
||||
transition: $sidebarTransition
|
||||
background-color: $boxHoverBackgroundColor
|
||||
z-index: 0
|
||||
|
||||
&:hover
|
||||
border-color: var(--active-color)
|
||||
|
||||
&.additional
|
||||
color: var(--text-secondary-color)
|
||||
|
||||
.studyModality
|
||||
color: var(--primary-background-color)
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
background-color: var(--active-color)
|
||||
|
||||
&.additional
|
||||
color: var(--text-primary-color)
|
||||
font-size: 16px
|
||||
font-weight: normal
|
||||
height: 91px
|
||||
line-height: 91px
|
||||
padding: 0
|
||||
text-align: center
|
||||
|
||||
.studyText
|
||||
font-size: 13px
|
||||
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
|
||||
line-height: 14px
|
||||
position: absolute
|
||||
right: $spacerX
|
||||
top: $spacerY
|
||||
.studyDate
|
||||
margin-top: 8px
|
||||
color: var(--text-secondary-color)
|
||||
.studyDescription
|
||||
margin-top: 8px
|
||||
color: var(--text-primary-color)
|
||||
.studyAvailability
|
||||
margin-top: 24px
|
||||
color: var(--text-primary-color)
|
||||
|
||||
.studyModality
|
||||
color: var(--text-secondary-color)
|
||||
font-size: 20px
|
||||
line-height: $boxWidth
|
||||
margin-left: $nestingMargin * 2
|
||||
margin-top: $nestingMargin * 2
|
||||
position: relative
|
||||
|
||||
.studyModalityText
|
||||
height: 100%
|
||||
overflow: hidden
|
||||
text-align: center
|
||||
text-transform: uppercase
|
||||
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
border: $borderThickness solid var(--primary-background-color)
|
||||
background-color: var(--box-background-color)
|
||||
border-radius: $boxBorderRadius
|
||||
height: $boxWidth + ($borderThickness * 2)
|
||||
transition($sidebarTransition)
|
||||
width: $boxWidth + ($borderThickness * 2)
|
||||
|
||||
&:before
|
||||
&:after
|
||||
display: block
|
||||
content: ''
|
||||
position: absolute
|
||||
|
||||
&:before
|
||||
top: -($borderThickness + $nestingMargin)
|
||||
left: -($borderThickness + $nestingMargin)
|
||||
z-index: -1
|
||||
|
||||
&:after
|
||||
top: -($borderThickness + ($nestingMargin * 2))
|
||||
left: -($borderThickness + ($nestingMargin * 2))
|
||||
z-index: -2
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StudiesItem } from './StudiesItem.js';
|
||||
import './StudiesList.styl';
|
||||
|
||||
export class StudiesList extends Component {
|
||||
static propTypes = {
|
||||
class: PropTypes.string,
|
||||
studyListData: PropTypes.array.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
activeStudyInstanceUid: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`studiesList ${this.props.class}`}>
|
||||
{this.getBrowserItems()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getBrowserItems = () => {
|
||||
return this.props.studyListData.map((studyData, index) => {
|
||||
return (
|
||||
<StudiesItem
|
||||
key={index}
|
||||
studyData={studyData}
|
||||
active={
|
||||
studyData.studyInstanceUid === this.props.activeStudyInstanceUid
|
||||
}
|
||||
onClick={event => this.props.onClick(studyData)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: Quick Switch
|
||||
menu: Components
|
||||
route: /components/quick-switch
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { QuickSwitch } from './../index.js'
|
||||
//
|
||||
import studies from './studies.js'
|
||||
|
||||
# Quick Switch
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
studyListData: studies,
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<QuickSwitch
|
||||
studyListData={state.studyListData}
|
||||
// If this is always activeStudy's thumbnails,
|
||||
// Why do we need to specify this?
|
||||
seriesListData={
|
||||
state.studyListData.find((study) => {
|
||||
return !!study.active;
|
||||
}).thumbnails}
|
||||
onSeriesSelected={(selectedSeries) => {
|
||||
const { studyListData } = state;
|
||||
const selectedId = selectedSeries.displaySetInstanceUid;
|
||||
|
||||
studyListData.forEach(study => {
|
||||
study.thumbnails.forEach(series => {
|
||||
series.active = series.displaySetInstanceUid === selectedId;
|
||||
})
|
||||
});
|
||||
|
||||
setState({ studyListData });
|
||||
}}
|
||||
onStudySelected={(selectedStudy) => {
|
||||
const { studyListData } = state;
|
||||
const selectedId = selectedStudy.studyInstanceUid;
|
||||
|
||||
studyListData.forEach(study => {
|
||||
study.active = study.studyInstanceUid === selectedId;
|
||||
});
|
||||
|
||||
setState({ studyListData });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={QuickSwitch} />
|
||||
@@ -0,0 +1,76 @@
|
||||
export default [
|
||||
{
|
||||
studyInstanceUid: '10001',
|
||||
studyDescription: 'Anti-PD-1',
|
||||
modalities: 'CT',
|
||||
studyDate: '18-nov-2018',
|
||||
active: false,
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg',
|
||||
seriesDescription: 'Anti-PD-1_Lung',
|
||||
seriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
displaySetInstanceUid: '10001-1',
|
||||
stackPercentComplete: 30,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg',
|
||||
seriesDescription: 'Anti-PD-1_MELANOMA',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
displaySetInstanceUid: '10001-2',
|
||||
stackPercentComplete: 70,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
studyInstanceUid: '10002',
|
||||
studyDescription: 'CPTAC',
|
||||
modalities: 'CT',
|
||||
studyDate: '16-aug-2017',
|
||||
active: true,
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg',
|
||||
seriesDescription: 'CPTAC-GBM',
|
||||
active: true,
|
||||
seriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
displaySetInstanceUid: '10002-1',
|
||||
stackPercentComplete: 100,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg',
|
||||
seriesDescription: 'CPTAC-CM',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
displaySetInstanceUid: '10002-2',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg',
|
||||
seriesDescription: 'CPTAC-HNSCC',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
displaySetInstanceUid: '10002-3',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg',
|
||||
seriesDescription: 'CPTAC-LSCC',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
displaySetInstanceUid: '10002-4',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export { QuickSwitch } from './QuickSwitch.js';
|
||||
@@ -0,0 +1,62 @@
|
||||
.ohif-radio-button-container {
|
||||
--radio-button-dim: 20px;
|
||||
}
|
||||
|
||||
.ohif-radio-button-container input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: 10000;
|
||||
cursor: pointer;
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.ohif-radio-button-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 35px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 22px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ohif-radio-button {
|
||||
width: var(--radio-button-dim);
|
||||
height: var(--radio-button-dim);
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 0;
|
||||
background-color: var(--ui-gray-lighter);
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ohif-radio-button:hover {
|
||||
background-color: var(--default-color);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.ohif-radio-button:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.ohif-radio-button-container .ohif-radio-button.ohif-selected:after {
|
||||
display: block;
|
||||
top: 14%;
|
||||
left: 14%;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.ohif-radio-button-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RadioButtonList.css';
|
||||
|
||||
export class RadioButtonList extends Component {
|
||||
static className = 'RadioButtonList';
|
||||
|
||||
//TODO: Add fields to propTypes.description?
|
||||
//These would be label (required), id (required), and checked (optional).
|
||||
static propTypes = {
|
||||
description: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
|
||||
for (let button of props.description) {
|
||||
if (button.checked) {
|
||||
this.state.checked = button.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({ checked: e.target.value });
|
||||
}
|
||||
|
||||
render() {
|
||||
let buttons = this.props.description.map(button => {
|
||||
let input = (
|
||||
<input
|
||||
type="radio"
|
||||
checked={this.state.checked === button.id}
|
||||
onChange={e => {
|
||||
this.handleChange(e);
|
||||
}}
|
||||
value={button.id}
|
||||
/>
|
||||
);
|
||||
|
||||
//needed to style the custom radio check
|
||||
let inputSpan;
|
||||
if (this.state.checked === button.id) {
|
||||
inputSpan = (
|
||||
<span className="ohif-radio-button ohif-selected">{input}</span>
|
||||
);
|
||||
} else {
|
||||
inputSpan = <span className="ohif-radio-button">{input}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="ohif-radio-button-container" key={button.id}>
|
||||
<label className="ohif-radio-button-label">
|
||||
{inputSpan}
|
||||
{button.label}
|
||||
</label>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="ohif-radio-button-group">
|
||||
<form>{buttons}</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: Radio Button List
|
||||
menu: Elements
|
||||
route: /components/radio-button-list
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { RadioButtonList } from './../index'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
|
||||
#Radio Button List
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State
|
||||
initial={{
|
||||
description : [
|
||||
{
|
||||
'label' : 'Button 1',
|
||||
'id' : 'button1',
|
||||
'checked' : false
|
||||
},
|
||||
{
|
||||
'label' : 'Button 2',
|
||||
'id' : 'button2',
|
||||
'checked' : false,
|
||||
},
|
||||
{
|
||||
'label' : 'Button 3',
|
||||
'id' : 'button3',
|
||||
'checked' : true,
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<pre>{JSON.stringify(state, null, 2)}</pre>
|
||||
</div>
|
||||
<div style={{ maxWidth: '300px', margin: '0 auto' }}>
|
||||
<RadioButtonList description={[
|
||||
{
|
||||
'label' : 'Button 1',
|
||||
'id' : 'button1',
|
||||
'checked' : false
|
||||
},
|
||||
{
|
||||
'label' : 'Button 2',
|
||||
'id' : 'button2',
|
||||
'checked' : false,
|
||||
|
||||
},
|
||||
{
|
||||
'label' : 'Button 3',
|
||||
'id' : 'coolButton3',
|
||||
'checked' : true,
|
||||
}
|
||||
]}/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</State>
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={RadioButtonList} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="RadioButtonList" />
|
||||
@@ -0,0 +1 @@
|
||||
export { RadioButtonList } from './RadioButtonList.js';
|
||||
@@ -0,0 +1,86 @@
|
||||
.RoundedButtonGroup {
|
||||
--height: 25px;
|
||||
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin-left: -2px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .roundedButtonWrapper .roundedButton {
|
||||
align-items: center;
|
||||
background-color: var(--ui-gray-dark);
|
||||
border: 2px solid var(--ui-border-color-dark);
|
||||
color: var(--text-secondary-color);
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
height: var(--height);
|
||||
line-height: var(--height);
|
||||
padding: 0 22px;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
transition: var(--sidebar-transition);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper
|
||||
.roundedButton
|
||||
svg
|
||||
.roundedButtonWrapper
|
||||
.roundedButton
|
||||
span {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper .roundedButton i {
|
||||
line-height: 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.bottomLabel {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .roundedButtonWrapper:first-child .roundedButton {
|
||||
border-bottom-left-radius: var(--height);
|
||||
border-top-left-radius: var(--height);
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .roundedButtonWrapper:last-child .roundedButton {
|
||||
border-bottom-right-radius: var(--height);
|
||||
border-top-right-radius: var(--height);
|
||||
}
|
||||
|
||||
.roundedButtonWrapper:hover .roundedButton {
|
||||
background-color: var(--box-background-color);
|
||||
color: var(--ui-gray-dark);
|
||||
}
|
||||
|
||||
.roundedButtonWrapper.active .roundedButton {
|
||||
background-color: var(--active-color);
|
||||
border-color: var(--ui-border-color-active);
|
||||
color: var(--ui-gray-dark);
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import './RoundedButtonGroup.css';
|
||||
|
||||
// TODO: Rename to Toggle?
|
||||
class RoundedButtonGroup extends Component {
|
||||
static className = 'RoundedButtonGroup';
|
||||
|
||||
static propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
),
|
||||
value: PropTypes.string,
|
||||
onValueChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
options: [],
|
||||
value: null,
|
||||
};
|
||||
|
||||
onClickOption = value => {
|
||||
let newValue = value;
|
||||
if (this.props.value === value) {
|
||||
newValue = null;
|
||||
}
|
||||
|
||||
if (this.props.onValueChanged) {
|
||||
this.props.onValueChanged(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let className = classnames(
|
||||
RoundedButtonGroup.className,
|
||||
'clearfix center-table'
|
||||
);
|
||||
|
||||
const buttons = this.props.options.map((option, index) => {
|
||||
const className = classnames({
|
||||
roundedButtonWrapper: true,
|
||||
noselect: true,
|
||||
active: this.props.value === option.value,
|
||||
});
|
||||
|
||||
const optionText = option.label && <span>{option.label}</span>;
|
||||
const iconProps =
|
||||
typeof option.icon === 'string' ? { name: option.icon } : option.icon;
|
||||
|
||||
const bottomLabel = option.bottomLabel && (
|
||||
<div className="bottomLabel">{option.bottomLabel}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={className}
|
||||
onClick={() => this.onClickOption(option.value)}
|
||||
>
|
||||
<div className="roundedButton">
|
||||
{optionText}
|
||||
{iconProps && <Icon {...iconProps} />}
|
||||
</div>
|
||||
{bottomLabel}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return <div className={className}>{buttons}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { RoundedButtonGroup };
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: Rounded Button Group
|
||||
menu: Components
|
||||
route: /components/rounded-button-group
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz';
|
||||
import { State } from 'react-powerplug';
|
||||
import { RoundedButtonGroup } from './../index.js';
|
||||
|
||||
# Rounded Button Group
|
||||
|
||||
A basic styled toggle switch
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State
|
||||
initial={{
|
||||
value: null,
|
||||
options: [
|
||||
{
|
||||
value: 'studies',
|
||||
icon: 'th',
|
||||
bottomLabel: 'Series',
|
||||
},
|
||||
{
|
||||
value: 'measurements',
|
||||
icon: 'th-list',
|
||||
bottomLabel: 'Measurements',
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<pre>value: {JSON.stringify(state.value, null, 2)}</pre>
|
||||
</div>
|
||||
<RoundedButtonGroup
|
||||
{...state}
|
||||
onValueChanged={value => setState({ value })}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</State>
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={RoundedButtonGroup} />
|
||||
@@ -0,0 +1 @@
|
||||
export { RoundedButtonGroup } from './RoundedButtonGroup.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class InputRadio extends Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
itemData: PropTypes.object.isRequired,
|
||||
labelClass: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
onSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const labelClass = this.props.labelClass ? this.props.labelClass : '';
|
||||
return (
|
||||
<label
|
||||
className={'wrapperLabel radioLabel ' + labelClass}
|
||||
htmlFor={this.props.id}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={this.props.id}
|
||||
className="radioInput"
|
||||
value={this.props.value}
|
||||
onChange={this.onSelected}
|
||||
/>
|
||||
<span className="wrapperText">{this.props.label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
onSelected = evt => {
|
||||
this.props.onSelected(evt, this.props.itemData);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import './SelectTree.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import InputRadio from './InputRadio.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import SelectTreeBreadcrumb from './SelectTreeBreadcrumb.js';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
export class SelectTree extends Component {
|
||||
static propTypes = {
|
||||
autoFocus: PropTypes.bool,
|
||||
searchEnabled: PropTypes.bool,
|
||||
selectTreeFirstTitle: PropTypes.string,
|
||||
selectTreeSecondTitle: PropTypes.string,
|
||||
/** Called when 'componentDidUpdate' is triggered */
|
||||
onComponentChange: PropTypes.func,
|
||||
/** [{ label, value, items[]}] - An array of items than can be expanded to show child items */
|
||||
items: PropTypes.array.isRequired,
|
||||
/** fn(evt, item) - Called when a child item is selected; receives event and selected item */
|
||||
onSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
searchEnabled: true,
|
||||
autoFocus: true,
|
||||
selectTreeFirstTitle: 'First Level itens',
|
||||
items: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
searchTerm: null,
|
||||
currentNode: null,
|
||||
value: null,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const treeItems = this.getTreeItems();
|
||||
|
||||
return (
|
||||
<div className="selectTree selectTreeRoot">
|
||||
<div className="treeContent">
|
||||
{this.headerItem()}
|
||||
<div className="treeOptions">
|
||||
{this.state.currentNode && (
|
||||
<SelectTreeBreadcrumb
|
||||
onSelected={this.onBreadcrumbSelected}
|
||||
label={this.state.currentNode.label}
|
||||
value={this.state.currentNode.value}
|
||||
/>
|
||||
)}
|
||||
<div className="treeInputsWrapper">
|
||||
<div className="treeInputs">{treeItems}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
if (this.props.onComponentChange) {
|
||||
this.props.onComponentChange();
|
||||
}
|
||||
};
|
||||
|
||||
isLeafSelected = item => item && !Array.isArray(item.items);
|
||||
|
||||
getLabelClass = item => {
|
||||
let labelClass = 'treeLeaf';
|
||||
if (this.state.searchTerm || Array.isArray(item.items)) {
|
||||
labelClass = 'treeNode';
|
||||
}
|
||||
return labelClass;
|
||||
};
|
||||
|
||||
filterItems() {
|
||||
const filteredItems = [];
|
||||
const rawItems = cloneDeep(this.props.items);
|
||||
rawItems.forEach(item => {
|
||||
if (Array.isArray(item.items)) {
|
||||
item.items.forEach(item => {
|
||||
const label = item.label.toLowerCase();
|
||||
const searchTerm = this.state.searchTerm.toLowerCase();
|
||||
if (label.indexOf(searchTerm) !== -1) {
|
||||
filteredItems.push(item);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const label = item.label.toLowerCase();
|
||||
const searchTerm = this.state.searchTerm.toLowerCase();
|
||||
if (label.indexOf(searchTerm) !== -1) {
|
||||
filteredItems.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
getTreeItems() {
|
||||
const storageKey = 'SelectTree';
|
||||
let treeItems;
|
||||
|
||||
if (this.state.searchTerm) {
|
||||
treeItems = this.filterItems();
|
||||
} else if (this.state.currentNode) {
|
||||
treeItems = cloneDeep(this.state.currentNode.items);
|
||||
} else {
|
||||
treeItems = cloneDeep(this.props.items);
|
||||
}
|
||||
|
||||
return treeItems.map((item, index) => {
|
||||
let itemKey = index;
|
||||
if (this.state.currentNode) {
|
||||
itemKey += `_${this.state.currentNode.value}`;
|
||||
}
|
||||
return (
|
||||
<InputRadio
|
||||
key={itemKey}
|
||||
id={`${storageKey}_${item.value}`}
|
||||
name={index}
|
||||
itemData={item}
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
labelClass={this.getLabelClass(item)}
|
||||
onSelected={this.onSelected}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
headerItem = () => {
|
||||
let title = this.props.selectTreeFirstTitle;
|
||||
if (this.state.currentNode && this.props.selectTreeSecondTitle) {
|
||||
title = this.props.selectTreeSecondTitle;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wrapperLabel treeHeader">
|
||||
<div className="wrapperText">{title}</div>
|
||||
{this.props.searchEnabled && (
|
||||
<div className="wrapperSearch">
|
||||
<div className="searchIcon">
|
||||
<Icon name="search" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="searchInput"
|
||||
placeholder="Search labels"
|
||||
autoFocus={this.props.autoFocus}
|
||||
onChange={this.searchLocations}
|
||||
value={this.state.searchTerm ? this.state.searchTerm : ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
searchLocations = evt => {
|
||||
this.setState({
|
||||
currentNode: null,
|
||||
searchTerm: evt.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
onSelected = (event, item) => {
|
||||
if (this.isLeafSelected(item)) {
|
||||
this.setState({
|
||||
searchTerm: null,
|
||||
currentNode: null,
|
||||
value: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
currentNode: item,
|
||||
});
|
||||
}
|
||||
return this.props.onSelected(event, item);
|
||||
};
|
||||
|
||||
onBreadcrumbSelected = () => {
|
||||
this.setState({
|
||||
currentNode: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.selectTreeRoot
|
||||
text-align: initial;
|
||||
width: 320px;
|
||||
max-height: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 26px;
|
||||
|
||||
.treeContent
|
||||
max-height: inherit;
|
||||
overflow: hidden;
|
||||
border: 1px solid #c4c4c4;
|
||||
border-radius: 5px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.treeHeader
|
||||
background-color: #c0c0c0;
|
||||
border-bottom: 1px solid #c4c4c4;
|
||||
color: #303030;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
|
||||
.wrapperText
|
||||
font-weight: bold;
|
||||
padding-bottom: 2px;
|
||||
|
||||
.wrapperSearch
|
||||
width: 100%;
|
||||
|
||||
.searchIcon
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 15px;
|
||||
margin: 7px 10px 13px 10px;
|
||||
color: #a3a3a3;
|
||||
|
||||
.searchInput
|
||||
width: 100%;
|
||||
padding: 6px 12px 6px 35px;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #9b9b9b;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #555;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
|
||||
&:focus
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px rgba(102, 175, 233, 0.6);
|
||||
|
||||
.wrapperText
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
.selectTreeBreadcrumb
|
||||
.wrapperLabel
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
height: 41px;
|
||||
width: 100%;
|
||||
line-height: 41px;
|
||||
|
||||
.backIcon
|
||||
padding-right: 10px;
|
||||
|
||||
svg
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
color: #457fb0;
|
||||
|
||||
.treeOptions
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
.selectTreeBreadcrumb + .treeInputsWrapper .treeInputs .treeLeaf
|
||||
border: none;
|
||||
height: initial;
|
||||
line-height: 26px;
|
||||
|
||||
& > .treeInputsWrapper > .treeInputs .treeLeaf
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 41px;
|
||||
line-height: 41px;
|
||||
|
||||
.treeNode, .treeLeaf
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-weight: normal;
|
||||
box-shadow: 0 0 0 200px transparent;
|
||||
padding: 0 12px;
|
||||
|
||||
.treeNode
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 41px;
|
||||
line-height: 41px;
|
||||
|
||||
.radioLabel .radioInput
|
||||
display: none;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
|
||||
export default class SelectTreeBreadcrumb extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="selectTreeBreadcrumb">
|
||||
<label
|
||||
className="wrapperLabel radioLabel"
|
||||
htmlFor="selectTreeBreadcrumb"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="selectTreeBreadcrumb"
|
||||
className="treeNode radioInput"
|
||||
value={this.props.value}
|
||||
onChange={this.props.onSelected}
|
||||
/>
|
||||
<span className="wrapperText">
|
||||
<span className="backIcon">
|
||||
<Icon name="fast-backward" />
|
||||
</span>
|
||||
{this.props.label}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Select Tree
|
||||
menu: Components
|
||||
route: /components/select-tree
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { SelectTree } from './../index.js'
|
||||
|
||||
import selectTreeItems from './selectTreeItems.js'
|
||||
|
||||
# SelectTree
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
items: selectTreeItems,
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<pre style={{ maxHeight: '135px', overflowX: 'auto' }}>
|
||||
{JSON.stringify(state.items, null, 2)}
|
||||
</pre>
|
||||
|
||||
<SelectTree
|
||||
items={state.items}
|
||||
onSelected={(event, item) => setState({ itemLabelSelected: item.label }) }
|
||||
selectTreeFirstTitle="Select Tree Example Header"
|
||||
autoFocus={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={SelectTree} />
|
||||
@@ -0,0 +1,30 @@
|
||||
export default [
|
||||
{
|
||||
label: 'A',
|
||||
value: 'A',
|
||||
items: [
|
||||
{
|
||||
label: 'Abdominal Wall',
|
||||
value: 'AbdominalWall',
|
||||
},
|
||||
{
|
||||
label: 'Adrenal Left',
|
||||
value: 'AdrenalLeft',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'B',
|
||||
value: 'B',
|
||||
items: [
|
||||
{
|
||||
label: 'Brain',
|
||||
value: 'Brain',
|
||||
},
|
||||
{
|
||||
label: 'Breast',
|
||||
value: 'Breast',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export { SelectTree } from './SelectTree';
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './SimpleDialog.styl';
|
||||
|
||||
class SimpleDialog extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
componentRef: PropTypes.any,
|
||||
componentStyle: PropTypes.object,
|
||||
rootClass: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
headerTitle: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: true,
|
||||
componentStyle: {},
|
||||
rootClass: '',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.isOpen && (
|
||||
<div
|
||||
className={`simpleDialog ${this.props.rootClass} `}
|
||||
ref={this.props.componentRef}
|
||||
style={this.props.componentStyle}
|
||||
>
|
||||
<form>
|
||||
<div className="header">
|
||||
<span className="closeBtn" onClick={this.onClose}>
|
||||
<span className="closeIcon">x</span>
|
||||
</span>
|
||||
<h4 className="title">{this.props.headerTitle}</h4>
|
||||
</div>
|
||||
<div className="content">{this.props.children}</div>
|
||||
<div className="footer">
|
||||
<button className="btn btn-default" onClick={this.onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={this.onConfirm}>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
onClose = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClose();
|
||||
};
|
||||
|
||||
onConfirm = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onConfirm();
|
||||
};
|
||||
}
|
||||
|
||||
export { SimpleDialog };
|
||||
@@ -0,0 +1,108 @@
|
||||
@import './../../design/styles/common/button.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.simpleDialog
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 1000;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background-color: var(--ui-gray-darker);
|
||||
|
||||
.header
|
||||
border-bottom: 3px solid var(--primary-background-color);
|
||||
padding: 19px 22px 17px;
|
||||
position: relative;
|
||||
|
||||
.title
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
padding-right: 40px;
|
||||
color: var(--text-secondary-color);
|
||||
margin: 0;
|
||||
|
||||
.closeBtn
|
||||
-webkit-transform: translateY(-50%);
|
||||
color: var(--text-secondary-color);
|
||||
height: 20px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
right: 21px;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: color 0.3s ease;
|
||||
width: 20px;
|
||||
|
||||
.closeIcon
|
||||
color: transparent;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
height: 100%;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&:after, &:before
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 2px;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 19px;
|
||||
background-color: var(--text-secondary-color);
|
||||
|
||||
&:before
|
||||
left: 1px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
-webkit-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform-origin: 1px 50%;
|
||||
transform-origin: 1px 50%;
|
||||
|
||||
&:after
|
||||
right: 1px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
-webkit-transform-origin: calc(100% - 1px) 50%;
|
||||
transform-origin: calc(100% - 1px) 50%;
|
||||
|
||||
.content
|
||||
padding: 16px 22px 25px;
|
||||
position: relative;
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.simpleDialogSelect, .simpleDialogInput
|
||||
background-color: var(--input-background-color);
|
||||
color: var(--text-primary-color);
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
line-height: 16px;
|
||||
padding: 8px 9px 6px;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
|
||||
.simpleDialogLabelFor
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
margin: 10px 0 0 0;
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.footer
|
||||
padding: 15px;
|
||||
text-align: right;
|
||||
|
||||
.btn
|
||||
margin-left: 5px;
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: Simple Dialog
|
||||
menu: Components
|
||||
route: /components/simple-dialog
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { SimpleDialog } from './../index.js'
|
||||
|
||||
# Simple Dialog
|
||||
|
||||
Its similar to a modal but we can pass its style as props and manipulate it as
|
||||
we want. Initially used to display tool data changes in our Viewport.
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
isDialogOpen: false,
|
||||
inputValue: '',
|
||||
selectValue: '',
|
||||
dialogStyle: {
|
||||
top: '50px',
|
||||
left: '100px',
|
||||
position: 'absolute'
|
||||
}
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<div><pre>{JSON.stringify(state, null, 2) }</pre></div>
|
||||
|
||||
{/* Toggle Dialog */}
|
||||
<button onClick={() => setState({ isDialogOpen: !state.isDialogOpen})}>
|
||||
{state.isDialogOpen ? 'Close Dialog' : 'Open Dialog'}
|
||||
</button>
|
||||
|
||||
<SimpleDialog
|
||||
isOpen={state.isDialogOpen}
|
||||
headerTitle='Example Dialog Header'
|
||||
onClose={() => setState({ isDialogOpen: false })}
|
||||
onConfirm={() => setState({
|
||||
isDialogOpen: false,
|
||||
// If you don't want to update external state until complete,
|
||||
// This is where you could grab current field values and update parent
|
||||
// With setState
|
||||
//
|
||||
// inputValue: this.input.value,
|
||||
// selectValue: this.select.value
|
||||
})}
|
||||
componentStyle={state.dialogStyle}
|
||||
onValueChanged={(value) => setState({value}) }
|
||||
>
|
||||
<label htmlFor='input' className='simpleDialogLabelFor'>Input Example</label>
|
||||
<input
|
||||
id='input'
|
||||
type='text'
|
||||
className='simpleDialogInput'
|
||||
autoComplete='off'
|
||||
onChange={(e) => setState({inputValue: e.target.value})}
|
||||
/>
|
||||
<label htmlFor='select' className='simpleDialogLabelFor'>SelectExample</label>
|
||||
<select
|
||||
name='select'
|
||||
id='select'
|
||||
className='simpleDialogSelect'
|
||||
onChange={(e) => setState({selectValue: e.target.value})}
|
||||
>
|
||||
<option value='Option1'>Option1</option>
|
||||
<option value='Option2'>Option2</option>
|
||||
<option value='Option3'>Option3</option>
|
||||
<option value='Option4'>Option4</option>
|
||||
</select>
|
||||
</SimpleDialog>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={SimpleDialog} />
|
||||
@@ -0,0 +1 @@
|
||||
export { SimpleDialog } from './SimpleDialog.js';
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { PureComponent, memo } from 'react';
|
||||
import { DragLayer } from 'react-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ThumbnailEntry } from './ThumbnailEntry';
|
||||
import './DragPreview.styl';
|
||||
|
||||
let subscribedToOffsetChange = false;
|
||||
let dragPreviewRef = null;
|
||||
|
||||
const collector = monitor => {
|
||||
if (!subscribedToOffsetChange) {
|
||||
monitor.subscribeToOffsetChange(onOffsetChange(monitor));
|
||||
subscribedToOffsetChange = true;
|
||||
}
|
||||
|
||||
if (dragPreviewRef) {
|
||||
const offset =
|
||||
monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset();
|
||||
|
||||
if (offset) {
|
||||
const transform = `translate(${offset.x}px, ${offset.y}px)`;
|
||||
dragPreviewRef.style['transform'] = transform;
|
||||
dragPreviewRef.style['-webkit-transform'] = transform;
|
||||
}
|
||||
}
|
||||
|
||||
const item = monitor.getItem();
|
||||
let newItem = {};
|
||||
if (item) {
|
||||
newItem = {
|
||||
active: item.active,
|
||||
altImageText: item.altImageText,
|
||||
id: item.id,
|
||||
imageSrc: item.imageSrc,
|
||||
imageId: item.imageId,
|
||||
instanceNumber: item.instanceNumber,
|
||||
error: item.error,
|
||||
numImageFrames: item.numImageFrames,
|
||||
seriesDescription: item.seriesDescription,
|
||||
seriesNumber: item.seriesNumber,
|
||||
stackPercentComplete: item.stackPercentComplete,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...newItem,
|
||||
isDragging: monitor.isDragging(),
|
||||
};
|
||||
};
|
||||
|
||||
const onOffsetChange = monitor => () => {
|
||||
if (!dragPreviewRef) return;
|
||||
|
||||
const offset =
|
||||
monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset();
|
||||
if (!offset) return;
|
||||
|
||||
const transform = `translate(${offset.x}px, ${offset.y}px)`;
|
||||
dragPreviewRef.style['transform'] = transform;
|
||||
dragPreviewRef.style['-webkit-transform'] = transform;
|
||||
};
|
||||
|
||||
const updateRef = ref => {
|
||||
dragPreviewRef = ref;
|
||||
};
|
||||
|
||||
class DragPreview extends PureComponent {
|
||||
render() {
|
||||
const { isDragging } = this.props;
|
||||
if (!isDragging) return null;
|
||||
return (
|
||||
<div className="DragPreview">
|
||||
<div className="source-preview" ref={updateRef}>
|
||||
<ThumbnailEntry {...this.props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DragPreview.propTypes = {
|
||||
isDragging: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default DragLayer(collector)(memo(DragPreview));
|
||||
@@ -0,0 +1,14 @@
|
||||
.DragPreview
|
||||
|
||||
.source-preview {
|
||||
border-radius: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
position: fixed;
|
||||
opacity: 0.5;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transition: none;
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.ExampleDropTarget {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
min-height: 100px;
|
||||
|
||||
border: 2px dashed black;
|
||||
padding: 20px;
|
||||
transition: 0.2s all ease;
|
||||
}
|
||||
|
||||
.ExampleDropTarget h4,
|
||||
.ExampleDropTarger p {
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ExampleDropTarget.can-drop {
|
||||
border: 2px dashed #20a5d6;
|
||||
background-color: #fafafa;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.ExampleDropTarget.hovered {
|
||||
border: 2px dashed #20a5d6;
|
||||
background-color: black;
|
||||
color: #91b9cd;
|
||||
cursor: copy;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DropTarget } from 'react-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ExampleDropTarget.css';
|
||||
|
||||
// Drag sources and drop targets only interact
|
||||
// if they have the same string type.
|
||||
const Types = {
|
||||
THUMBNAIL: 'thumbnail',
|
||||
};
|
||||
|
||||
const divTarget = {
|
||||
drop(props, monitor, component) {
|
||||
// Note: For this example we use setState, but in
|
||||
// OHIF we will update the redux store instead
|
||||
const item = monitor.getItem();
|
||||
|
||||
component.setState({
|
||||
item: {
|
||||
id: item.id,
|
||||
seriesDescription: item.seriesDescription,
|
||||
},
|
||||
});
|
||||
return { id: 'ExampleDropTarget' };
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: Find out why we can't move this into the Example app instead.
|
||||
// It looks like the context isn't properly shared.
|
||||
class CustomDropTarget extends Component {
|
||||
static className = 'ExampleDropTarget';
|
||||
|
||||
state = {
|
||||
item: null,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isOver: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
connectDropTarget: PropTypes.func.isRequired,
|
||||
canDrop: PropTypes.bool.isRequired,
|
||||
isOver: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { canDrop, isOver, connectDropTarget } = this.props;
|
||||
const isActive = canDrop && isOver;
|
||||
|
||||
let className = ExampleDropTarget.className;
|
||||
|
||||
if (isActive) {
|
||||
className += ' hovered';
|
||||
} else if (canDrop) {
|
||||
className += ' can-drop';
|
||||
}
|
||||
|
||||
return connectDropTarget(
|
||||
<div className={className}>
|
||||
<h4>
|
||||
{isActive
|
||||
? 'Release to drop'
|
||||
: 'Drag / Drop something from the Study Browser here'}
|
||||
</h4>
|
||||
<p className="study-drop-results">
|
||||
{this.state.item && JSON.stringify(this.state.item)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const collect = (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
});
|
||||
|
||||
const ExampleDropTarget = DropTarget(Types.THUMBNAIL, divTarget, collect)(
|
||||
CustomDropTarget
|
||||
);
|
||||
|
||||
export { ExampleDropTarget };
|
||||
@@ -0,0 +1,136 @@
|
||||
import './ImageThumbnail.styl';
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator';
|
||||
import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
|
||||
|
||||
// TODO: How should we have this component depend on Cornerstone?
|
||||
// - Passed in as a prop?
|
||||
// - Set as external dependency?
|
||||
// - Pass in the entire load and render function as a prop?
|
||||
//import cornerstone from 'cornerstone-core';
|
||||
|
||||
/**
|
||||
* Asynchronous wrapper around Cornerstone's renderToCanvas method.
|
||||
*
|
||||
* @param {HTMLElement} canvasElement An HTML <canvas> element
|
||||
* @param {Image} image A Cornerstone Image
|
||||
*
|
||||
* @return {Promise} A promise tracking the progress of the rendering. Resolves empty.
|
||||
*/
|
||||
function renderAsync(canvasElement, image) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
cornerstone.renderToCanvas(canvasElement, image);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default class ImageThumbnail extends PureComponent {
|
||||
static propTypes = {
|
||||
imageSrc: PropTypes.string,
|
||||
imageId: PropTypes.string,
|
||||
error: PropTypes.bool.isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
stackPercentComplete: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
error: false,
|
||||
stackPercentComplete: 0,
|
||||
width: 217,
|
||||
height: 123,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.canvas = React.createRef();
|
||||
|
||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
||||
|
||||
this.state = {
|
||||
loading: renderIntoCanvas,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
||||
|
||||
if (renderIntoCanvas) {
|
||||
const { imageId } = this.props;
|
||||
const canvas = this.canvas.current;
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(
|
||||
image => {
|
||||
renderAsync(canvas, image).then(
|
||||
() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
},
|
||||
error => {
|
||||
// TODO: Set state?
|
||||
throw new Error(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
// TODO: Set state?
|
||||
throw new Error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let loadingOrError;
|
||||
if (this.props.error) {
|
||||
loadingOrError = <ViewportErrorIndicator />;
|
||||
} else if (this.state.loading) {
|
||||
loadingOrError = <ViewportLoadingIndicator />;
|
||||
}
|
||||
|
||||
const showStackLoadingProgressBar =
|
||||
this.props.stackPercentComplete !== undefined;
|
||||
|
||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
||||
|
||||
return (
|
||||
<div className="ImageThumbnail">
|
||||
<div className="image-thumbnail-canvas">
|
||||
{renderIntoCanvas ? (
|
||||
<canvas
|
||||
ref={this.canvas}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="static-image"
|
||||
src={this.props.imageSrc}
|
||||
//width={this.props.width}
|
||||
height={this.props.height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{loadingOrError}
|
||||
{showStackLoadingProgressBar && (
|
||||
<div className="image-thumbnail-progress-bar">
|
||||
<div
|
||||
className="image-thumbnail-progress-bar-inner"
|
||||
style={{ width: `${this.props.stackPercentComplete}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
:root {
|
||||
--text-secondary-color: #91B9CD;
|
||||
--active-color: #20A5D6;
|
||||
--primary-background-color: black;
|
||||
--ui-border-color-dark: #3C5D80
|
||||
--sidebar-transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.ThumbnailEntry.active .ImageThumbnail
|
||||
border-color: var(--active-color);
|
||||
box-shadow: none
|
||||
transition: var(--sidebar-transition);
|
||||
|
||||
.ImageThumbnail
|
||||
background-color: var(--primary-background-color);
|
||||
box-shadow: inset 0 0 0 1px var(--ui-border-color-dark);
|
||||
border: 5px solid transparent
|
||||
border-radius: 12px
|
||||
height: 135px
|
||||
margin: 0 auto
|
||||
padding 5px
|
||||
position: relative
|
||||
transition: var(--sidebar-transition);
|
||||
width: 217px
|
||||
-moz-background-clip: padding
|
||||
-webkit-background-clip: padding
|
||||
background-clip: padding-box
|
||||
|
||||
.image-thumbnail-canvas
|
||||
height: 100%
|
||||
overflow: hidden
|
||||
display: flex
|
||||
justify-content: center
|
||||
|
||||
img
|
||||
-webkit-user-drag: none
|
||||
pointer-events: none
|
||||
|
||||
|
||||
.thumbnailLoadingIndicator
|
||||
display: none
|
||||
pointer-events: none
|
||||
color: var(--text-secondary-color);
|
||||
height: 20px
|
||||
width: 100%
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
margin: auto
|
||||
position: absolute
|
||||
|
||||
&.d-block
|
||||
display: block;
|
||||
|
||||
p
|
||||
text-align: center
|
||||
font-size: 10pt
|
||||
|
||||
.image-thumbnail-progress-bar
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 3px
|
||||
top: -5px
|
||||
|
||||
.image-thumbnail-progress-bar-inner
|
||||
height: 100%
|
||||
width: 0
|
||||
border-radius: 5px
|
||||
background-color: var(--active-color);
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ThumbnailEntry } from './ThumbnailEntry';
|
||||
import ThumbnailEntryDragSource from './ThumbnailEntryDragSource.js';
|
||||
import './StudyBrowser.styl';
|
||||
|
||||
class StudyBrowser extends Component {
|
||||
static defaultProps = {
|
||||
studies: [],
|
||||
supportsDragAndDrop: true,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
studies: PropTypes.array.isRequired,
|
||||
supportsDragAndDrop: PropTypes.bool.isRequired,
|
||||
onThumbnailClick: PropTypes.func,
|
||||
onThumbnailDoubleClick: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
const studies = this.props.studies;
|
||||
|
||||
const thumbnails = studies.map((study, studyIndex) => {
|
||||
return study.thumbnails.map((thumb, thumbIndex) => {
|
||||
if (this.props.supportsDragAndDrop) {
|
||||
return (
|
||||
<ThumbnailEntryDragSource
|
||||
key={`${studyIndex}_${thumbIndex}`}
|
||||
{...study}
|
||||
{...thumb}
|
||||
id={`${studyIndex}_${thumbIndex}`}
|
||||
onClick={this.props.onThumbnailClick}
|
||||
onDoubleClick={this.props.onThumbnailDoubleClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="ThumbnailEntryContainer">
|
||||
<ThumbnailEntry
|
||||
key={`${studyIndex}_${thumbIndex}`}
|
||||
{...study}
|
||||
{...thumb}
|
||||
id={`${studyIndex}_${thumbIndex}`}
|
||||
onClick={this.props.onThumbnailClick}
|
||||
onDoubleClick={this.props.onThumbnailDoubleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const components = thumbnails.flat();
|
||||
return (
|
||||
<div className="StudyBrowser">
|
||||
<div className="scrollable-study-thumbnails">{components}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { StudyBrowser };
|
||||
@@ -0,0 +1,40 @@
|
||||
.StudyBrowser
|
||||
float: left
|
||||
height: 100%
|
||||
width: 100%
|
||||
overflow: hidden
|
||||
background-color: black
|
||||
padding-bottom: 20px
|
||||
padding-top: 10px
|
||||
|
||||
.scrollable-study-thumbnails
|
||||
height: 100%
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
padding-bottom: 50px
|
||||
padding-right: 16px
|
||||
padding-left: 4px
|
||||
margin-right: -16px
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar
|
||||
display: none
|
||||
|
||||
.ThumbnailEntryContainer
|
||||
margin: 0 auto;
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
.noselect
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer */
|
||||
-khtml-user-select: none; /* KHTML browsers (e.g. Konqueror) */
|
||||
-webkit-user-select: none; /* Chrome, Safari, and Opera */
|
||||
-webkit-touch-callout: none; /* Disable Android and iOS callouts*/
|
||||
|
||||
|
||||
.draggable
|
||||
cursor: copy
|
||||
cursor: -webkit-grab
|
||||
cursor: -moz-grab
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImageThumbnail from './ImageThumbnail';
|
||||
import './ThumbnailEntry.styl';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class ThumbnailEntry extends Component {
|
||||
static defaultProps = {
|
||||
active: false,
|
||||
error: false,
|
||||
stackPercentComplete: 0,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string,
|
||||
imageId: PropTypes.string,
|
||||
error: PropTypes.bool.isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
stackPercentComplete: PropTypes.number,
|
||||
/**
|
||||
altImageText will be used when no imageId or imageSrc is provided.
|
||||
It will be displayed inside the <div>. This is useful when it is difficult
|
||||
to make a preview for a type of DICOM series (e.g. DICOM-SR)
|
||||
*/
|
||||
altImageText: PropTypes.string,
|
||||
seriesDescription: PropTypes.string,
|
||||
seriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
instanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
numImageFrames: PropTypes.number,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
const hasInstanceNumber = this.props.instanceNumber !== undefined;
|
||||
|
||||
let className = classnames('ThumbnailEntry noselect', {
|
||||
active: this.props.active,
|
||||
});
|
||||
const infoOnly = false;
|
||||
|
||||
let contents = null;
|
||||
if (this.props.imageSrc || this.props.imageId) {
|
||||
contents = (
|
||||
<div className="p-x-1">
|
||||
<ImageThumbnail
|
||||
imageSrc={this.props.imageSrc}
|
||||
imageId={this.props.imageId}
|
||||
error={this.props.error}
|
||||
stackPercentComplete={this.props.stackPercentComplete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.altImageText) {
|
||||
contents = (
|
||||
<div className={'alt-image-text p-x-1'}>
|
||||
<h1>{this.props.altImageText}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
{contents}
|
||||
<div
|
||||
className={infoOnly ? 'series-details info-only' : 'series-details'}
|
||||
>
|
||||
<div className="series-description">
|
||||
{this.props.seriesDescription}
|
||||
</div>
|
||||
<div className="series-information">
|
||||
<div className="item item-series clearfix">
|
||||
<div className="icon">S:</div>
|
||||
<div className="value">{this.props.seriesNumber}</div>
|
||||
</div>
|
||||
{hasInstanceNumber && (
|
||||
<div className="item item-series clearfix">
|
||||
<div className="icon">I:</div>
|
||||
<div className="value">{this.props.instanceNumber}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="item item-frames clearfix">
|
||||
<div className="icon">
|
||||
<div />
|
||||
</div>
|
||||
<div className="value">{this.props.numImageFrames}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
onDoubleClick = () => {
|
||||
if (this.props.onDoubleClick) {
|
||||
this.props.onDoubleClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { ThumbnailEntry };
|
||||
@@ -0,0 +1,127 @@
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
:root {
|
||||
--series-count-background-color: #678696;
|
||||
}
|
||||
|
||||
.ThumbnailEntry
|
||||
cursor: pointer
|
||||
display: table
|
||||
|
||||
.p-x-1
|
||||
padding: 0 1rem
|
||||
|
||||
.alt-image-text
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--primary-background-color);
|
||||
box-shadow: inset 0 0 0 1px var(--ui-border-color-dark);
|
||||
border: 5px solid transparent;
|
||||
border-radius: 12px;
|
||||
height: 135px;
|
||||
margin: 0 auto;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
transition: var(--sidebar-transition);
|
||||
width: 217px;
|
||||
display: flex;
|
||||
|
||||
h1
|
||||
text-align: center
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.series-details
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 14px
|
||||
line-height: 1.3em
|
||||
margin-top: 5px
|
||||
margin-left: 1rem
|
||||
margin-right: 0.5rem
|
||||
max-width: 217px
|
||||
min-height: 36px
|
||||
position: relative
|
||||
word-wrap: break-word
|
||||
|
||||
.series-description
|
||||
flex-grow: 1
|
||||
|
||||
&.info-only
|
||||
.series-description
|
||||
display: none
|
||||
|
||||
.series-information
|
||||
display: flex
|
||||
flex-grow: 1
|
||||
float: none
|
||||
max-width: none
|
||||
padding-right: 0
|
||||
|
||||
.item
|
||||
flex: 1
|
||||
text-align: center
|
||||
|
||||
.icon, .value
|
||||
display: inline
|
||||
float: none
|
||||
line-height: 25px
|
||||
|
||||
.value
|
||||
margin-left: 0
|
||||
width: auto
|
||||
|
||||
.series-information
|
||||
padding-right: 4px
|
||||
max-width: 50px
|
||||
|
||||
.item-frames .icon
|
||||
height: 18px
|
||||
|
||||
.value
|
||||
color: var(--text-secondary-color);
|
||||
display: inline-block
|
||||
float: right
|
||||
font-size: 12px
|
||||
margin-left: 4px
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
width: calc(100% - 15px)
|
||||
|
||||
.icon
|
||||
color: var(--active-color);
|
||||
display: inline-block
|
||||
float: left
|
||||
font-size: 10px
|
||||
font-weight: 900
|
||||
text-align: right
|
||||
width: 11px
|
||||
|
||||
div
|
||||
background-color: var(--series-count-background-color);
|
||||
margin-top: 6px
|
||||
position: relative
|
||||
|
||||
&:after
|
||||
background-color: var(--active-color);
|
||||
box-shadow: 1px 1px rgba(0, 0, 0, .115)
|
||||
left: -4px
|
||||
position: absolute
|
||||
top: -4px
|
||||
|
||||
&
|
||||
&:after
|
||||
border: 0px solid var(--series-count-background-color)
|
||||
content: ''
|
||||
display: inline-block
|
||||
height: 11px
|
||||
width: 11px
|
||||
|
||||
.image-thumbnail-clone
|
||||
z-index: 100000
|
||||
pointer-events: none
|
||||
|
||||
&:hover
|
||||
border-color: var(--hover-color);
|
||||
box-shadow: none
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragSource } from 'react-dnd';
|
||||
import DragPreview from './DragPreview';
|
||||
import { ThumbnailEntry } from './ThumbnailEntry.js';
|
||||
|
||||
// Drag sources and drop targets only interact
|
||||
// if they have the same string type.
|
||||
const Types = {
|
||||
THUMBNAIL: 'thumbnail',
|
||||
};
|
||||
|
||||
const thumbnailSource = {
|
||||
/*canDrag(props) {
|
||||
return props.error === false;
|
||||
},*/
|
||||
|
||||
beginDrag(props) {
|
||||
return props;
|
||||
},
|
||||
|
||||
endDrag(props, monitor) {
|
||||
//const item = monitor.getItem();
|
||||
const dropResult = monitor.getDropResult();
|
||||
|
||||
if (dropResult) {
|
||||
//console.log(`You dropped ${item.id} into ${dropResult.id}!`);
|
||||
//console.log(item);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
class ThumbnailEntryDragSource extends Component {
|
||||
static propTypes = {
|
||||
connectDragSource: PropTypes.func.isRequired,
|
||||
isDragging: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isDragging: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { connectDragSource } = this.props;
|
||||
const dropEffect = 'copy';
|
||||
|
||||
return connectDragSource(
|
||||
<div className="ThumbnailEntryContainer">
|
||||
<DragPreview {...this.props} />
|
||||
<ThumbnailEntry {...this.props} />
|
||||
</div>,
|
||||
{ dropEffect }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const collect = (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging(),
|
||||
});
|
||||
|
||||
export default DragSource(Types.THUMBNAIL, thumbnailSource, collect)(
|
||||
ThumbnailEntryDragSource
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
export const studies = [
|
||||
{
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg',
|
||||
seriesDescription: 'Anti-PD-1_Lung',
|
||||
active: true,
|
||||
seriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
stackPercentComplete: 30,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg',
|
||||
seriesDescription: 'Anti-PD-1_MELANOMA',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
stackPercentComplete: 70,
|
||||
},
|
||||
{
|
||||
altImageText: 'SR',
|
||||
seriesDescription: 'Imaging Measurement Report',
|
||||
seriesNumber: '3',
|
||||
stackPercentComplete: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg',
|
||||
seriesDescription: 'CPTAC-GBM',
|
||||
active: true,
|
||||
seriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
stackPercentComplete: 100,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg',
|
||||
seriesDescription: 'CPTAC-CM',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg',
|
||||
seriesDescription: 'CPTAC-HNSCC',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg',
|
||||
seriesDescription: 'CPTAC-LSCC',
|
||||
seriesNumber: '2',
|
||||
instanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function onThumbnailClick() {
|
||||
console.warn('onThumbnailClick');
|
||||
}
|
||||
|
||||
export function onThumbnailDoubleClick() {
|
||||
console.warn('onThumbnailDoubleClick');
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Study Browser
|
||||
menu: Components
|
||||
route: /components/study-browser
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { WrappedStudyBrowser } from './wrappedStudyBrowser.js'
|
||||
import { StudyBrowser } from './../index.js'
|
||||
|
||||
# Study Browser
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<WrappedStudyBrowser />
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={StudyBrowser} />
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import TouchBackend from 'react-dnd-touch-backend';
|
||||
|
||||
//
|
||||
import {
|
||||
studies,
|
||||
onThumbnailClick,
|
||||
onThumbnailDoubleClick,
|
||||
} from './exampleStudies.js';
|
||||
import { ExampleDropTarget, StudyBrowser } from './../index.js';
|
||||
|
||||
class StudyBrowserContainer extends Component {
|
||||
render() {
|
||||
//const viewportData = [null, null, null, null];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ExampleDropTarget />
|
||||
<StudyBrowser
|
||||
studies={studies}
|
||||
onThumbnailClick={onThumbnailClick}
|
||||
onThumbnailDoubleClick={onThumbnailDoubleClick}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note:
|
||||
// Normally, the top level APP component is wrapped with the DragDropContext
|
||||
// We wrap this component to create a simple/local example.
|
||||
const WrappedStudyBrowser = DragDropContext(
|
||||
TouchBackend({ enableMouseEvents: true }),
|
||||
null,
|
||||
true
|
||||
)(StudyBrowserContainer);
|
||||
|
||||
// http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context
|
||||
export { WrappedStudyBrowser };
|
||||
@@ -0,0 +1,3 @@
|
||||
export { ExampleDropTarget } from './ExampleDropTarget.js';
|
||||
export { StudyBrowser } from './StudyBrowser.js';
|
||||
export { ThumbnailEntry } from './ThumbnailEntry.js';
|
||||
@@ -0,0 +1,99 @@
|
||||
// If you want to continue using CSS stylesheets and classes...
|
||||
// https://github.com/airbnb/react-dates#initialize
|
||||
import 'react-dates/initialize';
|
||||
import 'react-dates/lib/css/_datepicker.css';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DateRangePicker } from 'react-dates';
|
||||
|
||||
import './CustomDateRangePicker.styl';
|
||||
|
||||
export default class CustomDateRangePicker extends React.Component {
|
||||
static propTypes = {
|
||||
presets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
start: PropTypes.required,
|
||||
end: PropTypes.required,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
startDate: this.props.startDate,
|
||||
endDate: this.props.endDate,
|
||||
};
|
||||
|
||||
this.renderDatePresets = this.renderDatePresets.bind(this);
|
||||
this.onDatesChange = this.onDatesChange.bind(this);
|
||||
}
|
||||
|
||||
onDatesChange({ startDate, endDate, preset }) {
|
||||
this.setState({ startDate, endDate });
|
||||
if (this.props.onDatesChange) {
|
||||
this.props.onDatesChange({ startDate, endDate, preset });
|
||||
}
|
||||
}
|
||||
|
||||
renderDatePresets() {
|
||||
const { presets } = this.props;
|
||||
const { startDate, endDate } = this.state;
|
||||
|
||||
return (
|
||||
<div className="PresetDateRangePicker_panel">
|
||||
{presets.map(({ text, start, end }) => {
|
||||
const isSelected = startDate === start && endDate === end;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={text}
|
||||
type="button"
|
||||
className={`PresetDateRangePicker_button ${
|
||||
isSelected ? 'PresetDateRangePicker_button__selected' : ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
this.onDatesChange({
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
preset: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
autoFocus,
|
||||
autoFocusEndDate,
|
||||
initialStartDate,
|
||||
initialEndDate,
|
||||
stateDateWrapper,
|
||||
onDatesChange,
|
||||
startDate,
|
||||
endDate,
|
||||
presets,
|
||||
...dateRangePickerProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DateRangePicker
|
||||
{...dateRangePickerProps}
|
||||
startDate={this.state.startDate}
|
||||
endDate={this.state.endDate}
|
||||
renderCalendarInfo={this.renderDatePresets}
|
||||
onDatesChange={this.onDatesChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
.CalendarDay__selected_span {
|
||||
background: var(--calendar-main-color);
|
||||
color: var(--calendar-day-color);
|
||||
border: 1px solid;
|
||||
border-color: var(--calendar-day-border-color);
|
||||
}
|
||||
|
||||
.CalendarDay__default:hover {
|
||||
color: var(--calendar-main-color);
|
||||
}
|
||||
|
||||
.CalendarDay__selected_span:active, .CalendarDay__selected_span:hover {
|
||||
background: var(--calendar-day-active-hover-background-color);
|
||||
border-color: var(--calendar-day-border-color);
|
||||
color: var(--calendar-day-color);
|
||||
}
|
||||
|
||||
.CalendarDay__selected {
|
||||
background: var(--calendar-main-color);
|
||||
color: var(--calendar-day-color);
|
||||
border-color: var(--calendar-day-border-color);
|
||||
}
|
||||
|
||||
.CalendarDay__selected:hover {
|
||||
background: var(--calendar-main-color);
|
||||
color: var(--calendar-day-color);
|
||||
border-color: var(--calendar-day-border-color);
|
||||
}
|
||||
|
||||
.CalendarDay__hovered_span:hover,
|
||||
.CalendarDay__hovered_span {
|
||||
background: var(--calendar-main-color);
|
||||
color: var(--calendar-day-color);
|
||||
border-color:var(--calendar-day-color);
|
||||
}
|
||||
|
||||
.DateInput {
|
||||
width: 97px
|
||||
}
|
||||
|
||||
.DateInput_input {
|
||||
background-color: var(--input-background-color)
|
||||
border-color: var(--input-background-color)
|
||||
color: var(--input-placeholder-color)
|
||||
height: 38px
|
||||
font-size: 10pt
|
||||
}
|
||||
|
||||
.DateRangePickerInput {
|
||||
background-color: var(--input-background-color)
|
||||
border-color: var(--input-background-color)
|
||||
color: var(--input-placeholder-color)
|
||||
height: 40px
|
||||
}
|
||||
|
||||
.PresetDateRangePicker_panel: {
|
||||
padding: 0 22px 11px 22px;
|
||||
}
|
||||
|
||||
.PresetDateRangePicker_button {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
textAlign: center;
|
||||
background: none;
|
||||
border: 2px solid var(--calendar-main-color);
|
||||
color: var(--calendar-main-color);
|
||||
padding: 4px 12px;
|
||||
marginRight: 8;
|
||||
font: inherit;
|
||||
fontWeight: 700;
|
||||
lineHeight: normal;
|
||||
overflow: visible;
|
||||
boxSizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
:active: {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.PresetDateRangePicker_button__selected: {
|
||||
color: var(--calendar-day-color);
|
||||
background: var(--calendar-main-color);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './PaginationArea.styl';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
class PaginationArea extends PureComponent {
|
||||
static defaultProps = {
|
||||
pageOptions: [5, 10, 25, 50, 100],
|
||||
rowsPerPage: 25,
|
||||
currentPage: 0,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
pageOptions: PropTypes.array.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
nextPageFunc: PropTypes.func,
|
||||
prevPageFunc: PropTypes.func,
|
||||
onRowsPerPageChange: PropTypes.func,
|
||||
recordCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
nextPage = () => {
|
||||
this.props.nextPageFunc(this.props.currentPage);
|
||||
};
|
||||
|
||||
prevPage = () => {
|
||||
this.props.prevPageFunc(this.props.currentPage);
|
||||
};
|
||||
|
||||
onRowsPerPageChange = event => {
|
||||
this.props.onRowsPerPageChange(parseInt(event.target.value));
|
||||
};
|
||||
|
||||
renderPaginationButtons() {
|
||||
return (
|
||||
<div className="col-xs-8 col-sm-9 col-md-9">
|
||||
<div className="form-inline form-group page-buttons noselect">
|
||||
<React.Fragment>
|
||||
<ul className="pagination-control no-margins">
|
||||
<li className="page-item prev">
|
||||
<button
|
||||
onClick={this.prevPage}
|
||||
disabled={this.props.currentPage === 0}
|
||||
className="btn page-link"
|
||||
>
|
||||
{this.props.t('Previous')}
|
||||
</button>
|
||||
</li>
|
||||
<li className="page-item next">
|
||||
<button
|
||||
onClick={this.nextPage}
|
||||
disabled={
|
||||
this.props.recordCount === 0 ||
|
||||
this.props.rowsPerPage > this.props.recordCount
|
||||
}
|
||||
className="btn page-link"
|
||||
>
|
||||
{this.props.t('Next')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderRowsPerPageDropdown() {
|
||||
return (
|
||||
<div className="form-inline form-group rows-per-page">
|
||||
<span>{this.props.t('Show')}</span>
|
||||
<select
|
||||
onChange={this.onRowsPerPageChange}
|
||||
defaultValue={this.props.rowsPerPage}
|
||||
>
|
||||
{this.props.pageOptions.map(pageNumber => {
|
||||
return (
|
||||
<option key={pageNumber} value={pageNumber}>
|
||||
{pageNumber}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>{this.props.t('RowsPerPage')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div name="paginationArea">
|
||||
<div className="pagination-area">
|
||||
<div className="row">
|
||||
<div className="rows-dropdown">
|
||||
{this.renderRowsPerPageDropdown()}
|
||||
</div>
|
||||
<div className="pagination-buttons">
|
||||
<div className="form-inline form-group page-number pull-right">
|
||||
{this.renderPaginationButtons()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('Common')(PaginationArea);
|
||||
export { connectedComponent as PaginationArea };
|
||||
@@ -0,0 +1,64 @@
|
||||
.pagination-area
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 13px
|
||||
font-weight: normal !important
|
||||
|
||||
label
|
||||
font-weight: normal
|
||||
|
||||
select
|
||||
margin: 5px
|
||||
background-color: var(--primary-background-color)
|
||||
color: white
|
||||
|
||||
.row
|
||||
display: flex;
|
||||
|
||||
.rows-dropdown
|
||||
width: 25%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
|
||||
.pagination-buttons
|
||||
width: 75%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
|
||||
.form-group
|
||||
margin-bottom: 15px;
|
||||
|
||||
.rows-per-page label.wrapperLabel
|
||||
display: inline-table !important
|
||||
margin: 0 4px
|
||||
|
||||
select
|
||||
margin: 0px 4px 0px 4px
|
||||
width: 42px
|
||||
|
||||
.page-buttons
|
||||
margin: 0
|
||||
text-align: right
|
||||
font-weight: normal
|
||||
ul.pagination-control
|
||||
margin: 0
|
||||
|
||||
li
|
||||
display: table-cell
|
||||
padding: 5px 2px
|
||||
|
||||
button
|
||||
padding: 4px 8px
|
||||
background-color: var(--primary-background-color)
|
||||
border-color: var(--ui-gray)
|
||||
color: var(--ui-gray-darkest)
|
||||
color: white
|
||||
text-decoration: none
|
||||
|
||||
&:hover:enabled
|
||||
color: var(--active-color)
|
||||
|
||||
.active
|
||||
button
|
||||
background-color: var(--ui-gray)
|
||||
border-color: #ddd
|
||||
color: white
|
||||
@@ -0,0 +1,428 @@
|
||||
import './StudyList.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import CustomDateRangePicker from './CustomDateRangePicker.js';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { PaginationArea } from './PaginationArea.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyListLoadingText } from './StudyListLoadingText.js';
|
||||
import { StudylistToolbar } from './StudyListToolbar.js';
|
||||
import { isInclusivelyBeforeDay } from 'react-dates';
|
||||
import moment from 'moment';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
const today = moment();
|
||||
const lastWeek = moment().subtract(7, 'day');
|
||||
const lastMonth = moment().subtract(1, 'month');
|
||||
|
||||
class StudyList extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array.isRequired,
|
||||
onSelectItem: PropTypes.func.isRequired,
|
||||
onSearch: PropTypes.func.isRequired,
|
||||
currentPage: PropTypes.number,
|
||||
rowsPerPage: PropTypes.number,
|
||||
studyListDateFilterNumDays: PropTypes.number,
|
||||
studyListFunctionsEnabled: PropTypes.bool,
|
||||
defaultSort: PropTypes.shape({
|
||||
field: PropTypes.string.isRequired,
|
||||
order: PropTypes.oneOf(['desc', 'asc']).isRequired,
|
||||
}),
|
||||
onImport: PropTypes.func,
|
||||
pageOptions: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
currentPage: 0,
|
||||
rowsPerPage: 25,
|
||||
studyListDateFilterNumDays: 7,
|
||||
};
|
||||
|
||||
static studyDatePresets = [
|
||||
{
|
||||
text: 'Today',
|
||||
start: today,
|
||||
end: today,
|
||||
},
|
||||
{
|
||||
text: 'Last 7 days',
|
||||
start: lastWeek,
|
||||
end: today,
|
||||
},
|
||||
{
|
||||
text: 'Last 30 days',
|
||||
start: lastMonth,
|
||||
end: today,
|
||||
},
|
||||
];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const sortData = {
|
||||
field: undefined,
|
||||
order: undefined,
|
||||
};
|
||||
|
||||
// init from props
|
||||
if (props.defaultSort) {
|
||||
sortData.field = props.defaultSort.field;
|
||||
// todo: -1, 0, 1?
|
||||
sortData.order = props.defaultSort.order; // asc, desc
|
||||
}
|
||||
|
||||
this.defaultStartDate = moment().subtract(
|
||||
this.props.studyListDateFilterNumDays,
|
||||
'days'
|
||||
);
|
||||
this.defaultEndDate = moment();
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
error: false,
|
||||
searchData: {
|
||||
sortData,
|
||||
currentPage: this.props.currentPage,
|
||||
rowsPerPage: this.props.rowsPerPage,
|
||||
studyDateFrom: this.defaultStartDate,
|
||||
studyDateTo: this.defaultEndDate,
|
||||
},
|
||||
highlightedItem: '',
|
||||
};
|
||||
|
||||
this.getChangeHandler = this.getChangeHandler.bind(this);
|
||||
this.getBlurHandler = this.getBlurHandler.bind(this);
|
||||
this.onInputKeydown = this.onInputKeydown.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
this.prevPage = this.prevPage.bind(this);
|
||||
this.onRowsPerPageChange = this.onRowsPerPageChange.bind(this);
|
||||
this.delayedSearch = debounce(this.search, 250);
|
||||
}
|
||||
|
||||
getChangeHandler(key) {
|
||||
return event => {
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData(key, event.target.value, this.delayedSearch);
|
||||
};
|
||||
}
|
||||
|
||||
getBlurHandler(key) {
|
||||
return event => {
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData(key, event.target.value, this.search);
|
||||
};
|
||||
}
|
||||
|
||||
setSearchData(key, value, callback) {
|
||||
const searchData = this.state.searchData;
|
||||
searchData[key] = value;
|
||||
this.setState({ searchData }, callback);
|
||||
}
|
||||
|
||||
setSearchDataBatch(keyValues, callback) {
|
||||
const searchData = this.state.searchData;
|
||||
|
||||
Object.keys(keyValues).forEach(key => {
|
||||
searchData[key] = keyValues[key];
|
||||
});
|
||||
|
||||
this.setState({ searchData }, callback);
|
||||
}
|
||||
|
||||
async onInputKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.delayedSearch.cancel();
|
||||
// reset the page because user is doing a new search
|
||||
this.setSearchData('currentPage', 0, this.search);
|
||||
}
|
||||
}
|
||||
|
||||
async search() {
|
||||
try {
|
||||
this.setState({ loading: true, error: false });
|
||||
await this.props.onSearch(this.state.searchData);
|
||||
} catch (error) {
|
||||
this.setState({ error: true });
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
renderNoMachingResults() {
|
||||
if (!this.props.studies.length && !this.state.error) {
|
||||
return <div className="notFound">No matching results</div>;
|
||||
}
|
||||
}
|
||||
|
||||
renderHasError() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="notFound">There was an error fetching studies</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderIsLoading() {
|
||||
if (this.state.loading) {
|
||||
return (
|
||||
<div className="loading">
|
||||
<StudyListLoadingText />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(currentPage) {
|
||||
currentPage = currentPage + 1;
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData('currentPage', currentPage, this.search);
|
||||
}
|
||||
|
||||
prevPage(currentPage) {
|
||||
currentPage = currentPage - 1;
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData('currentPage', currentPage, this.search);
|
||||
}
|
||||
|
||||
onRowsPerPageChange(rowsPerPage) {
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }, this.search);
|
||||
}
|
||||
|
||||
onSortClick(field) {
|
||||
return () => {
|
||||
let order;
|
||||
const sort = this.state.searchData.sortData;
|
||||
const isSortedField = sort.field === field;
|
||||
|
||||
if (isSortedField) {
|
||||
if (sort.order === 'asc') {
|
||||
order = 'desc';
|
||||
} else {
|
||||
order = undefined;
|
||||
field = undefined;
|
||||
}
|
||||
} else {
|
||||
order = 'asc';
|
||||
}
|
||||
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData('sortData', { field, order }, this.search);
|
||||
};
|
||||
}
|
||||
|
||||
onHighlightItem(studyItemUid) {
|
||||
this.setState({ highlightedItem: studyItemUid });
|
||||
}
|
||||
|
||||
renderTableRow(study) {
|
||||
return (
|
||||
<tr
|
||||
key={study.studyInstanceUid}
|
||||
className={
|
||||
this.state.highlightedItem === study.studyInstanceUid
|
||||
? 'studylistStudy noselect active'
|
||||
: 'studylistStudy noselect'
|
||||
}
|
||||
onMouseDown={event => {
|
||||
// middle/wheel click
|
||||
if (event.button === 1) {
|
||||
this.props.onSelectItem(study.studyInstanceUid);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
this.onHighlightItem(study.studyInstanceUid);
|
||||
this.props.onSelectItem(study.studyInstanceUid);
|
||||
}}
|
||||
>
|
||||
<td className={study.patientName ? 'patientName' : 'emptyCell'}>
|
||||
{study.patientName || `(${this.props.t('Empty')})`}
|
||||
</td>
|
||||
|
||||
<td className="patientId">{study.patientId}</td>
|
||||
<td className="accessionNumber">{study.accessionNumber}</td>
|
||||
<td className="studyDate">{study.studyDate}</td>
|
||||
<td className="modalities">{study.modalities}</td>
|
||||
<td className="studyDescription">{study.studyDescription}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const tableMeta = {
|
||||
patientName: {
|
||||
displayText: this.props.t('PatientName'),
|
||||
sort: 0,
|
||||
},
|
||||
patientId: {
|
||||
displayText: this.props.t('MRN'),
|
||||
sort: 0,
|
||||
},
|
||||
accessionNumber: {
|
||||
displayText: this.props.t('AccessionNumber'),
|
||||
sort: 0,
|
||||
},
|
||||
studyDate: {
|
||||
displayText: this.props.t('StudyDate'),
|
||||
inputType: 'date-range',
|
||||
sort: 0,
|
||||
},
|
||||
modalities: {
|
||||
displayText: this.props.t('Modality'),
|
||||
sort: 0,
|
||||
},
|
||||
studyDescription: {
|
||||
displayText: this.props.t('StudyDescription'),
|
||||
sort: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Apply sort
|
||||
const sortedFieldName = this.state.searchData.sortData.field;
|
||||
const sortedField = tableMeta[sortedFieldName];
|
||||
|
||||
if (sortedField) {
|
||||
const sortOrder = this.state.searchData.sortData.order;
|
||||
sortedField.sort = sortOrder === 'asc' ? 1 : 2;
|
||||
}
|
||||
|
||||
// Sort Icons
|
||||
const sortIcons = ['sort', 'sort-up', 'sort-down'];
|
||||
|
||||
return (
|
||||
<div className="StudyList">
|
||||
<div className="studyListToolbar clearfix">
|
||||
<div className="header pull-left">{this.props.t('StudyList')}</div>
|
||||
<div className="studyCount pull-right">
|
||||
{this.props.studies.length}
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
{this.props.studyListFunctionsEnabled ? (
|
||||
<StudylistToolbar onImport={this.props.onImport} />
|
||||
) : null}
|
||||
</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
<div className="theadBackground" />
|
||||
<div id="studyListContainer">
|
||||
<table id="tblStudyList" className="studylistResult table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
{Object.keys(tableMeta).map((fieldName, i) => {
|
||||
const field = tableMeta[fieldName];
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<th className={fieldName}>
|
||||
<div
|
||||
id={`_${fieldName}`}
|
||||
className="display-text"
|
||||
onClick={this.onSortClick(fieldName)}
|
||||
>
|
||||
<span>{field.displayText}</span>
|
||||
<Icon
|
||||
name={sortIcons[field.sort]}
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
{!field.inputType && (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control studylist-search"
|
||||
id={fieldName}
|
||||
value={this.state[fieldName]}
|
||||
onKeyDown={this.onInputKeydown}
|
||||
onChange={this.getChangeHandler(fieldName)}
|
||||
onBlur={this.getBlurHandler(fieldName)}
|
||||
/>
|
||||
)}
|
||||
{field.inputType === 'date-range' && (
|
||||
<div style={{ display: 'block' }}>
|
||||
<CustomDateRangePicker
|
||||
presets={StudyList.studyDatePresets}
|
||||
showClearDates={true}
|
||||
startDateId="studyListStartDate"
|
||||
endDateId="studyListEndDate"
|
||||
startDate={this.defaultStartDate}
|
||||
endDate={this.defaultEndDate}
|
||||
hideKeyboardShortcutsPanel={true}
|
||||
anchorDirection="left"
|
||||
isOutsideRange={day =>
|
||||
!isInclusivelyBeforeDay(day, moment())
|
||||
}
|
||||
onDatesChange={({
|
||||
startDate,
|
||||
endDate,
|
||||
preset = false,
|
||||
}) => {
|
||||
if (
|
||||
startDate &&
|
||||
endDate &&
|
||||
(this.state.focusedInput === 'endDate' ||
|
||||
preset)
|
||||
) {
|
||||
this.setSearchDataBatch(
|
||||
{
|
||||
studyDateFrom: startDate.toDate(),
|
||||
studyDateTo: endDate.toDate(),
|
||||
},
|
||||
this.search
|
||||
);
|
||||
this.setState({ focusedInput: false });
|
||||
} else if (!startDate && !endDate) {
|
||||
this.setSearchDataBatch(
|
||||
{
|
||||
studyDateFrom: null,
|
||||
studyDateTo: null,
|
||||
},
|
||||
this.search
|
||||
);
|
||||
}
|
||||
}}
|
||||
focusedInput={this.state.focusedInput}
|
||||
onFocusChange={focusedInput => {
|
||||
this.setState({ focusedInput });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="studyListData">
|
||||
{this.props.studies.map(study => {
|
||||
return this.renderTableRow(study);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{this.renderIsLoading()}
|
||||
{this.renderHasError()}
|
||||
{this.renderNoMachingResults()}
|
||||
|
||||
<PaginationArea
|
||||
pageOptions={this.props.pageOptions}
|
||||
currentPage={this.state.searchData.currentPage}
|
||||
nextPageFunc={this.nextPage}
|
||||
prevPageFunc={this.prevPage}
|
||||
onRowsPerPageChange={this.onRowsPerPageChange}
|
||||
rowsPerPage={this.state.searchData.rowsPerPage}
|
||||
recordCount={this.props.studies.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('StudyList')(StudyList);
|
||||
export { connectedComponent as StudyList };
|
||||
@@ -0,0 +1,282 @@
|
||||
@import './../../design/styles/common/form.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
$study-list-padding = 8%;
|
||||
$study-list-toolbar-height = 75px;
|
||||
$body-cell-height = 40px;
|
||||
|
||||
$tablePaddingMediumScreen = 5px
|
||||
$tablePaddingBigScreen = 3%
|
||||
$tablePaddingLargeScreen = 5%
|
||||
|
||||
// TODO: move this to a common mixin files
|
||||
placeholder-color(c)
|
||||
&::-webkit-input-placeholder
|
||||
color: c
|
||||
&:-moz-placeholder
|
||||
color: c
|
||||
&::-moz-placeholder
|
||||
color: c
|
||||
&:-ms-input-placeholder
|
||||
color: c
|
||||
|
||||
.DateRangePicker
|
||||
height: 40px
|
||||
margin: 0 5px 20px 5px
|
||||
cursor: pointer
|
||||
border: none
|
||||
position: block
|
||||
|
||||
.studyListToolbar
|
||||
background-color: var(--ui-gray-darker)
|
||||
height: $study-list-toolbar-height
|
||||
margin-bottom: 2px
|
||||
padding: 0 $study-list-padding
|
||||
|
||||
&>div
|
||||
display: inline-block
|
||||
|
||||
.header
|
||||
font-size: 22px
|
||||
font-weight: 300
|
||||
color: var(--table-text-secondary-color)
|
||||
line-height: $study-list-toolbar-height
|
||||
|
||||
.studylistToolbar
|
||||
height: $study-list-toolbar-height
|
||||
line-height: $study-list-toolbar-height
|
||||
|
||||
.studyCount
|
||||
color: var(--large-numbers-color)
|
||||
font-size: 40px
|
||||
font-weight: 100
|
||||
line-height: $study-list-toolbar-height
|
||||
|
||||
.theadBackground
|
||||
height: 121px
|
||||
position: absolute
|
||||
width: 100%
|
||||
|
||||
&:before, &:after
|
||||
content: ''
|
||||
display: block
|
||||
left: 0
|
||||
position: absolute
|
||||
width: 100%
|
||||
|
||||
&:before
|
||||
background-color: var(--ui-gray-darker)
|
||||
height: 100%
|
||||
top: 0
|
||||
z-index: 1
|
||||
|
||||
&:after
|
||||
background-color: var(--ui-border-color-active)
|
||||
bottom: -1px
|
||||
height: 1px
|
||||
z-index: 2
|
||||
|
||||
#studyListContainer
|
||||
width: 100%
|
||||
padding: 0 $study-list-padding
|
||||
position: absolute
|
||||
z-index: 2
|
||||
|
||||
.loading-text
|
||||
color: var(--table-text-secondary-color)
|
||||
font-size: 30px
|
||||
|
||||
.notFound
|
||||
color: var(--table-text-secondary-color)
|
||||
font-size: 30px
|
||||
font-weight: 200
|
||||
text-align: center
|
||||
|
||||
table#tblStudyList
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
> tr
|
||||
height: 20px
|
||||
|
||||
> thead
|
||||
white-space: nowrap
|
||||
|
||||
tr
|
||||
th
|
||||
padding: 0
|
||||
border-bottom: 1px solid var(--ui-border-color-active);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-top: 0;
|
||||
|
||||
&.studyDate
|
||||
min-width: 230px
|
||||
|
||||
.display-text
|
||||
display: inline-block
|
||||
cursor: pointer
|
||||
width: 100%
|
||||
min-width: 95px
|
||||
margin: 0 auto
|
||||
color: var(--table-text-primary-color)
|
||||
font-weight: 400
|
||||
padding: 20px
|
||||
-webkit-user-select: none
|
||||
-moz-user-select: none
|
||||
-ms-user-select: none
|
||||
user-select: none
|
||||
|
||||
span
|
||||
font-size: 15px
|
||||
float: left
|
||||
|
||||
i
|
||||
margin: 0 5px
|
||||
|
||||
&:hover
|
||||
color: var(--active-color)
|
||||
|
||||
&.active, &:active
|
||||
color: var(--active-color)
|
||||
|
||||
input.studylist-search
|
||||
height: 40px
|
||||
margin: 0 5px 20px 5px
|
||||
padding: 0 20px
|
||||
cursor: pointer
|
||||
border: none
|
||||
background-color: var(--input-background-color)
|
||||
color: var(--input-placeholder-color)
|
||||
font-size: 10pt
|
||||
font-weight: normal
|
||||
width: calc(100% - 10px)
|
||||
border-radius: 4px
|
||||
|
||||
transition(all 0.15s ease)
|
||||
placeholder-color(var(--input-placeholder-color))
|
||||
|
||||
&.invisible
|
||||
visibility: hidden
|
||||
|
||||
&:active, &:hover
|
||||
background-color: var(--input-background-color)
|
||||
|
||||
::-webkit-datetime-edit-year-field:not([aria-valuenow]),
|
||||
::-webkit-datetime-edit-month-field:not([aria-valuenow]),
|
||||
::-webkit-datetime-edit-day-field:not([aria-valuenow])
|
||||
color: transparent
|
||||
|
||||
> tbody
|
||||
tr
|
||||
padding: 5px
|
||||
background-color: black
|
||||
|
||||
&:nth-child(even)
|
||||
background-color: var(--ui-gray-darker)
|
||||
|
||||
td
|
||||
padding: 8px;
|
||||
height: $body-cell-height
|
||||
line-height: $body-cell-height
|
||||
color: var(--table-text-primary-color)
|
||||
font-weight: 300
|
||||
border-top: 1px solid var(--ui-gray-lighter)
|
||||
border-bottom: 1px solid var(--ui-gray-lighter)
|
||||
white-space: nowrap
|
||||
transition(all 0.1s ease)
|
||||
|
||||
&.emptyCell
|
||||
color: #516873
|
||||
|
||||
&.emptyValue
|
||||
color: var(--ui-gray-light)
|
||||
|
||||
&:hover, &:active, &.active
|
||||
background-color: var(--table-hover-color)
|
||||
color: var(--text-primary-color)
|
||||
|
||||
td
|
||||
// This selector is necessary to override bootstrap's 'table' class
|
||||
border-top: 1px solid var(--ui-gray-lighter)
|
||||
border-bottom: 1px solid var(--ui-gray-lighter)
|
||||
background-color: var(--table-hover-color)
|
||||
|
||||
@media only screen and (max-width: 1362px)
|
||||
#studyListContainer
|
||||
padding: 0 $tablePaddingLargeScreen
|
||||
|
||||
table#tblStudyList
|
||||
thead,
|
||||
tbody
|
||||
tr
|
||||
th,
|
||||
td
|
||||
&:first-child
|
||||
padding-left: $tablePaddingLargeScreen
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePaddingLargeScreen
|
||||
|
||||
@media only screen and (max-width: 1161px)
|
||||
#studyListContainer
|
||||
padding: 0 $tablePaddingBigWidth
|
||||
|
||||
table#tblStudyList
|
||||
thead,
|
||||
tbody
|
||||
tr
|
||||
th,
|
||||
td
|
||||
&:first-child
|
||||
padding-left: $tablePaddingBigScreen
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePaddingBigScreen
|
||||
|
||||
@media only screen and (max-width: 1069px)
|
||||
.theadBackground
|
||||
height: 101px
|
||||
|
||||
.studylist-pagination > .row
|
||||
margin-right: 0;
|
||||
|
||||
.studyListToolbar
|
||||
padding: 0 $tablePaddingMediumScreen
|
||||
|
||||
#studyListContainer
|
||||
padding: 0
|
||||
|
||||
table#tblStudyList
|
||||
thead > tr > th
|
||||
|
||||
&:first-child
|
||||
padding-left: $tablePaddingMediumScreen
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePaddingMediumScreen
|
||||
|
||||
input.worklist-search
|
||||
padding: 10px
|
||||
|
||||
.display-text
|
||||
padding: 10px 5px
|
||||
|
||||
i
|
||||
width: auto
|
||||
|
||||
tbody > tr > td
|
||||
|
||||
&:first-child
|
||||
padding-left: $tablePaddingMediumScreen
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePaddingMediumScreen
|
||||
|
||||
.worklistPagination
|
||||
.row
|
||||
margin-left: 0
|
||||
margin-right: 0
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import React from 'react';
|
||||
|
||||
function StudyListLoadingText() {
|
||||
return (
|
||||
<div className="loading-text">
|
||||
Loading... <Icon name="circle-notch" animation="pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { StudyListLoadingText };
|
||||
@@ -0,0 +1,40 @@
|
||||
import './StudyListToolbar.styl';
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class StudylistToolbar extends PureComponent {
|
||||
static propTypes = {
|
||||
onImport: PropTypes.func,
|
||||
};
|
||||
|
||||
onImport = event => {
|
||||
if (this.props.onImport) {
|
||||
this.props.onImport(event);
|
||||
}
|
||||
};
|
||||
|
||||
getImportTool() {
|
||||
if (this.props.onImport) {
|
||||
return (
|
||||
<div className="addNewStudy btn-file">
|
||||
<label
|
||||
htmlFor="btnImport"
|
||||
style={{ width: '18px' }}
|
||||
onClick={this.onImport}
|
||||
>
|
||||
<Icon name="plus" />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="studylistToolbar">{this.getImportTool()}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export { StudylistToolbar };
|
||||
@@ -0,0 +1,26 @@
|
||||
.studyListToolbar
|
||||
.addNewStudy
|
||||
margin: 0 10px
|
||||
|
||||
label
|
||||
font-weight: 400
|
||||
cursor: pointer
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
|
||||
input
|
||||
width: 0.1px;
|
||||
height: 0.1px
|
||||
opacity: 0
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
z-index: -1
|
||||
|
||||
color: var(--text-secondary-color)
|
||||
|
||||
&:hover
|
||||
color: var(--hover-color)
|
||||
|
||||
&:active
|
||||
color: var(--active-color)
|
||||
@@ -0,0 +1,72 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export default function(searchData) {
|
||||
this.setState({ searchData });
|
||||
|
||||
const filter = (key, searchData, study) => {
|
||||
if (key === 'studyDateFrom' && searchData[key] && study['studyDate']) {
|
||||
const studyDate = moment(study['studyDate'], 'YYYYMMDD');
|
||||
return studyDate.isBetween(
|
||||
searchData['studyDateFrom'],
|
||||
searchData['studyDateTo'],
|
||||
'days',
|
||||
'[]'
|
||||
);
|
||||
} else if (searchData[key] && !study[key].includes(searchData[key])) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const { field, order } = searchData.sortData;
|
||||
|
||||
// just a example of local filtering
|
||||
let filteredStudies = this.defaultStudies
|
||||
.filter(function(study) {
|
||||
const all = [
|
||||
'patientName',
|
||||
'patientId',
|
||||
'accessionNumber',
|
||||
'modalities',
|
||||
'studyDescription',
|
||||
'studyDateFrom',
|
||||
].every(key => {
|
||||
return filter(key, searchData, study);
|
||||
});
|
||||
|
||||
return all;
|
||||
})
|
||||
.sort(function(a, b) {
|
||||
if (order === 'desc') {
|
||||
if (a[field] < b[field]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[field] > b[field]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
if (a[field] > b[field]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[field] < b[field]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// User can notice the loading icon
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
const first = searchData.currentPage * searchData.rowsPerPage;
|
||||
let last =
|
||||
searchData.currentPage * searchData.rowsPerPage +
|
||||
searchData.rowsPerPage;
|
||||
last = last >= filteredStudies.length ? filteredStudies.length : last;
|
||||
this.setState({ studies: filteredStudies.slice(first, last) });
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import moment from 'moment';
|
||||
|
||||
const studies = [
|
||||
{
|
||||
studyInstanceUid: '11111.111111.111111.11111',
|
||||
patientName: 'John Doe',
|
||||
patientId: '1',
|
||||
accessionNumber: '1234567',
|
||||
studyDate: '19930201',
|
||||
modalities: 'MR',
|
||||
studyDescription: 'BRAIN',
|
||||
},
|
||||
{
|
||||
studyInstanceUid: '2222.222222.22222.22222',
|
||||
patientName: 'José Silva',
|
||||
patientId: '2',
|
||||
accessionNumber: '7654321',
|
||||
studyDate: moment().format('YYYYMMDD'),
|
||||
modalities: 'CT',
|
||||
studyDescription: 'PET CT STANDARD',
|
||||
},
|
||||
{
|
||||
studyInstanceUid: '3333.333333.33333.33333',
|
||||
patientName: 'Antônio Jefferson',
|
||||
patientId: '3',
|
||||
accessionNumber: '732311',
|
||||
studyDate: moment()
|
||||
.subtract(14, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
studyDescription: '0',
|
||||
},
|
||||
{
|
||||
studyInstanceUid: '444444.44444.44444.4444',
|
||||
patientName: 'Antonio da Silva',
|
||||
patientId: '4',
|
||||
accessionNumber: '732311',
|
||||
studyDate: moment()
|
||||
.subtract(1, 'months')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
studyDescription: '0',
|
||||
},
|
||||
{
|
||||
studyInstanceUid: '55555.55555.55555.55555',
|
||||
patientName: 'Bezerra Souza',
|
||||
patientId: '5',
|
||||
accessionNumber: '5134543',
|
||||
studyDate: moment()
|
||||
.subtract(6, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
studyDescription: '0',
|
||||
},
|
||||
{
|
||||
studyInstanceUid: '66666.66666.66666.6666',
|
||||
patientName: 'Geraldo Roger',
|
||||
patientId: '6',
|
||||
accessionNumber: '5315135',
|
||||
studyDate: moment()
|
||||
.subtract(7, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
studyDescription: 'US',
|
||||
},
|
||||
{
|
||||
studyInstanceUid: '77777.77777.77777.77777',
|
||||
patientName: '',
|
||||
patientId: '7',
|
||||
accessionNumber: '5315136',
|
||||
studyDate: moment()
|
||||
.subtract(5, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
studyDescription: 'US',
|
||||
},
|
||||
].sort(function(a, b) {
|
||||
if (a.patientName < b.patientName) {
|
||||
return -1;
|
||||
}
|
||||
if (a.patientName > b.patientName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
export default studies;
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: Study List
|
||||
menu: Components
|
||||
route: /components/study-list
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { StudyList } from './../index.js'
|
||||
// Data
|
||||
import defaultStudies from './studies.js'
|
||||
import onSearch from './onSearch.js'
|
||||
import moment from 'moment'
|
||||
|
||||
# Study List
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
searchData: {},
|
||||
studies: defaultStudies.filter(study => {
|
||||
const studyDate = moment(study['studyDate'], 'YYYYMMDD');
|
||||
const startDate = moment().subtract(5, 'days');
|
||||
const endDate = moment();
|
||||
|
||||
return studyDate.isBetween(startDate, endDate, 'days', '[]');
|
||||
}).slice(0, 5)
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<div className="row" style={{ backgroundColor: '#000', height: '600px' }}>
|
||||
<div className="col-xs-12" style={{ padding: 0 }}>
|
||||
<StudyList
|
||||
studies={state.studies}
|
||||
pageOptions={[1, 2, 3, 5, 10, 15, 20, 25, 50, 100]}
|
||||
studyListFunctionsEnabled={true}
|
||||
onImport={(e) => alert('Import study mock ' + e)}
|
||||
onSelectItem={(studyInstanceUid) => { alert(studyInstanceUid + ' has selected! Now you can open your study.'); }}
|
||||
rowsPerPage={5}
|
||||
defaultSort={{ field: 'patientName', order: 'desc', }}
|
||||
studyListDateFilterNumDays={7}
|
||||
onSearch={onSearch.bind({ setState, defaultStudies })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={StudyList} />
|
||||
@@ -0,0 +1 @@
|
||||
export { StudyList } from './StudyList.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './TableList.styl';
|
||||
|
||||
export class TableList extends Component {
|
||||
static propTypes = {
|
||||
customHeader: PropTypes.node,
|
||||
defaultItems: PropTypes.object,
|
||||
children: PropTypes.node.isRequired,
|
||||
headerTitle: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="tableList">
|
||||
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
||||
{this.getHeader()}
|
||||
</div>
|
||||
<div className="tableListContent">{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getHeader = () => {
|
||||
if (this.props.customHeader) {
|
||||
return this.props.customHeader;
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="tableListHeaderTitle">{this.props.headerTitle}</div>
|
||||
<div className="numberOfItems">{this.props.children.length}</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
$headerRowHeight = 63px
|
||||
|
||||
.tableList
|
||||
background-color: var(--primary-background-color)
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.tableListHeader
|
||||
background-color: var(--ui-gray-darker)
|
||||
color: var(--text-secondary-color)
|
||||
display: flex
|
||||
height: $headerRowHeight
|
||||
line-height: $headerRowHeight
|
||||
margin-top: 2px
|
||||
overflow: hidden
|
||||
width: 100%
|
||||
|
||||
.tableListHeaderTitle
|
||||
color: var(--text-secondary-color)
|
||||
font-size: 22px
|
||||
font-weight: 300
|
||||
line-height: $headerRowHeight
|
||||
padding: 0 10px
|
||||
text-align: left
|
||||
flex: 1;
|
||||
|
||||
.numberOfItems
|
||||
color: var(--ui-sky-blue)
|
||||
float: right
|
||||
font-weight: 300
|
||||
font-size: 40px
|
||||
max-width: 54px
|
||||
height: $headerRowHeight
|
||||
line-height: 66px
|
||||
flex: 1;
|
||||
@@ -0,0 +1,42 @@
|
||||
import './TableListItem.styl';
|
||||
|
||||
import { Component } from 'react';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export class TableListItem extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
itemClass: PropTypes.string,
|
||||
itemIndex: PropTypes.number,
|
||||
itemKey: PropTypes.oneOfType(['number', 'string']),
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={`tableListItem ${this.props.itemClass}`}
|
||||
onClick={this.onItemClick}
|
||||
>
|
||||
<div className="itemIndex">
|
||||
{this.props.itemIndex}
|
||||
<span className="warning-icon">
|
||||
<Icon name="exclamation-triangle" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="itemContent">{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onItemClick = event => {
|
||||
if (this.props.onItemClick) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.onItemClick(event, this.props.itemKey);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
.tableListItem
|
||||
display: flex
|
||||
margin-left: -6px
|
||||
margin-top: 2px
|
||||
padding-left: 6px
|
||||
opacity: 0.7
|
||||
cursor: pointer;
|
||||
width: calc(100% + 6px)
|
||||
|
||||
&:hover
|
||||
opacity 1
|
||||
|
||||
&.selected
|
||||
opacity 1
|
||||
.itemIndex
|
||||
color: $activeColor
|
||||
|
||||
&.hasWarning
|
||||
.warning
|
||||
display: block
|
||||
|
||||
.itemIndex
|
||||
background-color: var(--ui-gray)
|
||||
color: var(--text-secondary-color)
|
||||
cursor: pointer
|
||||
flex: 1
|
||||
max-width: 25px
|
||||
transition(all 0.3s ease)
|
||||
font-size: 14px
|
||||
font-weight: 400
|
||||
margin-right: 5px
|
||||
padding: 10px
|
||||
text-align: center
|
||||
-webkit-box-sizing: initial;
|
||||
-moz-box-sizing: initial;
|
||||
box-sizing: initial;
|
||||
|
||||
.itemContent
|
||||
flex: 1
|
||||
color: var(--text-secondary-color)
|
||||
font-weight: 400
|
||||
margin-left: 9px;
|
||||
margin-top: 9px;
|
||||
|
||||
.warning-icon
|
||||
display: none
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: Table List
|
||||
menu: Components
|
||||
route: /components/table-list
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { TableList, TableListItem } from './../index.js'
|
||||
|
||||
# Table List
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
selectedIndex: null,
|
||||
listItems: [
|
||||
{ label: 'Brain Left' },
|
||||
{ label: 'Brain Right' },
|
||||
{ label: 'Heart Left' },
|
||||
{ label: 'Heart Right' }
|
||||
]
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<TableList headerTitle='Example Table List Header'>
|
||||
{state.listItems.map( (item, index) => {
|
||||
return (
|
||||
<TableListItem
|
||||
key={`item_${index}`}
|
||||
itemClass={state.selectedIndex === index ? 'selected' : ''}
|
||||
itemIndex={index}
|
||||
onItemClick={() => alert('item clicked')}
|
||||
>
|
||||
<label>{item.label}</label>
|
||||
</TableListItem>
|
||||
)}
|
||||
)}
|
||||
</TableList>
|
||||
)}
|
||||
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={TableList} />
|
||||
@@ -0,0 +1,2 @@
|
||||
export { TableList } from './TableList';
|
||||
export { TableListItem } from './TableListItem';
|
||||
@@ -0,0 +1,65 @@
|
||||
import './ToolbarSection.styl';
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import ExpandableToolMenu from '../../viewer/ExpandableToolMenu';
|
||||
import PropTypes from 'prop-types';
|
||||
import ToolbarButton from '../../viewer/ToolbarButton';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class ToolbarSection extends PureComponent {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
buttons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
icon: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
]),
|
||||
/** Optional: Expandable Tool Menu */
|
||||
buttons: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
})
|
||||
).isRequired,
|
||||
/** Array of string button ids that should show as active */
|
||||
activeButtons: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
/** Class for toolbar section container */
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const items = this.props.buttons.map((button, index) => {
|
||||
if (button.buttons && Array.isArray(button.buttons)) {
|
||||
return (
|
||||
<ExpandableToolMenu
|
||||
key={`expandable-${index}`}
|
||||
{...button}
|
||||
activeCommand={button.activeButton}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={index}
|
||||
{...button}
|
||||
isActive={this.props.activeButtons.includes(button.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classnames('ToolbarSection', this.props.className)}>
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ToolbarSection };
|
||||
@@ -0,0 +1,4 @@
|
||||
.ToolbarSection
|
||||
float: left;
|
||||
background-color: var(--primary-background-color);
|
||||
border-radius: 8px;
|
||||
@@ -0,0 +1,41 @@
|
||||
export default [
|
||||
{
|
||||
id: 'Pan',
|
||||
label: 'Pan',
|
||||
icon: 'arrows',
|
||||
},
|
||||
{
|
||||
label: 'Zoom',
|
||||
icon: 'search',
|
||||
},
|
||||
{
|
||||
label: 'Bidirectional',
|
||||
icon: 'measure-target',
|
||||
},
|
||||
{
|
||||
label: 'Stack Scroll',
|
||||
icon: 'bars',
|
||||
},
|
||||
{
|
||||
label: 'Reset',
|
||||
icon: 'reset',
|
||||
},
|
||||
{
|
||||
label: 'Manual',
|
||||
icon: 'level',
|
||||
},
|
||||
{
|
||||
label: 'More',
|
||||
icon: 'ellipse-circle',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Reset 2',
|
||||
icon: 'reset',
|
||||
},
|
||||
{
|
||||
label: 'Manual 2',
|
||||
icon: 'adjust',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Loaded 100 of 919 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user