「AntV」5. xflow右键菜单配置(弹窗表单修改节点信息)

366 阅读3分钟

注意:写文章的时候,我用的xflow版本是1.x的

1. 使用

<KeyBindings config={keyBindingConfigs()} />

2.配置

// keyBindingConfigs
import { DeleteOutlined, EditOutlined, StopOutlined, UngroupOutlined } from '@ant-design/icons';
import {
  createCtxMenuConfig,
  IconStore,
  IMenuOptions,
  MenuItemType,
  NsGraph,
} from '@antv/xflow';
import RenameNodeCmd, { RenameNodeCommandArgs } from '../commands/rename';

/** menuitem 配置 */
/** 注册菜单依赖的icon */
IconStore.set('DeleteOutlined', DeleteOutlined);
IconStore.set('EditOutlined', EditOutlined);
IconStore.set('StopOutlined', StopOutlined);
IconStore.set('UngroupOutlined', UngroupOutlined);

// 具体配置参考文档
export const MenuItemConfig: Record<string, IMenuOptions> = {
  RENAME_NODE: {
    id: RenameNodeCmd.command.id,
    label: '重命名',
    iconName: 'EditOutlined',
    // 点击菜单项后触发的逻辑
    onClick: async ({ target, commandService }) => {
      const nodeConfig = target.data as NsGraph.INodeConfig & { extra: NodeExtraData };
      // 一般都是触发某个命令
      commandService.executeCommand<RenameNodeCommandArgs>(RenameNodeCmd.command.id, {
        nodeConfig,
        // 表单参数
        formConfig: {
          name: 'name',
          label: '名称',
        },
      });
    },
  },
};

export default createCtxMenuConfig((config, proxy) => {
  config.setMenuModelService(async (data, model, modelService, toDispose) => {
    const { type, cell } = data!;
    // 根据触发菜单的目标不同可以设定不同的菜单内容
    switch (type) {
      /** 节点菜单 */
      case 'node': {
        const cellData = cell.getData<NsGraph.INodeConfig>();
        const submenu = cellData.isGroup
          ? [MenuItemConfig.DEL_GROUP, MenuItemConfig.RENAME_NODE]
          : [MenuItemConfig.DELETE_NODE, MenuItemConfig.RENAME_NODE];
        model.setValue({
          id: 'root',
          type: MenuItemType.Root,
          submenu: submenu,
        });
        break;
      }
      /** 边菜单 */
      case 'edge':
        model.setValue({
          id: 'root',
          type: MenuItemType.Root,
          submenu: [MenuItemConfig.DELETE_EDGE],
        });
        break;
      /** 画布菜单 */
      case 'blank':
        model.setValue({
          id: 'root',
          type: MenuItemType.Root,
          submenu: [MenuItemConfig.EMPTY_MENU],
        });
        break;
      /** 默认菜单 */
      default:
        model.setValue({
          id: 'root',
          type: MenuItemType.Root,
          submenu: [MenuItemConfig.READ_ONLY],
        });
        break;
    }
  });
});

3. 弹窗表单

上面用到了一个自定义命令RenameNodeCmd,这个命令的效果如下:

msedge_KBbwRtP3CR.gif

当然这个思路不仅仅可以做弹窗,其他功能也能结合。代码参考的xflow代码地址

3.1 思路

  1. 点击重命名,触发命令,然后弹出弹窗表单
  2. 表单点击确定,关闭弹窗把数据返回,然后修改节点数据

3.2 新建命令

这里解释一下:

在我的业务中,我把后端传来的信息全部存在了node.extra中,也就是我自己定义的一个字段,所以显示的数据和修改的数据都是这个字段中的。

所以你们可以根据自己的需要修改。

import type {
  HookHub,
  ICmdHooks as IHooks,
  ICommandContributionConfig,
  NsGraph,
} from '@antv/xflow';
import { ManaSyringe } from '@antv/xflow';

import type { IArgsBase, ICommandHandler } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';

// 自定义参数
export interface RenameNodeCommandArgs extends IArgsBase {
  nodeConfig: NsGraph.INodeConfig & { extra?: NodeExtraData };
  /**
   * 表单字段配置
   *
   * 这里其实可以改成JSONFrom,那就更方便了
   */
  formConfig: {
    name: string;
    label: string;
  };
}
// 这个Result在这里面没有用到
// 实际上给hook用的
// 下面都是hook的配置,可以忽略,没用到
export interface RenameNodeCommandResult {
  err: string | null;
  preNodeName?: string;
  currentNodeName?: string;
}
export interface RenameNodeCommandHooks extends IHooks {
  renameNode: HookHub<RenameNodeCommandArgs, RenameNodeCommandResult>;
}
type ICommand = ICommandHandler<
  RenameNodeCommandArgs,
  RenameNodeCommandResult,
  RenameNodeCommandHooks
>;

@ManaSyringe.injectable()
/** 部署画布数据 */
export class RenameNodeCommand implements ICommand {
  @ManaSyringe.inject(ICommandContextProvider) contextProvider: ICommand['contextProvider'];
  /** 执行Cmd */
  execute = async () => {
    const ctx = this.contextProvider();
    const { args } = ctx.getArgs();
    const hooks = ctx.getHooks();
    // 执行命令,因为要触发hook所以就是hooks.xxx
    const result = await hooks.renameNode.call(args, async (args) => {
      const { nodeConfig, formConfig } = args;
     // 保存修改前的数据 nodeConfig.extra?.name是我的业务数据
     // 你可以根据需要自己修改
      const preNodeName = nodeConfig.extra?.name;
      const x6Graph = await ctx.getX6Graph();
      // 获取操作的节点
      const cell = x6Graph.getCellById(nodeConfig.id);
      if (!cell || !cell.isNode()) {
        return { err: `${nodeConfig.id}不是合法节点`, preNodeName, currentNodeName: '' };
      }
      // 获取表单数据
      const formValues = await showModal(nodeConfig, args);
      // 设置node数据
      return { err: null, preNodeName, currentNodeName: '' };
    });

    ctx.setResult(result ?? { err: null });
    return this;
  };

  /** undo cmd */
  undo = async () => {
    if (this.isUndoable()) {
      const ctx = this.contextProvider();
      ctx.undo();
    }
    return this;
  };

  /** redo cmd */
  redo = async () => {
    if (!this.isUndoable()) {
      await this.execute();
    }
    return this;
  };

  isUndoable(): boolean {
    const ctx = this.contextProvider();
    return ctx.isUndoable();
  }
}


const RenameNodeCmd: ICommandContributionConfig = {
  /** Command: 用于注册named factory */
  command: {
    id: 'xflow:rename-node',
    label: '重命名节点',
    category: '节点操作',
  },
  /** hook name */
  hookKey: 'renameNode',
  CommandHandler: RenameNodeCommand,
};

export default RenameNodeCmd;

3.3 showModal实现

弹窗有两个功能:

  1. 收集表单信息
  2. 返回表单数据

弹窗我没有使用antd-proModalForm因为无法编程触发,所以我用的antdmodalProForm

function showModal(node: NsGraph.INodeConfig, commandArgs: RenameNodeCommandArgs) {
  /** showModal 返回一个Promise */
  // 这个Deferred是xflow自带的
  // 作用你就想象成更方便一点的return Promise
  const defer = new Deferred<Record<any, any>>();
  alertModal({
    content: (modal) => (
      <ProForm
        onFinish={async (values) => {
            // 提交的时候返回数据
          defer.resolve(values);
          modal.destroy();
        }}
        initialValues={{ ...(node.extra ?? node) }}
      >
        <ProFormText
          label={commandArgs.formConfig.label}
          name={commandArgs.formConfig.name}
          formItemProps={{ rules: [{ required: true }] }}
        />
      </ProForm>
    ),
    closable: true,
    onCancel: defer.resolve,
    footer: null,
    width: 800,
  });

  return defer.promise;
}

上面的alertModal就是触发一个Modal,封装了一下,主要是为了能在表单中关闭弹窗。

export const alertModal = ({ content, ...modalPorps }: AlertModalProps) => {
  const modal = Modal.confirm({
    ...modalPorps,
    icon: null,
  });
  // 把modal实例又传给了内容组件
  // 这样内容组件就可以关闭弹窗了
  modal.update({
    content: content(modal),
  });
};

3.4. 修改节点数据

获取到表单数据后就可以修改节点数据了

// ....
    // 获取表单数据 
    const formValues = await showModal(nodeConfig, args);
    // formConfig就是指定了表单name和label叫什么
      if (formValues && formValues[formConfig.name]) {
      // 获取节点数据
        const cellData = cell.getData<NsGraph.INodeConfig & { extra?: NodeExtraData }>();
        // 如果是修改的分组名称
        if (cellData.isGroup) {
        // 主要就是用setData修改数据
          cell.setData({
            ...cellData,
            label: formValues[formConfig.name],
          } as NsGraph.INodeConfig);
        } else {
          // 修改的节点名称
          cell.setData({
            ...cellData,
            // 这个extra是我自己定义的字段,根据情况自行定义
            extra: { ...(cellData.extra ?? {}), [formConfig.name]: formValues[formConfig.name] },
          } as NsGraph.INodeConfig);
        }
        return { err: null, preNodeName, currentNodeName: formValues[formConfig.name] };
      }
// ....