mirror of
https://github.com/nocobase/nocobase.git
synced 2026-10-29 18:16:18 +08:00
Merge branch 'next' into develop
This commit is contained in:
@@ -16,7 +16,6 @@ import {
|
||||
NavigateOptions,
|
||||
useHref,
|
||||
useLocation,
|
||||
useMatch,
|
||||
useNavigate,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
@@ -40,12 +39,6 @@ IsAdminPageContext.displayName = 'IsAdminPageContext';
|
||||
export const CurrentPageUidContext = React.createContext<string>('');
|
||||
CurrentPageUidContext.displayName = 'CurrentPageUidContext';
|
||||
|
||||
const MatchAdminContext = React.createContext<boolean>(false);
|
||||
MatchAdminContext.displayName = 'MatchAdminContext';
|
||||
|
||||
const MatchAdminNameContext = React.createContext<boolean>(false);
|
||||
MatchAdminNameContext.displayName = 'MatchAdminNameContext';
|
||||
|
||||
const IsInSettingsPageContext = React.createContext<boolean>(false);
|
||||
IsInSettingsPageContext.displayName = 'IsInSettingsPageContext';
|
||||
|
||||
@@ -61,7 +54,7 @@ SearchParamsContext.displayName = 'SearchParamsContext';
|
||||
const RouterBasenameContext = React.createContext<string>('');
|
||||
RouterBasenameContext.displayName = 'RouterBasenameContext';
|
||||
|
||||
const IsSubPageClosedByPageMenuContext = React.createContext<{
|
||||
export const IsSubPageClosedByPageMenuContext = React.createContext<{
|
||||
isSubPageClosedByPageMenu: () => boolean;
|
||||
setFieldSchema: React.Dispatch<React.SetStateAction<Schema>>;
|
||||
reset: () => void;
|
||||
@@ -121,16 +114,6 @@ const IsInSettingsPageProvider: FC = ({ children }) => {
|
||||
return <IsInSettingsPageContext.Provider value={isInSettingsPage}>{children}</IsInSettingsPageContext.Provider>;
|
||||
};
|
||||
|
||||
const MatchAdminProvider: FC = ({ children }) => {
|
||||
const isMatchAdmin = !!useMatch('/admin');
|
||||
return <MatchAdminContext.Provider value={isMatchAdmin}>{children}</MatchAdminContext.Provider>;
|
||||
};
|
||||
|
||||
const MatchAdminNameProvider: FC = ({ children }) => {
|
||||
const isMatchAdminName = !!useMatch('/admin/:name');
|
||||
return <MatchAdminNameContext.Provider value={isMatchAdminName}>{children}</MatchAdminNameContext.Provider>;
|
||||
};
|
||||
|
||||
const IsAdminPageProvider: FC = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const isAdminPage = location.pathname.startsWith('/admin');
|
||||
@@ -219,14 +202,6 @@ export const useCurrentPageUid = () => {
|
||||
return React.useContext(CurrentPageUidContext);
|
||||
};
|
||||
|
||||
export const useMatchAdmin = () => {
|
||||
return React.useContext(MatchAdminContext);
|
||||
};
|
||||
|
||||
export const useMatchAdminName = () => {
|
||||
return React.useContext(MatchAdminNameContext);
|
||||
};
|
||||
|
||||
export const useIsInSettingsPage = () => {
|
||||
return React.useContext(IsInSettingsPageContext);
|
||||
};
|
||||
@@ -266,15 +241,11 @@ export const CustomRouterContextProvider: FC = ({ children }) => {
|
||||
<LocationNoUpdateProvider>
|
||||
<IsAdminPageProvider>
|
||||
<LocationSearchProvider>
|
||||
<MatchAdminProvider>
|
||||
<MatchAdminNameProvider>
|
||||
<SearchParamsProvider>
|
||||
<RouterBasenameProvider>
|
||||
<IsInSettingsPageProvider>{children}</IsInSettingsPageProvider>
|
||||
</RouterBasenameProvider>
|
||||
</SearchParamsProvider>
|
||||
</MatchAdminNameProvider>
|
||||
</MatchAdminProvider>
|
||||
<SearchParamsProvider>
|
||||
<RouterBasenameProvider>
|
||||
<IsInSettingsPageProvider>{children}</IsInSettingsPageProvider>
|
||||
</RouterBasenameProvider>
|
||||
</SearchParamsProvider>
|
||||
</LocationSearchProvider>
|
||||
</IsAdminPageProvider>
|
||||
</LocationNoUpdateProvider>
|
||||
|
||||
@@ -7,12 +7,19 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { RouteContext } from '@ant-design/pro-layout';
|
||||
import { SchemaComponentsContext, SchemaExpressionScopeContext, SchemaOptionsContext } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import React, { createContext, FC, useContext, useRef } from 'react';
|
||||
import { UNSAFE_LocationContext, UNSAFE_RouteContext } from 'react-router-dom';
|
||||
import { Context as MotionContext } from 'rc-motion/es/context';
|
||||
import React, { createContext, FC, memo, useContext, useRef } from 'react';
|
||||
import {
|
||||
UNSAFE_DataRouterContext,
|
||||
UNSAFE_DataRouterStateContext,
|
||||
UNSAFE_LocationContext,
|
||||
UNSAFE_RouteContext,
|
||||
} from 'react-router-dom';
|
||||
import { ACLContext } from '../../../acl/ACLProvider';
|
||||
import { CurrentPageUidContext } from '../../../application/CustomRouterContextProvider';
|
||||
import { IsSubPageClosedByPageMenuContext } from '../../../application/CustomRouterContextProvider';
|
||||
import { SchemaComponentContext } from '../../../schema-component/context';
|
||||
|
||||
const KeepAliveContext = createContext(true);
|
||||
@@ -56,40 +63,77 @@ const DesignableInterceptor: FC<{ active: boolean }> = ({ children, active }) =>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeepAliveProvider: FC<{ active: boolean }> = ({ children, active }) => {
|
||||
const currentLocationContext = useContext(UNSAFE_LocationContext);
|
||||
const currentRouteContext = useContext(UNSAFE_RouteContext);
|
||||
const prevLocationContextRef = useRef(currentLocationContext);
|
||||
const prevRouteContextRef = useRef(currentRouteContext);
|
||||
const hidden = { display: 'none' };
|
||||
|
||||
if (
|
||||
active &&
|
||||
// Skip comparing location key to improve LocationContext rendering performance
|
||||
!_.isEqual(_.omit(prevLocationContextRef.current.location, 'key'), _.omit(currentLocationContext.location, 'key'))
|
||||
) {
|
||||
prevLocationContextRef.current = currentLocationContext;
|
||||
}
|
||||
export const KeepAliveProvider: FC<{ active: boolean; parentActive: boolean }> = memo(
|
||||
({ children, active, parentActive }) => {
|
||||
const currentLocationContext = useContext(UNSAFE_LocationContext);
|
||||
const currentRouteContext = useContext(UNSAFE_RouteContext);
|
||||
const currentDataRouterContext = useContext(UNSAFE_DataRouterContext);
|
||||
const currentDataRouterStateContext = useContext(UNSAFE_DataRouterStateContext);
|
||||
const subPageClosedContext = useContext(IsSubPageClosedByPageMenuContext);
|
||||
const motionContext = useContext(MotionContext);
|
||||
const routeContextValue = useContext(RouteContext);
|
||||
|
||||
if (active && !_.isEqual(prevRouteContextRef.current, currentRouteContext)) {
|
||||
prevRouteContextRef.current = currentRouteContext;
|
||||
}
|
||||
const prevLocationContextRef = useRef(currentLocationContext);
|
||||
const prevRouteContextRef = useRef(currentRouteContext);
|
||||
const prevDataRouterContextRef = useRef(currentDataRouterContext);
|
||||
const prevDataRouterStateContextRef = useRef(currentDataRouterStateContext);
|
||||
const prevSubPageClosedContextRef = useRef(subPageClosedContext);
|
||||
const prevMotionContextRef = useRef(motionContext);
|
||||
const prevRouteContextValueRef = useRef(routeContextValue);
|
||||
|
||||
// When the page is inactive, we use UNSAFE_LocationContext and UNSAFE_RouteContext to prevent child components
|
||||
// from receiving Context updates, thereby optimizing performance.
|
||||
// This is based on how React Context works:
|
||||
// 1. When Context value changes, React traverses the component tree from top to bottom
|
||||
// 2. During traversal, React finds components using that Context and marks them for update
|
||||
// 3. When encountering the same Context Provider, traversal stops, avoiding unnecessary child component updates
|
||||
return (
|
||||
<KeepAliveContext.Provider value={active}>
|
||||
<DesignableInterceptor active={active}>
|
||||
<UNSAFE_LocationContext.Provider value={prevLocationContextRef.current}>
|
||||
<UNSAFE_RouteContext.Provider value={prevRouteContextRef.current}>{children}</UNSAFE_RouteContext.Provider>
|
||||
</UNSAFE_LocationContext.Provider>
|
||||
</DesignableInterceptor>
|
||||
</KeepAliveContext.Provider>
|
||||
);
|
||||
};
|
||||
if (active) {
|
||||
prevDataRouterContextRef.current = currentDataRouterContext;
|
||||
prevDataRouterStateContextRef.current = currentDataRouterStateContext;
|
||||
prevSubPageClosedContextRef.current = subPageClosedContext;
|
||||
prevMotionContextRef.current = motionContext;
|
||||
prevRouteContextValueRef.current = routeContextValue;
|
||||
}
|
||||
|
||||
if (
|
||||
active &&
|
||||
// Skip comparing location key to improve LocationContext rendering performance
|
||||
!_.isEqual(_.omit(prevLocationContextRef.current.location, 'key'), _.omit(currentLocationContext.location, 'key'))
|
||||
) {
|
||||
prevLocationContextRef.current = currentLocationContext;
|
||||
}
|
||||
|
||||
if (active && !_.isEqual(prevRouteContextRef.current, currentRouteContext)) {
|
||||
prevRouteContextRef.current = currentRouteContext;
|
||||
}
|
||||
|
||||
// When the page is inactive, we use UNSAFE_LocationContext and UNSAFE_RouteContext to prevent child components
|
||||
// from receiving Context updates, thereby optimizing performance.
|
||||
// This is based on how React Context works:
|
||||
// 1. When Context value changes, React traverses the component tree from top to bottom
|
||||
// 2. During traversal, React finds components using that Context and marks them for update
|
||||
// 3. When encountering the same Context Provider, traversal stops, avoiding unnecessary child component updates
|
||||
const contextProviders = (
|
||||
<RouteContext.Provider value={prevRouteContextValueRef.current}>
|
||||
<MotionContext.Provider value={prevMotionContextRef.current}>
|
||||
<UNSAFE_DataRouterContext.Provider value={prevDataRouterContextRef.current}>
|
||||
<UNSAFE_DataRouterStateContext.Provider value={prevDataRouterStateContextRef.current}>
|
||||
<UNSAFE_LocationContext.Provider value={prevLocationContextRef.current}>
|
||||
<UNSAFE_RouteContext.Provider value={prevRouteContextRef.current}>
|
||||
<KeepAliveContext.Provider value={parentActive === false ? false : active}>
|
||||
<DesignableInterceptor active={active}>
|
||||
<IsSubPageClosedByPageMenuContext.Provider value={prevSubPageClosedContextRef.current}>
|
||||
{children}
|
||||
</IsSubPageClosedByPageMenuContext.Provider>
|
||||
</DesignableInterceptor>
|
||||
</KeepAliveContext.Provider>
|
||||
</UNSAFE_RouteContext.Provider>
|
||||
</UNSAFE_LocationContext.Provider>
|
||||
</UNSAFE_DataRouterStateContext.Provider>
|
||||
</UNSAFE_DataRouterContext.Provider>
|
||||
</MotionContext.Provider>
|
||||
</RouteContext.Provider>
|
||||
);
|
||||
|
||||
return <div style={active ? null : hidden}>{contextProviders}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Used on components that don't need KeepAlive context, to improve performance when Context values change
|
||||
@@ -169,21 +213,22 @@ const MAX_RENDERED_PAGE_COUNT = getMaxPageCount();
|
||||
* Implements a Vue-like KeepAlive effect
|
||||
*/
|
||||
export const KeepAlive: FC<KeepAliveProps> = React.memo(({ children, uid }) => {
|
||||
const renderedPageRef = useRef([]);
|
||||
const { active } = useKeepAlive();
|
||||
const renderedUidListRef = useRef([]);
|
||||
|
||||
if (!renderedPageRef.current.includes(uid)) {
|
||||
renderedPageRef.current.push(uid);
|
||||
if (renderedPageRef.current.length > MAX_RENDERED_PAGE_COUNT) {
|
||||
renderedPageRef.current = renderedPageRef.current.slice(-MAX_RENDERED_PAGE_COUNT);
|
||||
if (!renderedUidListRef.current.includes(uid)) {
|
||||
renderedUidListRef.current.push(uid);
|
||||
if (renderedUidListRef.current.length > MAX_RENDERED_PAGE_COUNT) {
|
||||
renderedUidListRef.current = renderedUidListRef.current.slice(-MAX_RENDERED_PAGE_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderedPageRef.current.map((renderedUid) => (
|
||||
<CurrentPageUidContext.Provider value={renderedUid} key={renderedUid}>
|
||||
<KeepAliveProvider active={renderedUid === uid}>{children(renderedUid)}</KeepAliveProvider>
|
||||
</CurrentPageUidContext.Provider>
|
||||
{renderedUidListRef.current.map((renderedUid) => (
|
||||
<KeepAliveProvider active={renderedUid === uid} key={renderedUid} parentActive={active}>
|
||||
{children(renderedUid)}
|
||||
</KeepAliveProvider>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
||||
import { css } from '@emotion/css';
|
||||
import { theme as antdTheme, ConfigProvider, Popover, Result, Tooltip } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { createContext, FC, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
useToken,
|
||||
} from '../../../';
|
||||
import {
|
||||
CurrentPageUidContext,
|
||||
CurrentPageUidProvider,
|
||||
CurrentTabUidProvider,
|
||||
IsSubPageClosedByPageMenuProvider,
|
||||
@@ -50,21 +51,21 @@ import { AppNotFound } from '../../../common/AppNotFound';
|
||||
import { withTooltipComponent } from '../../../hoc/withTooltipComponent';
|
||||
import { menuItemInitializer } from '../../../modules/menu/menuItemInitializer';
|
||||
import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
|
||||
import { KeepAlive } from './KeepAlive';
|
||||
import { KeepAlive, useKeepAlive } from './KeepAlive';
|
||||
import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
|
||||
import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings';
|
||||
import { userCenterSettings } from './userCenterSettings';
|
||||
|
||||
export { KeepAlive, NocoBaseDesktopRouteType };
|
||||
export { KeepAlive, NocoBaseDesktopRouteType, useKeepAlive };
|
||||
|
||||
export const NocoBaseRouteContext = createContext<NocoBaseDesktopRoute | null>(null);
|
||||
NocoBaseRouteContext.displayName = 'NocoBaseRouteContext';
|
||||
|
||||
export const CurrentRouteProvider: FC<{ uid: string }> = ({ children, uid }) => {
|
||||
export const CurrentRouteProvider: FC<{ uid: string }> = memo(({ children, uid }) => {
|
||||
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||
const routeNode = useMemo(() => findRouteBySchemaUid(uid, allAccessRoutes), [uid, allAccessRoutes]);
|
||||
return <NocoBaseRouteContext.Provider value={routeNode}>{children}</NocoBaseRouteContext.Provider>;
|
||||
};
|
||||
});
|
||||
|
||||
export const useCurrentRoute = () => {
|
||||
return useContext(NocoBaseRouteContext) || {};
|
||||
@@ -76,7 +77,7 @@ const AllAccessDesktopRoutesContext = createContext<{
|
||||
refresh: () => void;
|
||||
}>({
|
||||
allAccessRoutes: emptyArray,
|
||||
refresh: () => {},
|
||||
refresh: () => { },
|
||||
});
|
||||
AllAccessDesktopRoutesContext.displayName = 'AllAccessDesktopRoutesContext';
|
||||
|
||||
@@ -144,9 +145,11 @@ export const AdminDynamicPage = () => {
|
||||
return (
|
||||
<KeepAlive uid={currentPageUid}>
|
||||
{(uid) => (
|
||||
<CurrentRouteProvider uid={uid}>
|
||||
<RemoteSchemaComponent uid={uid} />
|
||||
</CurrentRouteProvider>
|
||||
<CurrentPageUidContext.Provider value={uid}>
|
||||
<CurrentRouteProvider uid={uid}>
|
||||
<RemoteSchemaComponent uid={uid} />
|
||||
</CurrentRouteProvider>
|
||||
</CurrentPageUidContext.Provider>
|
||||
)}
|
||||
</KeepAlive>
|
||||
);
|
||||
@@ -589,7 +592,7 @@ const IsMobileLayoutContext = React.createContext<{
|
||||
setIsMobileLayout: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}>({
|
||||
isMobileLayout: false,
|
||||
setIsMobileLayout: () => {},
|
||||
setIsMobileLayout: () => { },
|
||||
});
|
||||
|
||||
const MobileLayoutProvider: FC = (props) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
useCurrentRoute,
|
||||
useMobileLayout,
|
||||
} from '../../../route-switch/antd/admin-layout';
|
||||
import { KeepAliveProvider, useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||
import { KeepAlive, useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
|
||||
import { DndContext } from '../../common';
|
||||
import { SortableItem } from '../../common/sortable-item';
|
||||
@@ -111,13 +111,6 @@ const InternalPage = React.memo((props: PageProps) => {
|
||||
);
|
||||
});
|
||||
|
||||
const hiddenStyle: React.CSSProperties = {
|
||||
// Visually hide the element while keeping it in document flow to prevent reflow/repaint
|
||||
transform: 'scale(0)',
|
||||
// Prevent element from receiving any pointer events (clicks, hovers etc) to avoid interfering with other elements
|
||||
pointerEvents: 'none',
|
||||
};
|
||||
|
||||
export const Page = React.memo((props: PageProps) => {
|
||||
const { hashId, componentCls } = useStyles();
|
||||
const { active: pageActive } = useKeepAlive();
|
||||
@@ -130,7 +123,7 @@ export const Page = React.memo((props: PageProps) => {
|
||||
|
||||
return (
|
||||
<AllDataBlocksProvider>
|
||||
<div className={`${componentCls} ${hashId} ${antTableCell}`} style={pageActive ? null : hiddenStyle}>
|
||||
<div className={`${componentCls} ${hashId} ${antTableCell}`}>
|
||||
{/* Avoid passing values down to improve rendering performance */}
|
||||
<CurrentTabUidContext.Provider value={''}>
|
||||
<InternalPage currentTabUid={tabUidRef.current} className={props.className} />
|
||||
@@ -168,40 +161,6 @@ const className1 = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const displayBlock = {
|
||||
display: 'block',
|
||||
};
|
||||
|
||||
const displayNone = {
|
||||
display: 'none',
|
||||
};
|
||||
|
||||
// Add a TabPane component to manage caching, implementing an effect similar to Vue's keep-alive
|
||||
const TabPane = React.memo(({ active: tabActive, uid }: { active: boolean; uid: string }) => {
|
||||
const mountedRef = useRef(false);
|
||||
const { active: pageActive } = useKeepAlive();
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
if (tabActive && !mountedRef.current) {
|
||||
mountedRef.current = true;
|
||||
}
|
||||
|
||||
if (!mountedRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={tabActive ? displayBlock : displayNone}>
|
||||
<KeepAliveProvider active={pageActive && tabActive}>
|
||||
<RemoteSchemaComponent
|
||||
uid={uid}
|
||||
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||
/>
|
||||
</KeepAliveProvider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface PageContentProps {
|
||||
loading: boolean;
|
||||
disablePageHeader: any;
|
||||
@@ -243,15 +202,16 @@ const InternalPageContent = (props: PageContentProps) => {
|
||||
|
||||
if (!disablePageHeader && enablePageTabs) {
|
||||
return (
|
||||
<>
|
||||
{currentRoute.children?.map((tabRoute) => {
|
||||
return (
|
||||
<NocoBaseRouteContext.Provider value={tabRoute} key={tabRoute.schemaUid}>
|
||||
<TabPane active={tabRoute.schemaUid === activeKey} uid={tabRoute.schemaUid} />
|
||||
</NocoBaseRouteContext.Provider>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<KeepAlive uid={activeKey}>
|
||||
{(uid) => (
|
||||
<NocoBaseRouteContext.Provider value={currentRoute.children?.find((item) => item.schemaUid === uid)}>
|
||||
<RemoteSchemaComponent
|
||||
uid={uid}
|
||||
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||
/>
|
||||
</NocoBaseRouteContext.Provider>
|
||||
)}
|
||||
</KeepAlive>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user