Test state machine hook

This commit is contained in:
dannyrb 2020-05-27 21:25:44 -04:00
parent c3dd2608b2
commit 870489be1d
6 changed files with 3432 additions and 2458 deletions

View File

@ -36,6 +36,8 @@
},
"dependencies": {
"@babel/runtime": "7.7.6",
"@xstate/immer": "^0.1.0",
"@xstate/react": "^0.8.1",
"xstate": "^4.10.0"
}
}

View File

@ -1,13 +1,10 @@
import React, { useContext, useReducer } from 'react';
const TRACKED_MEASUREMENTS_INITIAL_STATE = {
/** StudyInstanceUID */
trackedStudy: '',
/** string[] of SeriesInstanceUID */
trackedSeries: [],
/** key, value => displaySetInstanceUID, string label */
viewportLabels: new Map(),
};
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useMachine } from '@xstate/react';
import {
measurementTrackingMachine,
defaultOptions,
} from './measurementTrackingMachine';
const TrackedMeasurementsContext = React.createContext();
@ -20,49 +17,27 @@ const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
* @param {*} param0
*/
function TrackedMeasurementsContextProvider({ children }) {
/**
*
* @param {object} state - previous state
* @param {object} action
* @param {string} action.type
* @param {object} action.payload
*/
const reducer = (state, action) => {
switch (action.type) {
case 'TRACK_SERIES':
const { StudyInstanceUID, SeriesInstanceUID } = action.payload;
return state;
case 'UNTRACK_SERIES':
return state;
default:
return state;
}
};
// Computed:
// numTrackedSeries
// API
// -
// - TrackSeries(studyUid, seriesUid) (or... TrackViewport?)
// - Checks against tracked study + series
// - Initiates prompt if required
// - does nothing for SR modality? (how do we know SR viewport?????????)
// - UntrackSeries(seriesUid)
const [trackedMeasurements, dispatchTrackedMeasurements] = useReducer(
reducer,
TRACKED_MEASUREMENTS_INITIAL_STATE
);
// TODO: Configure `UIViewportNotificationService` for appropriate actions
const measurementTrackingMachineOptions = Object.assign({}, defaultOptions);
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
trackedMeasurementsService,
] = useMachine(measurementTrackingMachine, measurementTrackingMachineOptions);
return (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, dispatchTrackedMeasurements]}
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
>
{children}
</TrackedMeasurementsContext.Provider>
);
}
TrackedMeasurementsContextProvider.propTypes = {
children: PropTypes.oneOf([PropTypes.func, PropTypes.node]),
};
export {
TrackedMeasurementsContext,
TrackedMeasurementsContextProvider,

View File

@ -0,0 +1,196 @@
import { Machine, assign } from 'xstate';
// import { assign } from '@xstate/immer';
const machineConfiguration = {
id: 'measurementTracking',
initial: 'notTracking',
context: {
prevTrackedStudy: '',
prevTrackedSeries: [],
trackedStudy: '',
trackedSeries: [],
promptResponse: 0,
},
states: {
notTracking: {
entry: 'clearContext',
on: {
TRACK_SERIES: {
target: 'awaitShouldTrackPrompt',
actions: 'trackSeries',
},
},
},
awaitShouldTrackPrompt: {
initial: 'prompt',
states: {
prompt: {
invoke: {
id: 'shouldTrackPrompt',
src: () => confirmDialog('Should we start tracking?'),
onDone: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
onError: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
},
},
validateResponse: {
on: {
'': [
{
target: '#measurementTracking.tracking',
cond: 'acceptResponse',
},
{
target: '#measurementTracking.neverTrack',
cond: 'rejectResponse',
},
{
target: '#measurementTracking.notTracking',
cond: 'cancelResponse',
},
],
},
},
},
},
neverTrack: {
type: 'final',
},
tracking: {
on: {
TRACK_SERIES: [
{
target: 'tracking',
cond: 'hasNewSeriesForTrackedStudy',
actions: 'trackSeries',
},
{
target: 'awaitShouldTrackNewStudyPrompt',
cond: 'willTrackNewStudy',
actions: ['setPrevTracked', 'clearTrackedSeries', 'trackSeries'],
},
],
UNTRACK_SERIES: [
{
target: 'notTracking',
cond: 'willBeEmpty',
actions: 'untrackSeries',
},
{
target: 'tracking',
actions: 'untrackSeries',
},
],
},
},
awaitShouldTrackNewStudyPrompt: {
initial: 'prompt',
states: {
prompt: {
invoke: {
id: 'shouldTrackPrompt',
src: () => confirmDialog('Should we track new study?'),
onDone: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
onError: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
},
},
validateResponse: {
on: {
'': [
{
target: '#measurementTracking.tracking',
cond: 'acceptResponse',
},
{
target: '#measurementTracking.tracking',
cond: 'rejectResponse',
actions: 'restorePrevTracked',
},
],
},
},
},
},
},
strict: true,
};
function confirmDialog(msg) {
return new Promise(function(resolve, reject) {
let confirmed = window.confirm(msg);
return confirmed ? resolve(1) : reject(-1);
});
}
const defaultOptions = {
actions: {
clearContext: assign({
prevTrackedStudy: '',
prevTrackedSeries: [],
trackedStudy: '',
trackedSeries: [],
promptResponse: 0,
}),
setPrevTracked: assign(ctx => ({
prevTrackedStudy: ctx.trackedStudy,
prevTrackedSeries: ctx.trackedSeries.slice(),
})),
restorePrevTracked: assign(ctx => ({
trackedStudy: ctx.prevTrackedStudy,
trackedSeries: ctx.prevTrackedSeries.slice(),
})),
clearTrackedSeries: assign(() => ({
trackedStudy: '',
trackedSeries: [],
})),
trackSeries: assign((ctx, evt) => {
const prevTrackedSeries = ctx.trackedSeries.slice();
return {
trackedStudy: evt.StudyInstanceUID || '',
trackedSeries: [...prevTrackedSeries, evt.SeriesInstanceUID],
};
}),
untrackSeries: assign((ctx, evt) => {
const prevTrackedSeries = ctx.trackedSeries.slice();
return {
trackedSeries: prevTrackedSeries.filter(
serUid => serUid !== evt.SeriesInstanceUID
),
};
}),
},
guards: {
hasNewSeriesForTrackedStudy: (ctx, evt) =>
evt.StudyInstanceUID === ctx.trackedStudy &&
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
willTrackNewStudy: ctx => ctx.StudyInstanceUID !== ctx.trackedStudy,
willBeEmpty: (ctx, evt) =>
evt.SeriesInstanceUID &&
ctx.trackedSeries.length === 1 &&
ctx.trackedSeries[0] === evt.SeriesInstanceUID,
acceptResponse: ctx => ctx.promptResponse === 1,
cancelResponse: ctx => ctx.promptResponse === 0,
rejectResponse: ctx => ctx.promptResponse === -1,
},
};
const measurementTrackingMachine = Machine(machineConfiguration, defaultOptions);
// .transition(state, eventArgument).value
// const service = interpret(measurementTrackingMachine).start();
// .send(event): nextState
// .state (getter)
// .onTransition(state => { state.vale })
export { defaultOptions, machineConfiguration, measurementTrackingMachine };
export default measurementTrackingMachine;

View File

@ -1,116 +0,0 @@
import { Machine, interpret } from 'xstate';
// {
// "type": "TRACK_SERIES",
// "StudyInstanceUID": "study1",
// "SeriesInstanceUID": "series1"
// }
// TODO:
// userPrompts should invoke promises:
// https://egghead.io/lessons/xstate-invoking-a-promise-for-asynchronous-state-transitions-in-xstate
const config = {
id: 'measurementTracking',
initial: 'notTracking',
context: {
prevTrackedStudy: '',
prevTrackedSeries: [],
trackedStudy: '',
trackedSeries: [],
},
states: {
notTracking: {
on: {
TRACK_SERIES: {
target: 'userPromptShouldTrack',
actions: 'trackSeries',
},
}
},
userPromptShouldTrack: {
on: {
YES: 'tracking',
CANCEL: 'notTracking',
NEVER: 'neverTracking',
},
},
neverTracking: {
type: 'final',
},
tracking: {
on: {
TRACK_SERIES: [
{ target: 'tracking', cond: 'hasNewSeriesForTrackedStudy', actions: 'trackSeries' },
{ target: 'userPromptTrackingConflict', cond: 'hasConflictingStudyInstanceUID', actions: ['setPrevTracked', 'clearTrackedSeries', 'trackSeries'] },
],
UNTRACK_SERIES: [
{ target: 'tracking', cond: '', actions: ''},
{ target: 'notTracking', cond: '', actions: '' },
],
},
},
userPromptShouldTrackNewStudy: {
on: {
CANCEL: {
target: 'tracking',
actions: 'restorePrevTracked',
},
YES: 'tracking',
},
},
},
strict: true,
};
// Actions:
// - entry
// - exit
// - transition (on)
// Activities:
// - can return a "clean-up" function to call on exit
// actions, guards, services, activities, delays
const options = {
actions: {
logStuff: (context, event) => { console.log(context, event ) },
setPrevTracked: assign(context => ({
prevTrackedStudy: context.trackedStudy,
prevTrackedSeries: context.trackedSeries.slice(),
})),
restorePrevTracked: assign(context => ({
trackedStudy: context.prevTrackedStudy,
trackedSeries: context.prevTrackedSeries.slice(),
})),
clearTrackedSeries: assign(() => ({
trackedStudy: '',
trackedSeries: [],
})),
trackSeries: assign((context, event) =>
{
console.log(context, event);
const prevTrackedSeries = context.trackedSeries.slice();
return {
trackedStudy: event.StudyInstanceUID || '',
trackedSeries: [...prevTrackedSeries, event.SeriesInstanceUID],
}
}),
},
guards: {
hasConflictingStudyInstanceUID: (context, event) =>
context.StudyInstanceUID !== context.trackedStudy,
hasNewSeriesForTrackedStudy: (context, event) =>
event.StudyInstanceUID === context.trackedStudy &&
!context.trackedSeries.includes(event.SeriesInstanceUID),
},
}
const measurementTrackingMachine = Machine(config, options);
// .transition(state, eventArgument).value
const service = interpret(measurementTrackingMachine).start();
// .send(event): nextState
// .state (getter)
// .onTransition(state => { state.vale })
export default measurementTrackingMachine;

View File

@ -20,7 +20,7 @@ function PanelStudyBrowserTracking({
const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer();
const [
trackedMeasurements,
dispatchTrackedMeasurements,
sendTrackedMeasurementsEvent,
] = useTrackedMeasurements();
console.warn('trackedMeasurementsState: ', trackedMeasurements);
const [activeTabName, setActiveTabName] = useState('primary');
@ -33,15 +33,14 @@ function PanelStudyBrowserTracking({
// TODO: REPLACE ME
useEffect(() => {
dispatchTrackedMeasurements({
type: 'TRACK_SERIES',
payload: {
StudyInstanceUID:
'1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1',
SeriesInstanceUID: '2.25.406240484039997428244790346419850090743',
},
sendTrackedMeasurementsEvent('TRACK_SERIES', {
StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1',
SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125095258.2',
//'2.25.406240484039997428244790346419850090743',
});
}, [dispatchTrackedMeasurements]);
}, [sendTrackedMeasurementsEvent]);
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
// ~~ studyDisplayList
useEffect(() => {
@ -94,11 +93,16 @@ function PanelStudyBrowserTracking({
const currentDisplaySets = DisplaySetService.activeDisplaySets;
const mappedDisplaySets = _mapDisplaySets(
currentDisplaySets,
thumbnailImageSrcMap
thumbnailImageSrcMap,
trackedSeries
);
setDisplaySets(mappedDisplaySets);
}, [DisplaySetService.activeDisplaySets, thumbnailImageSrcMap]);
}, [
DisplaySetService.activeDisplaySets,
trackedSeries,
thumbnailImageSrcMap,
]);
// ~~ subscriptions --> displaySets
useEffect(() => {
@ -132,7 +136,8 @@ function PanelStudyBrowserTracking({
changedDisplaySets => {
const mappedDisplaySets = _mapDisplaySets(
changedDisplaySets,
thumbnailImageSrcMap
thumbnailImageSrcMap,
trackedSeries
);
setDisplaySets(mappedDisplaySets);
@ -143,7 +148,13 @@ function PanelStudyBrowserTracking({
SubscriptionDisplaySetsAdded.unsubscribe();
SubscriptionDisplaySetsChanged.unsubscribe();
};
}, [DisplaySetService, dataSource, getImageSrc, thumbnailImageSrcMap]);
}, [
DisplaySetService,
dataSource,
getImageSrc,
thumbnailImageSrcMap,
trackedSeries,
]);
const tabs = _createStudyBrowserTabs(
StudyInstanceUIDs,
@ -221,7 +232,11 @@ function _mapDataSourceStudies(studies) {
});
}
function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
function _mapDisplaySets(
displaySets,
thumbnailImageSrcMap,
trackedSeriesInstanceUIDs
) {
return displaySets.map(ds => {
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
return {
@ -239,6 +254,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
displaySetInstanceUID: ds.displaySetInstanceUID,
// .. Any other data to pass
},
isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID),
};
});
}

5469
yarn.lock

File diff suppressed because it is too large Load Diff