Simplify custom Actions: universal Action predicates instead of

action-specific guards.
This commit is contained in:
Daniel J. Geiger 2023-01-27 13:23:40 -06:00
parent 512e506798
commit c8d4e8c421
9 changed files with 105 additions and 193 deletions

View file

@ -1,25 +0,0 @@
import { Action, ActionName, DisableFn, EnableFn } from "./types";
const disablers = {} as Record<ActionName, DisableFn[]>;
const enablers = {} as Record<Action["name"], EnableFn[]>;
export const getActionDisablers = () => disablers;
export const getActionEnablers = () => enablers;
export const registerDisableFn = (name: ActionName, disabler: DisableFn) => {
if (!(name in disablers)) {
disablers[name] = [] as DisableFn[];
}
if (!disablers[name].includes(disabler)) {
disablers[name].push(disabler);
}
};
export const registerEnableFn = (name: Action["name"], enabler: EnableFn) => {
if (!(name in enablers)) {
enablers[name] = [] as EnableFn[];
}
if (!enablers[name].includes(enabler)) {
enablers[name].push(enabler);
}
};

View file

@ -2,15 +2,12 @@ import React from "react";
import {
Action,
UpdaterFn,
ActionName,
ActionResult,
PanelComponentProps,
ActionSource,
DisableFn,
EnableFn,
ActionPredicateFn,
isActionName,
} from "./types";
import { getActionDisablers, getActionEnablers } from "./guards";
import { ExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
import { trackEvent } from "../analytics";
@ -44,10 +41,8 @@ const trackAction = (
};
export class ActionManager {
actions = {} as Record<ActionName | Action["name"], Action>;
disablers = {} as Record<ActionName, DisableFn[]>;
enablers = {} as Record<Action["name"], EnableFn[]>;
actions = {} as Record<Action["name"], Action>;
actionPredicates = [] as ActionPredicateFn[];
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
@ -75,36 +70,9 @@ export class ActionManager {
this.app = app;
}
registerActionGuards() {
const disablers = getActionDisablers();
for (const d in disablers) {
const dName = d as ActionName;
disablers[dName].forEach((disabler) =>
this.registerDisableFn(dName, disabler),
);
}
const enablers = getActionEnablers();
for (const e in enablers) {
const eName = e as Action["name"];
enablers[e].forEach((enabler) => this.registerEnableFn(eName, enabler));
}
}
registerDisableFn(name: ActionName, disabler: DisableFn) {
if (!(name in this.disablers)) {
this.disablers[name] = [] as DisableFn[];
}
if (!this.disablers[name].includes(disabler)) {
this.disablers[name].push(disabler);
}
}
registerEnableFn(name: Action["name"], enabler: EnableFn) {
if (!(name in this.enablers)) {
this.enablers[name] = [] as EnableFn[];
}
if (!this.enablers[name].includes(enabler)) {
this.enablers[name].push(enabler);
registerActionPredicate(predicate: ActionPredicateFn) {
if (!this.actionPredicates.includes(predicate)) {
this.actionPredicates.push(predicate);
}
}
@ -196,10 +164,7 @@ export class ActionManager {
/**
* @param data additional data sent to the PanelComponent
*/
renderAction = (
name: ActionName | Action["name"],
data?: PanelComponentProps["data"],
) => {
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
const canvasActions = this.app.props.UIOptions.canvasActions;
if (
@ -243,7 +208,7 @@ export class ActionManager {
};
isActionEnabled = (
action: Action | ActionName,
action: Action,
opts?: {
elements?: readonly ExcalidrawElement[];
data?: Record<string, any>;
@ -254,29 +219,19 @@ export class ActionManager {
const appState = this.getAppState();
const data = opts?.data;
const _action = isActionName(action) ? this.actions[action] : action;
if (
!opts?.guardsOnly &&
_action.predicate &&
!_action.predicate(elements, appState, this.app.props, this.app, data)
action.predicate &&
!action.predicate(elements, appState, this.app.props, this.app, data)
) {
return false;
}
if (isActionName(_action.name)) {
return !(
_action.name in this.disablers &&
this.disablers[_action.name].some((fn) =>
fn(elements, appState, _action.name as ActionName),
)
);
}
return (
_action.name in this.enablers &&
this.enablers[_action.name].some((fn) =>
fn(elements, appState, _action.name),
)
);
let enabled = true;
this.actionPredicates.forEach((fn) => {
if (!fn(action, elements, appState, data)) {
enabled = false;
}
});
return enabled;
};
}

View file

@ -1,14 +1,8 @@
import { Action, isActionName } from "./types";
import { Action } from "./types";
let actions: readonly Action[] = [];
let customActions: readonly Action[] = [];
export const getCustomActions = () => customActions;
export const getActions = () => actions;
export let actions: readonly Action[] = [];
export const register = <T extends Action>(action: T) => {
if (!isActionName(action.name)) {
customActions = customActions.concat(action);
}
actions = actions.concat(action);
return action as T & {
keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"];

View file

@ -31,20 +31,13 @@ type ActionFn = (
app: AppClassProperties,
) => ActionResult | Promise<ActionResult>;
// Return `true` to indicate the standard Action with name `actionName`
// should be disabled given `elements` and `appState`.
export type DisableFn = (
// Return `true` *unless* `Action` should be disabled
// given `elements`, `appState`, and optionally `data`.
export type ActionPredicateFn = (
action: Action,
elements: readonly ExcalidrawElement[],
appState: AppState,
actionName: ActionName,
) => boolean;
// Return `true` to indicate the custom Action with name `actionName`
// should be enabled given `elements` and `appState`.
export type EnableFn = (
elements: readonly ExcalidrawElement[],
appState: AppState,
actionName: Action["name"],
data?: Record<string, any>,
) => boolean;
export type UpdaterFn = (res: ActionResult) => void;