From 8741c26a866556ac925808a57ea405c844170e01 Mon Sep 17 00:00:00 2001 From: Katherine Date: Mon, 10 Mar 2025 20:37:34 +0800 Subject: [PATCH] fix: formDrawer theme context issue (#6403) --- .../antd/form-drawer/index.tsx | 214 ++++++++++++++++++ .../client/src/schema-component/antd/index.ts | 2 +- .../src/client/components/EnvironmentTabs.tsx | 94 ++++---- 3 files changed, 261 insertions(+), 49 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/form-drawer/index.tsx diff --git a/packages/core/client/src/schema-component/antd/form-drawer/index.tsx b/packages/core/client/src/schema-component/antd/form-drawer/index.tsx new file mode 100644 index 0000000000..0cd7db21de --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-drawer/index.tsx @@ -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 | React.MouseEvent; + +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): IFormDrawer; + open(props?: IFormProps): Promise; + 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 {isFn(renderer) ? renderer(env.form) : renderer}; + }); + const renderDrawer = (visible = true) => { + return ( + //@ts-ignore + + + + + + + + ); + }; + + document.body.appendChild(env.host); + const formDrawer = { + forOpen: (middleware: IMiddleware) => { + 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(); + const [extra, setExtra] = useState(); + const extraRef = useRef(); + 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 ( +
+ {extra && createPortal(props.children, extra)} +
+ ); +}; + +const DrawerFooter: ReactFC = (props) => { + const ref = useRef(); + const [footer, setFooter] = useState(); + const footerRef = useRef(); + 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 ( +
+ {footer && createPortal(props.children, footer)} +
+ ); +}; + +FormDrawer.Extra = DrawerExtra; + +FormDrawer.Footer = DrawerFooter; + +FormDrawer.Portal = createPortalProvider('form-drawer'); + +export default FormDrawer; diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index 130f9e349b..b4c932adb5 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -65,5 +65,5 @@ export * from './tree-select'; export * from './unix-timestamp'; export * from './upload'; export * from './variable'; - +export * from './form-drawer'; import './index.less'; diff --git a/packages/plugins/@nocobase/plugin-environment-variables/src/client/components/EnvironmentTabs.tsx b/packages/plugins/@nocobase/plugin-environment-variables/src/client/components/EnvironmentTabs.tsx index 46bd38d8fd..e8a1b8eb55 100644 --- a/packages/plugins/@nocobase/plugin-environment-variables/src/client/components/EnvironmentTabs.tsx +++ b/packages/plugins/@nocobase/plugin-environment-variables/src/client/components/EnvironmentTabs.tsx @@ -7,20 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ import { DeleteOutlined, DownOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; -import { - Checkbox, - FormButtonGroup, - FormDrawer, - FormItem, - FormLayout, - Input, - Radio, - Reset, - Submit, -} from '@formily/antd-v5'; +import { Checkbox, FormButtonGroup, FormItem, FormLayout, Input, Radio, Reset, Submit } from '@formily/antd-v5'; import { registerValidateRules } from '@formily/core'; 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 React, { useContext, useState } from 'react'; import { VAR_NAME_RE } from '../../re'; @@ -122,6 +112,7 @@ export function EnvironmentVariables({ request, setSelectRowKeys }) { const t = useT(); const api = useAPIClient(); const { data, loading, refresh } = request || {}; + const { theme } = useGlobalTheme(); const typEnum = { default: { @@ -150,40 +141,45 @@ export function EnvironmentVariables({ request, setSelectRowKeys }) { }; const handleEdit = async (initialValues) => { - const drawer = FormDrawer({ title: t('Edit') }, () => { - return ( - - - - - - - { - drawer.close(); - }} - > - {t('Cancel')} - - { - await api.request({ - url: `environmentVariables:update?filterByTk=${initialValues.name}`, - method: 'post', - data: { - ...data, - }, - }); - request.refresh(); - }} - > - {t('Submit')} - - - - - ); - }); + const drawer = FormDrawer( + { title: t('Edit') }, + 'edit', + () => { + return ( + + + + + + + { + drawer.close(); + }} + > + {t('Cancel')} + + { + await api.request({ + url: `environmentVariables:update?filterByTk=${initialValues.name}`, + method: 'post', + data: { + ...data, + }, + }); + request.refresh(); + }} + > + {t('Submit')} + + + + + ); + }, + theme, + ); drawer.open({ initialValues: { ...initialValues }, }); @@ -261,7 +257,7 @@ export function EnvironmentTabs() { const { variablesRequest } = useContext(EnvAndSecretsContext); const [selectRowKeys, setSelectRowKeys] = useState([]); const resource = api.resource('environmentVariables'); - + const { theme } = useGlobalTheme(); const handleBulkImport = async (importData) => { const arr = Object.entries(importData).map(([type, dataString]) => { return parseKeyValuePairs(dataString, type).filter(Boolean); @@ -434,7 +430,8 @@ export function EnvironmentTabs() { { variable: t('Add variable'), bulk: t('Bulk import'), - }[info.key], + }[info.key] as any, + 'add-new', () => { return ( @@ -468,6 +465,7 @@ export function EnvironmentTabs() { ); }, + theme, ) .open({ initialValues: {},