mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-08 10:48:12 +08:00
undoable TS typing (#2374)
* type undoable-related TS * style change to type declaration
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
|
import { Reducer } from "redux";
|
||||||
import undoable from "../../src/reducers/undoable";
|
import undoable from "../../src/reducers/undoable";
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
test("no keys", () => {
|
test("no keys", () => {
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2-3 arguments, but got 1.
|
expect(() =>
|
||||||
expect(() => undoable(() => {})).toThrow();
|
undoable(() => {}, undefined as unknown as string[])
|
||||||
expect(() => undoable(() => {}, null)).toThrow();
|
).toThrow();
|
||||||
|
expect(() => undoable(() => {}, null as unknown as string[])).toThrow();
|
||||||
expect(() => undoable(() => {}, [])).toThrow();
|
expect(() => undoable(() => {}, [])).toThrow();
|
||||||
expect(() => undoable(() => {}, [], {})).toThrow();
|
expect(() => undoable(() => {}, [], {})).toThrow();
|
||||||
});
|
});
|
||||||
@@ -24,8 +26,7 @@ describe("create", () => {
|
|||||||
describe("undo", () => {
|
describe("undo", () => {
|
||||||
test("expected state modifications", () => {
|
test("expected state modifications", () => {
|
||||||
const initialState = { a: 0, b: 1000 };
|
const initialState = { a: 0, b: 1000 };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
||||||
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 });
|
|
||||||
const undoableReducer = undoable(reducer, ["a"]);
|
const undoableReducer = undoable(reducer, ["a"]);
|
||||||
|
|
||||||
const s1 = undoableReducer(initialState, { type: "test" });
|
const s1 = undoableReducer(initialState, { type: "test" });
|
||||||
@@ -43,10 +44,8 @@ describe("undo", () => {
|
|||||||
|
|
||||||
describe("redo", () => {
|
describe("redo", () => {
|
||||||
const initialState = { a: 0, b: 1000 };
|
const initialState = { a: 0, b: 1000 };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
||||||
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 });
|
let UR: Reducer;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
let UR: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UR = undoable(reducer, ["a"]);
|
UR = undoable(reducer, ["a"]);
|
||||||
|
|||||||
@@ -51,27 +51,55 @@ history state processing. The undoable action object contents, by key:
|
|||||||
filter state are entirely at the discretion of the action filter.
|
filter state are entirely at the discretion of the action filter.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
import { Reducer, AnyAction } from "redux";
|
||||||
import fromEntries from "../util/fromEntries";
|
import fromEntries from "../util/fromEntries";
|
||||||
|
|
||||||
const historyKeyPrefix = "@@undoable/";
|
export const pastKey = "@@undoable/past";
|
||||||
const pastKey = `${historyKeyPrefix}past`;
|
export const futureKey = "@@undoable/future";
|
||||||
const futureKey = `${historyKeyPrefix}future`;
|
export const filterStateKey = "@@undoable/filterState";
|
||||||
const filterStateKey = `${historyKeyPrefix}filterState`;
|
export const filterActionKey = "@@undoable/filterAction";
|
||||||
const filterActionKey = `${historyKeyPrefix}filterAction`;
|
export const pendingKey = "@@undoable/pending";
|
||||||
const pendingKey = `${historyKeyPrefix}pending`;
|
|
||||||
const defaultHistoryLimit = -100;
|
const defaultHistoryLimit = -100;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
export interface UndoableFilterState {
|
||||||
const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
[name: string]: unknown;
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'debug' does not exist on type '{}'.
|
}
|
||||||
const { debug } = options;
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'historyLimit' does not exist on type '{}... Remove this comment to see the full error message
|
export interface UndoableConfig<FilterStateType extends UndoableFilterState> {
|
||||||
|
debug?: boolean | number;
|
||||||
|
historyLimit?: number;
|
||||||
|
actionFilter?: ActionFilterFn<FilterStateType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UndoableAction<FilterStateType extends UndoableFilterState> {
|
||||||
|
[filterActionKey]: string;
|
||||||
|
[filterStateKey]?: FilterStateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActionFilterFn<FilterStateType extends UndoableFilterState> = (
|
||||||
|
undoableState: UndoableState<FilterStateType>,
|
||||||
|
action: AnyAction,
|
||||||
|
filterState?: FilterStateType
|
||||||
|
) => UndoableAction<FilterStateType>;
|
||||||
|
|
||||||
|
export interface UndoableState<FilterStateType extends UndoableFilterState> {
|
||||||
|
[pastKey]: [string, unknown][][];
|
||||||
|
[futureKey]: [string, unknown][][];
|
||||||
|
[pendingKey]: [string, unknown][] | null;
|
||||||
|
[filterStateKey]: FilterStateType | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Undoable = <FilterStateType extends UndoableFilterState>(
|
||||||
|
reducer: Reducer,
|
||||||
|
undoableKeys: string[],
|
||||||
|
options: UndoableConfig<FilterStateType> = {}
|
||||||
|
): Reducer => {
|
||||||
|
const debug = options?.debug ?? false;
|
||||||
let { historyLimit } = options;
|
let { historyLimit } = options;
|
||||||
if (!historyLimit) historyLimit = defaultHistoryLimit;
|
if (!historyLimit) historyLimit = defaultHistoryLimit;
|
||||||
if (historyLimit > 0) historyLimit = -historyLimit;
|
if (historyLimit > 0) historyLimit = -historyLimit;
|
||||||
const actionFilter =
|
const actionFilter: ActionFilterFn<FilterStateType> =
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
options?.actionFilter ?? (() => ({ [filterActionKey]: "save" }));
|
||||||
(options as any).actionFilter || (() => ({ [filterActionKey]: "save" }));
|
|
||||||
|
|
||||||
if (!Array.isArray(undoableKeys) || undoableKeys.length === 0)
|
if (!Array.isArray(undoableKeys) || undoableKeys.length === 0)
|
||||||
throw new Error("undoable keys array must be specified");
|
throw new Error("undoable keys array must be specified");
|
||||||
@@ -80,8 +108,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Undo the current to previous history
|
Undo the current to previous history
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function undo(
|
||||||
function undo(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey];
|
const past = currentState[pastKey];
|
||||||
const future = currentState[futureKey];
|
const future = currentState[futureKey];
|
||||||
if (past.length === 0) return currentState;
|
if (past.length === 0) return currentState;
|
||||||
@@ -89,7 +118,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
undoableKeysSet.has(kv[0])
|
undoableKeysSet.has(kv[0])
|
||||||
);
|
);
|
||||||
const newPast = [...past];
|
const newPast = [...past];
|
||||||
const newState = newPast.pop();
|
const newState = newPast.pop() || [];
|
||||||
const newFuture = push(future, currentUndoableState);
|
const newFuture = push(future, currentUndoableState);
|
||||||
const nextState = {
|
const nextState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -104,8 +133,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Replay future, previously undone.
|
Replay future, previously undone.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function redo(
|
||||||
function redo(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey] || [];
|
const past = currentState[pastKey] || [];
|
||||||
const future = currentState[futureKey] || [];
|
const future = currentState[futureKey] || [];
|
||||||
if (future.length === 0) return currentState;
|
if (future.length === 0) return currentState;
|
||||||
@@ -113,7 +143,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
undoableKeysSet.has(kv[0])
|
undoableKeysSet.has(kv[0])
|
||||||
);
|
);
|
||||||
const newFuture = [...future];
|
const newFuture = [...future];
|
||||||
const newState = newFuture.pop();
|
const newState = newFuture.pop() || [];
|
||||||
const newPast = push(past, currentUndoableState);
|
const newPast = push(past, currentUndoableState);
|
||||||
const nextState = {
|
const nextState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -128,13 +158,14 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Clear the history state. No side-effects on current state.
|
Clear the history state. No side-effects on current state.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function clear(
|
||||||
function clear(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
[pastKey]: [],
|
[pastKey]: [],
|
||||||
[futureKey]: [],
|
[futureKey]: [],
|
||||||
[filterStateKey]: {},
|
[filterStateKey]: undefined,
|
||||||
[pendingKey]: null,
|
[pendingKey]: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -142,8 +173,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Reduce current action, with no history side-effects
|
Reduce current action, with no history side-effects
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function skip(
|
||||||
function skip(currentState: any, action: any, filterState: any) {
|
currentState: UndoableState<FilterStateType>,
|
||||||
|
action: AnyAction,
|
||||||
|
filterState: UndoableFilterState
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey] || [];
|
const past = currentState[pastKey] || [];
|
||||||
const future = currentState[futureKey] || [];
|
const future = currentState[futureKey] || [];
|
||||||
const pending = currentState[pendingKey];
|
const pending = currentState[pendingKey];
|
||||||
@@ -160,8 +194,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Save current state in the history, then reduce action.
|
Save current state in the history, then reduce action.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function save(
|
||||||
function save(currentState: any, action: any, filterState: any) {
|
currentState: UndoableState<FilterStateType>,
|
||||||
|
action: AnyAction,
|
||||||
|
filterState: UndoableFilterState
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey] || [];
|
const past = currentState[pastKey] || [];
|
||||||
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
||||||
undoableKeysSet.has(kv[0])
|
undoableKeysSet.has(kv[0])
|
||||||
@@ -181,8 +218,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Save current state as pending history change. No other side effects.
|
Save current state as pending history change. No other side effects.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function stashPending(
|
||||||
function stashPending(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
||||||
undoableKeysSet.has(kv[0])
|
undoableKeysSet.has(kv[0])
|
||||||
);
|
);
|
||||||
@@ -195,8 +233,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Cancel pending history state change. No other side effects.
|
Cancel pending history state change. No other side effects.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function cancelPending(
|
||||||
function cancelPending(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
[pendingKey]: null,
|
[pendingKey]: null,
|
||||||
@@ -206,10 +245,12 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Push pending state onto the history stack
|
Push pending state onto the history stack
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function applyPending(
|
||||||
function applyPending(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
const past = currentState[pastKey] || [];
|
): UndoableState<FilterStateType> {
|
||||||
|
const past = currentState[pastKey];
|
||||||
const pendingState = currentState[pendingKey];
|
const pendingState = currentState[pendingKey];
|
||||||
|
if (pendingState === null) return currentState;
|
||||||
const newPast = push(past, pendingState, historyLimit);
|
const newPast = push(past, pendingState, historyLimit);
|
||||||
const nextState = {
|
const nextState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -221,14 +262,13 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
currentState = {
|
currentState: UndoableState<FilterStateType> = {
|
||||||
[pastKey]: [],
|
[pastKey]: [],
|
||||||
[futureKey]: [],
|
[futureKey]: [],
|
||||||
[filterStateKey]: {},
|
[filterStateKey]: undefined,
|
||||||
[pendingKey]: null,
|
[pendingKey]: null,
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
action: AnyAction
|
||||||
action: any
|
|
||||||
) => {
|
) => {
|
||||||
if (debug > 1) console.log("---- ACTION", action.type);
|
if (debug > 1) console.log("---- ACTION", action.type);
|
||||||
const aType = action.type;
|
const aType = action.type;
|
||||||
@@ -288,8 +328,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function push<T = unknown>(arr: T[], val: T, limit?: number) {
|
||||||
function push(arr: any, val: any, limit = undefined) {
|
|
||||||
/*
|
/*
|
||||||
functional array push, with a max length limit to the new array.
|
functional array push, with a max length limit to the new array.
|
||||||
Like Array.push, except it returns new array and discards as needed
|
Like Array.push, except it returns new array and discards as needed
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import StateMachine from "../util/statemachine";
|
import { AnyAction } from "redux";
|
||||||
|
import { StateMachine, FsmActionFn, FsmErrorFn } from "../util/statemachine";
|
||||||
|
import {
|
||||||
|
UndoableConfig,
|
||||||
|
UndoableState,
|
||||||
|
UndoableFilterState,
|
||||||
|
UndoableAction,
|
||||||
|
filterActionKey,
|
||||||
|
filterStateKey,
|
||||||
|
} from "./undoable";
|
||||||
import createFsmTransitions from "./undoableFsm";
|
import createFsmTransitions from "./undoableFsm";
|
||||||
|
|
||||||
const actionKey = "@@undoable/filterAction";
|
|
||||||
const stateKey = "@@undoable/filterState";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
these actions will not affect history
|
these actions will not affect history
|
||||||
*/
|
*/
|
||||||
const skipOnActions = new Set([
|
const skipOnActions = new Set<string>([
|
||||||
"annoMatrix: init complete",
|
"annoMatrix: init complete",
|
||||||
"url changed",
|
"url changed",
|
||||||
"initial data load start",
|
"initial data load start",
|
||||||
@@ -58,12 +64,12 @@ const skipOnActions = new Set([
|
|||||||
identical, repeated occurances of these action types will be debounced.
|
identical, repeated occurances of these action types will be debounced.
|
||||||
Entire action must be identical (all keys).
|
Entire action must be identical (all keys).
|
||||||
*/
|
*/
|
||||||
const debounceOnActions = new Set([]);
|
const debounceOnActions = new Set<string>([]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
history will be cleared when these actions occur
|
history will be cleared when these actions occur
|
||||||
*/
|
*/
|
||||||
const clearOnActions = new Set([
|
const clearOnActions = new Set<string>([
|
||||||
"initial data load complete",
|
"initial data load complete",
|
||||||
"initial data load error",
|
"initial data load error",
|
||||||
]);
|
]);
|
||||||
@@ -71,7 +77,7 @@ const clearOnActions = new Set([
|
|||||||
/*
|
/*
|
||||||
An immediate history save will be done for these
|
An immediate history save will be done for these
|
||||||
*/
|
*/
|
||||||
const saveOnActions = new Set([
|
const saveOnActions = new Set<string>([
|
||||||
"categorical metadata filter select",
|
"categorical metadata filter select",
|
||||||
"categorical metadata filter deselect",
|
"categorical metadata filter deselect",
|
||||||
"categorical metadata filter all of these",
|
"categorical metadata filter all of these",
|
||||||
@@ -119,35 +125,43 @@ StateMachine - processing complex action handling - see FSM graph for
|
|||||||
actual structure, in undoableFsm.js
|
actual structure, in undoableFsm.js
|
||||||
**/
|
**/
|
||||||
|
|
||||||
|
interface MyFilterState extends UndoableFilterState {
|
||||||
|
prevAction?: AnyAction;
|
||||||
|
fsm: StateMachine<MyUndoableAction> | null;
|
||||||
|
}
|
||||||
|
type MyUndoableAction = UndoableAction<MyFilterState>;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Default FSM actions. Used to side-effect transitions in the graph.
|
Default FSM actions. Used to side-effect transitions in the graph.
|
||||||
See graph definition for the transitions that use each.
|
See graph definition for the transitions that use each.
|
||||||
|
|
||||||
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
|
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const stashPending: FsmActionFn<MyUndoableAction> = (
|
||||||
const stashPending = (fsm: any) => ({
|
fsm: StateMachine<MyUndoableAction>
|
||||||
[actionKey]: "stashPending",
|
) => ({
|
||||||
[stateKey]: { fsm },
|
[filterActionKey]: "stashPending",
|
||||||
|
[filterStateKey]: { fsm },
|
||||||
});
|
});
|
||||||
const cancelPending = () => ({
|
const cancelPending: FsmActionFn<MyUndoableAction> = () => ({
|
||||||
[actionKey]: "cancelPending",
|
[filterActionKey]: "cancelPending",
|
||||||
[stateKey]: { fsm: null },
|
[filterStateKey]: { fsm: null },
|
||||||
});
|
});
|
||||||
const applyPending = () => ({
|
const applyPending: FsmActionFn<MyUndoableAction> = () => ({
|
||||||
[actionKey]: "applyPending",
|
[filterActionKey]: "applyPending",
|
||||||
[stateKey]: { fsm: null },
|
[filterStateKey]: { fsm: null },
|
||||||
});
|
});
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type.
|
const skip: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
|
||||||
const skip = (fsm, transition) => ({
|
[filterActionKey]: "skip",
|
||||||
[actionKey]: "skip",
|
[filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
|
||||||
});
|
});
|
||||||
const clear = () => ({ [actionKey]: "clear", [stateKey]: { fsm: null } });
|
const clear: FsmActionFn<MyUndoableAction> = () => ({
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type.
|
[filterActionKey]: "clear",
|
||||||
const save = (fsm, transition) => ({
|
[filterStateKey]: { fsm: null },
|
||||||
[actionKey]: "save",
|
});
|
||||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
const save: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
|
||||||
|
[filterActionKey]: "save",
|
||||||
|
[filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -156,11 +170,13 @@ StateMachine when it doesn't know what to do.
|
|||||||
|
|
||||||
Signature: (fsm, event, from) => undoableAction
|
Signature: (fsm, event, from) => undoableAction
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const onFsmError: FsmErrorFn<MyUndoableAction> = (fsm, event, from) => {
|
||||||
const onFsmError = (fsm: any, event: any, from: any) => {
|
|
||||||
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
|
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
|
||||||
// In production, try to recover gracefully if we have unexpected state
|
// In production, try to recover gracefully if we have unexpected state
|
||||||
return clear();
|
return {
|
||||||
|
[filterActionKey]: "clear",
|
||||||
|
[filterStateKey]: { fsm: null },
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -175,7 +191,11 @@ const fsmTransitions = createFsmTransitions(
|
|||||||
save
|
save
|
||||||
);
|
);
|
||||||
/* State machine we clone whenever we need to run it */
|
/* State machine we clone whenever we need to run it */
|
||||||
const seedFsm = new StateMachine("init", fsmTransitions, onFsmError);
|
const seedFsm = new StateMachine<MyUndoableAction>(
|
||||||
|
"init",
|
||||||
|
fsmTransitions,
|
||||||
|
onFsmError
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
See undoable.js for description action filter interface description.
|
See undoable.js for description action filter interface description.
|
||||||
@@ -185,53 +205,52 @@ Basic approach:
|
|||||||
* only implement complex state machines where absolutely required (eg,
|
* only implement complex state machines where absolutely required (eg,
|
||||||
multi-event selection and the like)
|
multi-event selection and the like)
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const actionFilter =
|
||||||
const actionFilter = (debug: any) => (
|
(debug: boolean) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(
|
||||||
state: any,
|
state: UndoableState<MyFilterState>,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
action: AnyAction,
|
||||||
action: any,
|
prevFilterState: MyFilterState | undefined
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
): UndoableAction<MyFilterState> => {
|
||||||
prevFilterState: any
|
const actionType = action.type;
|
||||||
) => {
|
prevFilterState = prevFilterState || { fsm: null };
|
||||||
const actionType = action.type;
|
const filterState: MyFilterState = {
|
||||||
const filterState = {
|
...prevFilterState,
|
||||||
...prevFilterState,
|
prevAction: action,
|
||||||
prevAction: action,
|
};
|
||||||
};
|
if (skipOnActions.has(actionType)) {
|
||||||
if (skipOnActions.has(actionType)) {
|
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
}
|
||||||
}
|
if (
|
||||||
if (
|
debounceOnActions.has(actionType) &&
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
prevFilterState.prevAction &&
|
||||||
debounceOnActions.has(actionType) &&
|
shallowObjectEq(action, prevFilterState.prevAction)
|
||||||
shallowObjectEq(action, prevFilterState.prevAction)
|
) {
|
||||||
) {
|
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
}
|
||||||
}
|
if (clearOnActions.has(actionType)) {
|
||||||
if (clearOnActions.has(actionType)) {
|
return { [filterActionKey]: "clear", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "clear", [stateKey]: filterState };
|
}
|
||||||
}
|
if (saveOnActions.has(actionType)) {
|
||||||
if (saveOnActions.has(actionType)) {
|
return { [filterActionKey]: "save", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "save", [stateKey]: filterState };
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Else, something more complex OR unknown to us....
|
Else, something more complex OR unknown to us....
|
||||||
*/
|
*/
|
||||||
if (seedFsm.events.has(actionType)) {
|
if (seedFsm.events.has(actionType)) {
|
||||||
let { fsm } = filterState;
|
let { fsm } = filterState;
|
||||||
if (!fsm) {
|
if (!fsm) {
|
||||||
/* no active FSM, so create one in init state */
|
/* no active FSM, so create one in init state */
|
||||||
fsm = seedFsm.clone("init");
|
fsm = seedFsm.clone("init");
|
||||||
|
}
|
||||||
|
return fsm.next(action.type, { state, action });
|
||||||
}
|
}
|
||||||
return fsm.next(action.type, { state, action });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* else, we have no idea what this is - skip it */
|
/* else, we have no idea what this is - skip it */
|
||||||
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
return true if objA and objB are ===, OR if:
|
return true if objA and objB are ===, OR if:
|
||||||
@@ -239,8 +258,10 @@ return true if objA and objB are ===, OR if:
|
|||||||
- have same own properties
|
- have same own properties
|
||||||
- all values are strict equal (===)
|
- all values are strict equal (===)
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function shallowObjectEq(
|
||||||
function shallowObjectEq(objA: any, objB: any) {
|
objA: Record<string | number | symbol, unknown>,
|
||||||
|
objB: Record<string | number | symbol, unknown>
|
||||||
|
) {
|
||||||
if (objA === objB) return true;
|
if (objA === objB) return true;
|
||||||
if (!objA || !objB) return false;
|
if (!objA || !objB) return false;
|
||||||
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
|
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
|
||||||
@@ -252,8 +273,7 @@ function shallowObjectEq(objA: any, objB: any) {
|
|||||||
return true if arrA and arrB contain the same strict-equal values,
|
return true if arrA and arrB contain the same strict-equal values,
|
||||||
in the same order.
|
in the same order.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function shallowArrayEq(arrA: unknown[], arrB: unknown[]) {
|
||||||
function shallowArrayEq(arrA: any, arrB: any) {
|
|
||||||
if (arrA.length !== arrB.length) return false;
|
if (arrA.length !== arrB.length) return false;
|
||||||
for (let i = 0, l = arrA.length; i < l; i += 1) {
|
for (let i = 0, l = arrA.length; i < l; i += 1) {
|
||||||
if (arrA[i] !== arrB[i]) return false;
|
if (arrA[i] !== arrB[i]) return false;
|
||||||
@@ -268,7 +288,7 @@ Set to true or 1 for base logging, high number for more verbosity (currently onl
|
|||||||
or 2).
|
or 2).
|
||||||
*/
|
*/
|
||||||
const debug = false;
|
const debug = false;
|
||||||
const undoableConfig = {
|
const undoableConfig: UndoableConfig<MyFilterState> = {
|
||||||
debug,
|
debug,
|
||||||
historyLimit: 50, // maximum history size
|
historyLimit: 50, // maximum history size
|
||||||
actionFilter: actionFilter(debug),
|
actionFilter: actionFilter(debug),
|
||||||
@@ -307,7 +327,7 @@ if (debug) {
|
|||||||
);
|
);
|
||||||
if (trivialOverlapWithFsm.size > 0) {
|
if (trivialOverlapWithFsm.size > 0) {
|
||||||
console.error(
|
console.error(
|
||||||
"Undoable misconfiguration - trivival action filter blocking FSM filter",
|
"Undoable misconfiguration - trivial action filter blocking FSM filter",
|
||||||
[...trivialOverlapWithFsm]
|
[...trivialOverlapWithFsm]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,22 +18,16 @@ b) compound actions that should be collapsed into a single history change.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
import { StateMachine, FsmTransition, FsmActionFn } from "../util/statemachine";
|
||||||
const createFsmTransitions = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
const createFsmTransitions = <ActionReturnType>(
|
||||||
stashPending: any,
|
stashPending: FsmActionFn<ActionReturnType>,
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
cancelPending: FsmActionFn<ActionReturnType>,
|
||||||
cancelPending: any,
|
applyPending: FsmActionFn<ActionReturnType>,
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
skip: FsmActionFn<ActionReturnType>,
|
||||||
applyPending: any,
|
_clear: FsmActionFn<ActionReturnType>,
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
save: FsmActionFn<ActionReturnType>
|
||||||
skip: any,
|
): FsmTransition<ActionReturnType>[] => [
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'clear' is declared but its value is never read.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
clear: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
save: any
|
|
||||||
) => [
|
|
||||||
/* graph selection brushing */
|
/* graph selection brushing */
|
||||||
{
|
{
|
||||||
event: "graph brush start",
|
event: "graph brush start",
|
||||||
@@ -52,12 +46,14 @@ const createFsmTransitions = (
|
|||||||
from: "graph brush in progress",
|
from: "graph brush in progress",
|
||||||
to: "done",
|
to: "done",
|
||||||
/* if current selection is all, cancelPending. Else, applyPending */
|
/* if current selection is all, cancelPending. Else, applyPending */
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read.
|
action: (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fsm: StateMachine<ActionReturnType>,
|
||||||
action: (fsm: any, transition: any, data: any) =>
|
transition: FsmTransition<ActionReturnType>,
|
||||||
|
data: any // eslint-disable-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. Requires state typing.
|
||||||
|
) =>
|
||||||
data.state.graphSelection.selection.mode === "all"
|
data.state.graphSelection.selection.mode === "all"
|
||||||
? cancelPending()
|
? cancelPending(fsm, transition, data)
|
||||||
: applyPending(),
|
: applyPending(fsm, transition, data),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: "graph brush end",
|
event: "graph brush end",
|
||||||
@@ -84,12 +80,14 @@ const createFsmTransitions = (
|
|||||||
from: "graph lasso in progress",
|
from: "graph lasso in progress",
|
||||||
to: "done",
|
to: "done",
|
||||||
/* if current selection is all, cancelPending. Else, applyPending */
|
/* if current selection is all, cancelPending. Else, applyPending */
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read.
|
action: (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fsm: StateMachine<ActionReturnType>,
|
||||||
action: (fsm: any, transition: any, data: any) =>
|
transition: FsmTransition<ActionReturnType>,
|
||||||
|
data: any // eslint-disable-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. Requires state typing.
|
||||||
|
) =>
|
||||||
data.state.graphSelection.selection.mode === "all"
|
data.state.graphSelection.selection.mode === "all"
|
||||||
? cancelPending()
|
? cancelPending(fsm, transition, data)
|
||||||
: applyPending(),
|
: applyPending(fsm, transition, data),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: "graph lasso end",
|
event: "graph lasso end",
|
||||||
|
|||||||
@@ -13,18 +13,17 @@ Where:
|
|||||||
to: state_name_transitioning_to,
|
to: state_name_transitioning_to,
|
||||||
from: state_name_transitioning_from,
|
from: state_name_transitioning_from,
|
||||||
event: value_that_will_cause_transition,
|
event: value_that_will_cause_transition,
|
||||||
action: optional_callback_upon_transition
|
action: callback_upon_transition
|
||||||
}
|
}
|
||||||
The transition will be provided to the action callback, so other data
|
The transition will be provided to the action callback, so other data
|
||||||
may be stored in the transition object for use by the action callback.
|
may be stored in the transition object for use by the action callback.
|
||||||
* onErrorCallback - a callback function called if the FSM receives an event
|
* onErrorCallback - a callback function called if the FSM receives an event
|
||||||
for which it has no defined transition.
|
for which it has no defined transition.
|
||||||
|
|
||||||
|
|
||||||
Interface:
|
Interface:
|
||||||
* states - property containing the state names. A Set(), contianing the
|
* states - property containing the state names. A Set(), containing the
|
||||||
union of to: and from: values.
|
union of to: and from: values.
|
||||||
* events - property containing all of the accepted event values. Set().
|
* events - property containing all of the accepted event values. Set().
|
||||||
* graph - a Map of Maps, organized as graph[eventValue][fromStateValue]
|
* graph - a Map of Maps, organized as graph[eventValue][fromStateValue]
|
||||||
* clone() - clone the entire statemachine.
|
* clone() - clone the entire statemachine.
|
||||||
* next(eventValue) - drive the FSM to the next state. If the event
|
* next(eventValue) - drive the FSM to the next state. If the event
|
||||||
@@ -40,47 +39,69 @@ Example:
|
|||||||
const fsm = new StateMachine("A", transitions, () => { throw new Error("oops") });
|
const fsm = new StateMachine("A", transitions, () => { throw new Error("oops") });
|
||||||
fsm.next("yo"); // returns 42
|
fsm.next("yo"); // returns 42
|
||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export default class StateMachine {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
events: any;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
export type FsmState = number | string;
|
||||||
graph: any;
|
export type FsmEvent = string; // by convention, we assume Events are redux action types, aka strings
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
export type FsmActionFn<ActionReturnType> = (
|
||||||
onError: any;
|
fsm: StateMachine<ActionReturnType>,
|
||||||
|
transition: FsmTransition<ActionReturnType>,
|
||||||
|
data: unknown
|
||||||
|
) => ActionReturnType;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
export interface FsmTransition<ActionReturnType> {
|
||||||
state: any;
|
from: FsmState;
|
||||||
|
to: FsmState;
|
||||||
|
event: FsmEvent;
|
||||||
|
action: FsmActionFn<ActionReturnType>;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
export type FsmErrorFn<ActionReturnType> = (
|
||||||
states: any;
|
fsm: StateMachine<ActionReturnType>,
|
||||||
|
event: FsmEvent,
|
||||||
|
state: FsmState
|
||||||
|
) => ActionReturnType;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
export class StateMachine<ActionReturnType> {
|
||||||
constructor(initState: any, transitions: any, onError: any) {
|
events: Set<FsmEvent>;
|
||||||
this.onError = onError || (() => undefined);
|
|
||||||
|
graph: Map<FsmEvent, Map<FsmState, FsmTransition<ActionReturnType>>>;
|
||||||
|
|
||||||
|
onError: FsmErrorFn<ActionReturnType>;
|
||||||
|
|
||||||
|
state: FsmState;
|
||||||
|
|
||||||
|
states: Set<FsmState>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
initState: FsmState,
|
||||||
|
transitions: FsmTransition<ActionReturnType>[],
|
||||||
|
onError: FsmErrorFn<ActionReturnType>
|
||||||
|
) {
|
||||||
|
this.onError = onError;
|
||||||
this.state = initState;
|
this.state = initState;
|
||||||
|
|
||||||
// all states
|
// all states
|
||||||
this.states = new Set(
|
this.states = new Set(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
transitions.reduce(
|
||||||
transitions.reduce((names: any, tsn: any) => {
|
(names: Array<FsmState>, tsn: FsmTransition<ActionReturnType>) => {
|
||||||
names.push(tsn.from);
|
names.push(tsn.from);
|
||||||
names.push(tsn.to);
|
names.push(tsn.to);
|
||||||
return names;
|
return names;
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// all transition names (aka events)
|
// all transition names (aka events)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
this.events = new Set(
|
||||||
this.events = new Set(transitions.map((tsn: any) => tsn.event));
|
transitions.map((tsn: FsmTransition<ActionReturnType>) => tsn.event)
|
||||||
|
);
|
||||||
|
|
||||||
// the transition graph.
|
// the transition graph.
|
||||||
// graph[event][from] -> transition
|
// graph[event][from] -> transition
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
this.graph = transitions.reduce((graph, tsn) => {
|
||||||
this.graph = transitions.reduce((graph: any, tsn: any) => {
|
|
||||||
const { event, from } = tsn;
|
const { event, from } = tsn;
|
||||||
if (!graph.has(event)) graph.set(event, new Map());
|
if (!graph.has(event)) graph.set(event, new Map());
|
||||||
const tsnMap = graph.get(event);
|
const tsnMap = graph.get(event);
|
||||||
@@ -89,29 +110,23 @@ export default class StateMachine {
|
|||||||
}, new Map());
|
}, new Map());
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
clone(initState: FsmState): StateMachine<ActionReturnType> {
|
||||||
clone(initState: any) {
|
const fsm = new StateMachine<ActionReturnType>(initState, [], this.onError);
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
|
||||||
const fsm = new StateMachine(initState, []);
|
|
||||||
fsm.onError = this.onError;
|
|
||||||
fsm.states = this.states;
|
fsm.states = this.states;
|
||||||
fsm.events = this.events;
|
fsm.events = this.events;
|
||||||
fsm.graph = this.graph;
|
fsm.graph = this.graph;
|
||||||
return fsm;
|
return fsm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
next(event: FsmEvent, data: unknown): ActionReturnType {
|
||||||
next(event: any, data: any) {
|
|
||||||
const { graph, state } = this;
|
const { graph, state } = this;
|
||||||
const tsnMap = graph.get(event);
|
const tsnMap = graph.get(event);
|
||||||
if (!tsnMap) return this.onError(this, event, state, undefined);
|
if (!tsnMap) return this.onError(this, event, state);
|
||||||
|
|
||||||
const transition = tsnMap.get(state);
|
const transition = tsnMap.get(state);
|
||||||
if (!transition) return this.onError(this, event, state, undefined);
|
if (!transition) return this.onError(this, event, state);
|
||||||
|
|
||||||
this.state = transition.to;
|
this.state = transition.to;
|
||||||
return transition.action
|
return transition.action(this, transition, data);
|
||||||
? transition.action(this, transition, data)
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user