mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08: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 { 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})` },
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
|
@ -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 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');
|
@ -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.
|
* 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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';
|
||||||
|
@ -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.
|
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>
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user