- {offline}
+
+ {offline}
);
@@ -111,10 +105,6 @@ const Root = styled('div', {
[`& .${ShowClasses.noActions}`]: {
marginTop: '1em',
},
- [`& .${ShowClasses.offline}`]: {
- flexDirection: 'column',
- alignItems: 'unset',
- },
[`& .${ShowClasses.card}`]: {
flex: '1 1 auto',
},
From 84207e0989c447739ddeebb7577a87a8be1e1358 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Tue, 20 May 2025 16:11:52 +0200
Subject: [PATCH 37/39] Rename isOnline hook to useIsOffline
---
examples/simple/src/Layout.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/examples/simple/src/Layout.tsx b/examples/simple/src/Layout.tsx
index 491cae62269..beb9249aa00 100644
--- a/examples/simple/src/Layout.tsx
+++ b/examples/simple/src/Layout.tsx
@@ -13,12 +13,12 @@ import OfflineIcon from '@mui/icons-material/SignalWifiConnectedNoInternet4';
import '../assets/app.css';
const MyAppBar = () => {
- const isOnline = useIsOnline();
+ const isOffline = useIsOffine();
return (
- {!isOnline ? (
+ {isOffline ? (
(
>
);
-const useIsOnline = () => {
+const useIsOffine = () => {
const [isOnline, setIsOnline] = React.useState(onlineManager.isOnline());
React.useEffect(() => {
@@ -58,7 +58,7 @@ const useIsOnline = () => {
return onlineManager.subscribe(handleChange);
}, []);
- return isOnline;
+ return !isOnline;
};
/**
From 2e2e10cd058d130c7ec2ac2555956df3b7eecbc9 Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:51:30 +0200
Subject: [PATCH 38/39] Improve Offline component design for reference fields
and inputs
---
docs/DataProviders.md | 4 +++
examples/simple/src/comments/CommentList.tsx | 4 ++-
examples/simple/src/posts/PostEdit.tsx | 19 ++++++++++++
examples/simple/src/posts/PostShow.tsx | 30 ++++++++++++++++++-
examples/simple/src/tags/TagList.tsx | 23 ++++++++++----
packages/ra-ui-materialui/src/Offline.tsx | 29 +++++++++++++++---
.../src/field/ReferenceField.tsx | 2 +-
.../src/field/ReferenceManyCount.tsx | 2 +-
.../src/field/ReferenceOneField.tsx | 2 +-
.../src/input/ReferenceArrayInput.tsx | 8 +++--
.../src/list/SingleFieldList.tsx | 2 +-
11 files changed, 108 insertions(+), 17 deletions(-)
diff --git a/docs/DataProviders.md b/docs/DataProviders.md
index e05009063ec..4bf666a7425 100644
--- a/docs/DataProviders.md
+++ b/docs/DataProviders.md
@@ -945,6 +945,10 @@ export const App = () => (
```
{% endraw %}
+This is enough to make all the standard react-admin features support offline scenarios.
+
+## Adding Offline Support To Custom Mutations
+
If you have [custom mutations](./Actions.md#calling-custom-methods) on your dataProvider, you can enable offline support for them too. For instance, if your `dataProvider` exposes a `banUser()` method:
```ts
diff --git a/examples/simple/src/comments/CommentList.tsx b/examples/simple/src/comments/CommentList.tsx
index cdc6bb02d9d..23b4f365f20 100644
--- a/examples/simple/src/comments/CommentList.tsx
+++ b/examples/simple/src/comments/CommentList.tsx
@@ -30,6 +30,7 @@ import {
useListContext,
useTranslate,
Exporter,
+ Offline,
} from 'react-admin';
const commentFilters = [
@@ -63,9 +64,10 @@ const exporter: Exporter = (records, fetchRelatedRecords) =>
});
const CommentGrid = () => {
- const { data } = useListContext();
+ const { data, isPaused, isPlaceholderData } = useListContext();
const translate = useTranslate();
+ if (isPaused && (data == null || isPlaceholderData)) return ;
if (!data) return null;
return (
diff --git a/examples/simple/src/posts/PostEdit.tsx b/examples/simple/src/posts/PostEdit.tsx
index e45a2c2b736..a88acfc5e3a 100644
--- a/examples/simple/src/posts/PostEdit.tsx
+++ b/examples/simple/src/posts/PostEdit.tsx
@@ -31,6 +31,7 @@ import {
useCreateSuggestionContext,
EditActionsProps,
CanAccess,
+ Translate,
} from 'react-admin';
import {
Box,
@@ -40,7 +41,9 @@ import {
DialogActions,
DialogContent,
TextField as MuiTextField,
+ Tooltip,
} from '@mui/material';
+import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
import PostTitle from './PostTitle';
import TagReferenceInput from './TagReferenceInput';
@@ -229,6 +232,22 @@ const PostEdit = () => (
reference="comments"
target="post_id"
sx={{ lineHeight: 'inherit' }}
+ offline={
+
+ }
+ >
+ theme.spacing(0.5),
+ }}
+ />
+
+ }
/>
}
>
diff --git a/examples/simple/src/posts/PostShow.tsx b/examples/simple/src/posts/PostShow.tsx
index e3e96c84656..0921a591239 100644
--- a/examples/simple/src/posts/PostShow.tsx
+++ b/examples/simple/src/posts/PostShow.tsx
@@ -23,7 +23,10 @@ import {
useShowController,
useLocaleState,
useRecordContext,
+ Translate,
} from 'react-admin';
+import { Tooltip } from '@mui/material';
+import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
import PostTitle from './PostTitle';
const CreateRelatedComment = () => {
@@ -112,11 +115,36 @@ const PostShow = () => {
span': {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ }}
count={
+ }
+ >
+
+ theme.spacing(0.5),
+ }}
+ />
+
+ }
/>
}
>
diff --git a/examples/simple/src/tags/TagList.tsx b/examples/simple/src/tags/TagList.tsx
index bf3dd443ee5..ff2329705f4 100644
--- a/examples/simple/src/tags/TagList.tsx
+++ b/examples/simple/src/tags/TagList.tsx
@@ -6,6 +6,7 @@ import {
useListContext,
EditButton,
Title,
+ Offline,
} from 'react-admin';
import {
Box,
@@ -25,15 +26,27 @@ const TagList = () => (
-
-
-
-
-
+
);
+const TagListView = () => {
+ const { data, isPaused } = useListContext();
+
+ if (isPaused && data == null) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
const Tree = () => {
const { data, defaultTitle } = useListContext();
const [openChildren, setOpenChildren] = useState([]);
diff --git a/packages/ra-ui-materialui/src/Offline.tsx b/packages/ra-ui-materialui/src/Offline.tsx
index 953fad29111..d80766bed7a 100644
--- a/packages/ra-ui-materialui/src/Offline.tsx
+++ b/packages/ra-ui-materialui/src/Offline.tsx
@@ -7,9 +7,10 @@ import {
Typography,
} from '@mui/material';
import { useGetResourceLabel, useResourceContext, useTranslate } from 'ra-core';
+import clsx from 'clsx';
export const Offline = (props: Offline) => {
- const { message: messageProp } = props;
+ const { icon, message: messageProp, variant = 'standard', ...rest } = props;
const translate = useTranslate();
const resource = useResourceContext(props);
const getResourceLabel = useGetResourceLabel();
@@ -31,23 +32,43 @@ export const Offline = (props: Offline) => {
}
);
return (
-
+
{message}
);
};
-export interface Offline extends AlertProps {
+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 {
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
index 1a12b899e29..23d3c4fe9aa 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
@@ -105,7 +105,7 @@ export interface ReferenceFieldProps<
// useful to prevent click bubbling in a datagrid with rowClick
const stopPropagation = e => e.stopPropagation();
-const defaultOffline = ;
+const defaultOffline = ;
export const ReferenceFieldView = <
RecordType extends Record = Record,
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx
index a31da69b352..25bb45d516b 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx
@@ -21,7 +21,7 @@ import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
import { Link } from '../Link';
import { Offline } from '../Offline';
-const defaultOffline = ;
+const defaultOffline = ;
/**
* Fetch and render the number of records related to the current one
diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
index 5079bb547f0..929082914df 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
@@ -19,7 +19,7 @@ import { FieldProps } from './types';
import { ReferenceFieldView } from './ReferenceField';
import { Offline } from '../Offline';
-const defaultOffline = ;
+const defaultOffline = ;
/**
* Render the related record in a one-to-one relationship
diff --git a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx
index 5d69ad47451..4049e3deca5 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx
@@ -101,8 +101,12 @@ export const ReferenceArrayInput = (props: ReferenceArrayInputProps) => {
return isPaused && allChoices == null ? (
offline ?? (
-
-
+
+
)
) : (
diff --git a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
index a82d2acb198..c85b3a865ef 100644
--- a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
+++ b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
@@ -209,4 +209,4 @@ declare module '@mui/material/styles' {
}
}
-const DefaultOffline = ;
+const DefaultOffline = ;
From 8452c4f966615d37991647d332f65ec4bd23615c Mon Sep 17 00:00:00 2001
From: Gildas <1122076+djhi@users.noreply.github.com>
Date: Fri, 6 Jun 2025 16:28:11 +0200
Subject: [PATCH 39/39] Make sure users know about pending operations
---
examples/simple/src/Layout.tsx | 16 +----
packages/ra-core/src/core/index.ts | 1 +
packages/ra-core/src/core/useIsOffine.ts | 21 ++++++
packages/ra-language-english/src/index.ts | 2 +
packages/ra-language-french/src/index.ts | 2 +
.../src/layout/LoadingIndicator.tsx | 67 +++++++++++++++----
6 files changed, 82 insertions(+), 27 deletions(-)
create mode 100644 packages/ra-core/src/core/useIsOffine.ts
diff --git a/examples/simple/src/Layout.tsx b/examples/simple/src/Layout.tsx
index beb9249aa00..498aa61d1aa 100644
--- a/examples/simple/src/Layout.tsx
+++ b/examples/simple/src/Layout.tsx
@@ -6,8 +6,9 @@ import {
InspectorButton,
TitlePortal,
useNotify,
+ useIsOffine,
} from 'react-admin';
-import { onlineManager, useQueryClient } from '@tanstack/react-query';
+import { useQueryClient } from '@tanstack/react-query';
import { Stack, Tooltip } from '@mui/material';
import OfflineIcon from '@mui/icons-material/SignalWifiConnectedNoInternet4';
import '../assets/app.css';
@@ -48,19 +49,6 @@ export const MyLayout = ({ children }) => (
>
);
-const useIsOffine = () => {
- const [isOnline, setIsOnline] = React.useState(onlineManager.isOnline());
-
- React.useEffect(() => {
- const handleChange = () => {
- setIsOnline(onlineManager.isOnline());
- };
- return onlineManager.subscribe(handleChange);
- }, []);
-
- return !isOnline;
-};
-
/**
* When react-query resumes persisted mutations through their default functions (provided in the getOfflineFirstQueryClient file) after the browser tab
* has been closed, it cannot handle their side effects unless we set up some defaults. In order to leverage the react-admin notification system
diff --git a/packages/ra-core/src/core/index.ts b/packages/ra-core/src/core/index.ts
index fb7b21aa759..543c0ca85a8 100644
--- a/packages/ra-core/src/core/index.ts
+++ b/packages/ra-core/src/core/index.ts
@@ -15,6 +15,7 @@ export * from './SourceContext';
export * from './useFirstResourceWithListAccess';
export * from './useGetResourceLabel';
export * from './useGetRecordRepresentation';
+export * from './useIsOffine';
export * from './useResourceDefinitionContext';
export * from './useResourceContext';
export * from './useResourceDefinition';
diff --git a/packages/ra-core/src/core/useIsOffine.ts b/packages/ra-core/src/core/useIsOffine.ts
new file mode 100644
index 00000000000..6e54c89ef1d
--- /dev/null
+++ b/packages/ra-core/src/core/useIsOffine.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;
+};
diff --git a/packages/ra-language-english/src/index.ts b/packages/ra-language-english/src/index.ts
index 3e0f0370f0e..389d4f150d2 100644
--- a/packages/ra-language-english/src/index.ts
+++ b/packages/ra-language-english/src/index.ts
@@ -174,6 +174,8 @@ const englishMessages: TranslationMessages = {
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.',
+ pending_operations:
+ 'There is a pending operation due to network not being available |||| There are %{smart_count} pending operations due to network not being available',
},
validation: {
required: 'Required',
diff --git a/packages/ra-language-french/src/index.ts b/packages/ra-language-french/src/index.ts
index 90a3e8de19a..06e14d7f8b8 100644
--- a/packages/ra-language-french/src/index.ts
+++ b/packages/ra-language-french/src/index.ts
@@ -182,6 +182,8 @@ const frenchMessages: TranslationMessages = {
"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.',
+ pending_operations:
+ 'Il y a une opération en attente due à un problème de réseau |||| Il y a %{smart_count} opérations en attente dues à un problème de réseau',
},
validation: {
required: 'Ce champ est requis',
diff --git a/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx b/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx
index 99007ab5bf7..385d4eb4859 100644
--- a/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx
+++ b/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx
@@ -9,9 +9,11 @@ import {
} from '@mui/material/styles';
import clsx from 'clsx';
import CircularProgress from '@mui/material/CircularProgress';
-import { useLoading } from 'ra-core';
+import { Translate, useIsOffine, useLoading } from 'ra-core';
import { RefreshIconButton, type RefreshIconButtonProps } from '../button';
+import { Badge, Tooltip } from '@mui/material';
+import { useMutationState } from '@tanstack/react-query';
export const LoadingIndicator = (inProps: LoadingIndicatorProps) => {
const props = useThemeProps({
@@ -20,6 +22,12 @@ export const LoadingIndicator = (inProps: LoadingIndicatorProps) => {
});
const { className, onClick, sx, ...rest } = props;
const loading = useLoading();
+ const isOffline = useIsOffine();
+ const pendingMutations = useMutationState({
+ filters: {
+ status: 'pending',
+ },
+ });
const theme = useTheme();
return (
@@ -30,18 +38,51 @@ export const LoadingIndicator = (inProps: LoadingIndicatorProps) => {
}`}
onClick={onClick}
/>
- {loading && (
-
- )}
+ {loading ? (
+ isOffline ? (
+
+ {pendingMutations.length > 1
+ ? `There are ${pendingMutations.length} pending
+ operations due to network not being available`
+ : `There is a pending operation due to network not being available`}
+
+ }
+ >
+
+
+
+
+ ) : (
+
+ )
+ ) : null}
);
};