mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
feat: add error boundary
This commit is contained in:
parent
1fc045e45c
commit
c4d31e802d
@ -18,7 +18,12 @@ function Grid({ items }) {
|
|||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<div key={item.uid} style={{ marginBottom: 16 }}>
|
<div key={item.uid} style={{ marginBottom: 16 }}>
|
||||||
<FlowModelRenderer model={item} key={item.uid} showFlowSettings={{ showBackground: false }} />
|
<FlowModelRenderer
|
||||||
|
model={item}
|
||||||
|
key={item.uid}
|
||||||
|
showFlowSettings={{ showBackground: false }}
|
||||||
|
showErrorFallback
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
314
packages/core/flow-engine/src/components/FlowErrorFallback.tsx
Normal file
314
packages/core/flow-engine/src/components/FlowErrorFallback.tsx
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Button, Result, Typography } from 'antd';
|
||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { FallbackProps } from 'react-error-boundary';
|
||||||
|
import { useFlowModel } from '../hooks/useFlowModel';
|
||||||
|
|
||||||
|
const { Paragraph, Text } = Typography;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部组件,用于获取 FlowModel
|
||||||
|
*/
|
||||||
|
const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const model = useFlowModel(); // 在这里安全地使用 Hook
|
||||||
|
|
||||||
|
const handleCopyError = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const errorInfo = {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
modelInfo: model
|
||||||
|
? {
|
||||||
|
uid: model.uid,
|
||||||
|
className: model.constructor.name,
|
||||||
|
props: model.props,
|
||||||
|
stepParams: model.stepParams,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(JSON.stringify(errorInfo, null, 2));
|
||||||
|
console.log('Error information copied to clipboard');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy error information:', err);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否可以下载日志
|
||||||
|
const canDownloadLogs = model?.ctx?.globals?.api;
|
||||||
|
|
||||||
|
const handleDownloadLogs = async () => {
|
||||||
|
if (!canDownloadLogs) {
|
||||||
|
console.error('API client not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 从 model.ctx.globals.api 获取 apiClient
|
||||||
|
const apiClient = model.ctx.globals.api;
|
||||||
|
|
||||||
|
// 从 window 对象获取位置信息
|
||||||
|
const location = {
|
||||||
|
pathname: window.location.pathname,
|
||||||
|
search: window.location.search,
|
||||||
|
hash: window.location.hash,
|
||||||
|
href: window.location.href,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await apiClient.request({
|
||||||
|
url: 'logger:collect',
|
||||||
|
method: 'post',
|
||||||
|
responseType: 'blob',
|
||||||
|
data: {
|
||||||
|
error: {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
},
|
||||||
|
location,
|
||||||
|
modelInfo: model
|
||||||
|
? {
|
||||||
|
uid: model.uid,
|
||||||
|
className: model.constructor.name,
|
||||||
|
props: model.props,
|
||||||
|
stepParams: model.stepParams,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res.data], { type: 'application/gzip' }));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', 'logs.tar.gz');
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to download logs:', err);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const subTitle = (
|
||||||
|
<span>
|
||||||
|
{'This is likely a NocoBase internals bug. Please open an issue at '}
|
||||||
|
<a href="https://github.com/nocobase/nocobase/issues" target="_blank" rel="noopener noreferrer">
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
{model && (
|
||||||
|
<div style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}>
|
||||||
|
Model: {model.constructor.name} (uid: {model.uid})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ backgroundColor: 'white' }}>
|
||||||
|
<Result
|
||||||
|
style={{ maxWidth: '60vw', margin: 'auto' }}
|
||||||
|
status="error"
|
||||||
|
title="Render Failed"
|
||||||
|
subTitle={subTitle}
|
||||||
|
extra={[
|
||||||
|
<Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank">
|
||||||
|
Feedback
|
||||||
|
</Button>,
|
||||||
|
canDownloadLogs && (
|
||||||
|
<Button key="log" loading={loading} onClick={handleDownloadLogs}>
|
||||||
|
Download logs
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
<Button key="copy" loading={loading} onClick={handleCopyError}>
|
||||||
|
Copy Error Info
|
||||||
|
</Button>,
|
||||||
|
resetErrorBoundary && (
|
||||||
|
<Button key="retry" danger onClick={resetErrorBoundary}>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
].filter(Boolean)}
|
||||||
|
>
|
||||||
|
<Paragraph copyable={{ text: error.stack }}>
|
||||||
|
<Text type="danger" style={{ whiteSpace: 'pre-line', textAlign: 'center' }}>
|
||||||
|
{error.stack}
|
||||||
|
</Text>
|
||||||
|
</Paragraph>
|
||||||
|
</Result>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简化版的错误回退组件,专为 flow-engine 设计
|
||||||
|
* 保持与原始 ErrorFallback 相同的样式和结构
|
||||||
|
*/
|
||||||
|
export const FlowErrorFallback: FC<FallbackProps> & {
|
||||||
|
Modal: FC<FallbackProps & { children?: React.ReactNode }>;
|
||||||
|
} = ({ error, resetErrorBoundary }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 尝试渲染内部组件,如果不在 FlowModelProvider 中则显示简化版本
|
||||||
|
try {
|
||||||
|
return <FlowErrorFallbackInner error={error} resetErrorBoundary={resetErrorBoundary} />;
|
||||||
|
} catch {
|
||||||
|
// 如果不在 FlowModelProvider 中,显示简化版本(不包含 model 信息)
|
||||||
|
const handleCopyError = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const errorInfo = {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(JSON.stringify(errorInfo, null, 2));
|
||||||
|
console.log('Error information copied to clipboard');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy error information:', err);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const subTitle = (
|
||||||
|
<span>
|
||||||
|
{'This is likely a NocoBase internals bug. Please open an issue at '}
|
||||||
|
<a href="https://github.com/nocobase/nocobase/issues" target="_blank" rel="noopener noreferrer">
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ backgroundColor: 'white' }}>
|
||||||
|
<Result
|
||||||
|
style={{ maxWidth: '60vw', margin: 'auto' }}
|
||||||
|
status="error"
|
||||||
|
title="Render Failed"
|
||||||
|
subTitle={subTitle}
|
||||||
|
extra={[
|
||||||
|
<Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank">
|
||||||
|
Feedback
|
||||||
|
</Button>,
|
||||||
|
<Button key="copy" loading={loading} onClick={handleCopyError}>
|
||||||
|
Copy Error Info
|
||||||
|
</Button>,
|
||||||
|
resetErrorBoundary && (
|
||||||
|
<Button key="retry" danger onClick={resetErrorBoundary}>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
].filter(Boolean)}
|
||||||
|
>
|
||||||
|
<Paragraph copyable={{ text: error.stack }}>
|
||||||
|
<Text type="danger" style={{ whiteSpace: 'pre-line', textAlign: 'center' }}>
|
||||||
|
{error.stack}
|
||||||
|
</Text>
|
||||||
|
</Paragraph>
|
||||||
|
</Result>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模态框版本的错误回退组件
|
||||||
|
*/
|
||||||
|
export const FlowErrorFallbackModal: FC<FallbackProps & { children?: React.ReactNode }> = ({
|
||||||
|
error,
|
||||||
|
resetErrorBoundary,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const defaultChildren = (
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
marginBottom: 0,
|
||||||
|
}}
|
||||||
|
copyable={{ text: error.message }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
type="danger"
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'inline-block',
|
||||||
|
maxWidth: '200px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Error: {error.message}
|
||||||
|
</Text>
|
||||||
|
</Paragraph>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onMouseOver={() => setOpen(true)}>{children || defaultChildren}</div>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10000,
|
||||||
|
}}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '24px',
|
||||||
|
maxWidth: '60vw',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '8px',
|
||||||
|
right: '12px',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '18px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#999',
|
||||||
|
}}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<FlowErrorFallback error={error} resetErrorBoundary={resetErrorBoundary} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FlowErrorFallback.Modal = FlowErrorFallbackModal;
|
@ -39,10 +39,12 @@ import { observer } from '@formily/reactive-react';
|
|||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { Suspense, useEffect } from 'react';
|
import React, { Suspense, useEffect } from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useApplyAutoFlows, useFlowExtraContext, FlowModelProvider } from '../hooks';
|
import { useApplyAutoFlows, useFlowExtraContext, FlowModelProvider } from '../hooks';
|
||||||
import { FlowModel } from '../models';
|
import { FlowModel } from '../models';
|
||||||
import { FlowsContextMenu } from './settings/wrappers/contextual/FlowsContextMenu';
|
import { FlowsContextMenu } from './settings/wrappers/contextual/FlowsContextMenu';
|
||||||
import { FlowsFloatContextMenu } from './settings/wrappers/contextual/FlowsFloatContextMenu';
|
import { FlowsFloatContextMenu } from './settings/wrappers/contextual/FlowsFloatContextMenu';
|
||||||
|
import { FlowErrorFallback } from './FlowErrorFallback';
|
||||||
|
|
||||||
interface FlowModelRendererProps {
|
interface FlowModelRendererProps {
|
||||||
model?: FlowModel;
|
model?: FlowModel;
|
||||||
@ -70,6 +72,9 @@ interface FlowModelRendererProps {
|
|||||||
|
|
||||||
/** 是否为每个组件独立执行 auto flow,默认 false */
|
/** 是否为每个组件独立执行 auto flow,默认 false */
|
||||||
independentAutoFlowExecution?: boolean; // 默认 false
|
independentAutoFlowExecution?: boolean; // 默认 false
|
||||||
|
|
||||||
|
/** 是否在最外层包装 FlowErrorFallback 组件,默认 false */
|
||||||
|
showErrorFallback?: boolean; // 默认 false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,6 +88,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|||||||
extraContext?: Record<string, any>;
|
extraContext?: Record<string, any>;
|
||||||
sharedContext?: Record<string, any>;
|
sharedContext?: Record<string, any>;
|
||||||
independentAutoFlowExecution?: boolean;
|
independentAutoFlowExecution?: boolean;
|
||||||
|
showErrorFallback?: boolean;
|
||||||
}> = observer(
|
}> = observer(
|
||||||
({
|
({
|
||||||
model,
|
model,
|
||||||
@ -92,6 +98,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|||||||
extraContext,
|
extraContext,
|
||||||
sharedContext,
|
sharedContext,
|
||||||
independentAutoFlowExecution,
|
independentAutoFlowExecution,
|
||||||
|
showErrorFallback,
|
||||||
}) => {
|
}) => {
|
||||||
const defaultExtraContext = useFlowExtraContext();
|
const defaultExtraContext = useFlowExtraContext();
|
||||||
useApplyAutoFlows(model, extraContext || defaultExtraContext, !independentAutoFlowExecution);
|
useApplyAutoFlows(model, extraContext || defaultExtraContext, !independentAutoFlowExecution);
|
||||||
@ -103,6 +110,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|||||||
showFlowSettings={showFlowSettings}
|
showFlowSettings={showFlowSettings}
|
||||||
flowSettingsVariant={flowSettingsVariant}
|
flowSettingsVariant={flowSettingsVariant}
|
||||||
hideRemoveInSettings={hideRemoveInSettings}
|
hideRemoveInSettings={hideRemoveInSettings}
|
||||||
|
showErrorFallback={showErrorFallback}
|
||||||
/>
|
/>
|
||||||
</FlowModelProvider>
|
</FlowModelProvider>
|
||||||
);
|
);
|
||||||
@ -118,18 +126,22 @@ const FlowModelRendererWithoutAutoFlows: React.FC<{
|
|||||||
flowSettingsVariant: string;
|
flowSettingsVariant: string;
|
||||||
hideRemoveInSettings: boolean;
|
hideRemoveInSettings: boolean;
|
||||||
sharedContext?: Record<string, any>;
|
sharedContext?: Record<string, any>;
|
||||||
}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, sharedContext }) => {
|
showErrorFallback?: boolean;
|
||||||
return (
|
}> = observer(
|
||||||
<FlowModelProvider model={model}>
|
({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, sharedContext, showErrorFallback }) => {
|
||||||
<FlowModelRendererCore
|
return (
|
||||||
model={model}
|
<FlowModelProvider model={model}>
|
||||||
showFlowSettings={showFlowSettings}
|
<FlowModelRendererCore
|
||||||
flowSettingsVariant={flowSettingsVariant}
|
model={model}
|
||||||
hideRemoveInSettings={hideRemoveInSettings}
|
showFlowSettings={showFlowSettings}
|
||||||
/>
|
flowSettingsVariant={flowSettingsVariant}
|
||||||
</FlowModelProvider>
|
hideRemoveInSettings={hideRemoveInSettings}
|
||||||
);
|
showErrorFallback={showErrorFallback}
|
||||||
});
|
/>
|
||||||
|
</FlowModelProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 核心渲染逻辑组件
|
* 核心渲染逻辑组件
|
||||||
@ -139,13 +151,22 @@ const FlowModelRendererCore: React.FC<{
|
|||||||
showFlowSettings: boolean | { showBackground?: boolean; showBorder?: boolean };
|
showFlowSettings: boolean | { showBackground?: boolean; showBorder?: boolean };
|
||||||
flowSettingsVariant: string;
|
flowSettingsVariant: string;
|
||||||
hideRemoveInSettings: boolean;
|
hideRemoveInSettings: boolean;
|
||||||
}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings }) => {
|
showErrorFallback?: boolean;
|
||||||
|
}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, showErrorFallback }) => {
|
||||||
// 渲染模型内容
|
// 渲染模型内容
|
||||||
const modelContent = model.render();
|
const modelContent = model.render();
|
||||||
|
|
||||||
// 如果不显示流程设置,直接返回模型内容
|
// 包装 ErrorBoundary 的辅助函数
|
||||||
|
const wrapWithErrorBoundary = (children: React.ReactNode) => {
|
||||||
|
if (showErrorFallback) {
|
||||||
|
return <ErrorBoundary FallbackComponent={FlowErrorFallback}>{children}</ErrorBoundary>;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果不显示流程设置,直接返回模型内容(可能包装 ErrorBoundary)
|
||||||
if (!showFlowSettings) {
|
if (!showFlowSettings) {
|
||||||
return modelContent;
|
return wrapWithErrorBoundary(modelContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 flowSettingsVariant 包装相应的设置组件
|
// 根据 flowSettingsVariant 包装相应的设置组件
|
||||||
@ -158,26 +179,26 @@ const FlowModelRendererCore: React.FC<{
|
|||||||
showBackground={_.isObject(showFlowSettings) ? showFlowSettings.showBackground : undefined}
|
showBackground={_.isObject(showFlowSettings) ? showFlowSettings.showBackground : undefined}
|
||||||
showBorder={_.isObject(showFlowSettings) ? showFlowSettings.showBorder : undefined}
|
showBorder={_.isObject(showFlowSettings) ? showFlowSettings.showBorder : undefined}
|
||||||
>
|
>
|
||||||
{modelContent}
|
{wrapWithErrorBoundary(modelContent)}
|
||||||
</FlowsFloatContextMenu>
|
</FlowsFloatContextMenu>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'contextMenu':
|
case 'contextMenu':
|
||||||
return (
|
return (
|
||||||
<FlowsContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>
|
<FlowsContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>
|
||||||
{modelContent}
|
{wrapWithErrorBoundary(modelContent)}
|
||||||
</FlowsContextMenu>
|
</FlowsContextMenu>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'modal':
|
case 'modal':
|
||||||
// TODO: 实现 modal 模式的流程设置
|
// TODO: 实现 modal 模式的流程设置
|
||||||
console.warn('FlowModelRenderer: modal variant is not implemented yet');
|
console.warn('FlowModelRenderer: modal variant is not implemented yet');
|
||||||
return modelContent;
|
return wrapWithErrorBoundary(modelContent);
|
||||||
|
|
||||||
case 'drawer':
|
case 'drawer':
|
||||||
// TODO: 实现 drawer 模式的流程设置
|
// TODO: 实现 drawer 模式的流程设置
|
||||||
console.warn('FlowModelRenderer: drawer variant is not implemented yet');
|
console.warn('FlowModelRenderer: drawer variant is not implemented yet');
|
||||||
return modelContent;
|
return wrapWithErrorBoundary(modelContent);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`FlowModelRenderer: Unknown flowSettingsVariant '${flowSettingsVariant}', falling back to dropdown`);
|
console.warn(`FlowModelRenderer: Unknown flowSettingsVariant '${flowSettingsVariant}', falling back to dropdown`);
|
||||||
@ -188,7 +209,7 @@ const FlowModelRendererCore: React.FC<{
|
|||||||
showBackground={_.isObject(showFlowSettings) ? showFlowSettings.showBackground : undefined}
|
showBackground={_.isObject(showFlowSettings) ? showFlowSettings.showBackground : undefined}
|
||||||
showBorder={_.isObject(showFlowSettings) ? showFlowSettings.showBorder : undefined}
|
showBorder={_.isObject(showFlowSettings) ? showFlowSettings.showBorder : undefined}
|
||||||
>
|
>
|
||||||
{modelContent}
|
{wrapWithErrorBoundary(modelContent)}
|
||||||
</FlowsFloatContextMenu>
|
</FlowsFloatContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -221,6 +242,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
|||||||
extraContext,
|
extraContext,
|
||||||
sharedContext,
|
sharedContext,
|
||||||
independentAutoFlowExecution = false,
|
independentAutoFlowExecution = false,
|
||||||
|
showErrorFallback = false,
|
||||||
}) => {
|
}) => {
|
||||||
if (!model || typeof model.render !== 'function') {
|
if (!model || typeof model.render !== 'function') {
|
||||||
// 可以选择渲染 null 或者一个错误/提示信息
|
// 可以选择渲染 null 或者一个错误/提示信息
|
||||||
@ -242,6 +264,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
|||||||
flowSettingsVariant={flowSettingsVariant}
|
flowSettingsVariant={flowSettingsVariant}
|
||||||
hideRemoveInSettings={hideRemoveInSettings}
|
hideRemoveInSettings={hideRemoveInSettings}
|
||||||
sharedContext={sharedContext}
|
sharedContext={sharedContext}
|
||||||
|
showErrorFallback={showErrorFallback}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
@ -256,6 +279,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
|||||||
extraContext={extraContext}
|
extraContext={extraContext}
|
||||||
sharedContext={sharedContext}
|
sharedContext={sharedContext}
|
||||||
independentAutoFlowExecution={independentAutoFlowExecution}
|
independentAutoFlowExecution={independentAutoFlowExecution}
|
||||||
|
showErrorFallback={showErrorFallback}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
@ -11,4 +11,5 @@ export * from './common/FlowSettingsButton';
|
|||||||
export * from './FlowModelRenderer';
|
export * from './FlowModelRenderer';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './subModel';
|
export * from './subModel';
|
||||||
|
export * from './FlowErrorFallback';
|
||||||
//
|
//
|
||||||
|
Loading…
x
Reference in New Issue
Block a user