fix: formDrawer theme context issue (#6403)

This commit is contained in:
Katherine 2025-03-10 20:37:34 +08:00 committed by GitHub
parent e5507d0758
commit 8741c26a86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 261 additions and 49 deletions

View File

@ -0,0 +1,214 @@
/**
* 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, { Fragment, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { createForm, IFormProps, Form, onFormSubmitSuccess } from '@formily/core';
import { toJS } from '@formily/reactive';
import { FormProvider, observer, ReactFC } from '@formily/react';
import { isNum, isStr, isBool, isFn, applyMiddleware, IMiddleware } from '@formily/shared';
import { Drawer, DrawerProps, ThemeConfig } from 'antd';
import { usePrefixCls, createPortalProvider, createPortalRoot, loading } from '../__builtins__';
import { GlobalThemeProvider } from '../../../global-theme';
type FormDrawerRenderer = React.ReactElement | ((form: Form) => React.ReactElement);
type DrawerTitle = string | number | React.ReactElement;
type EventType = React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement | HTMLButtonElement>;
const isDrawerTitle = (props: any): props is DrawerTitle => {
return isNum(props) || isStr(props) || isBool(props) || React.isValidElement(props);
};
const getDrawerProps = (props: any): IDrawerProps => {
if (isDrawerTitle(props)) {
return {
title: props,
};
} else {
return props;
}
};
export interface IFormDrawer {
forOpen(middleware: IMiddleware<IFormProps>): IFormDrawer;
open(props?: IFormProps): Promise<any>;
close(): void;
}
export interface IDrawerProps extends DrawerProps {
onClose?: (e: EventType) => void | boolean;
loadingText?: React.ReactNode;
}
export function FormDrawer(title: IDrawerProps, id: string, renderer: FormDrawerRenderer): IFormDrawer;
export function FormDrawer(title: IDrawerProps, id: FormDrawerRenderer): IFormDrawer;
export function FormDrawer(title: DrawerTitle, id: string, renderer: FormDrawerRenderer): IFormDrawer;
export function FormDrawer(
title: IDrawerProps,
id: string,
renderer: FormDrawerRenderer,
theme: ThemeConfig,
): IFormDrawer;
export function FormDrawer(title: DrawerTitle, id: FormDrawerRenderer): IFormDrawer;
export function FormDrawer(title: any, id: any, renderer?: any, theme?: any): IFormDrawer {
if (isFn(id) || React.isValidElement(id)) {
theme = renderer;
renderer = id;
id = 'form-drawer';
}
const env = {
host: document.createElement('div'),
openMiddlewares: [],
form: null,
promise: null,
};
const root = createPortalRoot(env.host, id);
const props = getDrawerProps(title);
const drawer = {
width: '40%',
...props,
onClose: (e: any) => {
if (props?.onClose?.(e) !== false) {
formDrawer.close();
}
},
afterVisibleChange: (visible: boolean) => {
props?.afterVisibleChange?.(visible);
if (visible) return;
root.unmount();
},
};
const DrawerContent = observer(() => {
return <Fragment>{isFn(renderer) ? renderer(env.form) : renderer}</Fragment>;
});
const renderDrawer = (visible = true) => {
return (
//@ts-ignore
<GlobalThemeProvider theme={theme}>
<Drawer {...drawer} visible={visible}>
<FormProvider form={env.form}>
<DrawerContent />
</FormProvider>
</Drawer>
</GlobalThemeProvider>
);
};
document.body.appendChild(env.host);
const formDrawer = {
forOpen: (middleware: IMiddleware<IFormProps>) => {
if (isFn(middleware)) {
env.openMiddlewares.push(middleware);
}
return formDrawer;
},
open: (props: IFormProps) => {
if (env.promise) return env.promise;
// eslint-disable-next-line no-async-promise-executor
env.promise = new Promise(async (resolve, reject) => {
try {
props = await loading(drawer.loadingText, () => applyMiddleware(props, env.openMiddlewares));
env.form =
env.form ||
createForm({
...props,
effects(form) {
onFormSubmitSuccess(() => {
resolve(toJS(form.values));
formDrawer.close();
});
props?.effects?.(form);
},
});
} catch (e) {
reject(e);
}
root.render(() => renderDrawer(false));
setTimeout(() => {
root.render(() => renderDrawer(true));
}, 16);
});
return env.promise;
},
close: () => {
if (!env.host) return;
root.render(() => renderDrawer(false));
},
};
return formDrawer;
}
const DrawerExtra: ReactFC = (props) => {
const ref = useRef<HTMLDivElement>();
const [extra, setExtra] = useState<HTMLDivElement>();
const extraRef = useRef<HTMLDivElement>();
const prefixCls = usePrefixCls('drawer');
useLayoutEffect(() => {
const content = ref.current?.closest(`.${prefixCls}-wrapper-body`)?.querySelector(`.${prefixCls}-header`);
if (content) {
if (!extraRef.current) {
extraRef.current = content.querySelector(`.${prefixCls}-extra`);
if (!extraRef.current) {
extraRef.current = document.createElement('div');
extraRef.current.classList.add(`${prefixCls}-extra`);
content.appendChild(extraRef.current);
}
}
setExtra(extraRef.current);
}
});
extraRef.current = extra;
return (
<div ref={ref} style={{ display: 'none' }}>
{extra && createPortal(props.children, extra)}
</div>
);
};
const DrawerFooter: ReactFC = (props) => {
const ref = useRef<HTMLDivElement>();
const [footer, setFooter] = useState<HTMLDivElement>();
const footerRef = useRef<HTMLDivElement>();
const prefixCls = usePrefixCls('drawer');
useLayoutEffect(() => {
const content = ref.current?.closest(`.${prefixCls}-wrapper-body`);
if (content) {
if (!footerRef.current) {
footerRef.current = content.querySelector(`.${prefixCls}-footer`);
if (!footerRef.current) {
footerRef.current = document.createElement('div');
footerRef.current.classList.add(`${prefixCls}-footer`);
content.appendChild(footerRef.current);
}
}
setFooter(footerRef.current);
}
});
footerRef.current = footer;
return (
<div ref={ref} style={{ display: 'none' }}>
{footer && createPortal(props.children, footer)}
</div>
);
};
FormDrawer.Extra = DrawerExtra;
FormDrawer.Footer = DrawerFooter;
FormDrawer.Portal = createPortalProvider('form-drawer');
export default FormDrawer;

View File

@ -65,5 +65,5 @@ export * from './tree-select';
export * from './unix-timestamp'; export * from './unix-timestamp';
export * from './upload'; export * from './upload';
export * from './variable'; export * from './variable';
export * from './form-drawer';
import './index.less'; import './index.less';

View File

@ -7,20 +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 { DeleteOutlined, DownOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { DeleteOutlined, DownOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { import { Checkbox, FormButtonGroup, FormItem, FormLayout, Input, Radio, Reset, Submit } from '@formily/antd-v5';
Checkbox,
FormButtonGroup,
FormDrawer,
FormItem,
FormLayout,
Input,
Radio,
Reset,
Submit,
} from '@formily/antd-v5';
import { registerValidateRules } from '@formily/core'; import { registerValidateRules } from '@formily/core';
import { createSchemaField, useField } from '@formily/react'; import { createSchemaField, useField } from '@formily/react';
import { SchemaComponent, SchemaComponentOptions, useAPIClient } from '@nocobase/client'; import { SchemaComponent, SchemaComponentOptions, useAPIClient, FormDrawer, useGlobalTheme } from '@nocobase/client';
import { Alert, App, Button, Card, Dropdown, Flex, Space, Table, Tag } from 'antd'; import { Alert, App, Button, Card, Dropdown, Flex, Space, Table, Tag } from 'antd';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { VAR_NAME_RE } from '../../re'; import { VAR_NAME_RE } from '../../re';
@ -122,6 +112,7 @@ export function EnvironmentVariables({ request, setSelectRowKeys }) {
const t = useT(); const t = useT();
const api = useAPIClient(); const api = useAPIClient();
const { data, loading, refresh } = request || {}; const { data, loading, refresh } = request || {};
const { theme } = useGlobalTheme();
const typEnum = { const typEnum = {
default: { default: {
@ -150,40 +141,45 @@ export function EnvironmentVariables({ request, setSelectRowKeys }) {
}; };
const handleEdit = async (initialValues) => { const handleEdit = async (initialValues) => {
const drawer = FormDrawer({ title: t('Edit') }, () => { const drawer = FormDrawer(
return ( { title: t('Edit') },
<FormLayout layout={'vertical'}> 'edit',
<SchemaComponentOptions scope={{ createOnly: false, t }}> () => {
<SchemaField schema={schema} /> return (
</SchemaComponentOptions> <FormLayout layout={'vertical'}>
<FormDrawer.Footer> <SchemaComponentOptions scope={{ createOnly: false, t }}>
<FormButtonGroup align="right"> <SchemaField schema={schema} />
<Reset </SchemaComponentOptions>
onClick={() => { <FormDrawer.Footer>
drawer.close(); <FormButtonGroup align="right">
}} <Reset
> onClick={() => {
{t('Cancel')} drawer.close();
</Reset> }}
<Submit >
onSubmit={async (data) => { {t('Cancel')}
await api.request({ </Reset>
url: `environmentVariables:update?filterByTk=${initialValues.name}`, <Submit
method: 'post', onSubmit={async (data) => {
data: { await api.request({
...data, url: `environmentVariables:update?filterByTk=${initialValues.name}`,
}, method: 'post',
}); data: {
request.refresh(); ...data,
}} },
> });
{t('Submit')} request.refresh();
</Submit> }}
</FormButtonGroup> >
</FormDrawer.Footer> {t('Submit')}
</FormLayout> </Submit>
); </FormButtonGroup>
}); </FormDrawer.Footer>
</FormLayout>
);
},
theme,
);
drawer.open({ drawer.open({
initialValues: { ...initialValues }, initialValues: { ...initialValues },
}); });
@ -261,7 +257,7 @@ export function EnvironmentTabs() {
const { variablesRequest } = useContext(EnvAndSecretsContext); const { variablesRequest } = useContext(EnvAndSecretsContext);
const [selectRowKeys, setSelectRowKeys] = useState([]); const [selectRowKeys, setSelectRowKeys] = useState([]);
const resource = api.resource('environmentVariables'); const resource = api.resource('environmentVariables');
const { theme } = useGlobalTheme();
const handleBulkImport = async (importData) => { const handleBulkImport = async (importData) => {
const arr = Object.entries(importData).map(([type, dataString]) => { const arr = Object.entries(importData).map(([type, dataString]) => {
return parseKeyValuePairs(dataString, type).filter(Boolean); return parseKeyValuePairs(dataString, type).filter(Boolean);
@ -434,7 +430,8 @@ export function EnvironmentTabs() {
{ {
variable: t('Add variable'), variable: t('Add variable'),
bulk: t('Bulk import'), bulk: t('Bulk import'),
}[info.key], }[info.key] as any,
'add-new',
() => { () => {
return ( return (
<FormLayout layout={'vertical'}> <FormLayout layout={'vertical'}>
@ -468,6 +465,7 @@ export function EnvironmentTabs() {
</FormLayout> </FormLayout>
); );
}, },
theme,
) )
.open({ .open({
initialValues: {}, initialValues: {},