历史记录

Undo/Redo 是 FlowGram.AI 的一个插件,在 @flowgram.ai/fixed-layout-editor 和 @flowgram.ai/free-layout-editor 两种模式的编辑器中均有提供该功能。

1. 快速开始

> Demo Detail

1.1. 开启 history

使用 Undo/Redo 功能前需要先引入编辑器,以固定布局编辑器为例。

  1. package.json 添加依赖
use-editor-props.tsx
export function useEditorProps() {
  return useMemo(
    () => ({
      history: {
        enable: true,
        enableChangeNode: true // Listen Node engine data change
      }
    })
  )
}

开启之后将获得以下能力:

简介描述自由布局固定布局
Undo/Redo 快捷键画布上使用 Cmd/Ctrl + Z 触发 Undo
画布上使用 Cmd/Ctrl + Shift + Z 触发 Redo
画布节点操作支持undo/redo增删节点
增删连线
移动节点
增删分支
移动分支
添加分组
取消分组
画布批量操作删除节点
移动节点

1.2. 关闭 history

如果某些系统触发的数据变更不希望被undo redo监听到,可以主动关掉 历史服务 操作完数据再重新启动

const { history } = useClientContext();

history.stop()
// 做一些不希望被捕获的操作, 这些变更不会被记录到操作栈
...
history.start()

1.3. History Undo/Redo 合并


const { history } = useClientContext();

history.startTransaction();

// 这里的 任意操作都会被合并成一个
...

history.endTransaction();

1.4. Undo/Redo 调用

一般 Undo/Redo 会在界面上提供两个按钮入口,点击了能触发 Undo 和 Redo,按钮本身需要有是否可以 Undo/Redo 的状态。

export function useUndoRedo(): UndoRedo {
  const { history } = useClientContext();
  const [canUndo, setCanUndo] = useState(false);
  const [canRedo, setCanRedo] = useState(false);

  useEffect(() => {
    const toDispose = history.undoRedoService.onChange(() => {
      setCanUndo(history.canUndo());
      setCanRedo(history.canRedo());
    });
    return () => {
      toDispose.dispose();
    };
  }, []);

  return {
    canUndo,
    canRedo,
    undo: () => history.undo(),
    redo: () => history.redo(),
  };
}

2. 功能扩展

2.1. 操作注册

操作通过 operationMetas 去注册操作

use-editor-props.tsx
...
history={{
  enable: true,
  operationMetas: [
    {
        type: 'addNode',
        apply: () => { console.log('addNode')},
        inverse: (op) => ({ type: 'deleteNode', value: op.value })
    }
  ]
}}

OperationMeta 核心定义如下

  • type 是操作的唯一标识
  • inverse 是一个函数,该函数返回当前操作的逆操作
  • apply 是操作被触发的时候执行的逻辑
export interface OperationMeta {
  /**
   * 操作类型 需要唯一
   */
  type: string;
  /**
   * 将一个操作转换成另一个逆操作, 如insert转成delete
   * @param op 操作
   * @returns 逆操作
   */
  inverse: (op: Operation) => Operation;
  /**
   * 执行操作
   * @param operation 操作
   */
  apply(operation: Operation, source: any): void | Promise<void>;
}

假设我要做增删节点支持 Undo/Redo 的功能,我就需要添加两个操作

{
  type: 'addNode',
  inverse: op => ({ ...op, type: 'deleteNode' }),
  apply(op, ctx) {
    document = ctx.get(Document)
    document.addNode(op.value)
  },
}
{
  type: 'deleteNode',
  inverse: op => ({ ...op, type: 'addNode' }),
  apply(op, ctx) {
    document = ctx.get(Document)
    document.deleteNode(op.value.id)
  },
}

2.2. 操作合并

operationMeta 支持 shouldMerge 来自定义合并策略,如果频繁触发的操作可以进行合并

shouldMerge 返回
  • 返回 false 代表不合并
  • 返回 true 代表合并进一个操作栈元素
  • 返回 Operation 代表合并成一个操作

以下示例是一个合并 500ms 内对同一个字段编辑进行合并

{
  type: 'changeData',
  inverse: op => ({ ...op, type: 'changeData' }),
  apply(op, ctx) {},
  shouldMerge: (op, prev, element) => {
    // 合并500ms内的操作
    if (Date.now() - element.getTimestamp() < 500) {
      if (
        op.type === prev.type && // 相同类型
        op.value.id === prev.value.id && // 相同节点
        op.value?.path === prev.value?.path // 相同路径
      ) {
        return {
          type: op.type,
          value: {
            ...op.value,
            value: op.value.value,
            oldValue: prev.value.oldValue,
          },
        };
      }
    }
    return false;
  }
}

2.3. 操作执行

  1. 单操作执行

通过 pushOperation 触发, 如下示例使用方在业务中触发刚刚定义的操作

function handleAddNode () {
   const { history } = useClientContext()
   history.pushOperation({
       type: 'addNode',
       value: {
          name: 'xx'
          id: 'xxx'
       }
   })
}
  1. 批量执行 通过 transact 调用的函数中所有执行的操作都会被合并进一个栈元素, undo/redo 的时候会被一起执行 如下是实现了一个批量删除的例子:
function deleteNodes(nodes: FlowNodeEntity[]) {
  const { history } = useClientContext()
  history.transact(() => {
    nodes.forEach(node => {
      history.pushOperation({
        type: OperationType.deleteNode,
        value: {
          fromId: fromNode.id,
          data: node.data,
        },
      });
    });
  });
}

2.4. 撤销重做

  1. 撤销重做 撤销执行 history.undo 方法 重做执行 history.redo 方法
function undo() {
    const { history } = useClientContext();
    history.undo();
}

function redo() {
    const { history } = useClientContext();
    history.redo();
}
  1. 监听撤销重做 监听 undoRedoService.onChange 的 onChange 事件即可 如下是一个 undo/redo 触发后路由对应操作的uri(选中对应节点或表单项)
function listenHistoryChange() {
  const { history } = useClientContext();
  history.undoRedoService.onChange(
    ({ type, element }) => {
      if (type === UndoRedoChangeType.PUSH) {
        return;
      }
      const op = element.getLastOperation();
      if (!op) {
        return;
      }
      if (op.uri) {
        // goto somewhere
      }
    },
  )
}

2.5. 操作历史

  1. 查看刷新 可以通过 HistoryStack.items 获得历史记录, 通过监听 HistoryStack.onChange 事件来刷新界面
import React from 'react';

export function HistoryList() {
  const { historyStack } = useService<HistoryManager>(HistoryManager)
  const { refresh } = useRefresh()
  let items = historyManager.historyStack.items;

  useEffect(() => {
      const disposable = historyStack.onChange(() => {
          refresh()
      ])

      return () => {
          disposable.dispose()
      }
  }, [])

  return (
      <ul>
        {items.map((item, index) => (
          <li key={index}>
            <div>
              {item.type}({item.id}):
              {item.operations.map((o, index) => (
                <Tooltip
                  key={index}
                  title={(o.description || '') + `----uri: ${o.uri?.displayName}`}
                >
                  {o.label || o.type}
                </Tooltip>
              ))}
            </div>

          </li>
        ))}
      </ul>
  );
}
  1. 持久化 持久化是通过 history-storage 插件实现
  • databaseName: 数据库名称
  • resourceStorageLimit: 资源存储限制数量

引入 @flowgram.ai/history-storage 包后,可使用该插件

import { createHistoryStoragePlugin } from '@flowgram.ai/history-storage';

createHistoryStoragePlugin({
    databaseName: 'your-history',
    resourceStorageLimit: 50,
}),

通过 useStorageHistoryItems 查询数据库列表

import {
  useStorageHistoryItems,
} from '@flowgram.ai/history-storage';

export const HistoryList = () => {
  const { uri } = useCurrentWidget();

  const { items } = useStorageHistoryItems(
    storage,
    uri.withoutQuery().toString(),
  );

  return <>
    { JSON.stringify(items) }
  </>
}

3. API 列表

3.1. OperationMeta

操作元数据,用以定义一个操作

3.2. Operation

操作数据,通过 type 和 OperationMeta 关联

3.3. OperationService

onApply 想监听某个触发的操作可以使用onApply

useService(OperationService).onApply((op: Operation) => {
    console.log(op)
    // 此处可以根据type执行自己的业务逻辑
})

3.4. HistoryService

History 模块核心 API 暴露的Service

3.5. UndoRedoService

管理 UndoRedo 栈的服务

3.6. HistoryStack

历史栈,监听所有 push undo redo 操作,并记录到栈里面

3.7. HistoryDatabase

持久化数据库操作