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:
parent
ec7cdba901
commit
9a03bde0ae
@ -37,6 +37,12 @@ const _generateReport = (
|
||||
// Add in top level series 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;
|
||||
};
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import commandsModule from './commandsModule';
|
||||
import init from './init';
|
||||
import { id } from './id.js';
|
||||
import toolNames from './tools/toolNames';
|
||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@ -74,3 +75,4 @@ const dicomSRExtension = {
|
||||
};
|
||||
|
||||
export default dicomSRExtension;
|
||||
export { hydrateStructuredReport };
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { utilities, metaData } from '@cornerstonejs/core';
|
||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
|
||||
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
|
||||
import { adapters } from 'dcmjs';
|
||||
|
||||
const { guid } = OHIF.utils;
|
||||
@ -12,9 +12,10 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
/**
|
||||
* Hydrates a structured report, for default viewports.
|
||||
*
|
||||
*/
|
||||
export default function _hydrateStructuredReport(
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
@ -250,7 +251,7 @@ function _mapLegacyDataSet(dataset) {
|
||||
return dataset;
|
||||
}
|
||||
|
||||
const toArray = function(x) {
|
||||
const toArray = function (x) {
|
||||
return Array.isArray(x) ? x : [x];
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
Icon,
|
||||
} from '@ohif/ui';
|
||||
import classNames from 'classnames';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
|
||||
const { formatDate } = utils;
|
||||
|
||||
@ -37,6 +38,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
const {
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
MeasurementService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// SR viewport will always have a single display set
|
||||
@ -69,18 +71,25 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
MEASUREMENT_TRACKING_EXTENSION_ID
|
||||
);
|
||||
|
||||
// TODO: this is a hook that fails if we register/de-register
|
||||
if (hasMeasurementTrackingExtension) {
|
||||
const contextModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
|
||||
);
|
||||
|
||||
const useTrackedMeasurements = () => useContext(contextModule.context);
|
||||
|
||||
[
|
||||
trackedMeasurements,
|
||||
sendTrackedMeasurementsEvent,
|
||||
] = useTrackedMeasurements();
|
||||
const tracked = useContext(contextModule.context);
|
||||
trackedMeasurements = tracked?.[0];
|
||||
sendTrackedMeasurementsEvent = tracked?.[1];
|
||||
}
|
||||
if (!sendTrackedMeasurementsEvent) {
|
||||
// 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,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription,
|
||||
seriesDescription: SeriesDescription || '',
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
|
||||
@ -23,6 +23,11 @@ export type ViewportOptions = {
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
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 = {
|
||||
@ -34,6 +39,7 @@ export type PublicViewportOptions = {
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
customViewportProps?: Record<string, unknown>;
|
||||
allowUnmatchedView?: boolean;
|
||||
};
|
||||
|
||||
export type PublicDisplaySetOptions = {
|
||||
|
||||
73
extensions/default/src/Actions/createReportAsync.tsx
Normal file
73
extensions/default/src/Actions/createReportAsync.tsx
Normal 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;
|
||||
@ -13,16 +13,10 @@ function ActionButtons({ onExportClick, onCreateReportClick }) {
|
||||
<Button className="px-2 py-2 text-base" onClick={onExportClick}>
|
||||
{t('Export CSV')}
|
||||
</Button>
|
||||
<Button className="px-2 py-2 text-base" onClick={onCreateReportClick}>
|
||||
{t('Create Report')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{/* <Button
|
||||
className="ml-2 text-base"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
color="black"
|
||||
onClick={onCreateReportClick}
|
||||
>
|
||||
{t('Create Report')}
|
||||
</Button> */}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,15 +5,27 @@ import ActionButtons from './ActionButtons';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
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;
|
||||
|
||||
export default function PanelMeasurementTable({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const { MeasurementService, UIDialogService } = servicesManager.services;
|
||||
const { activeViewportIndex, viewports } = viewportGrid;
|
||||
const {
|
||||
MeasurementService,
|
||||
UIDialogService,
|
||||
UINotificationService,
|
||||
DisplaySetService,
|
||||
} = servicesManager.services;
|
||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -56,6 +68,62 @@ export default function PanelMeasurementTable({
|
||||
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 }) => {
|
||||
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid);
|
||||
|
||||
@ -142,7 +210,7 @@ export default function PanelMeasurementTable({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
|
||||
className="overflow-x-hidden overflow-y-auto ohif-scrollbar"
|
||||
data-cy={'measurements-panel'}
|
||||
>
|
||||
<MeasurementTable
|
||||
@ -155,7 +223,8 @@ export default function PanelMeasurementTable({
|
||||
<div className="flex justify-center p-4">
|
||||
<ActionButtons
|
||||
onExportClick={exportReport}
|
||||
onCreateReportClick={() => {}}
|
||||
onClearMeasurementsClick={clearMeasurements}
|
||||
onCreateReportClick={createReport}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
143
extensions/default/src/Panels/createReportDialogPrompt.tsx
Normal file
143
extensions/default/src/Panels/createReportDialogPrompt.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -14,7 +14,16 @@ const defaultProtocol = {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// 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: [],
|
||||
},
|
||||
},
|
||||
@ -40,7 +49,6 @@ const defaultProtocol = {
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
options: [],
|
||||
id: 'defaultDisplaySetId',
|
||||
},
|
||||
],
|
||||
|
||||
@ -16,6 +16,7 @@ function getPanelModule({
|
||||
<PanelMeasurementTable
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
10
extensions/default/src/utils/getNextSRSeriesNumber.js
Normal file
10
extensions/default/src/utils/getNextSRSeriesNumber.js
Normal 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;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import hydrateStructuredReport from './_hydrateStructuredReport.js';
|
||||
import hydrateStructuredReport from '@ohif/extension-cornerstone-dicom-sr';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
|
||||
@ -23,6 +23,7 @@ export default class ExtensionManager {
|
||||
});
|
||||
this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} };
|
||||
this.dataSourceMap = {};
|
||||
this.dataSourceDefs = {};
|
||||
this.defaultDataSourceName = appConfig.defaultDataSourceName;
|
||||
this.activeDataSource = undefined;
|
||||
}
|
||||
@ -298,6 +299,9 @@ export default class ExtensionManager {
|
||||
|
||||
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
|
||||
const { UserAuthenticationService } = this._servicesManager.services;
|
||||
dataSources.forEach(dataSource => {
|
||||
this.dataSourceDefs[dataSource.sourceName] = dataSource;
|
||||
});
|
||||
|
||||
extensionModule.forEach(element => {
|
||||
const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`;
|
||||
|
||||
@ -467,6 +467,12 @@ class HangingProtocolService {
|
||||
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
|
||||
// currently, which means we need to check if the requested updated displaySet
|
||||
// follow the same rules as the current displaySets
|
||||
@ -1098,10 +1104,9 @@ class HangingProtocolService {
|
||||
) {
|
||||
const { seriesMatchingRules } = displaySetSelector;
|
||||
|
||||
if (seriesMatchingRules.length) {
|
||||
// only match the required rules
|
||||
const requiredRules = seriesMatchingRules.filter(rule => rule.required);
|
||||
|
||||
// only match the required rules
|
||||
const requiredRules = seriesMatchingRules.filter(rule => rule.required);
|
||||
if (requiredRules.length) {
|
||||
const matched = this.protocolEngine.findMatch(displaySet, requiredRules);
|
||||
|
||||
if (!matched || matched.score === 0) {
|
||||
|
||||
@ -57,19 +57,18 @@ validate.validators.endsWith = function(value, options, key) {
|
||||
}
|
||||
};
|
||||
|
||||
const getTestValue = options => options?.value ?? options;
|
||||
|
||||
validate.validators.greaterThan = function(value, options, key) {
|
||||
const testValue = options?.value ?? options;
|
||||
if (testValue !== undefined && value <= testValue) {
|
||||
const testValue = getTestValue(options);
|
||||
if (value === undefined || value === null || value <= testValue) {
|
||||
return key + 'with value ' + value + ' must be greater than ' + testValue;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.range = function(value, options, key) {
|
||||
const testValue = options?.value ?? options;
|
||||
if (
|
||||
(testValue !== undefined && value < testValue[0]) ||
|
||||
value > testValue[1]
|
||||
) {
|
||||
const testValue = getTestValue(options);
|
||||
if (value === undefined || value < testValue[0] || value > testValue[1]) {
|
||||
return (
|
||||
key +
|
||||
'with value ' +
|
||||
|
||||
@ -1,49 +1,107 @@
|
||||
import validate from "./validator.js";
|
||||
import validate from './validator.js';
|
||||
|
||||
describe("validator", () => {
|
||||
describe('validator', () => {
|
||||
const attributeMap = {
|
||||
str: "string",
|
||||
str: 'string',
|
||||
num: 3,
|
||||
nullValue: null,
|
||||
list: ["abc", "def"],
|
||||
}
|
||||
list: ['abc', 'def'],
|
||||
};
|
||||
|
||||
const options = {
|
||||
format: 'grouped',
|
||||
};
|
||||
|
||||
describe("contains", () => {
|
||||
it("returns match any list contains", () => {
|
||||
expect(validate(attributeMap, { list: { contains: 'a' } }, [options])).toBeUndefined();
|
||||
expect(validate(attributeMap, { str: { contains: 'i' } }, [options])).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('contains', () => {
|
||||
it('returns match any list contains', () => {
|
||||
expect(
|
||||
validate(attributeMap, { list: { contains: 'a' } }, [options])
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
validate(attributeMap, { str: { contains: 'i' } }, [options])
|
||||
).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", () => {
|
||||
it("returned undefined on equals", () => {
|
||||
expect(validate(attributeMap, { str: { equals: attributeMap.str } }, [options])).toBeUndefined();
|
||||
expect(validate(attributeMap, { num: { equals: { value: attributeMap.num } } }, [options])).toBeUndefined();
|
||||
})
|
||||
describe('equals', () => {
|
||||
it('returned undefined on equals', () => {
|
||||
expect(
|
||||
validate(attributeMap, { str: { equals: attributeMap.str } }, [options])
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
validate(
|
||||
attributeMap,
|
||||
{ num: { equals: { value: attributeMap.num } } },
|
||||
[options]
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns error on not equals", () => {
|
||||
expect(validate(attributeMap, { str: { equals: "abc" } }, [options])).not.toBeUndefined();
|
||||
expect(validate(attributeMap, { num: { equals: { value: 1 + attributeMap.num } } }, [options])).not.toBeUndefined();
|
||||
})
|
||||
})
|
||||
it('returns error on not equals', () => {
|
||||
expect(
|
||||
validate(attributeMap, { str: { equals: 'abc' } }, [options])
|
||||
).not.toBeUndefined();
|
||||
expect(
|
||||
validate(
|
||||
attributeMap,
|
||||
{ num: { equals: { value: 1 + attributeMap.num } } },
|
||||
[options]
|
||||
)
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("greaterThan", () => {
|
||||
it("returns undefined on greaterThan", () => {
|
||||
expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num - 1 } } }, [options])).toBeUndefined();
|
||||
expect(validate(attributeMap, { num: { greaterThan: attributeMap.num - 1 } }, [options])).toBeUndefined();
|
||||
})
|
||||
describe('greaterThan', () => {
|
||||
it('returns undefined on greaterThan', () => {
|
||||
expect(
|
||||
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", () => {
|
||||
expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num } } }, [options])).not.toBeUndefined();
|
||||
expect(validate(attributeMap, { num: { greaterThan: attributeMap.num } }, [options])).not.toBeUndefined();
|
||||
})
|
||||
})
|
||||
it('returns error on not greater than', () => {
|
||||
expect(
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -243,6 +243,7 @@ Button.propTypes = {
|
||||
'light',
|
||||
'default',
|
||||
'primary',
|
||||
'primaryActive',
|
||||
'secondary',
|
||||
'white',
|
||||
'black',
|
||||
|
||||
@ -130,6 +130,7 @@ Typography.propTypes = {
|
||||
'initial',
|
||||
'inherit',
|
||||
'primary',
|
||||
'primaryActive',
|
||||
'secondary',
|
||||
'error',
|
||||
]),
|
||||
|
||||
@ -26,7 +26,8 @@ window.config = {
|
||||
qidoRoot: '/dicomweb',
|
||||
wadoRoot: '/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
supportsReject: true,
|
||||
supportsStow: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
@ -46,6 +47,7 @@ window.config = {
|
||||
wadoRoot: 'https://viewer.flexview.ai/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
supportsStow: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
@ -66,6 +68,7 @@ window.config = {
|
||||
wadoRoot: '/viewer-testdata',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
supportsStow: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user