Skip to content

Commit 1dbc38e

Browse files
committed
Replace loading by authLoading and add loading / error props to Create, Edit, Show, List components.
1 parent 9fafcbd commit 1dbc38e

30 files changed

+1119
-133
lines changed

packages/ra-core/src/controller/create/CreateBase.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export const WithAuthProviderNoAccessControl = ({
118118
<CoreAdminContext authProvider={authProvider} dataProvider={dataProvider}>
119119
<CreateBase
120120
{...defaultProps}
121-
loading={<div>Authentication loading...</div>}
121+
authLoading={<div>Authentication loading...</div>}
122122
>
123123
<Child />
124124
</CreateBase>
@@ -141,7 +141,7 @@ export const AccessControl = ({
141141
<CoreAdminContext authProvider={authProvider} dataProvider={dataProvider}>
142142
<CreateBase
143143
{...defaultProps}
144-
loading={<div>Authentication loading...</div>}
144+
authLoading={<div>Authentication loading...</div>}
145145
>
146146
<Child />
147147
</CreateBase>

packages/ra-core/src/controller/create/CreateBase.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export const CreateBase = <
4646
>({
4747
children,
4848
render,
49-
loading = null,
49+
loading,
50+
authLoading = loading,
5051
...props
5152
}: CreateBaseProps<RecordType, ResultRecordType, MutationOptionsError>) => {
5253
const controllerProps = useCreateController<
@@ -60,21 +61,27 @@ export const CreateBase = <
6061
action: 'create',
6162
});
6263

63-
if (isAuthPending && !props.disableAuthentication) {
64-
return loading;
65-
}
66-
6764
if (!render && !children) {
6865
throw new Error(
6966
'<CreateBase> requires either a `render` prop or `children` prop'
7067
);
7168
}
7269

70+
const showAuthLoading =
71+
isAuthPending &&
72+
!props.disableAuthentication &&
73+
authLoading !== false &&
74+
authLoading !== undefined;
75+
7376
return (
7477
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
7578
<OptionalResourceContextProvider value={props.resource}>
7679
<CreateContextProvider value={controllerProps}>
77-
{render ? render(controllerProps) : children}
80+
{showAuthLoading
81+
? authLoading
82+
: render
83+
? render(controllerProps)
84+
: children}
7885
</CreateContextProvider>
7986
</OptionalResourceContextProvider>
8087
);
@@ -91,5 +98,9 @@ export interface CreateBaseProps<
9198
> {
9299
children?: ReactNode;
93100
render?: (props: CreateControllerResult<RecordType>) => ReactNode;
101+
authLoading?: ReactNode;
102+
/**
103+
* @deprecated use authLoading instead
104+
*/
94105
loading?: ReactNode;
95106
}

packages/ra-core/src/controller/edit/EditBase.spec.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import { testDataProvider } from '../../dataProvider';
66
import {
77
AccessControl,
88
DefaultTitle,
9+
FetchError,
10+
Loading,
911
NoAuthProvider,
1012
Offline,
13+
RedirectOnError,
1114
WithAuthProviderNoAccessControl,
1215
WithRenderProps,
1316
} from './EditBase.stories';
@@ -455,4 +458,41 @@ describe('EditBase', () => {
455458
await screen.findByText('You are offline, the data may be outdated');
456459
await screen.findByText('Hello');
457460
});
461+
it('should render loading component while loading', async () => {
462+
render(<Loading />);
463+
expect(screen.queryByText('Loading data...')).not.toBeNull();
464+
expect(screen.queryByText('Hello')).toBeNull();
465+
fireEvent.click(screen.getByText('Resolve loading'));
466+
await waitFor(() => {
467+
expect(screen.queryByText('Loading data...')).toBeNull();
468+
});
469+
await screen.findByText('Hello');
470+
});
471+
it('should render error component on error', async () => {
472+
jest.spyOn(console, 'error').mockImplementation(() => {});
473+
474+
render(<FetchError />);
475+
expect(screen.queryByText('Something went wrong.')).toBeNull();
476+
expect(screen.queryByText('Hello')).toBeNull();
477+
fireEvent.click(screen.getByText('Reject loading'));
478+
await waitFor(() => {
479+
expect(screen.queryByText('Something went wrong.')).not.toBeNull();
480+
});
481+
expect(screen.queryByText('Hello')).toBeNull();
482+
483+
jest.spyOn(console, 'error').mockRestore();
484+
});
485+
it('should redirect when no error component is provided', async () => {
486+
jest.spyOn(console, 'error').mockImplementation(() => {});
487+
488+
render(<RedirectOnError />);
489+
expect(screen.queryByText('Hello')).toBeNull();
490+
fireEvent.click(screen.getByText('Reject loading'));
491+
await waitFor(() => {
492+
expect(screen.queryByText('List view')).not.toBeNull();
493+
});
494+
expect(screen.queryByText('Hello')).toBeNull();
495+
496+
jest.spyOn(console, 'error').mockRestore();
497+
});
458498
});

packages/ra-core/src/controller/edit/EditBase.stories.tsx

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import {
1818
MutationMode,
1919
WithRecord,
2020
IsOffline,
21+
GetOneResult,
22+
TestMemoryRouter,
23+
CoreAdmin,
24+
Resource,
2125
} from '../..';
2226
import { onlineManager, useMutationState } from '@tanstack/react-query';
2327

@@ -125,7 +129,7 @@ export const WithAuthProviderNoAccessControl = ({
125129
<EditBase
126130
{...defaultProps}
127131
{...EditProps}
128-
loading={<div>Authentication loading...</div>}
132+
authLoading={<div>Authentication loading...</div>}
129133
>
130134
<Child />
131135
</EditBase>
@@ -148,7 +152,7 @@ export const AccessControl = ({
148152
<CoreAdminContext authProvider={authProvider} dataProvider={dataProvider}>
149153
<EditBase
150154
{...defaultProps}
151-
loading={<div>Authentication loading...</div>}
155+
authLoading={<div>Authentication loading...</div>}
152156
>
153157
<Child />
154158
</EditBase>
@@ -184,6 +188,98 @@ export const WithRenderProps = ({
184188
</CoreAdminContext>
185189
);
186190

191+
export const Loading = () => {
192+
let resolveGetOne: (() => void) | null = null;
193+
const dataProvider = {
194+
...defaultDataProvider,
195+
getOne: (resource, params) => {
196+
return new Promise<GetOneResult>(resolve => {
197+
resolveGetOne = () =>
198+
resolve(defaultDataProvider.getOne(resource, params));
199+
});
200+
},
201+
};
202+
203+
return (
204+
<CoreAdminContext dataProvider={dataProvider}>
205+
<button
206+
onClick={() => {
207+
resolveGetOne && resolveGetOne();
208+
}}
209+
>
210+
Resolve loading
211+
</button>
212+
<EditBase {...defaultProps} loading={<div>Loading data...</div>}>
213+
<Child />
214+
</EditBase>
215+
</CoreAdminContext>
216+
);
217+
};
218+
219+
export const FetchError = () => {
220+
let rejectGetOne: (() => void) | null = null;
221+
const dataProvider = {
222+
...defaultDataProvider,
223+
getOne: () => {
224+
return new Promise<GetOneResult>((_, reject) => {
225+
rejectGetOne = () => reject(new Error('Expected error.'));
226+
});
227+
},
228+
};
229+
230+
return (
231+
<CoreAdminContext dataProvider={dataProvider}>
232+
<button
233+
onClick={() => {
234+
rejectGetOne && rejectGetOne();
235+
}}
236+
>
237+
Reject loading
238+
</button>
239+
<EditBase {...defaultProps} error={<p>Something went wrong.</p>}>
240+
<Child />
241+
</EditBase>
242+
</CoreAdminContext>
243+
);
244+
};
245+
246+
export const RedirectOnError = () => {
247+
let rejectGetOne: (() => void) | null = null;
248+
const dataProvider = {
249+
...defaultDataProvider,
250+
getOne: () => {
251+
return new Promise<GetOneResult>((_, reject) => {
252+
rejectGetOne = () => reject(new Error('Expected error.'));
253+
});
254+
},
255+
};
256+
257+
return (
258+
<TestMemoryRouter initialEntries={['/posts/12/show']}>
259+
<CoreAdmin dataProvider={dataProvider}>
260+
<Resource
261+
name="posts"
262+
list={<p>List view</p>}
263+
show={
264+
<>
265+
<button
266+
onClick={() => {
267+
rejectGetOne && rejectGetOne();
268+
}}
269+
>
270+
Reject loading
271+
</button>
272+
<EditBase {...defaultProps}>
273+
<Child />
274+
</EditBase>
275+
</>
276+
}
277+
/>
278+
</CoreAdmin>
279+
</TestMemoryRouter>
280+
);
281+
};
282+
187283
export const Offline = ({
188284
dataProvider = defaultDataProvider,
189285
isOnline = true,

packages/ra-core/src/controller/edit/EditBase.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,20 @@ import { useIsAuthPending } from '../../auth';
4141
* );
4242
*/
4343
export const EditBase = <RecordType extends RaRecord = any, ErrorType = Error>({
44-
children,
44+
authLoading,
4545
loading,
4646
offline,
47+
error,
48+
redirectOnError,
49+
children,
4750
render,
4851
...props
4952
}: EditBaseProps<RecordType, ErrorType>) => {
50-
const controllerProps = useEditController<RecordType, ErrorType>(props);
53+
const hasError = error !== false && error !== undefined;
54+
const controllerProps = useEditController<RecordType, ErrorType>({
55+
...props,
56+
redirectOnError: redirectOnError ?? (hasError ? false : undefined),
57+
});
5158

5259
const isAuthPending = useIsAuthPending({
5360
resource: controllerProps.resource,
@@ -60,28 +67,40 @@ export const EditBase = <RecordType extends RaRecord = any, ErrorType = Error>({
6067
);
6168
}
6269

63-
const { isPaused, isPending } = controllerProps;
70+
const { isPaused, isPending, error: errorState } = controllerProps;
6471

65-
const shouldRenderLoading =
72+
const showAuthLoading =
6673
isAuthPending &&
6774
!props.disableAuthentication &&
75+
authLoading !== false &&
76+
authLoading !== undefined;
77+
78+
const showLoading =
79+
!isPaused &&
80+
((!props.disableAuthentication && isAuthPending) || isPending) &&
6881
loading !== false &&
6982
loading !== undefined;
7083

71-
const shouldRenderOffline =
84+
const showOffline =
7285
isPaused && isPending && offline !== false && offline !== undefined;
7386

87+
const showError = errorState && hasError;
88+
7489
return (
7590
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
7691
<OptionalResourceContextProvider value={props.resource}>
7792
<EditContextProvider value={controllerProps}>
78-
{shouldRenderLoading
79-
? loading
80-
: shouldRenderOffline
81-
? offline
82-
: render
83-
? render(controllerProps)
84-
: children}
93+
{showAuthLoading
94+
? authLoading
95+
: showLoading
96+
? loading
97+
: showOffline
98+
? offline
99+
: showError
100+
? error
101+
: render
102+
? render(controllerProps)
103+
: children}
85104
</EditContextProvider>
86105
</OptionalResourceContextProvider>
87106
);
@@ -91,8 +110,10 @@ export interface EditBaseProps<
91110
RecordType extends RaRecord = RaRecord,
92111
ErrorType = Error,
93112
> extends EditControllerProps<RecordType, ErrorType> {
94-
children?: ReactNode;
95-
render?: (props: EditControllerResult<RecordType, ErrorType>) => ReactNode;
113+
authLoading?: ReactNode;
96114
loading?: ReactNode;
97115
offline?: ReactNode;
116+
error?: ReactNode;
117+
children?: ReactNode;
118+
render?: (props: EditControllerResult<RecordType, ErrorType>) => ReactNode;
98119
}

packages/ra-core/src/controller/edit/useEditController.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const useEditController = <
6060
mutationOptions = {},
6161
queryOptions = {},
6262
redirect: redirectTo = DefaultRedirect,
63+
redirectOnError = DefaultRedirectOnError,
6364
transform,
6465
} = props;
6566
const resource = useResourceContext(props);
@@ -133,7 +134,7 @@ export const useEditController = <
133134
notify('ra.notification.item_doesnt_exist', {
134135
type: 'error',
135136
});
136-
redirect('list', resource);
137+
redirect(redirectOnError, resource);
137138
},
138139
refetchOnReconnect: false,
139140
refetchOnWindowFocus: false,
@@ -295,6 +296,7 @@ export const useEditController = <
295296
mutationMode,
296297
record,
297298
redirect: redirectTo,
299+
redirectOnError,
298300
refetch,
299301
registerMutationMiddleware,
300302
resource,
@@ -305,6 +307,7 @@ export const useEditController = <
305307
};
306308

307309
const DefaultRedirect = 'list';
310+
const DefaultRedirectOnError = 'list';
308311

309312
export interface EditControllerProps<
310313
RecordType extends RaRecord = any,
@@ -316,6 +319,7 @@ export interface EditControllerProps<
316319
mutationOptions?: UseUpdateOptions<RecordType, ErrorType>;
317320
queryOptions?: UseGetOneOptions<RecordType, ErrorType>;
318321
redirect?: RedirectionSideEffect;
322+
redirectOnError?: RedirectionSideEffect;
319323
resource?: string;
320324
transform?: TransformData;
321325

@@ -331,6 +335,7 @@ export interface EditControllerBaseResult<RecordType extends RaRecord = any>
331335
isPlaceholderData?: boolean;
332336
refetch: UseGetOneHookValue<RecordType>['refetch'];
333337
redirect: RedirectionSideEffect;
338+
redirectOnError: RedirectionSideEffect;
334339
resource: string;
335340
saving: boolean;
336341
}

0 commit comments

Comments
 (0)