Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ In order to display a list of songs for the selected artist, `<SongList>` should
import { List, Datagrid, TextField, useRecordContext } from 'react-admin';
import { useParams } from 'react-router-dom';
import { Button } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';

export const SongList = () => {
const { id } = useParams();
Expand Down
82 changes: 81 additions & 1 deletion packages/ra-core/src/core/Resource.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { Basic } from './Resource.stories';
import {
Basic,
OnlyList,
WithAllDialogs,
WithCreateDialog,
WithShowDialog,
} from './Resource.stories';

describe('<Resource>', () => {
it('renders resource routes by default', async () => {
Expand All @@ -23,4 +29,78 @@ describe('<Resource>', () => {
navigate('/posts/customroute');
await screen.findByText('PostCustomRoute');
});

it('always renders the list if only a list view is present', async () => {
let navigate;
render(
<OnlyList
navigateCallback={n => {
navigate = n;
}}
/>
);
navigate('/posts');
await screen.findByText('PostList');
navigate('/posts/123');
await screen.findByText('PostList');
navigate('/posts/123/show');
await screen.findByText('PostList');
navigate('/posts/create');
await screen.findByText('PostList');
navigate('/posts/customroute');
await screen.findByText('PostList');
});

it('allows to render all dialogs views declared in the list view', async () => {
let navigate;
render(
<WithAllDialogs
navigateCallback={n => {
navigate = n;
}}
/>
);
navigate('/posts');
await screen.findByText('PostList');
navigate('/posts/123');
await screen.findByText('PostEdit');
navigate('/posts/123/show');
await screen.findByText('PostShow');
navigate('/posts/create');
await screen.findByText('PostCreate');
});

it('allows to render a create dialog declared in the list even if there is an edit view', async () => {
let navigate;
render(
<WithCreateDialog
navigateCallback={n => {
navigate = n;
}}
/>
);
navigate('/posts');
await screen.findByText('PostList');
navigate('/posts/123');
await screen.findByText('PostEdit');
navigate('/posts/create');
await screen.findByText('PostCreate');
});

it('allows to render a show dialog declared in the list even if there is an edit view', async () => {
let navigate;
render(
<WithShowDialog
navigateCallback={n => {
navigate = n;
}}
/>
);
navigate('/posts');
await screen.findByText('PostList');
navigate('/posts/123');
await screen.findByText('PostEdit');
navigate('/posts/123/show');
await screen.findByText('PostShow');
});
});
187 changes: 182 additions & 5 deletions packages/ra-core/src/core/Resource.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { NavigateFunction, Route } from 'react-router';
import { Link } from 'react-router-dom';
import { NavigateFunction, Route, Routes } from 'react-router';
import { Link, useParams, useLocation } from 'react-router-dom';
import { TestMemoryRouter } from '../routing';
import { Resource } from './Resource';
import { CoreAdmin } from './CoreAdmin';
import { Browser } from '../storybook/FakeBrowser';

export default {
title: 'ra-core/core/Resource',
Expand Down Expand Up @@ -61,10 +62,186 @@ export const Basic = ({
navigateCallback?: (n: NavigateFunction) => void;
}) => (
<TestMemoryRouter navigateCallback={navigateCallback}>
<CoreAdmin loading={Loading}>
<Resource {...resource} />
</CoreAdmin>
<Browser>
<CoreAdmin loading={Loading}>
<Resource {...resource} />
</CoreAdmin>
</Browser>
</TestMemoryRouter>
);

const Loading = () => <div>Loading...</div>;

export const OnlyList = ({
navigateCallback,
}: {
navigateCallback?: (n: NavigateFunction) => void;
}) => (
<TestMemoryRouter navigateCallback={navigateCallback}>
<Browser>
<CoreAdmin loading={Loading}>
<Resource name="posts" list={PostList} />
</CoreAdmin>
</Browser>
</TestMemoryRouter>
);

export const WithAllDialogs = ({
navigateCallback,
}: {
navigateCallback?: (n: NavigateFunction) => void;
}) => (
<TestMemoryRouter navigateCallback={navigateCallback}>
<Browser>
<CoreAdmin loading={Loading}>
<Resource name="posts" list={PostListWithAllDialogs} />
</CoreAdmin>
</Browser>
</TestMemoryRouter>
);

const PostListWithAllDialogs = () => (
<div>
<div>PostList</div>
<Link to="/posts/create">create</Link> <Link to="/posts/123">edit</Link>{' '}
<Link to="/posts/123/show">show</Link>
<PostEditDialog />
<PostCreateDialog />
<PostShowDialog />
</div>
);

const PostCreateDialog = () => (
<Routes>
<Route
path="create/*"
element={
<div
style={{
border: '1px solid black',
margin: '1em',
padding: '1em',
maxWidth: '400px',
}}
>
<div>
<Link to="/posts">close</Link>
</div>
<div>PostCreate</div>
</div>
}
/>
</Routes>
);

const PostEditDialog = () => {
return (
<Routes>
<Route path=":id/*" element={<PostEditDialogView />} />
</Routes>
);
};

const PostEditDialogView = () => {
const params = useParams<'id'>();
const location = useLocation();
const isMatch =
params.id &&
params.id !== 'create' &&
location.pathname.indexOf('/show') === -1;
return isMatch ? (
<div
style={{
border: '1px solid black',
margin: '1em',
padding: '1em',
maxWidth: '400px',
}}
>
<div>
<Link to="/posts">close</Link>
</div>
<div>PostEdit</div>
</div>
) : null;
};

const PostShowDialog = () => {
return (
<Routes>
<Route path=":id/show/*" element={<PostShowDialogView />} />
</Routes>
);
};

const PostShowDialogView = () => {
const params = useParams<'id'>();
const isMatch = params.id && params.id !== 'create';
return isMatch ? (
<div
style={{
border: '1px solid black',
margin: '1em',
padding: '1em',
maxWidth: '400px',
}}
>
<div>
<Link to="/posts">close</Link>
</div>
<div>PostShow</div>
</div>
) : null;
};

export const WithCreateDialog = ({
navigateCallback,
}: {
navigateCallback?: (n: NavigateFunction) => void;
}) => (
<TestMemoryRouter navigateCallback={navigateCallback}>
<Browser>
<CoreAdmin loading={Loading}>
<Resource
name="posts"
list={PostListWithCreateDialog}
edit={PostEdit}
/>
</CoreAdmin>
</Browser>
</TestMemoryRouter>
);

const PostListWithCreateDialog = () => (
<div>
<div>PostList</div>
<Link to="/posts/create">create</Link> <Link to="/posts/123">edit</Link>{' '}
<PostCreateDialog />
</div>
);

export const WithShowDialog = ({
navigateCallback,
}: {
navigateCallback?: (n: NavigateFunction) => void;
}) => (
<TestMemoryRouter navigateCallback={navigateCallback}>
<Browser>
<CoreAdmin loading={Loading}>
<Resource
name="posts"
list={PostListWithShowDialog}
edit={PostEdit}
/>
</CoreAdmin>
</Browser>
</TestMemoryRouter>
);

const PostListWithShowDialog = () => (
<div>
<div>PostList</div>
<Link to="/posts/123">edit</Link> <Link to="/posts/123/show">show</Link>
<PostShowDialog />
</div>
);
21 changes: 18 additions & 3 deletions packages/ra-core/src/core/Resource.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import * as React from 'react';
import { ComponentType, ReactElement, isValidElement } from 'react';
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useLocation, matchPath } from 'react-router-dom';
import { isValidElementType } from 'react-is';

import { ResourceProps } from '../types';
import { ResourceContextProvider } from './ResourceContextProvider';
import { RestoreScrollPosition } from '../routing/RestoreScrollPosition';
import { useSplatPathBase } from '../routing';

export const Resource = (props: ResourceProps) => {
const { create, edit, list, name, show } = props;
const location = useLocation();
const splatPathBase = useSplatPathBase();
const matchCreate = matchPath(
`${splatPathBase}/create/*`,
location.pathname
);
const matchShow = matchPath(
`${splatPathBase}/:id/show/*`,
location.pathname
);

return (
<ResourceContextProvider value={name}>
<Routes>
{create && (
<Route path="create/*" element={getElement(create)} />
)}
{show && <Route path=":id/show/*" element={getElement(show)} />}
{edit && <Route path=":id/*" element={getElement(edit)} />}
{!matchCreate && show && (
<Route path=":id/show/*" element={getElement(show)} />
)}
{!matchCreate && !matchShow && edit && (
<Route path=":id/*" element={getElement(edit)} />
)}
{list && (
<Route
path="/*"
Expand Down
Loading
Loading