feat: improve openStepSettingsDialog

This commit is contained in:
chenos 2025-07-01 09:30:47 +08:00
parent 7561db0fa0
commit 3b39df9147
5 changed files with 71 additions and 290 deletions

View File

@ -278,6 +278,7 @@ export class Application {
api: this.apiClient, api: this.apiClient,
i18n: this.i18n, i18n: this.i18n,
router: this.router.router, router: this.router.router,
flowEngine: this.flowEngine,
}); });
this.use(FlowEngineProvider, { engine: this.flowEngine }); this.use(FlowEngineProvider, { engine: this.flowEngine });
this.use(FlowEngineGlobalsContextProvider); this.use(FlowEngineGlobalsContextProvider);

View File

@ -7,13 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { createSchemaField, ISchema } from '@formily/react'; import { FormButtonGroup } from '@formily/antd-v5';
import { message } from 'antd'; import { createForm } from '@formily/core';
import { createSchemaField, FormProvider, ISchema } from '@formily/react';
import { toJS } from '@formily/reactive';
import { Button, message, Space } from 'antd';
import React from 'react'; import React from 'react';
import { StepSettingsDialogProps } from '../../../../types'; import { StepSettingsDialogProps } from '../../../../types';
import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils'; import { compileUiSchema, getT, resolveDefaultParams, resolveUiSchema } from '../../../../utils';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext'; import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
import { toJS } from '@formily/reactive';
const SchemaField = createSchemaField(); const SchemaField = createSchemaField();
@ -32,8 +34,10 @@ const openStepSettingsDialog = async ({
stepKey, stepKey,
dialogWidth = 600, dialogWidth = 600,
dialogTitle, dialogTitle,
mode = 'dialog',
}: StepSettingsDialogProps): Promise<any> => { }: StepSettingsDialogProps): Promise<any> => {
const t = getT(model); const t = getT(model);
const message = model.flowEngine.context.message;
if (!model) { if (!model) {
message.error(t('Invalid model provided')); message.error(t('Invalid model provided'));
@ -127,25 +131,47 @@ const openStepSettingsDialog = async ({
}, },
}; };
// 动态导入FormDialog const view = model.flowEngine.context[mode];
let FormDialog;
try {
({ FormDialog } = await import('@formily/antd-v5'));
} catch (error) {
throw new Error(`${t('Failed to import FormDialog')}: ${error.message}`);
}
// 创建FormDialog const form = createForm({
const formDialog = FormDialog( initialValues: compileUiSchema(scopes, initialValues),
{ });
title: dialogTitle || `${t(title)} - ${t('Configuration')}`,
const currentDialog = view.open({
title: dialogTitle || t(title),
width: dialogWidth, width: dialogWidth,
okText: t('OK'),
cancelText: t('Cancel'),
destroyOnClose: true, destroyOnClose: true,
}, footer: (
(form) => { <Space align="end">
// 创建上下文值 <Button
type="default"
onClick={() => {
currentDialog.close();
}}
>
{t('Cancel')}
</Button>
<Button
type="primary"
onClick={async () => {
try {
await form.submit();
const currentValues = form.values;
model.setStepParams(flowKey, stepKey, currentValues);
await model.save();
message.success(t('Configuration saved'));
currentDialog.close();
} catch (error) {
console.error(t('Error saving configuration'), ':', error);
message.error(t('Error saving configuration, please check console'));
}
}}
>
{t('OK')}
</Button>
</Space>
),
content: (currentDialog) => {
const contextValue: StepSettingContextType = { const contextValue: StepSettingContextType = {
model, model,
globals: model.flowEngine?.context || {}, globals: model.flowEngine?.context || {},
@ -155,11 +181,10 @@ const openStepSettingsDialog = async ({
flowKey, flowKey,
stepKey, stepKey,
}; };
// 编译 formSchema 中的表达式 // 编译 formSchema 中的表达式
const compiledFormSchema = compileUiSchema(scopes, formSchema); const compiledFormSchema = compileUiSchema(scopes, formSchema);
return ( return (
<FormProvider form={form}>
<StepSettingContextProvider value={contextValue}> <StepSettingContextProvider value={contextValue}>
<SchemaField <SchemaField
schema={compiledFormSchema} schema={compiledFormSchema}
@ -169,31 +194,9 @@ const openStepSettingsDialog = async ({
scope={scopes} scope={scopes}
/> />
</StepSettingContextProvider> </StepSettingContextProvider>
</FormProvider>
); );
}, },
);
// 设置保存回调
formDialog.forConfirm(async (payload, next) => {
try {
// 获取表单当前值
const currentValues = payload.values;
model.setStepParams(flowKey, stepKey, currentValues);
await model.save();
message.success(t('Configuration saved'));
next(payload);
} catch (error) {
console.error(t('Error saving configuration'), ':', error);
message.error(t('Error saving configuration, please check console'));
throw error;
}
});
formDialog.forCancel(async (payload, next) => next(payload));
// 打开对话框
return formDialog.open({
initialValues: compileUiSchema(scopes, initialValues),
}); });
}; };

View File

@ -6,17 +6,8 @@
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { createSchemaField, ISchema } from '@formily/react';
import { message, Button, Space } from 'antd';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StepSettingsDrawerProps } from '../../../../types'; import { StepSettingsDrawerProps } from '../../../../types';
import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils'; import { openStepSettingsDialog } from './StepSettingsDialog';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
import { toJS } from '@formily/reactive';
const SchemaField = createSchemaField();
/** /**
* StepSettingsDrawer组件 - 使 Drawer * StepSettingsDrawer组件 - 使 Drawer
@ -27,230 +18,8 @@ const SchemaField = createSchemaField();
* @param props.drawerTitle 使step的title * @param props.drawerTitle 使step的title
* @returns Promise<any> * @returns Promise<any>
*/ */
const openStepSettingsDrawer = async ({ const openStepSettingsDrawer = async (options: StepSettingsDrawerProps): Promise<any> => {
model, return openStepSettingsDialog({ ...options, mode: 'drawer' });
flowKey,
stepKey,
drawerWidth = 600,
drawerTitle,
}: StepSettingsDrawerProps): Promise<any> => {
const t = getT(model);
if (!model) {
message.error(t('Invalid model provided'));
throw new Error(t('Invalid model provided'));
}
// 获取流程和步骤信息
const flow = model.getFlow(flowKey);
const step = flow?.steps?.[stepKey];
if (!flow) {
message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
}
if (!step) {
message.error(t('Step with key {{stepKey}} not found', { stepKey }));
throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
}
let title = step.title;
// 创建参数解析上下文
const paramsContext = {
model,
globals: model.flowEngine?.context || {},
app: model.flowEngine?.context?.app,
};
const stepUiSchema = step.uiSchema || {};
let actionDefaultParams = {};
// 如果step使用了action也获取action的uiSchema
let actionUiSchema = {};
if (step.use) {
const action = model.flowEngine?.getAction?.(step.use);
if (action && action.uiSchema) {
actionUiSchema = action.uiSchema;
}
actionDefaultParams = action.defaultParams || {};
title = title || action.title;
}
// 解析动态 uiSchema
const resolvedActionUiSchema = await resolveUiSchema(actionUiSchema, paramsContext);
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
// 合并uiSchema确保step的uiSchema优先级更高
const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
if (mergedUiSchema[fieldKey]) {
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
} else {
mergedUiSchema[fieldKey] = schema;
}
});
// 如果没有可配置的UI Schema显示提示
if (Object.keys(mergedUiSchema).length === 0) {
message.info(t('This step has no configurable parameters'));
return {};
}
// 获取初始值
const stepParams = model.getStepParams(flowKey, stepKey) || {};
const flowEngine = model.flowEngine;
const scopes = {
useStepSettingContext,
...flowEngine.flowSettings?.scopes,
};
// 解析 defaultParams
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
const resolveActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
const initialValues = { ...toJS(resolveActionDefaultParams), ...toJS(resolvedDefaultParams), ...toJS(stepParams) };
// 构建表单Schema
const formSchema: ISchema = {
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: mergedUiSchema,
},
},
};
// 动态导入Formily组件
let Form, createForm;
try {
({ Form } = await import('@formily/antd-v5'));
({ createForm } = await import('@formily/core'));
} catch (error) {
throw new Error(`${t('Failed to import Formily components')}: ${error.message}`);
}
// 获取drawer API
const drawer = model.flowEngine?.context?.drawer;
if (!drawer) {
throw new Error(t('Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider'));
}
return new Promise((resolve) => {
// 用于跟踪Promise状态避免重复调用resolve
let isResolved = false;
// 创建表单实例
const form = createForm({
initialValues: compileUiSchema(scopes, initialValues),
});
// 创建抽屉内容组件
const DrawerContent: React.FC = () => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
setLoading(true);
// 先获取表单当前值,然后验证
const currentValues = form.values;
await form.validate();
// 保存配置
model.setStepParams(flowKey, stepKey, currentValues);
await model.save();
message.success(t('Configuration saved'));
isResolved = true;
drawerRef.destroy();
resolve(currentValues);
} catch (error) {
console.error(t('Error saving configuration'), ':', error);
message.error(t('Error saving configuration, please check console'));
} finally {
setLoading(false);
}
};
const handleCancel = () => {
if (!isResolved) {
isResolved = true;
resolve(null);
}
drawerRef.destroy();
};
// 创建上下文值
const contextValue: StepSettingContextType = {
model,
globals: model.flowEngine?.context || {},
app: model.flowEngine?.context?.app,
step,
flow,
flowKey,
stepKey,
};
// 编译 formSchema 中的表达式
const compiledFormSchema = compileUiSchema(scopes, formSchema);
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
<Form form={form} layout="vertical">
<StepSettingContextProvider value={contextValue}>
<SchemaField
schema={compiledFormSchema}
components={{
...flowEngine.flowSettings?.components,
}}
scope={scopes}
/>
</StepSettingContextProvider>
</Form>
</div>
<div
style={{
padding: '16px',
borderTop: '1px solid #f0f0f0',
display: 'flex',
justifyContent: 'flex-end',
gap: '8px',
}}
>
<Space>
<Button onClick={handleCancel}>{t('Cancel')}</Button>
<Button type="primary" loading={loading} onClick={handleSubmit}>
{t('OK')}
</Button>
</Space>
</div>
</div>
);
};
// 打开抽屉
const drawerRef = drawer.open({
title: drawerTitle || `${t(title)} - ${t('Configuration')}`,
width: drawerWidth,
content: <DrawerContent />,
onClose: () => {
// 只有在Promise还未被处理时才reject
if (!isResolved) {
isResolved = true;
resolve(null);
}
},
});
});
}; };
export { openStepSettingsDrawer }; export { openStepSettingsDrawer };

View File

@ -263,6 +263,7 @@ export interface StepSettingsDialogProps {
stepKey: string; stepKey: string;
dialogWidth?: number | string; dialogWidth?: number | string;
dialogTitle?: string; dialogTitle?: string;
mode?: 'dialog' | 'drawer'; // 设置模式,默认为'dialog'
} }
/** /**

View File

@ -34,6 +34,13 @@ const DrawerComponent = forwardRef<unknown, DrawerComponentProps>(({ afterClose,
setVisible(false); setVisible(false);
config.onClose?.(e); config.onClose?.(e);
}} }}
styles={{
...config?.styles,
footer: {
textAlign: 'end',
...config?.styles?.footer,
},
}}
afterOpenChange={(open) => { afterOpenChange={(open) => {
if (!open) { if (!open) {
afterClose?.(); afterClose?.();