Skip to content

Commit f47dd1d

Browse files
committed
Fix limitation preventing to declare create and show routes from the list view
1 parent 1768f81 commit f47dd1d

File tree

5 files changed

+317
-13
lines changed

5 files changed

+317
-13
lines changed

docs/Resource.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ In order to display a list of songs for the selected artist, `<SongList>` should
359359
import { List, Datagrid, TextField, useRecordContext } from 'react-admin';
360360
import { useParams } from 'react-router-dom';
361361
import { Button } from '@mui/material';
362+
import EditIcon from '@mui/icons-material/Edit';
362363

363364
export const SongList = () => {
364365
const { id } = useParams();

packages/ra-core/src/core/Resource.spec.tsx

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as React from 'react';
22
import { render, screen } from '@testing-library/react';
3-
import { Basic } from './Resource.stories';
3+
import {
4+
Basic,
5+
OnlyList,
6+
WithAllDialogs,
7+
WithCreateDialog,
8+
WithShowDialog,
9+
} from './Resource.stories';
410

511
describe('<Resource>', () => {
612
it('renders resource routes by default', async () => {
@@ -23,4 +29,78 @@ describe('<Resource>', () => {
2329
navigate('/posts/customroute');
2430
await screen.findByText('PostCustomRoute');
2531
});
32+
33+
it('always renders the list if only a list view is present', async () => {
34+
let navigate;
35+
render(
36+
<OnlyList
37+
navigateCallback={n => {
38+
navigate = n;
39+
}}
40+
/>
41+
);
42+
navigate('/posts');
43+
await screen.findByText('PostList');
44+
navigate('/posts/123');
45+
await screen.findByText('PostList');
46+
navigate('/posts/123/show');
47+
await screen.findByText('PostList');
48+
navigate('/posts/create');
49+
await screen.findByText('PostList');
50+
navigate('/posts/customroute');
51+
await screen.findByText('PostList');
52+
});
53+
54+
it('allows to render all dialogs views declared in the list view', async () => {
55+
let navigate;
56+
render(
57+
<WithAllDialogs
58+
navigateCallback={n => {
59+
navigate = n;
60+
}}
61+
/>
62+
);
63+
navigate('/posts');
64+
await screen.findByText('PostList');
65+
navigate('/posts/123');
66+
await screen.findByText('PostEdit');
67+
navigate('/posts/123/show');
68+
await screen.findByText('PostShow');
69+
navigate('/posts/create');
70+
await screen.findByText('PostCreate');
71+
});
72+
73+
it('allows to render a create dialog declared in the list even if there is an edit view', async () => {
74+
let navigate;
75+
render(
76+
<WithCreateDialog
77+
navigateCallback={n => {
78+
navigate = n;
79+
}}
80+
/>
81+
);
82+
navigate('/posts');
83+
await screen.findByText('PostList');
84+
navigate('/posts/123');
85+
await screen.findByText('PostEdit');
86+
navigate('/posts/create');
87+
await screen.findByText('PostCreate');
88+
});
89+
90+
it('allows to render a show dialog declared in the list even if there is an edit view', async () => {
91+
let navigate;
92+
render(
93+
<WithShowDialog
94+
navigateCallback={n => {
95+
navigate = n;
96+
}}
97+
/>
98+
);
99+
navigate('/posts');
100+
await screen.findByText('PostList');
101+
navigate('/posts/123');
102+
await screen.findByText('PostEdit');
103+
navigate('/posts/123/show');
104+
await screen.findByText('PostShow');
105+
});
26106
});

packages/ra-core/src/core/Resource.stories.tsx

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as React from 'react';
2-
import { NavigateFunction, Route } from 'react-router';
3-
import { Link } from 'react-router-dom';
2+
import { NavigateFunction, Route, Routes } from 'react-router';
3+
import { Link, useParams, useLocation } from 'react-router-dom';
44
import { TestMemoryRouter } from '../routing';
55
import { Resource } from './Resource';
66
import { CoreAdmin } from './CoreAdmin';
7+
import { Browser } from '../storybook/FakeBrowser';
78

89
export default {
910
title: 'ra-core/core/Resource',
@@ -61,10 +62,186 @@ export const Basic = ({
6162
navigateCallback?: (n: NavigateFunction) => void;
6263
}) => (
6364
<TestMemoryRouter navigateCallback={navigateCallback}>
64-
<CoreAdmin loading={Loading}>
65-
<Resource {...resource} />
66-
</CoreAdmin>
65+
<Browser>
66+
<CoreAdmin loading={Loading}>
67+
<Resource {...resource} />
68+
</CoreAdmin>
69+
</Browser>
6770
</TestMemoryRouter>
6871
);
6972

7073
const Loading = () => <div>Loading...</div>;
74+
75+
export const OnlyList = ({
76+
navigateCallback,
77+
}: {
78+
navigateCallback?: (n: NavigateFunction) => void;
79+
}) => (
80+
<TestMemoryRouter navigateCallback={navigateCallback}>
81+
<Browser>
82+
<CoreAdmin loading={Loading}>
83+
<Resource name="posts" list={PostList} />
84+
</CoreAdmin>
85+
</Browser>
86+
</TestMemoryRouter>
87+
);
88+
89+
export const WithAllDialogs = ({
90+
navigateCallback,
91+
}: {
92+
navigateCallback?: (n: NavigateFunction) => void;
93+
}) => (
94+
<TestMemoryRouter navigateCallback={navigateCallback}>
95+
<Browser>
96+
<CoreAdmin loading={Loading}>
97+
<Resource name="posts" list={PostListWithAllDialogs} />
98+
</CoreAdmin>
99+
</Browser>
100+
</TestMemoryRouter>
101+
);
102+
103+
const PostListWithAllDialogs = () => (
104+
<div>
105+
<div>PostList</div>
106+
<Link to="/posts/create">create</Link> <Link to="/posts/123">edit</Link>{' '}
107+
<Link to="/posts/123/show">show</Link>
108+
<PostEditDialog />
109+
<PostCreateDialog />
110+
<PostShowDialog />
111+
</div>
112+
);
113+
114+
const PostCreateDialog = () => (
115+
<Routes>
116+
<Route
117+
path="create/*"
118+
element={
119+
<div
120+
style={{
121+
border: '1px solid black',
122+
margin: '1em',
123+
padding: '1em',
124+
maxWidth: '400px',
125+
}}
126+
>
127+
<div>
128+
<Link to="/posts">close</Link>
129+
</div>
130+
<div>PostCreate</div>
131+
</div>
132+
}
133+
/>
134+
</Routes>
135+
);
136+
137+
const PostEditDialog = () => {
138+
return (
139+
<Routes>
140+
<Route path=":id/*" element={<PostEditDialogView />} />
141+
</Routes>
142+
);
143+
};
144+
145+
const PostEditDialogView = () => {
146+
const params = useParams<'id'>();
147+
const location = useLocation();
148+
const isMatch =
149+
params.id &&
150+
params.id !== 'create' &&
151+
location.pathname.indexOf('/show') === -1;
152+
return isMatch ? (
153+
<div
154+
style={{
155+
border: '1px solid black',
156+
margin: '1em',
157+
padding: '1em',
158+
maxWidth: '400px',
159+
}}
160+
>
161+
<div>
162+
<Link to="/posts">close</Link>
163+
</div>
164+
<div>PostEdit</div>
165+
</div>
166+
) : null;
167+
};
168+
169+
const PostShowDialog = () => {
170+
return (
171+
<Routes>
172+
<Route path=":id/show/*" element={<PostShowDialogView />} />
173+
</Routes>
174+
);
175+
};
176+
177+
const PostShowDialogView = () => {
178+
const params = useParams<'id'>();
179+
const isMatch = params.id && params.id !== 'create';
180+
return isMatch ? (
181+
<div
182+
style={{
183+
border: '1px solid black',
184+
margin: '1em',
185+
padding: '1em',
186+
maxWidth: '400px',
187+
}}
188+
>
189+
<div>
190+
<Link to="/posts">close</Link>
191+
</div>
192+
<div>PostShow</div>
193+
</div>
194+
) : null;
195+
};
196+
197+
export const WithCreateDialog = ({
198+
navigateCallback,
199+
}: {
200+
navigateCallback?: (n: NavigateFunction) => void;
201+
}) => (
202+
<TestMemoryRouter navigateCallback={navigateCallback}>
203+
<Browser>
204+
<CoreAdmin loading={Loading}>
205+
<Resource
206+
name="posts"
207+
list={PostListWithCreateDialog}
208+
edit={PostEdit}
209+
/>
210+
</CoreAdmin>
211+
</Browser>
212+
</TestMemoryRouter>
213+
);
214+
215+
const PostListWithCreateDialog = () => (
216+
<div>
217+
<div>PostList</div>
218+
<Link to="/posts/create">create</Link> <Link to="/posts/123">edit</Link>{' '}
219+
<PostCreateDialog />
220+
</div>
221+
);
222+
223+
export const WithShowDialog = ({
224+
navigateCallback,
225+
}: {
226+
navigateCallback?: (n: NavigateFunction) => void;
227+
}) => (
228+
<TestMemoryRouter navigateCallback={navigateCallback}>
229+
<Browser>
230+
<CoreAdmin loading={Loading}>
231+
<Resource
232+
name="posts"
233+
list={PostListWithShowDialog}
234+
edit={PostEdit}
235+
/>
236+
</CoreAdmin>
237+
</Browser>
238+
</TestMemoryRouter>
239+
);
240+
241+
const PostListWithShowDialog = () => (
242+
<div>
243+
<div>PostList</div>
244+
<Link to="/posts/123">edit</Link> <Link to="/posts/123/show">show</Link>
245+
<PostShowDialog />
246+
</div>
247+
);

packages/ra-core/src/core/Resource.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
import * as React from 'react';
22
import { ComponentType, ReactElement, isValidElement } from 'react';
3-
import { Route, Routes } from 'react-router-dom';
3+
import { Route, Routes, useLocation, matchPath } from 'react-router-dom';
44
import { isValidElementType } from 'react-is';
55

66
import { ResourceProps } from '../types';
77
import { ResourceContextProvider } from './ResourceContextProvider';
88
import { RestoreScrollPosition } from '../routing/RestoreScrollPosition';
9+
import { useSplatPathBase } from '../routing';
910

1011
export const Resource = (props: ResourceProps) => {
1112
const { create, edit, list, name, show } = props;
13+
const location = useLocation();
14+
const splatPathBase = useSplatPathBase();
15+
const matchCreate = matchPath(
16+
`${splatPathBase}/create/*`,
17+
location.pathname
18+
);
19+
const matchShow = matchPath(
20+
`${splatPathBase}/:id/show/*`,
21+
location.pathname
22+
);
1223

1324
return (
1425
<ResourceContextProvider value={name}>
1526
<Routes>
1627
{create && (
1728
<Route path="create/*" element={getElement(create)} />
1829
)}
19-
{show && <Route path=":id/show/*" element={getElement(show)} />}
20-
{edit && <Route path=":id/*" element={getElement(edit)} />}
30+
{!matchCreate && show && (
31+
<Route path=":id/show/*" element={getElement(show)} />
32+
)}
33+
{!matchCreate && !matchShow && edit && (
34+
<Route path=":id/*" element={getElement(edit)} />
35+
)}
2136
{list && (
2237
<Route
2338
path="/*"

0 commit comments

Comments
 (0)