fix: resolve tab switching issue (#5081)

* refactor: convert parameters to destructured object

* fix: resolve tab switching issue in multi-app pages

* fix: fix ineffective tab switching within nested popups
This commit is contained in:
Zeke Zhang 2024-08-18 08:38:52 +08:00 committed by GitHub
parent e61e6d0842
commit 70d96c3e33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 208 additions and 19 deletions

View File

@ -8,6 +8,7 @@
*/
export * from './useApp';
export * from './useAppSpin';
export * from './usePlugin';
export * from './useRouter';
export * from './useAppSpin';
export * from './useRouterBasename';

View File

@ -0,0 +1,19 @@
/**
* 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 { useHref } from 'react-router-dom';
/**
* see: https://stackoverflow.com/questions/50449423/accessing-basename-of-browserouter
* @returns {string} basename
*/
export const useRouterBasename = () => {
const basenameOfCurrentRouter = useHref('/');
return basenameOfCurrentRouter;
};

View File

@ -20,7 +20,7 @@ import omit from 'lodash/omit';
import qs from 'qs';
import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { NavigateFunction, useHref } from 'react-router-dom';
import { NavigateFunction } from 'react-router-dom';
import { useReactToPrint } from 'react-to-print';
import {
AssociationFilter,
@ -28,6 +28,7 @@ import {
useCollectionRecord,
useDataSourceHeaders,
useFormActiveFields,
useRouterBasename,
useTableBlockContext,
} from '../..';
import { useAPIClient, useRequest } from '../../api-client';
@ -1594,9 +1595,7 @@ export function useLinkActionProps(componentProps?: any) {
const searchParams = componentPropsValue?.['params'] || [];
const openInNewWindow = fieldSchema?.['x-component-props']?.['openInNewWindow'];
const { parseURLAndParams } = useParseURLAndParams();
// see: https://stackoverflow.com/questions/50449423/accessing-basename-of-browserouter
const basenameOfCurrentRouter = useHref('/');
const basenameOfCurrentRouter = useRouterBasename();
return {
type: 'default',

View File

@ -23,6 +23,7 @@ import { useStyles as useAClStyles } from '../../../acl/style';
import { useRequest } from '../../../api-client';
import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider';
import { useAppSpin } from '../../../application/hooks/useAppSpin';
import { useRouterBasename } from '../../../application/hooks/useRouterBasename';
import { useDocumentTitle } from '../../../document-title';
import { useGlobalTheme } from '../../../global-theme';
import { Icon } from '../../../icon';
@ -47,6 +48,7 @@ export const Page = (props) => {
const { theme } = useGlobalTheme();
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
const { tabUid, name: pageUid } = useParams();
const basenameOfCurrentRouter = useRouterBasename();
// react18 tab 动画会卡顿,所以第一个 tab 时,动画禁用,后面的 tab 才启用
const [hasMounted, setHasMounted] = useState(false);
@ -112,7 +114,7 @@ export const Page = (props) => {
}}
onChange={(activeKey) => {
setLoading(true);
navigateToTab(activeKey, navigate);
navigateToTab({ activeKey, navigate, basename: basenameOfCurrentRouter });
setTimeout(() => {
setLoading(false);
}, 50);
@ -320,11 +322,28 @@ const PageContent = memo(
);
PageContent.displayName = 'PageContent';
export function navigateToTab(activeKey: string, navigate: NavigateFunction, pathname = window.location.pathname) {
export function navigateToTab({
activeKey,
navigate,
basename,
pathname = window.location.pathname,
}: {
activeKey: string;
navigate: NavigateFunction;
/** the router basename */
basename: string;
pathname?: string;
}) {
pathname = pathname.replace(basename, '');
if (pathname.endsWith('/')) {
pathname = pathname.slice(0, -1);
}
if (!pathname.startsWith('/')) {
pathname = `/${pathname}`;
}
if (isTabPage(pathname)) {
navigate(`${pathname.replace(/\/tabs\/[^/]+$/, `/tabs/${activeKey}`)}`, { replace: true });
} else {

View File

@ -84,7 +84,8 @@ const PopupParamsProvider: FC<Omit<PopupProps, 'hidden'>> = (props) => {
return <PopupParamsProviderContext.Provider value={value}>{props.children}</PopupParamsProviderContext.Provider>;
};
const PopupTabsPropsProvider: FC<{ params: PopupParams }> = ({ children, params }) => {
const PopupTabsPropsProvider: FC = ({ children }) => {
const { params } = useCurrentPopupContext();
const { changeTab } = usePagePopup();
const onChange = useCallback(
(key: string) => {
@ -99,7 +100,7 @@ const PopupTabsPropsProvider: FC<{ params: PopupParams }> = ({ children, params
}
return (
<TabsContextProvider activeKey={params.tab} onChange={onChange}>
<TabsContextProvider activeKey={params?.tab} onChange={onChange}>
{children}
</TabsContextProvider>
);
@ -166,7 +167,7 @@ const PagePopupsItemProvider: FC<{
>
{/* Pass the service of the block where the button is located down, to refresh the block's data when the popup is closed */}
<BlockRequestContext.Provider value={storedContext.service}>
<PopupTabsPropsProvider params={params}>
<PopupTabsPropsProvider>
<div style={{ display: 'none' }}>{children}</div>
</PopupTabsPropsProvider>
</BlockRequestContext.Provider>

View File

@ -167,7 +167,7 @@ describe('utils', () => {
expect(isTabPage('/admin/test/tabs/tabId/')).toBe(true);
});
it('navigateToTab', () => {
it('navigateToTab with basename "/"', () => {
const navigate1 = vi.fn();
const navigate2 = vi.fn();
const navigate3 = vi.fn();
@ -177,28 +177,178 @@ describe('utils', () => {
const navigate7 = vi.fn();
const navigate8 = vi.fn();
navigateToTab('tabId', navigate1, '/admin/test');
navigateToTab({ activeKey: 'tabId', navigate: navigate1, pathname: '/admin/test', basename: '/' });
expect(navigate1).toBeCalledWith('/admin/test/tabs/tabId', { replace: true });
navigateToTab('tabId', navigate2, '/admin/test/');
navigateToTab({ activeKey: 'tabId', navigate: navigate2, pathname: '/admin/test/', basename: '/' });
expect(navigate2).toBeCalledWith('/admin/test/tabs/tabId', { replace: true });
navigateToTab('tabId', navigate3, '/admin/test/tabs/oldTabId');
navigateToTab({ activeKey: 'tabId', navigate: navigate3, pathname: '/admin/test/tabs/oldTabId', basename: '/' });
expect(navigate3).toBeCalledWith('/admin/test/tabs/tabId', { replace: true });
navigateToTab('tabId', navigate4, '/admin/test/tabs/oldTabId/');
navigateToTab({ activeKey: 'tabId', navigate: navigate4, pathname: '/admin/test/tabs/oldTabId/', basename: '/' });
expect(navigate4).toBeCalledWith('/admin/test/tabs/tabId', { replace: true });
navigateToTab('tabId', navigate5, '/admin/test/tabs/tab1/pages/pageId/tabs/tab2');
navigateToTab({
activeKey: 'tabId',
navigate: navigate5,
pathname: '/admin/test/tabs/tab1/pages/pageId/tabs/tab2',
basename: '/',
});
expect(navigate5).toBeCalledWith('/admin/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab('tabId', navigate6, '/admin/test/tabs/tab1/pages/pageId/tabs/tab2/');
navigateToTab({
activeKey: 'tabId',
navigate: navigate6,
pathname: '/admin/test/tabs/tab1/pages/pageId/tabs/tab2/',
basename: '/',
});
expect(navigate6).toBeCalledWith('/admin/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab('tabId', navigate7, '/admin/test/tabs/tab1/pages/pageId');
navigateToTab({
activeKey: 'tabId',
navigate: navigate7,
pathname: '/admin/test/tabs/tab1/pages/pageId',
basename: '/',
});
expect(navigate7).toBeCalledWith('/admin/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab('tabId', navigate8, '/admin/test/tabs/tab1/pages/pageId/');
navigateToTab({
activeKey: 'tabId',
navigate: navigate8,
pathname: '/admin/test/tabs/tab1/pages/pageId/',
basename: '/',
});
expect(navigate8).toBeCalledWith('/admin/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
});
it('navigateToTab with basename "/apps/appId"', () => {
const navigate1 = vi.fn();
const navigate2 = vi.fn();
const navigate3 = vi.fn();
const navigate4 = vi.fn();
const navigate5 = vi.fn();
const navigate6 = vi.fn();
const navigate7 = vi.fn();
const navigate8 = vi.fn();
navigateToTab({ activeKey: 'tabId', navigate: navigate1, pathname: '/apps/appId/test', basename: '/apps/appId' });
expect(navigate1).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({ activeKey: 'tabId', navigate: navigate2, pathname: '/apps/appId/test/', basename: '/apps/appId' });
expect(navigate2).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate3,
pathname: '/apps/appId/test/tabs/oldTabId',
basename: '/apps/appId',
});
expect(navigate3).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate4,
pathname: '/apps/appId/test/tabs/oldTabId/',
basename: '/apps/appId',
});
expect(navigate4).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate5,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId/tabs/tab2',
basename: '/apps/appId',
});
expect(navigate5).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate6,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId/tabs/tab2/',
basename: '/apps/appId',
});
expect(navigate6).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate7,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId',
basename: '/apps/appId',
});
expect(navigate7).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate8,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId/',
basename: '/apps/appId',
});
expect(navigate8).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
});
it('navigateToTab with basename "/apps/appId/"', () => {
const navigate1 = vi.fn();
const navigate2 = vi.fn();
const navigate3 = vi.fn();
const navigate4 = vi.fn();
const navigate5 = vi.fn();
const navigate6 = vi.fn();
const navigate7 = vi.fn();
const navigate8 = vi.fn();
navigateToTab({ activeKey: 'tabId', navigate: navigate1, pathname: '/apps/appId/test', basename: '/apps/appId/' });
expect(navigate1).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({ activeKey: 'tabId', navigate: navigate2, pathname: '/apps/appId/test/', basename: '/apps/appId/' });
expect(navigate2).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate3,
pathname: '/apps/appId/test/tabs/oldTabId',
basename: '/apps/appId/',
});
expect(navigate3).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate4,
pathname: '/apps/appId/test/tabs/oldTabId/',
basename: '/apps/appId/',
});
expect(navigate4).toBeCalledWith('/test/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate5,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId/tabs/tab2',
basename: '/apps/appId/',
});
expect(navigate5).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate6,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId/tabs/tab2/',
basename: '/apps/appId/',
});
expect(navigate6).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate7,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId',
basename: '/apps/appId/',
});
expect(navigate7).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
navigateToTab({
activeKey: 'tabId',
navigate: navigate8,
pathname: '/apps/appId/test/tabs/tab1/pages/pageId/',
basename: '/apps/appId/',
});
expect(navigate8).toBeCalledWith('/test/tabs/tab1/pages/pageId/tabs/tabId', { replace: true });
});
});