feat(client): refining error fallback for different components when catching errors (#4459)

* feat(client): refining error fallback for different components when catching errors

* fix: build

* refactor: `ErrorFallback.Inline` to `ErrorFallback.Modal`

* feat: toolbar error fallback

* chore: add deprecated comment

* fix: useSchemaToolbarRender
This commit is contained in:
YANG QIA 2024-05-28 13:26:38 +08:00 committed by GitHub
parent 9528da51be
commit 98a8e687b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 325 additions and 90 deletions

View File

@ -14,10 +14,11 @@ import React, { ComponentType, useCallback, useMemo, useState } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useNiceDropdownMaxHeight } from '../../common/useNiceDropdownHeight'; import { useNiceDropdownMaxHeight } from '../../common/useNiceDropdownHeight';
import { useFlag } from '../../flag-provider'; import { useFlag } from '../../flag-provider';
import { useDesignable } from '../../schema-component'; import { ErrorFallback, useDesignable } from '../../schema-component';
import { useSchemaInitializerStyles } from './components/style'; import { useSchemaInitializerStyles } from './components/style';
import { SchemaInitializerContext } from './context'; import { SchemaInitializerContext } from './context';
import { SchemaInitializerOptions } from './types'; import { SchemaInitializerOptions } from './types';
import { ErrorBoundary } from 'react-error-boundary';
const defaultWrap = (s: ISchema) => s; const defaultWrap = (s: ISchema) => s;
@ -87,53 +88,55 @@ export function withInitializer<T>(C: ComponentType<T>) {
} }
return ( return (
<SchemaInitializerContext.Provider <ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.error(err)}>
value={{ <SchemaInitializerContext.Provider
visible, value={{
setVisible, visible,
insert: insertSchema, setVisible,
options: props, insert: insertSchema,
}} options: props,
> }}
{popover === false ? ( >
React.createElement(C, cProps) {popover === false ? (
) : ( React.createElement(C, cProps)
<Popover ) : (
placement={'bottomLeft'} <Popover
{...popoverProps} placement={'bottomLeft'}
arrow={false} {...popoverProps}
overlayClassName={overlayClassName} arrow={false}
open={visible} overlayClassName={overlayClassName}
onOpenChange={setVisible} open={visible}
content={wrapSSR( onOpenChange={setVisible}
<div content={wrapSSR(
className={`${componentCls} ${hashId}`} <div
style={{ className={`${componentCls} ${hashId}`}
maxHeight: dropdownMaxHeight, style={{
overflowY: 'auto', maxHeight: dropdownMaxHeight,
}} overflowY: 'auto',
>
<ConfigProvider
theme={{
components: {
Menu: {
itemHeight: token.marginXL,
borderRadius: token.borderRadiusSM,
itemBorderRadius: token.borderRadiusSM,
subMenuItemBorderRadius: token.borderRadiusSM,
},
},
}} }}
> >
{children} <ConfigProvider
</ConfigProvider> theme={{
</div>, components: {
)} Menu: {
> itemHeight: token.marginXL,
{React.createElement(C, cProps)} borderRadius: token.borderRadiusSM,
</Popover> itemBorderRadius: token.borderRadiusSM,
)} subMenuItemBorderRadius: token.borderRadiusSM,
</SchemaInitializerContext.Provider> },
},
}}
>
{children}
</ConfigProvider>
</div>,
)}
>
{React.createElement(C, cProps)}
</Popover>
)}
</SchemaInitializerContext.Provider>
</ErrorBoundary>
); );
}, },
{ displayName: `WithInitializer(${C.displayName || C.name})` }, { displayName: `WithInitializer(${C.displayName || C.name})` },

View File

@ -10,7 +10,7 @@
import React, { FC, memo, useEffect, useMemo, useRef } from 'react'; import React, { FC, memo, useEffect, useMemo, useRef } from 'react';
import { useFieldComponentName } from '../../../common/useFieldComponentName'; import { useFieldComponentName } from '../../../common/useFieldComponentName';
import { useFindComponent } from '../../../schema-component'; import { ErrorFallback, useFindComponent } from '../../../schema-component';
import { import {
SchemaSettingsActionModalItem, SchemaSettingsActionModalItem,
SchemaSettingsCascaderItem, SchemaSettingsCascaderItem,
@ -27,6 +27,7 @@ import {
} from '../../../schema-settings/SchemaSettings'; } from '../../../schema-settings/SchemaSettings';
import { SchemaSettingItemContext } from '../context/SchemaSettingItemContext'; import { SchemaSettingItemContext } from '../context/SchemaSettingItemContext';
import { SchemaSettingsItemType } from '../types'; import { SchemaSettingsItemType } from '../types';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
export interface SchemaSettingsChildrenProps { export interface SchemaSettingsChildrenProps {
children: SchemaSettingsItemType[]; children: SchemaSettingsItemType[];
@ -46,6 +47,19 @@ const typeComponentMap = {
modal: SchemaSettingsModalItem, modal: SchemaSettingsModalItem,
}; };
const SchemaSettingsChildErrorFallback: FC<
FallbackProps & {
title: string;
}
> = (props) => {
const { title, ...fallbackProps } = props;
return (
<SchemaSettingsItem title={title}>
<ErrorFallback.Modal {...fallbackProps} />
</SchemaSettingsItem>
);
};
export const SchemaSettingsChildren: FC<SchemaSettingsChildrenProps> = (props) => { export const SchemaSettingsChildren: FC<SchemaSettingsChildrenProps> = (props) => {
const { children } = props; const { children } = props;
const { visible } = useSchemaSettings(); const { visible } = useSchemaSettings();
@ -70,7 +84,15 @@ export const SchemaSettingsChildren: FC<SchemaSettingsChildrenProps> = (props) =
// 两次渲染之间 props 可能发生变化,就可能报 hooks 调用顺序的错误。所以这里使用 fieldComponentName 和 item.name 拼成 // 两次渲染之间 props 可能发生变化,就可能报 hooks 调用顺序的错误。所以这里使用 fieldComponentName 和 item.name 拼成
// 一个不会重复的 key保证每次渲染都是新的组件。 // 一个不会重复的 key保证每次渲染都是新的组件。
const key = `${fieldComponentName ? fieldComponentName + '-' : ''}${item.name}`; const key = `${fieldComponentName ? fieldComponentName + '-' : ''}${item.name}`;
return <SchemaSettingsChild key={key} {...item} />; return (
<ErrorBoundary
key={key}
FallbackComponent={(props) => <SchemaSettingsChildErrorFallback {...props} title={key} />}
onError={(err) => console.log(err)}
>
<SchemaSettingsChild {...item} />
</ErrorBoundary>
);
})} })}
</> </>
); );

View File

@ -9,14 +9,29 @@
import { ISchema } from '@formily/json-schema'; import { ISchema } from '@formily/json-schema';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useComponent, useDesignable } from '../../../schema-component'; import { ErrorFallback, useComponent, useDesignable } from '../../../schema-component';
import { SchemaToolbar, SchemaToolbarProps } from '../../../schema-settings/GeneralSchemaDesigner'; import { SchemaToolbar, SchemaToolbarProps } from '../../../schema-settings/GeneralSchemaDesigner';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
const SchemaToolbarErrorFallback: React.FC<FallbackProps> = (props) => {
const { designable } = useDesignable();
if (!designable) {
return null;
}
return (
<ErrorFallback.Modal {...props}>
<SchemaToolbar title={`render toolbar error: ${props.error.message}`} />
</ErrorFallback.Modal>
);
};
export const useSchemaToolbarRender = (fieldSchema: ISchema) => { export const useSchemaToolbarRender = (fieldSchema: ISchema) => {
const { designable } = useDesignable(); const { designable } = useDesignable();
const toolbar = useMemo(() => { const toolbar = useMemo(() => {
if (fieldSchema['x-toolbar'] || fieldSchema['x-designer']) { if (fieldSchema['x-designer'] || fieldSchema['x-toolbar']) {
return fieldSchema['x-toolbar'] || fieldSchema['x-designer']; return fieldSchema['x-designer'] || fieldSchema['x-toolbar'];
} }
if (fieldSchema['x-settings']) { if (fieldSchema['x-settings']) {
@ -30,7 +45,11 @@ export const useSchemaToolbarRender = (fieldSchema: ISchema) => {
if (!designable || !C) { if (!designable || !C) {
return null; return null;
} }
return <C {...fieldSchema['x-toolbar-props']} {...props} />; return (
<ErrorBoundary FallbackComponent={SchemaToolbarErrorFallback} onError={(err) => console.error(err)}>
<C {...fieldSchema['x-toolbar-props']} {...props} />
</ErrorBoundary>
);
}, },
exists: !!C, exists: !!C,
}; };

View File

@ -14,9 +14,10 @@ import { concat } from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps'; import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
import { useCompile, useComponent } from '../../schema-component'; import { ErrorFallback, useCompile, useComponent } from '../../schema-component';
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider'; import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
import { ErrorBoundary } from 'react-error-boundary';
type Props = { type Props = {
component: any; component: any;
@ -96,9 +97,11 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
export const CollectionField = connect((props) => { export const CollectionField = connect((props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
return ( return (
<CollectionFieldProvider name={fieldSchema.name}> <ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}>
<CollectionFieldInternalField {...props} /> <CollectionFieldProvider name={fieldSchema.name}>
</CollectionFieldProvider> <CollectionFieldInternalField {...props} />
</CollectionFieldProvider>
</ErrorBoundary>
); );
}); });

View File

@ -11,18 +11,29 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
import { Drawer } from 'antd'; import { Drawer } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import { OpenSize } from './types'; import { ActionDrawerProps, OpenSize } from './types';
import { useStyles } from './Action.Drawer.style'; import { useStyles } from './Action.Drawer.style';
import { useActionContext } from './hooks'; import { useActionContext } from './hooks';
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer'; import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
import { ComposedActionDrawer } from './types'; import { ComposedActionDrawer } from './types';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { ErrorFallback } from '../error-fallback';
const DrawerErrorFallback: React.FC<FallbackProps> = (props) => {
const { visible, setVisible } = useActionContext();
return (
<Drawer open={visible} onClose={() => setVisible(false, true)} width="50%">
<ErrorFallback {...props} />
</Drawer>
);
};
const openSizeWidthMap = new Map<OpenSize, string>([ const openSizeWidthMap = new Map<OpenSize, string>([
['small', '30%'], ['small', '30%'],
['middle', '50%'], ['middle', '50%'],
['large', '70%'], ['large', '70%'],
]); ]);
export const ActionDrawer: ComposedActionDrawer = observer( export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
(props) => { (props) => {
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props; const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
const { visible, setVisible, openSize = 'middle', drawerProps, modalProps } = useActionContext(); const { visible, setVisible, openSize = 'middle', drawerProps, modalProps } = useActionContext();
@ -83,6 +94,12 @@ export const ActionDrawer: ComposedActionDrawer = observer(
{ displayName: 'ActionDrawer' }, { displayName: 'ActionDrawer' },
); );
export const ActionDrawer: ComposedActionDrawer = (props) => (
<ErrorBoundary FallbackComponent={DrawerErrorFallback} onError={(err) => console.log(err)}>
<InternalActionDrawer {...props} />
</ErrorBoundary>
);
ActionDrawer.Footer = observer( ActionDrawer.Footer = observer(
() => { () => {
const field = useField(); const field = useField();

View File

@ -14,15 +14,26 @@ import classNames from 'classnames';
import React from 'react'; import React from 'react';
import { useToken } from '../../../style'; import { useToken } from '../../../style';
import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal'; import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal';
import { ComposedActionDrawer, OpenSize } from './types'; import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
import { useActionContext } from './hooks'; import { useActionContext } from './hooks';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { ErrorFallback } from '../error-fallback';
const ModalErrorFallback: React.FC<FallbackProps> = (props) => {
const { visible, setVisible } = useActionContext();
return (
<Modal open={visible} onCancel={() => setVisible(false, true)} width="60%">
<ErrorFallback {...props} />
</Modal>
);
};
const openSizeWidthMap = new Map<OpenSize, string>([ const openSizeWidthMap = new Map<OpenSize, string>([
['small', '40%'], ['small', '40%'],
['middle', '60%'], ['middle', '60%'],
['large', '80%'], ['large', '80%'],
]); ]);
export const ActionModal: ComposedActionDrawer<ModalProps> = observer( export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer(
(props) => { (props) => {
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props; const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext(); const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext();
@ -116,6 +127,12 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
{ displayName: 'ActionModal' }, { displayName: 'ActionModal' },
); );
export const ActionModal: ComposedActionDrawer<ModalProps> = (props) => (
<ErrorBoundary FallbackComponent={ModalErrorFallback} onError={(err) => console.log(err)}>
<InternalActionModal {...props} />
</ErrorBoundary>
);
ActionModal.Footer = observer( ActionModal.Footer = observer(
() => { () => {
const field = useField(); const field = useField();

View File

@ -14,7 +14,7 @@ import classnames from 'classnames';
import { default as lodash } from 'lodash'; import { default as lodash } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { StablePopover, useActionContext } from '../..'; import { ErrorFallback, StablePopover, useActionContext } from '../..';
import { useDesignable } from '../../'; import { useDesignable } from '../../';
import { useACLActionParamsContext } from '../../../acl'; import { useACLActionParamsContext } from '../../../acl';
import { import {
@ -44,6 +44,9 @@ import { useA } from './hooks';
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction'; import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
import { ActionProps, ComposedAction } from './types'; import { ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils'; import { linkageAction, setInitialActionState } from './utils';
import { ErrorBoundary } from 'react-error-boundary';
const handleError = (err) => console.log(err);
export const Action: ComposedAction = withDynamicSchemaProps( export const Action: ComposedAction = withDynamicSchemaProps(
observer((props: ActionProps) => { observer((props: ActionProps) => {
@ -246,6 +249,11 @@ export const Action: ComposedAction = withDynamicSchemaProps(
Action.Popover = observer( Action.Popover = observer(
(props) => { (props) => {
const { button, visible, setVisible } = useActionContext(); const { button, visible, setVisible } = useActionContext();
const content = (
<ErrorBoundary FallbackComponent={ErrorFallback} onError={handleError}>
{props.children}
</ErrorBoundary>
);
return ( return (
<StablePopover <StablePopover
{...props} {...props}
@ -254,7 +262,7 @@ Action.Popover = observer(
onOpenChange={(visible) => { onOpenChange={(visible) => {
setVisible(visible); setVisible(visible);
}} }}
content={props.children} content={content}
> >
{button} {button}
</StablePopover> </StablePopover>

View File

@ -86,6 +86,8 @@ export type ComposedAction = React.FC<ActionProps> & {
[key: string]: any; [key: string]: any;
}; };
export type ComposedActionDrawer<T = DrawerProps> = React.FC<T & { footerNodeName?: string }> & { export type ActionDrawerProps<T = DrawerProps> = T & { footerNodeName?: string };
export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & {
Footer?: React.FC; Footer?: React.FC;
}; };

View File

@ -13,8 +13,11 @@ import React, { useMemo } from 'react';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { CustomCreateStylesUtils, createStyles } from '../../../style'; import { CustomCreateStylesUtils, createStyles } from '../../../style';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
import { useDesigner, useProps } from '../../hooks'; import { useProps } from '../../hooks';
import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem'; import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem';
import { ErrorBoundary } from 'react-error-boundary';
import { ErrorFallback } from '../error-fallback';
import { useSchemaToolbarRender } from '../../../application';
const useStyles = createStyles(({ css, token }: CustomCreateStylesUtils) => { const useStyles = createStyles(({ css, token }: CustomCreateStylesUtils) => {
return css` return css`
@ -79,16 +82,18 @@ export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
const { className, children } = useProps(props); const { className, children } = useProps(props);
const { styles: blockItemCss } = useStyles(); const { styles: blockItemCss } = useStyles();
const Designer = useDesigner();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaToolbarRender(fieldSchema);
const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name); const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name);
const label = useMemo(() => getAriaLabel(), [getAriaLabel]); const label = useMemo(() => getAriaLabel(), [getAriaLabel]);
return ( return (
<SortableItem role="button" aria-label={label} className={cls('nb-block-item', className, blockItemCss)}> <SortableItem role="button" aria-label={label} className={cls('nb-block-item', className, blockItemCss)}>
<Designer {...fieldSchema['x-toolbar-props']} /> {render()}
{children} <ErrorBoundary FallbackComponent={ErrorFallback} onError={(err) => console.log(err)}>
{children}
</ErrorBoundary>
</SortableItem> </SortableItem>
); );
}, },

View File

@ -11,10 +11,13 @@ import { Button, Result, Typography } from 'antd';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { FallbackProps, useErrorBoundary } from 'react-error-boundary'; import { FallbackProps, useErrorBoundary } from 'react-error-boundary';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { ErrorFallbackModal } from './ErrorFallbackModal';
const { Paragraph, Text, Link } = Typography; const { Paragraph, Text, Link } = Typography;
export const ErrorFallback: FC<FallbackProps> = ({ error }) => { export const ErrorFallback: FC<FallbackProps> & {
Modal: FC<FallbackProps>;
} = ({ error }) => {
const { resetBoundary } = useErrorBoundary(); const { resetBoundary } = useErrorBoundary();
const { t } = useTranslation(); const { t } = useTranslation();
@ -52,3 +55,5 @@ export const ErrorFallback: FC<FallbackProps> = ({ error }) => {
</div> </div>
); );
}; };
ErrorFallback.Modal = ErrorFallbackModal;

View File

@ -0,0 +1,50 @@
/**
* 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 { Modal } from 'antd';
import React, { FC } from 'react';
import { FallbackProps } from 'react-error-boundary';
import { ErrorFallback } from './ErrorFallback';
import { Typography } from 'antd';
const { Paragraph, Text } = Typography;
export const ErrorFallbackModal: FC<FallbackProps> = (props) => {
const [open, setOpen] = React.useState(false);
const defaultChildren = (
<Paragraph
style={{
display: 'flex',
marginBottom: 0,
}}
copyable={{ text: props.error.message }}
>
<Text
type="danger"
style={{
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
display: 'inline-block',
maxWidth: '200px',
}}
>
Error: {props.error.message}
</Text>
</Paragraph>
);
return (
<>
<div onMouseOver={() => setOpen(true)}>{props.children || defaultChildren}</div>
<Modal open={open} footer={null} onCancel={() => setOpen(false)} width={'60%'}>
<ErrorFallback {...props} />
</Modal>
</>
);
};

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { ErrorFallback } from '../ErrorFallback'; import { ErrorFallback } from '../../ErrorFallback';
const App = () => { const App = () => {
throw new Error('error message'); throw new Error('error message');

View File

@ -0,0 +1,24 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ErrorFallback } from '../../ErrorFallback';
const App = () => {
throw new Error('error message');
};
export default () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={console.error}>
<App />
</ErrorBoundary>
);
};

View File

@ -7,9 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { render, screen } from '@nocobase/test/client'; import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
import React from 'react'; import React from 'react';
import App1 from '../demos/demo1'; import App1 from './components/basic';
import InlineApp from './components/modal';
describe('ErrorFallback', () => { describe('ErrorFallback', () => {
it('should render correctly', () => { it('should render correctly', () => {
@ -24,4 +25,19 @@ describe('ErrorFallback', () => {
// 底部复制按钮 // 底部复制按钮
expect(document.querySelector('.ant-typography-copy')).toBeInTheDocument(); expect(document.querySelector('.ant-typography-copy')).toBeInTheDocument();
}); });
it('should render inline correctly', async () => {
render(<InlineApp />);
expect(screen.getByText(/Error: error message/i)).toBeInTheDocument();
expect(document.querySelector('.ant-typography-copy')).toBeInTheDocument();
await userEvent.hover(screen.getByText(/Error: error message/i));
await waitFor(() => {
expect(screen.getByText(/render failed/i)).toBeInTheDocument();
expect(
screen.getByText(/this is likely a nocobase internals bug\. please open an issue at/i),
).toBeInTheDocument();
});
});
}); });

View File

@ -1,5 +1,3 @@
import React from 'react'; import React from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { ErrorFallback } from '@nocobase/client'; import { ErrorFallback } from '@nocobase/client';

View File

@ -0,0 +1,26 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ErrorFallback } from '@nocobase/client';
import { Button } from 'antd';
const App = () => {
const [showError, setShowError] = React.useState(false);
if (showError) {
throw new Error('error message');
}
return (
<Button danger onClick={() => setShowError(true)}>
show error
</Button>
);
};
export default () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={console.error}>
<App />
</ErrorBoundary>
);
};

View File

@ -2,6 +2,12 @@
The component displayed when an error occurs during rendering. The component displayed when an error occurs during rendering.
其基于 [react-error-boundary](https://github.com/bvaughn/react-error-boundary) 库。 Based on the [react-error-boundary](https://github.com/bvaughn/react-error-boundary) library.
## Basic
<code src="./demos/new-demos/basic.tsx"></code> <code src="./demos/new-demos/basic.tsx"></code>
## Modal
<code src="./demos/new-demos/modal.tsx"></code>

View File

@ -6,4 +6,10 @@
其基于 [react-error-boundary](https://github.com/bvaughn/react-error-boundary) 库。 其基于 [react-error-boundary](https://github.com/bvaughn/react-error-boundary) 库。
## Basic
<code src="./demos/new-demos/basic.tsx"></code> <code src="./demos/new-demos/basic.tsx"></code>
## Modal
<code src="./demos/new-demos/modal.tsx"></code>

View File

@ -14,6 +14,10 @@ import { SchemaToolbar } from '../../schema-settings';
const DefaultSchemaToolbar = () => null; const DefaultSchemaToolbar = () => null;
/**
* @deprecated
* use `useSchemaToolbarRender` instead
*/
export const useDesigner = () => { export const useDesigner = () => {
const { designable } = useDesignable(); const { designable } = useDesignable();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();

View File

@ -200,7 +200,10 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
showBackground, showBackground,
showBorder = true, showBorder = true,
draggable = true, draggable = true,
} = { ...props, ...(fieldSchema['x-toolbar-props'] || {}) } as SchemaToolbarProps; } = {
...props,
...(fieldSchema['x-toolbar-props'] || {}),
} as SchemaToolbarProps;
const { designable } = useDesignable(); const { designable } = useDesignable();
const compile = useCompile(); const compile = useCompile();
const { styles } = useStyles(); const { styles } = useStyles();
@ -271,7 +274,14 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
useEffect(() => { useEffect(() => {
const toolbarElement = toolbarRef.current; const toolbarElement = toolbarRef.current;
const parentElement = toolbarElement?.parentElement; let parentElement = toolbarElement?.parentElement;
while (parentElement && window.getComputedStyle(parentElement).height === '0px') {
parentElement = parentElement.parentElement;
}
if (!parentElement) {
return;
}
function show() { function show() {
if (toolbarElement) { if (toolbarElement) {
toolbarElement.style.display = 'block'; toolbarElement.style.display = 'block';
@ -284,21 +294,17 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
} }
} }
if (parentElement) { const style = window.getComputedStyle(parentElement);
const style = window.getComputedStyle(parentElement); if (style.position === 'static') {
if (style.position === 'static') { parentElement.style.position = 'relative';
parentElement.style.position = 'relative';
}
parentElement.addEventListener('mouseenter', show);
parentElement.addEventListener('mouseleave', hide);
} }
parentElement.addEventListener('mouseenter', show);
parentElement.addEventListener('mouseleave', hide);
return () => { return () => {
if (parentElement) { parentElement.removeEventListener('mouseenter', show);
parentElement.removeEventListener('mouseenter', show); parentElement.removeEventListener('mouseleave', hide);
parentElement.removeEventListener('mouseleave', hide);
}
}; };
}, []); }, []);