Fix/sr hydration non tracking (#3080)

* fix:DICOM SR hydration - remove redundancies

feat:Allow save and restore on non-tracking view

* PR comments
This commit is contained in:
Bill Wallace 2022-12-29 10:13:06 -05:00 committed by GitHub
parent ec7cdba901
commit 9a03bde0ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 466 additions and 73 deletions

View File

@ -37,6 +37,12 @@ const _generateReport = (
// Add in top level series options // Add in top level series options
Object.assign(dataset, options); Object.assign(dataset, options);
// Set the default character set as UTF-8
// https://dicom.innolitics.com/ciods/nm-image/sop-common/00080005
if (typeof dataset.SpecificCharacterSet === 'undefined') {
dataset.SpecificCharacterSet = 'ISO_IR 192';
}
return dataset; return dataset;
}; };

View File

@ -5,6 +5,7 @@ import commandsModule from './commandsModule';
import init from './init'; import init from './init';
import { id } from './id.js'; import { id } from './id.js';
import toolNames from './tools/toolNames'; import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import( return import(
@ -74,3 +75,4 @@ const dicomSRExtension = {
}; };
export default dicomSRExtension; export default dicomSRExtension;
export { hydrateStructuredReport };

View File

@ -1,6 +1,6 @@
import { utilities, metaData } from '@cornerstonejs/core'; import { utilities, metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core'; import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData'; import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adapters } from 'dcmjs'; import { adapters } from 'dcmjs';
const { guid } = OHIF.utils; const { guid } = OHIF.utils;
@ -12,9 +12,10 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
/** /**
* Hydrates a structured report, for default viewports.
* *
*/ */
export default function _hydrateStructuredReport( export default function hydrateStructuredReport(
{ servicesManager, extensionManager }, { servicesManager, extensionManager },
displaySetInstanceUID displaySetInstanceUID
) { ) {
@ -250,7 +251,7 @@ function _mapLegacyDataSet(dataset) {
return dataset; return dataset;
} }
const toArray = function(x) { const toArray = function (x) {
return Array.isArray(x) ? x : [x]; return Array.isArray(x) ? x : [x];
}; };

View File

@ -13,6 +13,7 @@ import {
Icon, Icon,
} from '@ohif/ui'; } from '@ohif/ui';
import classNames from 'classnames'; import classNames from 'classnames';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
const { formatDate } = utils; const { formatDate } = utils;
@ -37,6 +38,7 @@ function OHIFCornerstoneSRViewport(props) {
const { const {
DisplaySetService, DisplaySetService,
CornerstoneViewportService, CornerstoneViewportService,
MeasurementService,
} = servicesManager.services; } = servicesManager.services;
// SR viewport will always have a single display set // SR viewport will always have a single display set
@ -69,18 +71,25 @@ function OHIFCornerstoneSRViewport(props) {
MEASUREMENT_TRACKING_EXTENSION_ID MEASUREMENT_TRACKING_EXTENSION_ID
); );
// TODO: this is a hook that fails if we register/de-register
if (hasMeasurementTrackingExtension) { if (hasMeasurementTrackingExtension) {
const contextModule = extensionManager.getModuleEntry( const contextModule = extensionManager.getModuleEntry(
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext' '@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
); );
const useTrackedMeasurements = () => useContext(contextModule.context); const tracked = useContext(contextModule.context);
trackedMeasurements = tracked?.[0];
[ sendTrackedMeasurementsEvent = tracked?.[1];
trackedMeasurements, }
sendTrackedMeasurementsEvent, if (!sendTrackedMeasurementsEvent) {
] = useTrackedMeasurements(); // if no panels from measurement-tracking extension is used, this code will trun
trackedMeasurements = null;
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
MeasurementService.clearMeasurements();
hydrateStructuredReport(
{ servicesManager, extensionManager },
displaySetInstanceUID
);
};
} }
/** /**
@ -362,7 +371,7 @@ function OHIFCornerstoneSRViewport(props) {
useAltStyling: true, useAltStyling: true,
studyDate: formatDate(StudyDate), studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber, currentSeries: SeriesNumber,
seriesDescription: SeriesDescription, seriesDescription: SeriesDescription || '',
patientInformation: { patientInformation: {
patientName: PatientName patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic) ? OHIF.utils.formatPN(PatientName.Alphabetic)

View File

@ -23,6 +23,11 @@ export type ViewportOptions = {
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
customViewportProps?: Record<string, unknown>; customViewportProps?: Record<string, unknown>;
/*
* Allows drag and drop of display sets not matching viewport options, but
* doesn't show them initially. Displays initially blank if no required match
*/
allowUnmatchedView?: boolean;
}; };
export type PublicViewportOptions = { export type PublicViewportOptions = {
@ -34,6 +39,7 @@ export type PublicViewportOptions = {
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
customViewportProps?: Record<string, unknown>; customViewportProps?: Record<string, unknown>;
allowUnmatchedView?: boolean;
}; };
export type PublicDisplaySetOptions = { export type PublicDisplaySetOptions = {

View File

@ -0,0 +1,73 @@
import React from 'react';
import { DicomMetadataStore } from '@ohif/core';
/**
*
* @param {*} servicesManager
* @param {*} dataSource
* @param {*} measurements
* @param {*} options
* @returns {string[]} displaySetInstanceUIDs
*/
async function createReportAsync(
servicesManager,
commandsManager,
dataSource,
measurements,
options
) {
const {
DisplaySetService,
UINotificationService,
UIDialogService,
} = servicesManager.services;
const loadingDialogId = UIDialogService.create({
showOverlay: true,
isDraggable: false,
centralize: true,
// TODO: Create a loading indicator component + zeplin design?
content: Loading,
});
try {
const naturalizedReport = await commandsManager.runCommand(
'storeMeasurements',
{
measurementData: measurements,
dataSource,
additionalFindingTypes: ['ArrowAnnotate'],
options,
},
'CORNERSTONE_STRUCTURED_REPORT'
);
// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
DicomMetadataStore.addInstances([naturalizedReport], true);
const displaySetInstanceUID = DisplaySetService.getMostRecentDisplaySet();
UINotificationService.show({
title: 'Create Report',
message: 'Measurements saved successfully',
type: 'success',
});
return [displaySetInstanceUID];
} catch (error) {
UINotificationService.show({
title: 'Create Report',
message: error.message || 'Failed to store measurements',
type: 'error',
});
} finally {
UIDialogService.dismiss({ id: loadingDialogId });
}
}
function Loading() {
return <div className="text-primary-active">Loading...</div>;
}
export default createReportAsync;

View File

@ -13,16 +13,10 @@ function ActionButtons({ onExportClick, onCreateReportClick }) {
<Button className="px-2 py-2 text-base" onClick={onExportClick}> <Button className="px-2 py-2 text-base" onClick={onExportClick}>
{t('Export CSV')} {t('Export CSV')}
</Button> </Button>
</ButtonGroup> <Button className="px-2 py-2 text-base" onClick={onCreateReportClick}>
{/* <Button
className="ml-2 text-base"
variant="outlined"
size="small"
color="black"
onClick={onCreateReportClick}
>
{t('Create Report')} {t('Create Report')}
</Button> */} </Button>
</ButtonGroup>
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -5,15 +5,27 @@ import ActionButtons from './ActionButtons';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import createReportDialogPrompt, {
CREATE_REPORT_DIALOG_RESPONSE,
} from './createReportDialogPrompt';
import createReportAsync from '../Actions/createReportAsync';
import getNextSRSeriesNumber from '../utils/getNextSRSeriesNumber';
const { downloadCSVReport } = utils; const { downloadCSVReport } = utils;
export default function PanelMeasurementTable({ export default function PanelMeasurementTable({
servicesManager, servicesManager,
commandsManager, commandsManager,
extensionManager,
}) { }) {
const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportGrid, viewportGridService] = useViewportGrid();
const { MeasurementService, UIDialogService } = servicesManager.services; const { activeViewportIndex, viewports } = viewportGrid;
const {
MeasurementService,
UIDialogService,
UINotificationService,
DisplaySetService,
} = servicesManager.services;
const [displayMeasurements, setDisplayMeasurements] = useState([]); const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => { useEffect(() => {
@ -56,6 +68,62 @@ export default function PanelMeasurementTable({
downloadCSVReport(measurements, MeasurementService); downloadCSVReport(measurements, MeasurementService);
} }
async function clearMeasurements() {
MeasurementService.clearMeasurements();
}
async function createReport() {
// filter measurements that are added to the active study
const activeViewport = viewports[activeViewportIndex];
const measurements = MeasurementService.getMeasurements();
const displaySet = DisplaySetService.getDisplaySetByUID(
activeViewport.displaySetInstanceUIDs[0]
);
const trackedMeasurements = measurements.filter(
m => displaySet.StudyInstanceUID === m.referenceStudyUID
);
if (trackedMeasurements.length <= 0) {
UINotificationService.show({
title: 'No Measurements',
message: 'No Measurements are added to the current Study.',
type: 'info',
duration: 3000,
});
return;
}
const promptResult = await createReportDialogPrompt(UIDialogService, {
extensionManager,
});
if (promptResult.action === CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT) {
const dataSources = extensionManager.getDataSources(
promptResult.dataSourceName
);
const dataSource = dataSources[0];
const SeriesDescription =
// isUndefinedOrEmpty
promptResult.value === undefined || promptResult.value === ''
? 'Research Derived Series' // default
: promptResult.value; // provided value
const SeriesNumber = getNextSRSeriesNumber(DisplaySetService);
const displaySetInstanceUIDs = await createReportAsync(
servicesManager,
commandsManager,
dataSource,
trackedMeasurements,
{
SeriesDescription,
SeriesNumber,
}
);
}
}
const jumpToImage = ({ uid, isActive }) => { const jumpToImage = ({ uid, isActive }) => {
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid); MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid);
@ -142,7 +210,7 @@ export default function PanelMeasurementTable({
return ( return (
<> <>
<div <div
className="overflow-x-hidden overflow-y-auto invisible-scrollbar" className="overflow-x-hidden overflow-y-auto ohif-scrollbar"
data-cy={'measurements-panel'} data-cy={'measurements-panel'}
> >
<MeasurementTable <MeasurementTable
@ -155,7 +223,8 @@ export default function PanelMeasurementTable({
<div className="flex justify-center p-4"> <div className="flex justify-center p-4">
<ActionButtons <ActionButtons
onExportClick={exportReport} onExportClick={exportReport}
onCreateReportClick={() => {}} onClearMeasurementsClick={clearMeasurements}
onCreateReportClick={createReport}
/> />
</div> </div>
</> </>

View File

@ -0,0 +1,143 @@
/* eslint-disable react/display-name */
import React from 'react';
import { Dialog, Input, Select } from '@ohif/ui';
export const CREATE_REPORT_DIALOG_RESPONSE = {
CANCEL: 0,
CREATE_REPORT: 1,
};
export default function createReportDialogPrompt(
UIDialogService,
{ extensionManager }
) {
return new Promise(function (resolve, reject) {
let dialogId = undefined;
const _handleClose = () => {
// Dismiss dialog
UIDialogService.dismiss({ id: dialogId });
// Notify of cancel action
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
value: undefined,
dataSourceName: undefined,
});
};
/**
*
* @param {string} param0.action - value of action performed
* @param {string} param0.value - value from input field
*/
const _handleFormSubmit = ({ action, value }) => {
UIDialogService.dismiss({ id: dialogId });
switch (action.id) {
case 'save':
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
value: value.label,
dataSourceName: value.dataSourceName,
});
break;
case 'cancel':
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
value: undefined,
dataSourceName: undefined,
});
break;
}
};
const dataSourcesOpts = Object.keys(extensionManager.dataSourceMap)
.filter(ds => {
const configuration =
extensionManager.dataSourceDefs[ds]?.configuration;
const supportsStow =
configuration?.supportsStow ?? configuration?.wadoRoot;
return supportsStow;
})
.map(ds => {
return {
value: ds,
label: ds,
placeHolder: ds,
};
});
dialogId = UIDialogService.create({
centralize: true,
isDraggable: false,
content: Dialog,
useLastPosition: false,
showOverlay: true,
contentProps: {
title: 'Provide a name for your report',
value: {
label: '',
dataSourceName: extensionManager.activeDataSource,
},
noCloseButton: true,
onClose: _handleClose,
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
],
// TODO: Should be on button press...
onSubmit: _handleFormSubmit,
body: ({ value, setValue }) => {
const onChangeHandler = event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
};
const onKeyPressHandler = event => {
if (event.key === 'Enter') {
UIDialogService.dismiss({ id: dialogId });
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
value: value.label,
});
}
};
return (
<>
<div className="p-4 bg-primary-dark">
{dataSourcesOpts.length > 1 && (
<Select
closeMenuOnSelect={true}
className="mr-2 bg-black border-primary-main"
options={dataSourcesOpts}
placeholder={
dataSourcesOpts.find(
option => option.value === value.dataSourceName
).placeHolder
}
value={value.dataSourceName}
onChange={evt => {
setValue(v => ({ ...v, dataSourceName: evt.value }));
}}
isClearable={false}
/>
)}
</div>
<div className="p-4 bg-primary-dark">
<Input
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
placeholder="Enter Report Name"
containerClassName="mr-2"
value={value.label}
onChange={onChangeHandler}
onKeyPress={onKeyPressHandler}
required
/>
</div>
</>
);
},
},
});
});
}

View File

@ -14,7 +14,16 @@ const defaultProtocol = {
// Unused currently // Unused currently
imageMatchingRules: [], imageMatchingRules: [],
// Matches displaysets, NOT series // Matches displaysets, NOT series
seriesMatchingRules: [], seriesMatchingRules: [
// Try to match series with images by default, to prevent weird display
// on SEG/SR containing studies
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
},
],
studyMatchingRules: [], studyMatchingRules: [],
}, },
}, },
@ -40,7 +49,6 @@ const defaultProtocol = {
}, },
displaySets: [ displaySets: [
{ {
options: [],
id: 'defaultDisplaySetId', id: 'defaultDisplaySetId',
}, },
], ],

View File

@ -16,6 +16,7 @@ function getPanelModule({
<PanelMeasurementTable <PanelMeasurementTable
commandsManager={commandsManager} commandsManager={commandsManager}
servicesManager={servicesManager} servicesManager={servicesManager}
extensionManager={extensionManager}
/> />
); );
}; };

View File

@ -0,0 +1,10 @@
const MIN_SR_SERIES_NUMBER = 4700;
export default function getNextSRSeriesNumber(DisplaySetService) {
const activeDisplaySets = DisplaySetService.getActiveDisplaySets();
const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR');
const srSeriesNumbers = srDisplaySets.map(ds => ds.SeriesNumber);
const maxSeriesNumber = Math.max(...srSeriesNumbers, MIN_SR_SERIES_NUMBER);
return maxSeriesNumber + 1;
}

View File

@ -1,4 +1,4 @@
import hydrateStructuredReport from './_hydrateStructuredReport.js'; import hydrateStructuredReport from '@ohif/extension-cornerstone-dicom-sr';
const RESPONSE = { const RESPONSE = {
NO_NEVER: -1, NO_NEVER: -1,

View File

@ -23,6 +23,7 @@ export default class ExtensionManager {
}); });
this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} }; this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} };
this.dataSourceMap = {}; this.dataSourceMap = {};
this.dataSourceDefs = {};
this.defaultDataSourceName = appConfig.defaultDataSourceName; this.defaultDataSourceName = appConfig.defaultDataSourceName;
this.activeDataSource = undefined; this.activeDataSource = undefined;
} }
@ -298,6 +299,9 @@ export default class ExtensionManager {
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) { _initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
const { UserAuthenticationService } = this._servicesManager.services; const { UserAuthenticationService } = this._servicesManager.services;
dataSources.forEach(dataSource => {
this.dataSourceDefs[dataSource.sourceName] = dataSource;
});
extensionModule.forEach(element => { extensionModule.forEach(element => {
const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`; const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`;

View File

@ -467,6 +467,12 @@ class HangingProtocolService {
return defaultReturn; return defaultReturn;
} }
// If the viewport options says to allow any instance, then we can assume
// it just updates this viewport
if (protocolViewport.viewportOptions.allowUnmatchedView) {
return defaultReturn;
}
// if the viewport is not empty, then we check the displaySets it is showing // if the viewport is not empty, then we check the displaySets it is showing
// currently, which means we need to check if the requested updated displaySet // currently, which means we need to check if the requested updated displaySet
// follow the same rules as the current displaySets // follow the same rules as the current displaySets
@ -1098,10 +1104,9 @@ class HangingProtocolService {
) { ) {
const { seriesMatchingRules } = displaySetSelector; const { seriesMatchingRules } = displaySetSelector;
if (seriesMatchingRules.length) {
// only match the required rules // only match the required rules
const requiredRules = seriesMatchingRules.filter(rule => rule.required); const requiredRules = seriesMatchingRules.filter(rule => rule.required);
if (requiredRules.length) {
const matched = this.protocolEngine.findMatch(displaySet, requiredRules); const matched = this.protocolEngine.findMatch(displaySet, requiredRules);
if (!matched || matched.score === 0) { if (!matched || matched.score === 0) {

View File

@ -57,19 +57,18 @@ validate.validators.endsWith = function(value, options, key) {
} }
}; };
const getTestValue = options => options?.value ?? options;
validate.validators.greaterThan = function(value, options, key) { validate.validators.greaterThan = function(value, options, key) {
const testValue = options?.value ?? options; const testValue = getTestValue(options);
if (testValue !== undefined && value <= testValue) { if (value === undefined || value === null || value <= testValue) {
return key + 'with value ' + value + ' must be greater than ' + testValue; return key + 'with value ' + value + ' must be greater than ' + testValue;
} }
}; };
validate.validators.range = function(value, options, key) { validate.validators.range = function(value, options, key) {
const testValue = options?.value ?? options; const testValue = getTestValue(options);
if ( if (value === undefined || value < testValue[0] || value > testValue[1]) {
(testValue !== undefined && value < testValue[0]) ||
value > testValue[1]
) {
return ( return (
key + key +
'with value ' + 'with value ' +

View File

@ -1,49 +1,107 @@
import validate from "./validator.js"; import validate from './validator.js';
describe("validator", () => { describe('validator', () => {
const attributeMap = { const attributeMap = {
str: "string", str: 'string',
num: 3, num: 3,
nullValue: null, nullValue: null,
list: ["abc", "def"], list: ['abc', 'def'],
} };
const options = { const options = {
format: 'grouped', format: 'grouped',
}; };
describe("contains", () => { describe('contains', () => {
it("returns match any list contains", () => { it('returns match any list contains', () => {
expect(validate(attributeMap, { list: { contains: 'a' } }, [options])).toBeUndefined(); expect(
expect(validate(attributeMap, { str: { contains: 'i' } }, [options])).toBeUndefined(); validate(attributeMap, { list: { contains: 'a' } }, [options])
expect(validate(attributeMap, { str: { contains: ['i'] } }, [options])).toBeUndefined(); ).toBeUndefined();
expect(validate(attributeMap, { list: { contains: ['a'] } }, [options])).toBeUndefined(); expect(
expect(validate(attributeMap, { list: { contains: ['z', 'd'] } }, [options])).toBeUndefined(); validate(attributeMap, { str: { contains: 'i' } }, [options])
expect(validate(attributeMap, { list: { contains: ['z'] } }, [options])).not.toBeUndefined(); ).toBeUndefined();
}) expect(
}) validate(attributeMap, { str: { contains: ['i'] } }, [options])
).toBeUndefined();
expect(
validate(attributeMap, { list: { contains: ['a'] } }, [options])
).toBeUndefined();
expect(
validate(attributeMap, { list: { contains: ['z', 'd'] } }, [options])
).toBeUndefined();
expect(
validate(attributeMap, { list: { contains: ['z'] } }, [options])
).not.toBeUndefined();
});
});
describe("equals", () => { describe('equals', () => {
it("returned undefined on equals", () => { it('returned undefined on equals', () => {
expect(validate(attributeMap, { str: { equals: attributeMap.str } }, [options])).toBeUndefined(); expect(
expect(validate(attributeMap, { num: { equals: { value: attributeMap.num } } }, [options])).toBeUndefined(); validate(attributeMap, { str: { equals: attributeMap.str } }, [options])
}) ).toBeUndefined();
expect(
validate(
attributeMap,
{ num: { equals: { value: attributeMap.num } } },
[options]
)
).toBeUndefined();
});
it("returns error on not equals", () => { it('returns error on not equals', () => {
expect(validate(attributeMap, { str: { equals: "abc" } }, [options])).not.toBeUndefined(); expect(
expect(validate(attributeMap, { num: { equals: { value: 1 + attributeMap.num } } }, [options])).not.toBeUndefined(); validate(attributeMap, { str: { equals: 'abc' } }, [options])
}) ).not.toBeUndefined();
}) expect(
validate(
attributeMap,
{ num: { equals: { value: 1 + attributeMap.num } } },
[options]
)
).not.toBeUndefined();
});
});
describe("greaterThan", () => { describe('greaterThan', () => {
it("returns undefined on greaterThan", () => { it('returns undefined on greaterThan', () => {
expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num - 1 } } }, [options])).toBeUndefined(); expect(
expect(validate(attributeMap, { num: { greaterThan: attributeMap.num - 1 } }, [options])).toBeUndefined(); validate(
}) attributeMap,
{ num: { greaterThan: { value: attributeMap.num - 1 } } },
[options]
)
).toBeUndefined();
expect(
validate(attributeMap, { num: { greaterThan: attributeMap.num - 1 } }, [
options,
])
).toBeUndefined();
});
it("returns error on not greater than", () => { it('returns error on not greater than', () => {
expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num } } }, [options])).not.toBeUndefined(); expect(
expect(validate(attributeMap, { num: { greaterThan: attributeMap.num } }, [options])).not.toBeUndefined(); validate(
}) attributeMap,
}) { num: { greaterThan: { value: attributeMap.num } } },
[options]
)
).not.toBeUndefined();
expect(
validate(attributeMap, { num: { greaterThan: attributeMap.num } }, [
options,
])
).not.toBeUndefined();
});
it('returns error on undefined value', () => {
expect(
validate(
attributeMap,
{ numUndefined: { greaterThan: { value: 3 } } },
[options]
)
).not.toBeUndefined();
});
});
}); });

View File

@ -243,6 +243,7 @@ Button.propTypes = {
'light', 'light',
'default', 'default',
'primary', 'primary',
'primaryActive',
'secondary', 'secondary',
'white', 'white',
'black', 'black',

View File

@ -130,6 +130,7 @@ Typography.propTypes = {
'initial', 'initial',
'inherit', 'inherit',
'primary', 'primary',
'primaryActive',
'secondary', 'secondary',
'error', 'error',
]), ]),

View File

@ -26,7 +26,8 @@ window.config = {
qidoRoot: '/dicomweb', qidoRoot: '/dicomweb',
wadoRoot: '/dicomweb', wadoRoot: '/dicomweb',
qidoSupportsIncludeField: false, qidoSupportsIncludeField: false,
supportsReject: false, supportsReject: true,
supportsStow: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true, enableStudyLazyLoad: true,
@ -46,6 +47,7 @@ window.config = {
wadoRoot: 'https://viewer.flexview.ai/dicomweb', wadoRoot: 'https://viewer.flexview.ai/dicomweb',
qidoSupportsIncludeField: false, qidoSupportsIncludeField: false,
supportsReject: false, supportsReject: false,
supportsStow: false,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true, enableStudyLazyLoad: true,
@ -66,6 +68,7 @@ window.config = {
wadoRoot: '/viewer-testdata', wadoRoot: '/viewer-testdata',
qidoSupportsIncludeField: false, qidoSupportsIncludeField: false,
supportsReject: false, supportsReject: false,
supportsStow: false,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true, enableStudyLazyLoad: true,