/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { DeleteOutlined, ExclamationCircleOutlined, QuestionCircleOutlined, SettingOutlined } from '@ant-design/icons'; import type { MenuProps } from 'antd'; import { Alert, Dropdown, Modal, Tooltip, theme } from 'antd'; import React, { useCallback, useEffect, useState } from 'react'; import { FlowRuntimeContext } from '../../../../flowContext'; import { useFlowModelById } from '../../../../hooks'; import { FlowModel } from '../../../../models'; import { getT, resolveStepDisabledInSettings, setupRuntimeContextSteps, shouldHideStepInSettings, } from '../../../../utils'; import { openStepSettingsDialog } from './StepSettingsDialog'; import { ActionDefinition } from '../../../../types'; import { observer } from '../../../../reactive'; // 右键菜单组件接口 interface ModelProvidedProps { model: any; children?: React.ReactNode; // 子组件,如果提供则作为wrapper模式 enabled?: boolean; // 是否启用右键菜单,默认为true position?: 'right' | 'left'; // 右键菜单位置,默认为right showDeleteButton?: boolean; // 是否显示删除按钮,默认为true } interface ModelByIdProps { uid: string; modelClassName: string; children?: React.ReactNode; // 子组件,如果提供则作为wrapper模式 enabled?: boolean; // 是否启用右键菜单,默认为true position?: 'right' | 'left'; // 右键菜单位置,默认为right showDeleteButton?: boolean; // 是否显示删除按钮,默认为true } type FlowsContextMenuProps = ModelProvidedProps | ModelByIdProps; // 判断是否是通过ID获取模型的props const isModelByIdProps = (props: FlowsContextMenuProps): props is ModelByIdProps => { return 'uid' in props && 'modelClassName' in props && Boolean(props.uid) && Boolean(props.modelClassName); }; /** * FlowsContextMenu组件 - 右键菜单组件 * * 功能特性: * - 右键菜单 * - Wrapper 模式支持 * - 删除功能 * - 按flow分组显示steps * * 支持两种使用方式: * 1. 直接提供model: {children} * 2. 通过uid和modelClassName获取model: {children} * * @param props.children 子组件,必须提供 * @param props.enabled 是否启用右键菜单,默认为true * @param props.position 右键菜单位置,默认为right * @param props.showDeleteButton 是否显示删除按钮,默认为true */ const FlowsContextMenu: React.FC = (props) => { if (isModelByIdProps(props)) { return ; } else { return ; } }; // 使用传入的model const FlowsContextMenuWithModel: React.FC = observer( ({ model, children, enabled = true, position = 'right', showDeleteButton = true }) => { const t = getT(model); const { token } = theme.useToken(); const disabledIconColor = token?.colorTextTertiary || token?.colorTextDescription || token?.colorTextSecondary; const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = useState([]); const isStepMenuItemDisabled = useCallback( (key: string) => { const [flowKey, stepKey] = key.split(':'); if (!flowKey || !stepKey) return false; return configurableFlowsAndSteps.some(({ flow, steps }) => { if (flow.key !== flowKey) return false; return steps.some((stepInfo) => stepInfo.stepKey === stepKey && !!stepInfo.disabled); }); }, [configurableFlowsAndSteps], ); const handleMenuClick = useCallback( ({ key }: { key: string }) => { if (key === 'delete') { // 处理删除操作 Modal.confirm({ title: t('Confirm delete'), icon: , content: t('Are you sure you want to delete this item? This action cannot be undone.'), okText: t('Confirm Delete'), okType: 'danger', cancelText: t('Cancel'), zIndex: 999999, onOk() { try { model.dispatchEvent('remove'); } catch (error) { console.error(t('Delete operation failed'), ':', error); Modal.error({ title: t('Delete failed'), content: t('Delete operation failed, please check the console for details.'), }); } }, }); } else { if (isStepMenuItemDisabled(key)) { return; } // 处理step配置,key格式为 "flowKey:stepKey" const [flowKey, stepKey] = key.split(':'); try { // 创建流程运行时上下文 const flow = model.getFlow(flowKey); const step = flow?.steps?.[stepKey]; if (!flow || !step) { console.error(`Flow ${flowKey} or step ${stepKey} not found`); return; } const ctx = new FlowRuntimeContext(model, flowKey, 'settings'); setupRuntimeContextSteps(ctx, flow.steps, model, flowKey); ctx.defineProperty('currentStep', { value: step }); openStepSettingsDialog({ model, flowKey, stepKey, ctx, }); } catch (error) { // 用户取消或出错,静默处理 console.log('配置弹窗已取消或出错:', error); } } }, [isStepMenuItemDisabled, model], ); if (!model) { return ; } // 如果未启用或没有children,直接返回children if (!enabled || !children) { return <>{children}; } // 获取可配置的flows和steps const getConfigurableFlowsAndSteps = useCallback(async () => { try { const flows = (model as FlowModel).getFlows(); const flowsArray = Array.from(flows.values()); const result = await Promise.all( flowsArray.map(async (flow) => { const configurableSteps = await Promise.all( Object.entries(flow.steps).map(async ([stepKey, stepDefinition]) => { const actionStep: any = stepDefinition; // 支持静态与动态 hideInSettings if (await shouldHideStepInSettings(model as FlowModel, flow, actionStep)) { return null; } const disabledState = await resolveStepDisabledInSettings(model as FlowModel, flow, actionStep); // 从step获取uiSchema(如果存在) const stepUiSchema: ActionDefinition['uiSchema'] = actionStep.uiSchema || {}; // 如果step使用了action,也获取action的uiSchema let actionUiSchema = {}; if (actionStep.use) { const action = model.getAction?.(actionStep.use); if (action && action.uiSchema) { actionUiSchema = action.uiSchema; } } // 合并uiSchema,确保step的uiSchema优先级更高 const mergedUiSchema = { ...actionUiSchema }; // 将stepUiSchema中的字段合并到mergedUiSchema Object.entries(stepUiSchema).forEach(([fieldKey, schema]) => { if (mergedUiSchema[fieldKey]) { mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema }; } else { mergedUiSchema[fieldKey] = schema; } }); // 如果没有可配置的UI Schema,返回null if (Object.keys(mergedUiSchema).length === 0) { return null; } return { stepKey, step: actionStep, uiSchema: mergedUiSchema, title: actionStep.title || stepKey, disabled: disabledState.disabled, disabledReason: disabledState.reason, }; }), ).then((steps) => steps.filter(Boolean)); return configurableSteps.length > 0 ? { flow, steps: configurableSteps } : null; }), ); return result.filter(Boolean); } catch (error) { console.warn('[FlowsContextMenu] 获取可配置flows失败:', error); return []; } }, [model]); useEffect(() => { let mounted = true; (async () => { const flows = await getConfigurableFlowsAndSteps(); if (mounted) { setConfigurableFlowsAndSteps(flows as any[]); } })(); return () => { mounted = false; }; }, [getConfigurableFlowsAndSteps]); // 如果没有可配置的flows且不显示删除按钮,直接返回children if (configurableFlowsAndSteps.length === 0 && !showDeleteButton) { return <>{children}; } // 构建右键菜单项 const menuItems: MenuProps['items'] = []; // 添加flows和steps配置项 if (configurableFlowsAndSteps.length > 0) { configurableFlowsAndSteps.forEach(({ flow, steps }) => { // 始终按flow分组显示 menuItems.push({ key: `flow-group-${flow.key}`, label: flow.title || flow.key, type: 'group', }); steps.forEach((stepInfo) => { menuItems.push({ key: `${flow.key}:${stepInfo.stepKey}`, icon: , label: stepInfo.disabled ? ( {stepInfo.title} ) : ( stepInfo.title ), disabled: !!stepInfo.disabled, }); }); }); } // 添加分割线和删除按钮 if (showDeleteButton) { // 如果有flows配置项,添加分割线 if (configurableFlowsAndSteps.length > 0) { menuItems.push({ type: 'divider' as const, }); } // 添加删除按钮 menuItems.push({ key: 'delete', icon: , label: '删除', }); } return ( <>
{children}
); }, ); // 通过useModelById hook获取model const FlowsContextMenuWithModelById: React.FC = observer( ({ uid, modelClassName, children, enabled = true, position = 'right', showDeleteButton = true }) => { const model = useFlowModelById(uid, modelClassName); if (!model) { return ; } return ( {children} ); }, ); export { FlowsContextMenu };