From 67b2001d6c7673768994c405e659648a35bc6eaa Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Fri, 24 Jan 2025 11:07:47 +0100 Subject: [PATCH 1/4] Fix TabbedForm and TabbedShowLayout with react-router v7 --- packages/ra-core/src/routing/index.ts | 1 + .../ra-core/src/routing/useSplatPathBase.ts | 24 +++++++++++++++++++ packages/ra-ui-materialui/src/detail/Tab.tsx | 9 +++++-- .../src/form/FormTabHeader.tsx | 12 ++++++---- .../src/form/TabbedFormView.tsx | 14 ++++------- 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 packages/ra-core/src/routing/useSplatPathBase.ts diff --git a/packages/ra-core/src/routing/index.ts b/packages/ra-core/src/routing/index.ts index 20ffa147f00..b214d3150cb 100644 --- a/packages/ra-core/src/routing/index.ts +++ b/packages/ra-core/src/routing/index.ts @@ -11,3 +11,4 @@ export * from './useScrollToTop'; export * from './useRestoreScrollPosition'; export * from './types'; export * from './TestMemoryRouter'; +export * from './useSplatPathBase'; diff --git a/packages/ra-core/src/routing/useSplatPathBase.ts b/packages/ra-core/src/routing/useSplatPathBase.ts new file mode 100644 index 00000000000..2978e1626d6 --- /dev/null +++ b/packages/ra-core/src/routing/useSplatPathBase.ts @@ -0,0 +1,24 @@ +import { useLocation, useParams } from 'react-router-dom'; + +/** + * Utility hook to get the base path of a splat path. + * Compatible both with react-router v6 and v7. + * + * Example: + * If a splat path is defined as `/posts/:id/show/*`, + * and the current location is `/posts/12/show/3`, + * this hook will return `/posts/12/show`. + * + * Solution inspired by + * https://github.com/remix-run/react-router/issues/11052#issuecomment-1828470203 + */ +export const useSplatPathBase = () => { + const location = useLocation(); + const params = useParams(); + const splatPathRelativePart = params['*']; + const splatPathBase = location.pathname.replace( + new RegExp(`/${splatPathRelativePart}$`), + '' + ); + return splatPathBase; +}; diff --git a/packages/ra-ui-materialui/src/detail/Tab.tsx b/packages/ra-ui-materialui/src/detail/Tab.tsx index 6377d514198..ad5e9e938e7 100644 --- a/packages/ra-ui-materialui/src/detail/Tab.tsx +++ b/packages/ra-ui-materialui/src/detail/Tab.tsx @@ -8,7 +8,7 @@ import { styled, } from '@mui/material'; import { ResponsiveStyleValue } from '@mui/system'; -import { useTranslate, RaRecord } from 'ra-core'; +import { useTranslate, RaRecord, useSplatPathBase } from 'ra-core'; import clsx from 'clsx'; import { Labeled } from '../Labeled'; @@ -76,9 +76,14 @@ export const Tab = ({ }: TabProps) => { const translate = useTranslate(); const location = useLocation(); + const splatPathBase = useSplatPathBase(); + const newPathName = + value == null || value === '' + ? splatPathBase + : `${splatPathBase}/${value}`; const propsForLink = { component: Link, - to: { ...location, pathname: value }, + to: { ...location, pathname: newPathName }, }; const renderHeader = () => { diff --git a/packages/ra-ui-materialui/src/form/FormTabHeader.tsx b/packages/ra-ui-materialui/src/form/FormTabHeader.tsx index 50eb01046a4..f7706d83856 100644 --- a/packages/ra-ui-materialui/src/form/FormTabHeader.tsx +++ b/packages/ra-ui-materialui/src/form/FormTabHeader.tsx @@ -3,7 +3,7 @@ import { ReactElement, ReactNode } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Tab as MuiTab, TabProps as MuiTabProps } from '@mui/material'; import clsx from 'clsx'; -import { useTranslate, useFormGroup } from 'ra-core'; +import { useTranslate, useFormGroup, useSplatPathBase } from 'ra-core'; import { TabbedFormClasses } from './TabbedFormView'; @@ -18,12 +18,16 @@ export const FormTabHeader = ({ ...rest }: FormTabHeaderProps): ReactElement => { const translate = useTranslate(); - const location = useLocation(); const formGroup = useFormGroup(value.toString()); - + const location = useLocation(); + const splatPathBase = useSplatPathBase(); + const newPathName = + value == null || value === '' + ? splatPathBase + : `${splatPathBase}/${value}`; const propsForLink = { component: Link, - to: { ...location, pathname: value }, + to: { ...location, pathname: newPathName }, }; let tabLabel = diff --git a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx index dd17c36abc4..47d02b70e12 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx @@ -10,16 +10,10 @@ import { useState, } from 'react'; import clsx from 'clsx'; -import { - Routes, - Route, - matchPath, - useResolvedPath, - useLocation, -} from 'react-router-dom'; +import { Routes, Route, matchPath, useLocation } from 'react-router-dom'; import { CardContent, Divider, SxProps } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { useResourceContext } from 'ra-core'; +import { useResourceContext, useSplatPathBase } from 'ra-core'; import { Toolbar } from './Toolbar'; import { TabbedFormTabs, getTabbedFormTabFullPath } from './TabbedFormTabs'; @@ -35,9 +29,9 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { ...rest } = props; const location = useLocation(); - const resolvedPath = useResolvedPath(''); const resource = useResourceContext(props); const [tabValue, setTabValue] = useState(0); + const splatPathBase = useSplatPathBase(); const handleTabChange = (event: ChangeEvent<{}>, value: any): void => { if (!syncWithLocation) { @@ -82,7 +76,7 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { const tabPath = getTabbedFormTabFullPath(tab, index); const hidden = syncWithLocation ? !matchPath( - `${resolvedPath.pathname}/${tabPath}`, + `${splatPathBase}/${tabPath}`, // The current location might have encoded segments (e.g. the record id) but resolvedPath.pathname doesn't // and the match would fail. getDecodedPathname(location.pathname) From 0d16fa0d58c40c27a7200d3df0ca3a2e383d2701 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Fri, 24 Jan 2025 11:39:39 +0100 Subject: [PATCH 2/4] fix encoded paths and fix test --- .../ra-ui-materialui/src/form/TabbedForm.spec.tsx | 3 ++- .../ra-ui-materialui/src/form/TabbedFormView.tsx | 15 ++++++--------- test-setup.js | 1 + 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx b/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx index 360762e3bf2..50efc7b9bdd 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx @@ -46,7 +46,8 @@ describe('', () => { const tabs = await screen.findAllByRole('tab'); expect(tabs.length).toEqual(2); - await screen.findByLabelText('Title'); + const titleInput = await screen.findByLabelText('Title'); + expect(titleInput).toBeVisible(); }); it('should set the style of an inactive Tab button with errors', async () => { diff --git a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx index 47d02b70e12..db31aa2a020 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx @@ -74,12 +74,15 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { return null; } const tabPath = getTabbedFormTabFullPath(tab, index); + console.log( + 'matchPath', + `${splatPathBase}/${tabPath}`, + location.pathname + ); const hidden = syncWithLocation ? !matchPath( `${splatPathBase}/${tabPath}`, - // The current location might have encoded segments (e.g. the record id) but resolvedPath.pathname doesn't - // and the match would fail. - getDecodedPathname(location.pathname) + location.pathname ) : tabValue !== index; @@ -98,12 +101,6 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { ); }; -/** - * Returns the pathname with each segment decoded - */ -const getDecodedPathname = (pathname: string) => - pathname.split('/').map(decodeURIComponent).join('/'); - const DefaultTabs = ; const DefaultComponent = ({ children }) => ( {children} diff --git a/test-setup.js b/test-setup.js index 750b1267520..65d62440e06 100644 --- a/test-setup.js +++ b/test-setup.js @@ -3,6 +3,7 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; +import '@testing-library/jest-dom/jest-globals'; // Ignore warnings about act() // See https://github.com/testing-library/react-testing-library/issues/281, From a0684b3d9a0de08daef9b51f0db1e5d3ea75439f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Fri, 24 Jan 2025 11:42:14 +0100 Subject: [PATCH 3/4] remove console.log --- packages/ra-ui-materialui/src/form/TabbedFormView.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx index db31aa2a020..5d156c91adb 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormView.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedFormView.tsx @@ -74,11 +74,6 @@ export const TabbedFormView = (props: TabbedFormViewProps): ReactElement => { return null; } const tabPath = getTabbedFormTabFullPath(tab, index); - console.log( - 'matchPath', - `${splatPathBase}/${tabPath}`, - location.pathname - ); const hidden = syncWithLocation ? !matchPath( `${splatPathBase}/${tabPath}`, From 92a6801b5f2e07ceadd06218b25bb77276375184 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Fri, 24 Jan 2025 11:50:28 +0100 Subject: [PATCH 4/4] fix stories --- .../src/detail/TabbedShowLayout.stories.tsx | 252 +++++++++--------- 1 file changed, 130 insertions(+), 122 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx index 477b999b3db..0c335cb460c 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx @@ -1,19 +1,23 @@ import * as React from 'react'; import { Divider as MuiDivider } from '@mui/material'; import { - RecordContextProvider, - ResourceContext, useRecordContext, WithRecord, TestMemoryRouter, + RaRecord, + testDataProvider, + ResourceContextProvider, } from 'ra-core'; import { Labeled } from '../Labeled'; import { TextField, NumberField } from '../field'; import { TabbedShowLayout } from './TabbedShowLayout'; +import { AdminContext } from '../AdminContext'; +import { Route, Routes } from 'react-router'; +import { Show } from './Show'; export default { title: 'ra-ui-materialui/detail/TabbedShowLayout' }; -const record = { +const data = { id: 1, title: 'War and Peace', author: 'Leo Tolstoy', @@ -22,47 +26,73 @@ const record = { year: 1869, }; -export const Basic = () => ( - - - - - - - - - - - - - - - - +const Wrapper = ({ + children, + record = data, +}: { + children: React.ReactNode; + record?: RaRecord; +}) => ( + + options?._ ?? x, + changeLocale: () => Promise.resolve(), + getLocale: () => 'en', + }} + dataProvider={testDataProvider({ + // @ts-ignore + getOne: () => Promise.resolve({ data: record }), + })} + defaultTheme="light" + > + + + {children}} + /> + + + ); +export const Basic = () => ( + + + + + + + + + + + + + +); + export const Count = () => ( - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); const BookTitle = () => { @@ -71,98 +101,76 @@ const BookTitle = () => { }; export const CustomChild = () => ( - - - - - - - {record.author}} - /> - - - - - + + + + + {record.author}} /> + + + ); export const CustomLabel = () => ( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + ); export const Spacing = () => ( - - - - - - - - - - - - - - - + + + + + + + + + + + ); export const Divider = () => ( - - - - }> - - - - - - - - - - - + + }> + + + + + + + + + ); export const SX = () => ( - - - - - - - - - - - - - - - + + + + + + + + + + + );