/** * 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. */ /** * 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. * * @example * // 基本使用 * * * // 显示设置但隐藏删除按钮 * * * // 显示设置和title * * * // 使用右键菜单模式并隐藏删除按钮 * */ import _ from 'lodash'; import React, { useEffect } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { FlowModelProvider, useApplyAutoFlows } from '../hooks'; import { getAutoFlowError, setAutoFlowError } from '../utils'; import { FlowModel } from '../models'; import { ToolbarItemConfig } from '../types'; import { FlowErrorFallback } from './FlowErrorFallback'; import { FlowsContextMenu } from './settings/wrappers/contextual/FlowsContextMenu'; import { FlowsFloatContextMenu } from './settings/wrappers/contextual/FlowsFloatContextMenu'; import { observer } from '../reactive'; export interface FlowModelRendererProps { model?: FlowModel; uid?: string; fallback?: React.ReactNode; // 渲染失败时的回退内容 key?: React.Key; /** 是否显示流程设置入口(如按钮、菜单等) */ showFlowSettings?: | boolean | { showBackground?: boolean; showBorder?: boolean; showDragHandle?: boolean; /** 是否显示事件流入口,默认 true */ showDynamicFlowsEditor?: boolean; /** 自定义工具栏样式,`top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */ style?: React.CSSProperties; /** * @default 'inside' */ toolbarPosition?: 'inside' | 'above' | 'below'; }; // 默认 false /** 流程设置的交互风格 */ flowSettingsVariant?: 'dropdown' | 'contextMenu' | 'modal' | 'drawer'; // 默认 'dropdown' /** 是否在设置中隐藏移除按钮 */ hideRemoveInSettings?: boolean; // 默认 false /** 是否在边框左上角显示模型title,默认 false */ showTitle?: boolean; // 默认 false /** 传递给 beforeRender 事件的运行时参数 */ inputArgs?: Record; /** 是否在最外层包装 FlowErrorFallback 组件,默认 false */ showErrorFallback?: boolean; // 默认 false /** 设置菜单层级:1=仅当前模型(默认),2=包含子模型 */ settingsMenuLevel?: number; /** 额外的工具栏项目,仅应用于此实例 */ extraToolbarItems?: ToolbarItemConfig[]; useCache?: boolean; } /** * 内部组件:带有 useApplyAutoFlows 的渲染器 */ const FlowModelRendererWithAutoFlows: React.FC<{ model: FlowModel; showFlowSettings: | boolean | { showBackground?: boolean; showBorder?: boolean; showDragHandle?: boolean; showDynamicFlowsEditor?: boolean; /** `top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */ style?: React.CSSProperties; /** * @default 'inside' */ toolbarPosition?: 'inside' | 'above' | 'below'; }; flowSettingsVariant: string; hideRemoveInSettings: boolean; showTitle: boolean; inputArgs?: Record; showErrorFallback?: boolean; settingsMenuLevel?: number; extraToolbarItems?: ToolbarItemConfig[]; fallback?: React.ReactNode; useCache?: boolean; }> = observer( ({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, showTitle, inputArgs, showErrorFallback, settingsMenuLevel, extraToolbarItems, fallback, useCache, }) => { // hidden 占位由模型自身处理;无需在此注入 const { loading: pending, error: autoFlowsError } = useApplyAutoFlows(model, inputArgs, { throwOnError: false, useCache, }); // 将错误下沉到 model 实例上,供内容层读取(类型安全的 WeakMap 存储) setAutoFlowError(model, autoFlowsError || null); if (pending) { return <>{fallback}; } return ( ); }, ); // 移除不带 beforeRender 执行的渲染器,统一触发 beforeRender 事件 /** * 核心渲染逻辑组件 */ const FlowModelRendererCore: React.FC<{ model: FlowModel; showFlowSettings: | boolean | { showBackground?: boolean; showBorder?: boolean; showDragHandle?: boolean; showDynamicFlowsEditor?: boolean; /** `top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */ style?: React.CSSProperties; /** * @default 'inside' */ toolbarPosition?: 'inside' | 'above' | 'below'; }; flowSettingsVariant: string; hideRemoveInSettings: boolean; showTitle: boolean; showErrorFallback?: boolean; settingsMenuLevel?: number; extraToolbarItems?: ToolbarItemConfig[]; }> = observer( ({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, showTitle, showErrorFallback, settingsMenuLevel, extraToolbarItems, }) => { const wrapWithErrorBoundary = (children: React.ReactNode) => { if (showErrorFallback) { return {children}; } return children; }; const ContentOrError: React.FC = () => { const autoError = getAutoFlowError(model); if (autoError) { // 将 beforeRender 事件错误转化为内容区错误,由内层边界兜住 throw autoError; } const rendered = model.render(); // RenderFunction 模式:render 返回函数,作为组件类型渲染,避免函数作为子节点的警告 if (typeof rendered === 'function') { return React.createElement(rendered as React.ComponentType); } return <>{rendered}; }; // 如果不显示流程设置,直接返回模型内容(可能包装 ErrorBoundary) // 当模型类或 use 变化时重挂载内容,规避组件内部状态残留 const rawUse = (model as any)?.use; const resolvedName = (model as any)?.constructor?.name || model.uid; const contentKey = typeof rawUse === 'string' ? `${rawUse}:${model.uid}` : `${resolvedName}:${model.uid}`; if (!showFlowSettings) { return wrapWithErrorBoundary(
, ); } // 根据 flowSettingsVariant 包装相应的设置组件 switch (flowSettingsVariant) { case 'dropdown': return ( {wrapWithErrorBoundary(
, )}
); case 'contextMenu': return ( {wrapWithErrorBoundary(
, )}
); case 'modal': // TODO: 实现 modal 模式的流程设置 console.warn('FlowModelRenderer: modal variant is not implemented yet'); return wrapWithErrorBoundary(); case 'drawer': // TODO: 实现 drawer 模式的流程设置 console.warn('FlowModelRenderer: drawer variant is not implemented yet'); return wrapWithErrorBoundary(); default: console.warn( `FlowModelRenderer: Unknown flowSettingsVariant '${flowSettingsVariant}', falling back to dropdown`, ); return ( {wrapWithErrorBoundary(
, )}
); } }, { displayName: 'FlowModelRendererCore', }, ); /** * A React component responsible for rendering a FlowModel. * It delegates the actual rendering logic to the `render` method of the provided model. * * @param {FlowModelRendererProps} props - The component's props. * @param {FlowModel} props.model - The FlowModel instance to render. * @param {string} props.uid - The unique identifier for the flow model. * @param {boolean} props.showFlowSettings - Whether to show flow settings entry (buttons, menus, etc.). * @param {string} props.flowSettingsVariant - The interaction style for flow settings. * @param {boolean} props.hideRemoveInSettings - Whether to hide remove button in settings. * @param {boolean} props.showTitle - Whether to show model title in the top-left corner of the border. * @param {any} props.inputArgs - Runtime arguments to pass to beforeRender event flows. * @param {number} props.settingsMenuLevel - Settings menu levels: 1=current model only (default), 2=include sub-models. * @param {ToolbarItemConfig[]} props.extraToolbarItems - Extra toolbar items to add to this renderer instance. * @returns {React.ReactNode | null} The rendered output of the model, or null if the model or its render method is invalid. */ export const FlowModelRenderer: React.FC = observer( ({ model, fallback = null, showFlowSettings = false, flowSettingsVariant = 'dropdown', hideRemoveInSettings = false, showTitle = false, inputArgs, showErrorFallback = true, settingsMenuLevel, extraToolbarItems, useCache, }) => { const resolvedUseCache = typeof useCache === 'boolean' ? useCache : model?.context?.useCache; useEffect(() => { if (model?.context && typeof resolvedUseCache !== 'undefined') { model.context.defineProperty('useCache', { value: resolvedUseCache, }); } }, [model?.context, resolvedUseCache]); if (!model || typeof model.render !== 'function') { // 可以选择渲染 null 或者一个错误/提示信息 console.warn('FlowModelRenderer: Invalid model or render method not found.', model); return null; } // 构建渲染内容:统一在渲染前触发 beforeRender 事件(带缓存) const content = ( ); // 当需要错误回退时,将整体包裹在 ErrorBoundary 和 FlowModelProvider 中 // 这样既能捕获 useApplyAutoFlows 中抛出的错误,也能在回退组件中获取 model 上下文 if (showErrorFallback) { return ( {content} ); } return content; }, ); // 为需要进一步优化渲染的场景提供一个 Memo 包装版本 // 仅在父级重渲且 props 浅比较未变时跳过渲染;不影响内部响应式更新 export const MemoFlowModelRenderer = React.memo(FlowModelRenderer); MemoFlowModelRenderer.displayName = 'MemoFlowModelRenderer';