Merge branch 'next' into develop

This commit is contained in:
Zeke Zhang
2025-05-20 12:04:30 +08:00
4 changed files with 119 additions and 140 deletions
@@ -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>
);
}