ArchitectureUndo and Redo

Architecture

Undo and Redo

Understand how the undo and redo system works.


Undo and Redo

The undo and redo functionality is implemented in the useWorkflow hook.

// src/shared/kits/workflow/hooks/use-workflow.ts

// ...

function reducer(state: State, action: Action): State {
  // ...
}

function init(schema: WorkflowSchema): State {
  const workflow = toWorkflow(schema);
  return {
    history: {
      workflows: [workflow],
      pointer: 0,
      current: workflow,
    },
    center: null,
  };
}

export function useWorkflow(schema: WorkflowSchema): [State, Dispatch<Action>] {
  return useReducer(reducer, schema, init);
}

This hook returns an object with a history property that contains:

  • workflows: The workflow states used for undo/redo, including past and future states.
  • pointer: The index in workflows representing the current position in the history.
  • current: The currently active workflow state.

The reducer handles both undo/redo logic and the application of workflow changes.

When applying a change, applyWorkflowChange returns a memory property that defines whether that change has to be saved in the history, as shown here.

// src/shared/kits/workflow/hooks/use-workflow.ts

// ...

function reducer(state: State, action: Action): State {
  switch (action.type) {
    /**
     * Applies all incoming workflow changes. Each change may request the resulting
     * workflow to be saved in the history (`memory`), in which case a new entry
     * is added and any forward history (redo states) is discarded.
     */
    case "applyWorkflowChanges": {
      const workflows: Workflow[] = [...state.history.workflows];
      let current: Workflow = state.history.current;
      let pointer: number = state.history.pointer;
      let center: string | null = null;
      for (const change of action.changes) {
        const output = applyWorkflowChange(change, current);
        current = output.workflow;
        if (output.memory) {
          pointer++;
          workflows.splice(pointer, workflows.length - pointer, {
            ...output.workflow,
            widget: null,
            active: null,
          });
        }
        if (output.center) {
          center = output.center;
        }
      }
      return {
        history: { workflows: workflows, pointer, current },
        center: center,
      };
    }

    // ...
  }
}

// ...

This is because not all changes, such as selecting or dragging nodes, need to be saved.