/**
* 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 };