feat: Start using group filtering to define measurements table layout (#4501)
This commit is contained in:
parent
bc73de9710
commit
82440e88d5
@ -1,8 +1,9 @@
|
|||||||
import { metaData, utilities } from '@cornerstonejs/core';
|
import { metaData, utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||||
|
import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default';
|
||||||
|
|
||||||
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
|
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
|
||||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||||
@ -11,7 +12,6 @@ const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
|||||||
const { log } = OHIF;
|
const { log } = OHIF;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param measurementData An array of measurements from the measurements service
|
* @param measurementData An array of measurements from the measurements service
|
||||||
* that you wish to serialize.
|
* that you wish to serialize.
|
||||||
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||||
@ -43,8 +43,30 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
|||||||
|
|
||||||
const commandsModule = (props: withAppTypes) => {
|
const commandsModule = (props: withAppTypes) => {
|
||||||
const { servicesManager, extensionManager, commandsManager } = props;
|
const { servicesManager, extensionManager, commandsManager } = props;
|
||||||
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
|
const { customizationService, measurementService, viewportGridService, uiDialogService } =
|
||||||
|
servicesManager.services;
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
changeColorMeasurement: ({ uid }) => {
|
||||||
|
// When this gets supported, it probably belongs in cornerstone, not sr
|
||||||
|
throw new Error('Unsupported operation: changeColorMeasurement');
|
||||||
|
// const { color } = measurementService.getMeasurement(uid);
|
||||||
|
// const rgbaColor = {
|
||||||
|
// r: color[0],
|
||||||
|
// g: color[1],
|
||||||
|
// b: color[2],
|
||||||
|
// a: color[3] / 255.0,
|
||||||
|
// };
|
||||||
|
// colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||||
|
// if (actionId === 'cancel') {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
|
||||||
|
// segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param measurementData An array of measurements from the measurements service
|
* @param measurementData An array of measurements from the measurements service
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
annotation,
|
annotation,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
import { Types as OhifTypes } from '@ohif/core';
|
import { Types as OhifTypes, utils } from '@ohif/core';
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import {
|
import {
|
||||||
callLabelAutocompleteDialog,
|
callLabelAutocompleteDialog,
|
||||||
@ -295,6 +295,60 @@ function commandsModule({
|
|||||||
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
|
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jumps to the specified (by uid) measurement in the active viewport.
|
||||||
|
* Also marks any provided display measurements isActive value
|
||||||
|
*/
|
||||||
|
jumpToMeasurement: ({ uid, displayMeasurements = [] }) => {
|
||||||
|
measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), uid);
|
||||||
|
for (const measurement of displayMeasurements) {
|
||||||
|
measurement.isActive = measurement.uid === uid;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMeasurement: ({ uid }) => {
|
||||||
|
measurementService.remove(uid);
|
||||||
|
},
|
||||||
|
|
||||||
|
renameMeasurement: ({ uid }) => {
|
||||||
|
const labelConfig = customizationService.get('measurementLabels');
|
||||||
|
const measurement = measurementService.getMeasurement(uid);
|
||||||
|
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => {
|
||||||
|
measurementService.update(
|
||||||
|
uid,
|
||||||
|
{
|
||||||
|
...val,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleLockMeasurement: ({ uid }) => {
|
||||||
|
measurementService.toggleLockMeasurement(uid);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleVisibilityMeasurement: ({ uid }) => {
|
||||||
|
measurementService.toggleVisibilityMeasurement(uid);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the measurements
|
||||||
|
*/
|
||||||
|
clearMeasurements: options => {
|
||||||
|
const { measurementFilter } = options;
|
||||||
|
measurementService.clearMeasurements(
|
||||||
|
measurementFilter ? measurementFilter.bind(options) : null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the CSV report for the measurements.
|
||||||
|
*/
|
||||||
|
downloadCSVMeasurementsReport: ({ measurementFilter }) => {
|
||||||
|
utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter));
|
||||||
|
},
|
||||||
|
|
||||||
// Retrieve value commands
|
// Retrieve value commands
|
||||||
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
|
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
|
||||||
|
|
||||||
@ -1281,6 +1335,27 @@ function commandsModule({
|
|||||||
updateMeasurement: {
|
updateMeasurement: {
|
||||||
commandFn: actions.updateMeasurement,
|
commandFn: actions.updateMeasurement,
|
||||||
},
|
},
|
||||||
|
clearMeasurements: {
|
||||||
|
commandFn: actions.clearMeasurements,
|
||||||
|
},
|
||||||
|
jumpToMeasurement: {
|
||||||
|
commandFn: actions.jumpToMeasurement,
|
||||||
|
},
|
||||||
|
removeMeasurement: {
|
||||||
|
commandFn: actions.removeMeasurement,
|
||||||
|
},
|
||||||
|
renameMeasurement: {
|
||||||
|
commandFn: actions.renameMeasurement,
|
||||||
|
},
|
||||||
|
toggleLockMeasurement: {
|
||||||
|
commandFn: actions.toggleLockMeasurement,
|
||||||
|
},
|
||||||
|
toggleVisibilityMeasurement: {
|
||||||
|
commandFn: actions.toggleVisibilityMeasurement,
|
||||||
|
},
|
||||||
|
downloadCSVMeasurementsReport: {
|
||||||
|
commandFn: actions.downloadCSVMeasurementsReport,
|
||||||
|
},
|
||||||
setViewportWindowLevel: {
|
setViewportWindowLevel: {
|
||||||
commandFn: actions.setViewportWindowLevel,
|
commandFn: actions.setViewportWindowLevel,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||||
|
import { StudySummary } from '@ohif/ui-next';
|
||||||
|
|
||||||
|
const { formatDate } = utils;
|
||||||
|
|
||||||
|
export function StudySummaryFromMetadata({ StudyInstanceUID }) {
|
||||||
|
if (!StudyInstanceUID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
|
if (!studyMeta?.series?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceMeta = studyMeta.series[0].instances[0];
|
||||||
|
const { StudyDate, StudyDescription } = instanceMeta;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StudySummary
|
||||||
|
date={formatDate(StudyDate)}
|
||||||
|
description={StudyDescription}
|
||||||
|
></StudySummary>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { Toolbox } from '@ohif/ui-next';
|
import { Toolbox } from '@ohif/ui-next';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||||
import PanelMeasurementTable from './panels/PanelMeasurement';
|
import PanelMeasurement from './panels/PanelMeasurement';
|
||||||
|
|
||||||
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
|
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
|
||||||
const wrappedPanelSegmentation = ({ configuration }) => {
|
const wrappedPanelSegmentation = ({ configuration }) => {
|
||||||
@ -59,7 +59,7 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
|
|||||||
|
|
||||||
const wrappedPanelMeasurement = ({ configuration }) => {
|
const wrappedPanelMeasurement = ({ configuration }) => {
|
||||||
return (
|
return (
|
||||||
<PanelMeasurementTable
|
<PanelMeasurement
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
|
|||||||
@ -62,10 +62,7 @@ export function useMeasurements(servicesManager, { measurementFilter }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDisplayMeasurements = () => {
|
const updateDisplayMeasurements = () => {
|
||||||
let measurements = measurementService.getMeasurements();
|
let measurements = measurementService.getMeasurements(measurementFilter);
|
||||||
if (measurementFilter) {
|
|
||||||
measurements = measurements.filter(measurementFilter);
|
|
||||||
}
|
|
||||||
const mappedMeasurements = measurements.map(m =>
|
const mappedMeasurements = measurements.map(m =>
|
||||||
mapMeasurementToDisplay(m, displaySetService)
|
mapMeasurementToDisplay(m, displaySetService)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -53,6 +53,7 @@ import PanelSegmentation from './panels/PanelSegmentation';
|
|||||||
import PanelMeasurement from './panels/PanelMeasurement';
|
import PanelMeasurement from './panels/PanelMeasurement';
|
||||||
import DicomUpload from './components/DicomUpload/DicomUpload';
|
import DicomUpload from './components/DicomUpload/DicomUpload';
|
||||||
import { useSegmentations } from './hooks/useSegmentations';
|
import { useSegmentations } from './hooks/useSegmentations';
|
||||||
|
import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata';
|
||||||
|
|
||||||
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
|
||||||
|
|
||||||
@ -254,5 +255,6 @@ export {
|
|||||||
PanelSegmentation,
|
PanelSegmentation,
|
||||||
PanelMeasurement,
|
PanelMeasurement,
|
||||||
DicomUpload,
|
DicomUpload,
|
||||||
|
StudySummaryFromMetadata,
|
||||||
};
|
};
|
||||||
export default cornerstoneExtension;
|
export default cornerstoneExtension;
|
||||||
|
|||||||
@ -1,19 +1,29 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
import { useViewportGrid } from '@ohif/ui-next';
|
||||||
import { MeasurementTable } from '@ohif/ui-next';
|
import { MeasurementTable } from '@ohif/ui-next';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
import { useMeasurements } from '../hooks/useMeasurements';
|
import { useMeasurements } from '../hooks/useMeasurements';
|
||||||
import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default';
|
|
||||||
|
|
||||||
export default function PanelMeasurementTable({
|
const {
|
||||||
|
filterAdditionalFindings: filterAdditionalFinding,
|
||||||
|
filterOr,
|
||||||
|
filterAny,
|
||||||
|
} = utils.MeasurementFilters;
|
||||||
|
|
||||||
|
export type withAppAndFilters = withAppTypes & {
|
||||||
|
measurementFilter: (item) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PanelMeasurement({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
customHeader,
|
customHeader,
|
||||||
measurementFilter,
|
measurementFilter = filterAny,
|
||||||
}: withAppTypes): React.ReactNode {
|
}: withAppAndFilters): React.ReactNode {
|
||||||
const measurementsPanelRef = useRef(null);
|
const measurementsPanelRef = useRef(null);
|
||||||
|
|
||||||
const [viewportGrid] = useViewportGrid();
|
const { measurementService } = servicesManager.services;
|
||||||
const { measurementService, customizationService, uiDialogService } = servicesManager.services;
|
|
||||||
|
|
||||||
const displayMeasurements = useMeasurements(servicesManager, {
|
const displayMeasurements = useMeasurements(servicesManager, {
|
||||||
measurementFilter,
|
measurementFilter,
|
||||||
@ -27,75 +37,37 @@ export default function PanelMeasurementTable({
|
|||||||
}
|
}
|
||||||
}, [displayMeasurements.length]);
|
}, [displayMeasurements.length]);
|
||||||
|
|
||||||
const onMeasurementItemClickHandler = (uid: string, isActive: boolean) => {
|
const bindCommand = (name: string | string[], options?) => {
|
||||||
if (isActive) {
|
return (uid: string) => {
|
||||||
return;
|
commandsManager.run(name, { ...options, uid });
|
||||||
}
|
|
||||||
|
|
||||||
const measurements = [...displayMeasurements];
|
|
||||||
const measurement = measurements.find(m => m.uid === uid);
|
|
||||||
|
|
||||||
measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
|
|
||||||
measurement.isActive = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const jumpToImage = (uid: string) => {
|
|
||||||
measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid);
|
|
||||||
onMeasurementItemClickHandler(uid, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeMeasurement = (uid: string) => {
|
|
||||||
measurementService.remove(uid);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renameMeasurement = (uid: string) => {
|
|
||||||
jumpToImage(uid);
|
|
||||||
const labelConfig = customizationService.get('measurementLabels');
|
|
||||||
const measurement = measurementService.getMeasurement(uid);
|
|
||||||
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => {
|
|
||||||
measurementService.update(
|
|
||||||
uid,
|
|
||||||
{
|
|
||||||
...val,
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeColorMeasurement = (uid: string) => {
|
|
||||||
const { color } = measurementService.getMeasurement(uid);
|
|
||||||
const rgbaColor = {
|
|
||||||
r: color[0],
|
|
||||||
g: color[1],
|
|
||||||
b: color[2],
|
|
||||||
a: color[3] / 255.0,
|
|
||||||
};
|
};
|
||||||
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
|
||||||
if (actionId === 'cancel') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
|
|
||||||
// segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleLockMeasurement = (uid: string) => {
|
const jumpToImage = bindCommand('jumpToMeasurement', { displayMeasurements });
|
||||||
measurementService.toggleLockMeasurement(uid);
|
const removeMeasurement = bindCommand('removeMeasurement');
|
||||||
};
|
const renameMeasurement = bindCommand(['jumpToMeasurement', 'renameMeasurement'], {
|
||||||
|
displayMeasurements,
|
||||||
|
});
|
||||||
|
const toggleLockMeasurement = bindCommand('toggleLockMeasurement');
|
||||||
|
const toggleVisibilityMeasurement = bindCommand('toggleVisibilityMeasurement');
|
||||||
|
|
||||||
const toggleVisibilityMeasurement = (uid: string) => {
|
const additionalFilter = filterAdditionalFinding(measurementService);
|
||||||
measurementService.toggleVisibilityMeasurement(uid);
|
|
||||||
};
|
|
||||||
|
|
||||||
const measurements = displayMeasurements.filter(
|
const measurements = displayMeasurements.filter(
|
||||||
dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId
|
item => !additionalFilter(item) && measurementFilter(item)
|
||||||
);
|
);
|
||||||
const additionalFindings = displayMeasurements.filter(
|
const additionalFindings = displayMeasurements.filter(
|
||||||
dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId
|
item => additionalFilter(item) && measurementFilter(item)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onArgs = {
|
||||||
|
onClick: jumpToImage,
|
||||||
|
onDelete: removeMeasurement,
|
||||||
|
onToggleVisibility: toggleVisibilityMeasurement,
|
||||||
|
onToggleLocked: toggleLockMeasurement,
|
||||||
|
onRename: renameMeasurement,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -104,13 +76,10 @@ export default function PanelMeasurementTable({
|
|||||||
data-cy={'trackedMeasurements-panel'}
|
data-cy={'trackedMeasurements-panel'}
|
||||||
>
|
>
|
||||||
<MeasurementTable
|
<MeasurementTable
|
||||||
|
key="tracked"
|
||||||
title="Measurements"
|
title="Measurements"
|
||||||
data={measurements}
|
data={measurements}
|
||||||
onClick={jumpToImage}
|
{...onArgs}
|
||||||
onDelete={removeMeasurement}
|
|
||||||
onToggleVisibility={toggleVisibilityMeasurement}
|
|
||||||
onToggleLocked={toggleLockMeasurement}
|
|
||||||
onRename={renameMeasurement}
|
|
||||||
// onColor={changeColorMeasurement}
|
// onColor={changeColorMeasurement}
|
||||||
>
|
>
|
||||||
<MeasurementTable.Header>
|
<MeasurementTable.Header>
|
||||||
@ -129,14 +98,10 @@ export default function PanelMeasurementTable({
|
|||||||
</MeasurementTable>
|
</MeasurementTable>
|
||||||
{additionalFindings.length > 0 && (
|
{additionalFindings.length > 0 && (
|
||||||
<MeasurementTable
|
<MeasurementTable
|
||||||
|
key="additional"
|
||||||
data={additionalFindings}
|
data={additionalFindings}
|
||||||
title="Additional Findings"
|
title="Additional Findings"
|
||||||
onClick={jumpToImage}
|
{...onArgs}
|
||||||
onDelete={removeMeasurement}
|
|
||||||
onToggleVisibility={toggleVisibilityMeasurement}
|
|
||||||
onToggleLocked={toggleLockMeasurement}
|
|
||||||
onRename={renameMeasurement}
|
|
||||||
// onColor={changeColorMeasurement}
|
|
||||||
>
|
>
|
||||||
<MeasurementTable.Body />
|
<MeasurementTable.Body />
|
||||||
</MeasurementTable>
|
</MeasurementTable>
|
||||||
|
|||||||
@ -562,27 +562,13 @@ const commandsModule = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
showContextMenu: {
|
showContextMenu: actions.showContextMenu,
|
||||||
commandFn: actions.showContextMenu,
|
closeContextMenu: actions.closeContextMenu,
|
||||||
},
|
clearMeasurements: actions.clearMeasurements,
|
||||||
closeContextMenu: {
|
displayNotification: actions.displayNotification,
|
||||||
commandFn: actions.closeContextMenu,
|
setHangingProtocol: actions.setHangingProtocol,
|
||||||
},
|
toggleHangingProtocol: actions.toggleHangingProtocol,
|
||||||
clearMeasurements: {
|
navigateHistory: actions.navigateHistory,
|
||||||
commandFn: actions.clearMeasurements,
|
|
||||||
},
|
|
||||||
displayNotification: {
|
|
||||||
commandFn: actions.displayNotification,
|
|
||||||
},
|
|
||||||
setHangingProtocol: {
|
|
||||||
commandFn: actions.setHangingProtocol,
|
|
||||||
},
|
|
||||||
toggleHangingProtocol: {
|
|
||||||
commandFn: actions.toggleHangingProtocol,
|
|
||||||
},
|
|
||||||
navigateHistory: {
|
|
||||||
commandFn: actions.navigateHistory,
|
|
||||||
},
|
|
||||||
nextStage: {
|
nextStage: {
|
||||||
commandFn: actions.deltaStage,
|
commandFn: actions.deltaStage,
|
||||||
options: { direction: 1 },
|
options: { direction: 1 },
|
||||||
@ -591,18 +577,10 @@ const commandsModule = ({
|
|||||||
commandFn: actions.deltaStage,
|
commandFn: actions.deltaStage,
|
||||||
options: { direction: -1 },
|
options: { direction: -1 },
|
||||||
},
|
},
|
||||||
setViewportGridLayout: {
|
setViewportGridLayout: actions.setViewportGridLayout,
|
||||||
commandFn: actions.setViewportGridLayout,
|
toggleOneUp: actions.toggleOneUp,
|
||||||
},
|
openDICOMTagViewer: actions.openDICOMTagViewer,
|
||||||
toggleOneUp: {
|
updateViewportDisplaySet: actions.updateViewportDisplaySet,
|
||||||
commandFn: actions.toggleOneUp,
|
|
||||||
},
|
|
||||||
openDICOMTagViewer: {
|
|
||||||
commandFn: actions.openDICOMTagViewer,
|
|
||||||
},
|
|
||||||
updateViewportDisplaySet: {
|
|
||||||
commandFn: actions.updateViewportDisplaySet,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,20 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { PanelMeasurement } from '@ohif/extension-cornerstone';
|
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
|
||||||
import { StudySummary } from '@ohif/ui-next';
|
|
||||||
import { Button, Icons } from '@ohif/ui-next';
|
|
||||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||||
|
import { useViewportGrid } from '@ohif/ui-next';
|
||||||
|
import { Button, Icons } from '@ohif/ui-next';
|
||||||
|
import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone';
|
||||||
import { useTrackedMeasurements } from '../getContextModule';
|
import { useTrackedMeasurements } from '../getContextModule';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const { downloadCSVReport, formatDate } = utils;
|
const { filterAnd, filterPlanarMeasurement, filterAny, filterMeasurementsBySeriesUID } =
|
||||||
|
utils.MeasurementFilters;
|
||||||
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
|
|
||||||
key: undefined, //
|
|
||||||
date: '', // '07-Sep-2010',
|
|
||||||
modality: '', // 'CT',
|
|
||||||
description: '', // 'CHEST/ABD/PELVIS W CONTRAST',
|
|
||||||
};
|
|
||||||
|
|
||||||
function PanelMeasurementTableTracking({
|
function PanelMeasurementTableTracking({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
@ -22,52 +14,12 @@ function PanelMeasurementTableTracking({
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
}: withAppTypes) {
|
}: withAppTypes) {
|
||||||
const [viewportGrid] = useViewportGrid();
|
const [viewportGrid] = useViewportGrid();
|
||||||
const { t } = useTranslation('MeasurementTable');
|
const { customizationService } = servicesManager.services;
|
||||||
const { measurementService, customizationService } = servicesManager.services;
|
|
||||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||||
const [displayStudySummary, setDisplayStudySummary] = useState(
|
const measurementFilter = trackedStudy
|
||||||
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
|
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
|
||||||
);
|
: filterPlanarMeasurement;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateDisplayStudySummary = async () => {
|
|
||||||
if (trackedMeasurements.matches('tracking') && trackedStudy) {
|
|
||||||
const studyMeta = DicomMetadataStore.getStudy(trackedStudy);
|
|
||||||
if (!studyMeta || !studyMeta.series || studyMeta.series.length === 0) {
|
|
||||||
console.debug('Study metadata not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instanceMeta = studyMeta.series[0].instances[0];
|
|
||||||
const { StudyDate, StudyDescription } = instanceMeta;
|
|
||||||
|
|
||||||
const modalities = new Set();
|
|
||||||
studyMeta.series.forEach(series => {
|
|
||||||
if (trackedSeries.includes(series.SeriesInstanceUID)) {
|
|
||||||
modalities.add(series.instances[0].Modality);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const modality = Array.from(modalities).join('/');
|
|
||||||
|
|
||||||
setDisplayStudySummary(prevSummary => {
|
|
||||||
if (prevSummary.key !== trackedStudy) {
|
|
||||||
return {
|
|
||||||
key: trackedStudy,
|
|
||||||
date: StudyDate,
|
|
||||||
modality,
|
|
||||||
description: StudyDescription,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return prevSummary;
|
|
||||||
});
|
|
||||||
} else if (!trackedStudy) {
|
|
||||||
setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateDisplayStudySummary();
|
|
||||||
}, [trackedMeasurements, trackedStudy, trackedSeries]);
|
|
||||||
|
|
||||||
const { disableEditing } = customizationService.getCustomization(
|
const { disableEditing } = customizationService.getCustomization(
|
||||||
'PanelMeasurement.disableEditing',
|
'PanelMeasurement.disableEditing',
|
||||||
@ -79,20 +31,12 @@ function PanelMeasurementTableTracking({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{displayStudySummary.key && (
|
<StudySummaryFromMetadata StudyInstanceUID={trackedStudy} />
|
||||||
<StudySummary
|
|
||||||
date={formatDate(displayStudySummary.date)}
|
|
||||||
description={displayStudySummary.description}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PanelMeasurement
|
<PanelMeasurement
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
measurementFilter={measurement =>
|
measurementFilter={measurementFilter}
|
||||||
trackedStudy === measurement.referenceStudyUID &&
|
|
||||||
trackedSeries.includes(measurement.referenceSeriesUID)
|
|
||||||
}
|
|
||||||
customHeader={({ additionalFindings, measurements }) => {
|
customHeader={({ additionalFindings, measurements }) => {
|
||||||
const disabled = additionalFindings.length === 0 && measurements.length === 0;
|
const disabled = additionalFindings.length === 0 && measurements.length === 0;
|
||||||
|
|
||||||
@ -108,14 +52,9 @@ function PanelMeasurementTableTracking({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="pl-1.5"
|
className="pl-1.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const measurements = measurementService.getMeasurements();
|
commandsManager.runCommand('downloadCSVMeasurementsReport', {
|
||||||
const trackedMeasurements = measurements.filter(
|
measurementFilter,
|
||||||
m =>
|
});
|
||||||
trackedStudy === m.referenceStudyUID &&
|
|
||||||
trackedSeries.includes(m.referenceSeriesUID)
|
|
||||||
);
|
|
||||||
|
|
||||||
downloadCSVReport(trackedMeasurements);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.Download className="h-5 w-5" />
|
<Icons.Download className="h-5 w-5" />
|
||||||
@ -140,7 +79,7 @@ function PanelMeasurementTableTracking({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="pl-0.5"
|
className="pl-0.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
measurementService.clearMeasurements();
|
commandsManager.runCommand('clearMeasurements', { measurementFilter });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.Delete />
|
<Icons.Delete />
|
||||||
|
|||||||
@ -15,7 +15,6 @@ const ohif = {
|
|||||||
wsiSopClassHandler:
|
wsiSopClassHandler:
|
||||||
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||||
measurements: '@ohif/extension-default.panelModule.measurements',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tracked = {
|
const tracked = {
|
||||||
@ -46,6 +45,7 @@ const dicomSeg = {
|
|||||||
|
|
||||||
const cornerstone = {
|
const cornerstone = {
|
||||||
panel: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
|
panel: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
|
||||||
|
measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
|
||||||
};
|
};
|
||||||
|
|
||||||
const dicomPmap = {
|
const dicomPmap = {
|
||||||
@ -150,7 +150,8 @@ function modeFactory() {
|
|||||||
// leftPanels: [ohif.thumbnailList],
|
// leftPanels: [ohif.thumbnailList],
|
||||||
// rightPanels: [dicomSeg.panel, ohif.measurements],
|
// rightPanels: [dicomSeg.panel, ohif.measurements],
|
||||||
leftPanels: [tracked.thumbnailList],
|
leftPanels: [tracked.thumbnailList],
|
||||||
rightPanels: [cornerstone.panel, tracked.measurements],
|
// Can use cornerstone.measurements for all measurements
|
||||||
|
rightPanels: [cornerstone.panel, tracked.measurements, cornerstone.measurements],
|
||||||
// rightPanelClosed: true, // optional prop to start with collapse panels
|
// rightPanelClosed: true, // optional prop to start with collapse panels
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -161,6 +161,16 @@ export class CommandsManager {
|
|||||||
* Run one or more commands with specified extra options.
|
* Run one or more commands with specified extra options.
|
||||||
* Returns the result of the last command run.
|
* Returns the result of the last command run.
|
||||||
*
|
*
|
||||||
|
* Example commands to run are:
|
||||||
|
* * 'updateMeasurement'
|
||||||
|
* * `{commandName: 'displayWhatever'}`
|
||||||
|
* * `['updateMeasurement', {commandName: 'displayWhatever'}]`
|
||||||
|
* * `{ commands: 'updateMeasurement' }`
|
||||||
|
* * `{ commands: ['updateMeasurement', {commandName: 'displayWhatever'}]}`
|
||||||
|
*
|
||||||
|
* Note how the various styles can be mixed, simplifying the declaration of
|
||||||
|
* sets of commands.
|
||||||
|
*
|
||||||
* @param toRun - A specification of one or more commands,
|
* @param toRun - A specification of one or more commands,
|
||||||
* typically an object of { commandName, commandOptions, context }
|
* typically an object of { commandName, commandOptions, context }
|
||||||
* or an array of such objects. It can also be a single commandName as string
|
* or an array of such objects. It can also be a single commandName as string
|
||||||
@ -169,7 +179,7 @@ export class CommandsManager {
|
|||||||
* the commandOptions specified in the base.
|
* the commandOptions specified in the base.
|
||||||
*/
|
*/
|
||||||
public run(
|
public run(
|
||||||
toRun: Command | Commands | Command[] | string | undefined,
|
toRun: Command | Commands | (Command | string)[] | string | undefined,
|
||||||
options?: Record<string, unknown>
|
options?: Record<string, unknown>
|
||||||
): unknown {
|
): unknown {
|
||||||
if (!toRun) {
|
if (!toRun) {
|
||||||
|
|||||||
@ -597,7 +597,10 @@ export default class ExtensionManager extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(definitions).forEach(commandName => {
|
Object.keys(definitions).forEach(commandName => {
|
||||||
const commandDefinition = definitions[commandName];
|
let commandDefinition = definitions[commandName];
|
||||||
|
if (typeof commandDefinition === 'function') {
|
||||||
|
commandDefinition = { commandFn: commandDefinition };
|
||||||
|
}
|
||||||
const commandHasContextThatDoesNotExist =
|
const commandHasContextThatDoesNotExist =
|
||||||
commandDefinition.context && !this._commandsManager.getContext(commandDefinition.context);
|
commandDefinition.context && !this._commandsManager.getContext(commandDefinition.context);
|
||||||
|
|
||||||
|
|||||||
@ -92,6 +92,8 @@ const VALUE_TYPES = {
|
|||||||
ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual',
|
ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MeasurementFilter = (measurement) => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MeasurementService class that supports source management and measurement management.
|
* MeasurementService class that supports source management and measurement management.
|
||||||
* Sources can be any library that can provide "annotations" (e.g. cornerstone-tools, cornerstone, etc.)
|
* Sources can be any library that can provide "annotations" (e.g. cornerstone-tools, cornerstone, etc.)
|
||||||
@ -108,7 +110,7 @@ class MeasurementService extends PubSubService {
|
|||||||
public static REGISTRATION = {
|
public static REGISTRATION = {
|
||||||
name: 'measurementService',
|
name: 'measurementService',
|
||||||
altName: 'MeasurementService',
|
altName: 'MeasurementService',
|
||||||
create: ({ configuration = {} }) => {
|
create: _options => {
|
||||||
return new MeasurementService();
|
return new MeasurementService();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -120,10 +122,11 @@ class MeasurementService extends PubSubService {
|
|||||||
private measurements = new Map();
|
private measurements = new Map();
|
||||||
private unmappedMeasurements = new Map();
|
private unmappedMeasurements = new Map();
|
||||||
|
|
||||||
|
private sources = {};
|
||||||
|
private mappings = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EVENTS);
|
super(EVENTS);
|
||||||
this.sources = {};
|
|
||||||
this.mappings = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,12 +167,16 @@ class MeasurementService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all measurements.
|
* Gets measurements, optionally filtered by the filter
|
||||||
|
* function.
|
||||||
*
|
*
|
||||||
* @return {Measurement[]} Array of measurements
|
* @return {Measurement[]} Array of measurements
|
||||||
*/
|
*/
|
||||||
getMeasurements() {
|
public getMeasurements(filter?: MeasurementFilter) {
|
||||||
return [...this.measurements.values()];
|
const measurements = [...this.measurements.values()];
|
||||||
|
return filter
|
||||||
|
? measurements.filter(measurement => filter.call(this, measurement))
|
||||||
|
: measurements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -582,11 +589,20 @@ class MeasurementService extends PubSubService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMeasurements() {
|
/**
|
||||||
|
* Clears measurements that match the filter, defaulting to all of them.
|
||||||
|
* That allows, for example, clearing all of a single studies measurements
|
||||||
|
* without needing to clear other measurements.
|
||||||
|
*/
|
||||||
|
public clearMeasurements(filter?: MeasurementFilter) {
|
||||||
// Make a copy of the measurements
|
// Make a copy of the measurements
|
||||||
const measurements = [...this.measurements.values(), ...this.unmappedMeasurements.values()];
|
const toClear = this.getMeasurements(filter);
|
||||||
this.unmappedMeasurements.clear();
|
const unmappedClear = filter
|
||||||
this.measurements.clear();
|
? [...this.unmappedMeasurements.values()].filter(filter)
|
||||||
|
: this.unmappedMeasurements;
|
||||||
|
const measurements = [...toClear, ...unmappedClear];
|
||||||
|
unmappedClear.forEach(measurement => this.unmappedMeasurements.delete(measurement.uid));
|
||||||
|
toClear.forEach(measurement => this.measurements.delete(measurement.uid));
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ describe('Top level exports', () => {
|
|||||||
'progressTrackingUtils',
|
'progressTrackingUtils',
|
||||||
'uuidv4',
|
'uuidv4',
|
||||||
'addAccessors',
|
'addAccessors',
|
||||||
|
'MeasurementFilters',
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(utils.default).sort();
|
const exports = Object.keys(utils.default).sort();
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import {
|
|||||||
import { splitComma, getSplitParam } from './splitComma';
|
import { splitComma, getSplitParam } from './splitComma';
|
||||||
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
|
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
|
||||||
import { sopClassDictionary } from './sopClassDictionary';
|
import { sopClassDictionary } from './sopClassDictionary';
|
||||||
|
import * as MeasurementFilters from './measurementFilters';
|
||||||
|
|
||||||
// Commented out unused functionality.
|
// Commented out unused functionality.
|
||||||
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
||||||
@ -84,6 +85,7 @@ const utils = {
|
|||||||
getSplitParam,
|
getSplitParam,
|
||||||
generateAcceptHeader,
|
generateAcceptHeader,
|
||||||
createStudyBrowserTabs,
|
createStudyBrowserTabs,
|
||||||
|
MeasurementFilters,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -117,6 +119,7 @@ export {
|
|||||||
getSplitParam,
|
getSplitParam,
|
||||||
generateAcceptHeader,
|
generateAcceptHeader,
|
||||||
createStudyBrowserTabs,
|
createStudyBrowserTabs,
|
||||||
|
MeasurementFilters,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
|
|||||||
110
platform/core/src/utils/measurementFilters.ts
Normal file
110
platform/core/src/utils/measurementFilters.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Returns a filter function which filters for measurements belonging to both
|
||||||
|
* the study and series.
|
||||||
|
*/
|
||||||
|
export function filterMeasurementsBySeriesUID(selectedSeries: string[]) {
|
||||||
|
return measurement => selectedSeries.includes(measurement.referenceSeriesUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true for measurements include referencedImageId (coplanar with an image)
|
||||||
|
*/
|
||||||
|
export function filterPlanarMeasurement(measurement) {
|
||||||
|
return measurement?.referencedImageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A filter that always returns true */
|
||||||
|
export function filterAny(_measurement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A filter that excludes everything */
|
||||||
|
export function filterNone(_measurement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the measurements which are found in any of the specified
|
||||||
|
* filters. Strings will be looked up by name.
|
||||||
|
*/
|
||||||
|
export function filterOr(...filters) {
|
||||||
|
return function (item) {
|
||||||
|
for (let filter of filters) {
|
||||||
|
if (typeof filter === 'string') {
|
||||||
|
filter = this[filter];
|
||||||
|
}
|
||||||
|
if (typeof filter !== 'function') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (filter.call(this, item)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters for additional findings, that is, measurements with
|
||||||
|
* a value of type point, and having a referenced image
|
||||||
|
*/
|
||||||
|
export function filterAdditionalFindings(measurementService) {
|
||||||
|
const { POINT } = measurementService.VALUE_TYPES;
|
||||||
|
return dm => dm.type === POINT && dm.referencedImageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a filter that applies the second filter unless the first filter would
|
||||||
|
* include the given measurement.
|
||||||
|
* That is, (!filterUnless) && filterThen
|
||||||
|
*/
|
||||||
|
export function filterUnless(filterUnless, filterThen) {
|
||||||
|
return item => (filterUnless(item) ? false : filterThen(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isString = s => typeof s === 'string' || s instanceof String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if all the filters return true.
|
||||||
|
* Any filter can be a string name of a filter on the "this" object
|
||||||
|
* called on the final filter call.
|
||||||
|
*/
|
||||||
|
export function filterAnd(...filters) {
|
||||||
|
return function (item) {
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (isString(filter)) {
|
||||||
|
if (!this[filter](item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (!filter.call(this, item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a filter that returns true if none of the filters supplied return true.
|
||||||
|
* Any filter supplied can be a name, in which case hte filter will be retrieved
|
||||||
|
* from "this" object on the call.
|
||||||
|
*
|
||||||
|
* For example, for filterNot("otherFilterName"), if that is called on
|
||||||
|
* `{ otherFilterName: filterNone }`
|
||||||
|
* then otherFilterName will be called, returning false in this case and
|
||||||
|
* filterNot will return true.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function filterNot(...filters) {
|
||||||
|
if (filters.length !== 1) {
|
||||||
|
return filterAnd.apply(null, filters.map(filterNot));
|
||||||
|
}
|
||||||
|
const [filter] = filters;
|
||||||
|
if (isString(filter)) {
|
||||||
|
return function (item) {
|
||||||
|
return !this[filter](item);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item => !filter(item);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user