diff --git a/apps/app-mobile/package.json b/apps/app-mobile/package.json index ca60df9..9610252 100644 --- a/apps/app-mobile/package.json +++ b/apps/app-mobile/package.json @@ -14,6 +14,7 @@ "@coding-flow/flow-types": "workspace:*", "@coding-flow/flow-mobile-ui": "workspace:*", "@coding-flow/flow-mobile-approval": "workspace:*", + "@coding-flow/flow-approval-presenter": "workspace:*", "antd-mobile": "^5.42.3", "dayjs": "^1.11.19", "react": "^18.3.1", diff --git a/apps/app-mobile/src/components/flow-view/index.tsx b/apps/app-mobile/src/components/flow-view/index.tsx new file mode 100644 index 0000000..815b74f --- /dev/null +++ b/apps/app-mobile/src/components/flow-view/index.tsx @@ -0,0 +1,135 @@ +import {EventBus} from "@coding-flow/flow-core"; +import type {FormViewProps} from "@coding-flow/flow-types"; +import {type ActionInterceptorContext, useApprovalContext} from "@coding-flow/flow-approval-presenter"; +import {FormView} from "@coding-form/form-engine"; +import {Form, Input} from "antd-mobile"; +import React from "react"; + +/** + * 移动端流程表单视图(与 app-pc 的 FlowView 机制对齐)。 + * + * 通过 `register-form-view` 注册为 `ViewBindPlugin` 的 'default' 视图, + * 在审批表单渲染处挂载以下三种自定义能力: + * 1. EventBus 前端自定义事件触发; + * 2. 审批操作拦截器(action type / action id 二选一); + * 3. 审批弹框标题与内容提供器(action type / action id 二选一)。 + */ +export const FlowView: React.FC = (props) => { + + const form = props.form; + + const {state, context} = useApprovalContext(); + + const actionPresenter = context.getPresenter().getFlowActionPresenter(); + + React.useEffect(() => { + // 场景一:订阅前端自定义事件的触发操作,事件 KEY 在流程配置上设置 + EventBus.getInstance().on('CUSTOM', () => { + // 所有的按钮数据 + const actions = state.flow?.actionList || []; + // 找到通过的按钮 + const pass = actions.find(item => item.type === 'PASS'); + // 更新 form 表单数据 + form?.setFieldValue('test', '123'); + if (pass) { + // 触发某个按钮的交互操作 + EventBus.getInstance().emit(pass.id); + } + }); + + return () => { + // 注销前端自定义事件的触发操作 + EventBus.getInstance().off('CUSTOM'); + } + + }, []); + + React.useEffect(() => { + + // 场景二:订阅审批操作拦截器: + // - 所有审批按钮点击后、真正提交前会先执行拦截器 + // - 返回 true(或 resolve true)放行;返回 false(或 resolve false)拦截 + // - 支持异步,例如先弹窗二次确认、做业务校验、调用远程接口等 + // + // addActionInterceptor 返回取消订阅函数,必须在组件卸载时调用, + // 否则 StrictMode 或组件重复挂载会导致拦截器被重复添加。 + const unsubscribe = actionPresenter.addActionInterceptor( + (interceptorContext: ActionInterceptorContext) => { + const {action} = interceptorContext; + + // 如果根据 action?.type 来处理拦截的话,会影响整个视图下所有该类型下的按钮操作 + // 如果根据 action.id 来处理拦截的话,只会影响该视图下某一节点的按钮操作业务 + console.log('click action:', action); + + return true; + } + ); + + console.log('action unsubscribe:', unsubscribe); + + return () => { + // 组件卸载时取消订阅,避免重复添加拦截器 + unsubscribe(); + }; + }, []); + + React.useEffect(() => { + // 场景三:自定义按钮操作框标题和内容信息 + + // 根据按钮类型约定按钮的提示文字信息,将会影响该视图下所有节点类型下确认框信息 + // const unsubscribe = actionPresenter.addDialogContentProvider(({action}) => { + // if (action?.type === 'PASS') { + // return {title: '自定义确认'} + // } + // return null; + // } + // ); + + // 根据按钮 ID 约定按钮的提示文字信息,将只会影响该视图某一节点的按钮操作确认框信息 + const unsubscribe = actionPresenter.addDialogContentProvider(({actionId}) => { + if (actionId === 'KYykw8pigO') { + // 当返回content之后,若存在意见框等内容也会完全被content替换,因此使用content时请确保弹框内容已经时空白时在使用。 + return {title: '确认小额审批?', content: '提交后不可撤回'} + } + return null; + } + ); + + console.log('dialog unsubscribe:', unsubscribe); + + return () => { + // 组件卸载时取消订阅,避免重复添加拦截器 + unsubscribe(); + }; + + }, []); + + return ( + <> + { + props.onValuesChange?.(values); + }} + > + + + + + ) +} \ No newline at end of file diff --git a/apps/app-mobile/src/hooks/register-form-types.tsx b/apps/app-mobile/src/hooks/register-form-types.tsx index cba3894..36c7f32 100644 --- a/apps/app-mobile/src/hooks/register-form-types.tsx +++ b/apps/app-mobile/src/hooks/register-form-types.tsx @@ -1,4 +1,3 @@ -import React from "react"; import {FormTypeContext} from "@coding-flow/flow-types"; import {type FormType} from "@coding-flow/flow-types"; import { registerFormItems } from "@coding-form/form-engine"; @@ -14,12 +13,13 @@ class RegisterRef{ } -export const registerFormTypes = ()=>{ +// 模块级单例标记:注册逻辑可能在异步上下文中调用,不能使用 React.useRef +let registerRef: RegisterRef | undefined; - const ref = React.useRef(undefined); +export const registerFormTypes = ()=>{ - if(!ref.current){ - ref.current = new RegisterRef(); + if(!registerRef){ + registerRef = new RegisterRef(); const types:FormType[] = [ { diff --git a/apps/app-mobile/src/hooks/register-form-view.tsx b/apps/app-mobile/src/hooks/register-form-view.tsx new file mode 100644 index 0000000..27e638e --- /dev/null +++ b/apps/app-mobile/src/hooks/register-form-view.tsx @@ -0,0 +1,15 @@ +import { ViewBindPlugin } from "@coding-flow/flow-core"; +import { FlowView } from "@/components/flow-view"; + +class RegisterRef { + +} + +// 模块级单例标记:注册逻辑可能在异步上下文中调用,不能使用 React.useRef +let registerRef: RegisterRef | undefined; +export const registerFormView = () => { + if (!registerRef) { + registerRef = new RegisterRef(); + ViewBindPlugin.getInstance().register('default', FlowView); + } +} \ No newline at end of file diff --git a/apps/app-mobile/src/hooks/registrations.tsx b/apps/app-mobile/src/hooks/registrations.tsx new file mode 100644 index 0000000..db9f3de --- /dev/null +++ b/apps/app-mobile/src/hooks/registrations.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { registerFormTypes } from "@/hooks/register-form-types"; +import { registerFormView } from "@/hooks/register-form-view"; + +/** + * 注册组件。 + * + * 注意:@coding-form/form-engine 的 registerFormItems 内部调用了 React.useRef(), + * 因此所有注册逻辑只能在组件渲染期执行,不能放在 useEffect 的异步回调里调用 + * (会抛 Invalid hook call 导致整个注册链中断)。 + * + * 通过 React.lazy 动态加载本模块,注册相关依赖(antd-mobile Form、表单组件等) + * 不会进入首屏 chunk。 + */ +const Registrations: React.FC = () => { + + registerFormTypes(); + registerFormView(); + + React.useEffect(() => { + console.log('register flow success.'); + }, []); + + return null; +}; + +export default Registrations; \ No newline at end of file diff --git a/apps/app-mobile/src/index.tsx b/apps/app-mobile/src/index.tsx index b47d2aa..e4a9745 100644 --- a/apps/app-mobile/src/index.tsx +++ b/apps/app-mobile/src/index.tsx @@ -2,13 +2,23 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { RouterProvider } from 'react-router'; import { hashRouters } from './config/routers'; -import {registerFormTypes} from "@/hooks/register-form-types.tsx"; + + +// 注册逻辑动态加载:注册依赖(antd-mobile Form、表单组件等)不进入首屏 chunk +const Registrations = React.lazy(() => import('@/hooks/registrations')); const App = () => { - registerFormTypes(); return ( - + {/* 注册组件必须在渲染期执行(registerFormItems 内部调用了 hook), + 不能放在 useEffect 异步回调里调用 */} + + + + {/* 路由页面均为 React.lazy 动态加载,必须提供 Suspense 兜底 */} + + + ) } diff --git a/apps/app-pc/src/components/flow-view/index.tsx b/apps/app-pc/src/components/flow-view/index.tsx index 2e3bfa7..eed789c 100644 --- a/apps/app-pc/src/components/flow-view/index.tsx +++ b/apps/app-pc/src/components/flow-view/index.tsx @@ -3,7 +3,7 @@ import type { FormViewProps } from "@coding-flow/flow-types"; import { useApprovalContext, type ActionInterceptorContext } from "@coding-flow/flow-approval-presenter"; import { FormView } from "@coding-form/form-engine"; import React from "react"; -import { Form, Modal } from "antd"; +import { Form } from "antd"; export const FlowView: React.FC = (props) => { @@ -17,15 +17,29 @@ export const FlowView: React.FC = (props) => { // 诊断日志:确认 FlowView 是否被挂载,以及当前流程实际使用的视图名称 console.log('FlowView mounted, flow.view =', state.flow?.view); + // 订阅前端自定义事件的触发操作,事件KEY再流程配置上设置: EventBus.getInstance().on('CUSTOM', () => { + // 所有的按钮数据 const actions = state.flow?.actionList || []; + // 找到通过的按钮 const pass = actions.find(item => item.type === 'PASS'); + // 更新form表单数据 form?.setFieldValue('test', '123'); if (pass) { + // 触发某个按钮的交互操作 EventBus.getInstance().emit(pass.id); } }); + return () => { + // 注销前端自定义事件的触发操作 + EventBus.getInstance().off('CUSTOM'); + } + + }, []); + + React.useEffect(() => { + // 订阅审批操作拦截器: // - 所有审批按钮点击后、真正提交前会先执行拦截器 // - 返回 true(或 resolve true)放行;返回 false(或 resolve false)拦截 @@ -37,19 +51,53 @@ export const FlowView: React.FC = (props) => { (interceptorContext: ActionInterceptorContext) => { const { action } = interceptorContext; - console.log('click action:',action); + // 如果是根据action?.type 来处理拦截的话,会影响整个视图下所有该类型下的按钮操作 + // 如果是根据action.id 来处理拦截的话,只会影响该视图下某一节点的按钮操作业务 + console.log('click action:', action); - return false; + return true; } ); - console.log('unsubscribe:',unsubscribe); + console.log('action unsubscribe:', unsubscribe); + + return () => { + // 组件卸载时取消订阅,避免重复添加拦截器 + unsubscribe(); + }; + }, []); + + + + React.useEffect(() => { + // 自定义按钮操作框标题和内容信息 + + // 根据按钮类型约定按钮的提示文字信息,将会影响该视图下所有节点类型下确认框信息 + // const unsubscribe = actionPresenter.addDialogContentProvider(({action}) => { + // if (action?.type === 'PASS') { + // return {title: '自定义确认'} + // } + // return null; + // } + // ); + + // 根据按钮 ID 约定按钮的提示文字信息,将只会影响该视图某一节点的按钮操作确认框信息 + const unsubscribe = actionPresenter.addDialogContentProvider(({actionId}) => { + if (actionId === 'KYykw8pigO') { + // 当返回content之后,若存在意见框等内容也会完全被content替换,因此使用content时请确保弹框内容已经时空白时在使用。 + return {title: '确认小额审批?', content: '提交后不可撤回'} + } + return null; + } + ); + + console.log('dialog unsubscribe:', unsubscribe); return () => { - EventBus.getInstance().off('CUSTOM'); // 组件卸载时取消订阅,避免重复添加拦截器 unsubscribe(); }; + }, []); return ( diff --git a/apps/app-pc/src/hooks/register-form-view.tsx b/apps/app-pc/src/hooks/register-form-view.tsx index e758110..27e638e 100644 --- a/apps/app-pc/src/hooks/register-form-view.tsx +++ b/apps/app-pc/src/hooks/register-form-view.tsx @@ -10,6 +10,6 @@ let registerRef: RegisterRef | undefined; export const registerFormView = () => { if (!registerRef) { registerRef = new RegisterRef(); - // ViewBindPlugin.getInstance().register('default', FlowView); + ViewBindPlugin.getInstance().register('default', FlowView); } } \ No newline at end of file diff --git a/packages/flow-approval-presenter/CHANGELOG.md b/packages/flow-approval-presenter/CHANGELOG.md index 5c75234..4d1114a 100644 --- a/packages/flow-approval-presenter/CHANGELOG.md +++ b/packages/flow-approval-presenter/CHANGELOG.md @@ -1,5 +1,13 @@ # @coding-flow/flow-approval-presenter +## 0.1.8 + +### Patch Changes + +- 审批弹框标题/内容自定义能力(DialogContentProvider)+ 设计器按钮 ID 可复制 + - @coding-flow/flow-core@0.1.8 + - @coding-flow/flow-types@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-approval-presenter/package.json b/packages/flow-approval-presenter/package.json index 08f8c9e..cd407fe 100644 --- a/packages/flow-approval-presenter/package.json +++ b/packages/flow-approval-presenter/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-approval-presenter", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine approval presenter framework ", "type": "module", "sideEffects": [ diff --git a/packages/flow-approval-presenter/src/interceptor/dialog-content.ts b/packages/flow-approval-presenter/src/interceptor/dialog-content.ts new file mode 100644 index 0000000..c4b8796 --- /dev/null +++ b/packages/flow-approval-presenter/src/interceptor/dialog-content.ts @@ -0,0 +1,106 @@ +import { FlowAction } from "@coding-flow/flow-types"; +import React from "react"; + +/** + * 审批弹框内容覆盖结果。 + * + * 提供器可选择性返回 title / content 以覆盖默认弹框的标题与中间内容; + * 未提供的字段仍使用默认渲染(title 用各动作组件的默认标题,content 用审批意见框/签名表单)。 + */ +export interface DialogContent { + /** 弹框标题(缺省用动作组件的默认标题,如「审批通过」) */ + title?: React.ReactNode; + /** 弹框中间内容(提供后整块替换默认表单区域,确定按钮直接提交) */ + content?: React.ReactNode; +} + +/** + * 审批弹框内容提供器执行上下文。 + * + * 与 {@link ActionInterceptorContext} 对齐,仅提供 actionId 与 action 对象: + * - 按 actionId 匹配:适合对单个按钮定制(区分两个同为 CUSTOM 的自定义按钮); + * - 按 actionType 匹配:通过 `context.action?.type` 判断,适合对所有同类型按钮统一定制。 + */ +export interface DialogContentProviderContext { + /** 触发的动作 ID */ + actionId: string; + /** 触发的动作对象(未找到时为 null) */ + action: FlowAction | null; +} + +/** + * 审批弹框内容提供器。 + * + * 返回 DialogContent 表示覆盖;返回 null / undefined 表示不覆盖(使用默认渲染)。 + * 支持同步与异步(返回 DialogContent 或 Promise)。 + */ +export type DialogContentProvider = ( + context: DialogContentProviderContext +) => DialogContent | null | undefined | Promise; + +/** + * 审批弹框内容提供器管理器。 + * + * 通过订阅方式注册提供器,按订阅顺序依次执行,首个返回非 null 内容者命中并短路。 + * 典型用法:自定义视图通过 + * `useApprovalContext().context.getPresenter().getFlowActionPresenter()` 获取 Presenter, + * 调用 `addDialogContentProvider(provider)` 订阅,返回的函数用于取消订阅。 + */ +export class DialogContentManager { + + private providers: DialogContentProvider[] = []; + + /** + * 订阅内容提供器 + * @param provider 提供器函数 + * @returns 取消订阅函数 + */ + public add(provider: DialogContentProvider): () => void { + this.providers.push(provider); + return () => { + this.remove(provider); + }; + } + + /** + * 移除指定提供器 + * @param provider 待移除的提供器函数 + */ + public remove(provider: DialogContentProvider): void { + const index = this.providers.findIndex(item => item === provider); + if (index !== -1) { + this.providers.splice(index, 1); + } + } + + /** + * 清空全部提供器 + */ + public clear(): void { + this.providers = []; + } + + /** + * 当前订阅的提供器数量 + */ + public size(): number { + return this.providers.length; + } + + /** + * 按订阅顺序依次执行所有提供器(支持异步)。 + * 首个返回非 null 内容者命中并立即返回;全部未命中返回 null。 + * @param context 提供器上下文 + * @returns 命中的内容;未命中返回 null + */ + public async resolve(context: DialogContentProviderContext): Promise { + for (const provider of this.providers) { + const content = await provider(context); + if (content != null) { + return content; + } + } + return null; + } + +} \ No newline at end of file diff --git a/packages/flow-approval-presenter/src/interceptor/index.ts b/packages/flow-approval-presenter/src/interceptor/index.ts index 1306945..e042af6 100644 --- a/packages/flow-approval-presenter/src/interceptor/index.ts +++ b/packages/flow-approval-presenter/src/interceptor/index.ts @@ -1 +1,2 @@ export * from "./action-interceptor"; +export * from "./dialog-content"; diff --git a/packages/flow-approval-presenter/src/presenters/action.ts b/packages/flow-approval-presenter/src/presenters/action.ts index 2507319..90763ba 100644 --- a/packages/flow-approval-presenter/src/presenters/action.ts +++ b/packages/flow-approval-presenter/src/presenters/action.ts @@ -1,6 +1,6 @@ import { ApprovalState, FlowApprovalApi } from "@/typings"; import { FormActionContext, FlowAction } from "@coding-flow/flow-types"; -import { ActionInterceptor, ActionInterceptorManager } from "@/interceptor"; +import { ActionInterceptor, ActionInterceptorManager, DialogContent, DialogContentManager, DialogContentProvider } from "@/interceptor"; export class FlowActionPresenter { @@ -10,6 +10,7 @@ export class FlowActionPresenter { private readonly mockKey: string; private readonly setLoading: (loading: boolean) => void; private readonly interceptorManager: ActionInterceptorManager; + private readonly dialogContentManager: DialogContentManager; private submitRecordIds: number[]; @@ -36,6 +37,7 @@ export class FlowActionPresenter { this.mockKey = mockKey; this.setLoading = setLoading; this.interceptorManager = new ActionInterceptorManager(); + this.dialogContentManager = new DialogContentManager(); } @@ -65,6 +67,49 @@ export class FlowActionPresenter { this.interceptorManager.remove(interceptor); } + /** + * 订阅审批弹框内容提供器。 + * + * 各审批操作按钮弹框渲染时,会通过 {@link resolveDialogContent} 触发已订阅的提供器, + * 首个返回非 null 内容的提供器命中,用于覆盖弹框标题与中间内容 + * (典型场景:隐藏审批意见后弹框退化为确认框,定制确认文案)。 + * + * 自定义视图可通过 + * `useApprovalContext().context.getPresenter().getFlowActionPresenter()` + * 获取本对象后订阅,并在组件卸载时调用返回的函数取消订阅。 + * + * @param provider 内容提供器函数 + * @returns 取消订阅函数 + */ + public addDialogContentProvider(provider: DialogContentProvider): () => void { + return this.dialogContentManager.add(provider); + } + + /** + * 移除指定审批弹框内容提供器 + * @param provider 待移除的内容提供器函数 + */ + public removeDialogContentProvider(provider: DialogContentProvider): void { + this.dialogContentManager.remove(provider); + } + + /** + * 解析指定动作的弹框内容。 + * + * 按订阅顺序执行内容提供器,首个命中者返回;未命中返回 null(使用默认渲染)。 + * 上下文携带 actionId 与 action 对象(含 type),由提供器内部选择过滤键。 + * + * @param actionId 动作 ID + * @returns 命中的弹框内容;未命中返回 null + */ + public async resolveDialogContent(actionId: string): Promise { + const action = this.getAction(actionId); + return await this.dialogContentManager.resolve({ + actionId, + action, + }); + } + public setSubmitRecordIds(submitRecordIds: number[]) { this.submitRecordIds = []; diff --git a/packages/flow-approval-presenter/tests/dialog-content.test.ts b/packages/flow-approval-presenter/tests/dialog-content.test.ts new file mode 100644 index 0000000..fd8cd22 --- /dev/null +++ b/packages/flow-approval-presenter/tests/dialog-content.test.ts @@ -0,0 +1,212 @@ +import { beforeEach, describe, expect, it, rs } from "@rstest/core"; +import { DialogContentManager, DialogContentProvider } from "@/interceptor/dialog-content"; +import { FlowActionPresenter } from "@/presenters/action"; +import { ApprovalState, FlowApprovalApi } from "@/typings"; +import { FlowAction, FormActionContext } from "@coding-flow/flow-types"; + +/** 构造一个最小可用的流程操作按钮对象 */ +const buildAction = (id: string, type: 'PASS' | 'REJECT' | 'CUSTOM' = 'PASS'): FlowAction => ({ + id, + title: id, + type, + display: 'BUTTON', + enable: true, +} as FlowAction); + +/** 构造审批状态 */ +const buildState = (actions: FlowAction[]): ApprovalState => ({ + flow: { + recordId: 1, + actions, + mergeable: false, + }, +} as unknown as ApprovalState); + +/** 构造可观测调用情况的 API mock */ +const buildApi = () => { + const api: FlowApprovalApi = { + create: rs.fn(async () => 100), + processNodes: rs.fn(async () => []), + action: rs.fn(async () => ({ success: true })), + revoke: rs.fn(async () => ({ success: true })), + urge: rs.fn(async () => ({ success: true })), + }; + return api; +}; + +const buildPresenter = (actions: FlowAction[], setLoading = () => {}) => { + return new FlowActionPresenter( + buildState(actions), + buildApi(), + new FormActionContext(), + 'mockKey', + setLoading, + ); +}; + +describe.sequential('DialogContentManager', () => { + + let manager: DialogContentManager; + + beforeEach(() => { + manager = new DialogContentManager(); + }); + + it('未订阅任何提供器时返回 null', async () => { + const result = await manager.resolve({ actionId: 'a', action: null }); + expect(result).toBeNull(); + }); + + it('提供器返回内容时命中', async () => { + manager.add(() => ({ title: '确认通过?' })); + const result = await manager.resolve({ actionId: 'a', action: null }); + expect(result).toEqual({ title: '确认通过?' }); + }); + + it('提供器返回 null 时继续查找下一个', async () => { + manager.add(() => null); + manager.add(() => ({ content: '确认文案' })); + const result = await manager.resolve({ actionId: 'a', action: null }); + expect(result).toEqual({ content: '确认文案' }); + }); + + it('首个返回非 null 内容的提供器命中并短路', async () => { + const second = rs.fn(() => ({ title: '不应命中' })); + manager.add(() => ({ title: '命中' })); + manager.add(second); + const result = await manager.resolve({ actionId: 'a', action: null }); + expect(result).toEqual({ title: '命中' }); + expect(second).not.toHaveBeenCalled(); + }); + + it('异步提供器 resolve 内容后命中', async () => { + manager.add(async () => { + await Promise.resolve(); + return { title: '异步命中' }; + }); + const result = await manager.resolve({ actionId: 'a', action: null }); + expect(result).toEqual({ title: '异步命中' }); + }); + + it('add 返回的函数可取消订阅', async () => { + const unsubscribe = manager.add(() => ({ title: '确认' })); + expect(manager.size()).toBe(1); + unsubscribe(); + expect(manager.size()).toBe(0); + const result = await manager.resolve({ actionId: 'a', action: null }); + expect(result).toBeNull(); + }); + + it('remove 可移除指定提供器', () => { + const provider: DialogContentProvider = () => ({ title: '确认' }); + manager.add(provider); + manager.remove(provider); + expect(manager.size()).toBe(0); + }); + + it('clear 清空全部提供器', () => { + manager.add(() => ({ title: '确认' })); + manager.add(() => ({ content: '文案' })); + manager.clear(); + expect(manager.size()).toBe(0); + }); + + it('提供器接收到完整上下文', async () => { + const action = buildAction('pass-1'); + const received = rs.fn(() => ({ title: '确认' })); + manager.add(received); + await manager.resolve({ actionId: 'pass-1', action }); + expect(received).toHaveBeenCalledWith({ + actionId: 'pass-1', + action, + }); + }); +}); + +describe.sequential('FlowActionPresenter 弹框内容集成', () => { + + it('未订阅提供器时返回 null', async () => { + const presenter = buildPresenter([buildAction('pass-1')]); + + const result = await presenter.resolveDialogContent('pass-1'); + + expect(result).toBeNull(); + }); + + it('按 actionId 命中的提供器返回内容', async () => { + const action = buildAction('custom-1', 'CUSTOM'); + const presenter = buildPresenter([action]); + presenter.addDialogContentProvider(({ actionId }) => { + if (actionId === 'custom-1') { + return { title: '确认自定义审批?', content: '提交后不可撤回' }; + } + return null; + }); + + const result = await presenter.resolveDialogContent('custom-1'); + + expect(result).toEqual({ title: '确认自定义审批?', content: '提交后不可撤回' }); + }); + + it('按 actionType 命中的提供器返回内容', async () => { + const presenter = buildPresenter([buildAction('reject-1', 'REJECT')]); + presenter.addDialogContentProvider(({ action }) => { + if (action?.type === 'REJECT') { + return { title: '确认驳回?' }; + } + return null; + }); + + const result = await presenter.resolveDialogContent('reject-1'); + + expect(result).toEqual({ title: '确认驳回?' }); + }); + + it('未命中任何提供器时返回 null', async () => { + const presenter = buildPresenter([buildAction('pass-1')]); + presenter.addDialogContentProvider(() => null); + + const result = await presenter.resolveDialogContent('pass-1'); + + expect(result).toBeNull(); + }); + + it('取消订阅后恢复正常(返回 null)', async () => { + const presenter = buildPresenter([buildAction('pass-1')]); + const unsubscribe = presenter.addDialogContentProvider(() => ({ title: '确认通过?' })); + + const hit = await presenter.resolveDialogContent('pass-1'); + expect(hit).toEqual({ title: '确认通过?' }); + + unsubscribe(); + const miss = await presenter.resolveDialogContent('pass-1'); + expect(miss).toBeNull(); + }); + + it('removeDialogContentProvider 移除后恢复正常(返回 null)', async () => { + const presenter = buildPresenter([buildAction('pass-1')]); + const provider: DialogContentProvider = () => ({ title: '确认通过?' }); + presenter.addDialogContentProvider(provider); + + const hit = await presenter.resolveDialogContent('pass-1'); + expect(hit).toEqual({ title: '确认通过?' }); + + presenter.removeDialogContentProvider(provider); + const miss = await presenter.resolveDialogContent('pass-1'); + expect(miss).toBeNull(); + }); + + it('提供器上下文包含动作类型与动作对象', async () => { + const action = buildAction('custom-1', 'CUSTOM'); + const presenter = buildPresenter([action]); + const received = rs.fn(() => ({ title: '确认' })); + presenter.addDialogContentProvider(received); + + await presenter.resolveDialogContent('custom-1'); + + expect(received).toHaveBeenCalledWith({ + actionId: 'custom-1', + action, + }); + }); +}); \ No newline at end of file diff --git a/packages/flow-core/CHANGELOG.md b/packages/flow-core/CHANGELOG.md index 8d2ce8d..81ab23e 100644 --- a/packages/flow-core/CHANGELOG.md +++ b/packages/flow-core/CHANGELOG.md @@ -1,5 +1,7 @@ # @coding-flow/flow-core +## 0.1.8 + ## 0.1.7 ## 0.1.6 diff --git a/packages/flow-core/package.json b/packages/flow-core/package.json index efbaee8..002b736 100644 --- a/packages/flow-core/package.json +++ b/packages/flow-core/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-core", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine core lib framework ", "type": "module", "sideEffects": [ diff --git a/packages/flow-design/CHANGELOG.md b/packages/flow-design/CHANGELOG.md index f344240..0c19c20 100644 --- a/packages/flow-design/CHANGELOG.md +++ b/packages/flow-design/CHANGELOG.md @@ -1,5 +1,15 @@ # @coding-flow/flow-design +## 0.1.8 + +### Patch Changes + +- 审批弹框标题/内容自定义能力(DialogContentProvider)+ 设计器按钮 ID 可复制 + - @coding-flow/flow-core@0.1.8 + - @coding-flow/flow-icons@0.1.8 + - @coding-flow/flow-pc-ui@0.1.8 + - @coding-flow/flow-types@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-design/package.json b/packages/flow-design/package.json index f10b228..0cfa88f 100644 --- a/packages/flow-design/package.json +++ b/packages/flow-design/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-design", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine design components ", "type": "module", "sideEffects": [ diff --git a/packages/flow-design/src/components/design-editor/node-components/action/index.tsx b/packages/flow-design/src/components/design-editor/node-components/action/index.tsx index b44ccf9..148dc33 100644 --- a/packages/flow-design/src/components/design-editor/node-components/action/index.tsx +++ b/packages/flow-design/src/components/design-editor/node-components/action/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button, Form, Popconfirm, Space, Switch } from "antd"; +import { Button, Form, Popconfirm, Space, Switch, Typography } from "antd"; import { Table } from "@coding-flow/flow-pc-ui"; import { useNodeRenderContext } from "@/components/design-editor/hooks/use-node-render-context"; import { PlusOutlined } from "@ant-design/icons"; @@ -23,10 +23,18 @@ export const ActionTable: React.FC = (props) => { const columns = React.useCallback(() => { return [ { - title: 'id', + title: 'ID', dataIndex: 'id', key: 'id', - hidden: true, + render: (value: string) => { + return ( + + {value} + + ) + } }, { title: '标题', diff --git a/packages/flow-icons/CHANGELOG.md b/packages/flow-icons/CHANGELOG.md index eeb771a..6eb2ccd 100644 --- a/packages/flow-icons/CHANGELOG.md +++ b/packages/flow-icons/CHANGELOG.md @@ -1,5 +1,11 @@ # @coding-flow/flow-icons +## 0.1.8 + +### Patch Changes + +- @coding-flow/flow-core@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-icons/package.json b/packages/flow-icons/package.json index f2fc3b4..81dfb5b 100644 --- a/packages/flow-icons/package.json +++ b/packages/flow-icons/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-icons", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine icons ", "type": "module", "sideEffects": [ diff --git a/packages/flow-mobile/flow-mobile-approval/CHANGELOG.md b/packages/flow-mobile/flow-mobile-approval/CHANGELOG.md index 7d0a8a6..517c8f9 100644 --- a/packages/flow-mobile/flow-mobile-approval/CHANGELOG.md +++ b/packages/flow-mobile/flow-mobile-approval/CHANGELOG.md @@ -1,5 +1,18 @@ # @coding-flow/flow-mobile-approval +## 0.1.8 + +### Patch Changes + +- 审批弹框标题/内容自定义能力(DialogContentProvider)+ 设计器按钮 ID 可复制 +- Updated dependencies + - @coding-flow/flow-approval-presenter@0.1.8 + - @coding-flow/flow-mobile-ui@0.1.8 + - @coding-flow/flow-core@0.1.8 + - @coding-flow/flow-icons@0.1.8 + - @coding-flow/flow-mobile-form@0.1.8 + - @coding-flow/flow-types@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-mobile/flow-mobile-approval/package.json b/packages/flow-mobile/flow-mobile-approval/package.json index 59c01a7..5ee097b 100644 --- a/packages/flow-mobile/flow-mobile-approval/package.json +++ b/packages/flow-mobile/flow-mobile-approval/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-mobile-approval", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine pc mobile approval components", "type": "module", "sideEffects": [ diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/add-audit.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/add-audit.tsx index b7cf530..5ae56df 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/add-audit.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/add-audit.tsx @@ -1,7 +1,7 @@ import React from "react"; import {FlowActionProps} from "./type"; import {Form, Toast} from "antd-mobile"; -import {ApprovalViewPluginAction, useApprovalContext} from "@coding-flow/flow-approval-presenter"; +import {ApprovalViewPluginAction, DialogContent, useApprovalContext} from "@coding-flow/flow-approval-presenter"; import {AddAuditView} from "@/plugins/view/add-audit-view"; import {PopupModal} from "@coding-flow/flow-mobile-ui"; import {EventBus, ViewBindPlugin, FlowMessageKey, FlowMessageRegistry} from "@coding-flow/flow-core"; @@ -22,10 +22,24 @@ export const AddAuditAction: React.FC = (props) => { const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const actionRef = React.useRef(null); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = ()=>{ + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if(dialogContent?.content){ + handleSubmit(); + return; + } if(actionRef.current){ actionRef.current.onValidate().then(res=>{ if(res){ @@ -54,8 +68,7 @@ export const AddAuditAction: React.FC = (props) => { React.useEffect(()=>{ EventBus.getInstance().on(action.id,()=>{ - form.resetFields(); - setModalVisible(true); + openModal(); }); return () => { @@ -76,7 +89,7 @@ export const AddAuditAction: React.FC = (props) => { return ( <> setModalVisible(false)} @@ -84,6 +97,9 @@ export const AddAuditAction: React.FC = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/delegate.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/delegate.tsx index 1ef4b1e..794f672 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/delegate.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/delegate.tsx @@ -1,7 +1,7 @@ import React from "react"; import {FlowActionProps} from "./type"; import {Form, Toast} from "antd-mobile"; -import {ApprovalViewPluginAction, useApprovalContext} from "@coding-flow/flow-approval-presenter"; +import {ApprovalViewPluginAction, DialogContent, useApprovalContext} from "@coding-flow/flow-approval-presenter"; import {DelegateView} from "@/plugins/view/delegate-view"; import {EventBus, ViewBindPlugin, FlowMessageKey, FlowMessageRegistry} from "@coding-flow/flow-core"; import {PopupModal} from "@coding-flow/flow-mobile-ui"; @@ -22,10 +22,24 @@ export const DelegateAction: React.FC = (props) => { const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const actionRef = React.useRef(null); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = ()=>{ + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if(dialogContent?.content){ + handleSubmit(); + return; + } if(actionRef.current){ actionRef.current.onValidate().then(res=>{ if(res){ @@ -54,8 +68,7 @@ export const DelegateAction: React.FC = (props) => { React.useEffect(()=>{ EventBus.getInstance().on(action.id,()=>{ - form.resetFields(); - setModalVisible(true); + openModal(); }); return () => { @@ -76,7 +89,7 @@ export const DelegateAction: React.FC = (props) => { return ( <> setModalVisible(false)} @@ -84,6 +97,9 @@ export const DelegateAction: React.FC = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/pass.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/pass.tsx index 38e3e50..fc5c68c 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/pass.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/pass.tsx @@ -2,7 +2,7 @@ import React from "react"; import {Form, TextArea, Toast} from "antd-mobile"; import {PopupModal} from "@coding-flow/flow-mobile-ui"; import {FlowActionProps} from "./type"; -import {ApprovalViewPluginAction, useApprovalContext} from "@coding-flow/flow-approval-presenter"; +import {ApprovalViewPluginAction, DialogContent, useApprovalContext} from "@coding-flow/flow-approval-presenter"; import {SignKeyView} from "@/plugins/view/sign-key-view"; import {EventBus, ViewBindPlugin, FlowMessageKey, FlowMessageRegistry} from "@coding-flow/flow-core"; import {NodeOption} from "@coding-flow/flow-types"; @@ -22,6 +22,7 @@ export const PassAction: React.FC = (props) => { const actionPresenter = context.getPresenter().getFlowActionPresenter(); const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const [options, setOptions] = React.useState([]); const [request, setRequest] = React.useState({}); const [responseType, setResponseType] = React.useState(null); @@ -33,13 +34,20 @@ export const PassAction: React.FC = (props) => { const [form] = Form.useForm(); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + React.useEffect(() => { EventBus.getInstance().on(action.id, () => { if (isStartNode) { handleSubmit(); } else { - form.resetFields(); - setModalVisible(true); + openModal(); } }); @@ -51,6 +59,11 @@ export const PassAction: React.FC = (props) => { const actionRef = React.useRef(null); const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -106,6 +119,7 @@ export const PassAction: React.FC = (props) => { return ( <> = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { )}
+ )}
{options && options.length > 0 && responseType === 'OPERATOR_SELECT' && ( diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/reject.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/reject.tsx index dc0e24e..818c0f3 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/reject.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/reject.tsx @@ -1,7 +1,7 @@ import React from "react"; import {FlowActionProps} from "./type"; import {Form, TextArea, Toast} from "antd-mobile"; -import {useApprovalContext} from "@coding-flow/flow-approval-presenter"; +import {DialogContent, useApprovalContext} from "@coding-flow/flow-approval-presenter"; import {PopupModal} from "@coding-flow/flow-mobile-ui"; import {EventBus, ViewBindPlugin, FlowMessageKey, FlowMessageRegistry} from "@coding-flow/flow-core"; import {APPROVAL_ACTION_REJECT_KEY} from "@/components/flow-approval"; @@ -18,13 +18,20 @@ export const RejectAction: React.FC = (props) => { const actionPresenter = context.getPresenter().getFlowActionPresenter(); const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const [form] = Form.useForm(); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } React.useEffect(() => { EventBus.getInstance().on(action.id, () => { - form.resetFields(); - setModalVisible(true); + openModal(); }); return () => { @@ -67,14 +74,22 @@ export const RejectAction: React.FC = (props) => { return ( <> setModalVisible(false)} onOk={() => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } form.submit(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { )}
+ )}
) diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/return.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/return.tsx index b0e5975..b71c0bb 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/return.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/return.tsx @@ -1,7 +1,7 @@ import React from "react"; import {FlowActionProps} from "./type"; import {Form, Toast} from "antd-mobile"; -import {ApprovalViewPluginAction, useApprovalContext} from "@coding-flow/flow-approval-presenter"; +import {ApprovalViewPluginAction, DialogContent, useApprovalContext} from "@coding-flow/flow-approval-presenter"; import {ReturnView} from "@/plugins/view/return-view"; import {PopupModal} from "@coding-flow/flow-mobile-ui"; import {EventBus, ViewBindPlugin, FlowMessageKey, FlowMessageRegistry} from "@coding-flow/flow-core"; @@ -22,10 +22,24 @@ export const ReturnAction: React.FC = (props) => { const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const actionRef = React.useRef(null); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -54,8 +68,7 @@ export const ReturnAction: React.FC = (props) => { React.useEffect(() => { EventBus.getInstance().on(action.id, () => { - form.resetFields(); - setModalVisible(true); + openModal(); }); return () => { @@ -77,7 +90,7 @@ export const ReturnAction: React.FC = (props) => { return ( <> setModalVisible(false)} @@ -85,6 +98,9 @@ export const ReturnAction: React.FC = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx index 8b8a77d..e32387d 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx @@ -1,7 +1,7 @@ import React from "react"; import {FlowActionProps} from "./type"; import {Form, Toast} from "antd-mobile"; -import {ApprovalViewPluginAction, useApprovalContext} from "@coding-flow/flow-approval-presenter"; +import {ApprovalViewPluginAction, DialogContent, useApprovalContext} from "@coding-flow/flow-approval-presenter"; import {TransferView} from "@/plugins/view/transfer-view"; import {EventBus, ViewBindPlugin, FlowMessageKey, FlowMessageRegistry} from "@coding-flow/flow-core"; import {PopupModal} from "@coding-flow/flow-mobile-ui"; @@ -22,11 +22,25 @@ export const TransferAction: React.FC = (props) => { const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const actionRef = React.useRef(null); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -40,8 +54,7 @@ export const TransferAction: React.FC = (props) => { React.useEffect(() => { EventBus.getInstance().on(action.id, () => { - form.resetFields(); - setModalVisible(true); + openModal(); }); return () => { @@ -76,7 +89,7 @@ export const TransferAction: React.FC = (props) => { return ( <> setModalVisible(false)} @@ -84,6 +97,9 @@ export const TransferAction: React.FC = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-mobile/flow-mobile-form/CHANGELOG.md b/packages/flow-mobile/flow-mobile-form/CHANGELOG.md index 887713d..f5b12e4 100644 --- a/packages/flow-mobile/flow-mobile-form/CHANGELOG.md +++ b/packages/flow-mobile/flow-mobile-form/CHANGELOG.md @@ -1,5 +1,12 @@ # @coding-flow/flow-mobile-form +## 0.1.8 + +### Patch Changes + +- @coding-flow/flow-core@0.1.8 +- @coding-flow/flow-types@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-mobile/flow-mobile-form/package.json b/packages/flow-mobile/flow-mobile-form/package.json index 7459dbd..2b1257b 100644 --- a/packages/flow-mobile/flow-mobile-form/package.json +++ b/packages/flow-mobile/flow-mobile-form/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-mobile-form", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine mobile form engine", "type": "module", "sideEffects": [ diff --git a/packages/flow-mobile/flow-mobile-ui/CHANGELOG.md b/packages/flow-mobile/flow-mobile-ui/CHANGELOG.md index 469523e..b4cb206 100644 --- a/packages/flow-mobile/flow-mobile-ui/CHANGELOG.md +++ b/packages/flow-mobile/flow-mobile-ui/CHANGELOG.md @@ -1,5 +1,12 @@ # @coding-flow/flow-mobile-ui +## 0.1.8 + +### Patch Changes + +- 审批弹框标题/内容自定义能力(DialogContentProvider)+ 设计器按钮 ID 可复制 + - @coding-flow/flow-core@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-mobile/flow-mobile-ui/package.json b/packages/flow-mobile/flow-mobile-ui/package.json index f574bee..eca0df8 100644 --- a/packages/flow-mobile/flow-mobile-ui/package.json +++ b/packages/flow-mobile/flow-mobile-ui/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-mobile-ui", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine mobile ui components", "type": "module", "sideEffects": [ diff --git a/packages/flow-mobile/flow-mobile-ui/src/components/popup-modal/index.tsx b/packages/flow-mobile/flow-mobile-ui/src/components/popup-modal/index.tsx index e0c6597..1ff7206 100644 --- a/packages/flow-mobile/flow-mobile-ui/src/components/popup-modal/index.tsx +++ b/packages/flow-mobile/flow-mobile-ui/src/components/popup-modal/index.tsx @@ -7,7 +7,7 @@ interface PopupModalProps { onClose?: () => void; children?: React.ReactNode; onOk?: () => void; - title?: string; + title?: React.ReactNode; loading?: boolean; /** 弹层高度,默认 40vh。用于顺序弹出多个弹层时以不同高度区分层级 */ height?: string; diff --git a/packages/flow-pc/flow-pc-approval/CHANGELOG.md b/packages/flow-pc/flow-pc-approval/CHANGELOG.md index 87aee00..b5adc1e 100644 --- a/packages/flow-pc/flow-pc-approval/CHANGELOG.md +++ b/packages/flow-pc/flow-pc-approval/CHANGELOG.md @@ -1,5 +1,18 @@ # @coding-flow/flow-pc-approval +## 0.1.8 + +### Patch Changes + +- 审批弹框标题/内容自定义能力(DialogContentProvider)+ 设计器按钮 ID 可复制 +- Updated dependencies + - @coding-flow/flow-approval-presenter@0.1.8 + - @coding-flow/flow-core@0.1.8 + - @coding-flow/flow-icons@0.1.8 + - @coding-flow/flow-pc-form@0.1.8 + - @coding-flow/flow-pc-ui@0.1.8 + - @coding-flow/flow-types@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-pc/flow-pc-approval/package.json b/packages/flow-pc/flow-pc-approval/package.json index d5e9a97..49737c0 100644 --- a/packages/flow-pc/flow-pc-approval/package.json +++ b/packages/flow-pc/flow-pc-approval/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-pc-approval", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine pc form approval components", "type": "module", "sideEffects": [ diff --git a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/add-audit.tsx b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/add-audit.tsx index 4da3056..9d0dbd1 100644 --- a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/add-audit.tsx +++ b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/add-audit.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FlowActionProps } from "./type"; import { Form, message } from "antd"; -import { ApprovalViewPluginAction, useApprovalContext } from "@coding-flow/flow-approval-presenter"; +import { ApprovalViewPluginAction, DialogContent, useApprovalContext } from "@coding-flow/flow-approval-presenter"; import { AddAuditView } from "@/plugins/view/add-audit-view"; import { CustomStyleButton } from "@/components/flow-approval/components/custom-style-button"; import { ViewBindPlugin, FlowMessageKey, FlowMessageRegistry, EventBus } from "@coding-flow/flow-core"; @@ -23,10 +23,24 @@ export const AddAuditAction: React.FC = (props) => { const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const actionRef = React.useRef(null); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -56,8 +70,7 @@ export const AddAuditAction: React.FC = (props) => { React.useEffect(() => { EventBus.getInstance().on(action.id, () => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }); @@ -86,8 +99,7 @@ export const AddAuditAction: React.FC = (props) => { display={props.action.display} onClick={() => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }} title={action.title} @@ -95,7 +107,7 @@ export const AddAuditAction: React.FC = (props) => { )} = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/delegate.tsx b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/delegate.tsx index a5ab040..a163549 100644 --- a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/delegate.tsx +++ b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/delegate.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FlowActionProps } from "./type"; import { Form, message } from "antd"; -import { ApprovalViewPluginAction, useApprovalContext } from "@coding-flow/flow-approval-presenter"; +import { ApprovalViewPluginAction, DialogContent, useApprovalContext } from "@coding-flow/flow-approval-presenter"; import { DelegateView } from "@/plugins/view/delegate-view"; import { CustomStyleButton } from "@/components/flow-approval/components/custom-style-button"; import { APPROVAL_ACTION_DELEGATE_KEY } from "@/components/flow-approval"; @@ -23,10 +23,24 @@ export const DelegateAction: React.FC = (props) => { const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const actionRef = React.useRef(null); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -56,8 +70,7 @@ export const DelegateAction: React.FC = (props) => { React.useEffect(() => { EventBus.getInstance().on(action.id, () => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }); @@ -85,8 +98,7 @@ export const DelegateAction: React.FC = (props) => { display={props.action.display} onClick={() => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }} title={action.title} @@ -94,7 +106,7 @@ export const DelegateAction: React.FC = (props) => { )} setModalVisible(false)} @@ -102,6 +114,9 @@ export const DelegateAction: React.FC = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/pass.tsx b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/pass.tsx index 6bdd82e..cb07d1d 100644 --- a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/pass.tsx +++ b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/pass.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FlowActionProps } from "./type"; import { Form, Input, message } from "antd"; -import { ApprovalViewPluginAction, useApprovalContext } from "@coding-flow/flow-approval-presenter"; +import { ApprovalViewPluginAction, DialogContent, useApprovalContext } from "@coding-flow/flow-approval-presenter"; import { SignKeyView } from "@/plugins/view/sign-key-view"; import { CustomStyleButton } from "@/components/flow-approval/components/custom-style-button"; import { NodeOption } from "@coding-flow/flow-types"; @@ -26,6 +26,8 @@ export const PassAction: React.FC = (props) => { const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); + const [options, setOptions] = React.useState([]); const [request, setRequest] = React.useState({}); @@ -40,6 +42,14 @@ export const PassAction: React.FC = (props) => { const [form] = Form.useForm(); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handleSubmit = (params?: any) => { actionPresenter.action(action.id, params).then((res) => { if (res.success) { @@ -69,8 +79,7 @@ export const PassAction: React.FC = (props) => { if (isStartNode) { handleSubmit(); } else { - form.resetFields(); - setModalVisible(true); + openModal(); } } }); @@ -83,6 +92,11 @@ export const PassAction: React.FC = (props) => { const actionRef = React.useRef(null); const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -124,8 +138,7 @@ export const PassAction: React.FC = (props) => { if (isStartNode) { handleSubmit(); } else { - form.resetFields(); - setModalVisible(true); + openModal(); } } }} @@ -134,7 +147,7 @@ export const PassAction: React.FC = (props) => { )} = (props) => { padding: 16, }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { )}
+ )}
diff --git a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/reject.tsx b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/reject.tsx index b2f80fc..a153b37 100644 --- a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/reject.tsx +++ b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/reject.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FlowActionProps } from "./type"; import { Form, Input, message } from "antd"; -import { useApprovalContext } from "@coding-flow/flow-approval-presenter"; +import { DialogContent, useApprovalContext } from "@coding-flow/flow-approval-presenter"; import { CustomStyleButton } from "@/components/flow-approval/components/custom-style-button"; import { APPROVAL_ACTION_REJECT_KEY } from "@/components/flow-approval"; import { ViewBindPlugin, FlowMessageKey, FlowMessageRegistry, EventBus } from "@coding-flow/flow-core"; @@ -21,8 +21,17 @@ export const RejectAction: React.FC = (props) => { const actionPresenter = context.getPresenter().getFlowActionPresenter(); const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const [form] = Form.useForm(); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handleSubmit = (params?: any) => { actionPresenter.action(action.id, params).then((res) => { if (res.success) { @@ -41,8 +50,7 @@ export const RejectAction: React.FC = (props) => { React.useEffect(() => { EventBus.getInstance().on(action.id, () => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }); @@ -79,8 +87,7 @@ export const RejectAction: React.FC = (props) => { display={props.action.display} onClick={() => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }} title={action.title} @@ -89,7 +96,7 @@ export const RejectAction: React.FC = (props) => { } = (props) => { }} onCancel={() => setModalVisible(false)} onOk={() => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } form.submit(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { )}
+ )}
) diff --git a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/return.tsx b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/return.tsx index 377e596..f5edde0 100644 --- a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/return.tsx +++ b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/return.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FlowActionProps } from "./type"; import { Form, message } from "antd"; -import { ApprovalViewPluginAction, useApprovalContext } from "@coding-flow/flow-approval-presenter"; +import { ApprovalViewPluginAction, DialogContent, useApprovalContext } from "@coding-flow/flow-approval-presenter"; import { ReturnView } from "@/plugins/view/return-view"; import { CustomStyleButton } from "@/components/flow-approval/components/custom-style-button"; import { APPROVAL_ACTION_RETURN_KEY } from "@/components/flow-approval"; @@ -23,11 +23,25 @@ export const ReturnAction: React.FC = (props) => { const actionLoading = state.actionLoading ?? false; const [modalVisible, setModalVisible] = React.useState(false); + const [dialogContent, setDialogContent] = React.useState(null); const actionRef = React.useRef(null); + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -57,8 +71,7 @@ export const ReturnAction: React.FC = (props) => { React.useEffect(() => { EventBus.getInstance().on(action.id, () => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }); @@ -86,8 +99,7 @@ export const ReturnAction: React.FC = (props) => { display={props.action.display} onClick={() => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }} title={action.title} @@ -95,7 +107,7 @@ export const ReturnAction: React.FC = (props) => { )} = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/transfer.tsx b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/transfer.tsx index 8767e8d..e1fda7a 100644 --- a/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/transfer.tsx +++ b/packages/flow-pc/flow-pc-approval/src/components/flow-approval/components/action/transfer.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FlowActionProps } from "./type"; import { Form, message } from "antd"; -import { ApprovalViewPluginAction, useApprovalContext } from "@coding-flow/flow-approval-presenter"; +import { ApprovalViewPluginAction, DialogContent, useApprovalContext } from "@coding-flow/flow-approval-presenter"; import { TransferView } from "@/plugins/view/transfer-view"; import { CustomStyleButton } from "@/components/flow-approval/components/custom-style-button"; import { APPROVAL_ACTION_TRANSFER_KEY } from "@/components/flow-approval"; @@ -25,7 +25,22 @@ export const TransferAction: React.FC = (props) => { const actionRef = React.useRef(null); + const [dialogContent, setDialogContent] = React.useState(null); + + /** 打开弹框:解析自定义弹框内容(标题/中间内容),并重置表单 */ + const openModal = () => { + form.resetFields(); + setDialogContent(null); + actionPresenter.resolveDialogContent(action.id).then(setDialogContent); + setModalVisible(true); + } + const handlerOK = () => { + // 自定义弹框内容时退化为纯确认框,直接提交(无表单可校验) + if (dialogContent?.content) { + handleSubmit(); + return; + } if (actionRef.current) { actionRef.current.onValidate().then(res => { if (res) { @@ -57,8 +72,7 @@ export const TransferAction: React.FC = (props) => { React.useEffect(() => { EventBus.getInstance().on(action.id, () => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }); @@ -86,8 +100,7 @@ export const TransferAction: React.FC = (props) => { display={props.action.display} onClick={() => { if (props.onClickCheck?.(action.id)) { - form.resetFields(); - setModalVisible(true); + openModal(); } }} title={action.title} @@ -95,7 +108,7 @@ export const TransferAction: React.FC = (props) => { )} = (props) => { handlerOK(); }} > + {dialogContent?.content ? ( + dialogContent.content + ) : (
= (props) => { />
+ )}
) diff --git a/packages/flow-pc/flow-pc-form/CHANGELOG.md b/packages/flow-pc/flow-pc-form/CHANGELOG.md index 43c8d43..4438fc6 100644 --- a/packages/flow-pc/flow-pc-form/CHANGELOG.md +++ b/packages/flow-pc/flow-pc-form/CHANGELOG.md @@ -1,5 +1,12 @@ # @coding-flow/flow-pc-form +## 0.1.8 + +### Patch Changes + +- @coding-flow/flow-core@0.1.8 +- @coding-flow/flow-types@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-pc/flow-pc-form/package.json b/packages/flow-pc/flow-pc-form/package.json index ac607e0..94cf6e5 100644 --- a/packages/flow-pc/flow-pc-form/package.json +++ b/packages/flow-pc/flow-pc-form/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-pc-form", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine pc form engine", "type": "module", "sideEffects": [ diff --git a/packages/flow-pc/flow-pc-ui/CHANGELOG.md b/packages/flow-pc/flow-pc-ui/CHANGELOG.md index 75e70df..bc67bf0 100644 --- a/packages/flow-pc/flow-pc-ui/CHANGELOG.md +++ b/packages/flow-pc/flow-pc-ui/CHANGELOG.md @@ -1,5 +1,11 @@ # @coding-flow/flow-pc-ui +## 0.1.8 + +### Patch Changes + +- @coding-flow/flow-core@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-pc/flow-pc-ui/package.json b/packages/flow-pc/flow-pc-ui/package.json index 6447317..6718ba1 100644 --- a/packages/flow-pc/flow-pc-ui/package.json +++ b/packages/flow-pc/flow-pc-ui/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-pc-ui", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine pc ui components", "type": "module", "sideEffects": [ diff --git a/packages/flow-types/CHANGELOG.md b/packages/flow-types/CHANGELOG.md index bdc0f12..c095356 100644 --- a/packages/flow-types/CHANGELOG.md +++ b/packages/flow-types/CHANGELOG.md @@ -1,5 +1,11 @@ # @coding-flow/flow-types +## 0.1.8 + +### Patch Changes + +- @coding-flow/flow-core@0.1.8 + ## 0.1.7 ### Patch Changes diff --git a/packages/flow-types/package.json b/packages/flow-types/package.json index 3b0c463..e141415 100644 --- a/packages/flow-types/package.json +++ b/packages/flow-types/package.json @@ -1,6 +1,6 @@ { "name": "@coding-flow/flow-types", - "version": "0.1.7", + "version": "0.1.8", "description": "flow-engine types", "type": "module", "sideEffects": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c341d73..6ff177c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: apps/app-mobile: dependencies: + '@coding-flow/flow-approval-presenter': + specifier: workspace:* + version: link:../../packages/flow-approval-presenter '@coding-flow/flow-core': specifier: workspace:* version: link:../../packages/flow-core