mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 07:57:20 +00:00
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:
parent
9528da51be
commit
98a8e687b1
@ -14,10 +14,11 @@ import React, { ComponentType, useCallback, useMemo, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useNiceDropdownMaxHeight } from '../../common/useNiceDropdownHeight';
|
||||
import { useFlag } from '../../flag-provider';
|
||||
import { useDesignable } from '../../schema-component';
|
||||
import { ErrorFallback, useDesignable } from '../../schema-component';
|
||||
import { useSchemaInitializerStyles } from './components/style';
|
||||
import { SchemaInitializerContext } from './context';
|
||||
import { SchemaInitializerOptions } from './types';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
const defaultWrap = (s: ISchema) => s;
|
||||
|
||||
@ -87,53 +88,55 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaInitializerContext.Provider
|
||||
value={{
|
||||
visible,
|
||||
setVisible,
|
||||
insert: insertSchema,
|
||||
options: props,
|
||||
}}
|
||||
>
|
||||
{popover === false ? (
|
||||
React.createElement(C, cProps)
|
||||
) : (
|
||||
<Popover
|
||||
placement={'bottomLeft'}
|
||||
{...popoverProps}
|
||||
arrow={false}
|
||||
overlayClassName={overlayClassName}
|
||||
open={visible}
|
||||
onOpenChange={setVisible}
|
||||
content={wrapSSR(
|
||||
<div
|
||||
className={`${componentCls} ${hashId}`}
|
||||
style={{
|
||||
maxHeight: dropdownMaxHeight,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Menu: {
|
||||
itemHeight: token.marginXL,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
itemBorderRadius: token.borderRadiusSM,
|
||||
subMenuItemBorderRadius: token.borderRadiusSM,
|
||||
},
|
||||
},
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.error(err)}>
|
||||
<SchemaInitializerContext.Provider
|
||||
value={{
|
||||
visible,
|
||||
setVisible,
|
||||
insert: insertSchema,
|
||||
options: props,
|
||||
}}
|
||||
>
|
||||
{popover === false ? (
|
||||
React.createElement(C, cProps)
|
||||
) : (
|
||||
<Popover
|
||||
placement={'bottomLeft'}
|
||||
{...popoverProps}
|
||||
arrow={false}
|
||||
overlayClassName={overlayClassName}
|
||||
open={visible}
|
||||
onOpenChange={setVisible}
|
||||
content={wrapSSR(
|
||||
<div
|
||||
className={`${componentCls} ${hashId}`}
|
||||
style={{
|
||||
maxHeight: dropdownMaxHeight,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</div>,
|
||||
)}
|
||||
>
|
||||
{React.createElement(C, cProps)}
|
||||
</Popover>
|
||||
)}
|
||||
</SchemaInitializerContext.Provider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Menu: {
|
||||
itemHeight: token.marginXL,
|
||||
borderRadius: token.borderRadiusSM,
|
||||
itemBorderRadius: token.borderRadiusSM,
|
||||
subMenuItemBorderRadius: token.borderRadiusSM,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</div>,
|
||||
)}
|
||||
>
|
||||
{React.createElement(C, cProps)}
|
||||
</Popover>
|
||||
)}
|
||||
</SchemaInitializerContext.Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
},
|
||||
{ displayName: `WithInitializer(${C.displayName || C.name})` },
|
||||
|
@ -10,7 +10,7 @@
|
||||
import React, { FC, memo, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { useFieldComponentName } from '../../../common/useFieldComponentName';
|
||||
import { useFindComponent } from '../../../schema-component';
|
||||
import { ErrorFallback, useFindComponent } from '../../../schema-component';
|
||||
import {
|
||||
SchemaSettingsActionModalItem,
|
||||
SchemaSettingsCascaderItem,
|
||||
@ -27,6 +27,7 @@ import {
|
||||
} from '../../../schema-settings/SchemaSettings';
|
||||
import { SchemaSettingItemContext } from '../context/SchemaSettingItemContext';
|
||||
import { SchemaSettingsItemType } from '../types';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
|
||||
export interface SchemaSettingsChildrenProps {
|
||||
children: SchemaSettingsItemType[];
|
||||
@ -46,6 +47,19 @@ const typeComponentMap = {
|
||||
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) => {
|
||||
const { children } = props;
|
||||
const { visible } = useSchemaSettings();
|
||||
@ -70,7 +84,15 @@ export const SchemaSettingsChildren: FC<SchemaSettingsChildrenProps> = (props) =
|
||||
// 两次渲染之间 props 可能发生变化,就可能报 hooks 调用顺序的错误。所以这里使用 fieldComponentName 和 item.name 拼成
|
||||
// 一个不会重复的 key,保证每次渲染都是新的组件。
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
@ -9,14 +9,29 @@
|
||||
|
||||
import { ISchema } from '@formily/json-schema';
|
||||
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 { 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) => {
|
||||
const { designable } = useDesignable();
|
||||
const toolbar = useMemo(() => {
|
||||
if (fieldSchema['x-toolbar'] || fieldSchema['x-designer']) {
|
||||
return fieldSchema['x-toolbar'] || fieldSchema['x-designer'];
|
||||
if (fieldSchema['x-designer'] || fieldSchema['x-toolbar']) {
|
||||
return fieldSchema['x-designer'] || fieldSchema['x-toolbar'];
|
||||
}
|
||||
|
||||
if (fieldSchema['x-settings']) {
|
||||
@ -30,7 +45,11 @@ export const useSchemaToolbarRender = (fieldSchema: ISchema) => {
|
||||
if (!designable || !C) {
|
||||
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,
|
||||
};
|
||||
|
@ -14,9 +14,10 @@ import { concat } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
|
||||
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 { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
type Props = {
|
||||
component: any;
|
||||
@ -96,9 +97,11 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
|
||||
export const CollectionField = connect((props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return (
|
||||
<CollectionFieldProvider name={fieldSchema.name}>
|
||||
<CollectionFieldInternalField {...props} />
|
||||
</CollectionFieldProvider>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}>
|
||||
<CollectionFieldProvider name={fieldSchema.name}>
|
||||
<CollectionFieldInternalField {...props} />
|
||||
</CollectionFieldProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -11,18 +11,29 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
||||
import { Drawer } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { OpenSize } from './types';
|
||||
import { ActionDrawerProps, OpenSize } from './types';
|
||||
import { useStyles } from './Action.Drawer.style';
|
||||
import { useActionContext } from './hooks';
|
||||
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
||||
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>([
|
||||
['small', '30%'],
|
||||
['middle', '50%'],
|
||||
['large', '70%'],
|
||||
]);
|
||||
export const ActionDrawer: ComposedActionDrawer = observer(
|
||||
export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
||||
(props) => {
|
||||
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
|
||||
const { visible, setVisible, openSize = 'middle', drawerProps, modalProps } = useActionContext();
|
||||
@ -83,6 +94,12 @@ export const ActionDrawer: ComposedActionDrawer = observer(
|
||||
{ displayName: 'ActionDrawer' },
|
||||
);
|
||||
|
||||
export const ActionDrawer: ComposedActionDrawer = (props) => (
|
||||
<ErrorBoundary FallbackComponent={DrawerErrorFallback} onError={(err) => console.log(err)}>
|
||||
<InternalActionDrawer {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
ActionDrawer.Footer = observer(
|
||||
() => {
|
||||
const field = useField();
|
||||
|
@ -14,15 +14,26 @@ import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { useToken } from '../../../style';
|
||||
import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal';
|
||||
import { ComposedActionDrawer, OpenSize } from './types';
|
||||
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||
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>([
|
||||
['small', '40%'],
|
||||
['middle', '60%'],
|
||||
['large', '80%'],
|
||||
]);
|
||||
export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
|
||||
export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer(
|
||||
(props) => {
|
||||
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
|
||||
const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext();
|
||||
@ -116,6 +127,12 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
|
||||
{ displayName: 'ActionModal' },
|
||||
);
|
||||
|
||||
export const ActionModal: ComposedActionDrawer<ModalProps> = (props) => (
|
||||
<ErrorBoundary FallbackComponent={ModalErrorFallback} onError={(err) => console.log(err)}>
|
||||
<InternalActionModal {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
ActionModal.Footer = observer(
|
||||
() => {
|
||||
const field = useField();
|
||||
|
@ -14,7 +14,7 @@ import classnames from 'classnames';
|
||||
import { default as lodash } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StablePopover, useActionContext } from '../..';
|
||||
import { ErrorFallback, StablePopover, useActionContext } from '../..';
|
||||
import { useDesignable } from '../../';
|
||||
import { useACLActionParamsContext } from '../../../acl';
|
||||
import {
|
||||
@ -44,6 +44,9 @@ import { useA } from './hooks';
|
||||
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
||||
import { ActionProps, ComposedAction } from './types';
|
||||
import { linkageAction, setInitialActionState } from './utils';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
const handleError = (err) => console.log(err);
|
||||
|
||||
export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
observer((props: ActionProps) => {
|
||||
@ -246,6 +249,11 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
Action.Popover = observer(
|
||||
(props) => {
|
||||
const { button, visible, setVisible } = useActionContext();
|
||||
const content = (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={handleError}>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
return (
|
||||
<StablePopover
|
||||
{...props}
|
||||
@ -254,7 +262,7 @@ Action.Popover = observer(
|
||||
onOpenChange={(visible) => {
|
||||
setVisible(visible);
|
||||
}}
|
||||
content={props.children}
|
||||
content={content}
|
||||
>
|
||||
{button}
|
||||
</StablePopover>
|
||||
|
@ -86,6 +86,8 @@ export type ComposedAction = React.FC<ActionProps> & {
|
||||
[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;
|
||||
};
|
||||
|
@ -13,8 +13,11 @@ import React, { useMemo } from 'react';
|
||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||
import { CustomCreateStylesUtils, createStyles } from '../../../style';
|
||||
import { SortableItem } from '../../common';
|
||||
import { useDesigner, useProps } from '../../hooks';
|
||||
import { useProps } from '../../hooks';
|
||||
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) => {
|
||||
return css`
|
||||
@ -79,16 +82,18 @@ export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
|
||||
const { className, children } = useProps(props);
|
||||
const { styles: blockItemCss } = useStyles();
|
||||
|
||||
const Designer = useDesigner();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaToolbarRender(fieldSchema);
|
||||
const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name);
|
||||
|
||||
const label = useMemo(() => getAriaLabel(), [getAriaLabel]);
|
||||
|
||||
return (
|
||||
<SortableItem role="button" aria-label={label} className={cls('nb-block-item', className, blockItemCss)}>
|
||||
<Designer {...fieldSchema['x-toolbar-props']} />
|
||||
{children}
|
||||
{render()}
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={(err) => console.log(err)}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</SortableItem>
|
||||
);
|
||||
},
|
||||
|
@ -11,10 +11,13 @@ import { Button, Result, Typography } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import { FallbackProps, useErrorBoundary } from 'react-error-boundary';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { ErrorFallbackModal } from './ErrorFallbackModal';
|
||||
|
||||
const { Paragraph, Text, Link } = Typography;
|
||||
|
||||
export const ErrorFallback: FC<FallbackProps> = ({ error }) => {
|
||||
export const ErrorFallback: FC<FallbackProps> & {
|
||||
Modal: FC<FallbackProps>;
|
||||
} = ({ error }) => {
|
||||
const { resetBoundary } = useErrorBoundary();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -52,3 +55,5 @@ export const ErrorFallback: FC<FallbackProps> = ({ error }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorFallback.Modal = ErrorFallbackModal;
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,8 +1,6 @@
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorFallback } from '../ErrorFallback';
|
||||
import { ErrorFallback } from '../../ErrorFallback';
|
||||
|
||||
const App = () => {
|
||||
throw new Error('error message');
|
@ -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>
|
||||
);
|
||||
};
|
@ -7,9 +7,10 @@
|
||||
* 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 App1 from '../demos/demo1';
|
||||
import App1 from './components/basic';
|
||||
import InlineApp from './components/modal';
|
||||
|
||||
describe('ErrorFallback', () => {
|
||||
it('should render correctly', () => {
|
||||
@ -24,4 +25,19 @@ describe('ErrorFallback', () => {
|
||||
// 底部复制按钮
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorFallback } from '@nocobase/client';
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -2,6 +2,12 @@
|
||||
|
||||
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>
|
||||
|
||||
## Modal
|
||||
|
||||
<code src="./demos/new-demos/modal.tsx"></code>
|
||||
|
@ -6,4 +6,10 @@
|
||||
|
||||
其基于 [react-error-boundary](https://github.com/bvaughn/react-error-boundary) 库。
|
||||
|
||||
## Basic
|
||||
|
||||
<code src="./demos/new-demos/basic.tsx"></code>
|
||||
|
||||
## Modal
|
||||
|
||||
<code src="./demos/new-demos/modal.tsx"></code>
|
||||
|
@ -14,6 +14,10 @@ import { SchemaToolbar } from '../../schema-settings';
|
||||
|
||||
const DefaultSchemaToolbar = () => null;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use `useSchemaToolbarRender` instead
|
||||
*/
|
||||
export const useDesigner = () => {
|
||||
const { designable } = useDesignable();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -200,7 +200,10 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
showBackground,
|
||||
showBorder = true,
|
||||
draggable = true,
|
||||
} = { ...props, ...(fieldSchema['x-toolbar-props'] || {}) } as SchemaToolbarProps;
|
||||
} = {
|
||||
...props,
|
||||
...(fieldSchema['x-toolbar-props'] || {}),
|
||||
} as SchemaToolbarProps;
|
||||
const { designable } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const { styles } = useStyles();
|
||||
@ -271,7 +274,14 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
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() {
|
||||
if (toolbarElement) {
|
||||
toolbarElement.style.display = 'block';
|
||||
@ -284,21 +294,17 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (parentElement) {
|
||||
const style = window.getComputedStyle(parentElement);
|
||||
if (style.position === 'static') {
|
||||
parentElement.style.position = 'relative';
|
||||
}
|
||||
|
||||
parentElement.addEventListener('mouseenter', show);
|
||||
parentElement.addEventListener('mouseleave', hide);
|
||||
const style = window.getComputedStyle(parentElement);
|
||||
if (style.position === 'static') {
|
||||
parentElement.style.position = 'relative';
|
||||
}
|
||||
|
||||
parentElement.addEventListener('mouseenter', show);
|
||||
parentElement.addEventListener('mouseleave', hide);
|
||||
|
||||
return () => {
|
||||
if (parentElement) {
|
||||
parentElement.removeEventListener('mouseenter', show);
|
||||
parentElement.removeEventListener('mouseleave', hide);
|
||||
}
|
||||
parentElement.removeEventListener('mouseenter', show);
|
||||
parentElement.removeEventListener('mouseleave', hide);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user