feat: support extends toolbar setting icon

This commit is contained in:
gchust 2025-06-25 08:05:56 +08:00
parent 2e27ee90c0
commit a6d061a192
8 changed files with 396 additions and 684 deletions

View File

@ -0,0 +1,224 @@
/**
* 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 React, { useCallback } from 'react';
import { Dropdown, Modal, App } from 'antd';
import type { MenuProps } from 'antd';
import {
SettingOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
MenuOutlined,
CopyOutlined,
} from '@ant-design/icons';
import { FlowModel } from '../../../../models';
import { ActionStepDefinition } from '../../../../types';
import { openStepSettings } from './StepSettings';
/**
*
*
*/
export const DefaultSettingsIcon: React.FC<{
model: FlowModel;
showDeleteButton?: boolean;
showCopyUidButton?: boolean;
[key: string]: any; // 允许额外的 props
}> = ({ model, showDeleteButton = true, showCopyUidButton = true }) => {
const { message } = App.useApp();
const handleMenuClick = useCallback(
({ key }: { key: string }) => {
if (key === 'copy-uid') {
// 处理复制 uid 操作
navigator.clipboard
.writeText(model.uid)
.then(() => {
message.success('UID 已复制到剪贴板');
})
.catch((error) => {
console.error('复制失败:', error);
message.error('复制失败,请重试');
});
} else if (key === 'delete') {
// 处理删除操作
Modal.confirm({
title: '确认删除',
icon: <ExclamationCircleOutlined />,
content: '确定要删除此项吗?此操作不可撤销。',
okText: '确认删除',
okType: 'primary',
cancelText: '取消',
async onOk() {
try {
await model.destroy();
} catch (error) {
console.error('删除操作失败:', error);
Modal.error({
title: '删除失败',
content: '删除操作失败,请检查控制台获取详细信息。',
});
}
},
});
} else {
// 处理step配置key格式为 "flowKey:stepKey"
const [flowKey, stepKey] = key.split(':');
try {
openStepSettings({
model,
flowKey,
stepKey,
});
} catch (error) {
// 用户取消或出错,静默处理
console.log('配置弹窗已取消或出错:', error);
}
}
},
[model, message],
);
// 获取可配置的flows和steps
const getConfigurableFlowsAndSteps = useCallback(() => {
try {
const ModelClass = model.constructor as typeof FlowModel;
const flows = ModelClass.getFlows();
const flowsArray = Array.from(flows.values());
return flowsArray
.map((flow) => {
const configurableSteps = Object.entries(flow.steps)
.map(([stepKey, stepDefinition]) => {
const actionStep = stepDefinition as ActionStepDefinition;
// 如果步骤设置了 hideInSettings: true则跳过此步骤
if (actionStep.hideInSettings) {
return null;
}
// 从step获取uiSchema如果存在
const stepUiSchema = actionStep.uiSchema || {};
// 如果step使用了action也获取action的uiSchema
let actionUiSchema = {};
if (actionStep.use) {
const action = model.flowEngine?.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,
};
})
.filter(Boolean);
return configurableSteps.length > 0 ? { flow, steps: configurableSteps } : null;
})
.filter(Boolean);
} catch (error) {
console.warn('[DefaultSettingsIcon] 获取可配置flows失败:', error);
return [];
}
}, [model]);
const configurableFlowsAndSteps = getConfigurableFlowsAndSteps();
// 如果没有可配置的flows且不显示删除按钮和复制UID按钮不显示菜单
if (configurableFlowsAndSteps.length === 0 && !showDeleteButton && !showCopyUidButton) {
return null;
}
// 构建菜单项
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: <SettingOutlined />,
label: stepInfo.title,
});
});
});
}
// 添加分割线、复制 uid 和删除按钮
if (showCopyUidButton || showDeleteButton) {
// 如果有flows配置项添加分割线
if (configurableFlowsAndSteps.length > 0) {
menuItems.push({
type: 'divider' as const,
});
}
// 添加复制 uid 按钮
if (showCopyUidButton) {
menuItems.push({
key: 'copy-uid',
icon: <CopyOutlined />,
label: '复制 UID',
});
}
// 添加删除按钮
if (showDeleteButton) {
menuItems.push({
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
});
}
}
return (
<Dropdown
menu={{
items: menuItems,
onClick: handleMenuClick,
}}
trigger={['hover']}
placement="bottomRight"
>
<MenuOutlined role="button" aria-label="flows-settings" style={{ cursor: 'pointer', fontSize: 12 }} />
</Dropdown>
);
};

View File

@ -8,22 +8,14 @@
*/
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Alert, Modal, Space, Dropdown, App } from 'antd';
import type { MenuProps } from 'antd';
import {
SettingOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
MenuOutlined,
CopyOutlined,
} from '@ant-design/icons';
import { Alert, Space } from 'antd';
import { observer } from '@formily/react';
import { css } from '@emotion/css';
import { FlowModel } from '../../../../models';
import { ActionStepDefinition } from '../../../../types';
import { ToolbarItemConfig } from '../../../../types';
import { useFlowModelById } from '../../../../hooks';
import { useFlowEngine } from '../../../../provider';
import { openStepSettings } from './StepSettings';
import { FlowEngine } from '../../../../flowEngine';
// 检测DOM中直接子元素是否包含button元素的辅助函数
const detectButtonInDOM = (container: HTMLElement): boolean => {
@ -42,6 +34,41 @@ const detectButtonInDOM = (container: HTMLElement): boolean => {
return false;
};
// 渲染工具栏项目的辅助函数
const renderToolbarItems = (
model: FlowModel,
showDeleteButton: boolean,
showCopyUidButton: boolean,
flowEngine: FlowEngine,
) => {
const toolbarItems = flowEngine?.flowSettings?.getToolbarItems?.() || [];
return toolbarItems
.filter((itemConfig: ToolbarItemConfig) => {
// 检查项目是否应该显示
return itemConfig.visible ? itemConfig.visible(model) : true;
})
.map((itemConfig: ToolbarItemConfig) => {
// 渲染项目组件
const ItemComponent = itemConfig.component;
// 对于默认设置项目,传递额外的 props
if (itemConfig.key === 'settings-menu') {
return (
<ItemComponent
key={itemConfig.key}
model={model}
showDeleteButton={showDeleteButton}
showCopyUidButton={showCopyUidButton}
/>
);
}
// 其他项目只传递 model
return <ItemComponent key={itemConfig.key} model={model} />;
});
};
// 使用与 NocoBase 一致的悬浮工具栏样式
const floatContainerStyles = ({ showBackground, showBorder }) => css`
position: relative;
@ -196,7 +223,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
const [hideMenu, setHideMenu] = useState<boolean>(false);
const [hasButton, setHasButton] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null);
const { message } = App.useApp();
const flowEngine = useFlowEngine();
// 检测DOM中是否包含button元素
useEffect(() => {
@ -241,58 +268,6 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
}
}, []);
const handleMenuClick = useCallback(
({ key }: { key: string }) => {
if (key === 'copy-uid') {
// 处理复制 uid 操作
navigator.clipboard
.writeText(model.uid)
.then(() => {
message.success('UID 已复制到剪贴板');
})
.catch((error) => {
console.error('复制失败:', error);
message.error('复制失败,请重试');
});
} else if (key === 'delete') {
// 处理删除操作
Modal.confirm({
title: '确认删除',
icon: <ExclamationCircleOutlined />,
content: '确定要删除此项吗?此操作不可撤销。',
okText: '确认删除',
okType: 'primary',
cancelText: '取消',
async onOk() {
try {
await model.destroy();
} catch (error) {
console.error('删除操作失败:', error);
Modal.error({
title: '删除失败',
content: '删除操作失败,请检查控制台获取详细信息。',
});
}
},
});
} else {
// 处理step配置key格式为 "flowKey:stepKey"
const [flowKey, stepKey] = key.split(':');
try {
openStepSettings({
model,
flowKey,
stepKey,
});
} catch (error) {
// 用户取消或出错,静默处理
console.log('配置弹窗已取消或出错:', error);
}
}
},
[model, message],
);
if (!model) {
return <Alert message="提供的模型无效" type="error" />;
}
@ -302,130 +277,6 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
return <>{children}</>;
}
// 获取可配置的flows和steps
const getConfigurableFlowsAndSteps = useCallback(() => {
try {
const ModelClass = model.constructor as typeof FlowModel;
const flows = ModelClass.getFlows();
const flowsArray = Array.from(flows.values());
return flowsArray
.map((flow) => {
const configurableSteps = Object.entries(flow.steps)
.map(([stepKey, stepDefinition]) => {
const actionStep = stepDefinition as ActionStepDefinition;
// 如果步骤设置了 hideInSettings: true则跳过此步骤
if (actionStep.hideInSettings) {
return null;
}
// 从step获取uiSchema如果存在
const stepUiSchema = actionStep.uiSchema || {};
// 如果step使用了action也获取action的uiSchema
let actionUiSchema = {};
if (actionStep.use) {
const action = model.flowEngine?.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,
};
})
.filter(Boolean);
return configurableSteps.length > 0 ? { flow, steps: configurableSteps } : null;
})
.filter(Boolean);
} catch (error) {
console.warn('[FlowsFloatContextMenu] 获取可配置flows失败:', error);
return [];
}
}, [model]);
const configurableFlowsAndSteps = getConfigurableFlowsAndSteps();
// 如果没有可配置的flows且不显示删除按钮和复制UID按钮直接返回children
if (configurableFlowsAndSteps.length === 0 && !showDeleteButton && !showCopyUidButton) {
return <>{children}</>;
}
// 构建菜单项 - 使用与 FlowsContextMenu 相同的逻辑
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: <SettingOutlined />,
label: stepInfo.title,
});
});
});
}
// 添加分割线、复制 uid 和删除按钮
if (showCopyUidButton || showDeleteButton) {
// 如果有flows配置项添加分割线
if (configurableFlowsAndSteps.length > 0) {
menuItems.push({
type: 'divider' as const,
});
}
// 添加复制 uid 按钮
if (showCopyUidButton) {
menuItems.push({
key: 'copy-uid',
icon: <CopyOutlined />,
label: '复制 UID',
});
}
// 添加删除按钮
if (showDeleteButton) {
menuItems.push({
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
});
}
}
return (
<div
ref={containerRef}
@ -442,16 +293,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
<div className="general-schema-designer">
<div className="general-schema-designer-icons">
<Space size={3} align="center">
<Dropdown
menu={{
items: menuItems,
onClick: handleMenuClick,
}}
trigger={['hover']}
placement="bottomRight"
>
<MenuOutlined role="button" aria-label="flows-settings" style={{ cursor: 'pointer', fontSize: 12 }} />
</Dropdown>
{renderToolbarItems(model, showDeleteButton, showCopyUidButton, flowEngine)}
</Space>
</div>
</div>

View File

@ -1,103 +0,0 @@
/**
* 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 React from 'react';
import { Button } from 'antd';
import { openStepSettingsDrawer } from '../StepSettingsDrawer';
/**
*
*/
// 创建一个模拟的模型对象
const mockModel = {
uid: 'test-model',
flowEngine: {
context: {},
flowSettings: {
components: {},
scopes: {},
},
},
getFlow: (flowKey: string) => {
if (flowKey === 'testFlow') {
return {
key: 'testFlow',
title: '测试流程',
steps: {
drawerStep: {
title: '抽屉步骤',
settingMode: 'drawer',
uiSchema: {
title: {
type: 'string',
title: '标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
description: {
type: 'string',
title: '描述',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
title: '默认标题',
description: '默认描述',
},
},
},
};
}
return null;
},
getStepParams: (flowKey: string, stepKey: string) => {
return {
title: '当前标题',
description: '当前描述',
};
},
setStepParams: (flowKey: string, stepKey: string, params: any) => {
console.log('设置步骤参数:', { flowKey, stepKey, params });
},
save: async () => {
console.log('保存模型');
return Promise.resolve();
},
};
const MinimalDrawerTest: React.FC = () => {
const handleOpenDrawer = async () => {
try {
const result = await openStepSettingsDrawer({
model: mockModel,
flowKey: 'testFlow',
stepKey: 'drawerStep',
drawerWidth: 600,
drawerTitle: '测试抽屉配置',
});
console.log('抽屉配置结果:', result);
} catch (error) {
console.log('抽屉配置取消或出错:', error);
}
};
return (
<div style={{ padding: 20 }}>
<h3></h3>
<p></p>
<Button type="primary" onClick={handleOpenDrawer}>
</Button>
</div>
);
};
export default MinimalDrawerTest;

View File

@ -1,110 +0,0 @@
/**
* 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 React from 'react';
import { Button } from 'antd';
import { FlowModel } from '../../../../../models';
import { openStepSettings } from '../StepSettings';
/**
*
*/
// 创建一个简单的测试模型
class SimpleTestModel extends FlowModel {
render() {
return (
<div style={{ padding: 20, border: '1px solid #ccc', borderRadius: 4 }}>
<h3></h3>
<p></p>
<Button
type="primary"
onClick={() => {
openStepSettings({
model: this,
flowKey: 'testFlow',
stepKey: 'drawerStep',
})
.then((values) => {
console.log('保存的配置:', values);
})
.catch((error) => {
console.log('用户取消或出错:', error);
});
}}
>
</Button>
</div>
);
}
}
// 注册简单的测试流程
SimpleTestModel.registerFlow({
key: 'testFlow',
title: '测试流程',
steps: {
drawerStep: {
title: '抽屉步骤',
settingMode: 'drawer', // 使用抽屉模式
uiSchema: {
title: {
type: 'string',
title: '标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入标题',
},
},
description: {
type: 'string',
title: '描述',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
placeholder: '请输入描述',
rows: 3,
},
},
enabled: {
type: 'boolean',
title: '启用',
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
priority: {
type: 'string',
title: '优先级',
enum: [
{ label: '低', value: 'low' },
{ label: '中', value: 'medium' },
{ label: '高', value: 'high' },
],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
},
defaultParams: {
title: '默认标题',
description: '默认描述',
enabled: true,
priority: 'medium',
},
// 简单的处理函数
handler: (ctx, params) => {
console.log('执行抽屉步骤:', params);
return params;
},
},
},
});
export { SimpleTestModel };

View File

@ -1,151 +0,0 @@
/**
* 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 React from 'react';
import { Button, Space } from 'antd';
import { FlowModel } from '../../../../../models';
import { openStepSettings, getStepSettingMode } from '../StepSettings';
/**
* 使 settingMode: 'drawer'
*/
// 创建一个示例模型类
class ExampleModel extends FlowModel {
render() {
return (
<div style={{ padding: 20, border: '1px solid #ccc', borderRadius: 4 }}>
<h3></h3>
<p> settingMode </p>
<Space>
<Button
onClick={() => {
openStepSettings({
model: this,
flowKey: 'exampleFlow',
stepKey: 'dialogStep',
});
}}
>
(Dialog)
</Button>
<Button
onClick={() => {
openStepSettings({
model: this,
flowKey: 'exampleFlow',
stepKey: 'drawerStep',
});
}}
>
(Drawer)
</Button>
</Space>
<div style={{ marginTop: 10 }}>
<p>Dialog Step : {getStepSettingMode(this, 'exampleFlow', 'dialogStep')}</p>
<p>Drawer Step : {getStepSettingMode(this, 'exampleFlow', 'drawerStep')}</p>
</div>
</div>
);
}
}
// 注册示例流程
ExampleModel.registerFlow({
key: 'exampleFlow',
title: '示例流程',
auto: true,
steps: {
// 使用默认的 dialog 模式
dialogStep: {
title: '对话框步骤',
use: 'exampleAction',
// settingMode 默认为 'dialog'
uiSchema: {
message: {
type: 'string',
title: '消息内容',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
type: {
type: 'string',
title: '消息类型',
enum: [
{ label: '信息', value: 'info' },
{ label: '成功', value: 'success' },
{ label: '警告', value: 'warning' },
{ label: '错误', value: 'error' },
],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
},
defaultParams: {
message: '这是一个对话框配置的步骤',
type: 'info',
},
},
// 使用 drawer 模式
drawerStep: {
title: '抽屉步骤',
use: 'exampleAction',
settingMode: 'drawer', // 配置为使用抽屉模式
uiSchema: {
title: {
type: 'string',
title: '标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
description: {
type: 'string',
title: '描述',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
priority: {
type: 'string',
title: '优先级',
enum: [
{ label: '低', value: 'low' },
{ label: '中', value: 'medium' },
{ label: '高', value: 'high' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
enabled: {
type: 'boolean',
title: '启用',
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
},
defaultParams: {
title: '这是一个抽屉配置的步骤',
description: '抽屉模式提供更大的配置空间',
priority: 'medium',
enabled: true,
},
},
},
});
// 注册示例动作 (需要通过 FlowEngine 实例注册)
// ExampleModel.registerAction({
// name: 'exampleAction',
// title: '示例动作',
// handler: (ctx, params) => {
// console.log('执行示例动作:', params);
// return params;
// },
// });
export { ExampleModel };

View File

@ -1,121 +0,0 @@
/**
* 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 React from 'react';
import { Button } from 'antd';
import { FlowModel } from '../../../../../models';
import { openStepSettings } from '../StepSettings';
/**
*
*/
// 创建一个测试模型
class TestDrawerModel extends FlowModel {
render() {
return (
<div style={{ padding: 20, border: '1px solid #ccc', borderRadius: 4 }}>
<h3></h3>
<p></p>
<Button
type="primary"
onClick={() => {
openStepSettings({
model: this,
flowKey: 'testFlow',
stepKey: 'drawerStep',
})
.then((values) => {
console.log('保存的配置:', values);
})
.catch((error) => {
console.log('用户取消或出错:', error);
});
}}
>
()
</Button>
</div>
);
}
}
// 注册测试流程
TestDrawerModel.registerFlow({
key: 'testFlow',
title: '测试流程',
steps: {
drawerStep: {
title: '抽屉步骤',
settingMode: 'drawer', // 使用抽屉模式
uiSchema: {
title: {
type: 'string',
title: '标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入标题',
},
},
description: {
type: 'string',
title: '描述',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
placeholder: '请输入描述',
rows: 3,
},
},
enabled: {
type: 'boolean',
title: '启用',
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
priority: {
type: 'string',
title: '优先级',
enum: [
{ label: '低', value: 'low' },
{ label: '中', value: 'medium' },
{ label: '高', value: 'high' },
],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
tags: {
type: 'array',
title: '标签',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
mode: 'tags',
placeholder: '请输入标签',
},
},
},
defaultParams: {
title: '默认标题',
description: '默认描述',
enabled: true,
priority: 'medium',
tags: ['测试', '抽屉'],
},
// 简单的处理函数
handler: (ctx, params) => {
console.log('执行抽屉步骤:', params);
return params;
},
},
},
});
export { TestDrawerModel };

View File

@ -9,23 +9,41 @@
import { define, observable } from '@formily/reactive';
import { openStepSettingsDialog } from './components/settings/wrappers/contextual/StepSettingsDialog';
import { StepSettingsDialogProps } from './types';
import { StepSettingsDialogProps, ToolbarItemConfig } from './types';
import { DefaultSettingsIcon } from './components/settings/wrappers/contextual/DefaultSettingsIcon';
export class FlowSettings {
public components: Record<string, any> = {};
public scopes: Record<string, any> = {};
private antdComponentsLoaded = false;
public enabled: boolean;
public toolbarItems: ToolbarItemConfig[] = [];
constructor() {
// 初始默认为 false由 SchemaComponentProvider 根据实际设计模式状态同步设置
this.enabled = false;
// 添加默认的配置项目
this.addDefaultToolbarItems();
define(this, {
enabled: observable,
});
}
/**
*
* @private
*/
private addDefaultToolbarItems(): void {
// 添加基础的配置菜单项目(原有的菜单功能)
this.toolbarItems.push({
key: 'settings-menu',
component: DefaultSettingsIcon,
sort: 0, // 默认为0作为第一个添加的项目
});
}
/**
* FlowSettings
* @returns {Promise<void>}
@ -193,6 +211,105 @@ export class FlowSettings {
this.enabled = false;
}
/**
*
* @param {ToolbarItemConfig} config
* @example
* // 添加一个复制图标组件
* const CopyIcon = ({ model }) => {
* const handleCopy = () => {
* navigator.clipboard.writeText(model.uid);
* };
* return (
* <Tooltip title="复制">
* <CopyOutlined onClick={handleCopy} style={{ cursor: 'pointer', fontSize: 12 }} />
* </Tooltip>
* );
* };
*
* flowSettings.addToolbarItem({
* key: 'copy',
* component: CopyIcon,
* sort: 10 // 数字越小越靠右
* });
*
* // 添加下拉菜单项目组件
* const MoreActionsIcon = ({ model }) => {
* const menuItems = [
* { key: 'action1', label: '操作1', onClick: () => console.log('操作1', model) },
* { key: 'action2', label: '操作2', onClick: () => console.log('操作2', model) }
* ];
* return (
* <Dropdown menu={{ items: menuItems }} trigger={['hover']}>
* <MoreOutlined style={{ cursor: 'pointer', fontSize: 12 }} />
* </Dropdown>
* );
* };
*
* flowSettings.addToolbarItem({
* key: 'more-actions',
* component: MoreActionsIcon,
* visible: (model) => model.someCondition,
* sort: 20 // 数字越大越靠左
* });
*/
public addToolbarItem(config: ToolbarItemConfig): void {
// 检查是否已存在相同 key 的项目
const existingIndex = this.toolbarItems.findIndex((item) => item.key === config.key);
if (existingIndex !== -1) {
console.warn(`FlowSettings: Toolbar item with key '${config.key}' already exists and will be replaced.`);
this.toolbarItems[existingIndex] = config;
} else {
this.toolbarItems.push(config);
}
// 按 sort 字段反向排序sort 越小越靠右(先添加的在右边)
this.toolbarItems.sort((a, b) => (b.sort || 0) - (a.sort || 0));
}
/**
*
* @param {ToolbarItemConfig[]} configs
* @example
* flowSettings.addToolbarItems([
* { key: 'copy', component: CopyIcon, sort: 10 },
* { key: 'edit', component: EditIcon, sort: 20 }
* ]);
*/
public addToolbarItems(configs: ToolbarItemConfig[]): void {
configs.forEach((config) => this.addToolbarItem(config));
}
/**
*
* @param {string} key
* @example
* flowSettings.removeToolbarItem('copy');
*/
public removeToolbarItem(key: string): void {
const index = this.toolbarItems.findIndex((item) => item.key === key);
if (index !== -1) {
this.toolbarItems.splice(index, 1);
}
}
/**
*
* @returns {ToolbarItemConfig[]}
*/
public getToolbarItems(): ToolbarItemConfig[] {
return [...this.toolbarItems];
}
/**
*
* @example
* flowSettings.clearToolbarItems();
*/
public clearToolbarItems(): void {
this.toolbarItems = [];
}
/**
*
* @param {StepSettingsDialogProps} props

View File

@ -361,3 +361,17 @@ export interface FieldFlowModelMeta extends FlowModelMeta {
}
export type { ForkFlowModel } from './models';
/**
*
*/
export interface ToolbarItemConfig {
/** 项目的唯一标识 */
key: string;
/** 项目组件,接收 model 作为 props内部处理所有逻辑 */
component: React.ComponentType<{ model: FlowModel; [key: string]: any }>;
/** 是否显示项目的条件函数 */
visible?: (model: FlowModel) => boolean;
/** 排序权重,数字越小越靠右(先添加的在右边) */
sort?: number;
}