diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.js b/extensions/cornerstone-dicom-sr/src/commandsModule.js index 253583d15..d9c18cfc1 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.js +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.js @@ -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; }; diff --git a/extensions/cornerstone-dicom-sr/src/index.tsx b/extensions/cornerstone-dicom-sr/src/index.tsx index e53927d6d..bab79924a 100644 --- a/extensions/cornerstone-dicom-sr/src/index.tsx +++ b/extensions/cornerstone-dicom-sr/src/index.tsx @@ -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 }; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/utils/getLabelFromDCMJSImportedToolData.js b/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js similarity index 100% rename from extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/utils/getLabelFromDCMJSImportedToolData.js rename to extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js similarity index 97% rename from extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js rename to extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js index 2e123b018..0ca978028 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/_hydrateStructuredReport.js +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js @@ -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]; }; diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index 64ed151ef..bff4dba42 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -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) diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index ff1e75bfc..eae87ada0 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -23,6 +23,11 @@ export type ViewportOptions = { syncGroups?: SyncGroup[]; initialImageOptions?: InitialImageOptions; customViewportProps?: Record; + /* + * 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; + allowUnmatchedView?: boolean; }; export type PublicDisplaySetOptions = { diff --git a/extensions/default/src/Actions/createReportAsync.tsx b/extensions/default/src/Actions/createReportAsync.tsx new file mode 100644 index 000000000..f9641c801 --- /dev/null +++ b/extensions/default/src/Actions/createReportAsync.tsx @@ -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
Loading...
; +} + +export default createReportAsync; diff --git a/extensions/default/src/Panels/ActionButtons.tsx b/extensions/default/src/Panels/ActionButtons.tsx index 292bae684..f372a1ee0 100644 --- a/extensions/default/src/Panels/ActionButtons.tsx +++ b/extensions/default/src/Panels/ActionButtons.tsx @@ -13,16 +13,10 @@ function ActionButtons({ onExportClick, onCreateReportClick }) { + - {/* */} ); } diff --git a/extensions/default/src/Panels/PanelMeasurementTable.tsx b/extensions/default/src/Panels/PanelMeasurementTable.tsx index 78c6a905d..d213210d1 100644 --- a/extensions/default/src/Panels/PanelMeasurementTable.tsx +++ b/extensions/default/src/Panels/PanelMeasurementTable.tsx @@ -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 ( <>
{}} + onClearMeasurementsClick={clearMeasurements} + onCreateReportClick={createReport} />
diff --git a/extensions/default/src/Panels/createReportDialogPrompt.tsx b/extensions/default/src/Panels/createReportDialogPrompt.tsx new file mode 100644 index 000000000..d635b4354 --- /dev/null +++ b/extensions/default/src/Panels/createReportDialogPrompt.tsx @@ -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 ( + <> +
+ {dataSourcesOpts.length > 1 && ( + +
+ + ); + }, + }, + }); + }); +} diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index eac96d91c..cdcf242c3 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -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', }, ], diff --git a/extensions/default/src/getPanelModule.tsx b/extensions/default/src/getPanelModule.tsx index bfc66a36c..6f733e496 100644 --- a/extensions/default/src/getPanelModule.tsx +++ b/extensions/default/src/getPanelModule.tsx @@ -16,6 +16,7 @@ function getPanelModule({ ); }; diff --git a/extensions/default/src/utils/getNextSRSeriesNumber.js b/extensions/default/src/utils/getNextSRSeriesNumber.js new file mode 100644 index 000000000..d5865d38e --- /dev/null +++ b/extensions/default/src/utils/getNextSRSeriesNumber.js @@ -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; +} diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js index 089f05faa..37d7961e2 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js @@ -1,4 +1,4 @@ -import hydrateStructuredReport from './_hydrateStructuredReport.js'; +import hydrateStructuredReport from '@ohif/extension-cornerstone-dicom-sr'; const RESPONSE = { NO_NEVER: -1, diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index e72c72233..b8ea618e6 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -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}`; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 2a1a7230f..91b6c108a 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -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) { diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.js b/platform/core/src/services/HangingProtocolService/lib/validator.js index 8bdc06949..711ff01b9 100644 --- a/platform/core/src/services/HangingProtocolService/lib/validator.js +++ b/platform/core/src/services/HangingProtocolService/lib/validator.js @@ -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 ' + diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.test.js b/platform/core/src/services/HangingProtocolService/lib/validator.test.js index 4b20f8029..5566e97a7 100644 --- a/platform/core/src/services/HangingProtocolService/lib/validator.test.js +++ b/platform/core/src/services/HangingProtocolService/lib/validator.test.js @@ -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(); + }); + }); }); diff --git a/platform/ui/src/components/Button/Button.tsx b/platform/ui/src/components/Button/Button.tsx index 104db9368..fdafc91bb 100644 --- a/platform/ui/src/components/Button/Button.tsx +++ b/platform/ui/src/components/Button/Button.tsx @@ -243,6 +243,7 @@ Button.propTypes = { 'light', 'default', 'primary', + 'primaryActive', 'secondary', 'white', 'black', diff --git a/platform/ui/src/components/Typography/Typography.tsx b/platform/ui/src/components/Typography/Typography.tsx index ab18345f3..9f86c129e 100644 --- a/platform/ui/src/components/Typography/Typography.tsx +++ b/platform/ui/src/components/Typography/Typography.tsx @@ -130,6 +130,7 @@ Typography.propTypes = { 'initial', 'inherit', 'primary', + 'primaryActive', 'secondary', 'error', ]), diff --git a/platform/viewer/public/config/multiple.js b/platform/viewer/public/config/multiple.js index 92151f3a1..3a6f4d24d 100644 --- a/platform/viewer/public/config/multiple.js +++ b/platform/viewer/public/config/multiple.js @@ -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,