From 04723adeea3915acbaa5831e3e22231d7de6bbca Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 25 Jul 2025 11:12:52 +0200
Subject: [PATCH 01/15] Introduce `useIsOffline` hook and ``
component
---
packages/ra-core/src/core/IsOffline.spec.tsx | 14 ++++++++
.../ra-core/src/core/IsOffline.stories.tsx | 33 +++++++++++++++++++
packages/ra-core/src/core/IsOffline.tsx | 7 ++++
packages/ra-core/src/core/index.ts | 2 ++
packages/ra-core/src/core/useIsOffline.ts | 21 ++++++++++++
5 files changed, 77 insertions(+)
create mode 100644 packages/ra-core/src/core/IsOffline.spec.tsx
create mode 100644 packages/ra-core/src/core/IsOffline.stories.tsx
create mode 100644 packages/ra-core/src/core/IsOffline.tsx
create mode 100644 packages/ra-core/src/core/useIsOffline.ts
diff --git a/packages/ra-core/src/core/IsOffline.spec.tsx b/packages/ra-core/src/core/IsOffline.spec.tsx
new file mode 100644
index 00000000000..29695e98617
--- /dev/null
+++ b/packages/ra-core/src/core/IsOffline.spec.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import { render, screen } from '@testing-library/react';
+import { Basic } from './IsOffline.stories';
+
+describe('', () => {
+ it('should render children when offline', async () => {
+ const { rerender } = render();
+ await screen.findByText('You are offline, the data may be outdated');
+ rerender();
+ expect(
+ screen.queryByText('You are offline, the data may be outdated')
+ ).toBeNull();
+ });
+});
diff --git a/packages/ra-core/src/core/IsOffline.stories.tsx b/packages/ra-core/src/core/IsOffline.stories.tsx
new file mode 100644
index 00000000000..7f6c3c5ca67
--- /dev/null
+++ b/packages/ra-core/src/core/IsOffline.stories.tsx
@@ -0,0 +1,33 @@
+import { onlineManager } from '@tanstack/react-query';
+import React from 'react';
+import { IsOffline } from './IsOffline';
+
+export default {
+ title: 'ra-core/core/IsOffline',
+};
+
+export const Basic = ({ isOnline = true }: { isOnline?: boolean }) => {
+ React.useEffect(() => {
+ onlineManager.setOnline(isOnline);
+ }, [isOnline]);
+ return (
+ <>
+ Use the story controls to simulate offline mode:
+
+
+ You are offline, the data may be outdated
+
+
+ >
+ );
+};
+
+Basic.args = {
+ isOnline: true,
+};
+
+Basic.argTypes = {
+ isOnline: {
+ control: { type: 'boolean' },
+ },
+};
diff --git a/packages/ra-core/src/core/IsOffline.tsx b/packages/ra-core/src/core/IsOffline.tsx
new file mode 100644
index 00000000000..3e0c3dca7c2
--- /dev/null
+++ b/packages/ra-core/src/core/IsOffline.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { useIsOffine } from './useIsOffline';
+
+export const IsOffline = ({ children }: { children: React.ReactNode }) => {
+ const isOffline = useIsOffine();
+ return isOffline ? children : null;
+};
diff --git a/packages/ra-core/src/core/index.ts b/packages/ra-core/src/core/index.ts
index fb7b21aa759..91c75d1213f 100644
--- a/packages/ra-core/src/core/index.ts
+++ b/packages/ra-core/src/core/index.ts
@@ -5,6 +5,7 @@ export * from './CoreAdminUI';
export * from './CustomRoutes';
export * from './DefaultTitleContext';
export * from './HasDashboardContext';
+export * from './IsOffline';
export * from './NavigateToFirstResource';
export * from './OptionalResourceContextProvider';
export * from './Resource';
@@ -15,6 +16,7 @@ export * from './SourceContext';
export * from './useFirstResourceWithListAccess';
export * from './useGetResourceLabel';
export * from './useGetRecordRepresentation';
+export * from './useIsOffline';
export * from './useResourceDefinitionContext';
export * from './useResourceContext';
export * from './useResourceDefinition';
diff --git a/packages/ra-core/src/core/useIsOffline.ts b/packages/ra-core/src/core/useIsOffline.ts
new file mode 100644
index 00000000000..6e54c89ef1d
--- /dev/null
+++ b/packages/ra-core/src/core/useIsOffline.ts
@@ -0,0 +1,21 @@
+import * as React from 'react';
+import { onlineManager } from '@tanstack/react-query';
+
+/**
+ * Hook to determine if the application is offline.
+ * It uses the onlineManager from react-query to check the online status.
+ * It returns true if the application is offline, false otherwise.
+ * @returns {boolean} - True if offline, false if online.
+ */
+export const useIsOffine = () => {
+ const [isOnline, setIsOnline] = React.useState(onlineManager.isOnline());
+
+ React.useEffect(() => {
+ const handleChange = () => {
+ setIsOnline(onlineManager.isOnline());
+ };
+ return onlineManager.subscribe(handleChange);
+ }, []);
+
+ return !isOnline;
+};
From 9cb47ecfecae0082c0950a6b4b1bd75b27089d8d Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 25 Jul 2025 11:13:01 +0200
Subject: [PATCH 02/15] Add offline support to ``
---
.../src/controller/show/ShowBase.spec.tsx | 13 +++
.../src/controller/show/ShowBase.stories.tsx | 80 +++++++++++++++----
.../ra-core/src/controller/show/ShowBase.tsx | 30 +++++--
.../src/controller/show/useShowController.ts | 6 ++
4 files changed, 108 insertions(+), 21 deletions(-)
diff --git a/packages/ra-core/src/controller/show/ShowBase.spec.tsx b/packages/ra-core/src/controller/show/ShowBase.spec.tsx
index 7429cf5cfc9..f77709bedb6 100644
--- a/packages/ra-core/src/controller/show/ShowBase.spec.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.spec.tsx
@@ -7,6 +7,7 @@ import {
AccessControl,
DefaultTitle,
NoAuthProvider,
+ Offline,
WithAuthProviderNoAccessControl,
WithRenderProp,
} from './ShowBase.stories';
@@ -118,4 +119,16 @@ describe('ShowBase', () => {
expect(dataProvider.getOne).toHaveBeenCalled();
await screen.findByText('Hello');
});
+
+ it('should render the offline prop node when offline', async () => {
+ const { rerender } = render();
+ await screen.findByText('You are offline, cannot load data');
+ rerender();
+ await screen.findByText('Hello');
+ expect(
+ screen.queryByText('You are offline, cannot load data')
+ ).toBeNull();
+ rerender();
+ await screen.findByText('You are offline, the data may be outdated');
+ });
});
diff --git a/packages/ra-core/src/controller/show/ShowBase.stories.tsx b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
index adc42bc74e2..45a887686b8 100644
--- a/packages/ra-core/src/controller/show/ShowBase.stories.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
@@ -2,19 +2,21 @@ import * as React from 'react';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import polyglotI18nProvider from 'ra-i18n-polyglot';
+import fakeRestDataProvider from 'ra-data-fakerest';
import {
AuthProvider,
CoreAdminContext,
ShowBase,
ShowBaseProps,
DataProvider,
- testDataProvider,
- useRecordContext,
mergeTranslations,
I18nProvider,
useShowContext,
useLocaleState,
+ IsOffline,
+ WithRecord,
} from '../..';
+import { onlineManager } from '@tanstack/react-query';
export default {
title: 'ra-core/controller/ShowBase',
@@ -68,7 +70,7 @@ export const DefaultTitle = ({
translations?: 'default' | 'resource specific';
}) => (
@@ -88,7 +90,7 @@ DefaultTitle.argTypes = {
};
export const NoAuthProvider = ({
- dataProvider = defaultDataProvider,
+ dataProvider = defaultDataProvider(),
...props
}: {
dataProvider?: DataProvider;
@@ -107,7 +109,7 @@ export const WithAuthProviderNoAccessControl = ({
checkError: () => Promise.resolve(),
checkAuth: () => new Promise(resolve => setTimeout(resolve, 300)),
},
- dataProvider = defaultDataProvider,
+ dataProvider = defaultDataProvider(),
}: {
authProvider?: AuthProvider;
dataProvider?: DataProvider;
@@ -130,7 +132,7 @@ export const AccessControl = ({
checkAuth: () => new Promise(resolve => setTimeout(resolve, 300)),
canAccess: () => new Promise(resolve => setTimeout(resolve, 300, true)),
},
- dataProvider = defaultDataProvider,
+ dataProvider = defaultDataProvider(),
}: {
authProvider?: AuthProvider;
dataProvider?: DataProvider;
@@ -146,7 +148,7 @@ export const AccessControl = ({
);
export const WithRenderProp = ({
- dataProvider = defaultDataProvider,
+ dataProvider = defaultDataProvider(),
...props
}: {
dataProvider?: DataProvider;
@@ -162,11 +164,51 @@ export const WithRenderProp = ({
);
-const defaultDataProvider = testDataProvider({
- getOne: () =>
- // @ts-ignore
- Promise.resolve({ data: { id: 12, test: 'Hello', title: 'Hello' } }),
-});
+export const Offline = ({
+ dataProvider = defaultDataProvider(),
+ isOnline = true,
+ ...props
+}: {
+ dataProvider?: DataProvider;
+ isOnline?: boolean;
+} & Partial) => {
+ React.useEffect(() => {
+ onlineManager.setOnline(isOnline);
+ }, [isOnline]);
+ return (
+
+ You are offline, cannot load data
}
+ >
+
+
+
+ );
+};
+
+Offline.args = {
+ isOnline: true,
+};
+
+Offline.argTypes = {
+ isOnline: {
+ control: { type: 'boolean' },
+ },
+};
+
+const defaultDataProvider = (delay = 300) =>
+ fakeRestDataProvider(
+ {
+ posts: [
+ { id: 12, test: 'Hello', title: 'Hello' },
+ { id: 13, test: 'World', title: 'World' },
+ ],
+ },
+ process.env.NODE_ENV !== 'test',
+ process.env.NODE_ENV !== 'test' ? delay : 0
+ );
const defaultProps = {
id: 12,
@@ -174,9 +216,17 @@ const defaultProps = {
};
const Child = () => {
- const record = useRecordContext();
-
- return {record?.test}
;
+ return (
+ <>
+ Use the story controls to simulate offline mode:
+
+
+ You are offline, the data may be outdated
+
+
+ {record?.test}
} />
+ >
+ );
};
const Title = () => {
diff --git a/packages/ra-core/src/controller/show/ShowBase.tsx b/packages/ra-core/src/controller/show/ShowBase.tsx
index 264a07b7866..019f4346333 100644
--- a/packages/ra-core/src/controller/show/ShowBase.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.tsx
@@ -42,7 +42,8 @@ import { useIsAuthPending } from '../../auth';
export const ShowBase = ({
children,
render,
- loading = null,
+ loading,
+ offline,
...props
}: ShowBaseProps) => {
const controllerProps = useShowController(props);
@@ -52,21 +53,37 @@ export const ShowBase = ({
action: 'show',
});
- if (isAuthPending && !props.disableAuthentication) {
- return loading;
- }
-
if (!render && !children) {
throw new Error(
' requires either a `render` prop or `children` prop'
);
}
+ const { isPaused, record } = controllerProps;
+
return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
- {render ? render(controllerProps) : children}
+ {(() => {
+ if (
+ isAuthPending &&
+ !props.disableAuthentication &&
+ loading !== false &&
+ loading !== undefined
+ ) {
+ return loading;
+ }
+ if (
+ isPaused &&
+ !record &&
+ offline !== false &&
+ offline !== undefined
+ ) {
+ return offline;
+ }
+ return render ? render(controllerProps) : children;
+ })()}
);
@@ -77,4 +94,5 @@ export interface ShowBaseProps
children?: React.ReactNode;
render?: (props: ShowControllerResult) => React.ReactNode;
loading?: React.ReactNode;
+ offline?: React.ReactNode;
}
diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts
index 4f523a8b126..c45ae8d4c52 100644
--- a/packages/ra-core/src/controller/show/useShowController.ts
+++ b/packages/ra-core/src/controller/show/useShowController.ts
@@ -97,7 +97,9 @@ export const useShowController = <
error,
isLoading,
isFetching,
+ isPaused,
isPending,
+ isPlaceholderData,
refetch,
} = useGetOne(
resource,
@@ -159,7 +161,9 @@ export const useShowController = <
error,
isLoading,
isFetching,
+ isPaused,
isPending,
+ isPlaceholderData,
record,
refetch,
resource,
@@ -180,6 +184,8 @@ export interface ShowControllerBaseResult {
defaultTitle?: string;
isFetching: boolean;
isLoading: boolean;
+ isPaused: boolean;
+ isPlaceholderData: boolean;
resource: string;
record?: RecordType;
refetch: UseGetOneHookValue['refetch'];
From 72750f7f394b5a8e3724efb26e2b94af7e258102 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 25 Jul 2025 11:49:53 +0200
Subject: [PATCH 03/15] Introduce `` component
---
.../ra-core/src/i18n/TranslationMessages.ts | 1 +
packages/ra-language-english/src/index.ts | 1 +
packages/ra-language-french/src/index.ts | 1 +
.../ra-ui-materialui/src/Offline.spec.tsx | 14 +++
.../ra-ui-materialui/src/Offline.stories.tsx | 117 ++++++++++++++++++
packages/ra-ui-materialui/src/Offline.tsx | 91 ++++++++++++++
packages/ra-ui-materialui/src/index.ts | 1 +
7 files changed, 226 insertions(+)
create mode 100644 packages/ra-ui-materialui/src/Offline.spec.tsx
create mode 100644 packages/ra-ui-materialui/src/Offline.stories.tsx
create mode 100644 packages/ra-ui-materialui/src/Offline.tsx
diff --git a/packages/ra-core/src/i18n/TranslationMessages.ts b/packages/ra-core/src/i18n/TranslationMessages.ts
index 085bc7d61a1..9ea8666dd37 100644
--- a/packages/ra-core/src/i18n/TranslationMessages.ts
+++ b/packages/ra-core/src/i18n/TranslationMessages.ts
@@ -168,6 +168,7 @@ export interface TranslationMessages extends StringMap {
logged_out: string;
not_authorized: string;
application_update_available: string;
+ offline: string;
};
validation: {
[key: string]: StringMap | string;
diff --git a/packages/ra-language-english/src/index.ts b/packages/ra-language-english/src/index.ts
index d9435902eae..869bd8665e3 100644
--- a/packages/ra-language-english/src/index.ts
+++ b/packages/ra-language-english/src/index.ts
@@ -172,6 +172,7 @@ const englishMessages: TranslationMessages = {
logged_out: 'Your session has ended, please reconnect.',
not_authorized: "You're not authorized to access this resource.",
application_update_available: 'A new version is available.',
+ offline: 'No connectivity. Could not fetch data.',
},
validation: {
required: 'Required',
diff --git a/packages/ra-language-french/src/index.ts b/packages/ra-language-french/src/index.ts
index a42a2999aa2..3929b48e397 100644
--- a/packages/ra-language-french/src/index.ts
+++ b/packages/ra-language-french/src/index.ts
@@ -180,6 +180,7 @@ const frenchMessages: TranslationMessages = {
not_authorized:
"Vous n'êtes pas autorisé(e) à accéder à cette ressource.",
application_update_available: 'Une mise à jour est disponible.',
+ offline: 'Pas de connexion. Impossible de charger les données.',
},
validation: {
required: 'Ce champ est requis',
diff --git a/packages/ra-ui-materialui/src/Offline.spec.tsx b/packages/ra-ui-materialui/src/Offline.spec.tsx
new file mode 100644
index 00000000000..f6ae349a9d8
--- /dev/null
+++ b/packages/ra-ui-materialui/src/Offline.spec.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import { render, screen } from '@testing-library/react';
+import { I18n, I18nResourceSpecific } from './Offline.stories';
+
+describe('', () => {
+ it('should render the default message', async () => {
+ render();
+ await screen.findByText('No connectivity. Could not fetch data.');
+ });
+ it('should render the resource specific message', async () => {
+ render();
+ await screen.findByText('No connectivity. Could not fetch posts.');
+ });
+});
diff --git a/packages/ra-ui-materialui/src/Offline.stories.tsx b/packages/ra-ui-materialui/src/Offline.stories.tsx
new file mode 100644
index 00000000000..a04dd711e84
--- /dev/null
+++ b/packages/ra-ui-materialui/src/Offline.stories.tsx
@@ -0,0 +1,117 @@
+import * as React from 'react';
+import { mergeTranslations, Resource } from 'ra-core';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
+import englishMessages from 'ra-language-english';
+import { Paper } from '@mui/material';
+import {
+ bwDarkTheme,
+ bwLightTheme,
+ defaultDarkTheme,
+ defaultLightTheme,
+} from './theme';
+import { AdminContext } from './AdminContext';
+import { Offline } from './Offline';
+
+export default { title: 'ra-ui-materialui/Offline' };
+
+export const Standard = ({ theme }) => (
+
+
+
+
+ }
+ />
+
+);
+
+Standard.args = {
+ theme: 'default-light',
+};
+
+Standard.argTypes = {
+ theme: {
+ control: {
+ type: 'select',
+ },
+ options: ['default-light', 'default-dark', 'bw-light', 'bw-dark'],
+ mapping: {
+ 'default-light': defaultLightTheme,
+ 'default-dark': defaultDarkTheme,
+ 'bw-light': bwLightTheme,
+ 'bw-dark': bwDarkTheme,
+ },
+ },
+};
+
+export const Inline = ({ theme }) => (
+
+
+
+
+ }
+ />
+
+);
+
+Inline.args = {
+ theme: 'default-light',
+};
+
+Inline.argTypes = {
+ theme: {
+ control: {
+ type: 'select',
+ },
+ options: ['default-light', 'default-dark', 'bw-light', 'bw-dark'],
+ mapping: {
+ 'default-light': defaultLightTheme,
+ 'default-dark': defaultDarkTheme,
+ 'bw-light': bwLightTheme,
+ 'bw-dark': bwDarkTheme,
+ },
+ },
+};
+
+export const I18n = () => (
+ englishMessages)}>
+
+
+
+ }
+ />
+
+);
+
+export const I18nResourceSpecific = () => (
+
+ mergeTranslations(englishMessages, {
+ resources: {
+ posts: {
+ notification: {
+ offline: 'No connectivity. Could not fetch posts.',
+ },
+ },
+ },
+ })
+ )}
+ >
+
+
+
+ }
+ />
+
+);
diff --git a/packages/ra-ui-materialui/src/Offline.tsx b/packages/ra-ui-materialui/src/Offline.tsx
new file mode 100644
index 00000000000..797f4a89e68
--- /dev/null
+++ b/packages/ra-ui-materialui/src/Offline.tsx
@@ -0,0 +1,91 @@
+import * as React from 'react';
+import {
+ Alert,
+ AlertProps,
+ ComponentsOverrides,
+ styled,
+ Typography,
+} from '@mui/material';
+import {
+ useGetResourceLabel,
+ useResourceContext,
+ useResourceTranslation,
+} from 'ra-core';
+import clsx from 'clsx';
+
+export const Offline = (props: Offline) => {
+ const { icon, message: messageProp, variant = 'standard', ...rest } = props;
+ const resource = useResourceContext(props);
+ const getResourceLabel = useGetResourceLabel();
+ if (!resource) {
+ throw new Error(
+ ' must be used inside a component or provided a resource prop'
+ );
+ }
+ const message = useResourceTranslation({
+ baseI18nKey: 'ra.notification.offline',
+ resourceI18nKey: `resources.${resource}.notification.offline`,
+ userText: messageProp,
+ options: {
+ name: getResourceLabel(resource, 0),
+ _: 'No connectivity. Could not fetch data.',
+ },
+ });
+
+ return (
+
+ {message}
+
+ );
+};
+
+export interface Offline extends Omit {
+ resource?: string;
+ message?: string;
+ variant?: AlertProps['variant'] | 'inline';
+}
+
+const PREFIX = 'RaOffline';
+export const OfflineClasses = {
+ root: `${PREFIX}-root`,
+ inline: `${PREFIX}-inline`,
+};
+
+const Root = styled(Alert, {
+ name: PREFIX,
+ overridesResolver: (props, styles) => styles.root,
+})(() => ({
+ [`&.${OfflineClasses.inline}`]: {
+ border: 'none',
+ display: 'inline-flex',
+ padding: 0,
+ margin: 0,
+ },
+}));
+
+declare module '@mui/material/styles' {
+ interface ComponentNameToClassKey {
+ [PREFIX]: 'root';
+ }
+
+ interface ComponentsPropsList {
+ [PREFIX]: Partial;
+ }
+
+ interface Components {
+ [PREFIX]?: {
+ defaultProps?: ComponentsPropsList[typeof PREFIX];
+ styleOverrides?: ComponentsOverrides<
+ Omit
+ >[typeof PREFIX];
+ };
+ }
+}
diff --git a/packages/ra-ui-materialui/src/index.ts b/packages/ra-ui-materialui/src/index.ts
index 6ef5bfa3d89..811508dd4e3 100644
--- a/packages/ra-ui-materialui/src/index.ts
+++ b/packages/ra-ui-materialui/src/index.ts
@@ -9,6 +9,7 @@ export * from './Labeled';
export * from './layout';
export * from './Link';
export * from './list';
+export * from './Offline';
export * from './preferences';
export * from './AdminUI';
export * from './AdminContext';
From 80a1878663e61a2d4d0a6eae813792c2406d8f8e Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 25 Jul 2025 11:50:12 +0200
Subject: [PATCH 04/15] Add offline support to ``
---
.../ra-ui-materialui/src/detail/Show.spec.tsx | 27 +++++++
.../src/detail/Show.stories.tsx | 78 ++++++++++++++++++-
packages/ra-ui-materialui/src/detail/Show.tsx | 2 +
.../ra-ui-materialui/src/detail/ShowView.tsx | 25 ++++--
.../ra-ui-materialui/src/layout/Title.tsx | 4 +-
5 files changed, 125 insertions(+), 11 deletions(-)
diff --git a/packages/ra-ui-materialui/src/detail/Show.spec.tsx b/packages/ra-ui-materialui/src/detail/Show.spec.tsx
index 88db2e52036..e0d51bf66f6 100644
--- a/packages/ra-ui-materialui/src/detail/Show.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.spec.tsx
@@ -26,8 +26,10 @@ import {
TitleElement,
Themed,
WithRenderProp,
+ Offline,
} from './Show.stories';
import { Show } from './Show';
+import { Alert } from '@mui/material';
describe('', () => {
beforeEach(async () => {
@@ -226,4 +228,29 @@ describe('', () => {
await screen.findByText('Foo lorem');
});
});
+ it('should render the default offline component node when offline', async () => {
+ const { rerender } = render();
+ await screen.findByText('No connectivity. Could not fetch data.');
+ rerender();
+ await screen.findByText('War and Peace');
+ expect(
+ screen.queryByText('No connectivity. Could not fetch data.')
+ ).toBeNull();
+ rerender();
+ await screen.findByText('You are offline, the data may be outdated');
+ });
+ it('should render the custom offline component node when offline', async () => {
+ const CustomOffline = () => {
+ return You are offline!;
+ };
+ const { rerender } = render(
+ } />
+ );
+ await screen.findByText('You are offline!');
+ rerender(} />);
+ await screen.findByText('War and Peace');
+ expect(screen.queryByText('You are offline!')).toBeNull();
+ rerender(} />);
+ await screen.findByText('You are offline, the data may be outdated');
+ });
});
diff --git a/packages/ra-ui-materialui/src/detail/Show.stories.tsx b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
index 3fdee242f49..9463824f5a4 100644
--- a/packages/ra-ui-materialui/src/detail/Show.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
@@ -1,15 +1,21 @@
import * as React from 'react';
import { Admin } from 'react-admin';
-import { Resource, useRecordContext, TestMemoryRouter } from 'ra-core';
-import { Box, Card, Stack, ThemeOptions } from '@mui/material';
+import {
+ Resource,
+ useRecordContext,
+ TestMemoryRouter,
+ IsOffline,
+} from 'ra-core';
+import { Alert, Box, Card, Stack, ThemeOptions } from '@mui/material';
+import { deepmerge } from '@mui/utils';
+import { onlineManager } from '@tanstack/react-query';
import { TextField } from '../field';
import { Labeled } from '../Labeled';
import { SimpleShowLayout } from './SimpleShowLayout';
import { EditButton } from '../button';
import TopToolbar from '../layout/TopToolbar';
-import { Show } from './Show';
-import { deepmerge } from '@mui/utils';
+import { Show, ShowProps } from './Show';
import { defaultLightTheme } from '../theme';
export default { title: 'ra-ui-materialui/detail/Show' };
@@ -299,3 +305,67 @@ export const WithRenderProp = () => (
);
+
+export const Offline = ({
+ isOnline = true,
+ offline,
+}: {
+ isOnline?: boolean;
+ offline?: React.ReactNode;
+}) => {
+ React.useEffect(() => {
+ onlineManager.setOnline(isOnline);
+ }, [isOnline]);
+ return (
+
+
+ }
+ />
+
+
+ );
+};
+
+const BookShowOffline = (props: ShowProps) => {
+ return (
+
+
+
+ You are offline, the data may be outdated
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const CustomOffline = () => {
+ return You are offline!;
+};
+
+Offline.args = {
+ isOnline: true,
+ offline: false,
+};
+
+Offline.argTypes = {
+ isOnline: {
+ control: { type: 'boolean' },
+ },
+ offline: {
+ name: 'Offline component',
+ control: { type: 'radio' },
+ options: ['default', 'custom'],
+ mapping: {
+ default: undefined,
+ custom: ,
+ },
+ },
+};
diff --git a/packages/ra-ui-materialui/src/detail/Show.tsx b/packages/ra-ui-materialui/src/detail/Show.tsx
index 9a97860242c..717c586ce1f 100644
--- a/packages/ra-ui-materialui/src/detail/Show.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.tsx
@@ -90,6 +90,8 @@ export const Show = (
queryOptions={queryOptions}
resource={resource}
loading={loading}
+ // Disable offline support from ShowBase as it is handled by ShowView to keep the ShowView container
+ offline={false}
>
diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx
index 5028186e685..1554425676a 100644
--- a/packages/ra-ui-materialui/src/detail/ShowView.tsx
+++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { ReactElement, ElementType, ReactNode } from 'react';
+import type { ElementType, ReactNode } from 'react';
import {
Card,
type ComponentsOverrides,
@@ -16,8 +16,10 @@ import {
import { ShowActions } from './ShowActions';
import { Title } from '../layout';
import { ShowProps } from './Show';
+import { Offline } from '../Offline';
const defaultActions = ;
+const defaultOffline = ;
export const ShowView = (props: ShowViewProps) => {
const {
@@ -28,20 +30,32 @@ export const ShowView = (props: ShowViewProps) => {
className,
component: Content = Card,
emptyWhileLoading = false,
+ offline = defaultOffline,
title,
...rest
} = props;
const showContext = useShowContext();
- const { resource, defaultTitle, record } = showContext;
+ const { resource, defaultTitle, isPaused, record } = showContext;
const { hasEdit } = useResourceDefinition();
const finalActions =
typeof actions === 'undefined' && hasEdit ? defaultActions : actions;
+ if (!record && offline !== false && isPaused) {
+ return (
+
+
+ {offline}
+ {aside}
+
+
+ );
+ }
if (!record && emptyWhileLoading) {
return null;
}
+
return (
{title !== false && (
@@ -68,11 +82,12 @@ export const ShowView = (props: ShowViewProps) => {
export interface ShowViewProps
extends Omit, 'id' | 'title'> {
- actions?: ReactElement | false;
- aside?: ReactElement;
+ actions?: ReactNode | false;
+ aside?: ReactNode;
component?: ElementType;
emptyWhileLoading?: boolean;
- title?: string | ReactElement | false;
+ offline?: ReactNode;
+ title?: ReactNode;
sx?: SxProps;
render?: (showContext: ShowControllerResult) => ReactNode;
}
diff --git a/packages/ra-ui-materialui/src/layout/Title.tsx b/packages/ra-ui-materialui/src/layout/Title.tsx
index 057d9849a1e..878abd1529c 100644
--- a/packages/ra-ui-materialui/src/layout/Title.tsx
+++ b/packages/ra-ui-materialui/src/layout/Title.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { useEffect, useState, ReactElement } from 'react';
+import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { RaRecord, TitleComponent, warning } from 'ra-core';
@@ -50,6 +50,6 @@ export interface TitleProps {
className?: string;
defaultTitle?: TitleComponent;
record?: Partial;
- title?: string | ReactElement;
+ title?: React.ReactNode;
preferenceKey?: string | false;
}
From c31dde57c9def753dea90a3ce3900016ec10e1ae Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 25 Jul 2025 12:10:20 +0200
Subject: [PATCH 05/15] Update `` documentation
---
docs/ShowBase.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 51 insertions(+), 2 deletions(-)
diff --git a/docs/ShowBase.md b/docs/ShowBase.md
index 7e68a762962..c7e4d0600cd 100644
--- a/docs/ShowBase.md
+++ b/docs/ShowBase.md
@@ -63,10 +63,11 @@ const App = () => (
| Prop | Required | Type | Default | Description
|------------------|----------|-------------------|---------|--------------------------------------------------------
| `children` | Optional | `ReactNode` | | The components rendering the record fields
-| `render` | Optional | `(props: ShowControllerResult) => ReactNode` | | Alternative to children, a function that takes the ShowController context and renders the form
+| `render` | Optional | `(props: ShowControllerResult) => ReactNode` | | Alternative to children, a function that takes the ShowController context and renders the form
| `disable Authentication` | Optional | `boolean` | | Set to `true` to disable the authentication check
-| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading
| `id` | Optional | `string` | | The record identifier. If not provided, it will be deduced from the URL
+| `loading` | Optional | `ReactNode` | | The component to render while checking for authentication and permissions
+| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache
| `queryOptions` | Optional | `object` | | The options to pass to the `useQuery` hook
| `resource` | Optional | `string` | | The resource name, e.g. `posts`
@@ -139,6 +140,8 @@ const BookShow = () => (
By default, the `` component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the `disableAuthentication` prop to `true`.
```jsx
+import { ShowBase } from 'ra-core';
+
const PostShow = () => (
...
@@ -151,6 +154,8 @@ const PostShow = () => (
By default, `` deduces the identifier of the record to show from the URL path. So under the `/posts/123/show` path, the `id` prop will be `123`. You may want to force a different identifier. In this case, pass a custom `id` prop.
```jsx
+import { ShowBase } from 'ra-core';
+
export const PostShow = () => (
...
@@ -160,6 +165,48 @@ export const PostShow = () => (
**Tip**: Pass both a custom `id` and a custom `resource` prop to use `` independently of the current URL. This even allows you to use more than one `` component in the same page.
+## `loading`
+
+By default, `` renders nothing while checking for authentication and permissions. You can provide your own component via the `loading` prop:
+
+```jsx
+import { ShowBase } from 'ra-core';
+
+export const PostShow = () => (
+ Checking for permissions...}>
+ ...
+
+);
+```
+
+## `offline`
+
+By default, `` renders nothing when there is no connectivity and the record hasn't been cached yet. You can provide your own component via the `offline` prop:
+
+```jsx
+import { ShowBase } from 'ra-core';
+
+export const PostShow = () => (
+ No network. Could not load the post.}>
+ ...
+
+);
+```
+
+**Tip**: If the record is in the Tanstack Query cache but you want to warn the user that they may see an outdated version, you can use the `` component:
+
+```jsx
+import { ShowBase, IsOffline } from 'ra-core';
+
+export const PostShow = () => (
+ No network. Could not load the post.}>
+
+ No network. The post data may be outdated.
+
+
+);
+```
+
## `queryOptions`
`` accepts a `queryOptions` prop to pass options to the react-query client.
@@ -210,6 +257,8 @@ The default `onError` function is:
By default, `` operates on the current `ResourceContext` (defined at the routing level), so under the `/posts/1/show` path, the `resource` prop will be `posts`. You may want to force a different resource. In this case, pass a custom `resource` prop, and it will override the `ResourceContext` value.
```jsx
+import { ShowBase } from 'ra-core';
+
export const UsersShow = () => (
...
From 7ccd7ef77031e42cb51701dfc8ca5e33bdfd6a9d Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 25 Jul 2025 14:08:00 +0200
Subject: [PATCH 06/15] Update `` documentation
---
docs/Show.md | 41 ++++++++++++++++++++++++++++++++++++++++-
docs/ShowBase.md | 1 +
2 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/docs/Show.md b/docs/Show.md
index 470a50fb81b..ab4ceabe2f8 100644
--- a/docs/Show.md
+++ b/docs/Show.md
@@ -70,6 +70,7 @@ That's enough to display the post show view above.
| `disable Authentication` | Optional | `boolean` | | Set to `true` to disable the authentication check
| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the show is loading
| `id` | Optional | `string | number` | | The record id. If not provided, it will be deduced from the URL
+| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache
| `queryOptions` | Optional | `object` | | The options to pass to the `useQuery` hook
| `resource` | Optional | `string` | | The resource name, e.g. `posts`
| `sx` | Optional | `object` | | Override or extend the styles applied to the component
@@ -83,7 +84,7 @@ By default, `` includes an action toolbar with an `` if the `<
```jsx
import Button from '@mui/material/Button';
-import { EditButton, TopToolbar } from 'react-admin';
+import { EditButton, Show, TopToolbar } from 'react-admin';
const PostShowActions = () => (
@@ -158,6 +159,8 @@ React-admin provides 2 built-in show layout components:
To use an alternative layout, switch the `` child component:
```diff
+import { Show } from 'react-admin';
+
export const PostShow = () => (
-
@@ -188,6 +191,7 @@ You can override the main area container by passing a `component` prop:
{% raw %}
```jsx
+import { Show } from 'react-admin';
import { Box } from '@mui/material';
const ShowWrapper = ({ children }) => (
@@ -210,6 +214,8 @@ const PostShow = () => (
By default, the `` component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the `disableAuthentication` prop to `true`.
```jsx
+import { Show } from 'react-admin';
+
const PostShow = () => (
...
@@ -273,6 +279,8 @@ const BookShow = () => (
By default, `` deduces the identifier of the record to show from the URL path. So under the `/posts/123/show` path, the `id` prop will be `123`. You may want to force a different identifier. In this case, pass a custom `id` prop.
```jsx
+import { Show } from 'react-admin';
+
export const PostShow = () => (
...
@@ -282,6 +290,35 @@ export const PostShow = () => (
**Tip**: Pass both a custom `id` and a custom `resource` prop to use `` independently of the current URL. This even allows you to use more than one `` component in the same page.
+## `offline`
+
+By default, `` renders nothing when there is no connectivity and the record hasn't been cached yet. You can provide your own component via the `offline` prop:
+
+```jsx
+import { Show } from 'react-admin';
+
+export const PostShow = () => (
+ No network. Could not load the post.}>
+ ...
+
+);
+```
+
+**Tip**: If the record is in the Tanstack Query cache but you want to warn the user that they may see an outdated version, you can use the `` component:
+
+```jsx
+import { Show, IsOffline } from 'react-admin';
+
+export const PostShow = () => (
+ No network. Could not load the post.}>
+
+ No network. The post data may be outdated.
+
+ ...
+
+);
+```
+
## `queryOptions`
`` accepts a `queryOptions` prop to pass options to the react-query client.
@@ -372,6 +409,8 @@ export const PostShow = () => (
By default, `` operates on the current `ResourceContext` (defined at the routing level), so under the `/posts/1/show` path, the `resource` prop will be `posts`. You may want to force a different resource. In this case, pass a custom `resource` prop, and it will override the `ResourceContext` value.
```jsx
+import { Show } from 'react-admin';
+
export const UsersShow = () => (
...
diff --git a/docs/ShowBase.md b/docs/ShowBase.md
index c7e4d0600cd..7649675ee74 100644
--- a/docs/ShowBase.md
+++ b/docs/ShowBase.md
@@ -203,6 +203,7 @@ export const PostShow = () => (
No network. The post data may be outdated.
+ ...
);
```
From cc16194e3a8381617d8985a12ccbdc830b2e8c94 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 25 Jul 2025 14:13:03 +0200
Subject: [PATCH 07/15] Simplify ShowBase story
---
.../src/controller/show/ShowBase.stories.tsx | 33 +++++++++----------
1 file changed, 16 insertions(+), 17 deletions(-)
diff --git a/packages/ra-core/src/controller/show/ShowBase.stories.tsx b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
index 45a887686b8..54d8a4ad9c5 100644
--- a/packages/ra-core/src/controller/show/ShowBase.stories.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
@@ -70,7 +70,7 @@ export const DefaultTitle = ({
translations?: 'default' | 'resource specific';
}) => (
@@ -90,7 +90,7 @@ DefaultTitle.argTypes = {
};
export const NoAuthProvider = ({
- dataProvider = defaultDataProvider(),
+ dataProvider = defaultDataProvider,
...props
}: {
dataProvider?: DataProvider;
@@ -109,7 +109,7 @@ export const WithAuthProviderNoAccessControl = ({
checkError: () => Promise.resolve(),
checkAuth: () => new Promise(resolve => setTimeout(resolve, 300)),
},
- dataProvider = defaultDataProvider(),
+ dataProvider = defaultDataProvider,
}: {
authProvider?: AuthProvider;
dataProvider?: DataProvider;
@@ -132,7 +132,7 @@ export const AccessControl = ({
checkAuth: () => new Promise(resolve => setTimeout(resolve, 300)),
canAccess: () => new Promise(resolve => setTimeout(resolve, 300, true)),
},
- dataProvider = defaultDataProvider(),
+ dataProvider = defaultDataProvider,
}: {
authProvider?: AuthProvider;
dataProvider?: DataProvider;
@@ -148,7 +148,7 @@ export const AccessControl = ({
);
export const WithRenderProp = ({
- dataProvider = defaultDataProvider(),
+ dataProvider = defaultDataProvider,
...props
}: {
dataProvider?: DataProvider;
@@ -165,7 +165,7 @@ export const WithRenderProp = ({
);
export const Offline = ({
- dataProvider = defaultDataProvider(),
+ dataProvider = defaultDataProvider,
isOnline = true,
...props
}: {
@@ -198,17 +198,16 @@ Offline.argTypes = {
},
};
-const defaultDataProvider = (delay = 300) =>
- fakeRestDataProvider(
- {
- posts: [
- { id: 12, test: 'Hello', title: 'Hello' },
- { id: 13, test: 'World', title: 'World' },
- ],
- },
- process.env.NODE_ENV !== 'test',
- process.env.NODE_ENV !== 'test' ? delay : 0
- );
+const defaultDataProvider = fakeRestDataProvider(
+ {
+ posts: [
+ { id: 12, test: 'Hello', title: 'Hello' },
+ { id: 13, test: 'World', title: 'World' },
+ ],
+ },
+ process.env.NODE_ENV !== 'test',
+ process.env.NODE_ENV !== 'test' ? 300 : 0
+);
const defaultProps = {
id: 12,
From cc2ec92a6be1aa3d5f0c914f61f4eebca663c54f Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Mon, 28 Jul 2025 11:37:27 +0200
Subject: [PATCH 08/15] Only show offline instructions in offline story
---
packages/ra-core/src/controller/show/ShowBase.stories.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/packages/ra-core/src/controller/show/ShowBase.stories.tsx b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
index 54d8a4ad9c5..81b72f51c45 100644
--- a/packages/ra-core/src/controller/show/ShowBase.stories.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
@@ -182,7 +182,7 @@ export const Offline = ({
{...props}
offline={You are offline, cannot load data
}
>
-
+
);
@@ -215,6 +215,10 @@ const defaultProps = {
};
const Child = () => {
+ return {record?.test}
} />;
+};
+
+const OfflineChild = () => {
return (
<>
Use the story controls to simulate offline mode:
From 15223cfd3d81b8b5a031e7132fb2cf30deee4478 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Mon, 28 Jul 2025 12:04:59 +0200
Subject: [PATCH 09/15] reorder props sections in ShowBase documentation
---
docs/ShowBase.md | 54 ++++++++++++++++++++++++------------------------
1 file changed, 27 insertions(+), 27 deletions(-)
diff --git a/docs/ShowBase.md b/docs/ShowBase.md
index 7649675ee74..ee39a80f98a 100644
--- a/docs/ShowBase.md
+++ b/docs/ShowBase.md
@@ -108,33 +108,6 @@ const BookShow = () => (
```
{% endraw %}
-## `render`
-
-Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ShowContext` as argument.
-
-{% raw %}
-```jsx
-import { ShowBase, TextField, DateField, ReferenceField, WithRecord } from 'react-admin';
-
-const BookShow = () => (
- {
- if (isPending) {
- return Loading...
;
- }
-
- if (error) {
- return (
-
- {error.message}
-
- );
- }
- return {record.title}
;
- }}/>
-);
-```
-{% endraw %}
-
## `disableAuthentication`
By default, the `` component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the `disableAuthentication` prop to `true`.
@@ -253,6 +226,33 @@ The default `onError` function is:
}
```
+## `render`
+
+Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ShowContext` as argument.
+
+{% raw %}
+```jsx
+import { ShowBase, TextField, DateField, ReferenceField, WithRecord } from 'react-admin';
+
+const BookShow = () => (
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+ return {record.title}
;
+ }}/>
+);
+```
+{% endraw %}
+
## `resource`
By default, `` operates on the current `ResourceContext` (defined at the routing level), so under the `/posts/1/show` path, the `resource` prop will be `posts`. You may want to force a different resource. In this case, pass a custom `resource` prop, and it will override the `ResourceContext` value.
From 38af6380c2be12b4c80994756a8f6dc7c203db30 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Mon, 28 Jul 2025 12:24:12 +0200
Subject: [PATCH 10/15] Fix Show Offline story
---
packages/ra-ui-materialui/src/detail/Show.stories.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/detail/Show.stories.tsx b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
index 9463824f5a4..0826f766804 100644
--- a/packages/ra-ui-materialui/src/detail/Show.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
@@ -352,7 +352,7 @@ const CustomOffline = () => {
Offline.args = {
isOnline: true,
- offline: false,
+ offline: 'default',
};
Offline.argTypes = {
From 69c262f4afabd058eff3581fc6e04b66d95a9b9d Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Mon, 28 Jul 2025 15:57:04 +0200
Subject: [PATCH 11/15] Refactor rendered content in ShowBase
---
.../ra-core/src/controller/show/ShowBase.tsx | 38 +++++++++----------
1 file changed, 18 insertions(+), 20 deletions(-)
diff --git a/packages/ra-core/src/controller/show/ShowBase.tsx b/packages/ra-core/src/controller/show/ShowBase.tsx
index 019f4346333..91deecfbb88 100644
--- a/packages/ra-core/src/controller/show/ShowBase.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.tsx
@@ -41,9 +41,10 @@ import { useIsAuthPending } from '../../auth';
*/
export const ShowBase = ({
children,
- render,
+ disableAuthentication,
loading,
offline,
+ render,
...props
}: ShowBaseProps) => {
const controllerProps = useShowController(props);
@@ -61,29 +62,26 @@ export const ShowBase = ({
const { isPaused, record } = controllerProps;
+ const shouldRenderLoading =
+ isAuthPending &&
+ !disableAuthentication &&
+ loading !== false &&
+ loading !== undefined;
+
+ const shouldRenderOffline =
+ isPaused && !record && offline !== false && offline !== undefined;
+
return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
- {(() => {
- if (
- isAuthPending &&
- !props.disableAuthentication &&
- loading !== false &&
- loading !== undefined
- ) {
- return loading;
- }
- if (
- isPaused &&
- !record &&
- offline !== false &&
- offline !== undefined
- ) {
- return offline;
- }
- return render ? render(controllerProps) : children;
- })()}
+ {shouldRenderLoading
+ ? loading
+ : shouldRenderOffline
+ ? offline
+ : render
+ ? render(controllerProps)
+ : children}
);
From 60b6f9de112ea6870db1a90d2f982bf250a170b2 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Mon, 28 Jul 2025 16:43:23 +0200
Subject: [PATCH 12/15] Rename useIsOffine to useIsOffline
---
packages/ra-core/src/core/IsOffline.tsx | 4 ++--
packages/ra-core/src/core/useIsOffline.ts | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/ra-core/src/core/IsOffline.tsx b/packages/ra-core/src/core/IsOffline.tsx
index 3e0c3dca7c2..5e80c404b6d 100644
--- a/packages/ra-core/src/core/IsOffline.tsx
+++ b/packages/ra-core/src/core/IsOffline.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-import { useIsOffine } from './useIsOffline';
+import { useIsOffline } from './useIsOffline';
export const IsOffline = ({ children }: { children: React.ReactNode }) => {
- const isOffline = useIsOffine();
+ const isOffline = useIsOffline();
return isOffline ? children : null;
};
diff --git a/packages/ra-core/src/core/useIsOffline.ts b/packages/ra-core/src/core/useIsOffline.ts
index 6e54c89ef1d..77c11eabb8d 100644
--- a/packages/ra-core/src/core/useIsOffline.ts
+++ b/packages/ra-core/src/core/useIsOffline.ts
@@ -7,7 +7,7 @@ import { onlineManager } from '@tanstack/react-query';
* It returns true if the application is offline, false otherwise.
* @returns {boolean} - True if offline, false if online.
*/
-export const useIsOffine = () => {
+export const useIsOffline = () => {
const [isOnline, setIsOnline] = React.useState(onlineManager.isOnline());
React.useEffect(() => {
From c9c12d3af8a2ab4a27aa2d4c37bdf4cc4d11fc85 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Tue, 29 Jul 2025 17:29:06 +0200
Subject: [PATCH 13/15] Update docs/Show.md
Co-authored-by: Madeorsk
---
docs/Show.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/Show.md b/docs/Show.md
index ab4ceabe2f8..c325f20492d 100644
--- a/docs/Show.md
+++ b/docs/Show.md
@@ -292,7 +292,7 @@ export const PostShow = () => (
## `offline`
-By default, `` renders nothing when there is no connectivity and the record hasn't been cached yet. You can provide your own component via the `offline` prop:
+By default, `` renders the `` component when there is no connectivity and the record hasn't been cached yet. You can provide your own component via the `offline` prop:
```jsx
import { Show } from 'react-admin';
From 129c5e3176fc4a01e32166414dcda279cbe3ea53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?=
Date: Wed, 30 Jul 2025 06:50:52 +0200
Subject: [PATCH 14/15] Allow Offline to work outside of a ResourceContext
---
packages/ra-core/src/i18n/useResourceTranslation.ts | 9 +++++----
packages/ra-ui-materialui/src/Offline.tsx | 12 +++++-------
2 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/packages/ra-core/src/i18n/useResourceTranslation.ts b/packages/ra-core/src/i18n/useResourceTranslation.ts
index 0c0b9a271f2..753470e0689 100644
--- a/packages/ra-core/src/i18n/useResourceTranslation.ts
+++ b/packages/ra-core/src/i18n/useResourceTranslation.ts
@@ -13,17 +13,18 @@ export const useResourceTranslation = (
}
return translate(userText, { _: userText, ...options });
}
+ if (!resourceI18nKey) {
+ return translate(baseI18nKey, options);
+ }
- const translatedText = translate(resourceI18nKey, {
+ return translate(resourceI18nKey, {
...options,
_: translate(baseI18nKey, options),
});
-
- return translatedText;
};
export interface UseResourceTranslationOptions {
- resourceI18nKey: string;
+ resourceI18nKey?: string;
baseI18nKey: string;
userText?: ReactNode;
options?: Record;
diff --git a/packages/ra-ui-materialui/src/Offline.tsx b/packages/ra-ui-materialui/src/Offline.tsx
index 797f4a89e68..abc9b072047 100644
--- a/packages/ra-ui-materialui/src/Offline.tsx
+++ b/packages/ra-ui-materialui/src/Offline.tsx
@@ -17,17 +17,15 @@ export const Offline = (props: Offline) => {
const { icon, message: messageProp, variant = 'standard', ...rest } = props;
const resource = useResourceContext(props);
const getResourceLabel = useGetResourceLabel();
- if (!resource) {
- throw new Error(
- ' must be used inside a component or provided a resource prop'
- );
- }
+
const message = useResourceTranslation({
baseI18nKey: 'ra.notification.offline',
- resourceI18nKey: `resources.${resource}.notification.offline`,
+ resourceI18nKey: resource
+ ? `resources.${resource}.notification.offline`
+ : undefined,
userText: messageProp,
options: {
- name: getResourceLabel(resource, 0),
+ name: resource ? getResourceLabel(resource, 0) : undefined,
_: 'No connectivity. Could not fetch data.',
},
});
From e82301b3cff390373a0b050e06adf65346c00824 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?=
Date: Wed, 30 Jul 2025 06:51:06 +0200
Subject: [PATCH 15/15] Misc doc fixes
---
docs/Show.md | 5 ++++-
docs/ShowBase.md | 12 ++++++------
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/docs/Show.md b/docs/Show.md
index c325f20492d..5c43dddee4b 100644
--- a/docs/Show.md
+++ b/docs/Show.md
@@ -308,11 +308,14 @@ export const PostShow = () => (
```jsx
import { Show, IsOffline } from 'react-admin';
+import { Alert } from '@mui/material';
export const PostShow = () => (
No network. Could not load the post.}>
- No network. The post data may be outdated.
+
+ You are offline, the data may be outdated
+
...
diff --git a/docs/ShowBase.md b/docs/ShowBase.md
index ee39a80f98a..c0c84a6e398 100644
--- a/docs/ShowBase.md
+++ b/docs/ShowBase.md
@@ -113,7 +113,7 @@ const BookShow = () => (
By default, the `` component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the `disableAuthentication` prop to `true`.
```jsx
-import { ShowBase } from 'ra-core';
+import { ShowBase } from 'react-admin';
const PostShow = () => (
@@ -127,7 +127,7 @@ const PostShow = () => (
By default, `` deduces the identifier of the record to show from the URL path. So under the `/posts/123/show` path, the `id` prop will be `123`. You may want to force a different identifier. In this case, pass a custom `id` prop.
```jsx
-import { ShowBase } from 'ra-core';
+import { ShowBase } from 'react-admin';
export const PostShow = () => (
@@ -143,7 +143,7 @@ export const PostShow = () => (
By default, `` renders nothing while checking for authentication and permissions. You can provide your own component via the `loading` prop:
```jsx
-import { ShowBase } from 'ra-core';
+import { ShowBase } from 'react-admin';
export const PostShow = () => (
Checking for permissions...}>
@@ -157,7 +157,7 @@ export const PostShow = () => (
By default, `` renders nothing when there is no connectivity and the record hasn't been cached yet. You can provide your own component via the `offline` prop:
```jsx
-import { ShowBase } from 'ra-core';
+import { ShowBase } from 'react-admin';
export const PostShow = () => (
No network. Could not load the post.}>
@@ -169,7 +169,7 @@ export const PostShow = () => (
**Tip**: If the record is in the Tanstack Query cache but you want to warn the user that they may see an outdated version, you can use the `` component:
```jsx
-import { ShowBase, IsOffline } from 'ra-core';
+import { ShowBase, IsOffline } from 'react-admin';
export const PostShow = () => (
No network. Could not load the post.}>
@@ -258,7 +258,7 @@ const BookShow = () => (
By default, `` operates on the current `ResourceContext` (defined at the routing level), so under the `/posts/1/show` path, the `resource` prop will be `posts`. You may want to force a different resource. In this case, pass a custom `resource` prop, and it will override the `ResourceContext` value.
```jsx
-import { ShowBase } from 'ra-core';
+import { ShowBase } from 'react-admin';
export const UsersShow = () => (