refactor: remove page
This commit is contained in:
		
							parent
							
								
									d072b32b17
								
							
						
					
					
						commit
						a5145e64f7
					
				| @ -39,7 +39,6 @@ import { useOutboundActionProps } from './hooks/useOutboundActionProps'; | |||||||
| import { ExtendedAssociationField } from './schema-components/association-field/Editable'; | import { ExtendedAssociationField } from './schema-components/association-field/Editable'; | ||||||
| import { AssociatedField } from './components/AssociatedField'; | import { AssociatedField } from './components/AssociatedField'; | ||||||
| import { DatePicker } from './schema-components/date-picker'; | import { DatePicker } from './schema-components/date-picker'; | ||||||
| import { Page } from './schema-components/page'; |  | ||||||
| import { SettingBlockInitializer } from './schema-initializer/SettingBlockInitializer'; | import { SettingBlockInitializer } from './schema-initializer/SettingBlockInitializer'; | ||||||
| import { | import { | ||||||
|   EditFormulaTitleField, |   EditFormulaTitleField, | ||||||
| @ -158,7 +157,6 @@ export class PluginCoreClient extends Plugin { | |||||||
|       GroupBlockInitializer, |       GroupBlockInitializer, | ||||||
|       GroupBlockToolbar, |       GroupBlockToolbar, | ||||||
|       GroupBlockProvider, |       GroupBlockProvider, | ||||||
|       Page, |  | ||||||
|       DatePicker, |       DatePicker, | ||||||
|       RemoteSelect, |       RemoteSelect, | ||||||
|       SignatureInput, |       SignatureInput, | ||||||
|  | |||||||
| @ -1,145 +0,0 @@ | |||||||
| import { css } from '@emotion/css'; |  | ||||||
| import { useField, useFieldSchema } from '@formily/react'; |  | ||||||
| import React, { useContext, useEffect, useRef, useState } from 'react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { useRecord, useDesignable, SchemaSettingsSwitchItem } from '@nocobase/client'; |  | ||||||
| import { useIsBlockInPage } from './hooks/useIsBlockInPage'; |  | ||||||
| 
 |  | ||||||
| const FixedBlockContext = React.createContext<{ |  | ||||||
|   setFixedBlock: (value: string | false) => void; |  | ||||||
|   height: number | string; |  | ||||||
|   fixedBlockUID: boolean | string; |  | ||||||
|   fixedBlockUIDRef: React.MutableRefObject<boolean | string>; |  | ||||||
|   inFixedBlock: boolean; |  | ||||||
| }>({ |  | ||||||
|   setFixedBlock: () => {}, |  | ||||||
|   height: 0, |  | ||||||
|   fixedBlockUID: false, |  | ||||||
|   fixedBlockUIDRef: { current: false }, |  | ||||||
|   inFixedBlock: false, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const useFixedSchema = () => { |  | ||||||
|   const field = useField(); |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const { setFixedBlock, fixedBlockUID, fixedBlockUIDRef } = useFixedBlock(); |  | ||||||
|   const hasSet = useRef(false); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (!fixedBlockUIDRef.current || hasSet.current) { |  | ||||||
|       setFixedBlock(field?.decoratorProps?.fixedBlock ? fieldSchema['x-uid'] : false); |  | ||||||
|       hasSet.current = true; |  | ||||||
|     } |  | ||||||
|   }, [field?.decoratorProps?.fixedBlock]); |  | ||||||
| 
 |  | ||||||
|   return fieldSchema['x-uid'] === fixedBlockUID; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const useFixedBlock = () => { |  | ||||||
|   return useContext(FixedBlockContext); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const FixedBlockWrapper: React.FC = (props) => { |  | ||||||
|   const fixedBlock = useFixedSchema(); |  | ||||||
|   const { height, fixedBlockUID } = useFixedBlock(); |  | ||||||
|   const record = useRecord(); |  | ||||||
|   const isPopup = Object.keys(record).length; |  | ||||||
|   if (isPopup) { |  | ||||||
|     return <>{props.children}</>; |  | ||||||
|   } |  | ||||||
|   /** |  | ||||||
|    * The fixedBlockUID of false means that the page has no fixed blocks |  | ||||||
|    * isPopup means that the FixedBlock is in the popup mode |  | ||||||
|    */ |  | ||||||
|   if (!fixedBlock && fixedBlockUID) return null; |  | ||||||
|   return ( |  | ||||||
|     <div |  | ||||||
|       className="nb-fixed-block" |  | ||||||
|       style={{ |  | ||||||
|         height: fixedBlockUID ? `calc(100vh - ${height})` : undefined, |  | ||||||
|       }} |  | ||||||
|     > |  | ||||||
|       {props.children} |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const FixedBlockDesignerItem = () => { |  | ||||||
|   const field = useField(); |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const { dn } = useDesignable(); |  | ||||||
|   const { inFixedBlock } = useFixedBlock(); |  | ||||||
|   const { isBlockInPage } = useIsBlockInPage(); |  | ||||||
| 
 |  | ||||||
|   if (!isBlockInPage() || !inFixedBlock) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|   return ( |  | ||||||
|     <SchemaSettingsSwitchItem |  | ||||||
|       title={t('Fix block')} |  | ||||||
|       checked={fieldSchema['x-decorator-props']?.fixedBlock} |  | ||||||
|       onChange={async (fixedBlock) => { |  | ||||||
|         const decoratorProps = { |  | ||||||
|           ...fieldSchema['x-decorator-props'], |  | ||||||
|           fixedBlock, |  | ||||||
|         }; |  | ||||||
|         await dn.emit('patch', { |  | ||||||
|           schema: { |  | ||||||
|             ['x-uid']: fieldSchema['x-uid'], |  | ||||||
|             'x-decorator-props': decoratorProps, |  | ||||||
|           }, |  | ||||||
|         }); |  | ||||||
|         field.decoratorProps = fieldSchema['x-decorator-props'] = decoratorProps; |  | ||||||
|       }} |  | ||||||
|     /> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| interface FixedBlockProps { |  | ||||||
|   height: number | string; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const fixedBlockCss = css` |  | ||||||
|   overflow: hidden; |  | ||||||
|   position: relative; |  | ||||||
|   .noco-card-item { |  | ||||||
|     height: 100%; |  | ||||||
|     .ant-card { |  | ||||||
|       display: flex; |  | ||||||
|       flex-direction: column; |  | ||||||
|       height: 100%; |  | ||||||
|       .ant-card-body { |  | ||||||
|         height: 1px; |  | ||||||
|         flex: 1; |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         overflow: hidden; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| `;
 |  | ||||||
| 
 |  | ||||||
| const FixedBlock: React.FC<FixedBlockProps> = (props) => { |  | ||||||
|   const { height } = props; |  | ||||||
|   const [fixedBlockUID, _setFixedBlock] = useState<false | string>(false); |  | ||||||
|   const fixedBlockUIDRef = useRef(fixedBlockUID); |  | ||||||
|   const setFixedBlock = (v) => { |  | ||||||
|     fixedBlockUIDRef.current = v; |  | ||||||
|     _setFixedBlock(v); |  | ||||||
|   }; |  | ||||||
|   return ( |  | ||||||
|     <FixedBlockContext.Provider value={{ inFixedBlock: true, height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}> |  | ||||||
|       <div |  | ||||||
|         className={fixedBlockUID ? fixedBlockCss : ''} |  | ||||||
|         style={{ |  | ||||||
|           height: fixedBlockUID ? `calc(100vh - ${height})` : undefined, |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         {props.children} |  | ||||||
|       </div> |  | ||||||
|     </FixedBlockContext.Provider> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default FixedBlock; |  | ||||||
| @ -1,139 +0,0 @@ | |||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { ISchema, useField, useFieldSchema } from '@formily/react'; |  | ||||||
| import { SchemaSettings, useDesignable, useSchemaToolbar } from '@nocobase/client'; |  | ||||||
| 
 |  | ||||||
| function useNotDisableHeader() { |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   return !fieldSchema['x-component-props']?.disablePageHeader; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const pageSettings = new SchemaSettings({ |  | ||||||
|   name: 'PageSettings', |  | ||||||
|   items: [ |  | ||||||
|     { |  | ||||||
|       name: 'enablePageHeader', |  | ||||||
|       type: 'switch', |  | ||||||
|       useComponentProps() { |  | ||||||
|         const { dn } = useDesignable(); |  | ||||||
|         const { t } = useTranslation(); |  | ||||||
|         const fieldSchema = useFieldSchema(); |  | ||||||
|         return { |  | ||||||
|           title: t('Enable page header'), |  | ||||||
|           checked: !fieldSchema['x-component-props']?.disablePageHeader, |  | ||||||
|           onChange(v) { |  | ||||||
|             fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; |  | ||||||
|             fieldSchema['x-component-props']['disablePageHeader'] = !v; |  | ||||||
|             dn.emit('patch', { |  | ||||||
|               schema: { |  | ||||||
|                 ['x-uid']: fieldSchema['x-uid'], |  | ||||||
|                 ['x-component-props']: fieldSchema['x-component-props'], |  | ||||||
|               }, |  | ||||||
|             }); |  | ||||||
|             dn.refresh(); |  | ||||||
|           }, |  | ||||||
|         }; |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'divider', |  | ||||||
|       type: 'divider', |  | ||||||
|       useVisible: useNotDisableHeader, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'displayPageTitle', |  | ||||||
|       type: 'switch', |  | ||||||
|       useVisible: useNotDisableHeader, |  | ||||||
|       useComponentProps() { |  | ||||||
|         const { dn } = useDesignable(); |  | ||||||
|         const { t } = useTranslation(); |  | ||||||
|         const fieldSchema = useFieldSchema(); |  | ||||||
|         return { |  | ||||||
|           title: t('Display page title'), |  | ||||||
|           checked: !fieldSchema['x-component-props']?.hidePageTitle, |  | ||||||
|           onChange(v) { |  | ||||||
|             fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; |  | ||||||
|             fieldSchema['x-component-props']['hidePageTitle'] = !v; |  | ||||||
|             dn.emit('patch', { |  | ||||||
|               schema: { |  | ||||||
|                 ['x-uid']: fieldSchema['x-uid'], |  | ||||||
|                 ['x-component-props']: fieldSchema['x-component-props'], |  | ||||||
|               }, |  | ||||||
|             }); |  | ||||||
|             dn.refresh(); |  | ||||||
|           }, |  | ||||||
|         }; |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'editPageTitle', |  | ||||||
|       type: 'modal', |  | ||||||
|       useVisible() { |  | ||||||
|         const notDisablePageHeader = useNotDisableHeader(); |  | ||||||
|         const fieldSchema = useFieldSchema(); |  | ||||||
|         const notHidePageTitle = !fieldSchema['x-component-props']?.hidePageTitle; |  | ||||||
| 
 |  | ||||||
|         return notDisablePageHeader && notHidePageTitle; |  | ||||||
|       }, |  | ||||||
|       useComponentProps() { |  | ||||||
|         const { dn } = useDesignable(); |  | ||||||
|         const { t } = useTranslation(); |  | ||||||
|         const field = useField(); |  | ||||||
|         const fieldSchema = useFieldSchema(); |  | ||||||
|         const { title } = useSchemaToolbar(); |  | ||||||
|         return { |  | ||||||
|           hide: true, |  | ||||||
|           title: t('Edit page title'), |  | ||||||
|           schema: { |  | ||||||
|             type: 'object', |  | ||||||
|             title: t('Edit page title'), |  | ||||||
|             properties: { |  | ||||||
|               title: { |  | ||||||
|                 title: t('Title'), |  | ||||||
|                 required: true, |  | ||||||
|                 'x-decorator': 'FormItem', |  | ||||||
|                 'x-component': 'Input', |  | ||||||
|                 'x-component-props': {}, |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|           } as ISchema, |  | ||||||
|           initialValues: { title }, |  | ||||||
|           onSubmit({ title }) { |  | ||||||
|             field.title = title; |  | ||||||
|             fieldSchema['title'] = title; |  | ||||||
|             dn.emit('patch', { |  | ||||||
|               schema: { |  | ||||||
|                 ['x-uid']: fieldSchema['x-uid'], |  | ||||||
|                 title, |  | ||||||
|               }, |  | ||||||
|             }); |  | ||||||
|           }, |  | ||||||
|         }; |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'enablePageTabs', |  | ||||||
|       type: 'switch', |  | ||||||
|       useVisible: useNotDisableHeader, |  | ||||||
|       useComponentProps() { |  | ||||||
|         const { dn } = useDesignable(); |  | ||||||
|         const { t } = useTranslation(); |  | ||||||
|         const fieldSchema = useFieldSchema(); |  | ||||||
|         return { |  | ||||||
|           title: t('Enable page tabs'), |  | ||||||
|           checked: fieldSchema['x-component-props']?.enablePageTabs, |  | ||||||
|           onChange(v) { |  | ||||||
|             fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; |  | ||||||
|             fieldSchema['x-component-props']['enablePageTabs'] = v; |  | ||||||
|             dn.emit('patch', { |  | ||||||
|               schema: { |  | ||||||
|                 ['x-uid']: fieldSchema['x-uid'], |  | ||||||
|                 ['x-component-props']: fieldSchema['x-component-props'], |  | ||||||
|               }, |  | ||||||
|             }); |  | ||||||
|             dn.refresh(); |  | ||||||
|           }, |  | ||||||
|         }; |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }); |  | ||||||
| @ -1,235 +0,0 @@ | |||||||
| import { PlusOutlined } from '@ant-design/icons'; |  | ||||||
| import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout'; |  | ||||||
| import { FormLayout } from '@formily/antd-v5'; |  | ||||||
| import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react'; |  | ||||||
| import { Button, Tabs } from 'antd'; |  | ||||||
| import classNames from 'classnames'; |  | ||||||
| import React, { useContext, useEffect, useMemo, useState } from 'react'; |  | ||||||
| import { ErrorBoundary } from 'react-error-boundary'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { useSearchParams } from 'react-router-dom'; |  | ||||||
| import FixedBlock from './FixedBlock'; |  | ||||||
| import { PageDesigner, PageTabDesigner } from './PageTabDesigner'; |  | ||||||
| import { useAClStyles, useStyles } from './style'; |  | ||||||
| import { |  | ||||||
|   DndContext, |  | ||||||
|   FilterBlockProvider, |  | ||||||
|   FormDialog, |  | ||||||
|   Icon, |  | ||||||
|   SchemaComponent, |  | ||||||
|   SchemaComponentOptions, |  | ||||||
|   SortableItem, |  | ||||||
|   useCompile, |  | ||||||
|   useDesignable, |  | ||||||
|   useDocumentTitle, |  | ||||||
|   useGlobalTheme, |  | ||||||
|   useToken, |  | ||||||
| } from '@nocobase/client'; |  | ||||||
| import { useAppSpin } from '../../layouts'; |  | ||||||
| import { useGetAriaLabelOfSchemaInitializer } from './hooks/useGetAriaLabelOfSchemaInitializer'; |  | ||||||
| import { ErrorFallback } from '../error-fallback'; |  | ||||||
| 
 |  | ||||||
| export const Page = (props) => { |  | ||||||
|   const { children, ...others } = props; |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const compile = useCompile(); |  | ||||||
|   const { title, setTitle } = useDocumentTitle(); |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const dn = useDesignable(); |  | ||||||
|   const { theme } = useGlobalTheme(); |  | ||||||
|   const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer(); |  | ||||||
| 
 |  | ||||||
|   // react18  tab 动画会卡顿,所以第一个 tab 时,动画禁用,后面的 tab 才启用
 |  | ||||||
|   const [hasMounted, setHasMounted] = useState(false); |  | ||||||
|   useEffect(() => { |  | ||||||
|     setTimeout(() => { |  | ||||||
|       setHasMounted(true); |  | ||||||
|     }); |  | ||||||
|   }, []); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (!title) { |  | ||||||
|       setTitle(t(fieldSchema.title)); |  | ||||||
|     } |  | ||||||
|   }, [fieldSchema.title, title]); |  | ||||||
|   const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader; |  | ||||||
|   const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs; |  | ||||||
|   const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle; |  | ||||||
|   const options = useContext(SchemaOptionsContext); |  | ||||||
|   const [searchParams, setSearchParams] = useSearchParams(); |  | ||||||
|   const [loading, setLoading] = useState(false); |  | ||||||
|   const activeKey = useMemo( |  | ||||||
|     () => searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(), |  | ||||||
|     [fieldSchema.properties, searchParams], |  | ||||||
|   ); |  | ||||||
|   const [height, setHeight] = useState(0); |  | ||||||
|   const { wrapSSR, hashId, componentCls } = useStyles(); |  | ||||||
|   const aclStyles = useAClStyles(); |  | ||||||
| 
 |  | ||||||
|   const handleErrors = (error) => { |  | ||||||
|     console.error(error); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const pageHeaderTitle = hidePageTitle ? undefined : fieldSchema.title || compile(title); |  | ||||||
|   return wrapSSR( |  | ||||||
|     <FilterBlockProvider> |  | ||||||
|       <div className={`${componentCls} ${hashId} ${aclStyles.styles}`}> |  | ||||||
|         <PageDesigner title={fieldSchema.title || title} /> |  | ||||||
|         <div |  | ||||||
|           ref={(ref) => { |  | ||||||
|             setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1); |  | ||||||
|           }} |  | ||||||
|         > |  | ||||||
|           {!disablePageHeader && ( |  | ||||||
|             <AntdPageHeader |  | ||||||
|               className={classNames('pageHeaderCss', pageHeaderTitle || enablePageTabs ? '' : 'height0')} |  | ||||||
|               ghost={false} |  | ||||||
|               // 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
 |  | ||||||
|               title={pageHeaderTitle || ' '} |  | ||||||
|               {...others} |  | ||||||
|               footer={ |  | ||||||
|                 enablePageTabs && ( |  | ||||||
|                   <DndContext> |  | ||||||
|                     <Tabs |  | ||||||
|                       size={'small'} |  | ||||||
|                       animated={hasMounted} |  | ||||||
|                       activeKey={activeKey} |  | ||||||
|                       onTabClick={(activeKey) => { |  | ||||||
|                         setLoading(true); |  | ||||||
|                         setSearchParams([['tab', activeKey]]); |  | ||||||
|                         setTimeout(() => { |  | ||||||
|                           setLoading(false); |  | ||||||
|                         }, 50); |  | ||||||
|                       }} |  | ||||||
|                       tabBarExtraContent={ |  | ||||||
|                         dn.designable && ( |  | ||||||
|                           <Button |  | ||||||
|                             aria-label={getAriaLabel('tabs')} |  | ||||||
|                             icon={<PlusOutlined />} |  | ||||||
|                             className={'addTabBtn'} |  | ||||||
|                             type={'dashed'} |  | ||||||
|                             onClick={async () => { |  | ||||||
|                               const values = await FormDialog( |  | ||||||
|                                 t('Add tab'), |  | ||||||
|                                 () => { |  | ||||||
|                                   return ( |  | ||||||
|                                     <SchemaComponentOptions |  | ||||||
|                                       scope={options.scope} |  | ||||||
|                                       components={{ ...options.components }} |  | ||||||
|                                     > |  | ||||||
|                                       <FormLayout layout={'vertical'}> |  | ||||||
|                                         <SchemaComponent |  | ||||||
|                                           schema={{ |  | ||||||
|                                             properties: { |  | ||||||
|                                               title: { |  | ||||||
|                                                 title: t('Tab name'), |  | ||||||
|                                                 'x-component': 'Input', |  | ||||||
|                                                 'x-decorator': 'FormItem', |  | ||||||
|                                                 required: true, |  | ||||||
|                                               }, |  | ||||||
|                                               icon: { |  | ||||||
|                                                 title: t('Icon'), |  | ||||||
|                                                 'x-component': 'IconPicker', |  | ||||||
|                                                 'x-decorator': 'FormItem', |  | ||||||
|                                               }, |  | ||||||
|                                             }, |  | ||||||
|                                           }} |  | ||||||
|                                         /> |  | ||||||
|                                       </FormLayout> |  | ||||||
|                                     </SchemaComponentOptions> |  | ||||||
|                                   ); |  | ||||||
|                                 }, |  | ||||||
|                                 theme, |  | ||||||
|                               ).open({ |  | ||||||
|                                 initialValues: {}, |  | ||||||
|                               }); |  | ||||||
|                               const { title, icon } = values; |  | ||||||
|                               dn.insertBeforeEnd({ |  | ||||||
|                                 type: 'void', |  | ||||||
|                                 title, |  | ||||||
|                                 'x-icon': icon, |  | ||||||
|                                 'x-component': 'Grid', |  | ||||||
|                                 'x-initializer': 'BlockInitializers', |  | ||||||
|                                 properties: {}, |  | ||||||
|                               }); |  | ||||||
|                             }} |  | ||||||
|                           > |  | ||||||
|                             {t('Add tab')} |  | ||||||
|                           </Button> |  | ||||||
|                         ) |  | ||||||
|                       } |  | ||||||
|                       items={fieldSchema.mapProperties((schema) => { |  | ||||||
|                         return { |  | ||||||
|                           label: ( |  | ||||||
|                             <SortableItem |  | ||||||
|                               id={schema.name as string} |  | ||||||
|                               schema={schema} |  | ||||||
|                               className={classNames('nb-action-link', 'designerCss', props.className)} |  | ||||||
|                             > |  | ||||||
|                               {schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />} |  | ||||||
|                               <span>{schema.title || t('Unnamed')}</span> |  | ||||||
|                               <PageTabDesigner schema={schema} /> |  | ||||||
|                             </SortableItem> |  | ||||||
|                           ), |  | ||||||
|                           key: schema.name as string, |  | ||||||
|                         }; |  | ||||||
|                       })} |  | ||||||
|                     /> |  | ||||||
|                   </DndContext> |  | ||||||
|                 ) |  | ||||||
|               } |  | ||||||
|             /> |  | ||||||
|           )} |  | ||||||
|         </div> |  | ||||||
|         <div className="nb-page-wrapper"> |  | ||||||
|           <ErrorBoundary FallbackComponent={ErrorFallback} onError={handleErrors}> |  | ||||||
|             {PageContent(loading, disablePageHeader, enablePageTabs, fieldSchema, activeKey, height, props)} |  | ||||||
|           </ErrorBoundary> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </FilterBlockProvider>, |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| Page.displayName = 'Page'; |  | ||||||
| 
 |  | ||||||
| function PageContent( |  | ||||||
|   loading: boolean, |  | ||||||
|   disablePageHeader: any, |  | ||||||
|   enablePageTabs: any, |  | ||||||
|   fieldSchema: Schema<any, any, any, any, any, any, any, any, any>, |  | ||||||
|   activeKey: string, |  | ||||||
|   height: number, |  | ||||||
|   props: any, |  | ||||||
| ): React.ReactNode { |  | ||||||
|   const { token } = useToken(); |  | ||||||
|   const { render } = useAppSpin(); |  | ||||||
| 
 |  | ||||||
|   if (loading) { |  | ||||||
|     return render(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return !disablePageHeader && enablePageTabs ? ( |  | ||||||
|     fieldSchema.mapProperties((schema) => { |  | ||||||
|       if (schema.name !== activeKey) return null; |  | ||||||
| 
 |  | ||||||
|       return ( |  | ||||||
|         <FixedBlock key={schema.name} height={`calc(${height}px + 46px + ${token.marginLG}px * 2)`}> |  | ||||||
|           <SchemaComponent |  | ||||||
|             schema={ |  | ||||||
|               new Schema({ |  | ||||||
|                 properties: { |  | ||||||
|                   [schema.name]: schema, |  | ||||||
|                 }, |  | ||||||
|               }) |  | ||||||
|             } |  | ||||||
|           /> |  | ||||||
|         </FixedBlock> |  | ||||||
|       ); |  | ||||||
|     }) |  | ||||||
|   ) : ( |  | ||||||
|     <FixedBlock height={`calc(${height}px + 46px + ${token.marginLG}px * 2)`}> |  | ||||||
|       <div className={`pageWithFixedBlockCss nb-page-content`}>{props.children}</div> |  | ||||||
|     </FixedBlock> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -1,83 +0,0 @@ | |||||||
| import { App } from 'antd'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { ISchema } from '@formily/json-schema'; |  | ||||||
| import { useDesignable, useSchemaToolbar, SchemaSettings } from '@nocobase/client'; |  | ||||||
| 
 |  | ||||||
| export const pageTabSettings = new SchemaSettings({ |  | ||||||
|   name: 'PageTabSettings', |  | ||||||
|   items: [ |  | ||||||
|     { |  | ||||||
|       name: 'edit', |  | ||||||
|       type: 'modal', |  | ||||||
|       useComponentProps() { |  | ||||||
|         const { t } = useTranslation(); |  | ||||||
|         const { schema } = useSchemaToolbar<{ schema: ISchema }>(); |  | ||||||
|         const { dn } = useDesignable(); |  | ||||||
|         return { |  | ||||||
|           title: t('Edit'), |  | ||||||
|           schema: { |  | ||||||
|             type: 'object', |  | ||||||
|             title: t('Edit tab'), |  | ||||||
|             properties: { |  | ||||||
|               title: { |  | ||||||
|                 title: t('Tab name'), |  | ||||||
|                 required: true, |  | ||||||
|                 'x-decorator': 'FormItem', |  | ||||||
|                 'x-component': 'Input', |  | ||||||
|                 'x-component-props': {}, |  | ||||||
|               }, |  | ||||||
|               icon: { |  | ||||||
|                 title: t('Icon'), |  | ||||||
|                 'x-decorator': 'FormItem', |  | ||||||
|                 'x-component': 'IconPicker', |  | ||||||
|                 'x-component-props': {}, |  | ||||||
|               }, |  | ||||||
|             }, |  | ||||||
|           } as ISchema, |  | ||||||
|           initialValues: { title: schema.title, icon: schema['x-icon'] }, |  | ||||||
|           onSubmit: ({ title, icon }) => { |  | ||||||
|             schema.title = title; |  | ||||||
|             schema['x-icon'] = icon; |  | ||||||
|             dn.emit('patch', { |  | ||||||
|               schema: { |  | ||||||
|                 ['x-uid']: schema['x-uid'], |  | ||||||
|                 title, |  | ||||||
|                 'x-icon': icon, |  | ||||||
|               }, |  | ||||||
|             }); |  | ||||||
|             dn.refresh(); |  | ||||||
|           }, |  | ||||||
|         }; |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'divider', |  | ||||||
|       type: 'divider', |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: 'delete', |  | ||||||
|       type: 'item', |  | ||||||
|       useComponentProps() { |  | ||||||
|         const { modal } = App.useApp(); |  | ||||||
|         const { dn } = useDesignable(); |  | ||||||
|         const { t } = useTranslation(); |  | ||||||
|         const { schema } = useSchemaToolbar(); |  | ||||||
|         return { |  | ||||||
|           title: 'Delete', |  | ||||||
|           eventKey: 'remove', |  | ||||||
|           onClick() { |  | ||||||
|             modal.confirm({ |  | ||||||
|               title: t('Delete block'), |  | ||||||
|               content: t('Are you sure you want to delete it?'), |  | ||||||
|               ...confirm, |  | ||||||
|               onOk() { |  | ||||||
|                 dn.remove(schema); |  | ||||||
|               }, |  | ||||||
|             }); |  | ||||||
|           }, |  | ||||||
|           children: t('Delete'), |  | ||||||
|         }; |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }); |  | ||||||
| @ -1,56 +0,0 @@ | |||||||
| import { DragOutlined } from '@ant-design/icons'; |  | ||||||
| import { useFieldSchema } from '@formily/react'; |  | ||||||
| import { DragHandler, SchemaToolbarProvider, useDesignable, useSchemaSettingsRender } from '@nocobase/client'; |  | ||||||
| import { Space } from 'antd'; |  | ||||||
| import React from 'react'; |  | ||||||
| import { useGetAriaLabelOfDesigner } from './hooks/useGetAriaLabelOfDesigner'; |  | ||||||
| 
 |  | ||||||
| export const PageDesigner = ({ title }) => { |  | ||||||
|   const { designable } = useDesignable(); |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const { render } = useSchemaSettingsRender( |  | ||||||
|     fieldSchema['x-settings'] || 'PageSettings', |  | ||||||
|     fieldSchema['x-settings-props'], |  | ||||||
|   ); |  | ||||||
|   if (!designable) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|   return ( |  | ||||||
|     <SchemaToolbarProvider title={title}> |  | ||||||
|       <div className={'general-schema-designer'}> |  | ||||||
|         <div className={'general-schema-designer-icons'}> |  | ||||||
|           <Space size={2} align={'center'}> |  | ||||||
|             {render()} |  | ||||||
|           </Space> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </SchemaToolbarProvider> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const PageTabDesigner = ({ schema }) => { |  | ||||||
|   const { designable } = useDesignable(); |  | ||||||
|   const { getAriaLabel } = useGetAriaLabelOfDesigner(); |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const { render } = useSchemaSettingsRender( |  | ||||||
|     fieldSchema['x-settings'] || 'PageTabSettings', |  | ||||||
|     fieldSchema['x-settings-props'], |  | ||||||
|   ); |  | ||||||
|   if (!designable) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|   return ( |  | ||||||
|     <SchemaToolbarProvider schema={schema}> |  | ||||||
|       <div className={'general-schema-designer'}> |  | ||||||
|         <div className={'general-schema-designer-icons'}> |  | ||||||
|           <Space size={3} align={'center'}> |  | ||||||
|             <DragHandler> |  | ||||||
|               <DragOutlined style={{ marginRight: 0 }} role="button" aria-label={getAriaLabel('drag-handler', 'tab')} /> |  | ||||||
|             </DragHandler> |  | ||||||
|             {render()} |  | ||||||
|           </Space> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </SchemaToolbarProvider> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| import React from 'react'; |  | ||||||
| import { render, screen, waitFor } from 'testUtils'; |  | ||||||
| import App1 from '../demos/demo1'; |  | ||||||
| import { GlobalThemeProvider } from '@nocobase/client'; |  | ||||||
| 
 |  | ||||||
| describe('Page', () => { |  | ||||||
|   it('should render correctly', async () => { |  | ||||||
|     render(<App1 />, { |  | ||||||
|       wrapper: GlobalThemeProvider, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(screen.getByText(/page title/i)).toBeInTheDocument(); |  | ||||||
|     }); |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(screen.getByText(/page content/i)).toBeInTheDocument(); |  | ||||||
|     }); |  | ||||||
|     await waitFor(() => { |  | ||||||
|       expect(document.title).toBe('Page Title - NocoBase'); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
| @ -1,37 +0,0 @@ | |||||||
| import { ISchema } from '@formily/react'; |  | ||||||
| import { DocumentTitleProvider, Page, SchemaComponent, SchemaComponentProvider, Application } from '@nocobase/client'; |  | ||||||
| import React from 'react'; |  | ||||||
| 
 |  | ||||||
| const schema: ISchema = { |  | ||||||
|   type: 'object', |  | ||||||
|   properties: { |  | ||||||
|     page1: { |  | ||||||
|       type: 'void', |  | ||||||
|       'x-component': 'Page', |  | ||||||
|       title: 'Page Title', |  | ||||||
|       properties: { |  | ||||||
|         content: { |  | ||||||
|           type: 'void', |  | ||||||
|           'x-component': 'div', |  | ||||||
|           'x-content': 'Page Content', |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const Root = () => { |  | ||||||
|   return ( |  | ||||||
|     <SchemaComponentProvider components={{ Page }}> |  | ||||||
|       <DocumentTitleProvider addonAfter={'NocoBase'}> |  | ||||||
|         <SchemaComponent schema={schema} /> |  | ||||||
|       </DocumentTitleProvider> |  | ||||||
|     </SchemaComponentProvider> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const app = new Application({ |  | ||||||
|   providers: [Root], |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export default app.getRootComponent(); |  | ||||||
| @ -1,28 +0,0 @@ | |||||||
| import { useFieldSchema } from '@formily/react'; |  | ||||||
| import { useCollection_deprecated } from '@nocobase/client'; |  | ||||||
| import { useCallback } from 'react'; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * label = 'designer' + name + x-component + [x-designer] + [collectionName] + [x-collection-field] + [postfix] |  | ||||||
|  * @returns |  | ||||||
|  */ |  | ||||||
| export const useGetAriaLabelOfDesigner = () => { |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const { name: _collectionName } = useCollection_deprecated(); |  | ||||||
|   const getAriaLabel = useCallback( |  | ||||||
|     (name: string, postfix?: string) => { |  | ||||||
|       if (!fieldSchema) return ''; |  | ||||||
| 
 |  | ||||||
|       const component = fieldSchema['x-component']; |  | ||||||
|       const designer = fieldSchema['x-designer'] ? `-${fieldSchema['x-designer']}` : ''; |  | ||||||
|       const collectionField = fieldSchema['x-collection-field'] ? `-${fieldSchema['x-collection-field']}` : ''; |  | ||||||
|       const collectionName = _collectionName ? `-${_collectionName}` : ''; |  | ||||||
|       postfix = postfix ? `-${postfix}` : ''; |  | ||||||
| 
 |  | ||||||
|       return `designer-${name}-${component}${designer}${collectionName}${collectionField}${postfix}`; |  | ||||||
|     }, |  | ||||||
|     [fieldSchema, _collectionName], |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   return { getAriaLabel }; |  | ||||||
| }; |  | ||||||
| @ -1,25 +0,0 @@ | |||||||
| import { useFieldSchema } from '@formily/react'; |  | ||||||
| import { useCollection_deprecated } from '@nocobase/client'; |  | ||||||
| import { useCallback } from 'react'; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * label = 'schema-initializer' + x-component + [x-initializer] + [collectionName] + [postfix] |  | ||||||
|  * @returns |  | ||||||
|  */ |  | ||||||
| export const useGetAriaLabelOfSchemaInitializer = () => { |  | ||||||
|   const fieldSchema = useFieldSchema(); |  | ||||||
|   const { name } = useCollection_deprecated(); |  | ||||||
|   const getAriaLabel = useCallback( |  | ||||||
|     (postfix?: string) => { |  | ||||||
|       if (!fieldSchema) return ''; |  | ||||||
|       const initializer = fieldSchema['x-initializer'] ? `-${fieldSchema['x-initializer']}` : ''; |  | ||||||
|       const collectionName = name ? `-${name}` : ''; |  | ||||||
|       postfix = postfix ? `-${postfix}` : ''; |  | ||||||
| 
 |  | ||||||
|       return `schema-initializer-${fieldSchema['x-component']}${initializer}${collectionName}${postfix}`; |  | ||||||
|     }, |  | ||||||
|     [fieldSchema, name], |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   return { getAriaLabel }; |  | ||||||
| }; |  | ||||||
| @ -1,15 +0,0 @@ | |||||||
| import { useActionContext } from '@nocobase/client'; |  | ||||||
| import { useCallback } from 'react'; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * 判断当前区块是否在页面而不是在弹窗中 |  | ||||||
|  */ |  | ||||||
| export const useIsBlockInPage = () => { |  | ||||||
|   const { visible } = useActionContext(); |  | ||||||
| 
 |  | ||||||
|   const isBlockInPage = useCallback(() => { |  | ||||||
|     return !visible; |  | ||||||
|   }, [visible]); |  | ||||||
| 
 |  | ||||||
|   return { isBlockInPage }; |  | ||||||
| }; |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| --- |  | ||||||
| group: |  | ||||||
|   title: Schema Components |  | ||||||
|   order: 3 |  | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| # Page |  | ||||||
| 
 |  | ||||||
| 可以与 DocumentTitleProvider 搭配使用,将 page title 显示在 document.title 上。 |  | ||||||
| 
 |  | ||||||
| <code src="./demos/demo1.tsx"></code> |  | ||||||
| @ -1,4 +0,0 @@ | |||||||
| export * from './Page'; |  | ||||||
| export * from './FixedBlock'; |  | ||||||
| export * from './PageTab.Settings'; |  | ||||||
| export * from './Page.Settings'; |  | ||||||
| @ -1,132 +0,0 @@ | |||||||
| import { createStyles } from 'antd-style'; |  | ||||||
| import { genStyleHook } from '../__builtins__'; |  | ||||||
| 
 |  | ||||||
| export const useAClStyles = createStyles(({ css }) => { |  | ||||||
|   return css` |  | ||||||
|     .ant-table-cell { |  | ||||||
|       > .ant-space-horizontal { |  | ||||||
|         .ant-space-item-split:has(+ .ant-space-item:empty) { |  | ||||||
|           display: none; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   `;
 |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const useStyles = genStyleHook('nb-page', (token) => { |  | ||||||
|   const { componentCls } = token; |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     [componentCls]: { |  | ||||||
|       position: 'relative', |  | ||||||
|       zIndex: 20, |  | ||||||
|       flex: 1, |  | ||||||
|       display: 'flex', |  | ||||||
|       flexDirection: 'column', |  | ||||||
|       '&:hover': { '> .general-schema-designer': { display: 'block' } }, |  | ||||||
|       '.ant-page-header': { zIndex: 1, position: 'relative' }, |  | ||||||
|       '> .general-schema-designer': { |  | ||||||
|         position: 'absolute', |  | ||||||
|         zIndex: 999, |  | ||||||
|         top: '0', |  | ||||||
|         bottom: '0', |  | ||||||
|         left: '0', |  | ||||||
|         right: '0', |  | ||||||
|         display: 'none', |  | ||||||
|         border: '0', |  | ||||||
|         pointerEvents: 'none', |  | ||||||
|         '> .general-schema-designer-icons': { |  | ||||||
|           zIndex: 9999, |  | ||||||
|           position: 'absolute', |  | ||||||
|           right: '2px', |  | ||||||
|           top: '2px', |  | ||||||
|           lineHeight: '16px', |  | ||||||
|           pointerEvents: 'all', |  | ||||||
|           '.ant-space-item': { |  | ||||||
|             backgroundColor: 'var(--colorSettings)', |  | ||||||
|             color: '#fff', |  | ||||||
|             lineHeight: '16px', |  | ||||||
|             width: '16px', |  | ||||||
|             paddingLeft: '1px', |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
| 
 |  | ||||||
|       '.pageHeaderCss': { |  | ||||||
|         backgroundColor: token.colorBgContainer, |  | ||||||
|         paddingInline: token.paddingLG, |  | ||||||
|         '&.ant-page-header-has-footer': { |  | ||||||
|           paddingTop: token.paddingSM, |  | ||||||
|           paddingBottom: '0', |  | ||||||
|           '.ant-page-header-heading-left': {}, |  | ||||||
|           '.ant-page-header-footer': { marginBlockStart: '0' }, |  | ||||||
|         }, |  | ||||||
|         '.ant-tabs-nav': { marginBottom: '0' }, |  | ||||||
|         '.ant-page-header-heading-title': { |  | ||||||
|           color: token.colorText, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
| 
 |  | ||||||
|       '.height0': { |  | ||||||
|         fontSize: 0, |  | ||||||
|         height: 0, |  | ||||||
|       }, |  | ||||||
| 
 |  | ||||||
|       '.addTabBtn': { |  | ||||||
|         borderColor: 'var(--colorSettings)', |  | ||||||
|         color: 'var(--colorSettings)', |  | ||||||
|       }, |  | ||||||
| 
 |  | ||||||
|       '.designerCss': { |  | ||||||
|         position: 'relative', |  | ||||||
|         '&:hover': { '> .general-schema-designer': { display: 'block' } }, |  | ||||||
|         '&.nb-action-link': { |  | ||||||
|           '> .general-schema-designer': { |  | ||||||
|             top: '-10px', |  | ||||||
|             bottom: '-10px', |  | ||||||
|             left: '-10px', |  | ||||||
|             right: '-10px', |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         '> .general-schema-designer': { |  | ||||||
|           position: 'absolute', |  | ||||||
|           zIndex: 999, |  | ||||||
|           top: '0', |  | ||||||
|           bottom: '0', |  | ||||||
|           left: '0', |  | ||||||
|           right: '0', |  | ||||||
|           display: 'none', |  | ||||||
|           background: 'var(--colorBgSettingsHover)', |  | ||||||
|           border: '0', |  | ||||||
|           pointerEvents: 'none', |  | ||||||
|           '> .general-schema-designer-icons': { |  | ||||||
|             position: 'absolute', |  | ||||||
|             right: '2px', |  | ||||||
|             top: '2px', |  | ||||||
|             lineHeight: '16px', |  | ||||||
|             pointerEvents: 'all', |  | ||||||
|             '.ant-space-item': { |  | ||||||
|               backgroundColor: 'var(--colorSettings)', |  | ||||||
|               color: '#fff', |  | ||||||
|               lineHeight: '16px', |  | ||||||
|               width: '16px', |  | ||||||
|               paddingLeft: '1px', |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
| 
 |  | ||||||
|       '.pageWithFixedBlockCss': { |  | ||||||
|         height: '100%', |  | ||||||
|         '> .nb-grid:not(:last-child)': { |  | ||||||
|           '> .nb-schema-initializer-button': { display: 'none' }, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
| 
 |  | ||||||
|       '.nb-page-wrapper': { |  | ||||||
|         margin: token.marginLG, |  | ||||||
|         flex: 1, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }; |  | ||||||
| }); |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user