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:
Danny Brown authored and GitHub committed 2019-08-13 15:36:46 -04:00
1 parent cac911f08e
commit 08ff214ea6
919 files changed
+45652 -52097

No files matched your search

@@ -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';