diff --git a/cypress/e2e/edit.cy.js b/cypress/e2e/edit.cy.js
index 9f85eecb013..b53aabaf5ba 100644
--- a/cypress/e2e/edit.cy.js
+++ b/cypress/e2e/edit.cy.js
@@ -47,7 +47,11 @@ describe('Edit Page', () => {
it('should redirect to list page after edit success', () => {
// For some unknown reason, the click on submit didn't work in cypress
// so we submit with enter
- EditPostPage.setInputValue('input', 'title', 'Lorem Ipsum{enter}');
+ EditPostPage.setInputValue(
+ 'input',
+ 'title',
+ 'Lorem Ipsum again{enter}'
+ );
cy.url().should('match', /\/#\/posts$/);
});
diff --git a/cypress/support/CreatePage.js b/cypress/support/CreatePage.js
index 138410b9721..65ef03c1dc7 100644
--- a/cypress/support/CreatePage.js
+++ b/cypress/support/CreatePage.js
@@ -11,13 +11,14 @@ export default url => ({
inputs: `.ra-input`,
richTextInputError: '.create-page .ra-rich-text-input-error',
snackbar: 'div[role="alert"]',
- submitButton: ".create-page div[role='toolbar'] button[type='submit']",
+ submitButton:
+ ".create-page div[role='toolbar'] div:first-child button[type='submit']",
submitAndShowButton:
- ".create-page form div[role='toolbar'] button[type='button']:nth-child(2)",
+ ".create-page form div[role='toolbar'] div:first-child button[type='button']:nth-child(2)",
submitAndAddButton:
- ".create-page form div[role='toolbar'] button[type='button']:nth-child(3)",
+ ".create-page form div[role='toolbar'] div:first-child button[type='button']:nth-child(3)",
submitCommentable:
- ".create-page form div[role='toolbar'] button[type='button']:last-child",
+ ".create-page form div[role='toolbar'] div:first-child button[type='button']:last-child",
descInput: '.ProseMirror',
tab: index => `.form-tab:nth-of-type(${index})`,
title: '#react-admin-title',
diff --git a/docs/DataProviders.md b/docs/DataProviders.md
index c1c618d8d69..f6cacc58509 100644
--- a/docs/DataProviders.md
+++ b/docs/DataProviders.md
@@ -888,3 +888,170 @@ export default App;
```
**Tip**: This example uses the function version of `setState` (`setDataProvider(() => dataProvider)`) instead of the more classic version (`setDataProvider(dataProvider)`). This is because some legacy Data Providers are actually functions, and `setState` would call them immediately on mount.
+
+## Offline Support
+
+React-admin supports offline/local-first applications. To enable this feature, install the following react-query packages:
+
+```sh
+yarn add @tanstack/react-query-persist-client @tanstack/query-sync-storage-persister
+```
+
+Then, register default functions for react-admin mutations on the `QueryClient` to enable resumable mutations (mutations triggered while offline). React-admin provides the `addOfflineSupportToQueryClient` function for this:
+
+```ts
+// in src/queryClient.ts
+import { addOfflineSupportToQueryClient } from 'react-admin';
+import { QueryClient } from '@tanstack/react-query';
+import { dataProvider } from './dataProvider';
+
+const baseQueryClient = new QueryClient();
+
+export const queryClient = addOfflineSupportToQueryClient({
+ queryClient: baseQueryClient,
+ dataProvider,
+ resources: ['posts', 'comments'],
+});
+```
+
+Then, wrap your `` inside a [``](https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#persistqueryclientprovider):
+
+{% raw %}
+```tsx
+// in src/App.tsx
+import { Admin, Resource } from 'react-admin';
+import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
+import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
+import { queryClient } from './queryClient';
+import { dataProvider } from './dataProvider';
+import { posts } from './posts';
+import { comments } from './comments';
+
+const localStoragePersister = createSyncStoragePersister({
+ storage: window.localStorage,
+});
+
+export const App = () => (
+ {
+ // resume mutations after initial restore from localStorage is successful
+ queryClient.resumePausedMutations();
+ }}
+ >
+
+
+
+
+
+)
+```
+{% 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
+const dataProvider = {
+ getList: /* ... */,
+ getOne: /* ... */,
+ getMany: /* ... */,
+ getManyReference: /* ... */,
+ create: /* ... */,
+ update: /* ... */,
+ updateMany: /* ... */,
+ delete: /* ... */,
+ deleteMany: /* ... */,
+ banUser: (userId: string) => {
+ return fetch(`/api/user/${userId}/ban`, { method: 'POST' })
+ .then(response => response.json());
+ },
+}
+
+export type MyDataProvider = DataProvider & {
+ banUser: (userId: string) => Promise<{ data: RaRecord }>
+}
+```
+
+First, you must set a `mutationKey` for this mutation:
+
+{% raw %}
+```tsx
+const BanUserButton = ({ userId }: { userId: string }) => {
+ const dataProvider = useDataProvider();
+ const { mutate, isPending } = useMutation({
+ mutationKey: 'banUser'
+ mutationFn: (userId) => dataProvider.banUser(userId)
+ });
+ return
);
diff --git a/examples/simple/src/posts/PostCreate.tsx b/examples/simple/src/posts/PostCreate.tsx
index 693953892c9..9db6b78e240 100644
--- a/examples/simple/src/posts/PostCreate.tsx
+++ b/examples/simple/src/posts/PostCreate.tsx
@@ -26,63 +26,113 @@ import {
useCreate,
useCreateSuggestionContext,
CanAccess,
+ MutationMode,
} from 'react-admin';
import { useFormContext, useWatch } from 'react-hook-form';
-import { Button, Dialog, DialogActions, DialogContent } from '@mui/material';
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ Menu,
+ MenuItem,
+ Stack,
+} from '@mui/material';
+import MoreButton from '@mui/icons-material/MoreVert';
+
+// Client side id generation. We start from 100 to avoid querying the post list to get the next id as we
+// may be offline and accessing this page directly (without going through the list page first) which would
+// be possible if the app was also a PWA.
+// We only do that for optimistic and undoable modes in order to not break any existing tests that expect
+// the id to be generated by the server (e.g. by FakeRest).
+let next_id = 100;
+const getNewId = (mutationMode: MutationMode) => {
+ const id = mutationMode === 'pessimistic' ? undefined : next_id++;
+ return id;
+};
-const PostCreateToolbar = () => {
+const PostCreateToolbar = ({
+ mutationMode,
+ setMutationMode,
+}: {
+ mutationMode: MutationMode;
+ setMutationMode: (mutationMode: MutationMode) => void;
+}) => {
const notify = useNotify();
const redirect = useRedirect();
const { reset } = useFormContext();
return (
-
-
- {
- notify('resources.posts.notifications.created', {
- type: 'info',
- messageArgs: { smart_count: 1 },
- });
- redirect('show', 'posts', data.id);
- },
- }}
- sx={{ display: { xs: 'none', sm: 'flex' } }}
- />
- {
- reset();
- window.scrollTo(0, 0);
- notify('resources.posts.notifications.created', {
- type: 'info',
- messageArgs: { smart_count: 1 },
- });
- },
- }}
- />
- {
- notify('resources.posts.notifications.created', {
- type: 'info',
- messageArgs: { smart_count: 1 },
- });
- redirect('show', 'posts', data.id);
- },
- }}
- transform={data => ({ ...data, average_note: 10 })}
- sx={{ display: { xs: 'none', sm: 'flex' } }}
+
+
+
+ {
+ notify('resources.posts.notifications.created', {
+ type: 'info',
+ messageArgs: { smart_count: 1 },
+ undoable: mutationMode === 'undoable',
+ });
+ redirect('show', 'posts', data.id);
+ },
+ }}
+ transform={data => ({
+ ...data,
+ id: getNewId(mutationMode),
+ average_note: 10,
+ })}
+ sx={{ display: { xs: 'none', sm: 'flex' } }}
+ />
+ {
+ reset();
+ window.scrollTo(0, 0);
+ notify('resources.posts.notifications.created', {
+ type: 'info',
+ messageArgs: { smart_count: 1 },
+ undoable: mutationMode === 'undoable',
+ });
+ },
+ }}
+ transform={data => ({
+ ...data,
+ id: getNewId(mutationMode),
+ average_note: 10,
+ })}
+ />
+ {
+ notify('resources.posts.notifications.created', {
+ type: 'info',
+ messageArgs: { smart_count: 1 },
+ undoable: mutationMode === 'undoable',
+ });
+ redirect('show', 'posts', data.id);
+ },
+ }}
+ transform={data => ({
+ ...data,
+ id: getNewId(mutationMode),
+ average_note: 10,
+ })}
+ sx={{ display: { xs: 'none', sm: 'flex' } }}
+ />
+
+
);
@@ -94,6 +144,7 @@ const backlinksDefaultValue = [
url: 'http://google.com',
},
];
+
const PostCreate = () => {
const defaultValues = useMemo(
() => ({
@@ -101,11 +152,22 @@ const PostCreate = () => {
}),
[]
);
+ const [mutationMode, setMutationMode] =
+ React.useState('pessimistic');
const dateDefaultValue = useMemo(() => new Date(), []);
return (
-
+ ({ ...data, id: getNewId(mutationMode) })}
+ >
}
+ toolbar={
+
+ }
defaultValues={defaultValues}
sx={{ maxWidth: { md: 'auto', lg: '30em' } }}
>
@@ -260,3 +322,58 @@ const CreateUser = () => {
);
};
+
+const MutationModes = ['pessimistic', 'optimistic', 'undoable'] as const;
+const MutationModesSelector = (props: {
+ mutationMode: MutationMode;
+ setMutationMode: (mode: MutationMode) => void;
+}) => {
+ const { setMutationMode, mutationMode } = props;
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const open = Boolean(anchorEl);
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const handleMenuItemClick = (mutationMode: MutationMode) => {
+ setMutationMode(mutationMode);
+ };
+
+ return (
+ <>
+ }
+ >
+ {mutationMode}
+
+
+ >
+ );
+};
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-core/src/auth/useAuthState.ts b/packages/ra-core/src/auth/useAuthState.ts
index aa75ef8d5cd..868da334f94 100644
--- a/packages/ra-core/src/auth/useAuthState.ts
+++ b/packages/ra-core/src/auth/useAuthState.ts
@@ -150,7 +150,7 @@ const useAuthState = (
return authProvider != null
? result
- : (noAuthProviderQueryResult as UseAuthStateResult);
+ : (noAuthProviderQueryResult as unknown as UseAuthStateResult);
};
type UseAuthStateOptions = Omit<
diff --git a/packages/ra-core/src/auth/useCanAccess.ts b/packages/ra-core/src/auth/useCanAccess.ts
index 63db7906666..ead40b597dc 100644
--- a/packages/ra-core/src/auth/useCanAccess.ts
+++ b/packages/ra-core/src/auth/useCanAccess.ts
@@ -93,7 +93,7 @@ export const useCanAccess = <
return authProviderHasCanAccess
? result
- : (emptyQueryObserverResult as UseCanAccessResult);
+ : (emptyQueryObserverResult as unknown as UseCanAccessResult);
};
const emptyQueryObserverResult = {
diff --git a/packages/ra-core/src/auth/usePermissions.ts b/packages/ra-core/src/auth/usePermissions.ts
index eddf21142c1..9ef83079a08 100644
--- a/packages/ra-core/src/auth/usePermissions.ts
+++ b/packages/ra-core/src/auth/usePermissions.ts
@@ -108,7 +108,10 @@ const usePermissions = (
);
return !authProvider || !authProvider.getPermissions
- ? (fakeQueryResult as UsePermissionsResult)
+ ? (fakeQueryResult as unknown as UsePermissionsResult<
+ PermissionsType,
+ ErrorType
+ >)
: result;
};
diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
index 15b72f8f8d4..bb49bfbcdff 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx
@@ -11,7 +11,7 @@ import { useUnselect } from '../../controller';
import { useRedirect, RedirectionSideEffect } from '../../routing';
import { useNotify } from '../../notification';
import { RaRecord, MutationMode, DeleteParams } from '../../types';
-import { useResourceContext } from '../../core';
+import { useIsOffine, useResourceContext } from '../../core';
import { useTranslate } from '../../i18n';
/**
@@ -90,6 +90,7 @@ const useDeleteWithConfirmController = <
const redirect = useRedirect();
const translate = useTranslate();
+ const isOffline = useIsOffine();
const [deleteOne, { isPending }] = useDelete(
resource,
undefined,
@@ -97,15 +98,23 @@ const useDeleteWithConfirmController = <
onSuccess: () => {
setOpen(false);
notify(
- successMessage ??
- `resources.${resource}.notifications.deleted`,
+ successMessage != null
+ ? successMessage
+ : isOffline
+ ? `resources.${resource}.notifications.pending_delete`
+ : `resources.${resource}.notifications.deleted`,
{
type: 'info',
messageArgs: {
smart_count: 1,
- _: translate('ra.notification.deleted', {
- smart_count: 1,
- }),
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_delete'
+ : 'ra.notification.deleted',
+ {
+ smart_count: 1,
+ }
+ ),
},
undoable: mutationMode === 'undoable',
}
diff --git a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
index f04445c4b86..2cee6be3cf0 100644
--- a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
+++ b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx
@@ -6,7 +6,7 @@ import { useUnselect } from '../../controller';
import { useRedirect, RedirectionSideEffect } from '../../routing';
import { useNotify } from '../../notification';
import { RaRecord, DeleteParams } from '../../types';
-import { useResourceContext } from '../../core';
+import { useIsOffine, useResourceContext } from '../../core';
import { useTranslate } from '../../i18n';
/**
@@ -63,21 +63,31 @@ const useDeleteWithUndoController = <
const unselect = useUnselect(resource);
const redirect = useRedirect();
const translate = useTranslate();
+
+ const isOffline = useIsOffine();
const [deleteOne, { isPending }] = useDelete(
resource,
undefined,
{
onSuccess: () => {
notify(
- successMessage ??
- `resources.${resource}.notifications.deleted`,
+ successMessage != null
+ ? successMessage
+ : isOffline
+ ? `resources.${resource}.notifications.pending_delete`
+ : `resources.${resource}.notifications.deleted`,
{
type: 'info',
messageArgs: {
smart_count: 1,
- _: translate('ra.notification.deleted', {
- smart_count: 1,
- }),
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_delete'
+ : 'ra.notification.deleted',
+ {
+ smart_count: 1,
+ }
+ ),
},
undoable: true,
}
diff --git a/packages/ra-core/src/controller/create/useCreateController.ts b/packages/ra-core/src/controller/create/useCreateController.ts
index 9ee07aee7f8..e8e31d094e8 100644
--- a/packages/ra-core/src/controller/create/useCreateController.ts
+++ b/packages/ra-core/src/controller/create/useCreateController.ts
@@ -20,6 +20,7 @@ import {
useResourceContext,
useResourceDefinition,
useGetResourceLabel,
+ useIsOffine,
} from '../../core';
/**
@@ -87,6 +88,7 @@ export const useCreateController = <
unregisterMutationMiddleware,
} = useMutationMiddlewares();
+ const isOffline = useIsOffine();
const [create, { isPending: saving }] = useCreate<
RecordType,
MutationOptionsError,
@@ -96,16 +98,26 @@ export const useCreateController = <
if (onSuccess) {
return onSuccess(data, variables, context);
}
- notify(`resources.${resource}.notifications.created`, {
- type: 'info',
- messageArgs: {
- smart_count: 1,
- _: translate(`ra.notification.created`, {
+ notify(
+ isOffline
+ ? `resources.${resource}.notifications.pending_create`
+ : `resources.${resource}.notifications.created`,
+ {
+ type: 'info',
+ messageArgs: {
smart_count: 1,
- }),
- },
- undoable: mutationMode === 'undoable',
- });
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_create'
+ : 'ra.notification.created',
+ {
+ smart_count: 1,
+ }
+ ),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
redirect(finalRedirectTo, resource, data.id, data);
},
onError: (error: MutationOptionsError, variables, context) => {
diff --git a/packages/ra-core/src/controller/edit/useEditController.ts b/packages/ra-core/src/controller/edit/useEditController.ts
index d345d5227ab..57791912da1 100644
--- a/packages/ra-core/src/controller/edit/useEditController.ts
+++ b/packages/ra-core/src/controller/edit/useEditController.ts
@@ -23,6 +23,7 @@ import {
useResourceContext,
useGetResourceLabel,
useGetRecordRepresentation,
+ useIsOffine,
} from '../../core';
import {
SaveContextValue,
@@ -113,6 +114,7 @@ export const useEditController = <
error,
isLoading,
isFetching,
+ isPaused,
isPending,
refetch,
} = useGetOne(
@@ -173,6 +175,7 @@ export const useEditController = <
const recordCached = { id, previousData: record };
+ const isOffline = useIsOffine();
const [update, { isPending: saving }] = useUpdate(
resource,
recordCached,
@@ -181,16 +184,26 @@ export const useEditController = <
if (onSuccess) {
return onSuccess(data, variables, context);
}
- notify(`resources.${resource}.notifications.updated`, {
- type: 'info',
- messageArgs: {
- smart_count: 1,
- _: translate('ra.notification.updated', {
+ notify(
+ isOffline
+ ? `resources.${resource}.notifications.pending_update`
+ : `resources.${resource}.notifications.updated`,
+ {
+ type: 'info',
+ messageArgs: {
smart_count: 1,
- }),
- },
- undoable: mutationMode === 'undoable',
- });
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_update'
+ : 'ra.notification.updated',
+ {
+ smart_count: 1,
+ }
+ ),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
redirect(redirectTo, resource, data.id, data);
},
onError: (error, variables, context) => {
@@ -289,6 +302,7 @@ export const useEditController = <
error,
isFetching,
isLoading,
+ isPaused,
isPending,
mutationMode,
record,
@@ -324,6 +338,7 @@ export interface EditControllerBaseResult
defaultTitle?: string;
isFetching: boolean;
isLoading: boolean;
+ isPaused: boolean;
refetch: UseGetOneHookValue['refetch'];
redirect: RedirectionSideEffect;
resource: string;
diff --git a/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts
index 73d4660b456..a657a681ba1 100644
--- a/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts
+++ b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts
@@ -76,32 +76,40 @@ export const useReferenceArrayFieldController = <
const { meta, ...otherQueryOptions } = queryOptions;
const ids = Array.isArray(value) ? value : emptyArray;
- const { data, error, isLoading, isFetching, isPending, refetch } =
- useGetManyAggregate(
- reference,
- { ids, meta },
- {
- onError: error =>
- notify(
- typeof error === 'string'
- ? error
- : (error as Error)?.message ||
- 'ra.notification.http_error',
- {
- type: 'error',
- messageArgs: {
- _:
- typeof error === 'string'
- ? error
- : (error as Error)?.message
- ? (error as Error).message
- : undefined,
- },
- }
- ),
- ...otherQueryOptions,
- }
- );
+ const {
+ data,
+ error,
+ isLoading,
+ isFetching,
+ isPaused,
+ isPending,
+ isPlaceholderData,
+ refetch,
+ } = useGetManyAggregate(
+ reference,
+ { ids, meta },
+ {
+ onError: error =>
+ notify(
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message ||
+ 'ra.notification.http_error',
+ {
+ type: 'error',
+ messageArgs: {
+ _:
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message
+ ? (error as Error).message
+ : undefined,
+ },
+ }
+ ),
+ ...otherQueryOptions,
+ }
+ );
const listProps = useList({
data,
@@ -109,7 +117,9 @@ export const useReferenceArrayFieldController = <
filter,
isFetching,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
page,
perPage,
sort,
diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts
index e74a3a3ceef..9d64ed01d30 100644
--- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts
+++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts
@@ -168,7 +168,9 @@ export const useReferenceManyFieldController = <
error,
isFetching,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
refetch,
} = useGetManyReference(
reference,
@@ -270,7 +272,9 @@ export const useReferenceManyFieldController = <
hideFilter,
isFetching,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
diff --git a/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx b/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx
index b4ab292201d..4bac19e1733 100644
--- a/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx
+++ b/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx
@@ -69,40 +69,48 @@ export const useReferenceOneFieldController = <
const notify = useNotify();
const { meta, ...otherQueryOptions } = queryOptions;
- const { data, error, isFetching, isLoading, isPending, refetch } =
- useGetManyReference(
- reference,
- {
- target,
- id: get(record, source),
- pagination: { page: 1, perPage: 1 },
- sort,
- filter,
- meta,
- },
- {
- enabled: !!record,
- onError: error =>
- notify(
- typeof error === 'string'
- ? error
- : (error as Error).message ||
- 'ra.notification.http_error',
- {
- type: 'error',
- messageArgs: {
- _:
- typeof error === 'string'
- ? error
- : (error as Error)?.message
- ? (error as Error).message
- : undefined,
- },
- }
- ),
- ...otherQueryOptions,
- }
- );
+ const {
+ data,
+ error,
+ isFetching,
+ isLoading,
+ isPending,
+ isPaused,
+ isPlaceholderData,
+ refetch,
+ } = useGetManyReference(
+ reference,
+ {
+ target,
+ id: get(record, source),
+ pagination: { page: 1, perPage: 1 },
+ sort,
+ filter,
+ meta,
+ },
+ {
+ enabled: !!record,
+ onError: error =>
+ notify(
+ typeof error === 'string'
+ ? error
+ : (error as Error).message ||
+ 'ra.notification.http_error',
+ {
+ type: 'error',
+ messageArgs: {
+ _:
+ typeof error === 'string'
+ ? error
+ : (error as Error)?.message
+ ? (error as Error).message
+ : undefined,
+ },
+ }
+ ),
+ ...otherQueryOptions,
+ }
+ );
return {
referenceRecord: data ? data[0] : undefined,
@@ -110,6 +118,8 @@ export const useReferenceOneFieldController = <
isFetching,
isLoading,
isPending,
+ isPaused,
+ isPlaceholderData,
refetch,
};
};
diff --git a/packages/ra-core/src/controller/input/ReferenceInputBase.tsx b/packages/ra-core/src/controller/input/ReferenceInputBase.tsx
index d0425f2a18b..9c0ef85af64 100644
--- a/packages/ra-core/src/controller/input/ReferenceInputBase.tsx
+++ b/packages/ra-core/src/controller/input/ReferenceInputBase.tsx
@@ -70,6 +70,7 @@ export const ReferenceInputBase = (props: ReferenceInputBaseProps) => {
reference,
sort = { field: 'id', order: 'DESC' },
filter = {},
+ offline = null,
} = props;
const controllerProps = useReferenceInputController({
@@ -78,7 +79,11 @@ export const ReferenceInputBase = (props: ReferenceInputBaseProps) => {
filter,
});
- return (
+ const { isPaused, allChoices } = controllerProps;
+
+ return isPaused && allChoices == null ? (
+ offline
+ ) : (
{children}
@@ -91,4 +96,5 @@ export interface ReferenceInputBaseProps
extends InputProps,
UseReferenceInputControllerParams {
children?: ReactNode;
+ offline?: ReactNode;
}
diff --git a/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts b/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts
index 4dd10aac617..fa883c58b21 100644
--- a/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts
+++ b/packages/ra-core/src/controller/input/useReferenceArrayInputController.ts
@@ -61,7 +61,9 @@ export const useReferenceArrayInputController = <
error: errorGetMany,
isLoading: isLoadingGetMany,
isFetching: isFetchingGetMany,
+ isPaused: isPausedGetMany,
isPending: isPendingGetMany,
+ isPlaceholderData: isPlaceholderDataGetMany,
refetch: refetchGetMany,
} = useGetManyAggregate(
reference,
@@ -99,7 +101,9 @@ export const useReferenceArrayInputController = <
error: errorGetList,
isLoading: isLoadingGetList,
isFetching: isFetchingGetList,
+ isPaused: isPausedGetList,
isPending: isPendingGetList,
+ isPlaceholderData: isPlaceholderDataGetList,
refetch: refetchGetMatching,
} = useGetList(
reference,
@@ -153,7 +157,9 @@ export const useReferenceArrayInputController = <
hideFilter: paramsModifiers.hideFilter,
isFetching: isFetchingGetMany || isFetchingGetList,
isLoading: isLoadingGetMany || isLoadingGetList,
+ isPaused: isPausedGetMany || isPausedGetList,
isPending: isPendingGetMany || isPendingGetList,
+ isPlaceholderData: isPlaceholderDataGetMany || isPlaceholderDataGetList,
page: params.page,
perPage: params.perPage,
refetch,
diff --git a/packages/ra-core/src/controller/input/useReferenceInputController.ts b/packages/ra-core/src/controller/input/useReferenceInputController.ts
index 7ddbced53d2..df5757c948b 100644
--- a/packages/ra-core/src/controller/input/useReferenceInputController.ts
+++ b/packages/ra-core/src/controller/input/useReferenceInputController.ts
@@ -79,11 +79,12 @@ export const useReferenceInputController = (
// fetch possible values
const {
- data: possibleValuesData = [],
+ data: possibleValuesData,
total,
pageInfo,
isFetching: isFetchingPossibleValues,
isLoading: isLoadingPossibleValues,
+ isPaused: isPausedPossibleValues,
isPending: isPendingPossibleValues,
error: errorPossibleValues,
refetch: refetchGetList,
@@ -112,12 +113,13 @@ export const useReferenceInputController = (
error: errorReference,
isLoading: isLoadingReference,
isFetching: isFetchingReference,
+ isPaused: isPausedReference,
isPending: isPendingReference,
} = useReference({
id: currentValue,
reference,
- // @ts-ignore the types of the queryOptions for the getMAny and getList are not compatible
options: {
+ // @ts-ignore the types of the queryOptions for the getMany and getList are not compatible
enabled: currentValue != null && currentValue !== '',
meta,
...otherQueryOptions,
@@ -128,6 +130,7 @@ export const useReferenceInputController = (
// The reference query isn't enabled when there is no value yet but as it has no data, react-query will flag it as pending
(currentValue != null && currentValue !== '' && isPendingReference) ||
isPendingPossibleValues;
+ const isPaused = isPausedPossibleValues || isPausedReference;
// We need to delay the update of the referenceRecord and the finalData
// to the next React state update, because otherwise it can raise a warning
@@ -140,17 +143,30 @@ export const useReferenceInputController = (
}, [currentReferenceRecord]);
// add current value to possible sources
- let finalData: RecordType[], finalTotal: number | undefined;
- if (
- !referenceRecord ||
- possibleValuesData.find(record => record.id === referenceRecord.id)
- ) {
- finalData = possibleValuesData;
- finalTotal = total;
- } else {
- finalData = [referenceRecord, ...possibleValuesData];
- finalTotal = total == null ? undefined : total + 1;
- }
+ const { finalData, finalTotal } = useMemo(() => {
+ if (isPaused && possibleValuesData == null) {
+ return {
+ finalData: null,
+ finalTotal: null,
+ };
+ }
+ if (
+ !referenceRecord ||
+ (possibleValuesData ?? []).find(
+ record => record.id === referenceRecord.id
+ )
+ ) {
+ return {
+ finalData: possibleValuesData,
+ finalTotal: total,
+ };
+ } else {
+ return {
+ finalData: [referenceRecord, ...(possibleValuesData ?? [])],
+ finalTotal: total == null ? undefined : total + 1,
+ };
+ }
+ }, [isPaused, referenceRecord, possibleValuesData, total]);
const refetch = useCallback(() => {
refetchGetList();
@@ -176,7 +192,8 @@ export const useReferenceInputController = (
hideFilter: paramsModifiers.hideFilter,
isFetching: isFetchingReference || isFetchingPossibleValues,
isLoading: isLoadingReference || isLoadingPossibleValues,
- isPending: isPending,
+ isPaused,
+ isPending,
page: params.page,
perPage: params.perPage,
refetch,
diff --git a/packages/ra-core/src/controller/list/useInfiniteListController.ts b/packages/ra-core/src/controller/list/useInfiniteListController.ts
index 8214e3ba9d0..23662d68042 100644
--- a/packages/ra-core/src/controller/list/useInfiniteListController.ts
+++ b/packages/ra-core/src/controller/list/useInfiniteListController.ts
@@ -102,7 +102,9 @@ export const useInfiniteListController = <
total,
error,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
isFetching,
hasNextPage,
hasPreviousPage,
@@ -204,7 +206,9 @@ export const useInfiniteListController = <
hideFilter: queryModifiers.hideFilter,
isFetching,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
diff --git a/packages/ra-core/src/controller/list/useList.ts b/packages/ra-core/src/controller/list/useList.ts
index d6a9b34bc39..71994cf6259 100644
--- a/packages/ra-core/src/controller/list/useList.ts
+++ b/packages/ra-core/src/controller/list/useList.ts
@@ -62,6 +62,8 @@ export const useList = (
isFetching = false,
isLoading = false,
isPending = false,
+ isPaused = false,
+ isPlaceholderData = false,
page: initialPage = 1,
perPage: initialPerPage = 1000,
sort: initialSort,
@@ -289,6 +291,8 @@ export const useList = (
isFetching: fetchingState,
isLoading: loadingState,
isPending: pendingState,
+ isPaused,
+ isPlaceholderData,
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
@@ -317,6 +321,8 @@ export interface UseListOptions<
isFetching?: boolean;
isLoading?: boolean;
isPending?: boolean;
+ isPaused?: boolean;
+ isPlaceholderData?: boolean;
page?: number;
perPage?: number;
sort?: SortPayload;
diff --git a/packages/ra-core/src/controller/list/useListContextWithProps.ts b/packages/ra-core/src/controller/list/useListContextWithProps.ts
index 12afb5b5e0f..34d88b2ca7d 100644
--- a/packages/ra-core/src/controller/list/useListContextWithProps.ts
+++ b/packages/ra-core/src/controller/list/useListContextWithProps.ts
@@ -80,7 +80,9 @@ const extractListContextProps = ({
hideFilter,
isFetching,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
onSelect,
onSelectAll,
onToggleItem,
@@ -107,7 +109,9 @@ const extractListContextProps = ({
hideFilter,
isFetching,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
onSelect,
onSelectAll,
onToggleItem,
diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts
index 50b56a53142..036a1f8d3fb 100644
--- a/packages/ra-core/src/controller/list/useListController.ts
+++ b/packages/ra-core/src/controller/list/useListController.ts
@@ -114,10 +114,10 @@ export const useListController = <
error,
isLoading,
isFetching,
- isPending,
- refetch,
isPaused,
+ isPending,
isPlaceholderData,
+ refetch,
} = useGetList(
resource,
{
@@ -216,7 +216,9 @@ export const useListController = <
hideFilter: queryModifiers.hideFilter,
isFetching,
isLoading,
+ isPaused,
isPending,
+ isPlaceholderData,
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
@@ -542,6 +544,8 @@ export interface ListControllerBaseResult {
hasPreviousPage?: boolean;
isFetching?: boolean;
isLoading?: boolean;
+ isPaused?: boolean;
+ isPlaceholderData?: boolean;
}
export interface ListControllerLoadingResult
diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts
index 4f523a8b126..c056939e565 100644
--- a/packages/ra-core/src/controller/show/useShowController.ts
+++ b/packages/ra-core/src/controller/show/useShowController.ts
@@ -97,6 +97,7 @@ export const useShowController = <
error,
isLoading,
isFetching,
+ isPaused,
isPending,
refetch,
} = useGetOne(
@@ -159,6 +160,7 @@ export const useShowController = <
error,
isLoading,
isFetching,
+ isPaused,
isPending,
record,
refetch,
@@ -180,6 +182,7 @@ export interface ShowControllerBaseResult {
defaultTitle?: string;
isFetching: boolean;
isLoading: boolean;
+ isPaused: boolean;
resource: string;
record?: RecordType;
refetch: UseGetOneHookValue['refetch'];
diff --git a/packages/ra-core/src/controller/useReference.spec.tsx b/packages/ra-core/src/controller/useReference.spec.tsx
index e8b991c781c..4c233df6335 100644
--- a/packages/ra-core/src/controller/useReference.spec.tsx
+++ b/packages/ra-core/src/controller/useReference.spec.tsx
@@ -130,7 +130,9 @@ describe('useReference', () => {
referenceRecord: undefined,
isFetching: true,
isLoading: true,
+ isPaused: false,
isPending: true,
+ isPlaceholderData: false,
error: null,
refetch: expect.any(Function),
});
@@ -138,7 +140,9 @@ describe('useReference', () => {
referenceRecord: { id: 1, title: 'foo' },
isFetching: false,
isLoading: false,
+ isPaused: false,
isPending: false,
+ isPlaceholderData: false,
error: null,
refetch: expect.any(Function),
});
@@ -170,7 +174,9 @@ describe('useReference', () => {
referenceRecord: { id: 1, title: 'foo' },
isFetching: true,
isLoading: false,
+ isPaused: false,
isPending: false,
+ isPlaceholderData: false,
error: null,
refetch: expect.any(Function),
});
@@ -178,7 +184,9 @@ describe('useReference', () => {
referenceRecord: { id: 1, title: 'foo' },
isFetching: false,
isLoading: false,
+ isPaused: false,
isPending: false,
+ isPlaceholderData: false,
error: null,
refetch: expect.any(Function),
});
diff --git a/packages/ra-core/src/controller/useReference.ts b/packages/ra-core/src/controller/useReference.ts
index b52526226c9..112b23d8693 100644
--- a/packages/ra-core/src/controller/useReference.ts
+++ b/packages/ra-core/src/controller/useReference.ts
@@ -21,7 +21,9 @@ export interface UseReferenceResult<
ErrorType = Error,
> {
isLoading: boolean;
+ isPaused: boolean;
isPending: boolean;
+ isPlaceholderData: boolean;
isFetching: boolean;
referenceRecord?: RecordType;
error?: ErrorType | null;
@@ -68,12 +70,20 @@ export const useReference = <
ErrorType
> => {
const { meta, ...otherQueryOptions } = options;
- const { data, error, isLoading, isFetching, isPending, refetch } =
- useGetManyAggregate(
- reference,
- { ids: [id], meta },
- otherQueryOptions
- );
+ const {
+ data,
+ error,
+ isLoading,
+ isFetching,
+ isPending,
+ isPaused,
+ isPlaceholderData,
+ refetch,
+ } = useGetManyAggregate(
+ reference,
+ { ids: [id], meta },
+ otherQueryOptions
+ );
return {
referenceRecord: error ? undefined : data ? data[0] : undefined,
refetch,
@@ -81,5 +91,7 @@ export const useReference = <
isLoading,
isFetching,
isPending,
+ isPaused,
+ isPlaceholderData,
};
};
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-core/src/dataProvider/addOfflineSupportToQueryClient.ts b/packages/ra-core/src/dataProvider/addOfflineSupportToQueryClient.ts
new file mode 100644
index 00000000000..360b17fa2bc
--- /dev/null
+++ b/packages/ra-core/src/dataProvider/addOfflineSupportToQueryClient.ts
@@ -0,0 +1,104 @@
+import type { QueryClient } from '@tanstack/react-query';
+import { DataProviderMutations } from './dataFetchActions';
+import type { DataProvider } from '../types';
+
+/**
+ * A function that registers default functions on the queryClient for the specified mutations and resources.
+ * react-query requires default mutation functions to allow resumable mutations
+ * (e.g. mutations triggered while offline and users navigated away from the component that triggered them).
+ *
+ * @example Adding offline support for the default mutations
+ * // in src/App.tsx
+ * import { addOfflineSupportToQueryClient } from 'react-admin';
+ * import { QueryClient } from '@tanstack/react-query';
+ * import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
+ * import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
+ * import { dataProvider } from './dataProvider';
+ * import { posts } from './posts';
+ * import { comments } from './comments';
+ *
+ * const localStoragePersister = createSyncStoragePersister({
+ * storage: window.localStorage,
+ * });
+ *
+ * const queryClient = addOfflineSupportToQueryClient({
+ * queryClient: new QueryClient(),
+ * dataProvider,
+ * resources: ['posts', 'comments'],
+ * });
+ *
+ * const App = () => (
+ * {
+ * // resume mutations after initial restore from localStorage was successful
+ * queryClient.resumePausedMutations();
+ * }}
+ * >
+ *
+ *
+ *
+ *
+ *
+ * );
+ *
+ * @example Adding offline support with custom mutations
+ * // in src/App.tsx
+ * import { Admin, Resource, addOfflineSupportToQueryClient, reactAdminMutations } from 'react-admin';
+ * import { QueryClient } from '@tanstack/react-query';
+ * import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
+ * import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
+ * import { dataProvider } from './dataProvider';
+ * import { posts } from './posts';
+ * import { comments } from './comments';
+ *
+ * const localStoragePersister = createSyncStoragePersister({
+ * storage: window.localStorage,
+ * });
+ *
+ * const queryClient = addOfflineSupportToQueryClient({
+ * queryClient: new QueryClient(),
+ * dataProvider,
+ * resources: ['posts', 'comments'],
+ * mutations: [...reactAdminMutations, 'myCustomMutation'],
+ * });
+ *
+ * const App = () => (
+ * {
+ * // resume mutations after initial restore from localStorage was successful
+ * queryClient.resumePausedMutations();
+ * }}
+ * >
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+export const addOfflineSupportToQueryClient = ({
+ dataProvider,
+ resources,
+ queryClient,
+}: {
+ dataProvider: DataProvider;
+ resources: string[];
+ queryClient: QueryClient;
+}) => {
+ resources.forEach(resource => {
+ DataProviderMutations.forEach(mutation => {
+ queryClient.setMutationDefaults([resource, mutation], {
+ mutationFn: async (params: any) => {
+ const dataProviderFn = dataProvider[mutation] as Function;
+ return dataProviderFn.apply(dataProviderFn, ...params);
+ },
+ });
+ });
+ });
+
+ return queryClient;
+};
diff --git a/packages/ra-core/src/dataProvider/dataFetchActions.ts b/packages/ra-core/src/dataProvider/dataFetchActions.ts
index 249d90a7647..d9c1eadcea0 100644
--- a/packages/ra-core/src/dataProvider/dataFetchActions.ts
+++ b/packages/ra-core/src/dataProvider/dataFetchActions.ts
@@ -8,6 +8,14 @@ export const UPDATE_MANY = 'UPDATE_MANY';
export const DELETE = 'DELETE';
export const DELETE_MANY = 'DELETE_MANY';
+export const DataProviderMutations = [
+ 'create',
+ 'delete',
+ 'update',
+ 'updateMany',
+ 'deleteMany',
+];
+
export const fetchActionsWithRecordResponse = ['getOne', 'create', 'update'];
export const fetchActionsWithArrayOfIdentifiedRecordsResponse = [
'getList',
diff --git a/packages/ra-core/src/dataProvider/index.ts b/packages/ra-core/src/dataProvider/index.ts
index ed5bd65b373..fe058181278 100644
--- a/packages/ra-core/src/dataProvider/index.ts
+++ b/packages/ra-core/src/dataProvider/index.ts
@@ -4,6 +4,7 @@ import HttpError from './HttpError';
import * as fetchUtils from './fetch';
import undoableEventEmitter from './undoableEventEmitter';
+export * from './addOfflineSupportToQueryClient';
export * from './combineDataProviders';
export * from './dataFetchActions';
export * from './defaultDataProvider';
diff --git a/packages/ra-core/src/dataProvider/useCreate.ts b/packages/ra-core/src/dataProvider/useCreate.ts
index 462e36e2ace..7d3a3ca8c15 100644
--- a/packages/ra-core/src/dataProvider/useCreate.ts
+++ b/packages/ra-core/src/dataProvider/useCreate.ts
@@ -1,4 +1,4 @@
-import { useMemo, useRef } from 'react';
+import { useEffect, useMemo, useRef } from 'react';
import {
useMutation,
UseMutationOptions,
@@ -96,7 +96,11 @@ export const useCreate = <
getMutateWithMiddlewares,
...mutationOptions
} = options;
+
const mode = useRef(mutationMode);
+ useEffect(() => {
+ mode.current = mutationMode;
+ }, [mutationMode]);
const paramsRef =
useRef>>>(params);
@@ -152,6 +156,7 @@ export const useCreate = <
MutationError,
Partial>
>({
+ mutationKey: [resource, 'create', params],
mutationFn: ({
resource: callTimeResource = resource,
data: callTimeData = paramsRef.current.data,
diff --git a/packages/ra-core/src/dataProvider/useDelete.ts b/packages/ra-core/src/dataProvider/useDelete.ts
index 25cfd6a9133..586f25a0558 100644
--- a/packages/ra-core/src/dataProvider/useDelete.ts
+++ b/packages/ra-core/src/dataProvider/useDelete.ts
@@ -194,6 +194,7 @@ export const useDelete = <
MutationError,
Partial>
>({
+ mutationKey: [resource, 'delete', params],
mutationFn: ({
resource: callTimeResource = resource,
id: callTimeId = paramsRef.current.id,
diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.ts b/packages/ra-core/src/dataProvider/useDeleteMany.ts
index dceb34cb667..92031824e6d 100644
--- a/packages/ra-core/src/dataProvider/useDeleteMany.ts
+++ b/packages/ra-core/src/dataProvider/useDeleteMany.ts
@@ -220,6 +220,7 @@ export const useDeleteMany = <
MutationError,
Partial>
>({
+ mutationKey: [resource, 'deleteMany', params],
mutationFn: ({
resource: callTimeResource = resource,
ids: callTimeIds = paramsRef.current.ids,
diff --git a/packages/ra-core/src/dataProvider/useGetList.ts b/packages/ra-core/src/dataProvider/useGetList.ts
index aa24430d175..e16296dfb3a 100644
--- a/packages/ra-core/src/dataProvider/useGetList.ts
+++ b/packages/ra-core/src/dataProvider/useGetList.ts
@@ -176,7 +176,7 @@ export const useGetList = <
}
: result,
[result]
- ) as UseQueryResult & {
+ ) as unknown as UseQueryResult & {
total?: number;
pageInfo?: {
hasNextPage?: boolean;
diff --git a/packages/ra-core/src/dataProvider/useGetManyReference.ts b/packages/ra-core/src/dataProvider/useGetManyReference.ts
index 001e930fc6b..c6639a9147d 100644
--- a/packages/ra-core/src/dataProvider/useGetManyReference.ts
+++ b/packages/ra-core/src/dataProvider/useGetManyReference.ts
@@ -155,7 +155,7 @@ export const useGetManyReference = <
}
: result,
[result]
- ) as UseQueryResult & {
+ ) as unknown as UseQueryResult & {
total?: number;
pageInfo?: {
hasNextPage?: boolean;
diff --git a/packages/ra-core/src/dataProvider/useUpdate.ts b/packages/ra-core/src/dataProvider/useUpdate.ts
index 9e3e53c21e8..b4cd0a055da 100644
--- a/packages/ra-core/src/dataProvider/useUpdate.ts
+++ b/packages/ra-core/src/dataProvider/useUpdate.ts
@@ -196,6 +196,7 @@ export const useUpdate = (
ErrorType,
Partial>
>({
+ mutationKey: [resource, 'update', params],
mutationFn: ({
resource: callTimeResource = resource,
id: callTimeId = paramsRef.current.id,
diff --git a/packages/ra-core/src/dataProvider/useUpdateMany.ts b/packages/ra-core/src/dataProvider/useUpdateMany.ts
index 664778de44e..1f261467d25 100644
--- a/packages/ra-core/src/dataProvider/useUpdateMany.ts
+++ b/packages/ra-core/src/dataProvider/useUpdateMany.ts
@@ -199,6 +199,7 @@ export const useUpdateMany = <
MutationError,
Partial>
>({
+ mutationKey: [resource, 'updateMany', params],
mutationFn: ({
resource: callTimeResource = resource,
ids: callTimeIds = paramsRef.current.ids,
diff --git a/packages/ra-core/src/dataTable/DataTableBase.tsx b/packages/ra-core/src/dataTable/DataTableBase.tsx
index b16e732da00..a89e4c50401 100644
--- a/packages/ra-core/src/dataTable/DataTableBase.tsx
+++ b/packages/ra-core/src/dataTable/DataTableBase.tsx
@@ -31,6 +31,7 @@ export const DataTableBase = function DataTable<
loading,
isRowSelectable,
isRowExpandable,
+ offline,
resource,
rowClick,
expandSingle = false,
@@ -39,7 +40,9 @@ export const DataTableBase = function DataTable<
const {
sort,
data,
+ isPaused,
isPending,
+ isPlaceholderData,
onSelect,
onToggleItem,
selectedIds,
@@ -145,7 +148,7 @@ export const DataTableBase = function DataTable<
]
);
- if (isPending === true) {
+ if (isPending && !isPaused) {
return loading;
}
@@ -154,7 +157,11 @@ export const DataTableBase = function DataTable<
* displaying the table header with zero data rows,
* the DataTable displays the empty component.
*/
- if (data == null || data.length === 0 || total === 0) {
+ if (
+ (data == null || data.length === 0 || total === 0) &&
+ !isPaused &&
+ !isPlaceholderData
+ ) {
if (empty) {
return empty;
}
@@ -162,6 +169,14 @@ export const DataTableBase = function DataTable<
return null;
}
+ if (isPaused && (isPlaceholderData || data == null)) {
+ if (offline) {
+ return offline;
+ }
+
+ return null;
+ }
+
/**
* After the initial load, if the data for the list isn't empty,
* and even if the data is refreshing (e.g. after a filter change),
@@ -206,6 +221,7 @@ export interface DataTableBaseProps {
hasBulkActions: boolean;
hover?: boolean;
empty: ReactNode;
+ offline: ReactNode;
isRowExpandable?: (record: RecordType) => boolean;
isRowSelectable?: (record: RecordType) => boolean;
loading: ReactNode;
diff --git a/packages/ra-core/src/form/choices/ChoicesContext.ts b/packages/ra-core/src/form/choices/ChoicesContext.ts
index 6e0b53dee80..a420a6351b0 100644
--- a/packages/ra-core/src/form/choices/ChoicesContext.ts
+++ b/packages/ra-core/src/form/choices/ChoicesContext.ts
@@ -20,6 +20,8 @@ export type ChoicesContextBaseValue = {
hideFilter: (filterName: string) => void;
isFetching: boolean;
isLoading: boolean;
+ isPaused: boolean;
+ isPlaceholderData: boolean;
page: number;
perPage: number;
refetch: (() => void) | UseGetListHookValue['refetch'];
diff --git a/packages/ra-core/src/form/choices/useChoicesContext.ts b/packages/ra-core/src/form/choices/useChoicesContext.ts
index e7a31c13684..7528ee5ef1b 100644
--- a/packages/ra-core/src/form/choices/useChoicesContext.ts
+++ b/packages/ra-core/src/form/choices/useChoicesContext.ts
@@ -21,6 +21,8 @@ export const useChoicesContext = (
isLoading: options.isLoading ?? false,
isPending: options.isPending ?? false,
isFetching: options.isFetching ?? false,
+ isPaused: options.isPaused ?? false,
+ isPlaceholderData: options.isPlaceholderData ?? false,
error: options.error,
// When not in a ChoicesContext, paginating does not make sense (e.g. AutocompleteInput).
perPage: Infinity,
@@ -44,6 +46,8 @@ export const useChoicesContext = (
isLoading: list.isLoading ?? false, // we must take the one for useList, otherwise the loading state isn't synchronized with the data
isPending: list.isPending ?? false, // same
isFetching: list.isFetching ?? false, // same
+ isPaused: list.isPaused ?? false, // same
+ isPlaceholderData: list.isPlaceholderData ?? false, // same
page: options.page ?? list.page,
perPage: options.perPage ?? list.perPage,
refetch: options.refetch ?? list.refetch,
diff --git a/packages/ra-language-english/src/index.ts b/packages/ra-language-english/src/index.ts
index 0e665685b0f..389d4f150d2 100644
--- a/packages/ra-language-english/src/index.ts
+++ b/packages/ra-language-english/src/index.ts
@@ -80,6 +80,8 @@ const englishMessages: TranslationMessages = {
'At least one of the associated references no longer appears to be available.',
single_missing:
'Associated reference no longer appears to be available.',
+ single_offline:
+ 'The associated reference cannot be fetched as you are offline.',
},
password: {
toggle_visible: 'Hide password',
@@ -171,6 +173,9 @@ 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.',
+ 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 fb450792c02..06e14d7f8b8 100644
--- a/packages/ra-language-french/src/index.ts
+++ b/packages/ra-language-french/src/index.ts
@@ -82,6 +82,8 @@ const frenchMessages: TranslationMessages = {
'Au moins une des références associées semble ne plus être disponible.',
single_missing:
'La référence associée ne semble plus disponible.',
+ single_offline:
+ 'La référence associée ne peut être chargée car vous êtes hors ligne.',
},
password: {
toggle_visible: 'Cacher le mot de passe',
@@ -179,6 +181,9 @@ 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.',
+ 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/Labeled.tsx b/packages/ra-ui-materialui/src/Labeled.tsx
index 8b188280690..416080ac252 100644
--- a/packages/ra-ui-materialui/src/Labeled.tsx
+++ b/packages/ra-ui-materialui/src/Labeled.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { ElementType, ReactElement } from 'react';
+import type { ElementType, ReactElement, ReactNode } from 'react';
import {
Stack,
type StackProps,
@@ -45,6 +45,14 @@ export const Labeled = (inProps: LabeledProps) => {
...rest
} = props;
+ const childrenProps = React.isValidElement(children) ? children.props : {};
+ const isLabeled = React.isValidElement(children)
+ ? // @ts-ignore
+ children.type?.displayName === 'Labeled'
+ : false;
+ const shouldAddLabel =
+ label !== false && childrenProps.label !== false && !isLabeled;
+
return (
{
})}
{...rest}
>
- {label !== false &&
- children.props.label !== false &&
- typeof children.type !== 'string' &&
- // @ts-ignore
- children.type?.displayName !== 'Labeled' &&
- // @ts-ignore
- children.type?.displayName !== 'Labeled' ? (
+ {shouldAddLabel ? (
{
{...TypographyProps}
>
@@ -90,7 +92,7 @@ export const Labeled = (inProps: LabeledProps) => {
Labeled.displayName = 'Labeled';
export interface LabeledProps extends StackProps {
- children: ReactElement;
+ children: ReactNode;
className?: string;
color?:
| ResponsiveStyleValue
diff --git a/packages/ra-ui-materialui/src/Offline.tsx b/packages/ra-ui-materialui/src/Offline.tsx
new file mode 100644
index 00000000000..fc6a711f189
--- /dev/null
+++ b/packages/ra-ui-materialui/src/Offline.tsx
@@ -0,0 +1,90 @@
+import * as React from 'react';
+import {
+ Alert,
+ AlertProps,
+ ComponentsOverrides,
+ styled,
+ Typography,
+} from '@mui/material';
+import { useGetResourceLabel, useResourceContext, useTranslate } from 'ra-core';
+import clsx from 'clsx';
+
+export const Offline = (props: Offline) => {
+ const { icon, message: messageProp, variant = 'standard', ...rest } = props;
+ const translate = useTranslate();
+ 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 = translate(
+ messageProp ?? `resources.${resource}.navigation.offline`,
+ {
+ name: getResourceLabel(resource, 0),
+ _:
+ messageProp ??
+ translate('ra.notification.offline', {
+ 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/button/BulkDeleteWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx
index f7b149d78e8..bdc1c4d89c4 100644
--- a/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx
@@ -10,6 +10,7 @@ import {
import {
type MutationMode,
useDeleteMany,
+ useIsOffine,
useListContext,
useNotify,
useRefresh,
@@ -50,6 +51,7 @@ export const BulkDeleteWithConfirmButton = (
const resource = useResourceContext(props);
const refresh = useRefresh();
const translate = useTranslate();
+ const isOffline = useIsOffine();
const [deleteMany, { isPending }] = useDeleteMany(
resource,
{ ids: selectedIds, meta: mutationMeta },
@@ -57,15 +59,23 @@ export const BulkDeleteWithConfirmButton = (
onSuccess: () => {
refresh();
notify(
- successMessage ??
- `resources.${resource}.notifications.deleted`,
+ successMessage != null
+ ? successMessage
+ : isOffline
+ ? `resources.${resource}.notifications.pending_delete`
+ : `resources.${resource}.notifications.deleted`,
{
type: 'info',
messageArgs: {
smart_count: selectedIds.length,
- _: translate('ra.notification.deleted', {
- smart_count: selectedIds.length,
- }),
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_delete'
+ : 'ra.notification.deleted',
+ {
+ smart_count: selectedIds.length,
+ }
+ ),
},
undoable: mutationMode === 'undoable',
}
diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx
index e8950e0b23a..7f2e258589a 100644
--- a/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx
@@ -7,6 +7,7 @@ import {
} from '@mui/material/styles';
import {
useDeleteMany,
+ useIsOffine,
useRefresh,
useNotify,
useResourceContext,
@@ -43,6 +44,7 @@ export const BulkDeleteWithUndoButton = (
const translate = useTranslate();
const [deleteMany, { isPending }] = useDeleteMany();
+ const isOffline = useIsOffine();
const handleClick = e => {
deleteMany(
resource,
@@ -50,15 +52,23 @@ export const BulkDeleteWithUndoButton = (
{
onSuccess: () => {
notify(
- successMessage ??
- `resources.${resource}.notifications.deleted`,
+ successMessage != null
+ ? successMessage
+ : isOffline
+ ? `resources.${resource}.notifications.pending_delete`
+ : `resources.${resource}.notifications.deleted`,
{
type: 'info',
messageArgs: {
smart_count: selectedIds.length,
- _: translate('ra.notification.deleted', {
- smart_count: selectedIds.length,
- }),
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_delete'
+ : 'ra.notification.deleted',
+ {
+ smart_count: selectedIds.length,
+ }
+ ),
},
undoable: true,
}
diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx
index 6358ba332ab..ae5f69b7930 100644
--- a/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx
@@ -16,6 +16,7 @@ import {
type MutationMode,
type RaRecord,
type UpdateManyParams,
+ useIsOffine,
} from 'ra-core';
import { Confirm } from '../layout';
@@ -36,6 +37,7 @@ export const BulkUpdateWithConfirmButton = (
const unselectAll = useUnselectAll(resource);
const [isOpen, setOpen] = useState(false);
const { selectedIds } = useListContext();
+ const isOffline = useIsOffine();
const {
confirmTitle = 'ra.message.bulk_update_title',
@@ -46,16 +48,26 @@ export const BulkUpdateWithConfirmButton = (
mutationMode = 'pessimistic',
onClick,
onSuccess = () => {
- notify(`resources.${resource}.notifications.updated`, {
- type: 'info',
- messageArgs: {
- smart_count: selectedIds.length,
- _: translate('ra.notification.updated', {
+ notify(
+ isOffline
+ ? `resources.${resource}.notifications.pending_update`
+ : `resources.${resource}.notifications.updated`,
+ {
+ type: 'info',
+ messageArgs: {
smart_count: selectedIds.length,
- }),
- },
- undoable: mutationMode === 'undoable',
- });
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_update'
+ : 'ra.notification.updated',
+ {
+ smart_count: selectedIds.length,
+ }
+ ),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
unselectAll();
setOpen(false);
},
diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx
index e612ceb30cf..35b8a17198f 100644
--- a/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx
+++ b/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx
@@ -15,6 +15,7 @@ import {
type RaRecord,
type UpdateManyParams,
useTranslate,
+ useIsOffine,
} from 'ra-core';
import type { UseMutationOptions } from '@tanstack/react-query';
@@ -34,6 +35,7 @@ export const BulkUpdateWithUndoButton = (
const unselectAll = useUnselectAll(resource);
const refresh = useRefresh();
const translate = useTranslate();
+ const isOffline = useIsOffine();
const {
data,
@@ -43,14 +45,21 @@ export const BulkUpdateWithUndoButton = (
onClick,
onSuccess = () => {
notify(
- successMessage ?? `resources.${resource}.notifications.updated`,
+ successMessage ?? isOffline
+ ? `resources.${resource}.notifications.pending_update`
+ : `resources.${resource}.notifications.updated`,
{
type: 'info',
messageArgs: {
smart_count: selectedIds.length,
- _: translate('ra.notification.updated', {
- smart_count: selectedIds.length,
- }),
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_update'
+ : 'ra.notification.updated',
+ {
+ smart_count: selectedIds.length,
+ }
+ ),
},
undoable: true,
}
diff --git a/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx
index daa8a36e0e9..2cc2843950e 100644
--- a/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx
+++ b/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx
@@ -17,6 +17,7 @@ import {
useRecordContext,
useUpdate,
useGetRecordRepresentation,
+ useIsOffine,
useResourceTranslation,
} from 'ra-core';
@@ -39,6 +40,7 @@ export const UpdateWithConfirmButton = <
const translate = useTranslate();
const resource = useResourceContext(props);
const [isOpen, setOpen] = useState(false);
+ const isOffline = useIsOffine();
const record = useRecordContext(props);
const {
@@ -61,14 +63,26 @@ export const UpdateWithConfirmButton = <
const {
meta: mutationMeta,
onSuccess = () => {
- notify(`resources.${resource}.notifications.updated`, {
- type: 'info',
- messageArgs: {
- smart_count: 1,
- _: translate('ra.notification.updated', { smart_count: 1 }),
- },
- undoable: mutationMode === 'undoable',
- });
+ notify(
+ isOffline
+ ? `resources.${resource}.notifications.pending_update`
+ : `resources.${resource}.notifications.updated`,
+ {
+ type: 'info',
+ messageArgs: {
+ smart_count: 1,
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_update'
+ : 'ra.notification.updated',
+ {
+ smart_count: 1,
+ }
+ ),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
},
onError = (error: MutationOptionsError) => {
notify(
diff --git a/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx
index 8a5da5361df..275915b441a 100644
--- a/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx
+++ b/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx
@@ -14,6 +14,7 @@ import {
useUpdate,
type UpdateParams,
useTranslate,
+ useIsOffine,
useGetRecordRepresentation,
useResourceTranslation,
} from 'ra-core';
@@ -31,6 +32,7 @@ export const UpdateWithUndoButton = (inProps: UpdateWithUndoButtonProps) => {
const notify = useNotify();
const resource = useResourceContext(props);
const refresh = useRefresh();
+ const isOffline = useIsOffine();
const {
data,
@@ -71,14 +73,26 @@ export const UpdateWithUndoButton = (inProps: UpdateWithUndoButtonProps) => {
const {
meta: mutationMeta,
onSuccess = () => {
- notify(`resources.${resource}.notifications.updated`, {
- type: 'info',
- messageArgs: {
- smart_count: 1,
- _: translate('ra.notification.updated', { smart_count: 1 }),
- },
- undoable: true,
- });
+ notify(
+ isOffline
+ ? `resources.${resource}.notifications.pending_update`
+ : `resources.${resource}.notifications.updated`,
+ {
+ type: 'info',
+ messageArgs: {
+ smart_count: 1,
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_update'
+ : 'ra.notification.updated',
+ {
+ smart_count: 1,
+ }
+ ),
+ },
+ undoable: true,
+ }
+ );
},
onError = (error: Error | string) => {
notify(
diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx
index 26cbbae0eec..6573a4adec5 100644
--- a/packages/ra-ui-materialui/src/detail/EditView.tsx
+++ b/packages/ra-ui-materialui/src/detail/EditView.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { ReactElement, ElementType } from 'react';
+import type { ReactNode, ElementType } from 'react';
import {
Card,
CardContent,
@@ -13,9 +13,11 @@ import { useEditContext, useResourceDefinition } from 'ra-core';
import { EditActions } from './EditActions';
import { Title } from '../layout';
+import { Offline } from '../Offline';
import { EditProps } from './Edit';
const defaultActions = ;
+const defaultOffline = ;
export const EditView = (props: EditViewProps) => {
const {
@@ -24,13 +26,25 @@ export const EditView = (props: EditViewProps) => {
children,
className,
component: Content = Card,
+ offline = defaultOffline,
emptyWhileLoading = false,
title,
...rest
} = props;
const { hasShow } = useResourceDefinition();
- const { resource, defaultTitle, record, isPending } = useEditContext();
+ const { resource, defaultTitle, record, isPending, isPaused } =
+ useEditContext();
+
+ if (isPaused && record == null && offline) {
+ return (
+
+
+ {offline}
+
+
+ );
+ }
const finalActions =
typeof actions === 'undefined' && hasShow ? defaultActions : actions;
@@ -64,11 +78,12 @@ export const EditView = (props: EditViewProps) => {
export interface EditViewProps
extends Omit, 'id' | 'title'> {
- actions?: ReactElement | false;
- aside?: ReactElement;
+ actions?: ReactNode;
+ aside?: ReactNode;
+ offline?: ReactNode;
component?: ElementType;
emptyWhileLoading?: boolean;
- title?: string | ReactElement | false;
+ title?: ReactNode;
sx?: SxProps;
}
diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx
index 844f6a1e7cc..aae98b82c0a 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 } from 'react';
+import type { ReactNode, ElementType } from 'react';
import {
Card,
type ComponentsOverrides,
@@ -11,9 +11,11 @@ import clsx from 'clsx';
import { useShowContext, useResourceDefinition } from 'ra-core';
import { ShowActions } from './ShowActions';
import { Title } from '../layout';
+import { Offline } from '../Offline';
import { ShowProps } from './Show';
const defaultActions = ;
+const defaultOffline = ;
export const ShowView = (props: ShowViewProps) => {
const {
@@ -23,13 +25,24 @@ export const ShowView = (props: ShowViewProps) => {
className,
component: Content = Card,
emptyWhileLoading = false,
+ offline = defaultOffline,
title,
...rest
} = props;
- const { resource, defaultTitle, record } = useShowContext();
+ const { resource, defaultTitle, isPaused, record } = useShowContext();
const { hasEdit } = useResourceDefinition();
+ if (isPaused && record == null && offline) {
+ return (
+
+
+ {offline}
+
+
+ );
+ }
+
const finalActions =
typeof actions === 'undefined' && hasEdit ? defaultActions : actions;
@@ -60,11 +73,12 @@ export const ShowView = (props: ShowViewProps) => {
export interface ShowViewProps
extends Omit, 'id' | 'title'> {
- actions?: ReactElement | false;
- aside?: ReactElement;
+ actions?: ReactNode;
+ aside?: ReactNode;
component?: ElementType;
emptyWhileLoading?: boolean;
- title?: string | ReactElement | false;
+ offline?: ReactNode;
+ title?: ReactNode;
sx?: SxProps;
}
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
index 532fc1c7e03..ed00e3318aa 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import fakeRestProvider from 'ra-data-fakerest';
-import { CardContent } from '@mui/material';
+import { CardContent, Typography } from '@mui/material';
import { ResourceDefinitionContextProvider } from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
@@ -26,7 +26,10 @@ const fakeData = {
{ id: 8, name: 'Charlie Watts' },
],
};
-const dataProvider = fakeRestProvider(fakeData, false);
+const dataProvider = fakeRestProvider(
+ fakeData,
+ process.env.NODE_ENV !== 'test'
+);
const resouceDefs = {
artists: {
@@ -69,6 +72,70 @@ export const Children = () => (
);
+const LoadChildrenOnDemand = ({ children }: { children: React.ReactNode }) => {
+ const [showChildren, setShowChildren] = React.useState(false);
+ const handleClick = () => {
+ setShowChildren(true);
+ };
+ return showChildren ? (
+ children
+ ) : (
+
+
+ Don't forget to go offline first
+
+
+
+ );
+};
+
+export const Offline = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const OfflineWithChildren = () => (
+ englishMessages)}
+ defaultTheme="light"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
const fakeDataWidthDifferentIdTypes = {
bands: [{ id: 1, name: 'band_1', members: [1, '2', '3'] }],
artists: [
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
index 01e184df22f..c92f9f8643c 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
@@ -149,11 +149,11 @@ export const ReferenceArrayFieldView = (
props: ReferenceArrayFieldViewProps
) => {
const { children, pagination, className, sx } = props;
- const { isPending, total } = useListContext();
+ const { isPending, isPaused, total } = useListContext();
return (
- {isPending ? (
+ {isPending && !isPaused ? (
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx
index d80e2e7f90e..fe0f2b7b616 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx
@@ -25,6 +25,7 @@ import {
SXLink,
SXNoLink,
SlowAccessControl,
+ ErrorWhileFetching,
Themed,
} from './ReferenceField.stories';
import { TextField } from './TextField';
@@ -460,29 +461,8 @@ describe('', () => {
it('should display an error icon if the dataProvider call fails', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
- const dataProvider = testDataProvider({
- getMany: jest.fn().mockRejectedValue(new Error('boo')),
- });
- render(
-
-
-
-
-
-
-
- );
- await new Promise(resolve => setTimeout(resolve, 10));
- const ErrorIcon = screen.queryByRole('presentation', {
- hidden: true,
- });
- expect(ErrorIcon).not.toBeNull();
- await screen.findByText('boo');
+ render();
+ await screen.findByText('ra.notification.http_error');
});
describe('link', () => {
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx
index 6da8201be34..6084f763978 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx
@@ -124,6 +124,18 @@ export const Loading = () => (
);
+const errorDataProvider = {
+ getMany: () => Promise.reject(new Error('Network error')),
+};
+
+export const ErrorWhileFetching = () => (
+
+
+
+
+
+);
+
export const MissingReferenceId = () => (
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
index 87c3098492b..412bf802f12 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import type { ReactNode } from 'react';
-import { Typography } from '@mui/material';
+import { Stack, Typography } from '@mui/material';
import {
type ComponentsOverrides,
styled,
@@ -12,11 +12,11 @@ import ErrorIcon from '@mui/icons-material/Error';
import {
type LinkToType,
useGetRecordRepresentation,
- useTranslate,
type RaRecord,
ReferenceFieldBase,
useReferenceFieldContext,
useFieldValue,
+ Translate,
} from 'ra-core';
import type { UseQueryOptions } from '@tanstack/react-query';
import clsx from 'clsx';
@@ -25,7 +25,7 @@ import { LinearProgress } from '../layout';
import { Link } from '../Link';
import type { FieldProps } from './types';
import { genericMemo } from './genericMemo';
-import { visuallyHidden } from '@mui/utils';
+import { Offline } from '../Offline';
/**
* Fetch reference record, and render its representation, or delegate rendering to child component.
@@ -69,13 +69,12 @@ export const ReferenceField = <
name: PREFIX,
});
const { emptyText } = props;
- const translate = useTranslate();
const id = useFieldValue(props);
if (id == null) {
return emptyText ? (
- {emptyText && translate(emptyText, { _: emptyText })}
+ {emptyText}
) : null;
}
@@ -106,6 +105,7 @@ export interface ReferenceFieldProps<
// useful to prevent click bubbling in a datagrid with rowClick
const stopPropagation = e => e.stopPropagation();
+const defaultOffline = ;
export const ReferenceFieldView = <
RecordType extends Record = Record,
@@ -113,25 +113,37 @@ export const ReferenceFieldView = <
>(
props: ReferenceFieldViewProps
) => {
- const { children, className, emptyText, reference, sx, ...rest } =
- useThemeProps({
- props: props,
- name: PREFIX,
- });
- const { error, link, isLoading, referenceRecord } =
+ const {
+ children,
+ className,
+ emptyText,
+ offline = defaultOffline,
+ reference,
+ sx,
+ ...rest
+ } = useThemeProps({
+ props: props,
+ name: PREFIX,
+ });
+ const { error, link, isLoading, isPaused, referenceRecord } =
useReferenceFieldContext();
const getRecordRepresentation = useGetRecordRepresentation(reference);
- const translate = useTranslate();
if (error) {
return (
-
+
-
- {typeof error === 'string' ? error : error?.message}
-
-
+
+
+ Server communication error
+
+
+
);
}
// We explicitly check isLoading here as the record may not have an id for the reference,
@@ -141,9 +153,12 @@ export const ReferenceFieldView = <
return ;
}
if (!referenceRecord) {
+ if (isPaused) {
+ return offline;
+ }
return emptyText ? (
- {emptyText && translate(emptyText, { _: emptyText })}
+ {emptyText}
) : null;
}
@@ -190,6 +205,7 @@ export interface ReferenceFieldViewProps<
> extends FieldProps,
Omit, 'link'> {
children?: ReactNode;
+ offline?: ReactNode;
reference: string;
resource?: string;
translateChoice?: Function | boolean;
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx
index 313b871c57d..80bd8c3ec78 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx
@@ -18,7 +18,7 @@ describe('', () => {
it('should render an error icon when the request fails', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
render();
- await screen.findByTitle('error');
+ await screen.findByText('Server communication error');
});
it('should accept a filter prop', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx
index 8f576fabe8a..09cc60fd1c8 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { ReactNode } from 'react';
import {
useReferenceManyFieldController,
useRecordContext,
@@ -6,9 +6,15 @@ import {
useCreatePath,
SortPayload,
RaRecord,
+ Translate,
} from 'ra-core';
+import {
+ Typography,
+ TypographyProps,
+ CircularProgress,
+ Stack,
+} from '@mui/material';
import clsx from 'clsx';
-import { Typography, TypographyProps, CircularProgress } from '@mui/material';
import {
ComponentsOverrides,
styled,
@@ -19,6 +25,9 @@ import ErrorIcon from '@mui/icons-material/Error';
import { FieldProps } from './types';
import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
import { Link } from '../Link';
+import { Offline } from '../Offline';
+
+const defaultOffline = ;
/**
* Fetch and render the number of records related to the current one
@@ -49,6 +58,7 @@ export const ReferenceManyCount = (
filter,
sort,
link,
+ offline = defaultOffline,
resource,
source = 'id',
timeout = 1000,
@@ -58,7 +68,7 @@ export const ReferenceManyCount = (
const oneSecondHasPassed = useTimeout(timeout);
const createPath = useCreatePath();
- const { isPending, error, total } =
+ const { isPaused, isPending, error, total } =
useReferenceManyFieldController({
filter,
sort,
@@ -66,23 +76,37 @@ export const ReferenceManyCount = (
perPage: 1,
record,
reference,
- // @ts-ignore remove when #8491 is released
resource,
source,
target,
});
- const body = isPending ? (
- oneSecondHasPassed ? (
-
- ) : (
- ''
- )
- ) : error ? (
-
- ) : (
- total
- );
+ let body: ReactNode = total;
+
+ if (isPaused && total == null) {
+ body = offline;
+ }
+
+ if (isPending && !isPaused && oneSecondHasPassed) {
+ body = ;
+ }
+
+ if (error) {
+ body = (
+
+
+
+
+ Server communication error
+
+
+
+ );
+ }
return (
extends Omit, 'source'>,
Omit {
+ offline?: ReactNode;
reference: string;
source?: string;
target: string;
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx
index af67b19b2dc..7a572bfe383 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx
@@ -8,7 +8,7 @@ import {
} from 'ra-core';
import { Admin, ListGuesser, Resource } from 'react-admin';
import type { AdminProps } from 'react-admin';
-import { ThemeProvider, Box, Stack } from '@mui/material';
+import { ThemeProvider, Box, Stack, Typography } from '@mui/material';
import { createTheme } from '@mui/material/styles';
import fakeDataProvider from 'ra-data-fakerest';
import polyglotI18nProvider from 'ra-i18n-polyglot';
@@ -123,6 +123,40 @@ export const Basic = () => (
);
+const LoadChildrenOnDemand = ({ children }: { children: React.ReactNode }) => {
+ const [showChildren, setShowChildren] = React.useState(false);
+ const handleClick = () => {
+ setShowChildren(true);
+ };
+ return showChildren ? (
+ children
+ ) : (
+
+
+ Don't forget to go offline first
+
+
+
+ );
+};
+
+export const Offline = () => (
+
+
+ }
+ perPage={5}
+ >
+
+
+
+
+
+
+);
+
export const WithSingleFieldList = () => (
diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
index ab2400cbbe6..f895eb4d322 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
@@ -1,4 +1,4 @@
-import React, { ReactElement, ReactNode, useMemo } from 'react';
+import React, { ReactNode, useMemo } from 'react';
import { UseQueryOptions } from '@tanstack/react-query';
import { Typography } from '@mui/material';
import {
@@ -17,6 +17,8 @@ import {
import { FieldProps } from './types';
import { ReferenceFieldView } from './ReferenceField';
+import { Offline } from '../Offline';
+
import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
import { useThemeProps } from '@mui/material/styles';
@@ -47,6 +49,7 @@ export const ReferenceOneField = <
source = 'id',
target,
emptyText,
+ offline = defaultOffline,
sort,
filter,
link,
@@ -91,11 +94,20 @@ export const ReferenceOneField = <
emptyText
) : null;
- return !record ||
+ if (
+ !record ||
(!controllerProps.isPending &&
- controllerProps.referenceRecord == null) ? (
- empty
- ) : (
+ !controllerProps.isPaused &&
+ controllerProps.referenceRecord == null)
+ ) {
+ return empty;
+ }
+
+ if (controllerProps.isPaused && controllerProps.referenceRecord == null) {
+ return offline;
+ }
+
+ return (
@@ -123,7 +135,8 @@ export interface ReferenceOneFieldProps<
source?: string;
filter?: any;
link?: LinkToType;
- emptyText?: string | ReactElement;
+ emptyText?: ReactNode;
+ offline?: ReactNode;
queryOptions?: Omit<
UseQueryOptions<{
data: ReferenceRecordType[];
@@ -138,6 +151,8 @@ export interface ReferenceOneFieldProps<
// leading to an incorrect sort indicator in a datagrid header
ReferenceOneField.sortable = false;
+const defaultOffline = ;
+
const PREFIX = 'RaReferenceOneField';
declare module '@mui/material/styles' {
diff --git a/packages/ra-ui-materialui/src/index.ts b/packages/ra-ui-materialui/src/index.ts
index 82696d33f40..0a645b57349 100644
--- a/packages/ra-ui-materialui/src/index.ts
+++ b/packages/ra-ui-materialui/src/index.ts
@@ -12,3 +12,4 @@ export * from './list';
export * from './preferences';
export * from './AdminUI';
export * from './AdminContext';
+export * from './Offline';
diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
index 425041913f4..7c78f9994ce 100644
--- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
+++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
@@ -48,6 +48,7 @@ import {
import type { CommonInputProps } from './CommonInputProps';
import { InputHelperText } from './InputHelperText';
import { sanitizeInputRestProps } from './sanitizeInputRestProps';
+import { Offline } from '../Offline';
const defaultFilterOptions = createFilterOptions();
@@ -161,6 +162,7 @@ export const AutocompleteInput = <
isRequired: isRequiredOverride,
label,
limitChoicesToValue,
+ loadingText = 'ra.message.loading',
matchSuggestion,
margin,
fieldState: fieldStateOverride,
@@ -168,6 +170,7 @@ export const AutocompleteInput = <
formState: formStateOverride,
multiple = false,
noOptionsText,
+ offline = defaultOffline,
onBlur,
onChange,
onCreate,
@@ -197,6 +200,8 @@ export const AutocompleteInput = <
const {
allChoices,
isPending,
+ isPaused,
+ isPlaceholderData,
error: fetchError,
resource,
source,
@@ -610,12 +615,22 @@ If you provided a React element for the optionText prop, you must also provide t
const renderHelperText = !!fetchError || helperText !== false || invalid;
const handleInputRef = useForkRef(field.ref, TextFieldProps?.inputRef);
+
return (
<>
any;
inputText?: (option: any) => string;
+ offline?: ReactNode;
onChange?: (
// We can't know upfront what the value type will be
value: Multiple extends true ? any[] : any,
@@ -912,6 +931,7 @@ const areSelectedItemsEqual = (
};
const DefaultFilterToQuery = searchText => ({ q: searchText });
+const defaultOffline = ;
declare module '@mui/material/styles' {
interface ComponentNameToClassKey {
diff --git a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx
index b9bc471acdc..78aacf96e5b 100644
--- a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.spec.tsx
@@ -4,6 +4,9 @@ import { render, screen, fireEvent } from '@testing-library/react';
import { Basic } from './InPlaceEditor.stories';
describe('InPlaceEditor', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
it('should render the field value on mount', async () => {
render();
await screen.findByText('John Doe');
diff --git a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx
index 53ce1d9130f..1704f6c190d 100644
--- a/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx
+++ b/packages/ra-ui-materialui/src/input/InPlaceEditor/InPlaceEditor.tsx
@@ -10,6 +10,7 @@ import {
RecordContextProvider,
type UseUpdateOptions,
type RaRecord,
+ useIsOffine,
} from 'ra-core';
import isEqual from 'lodash/isEqual';
import { styled } from '@mui/material/styles';
@@ -130,22 +131,33 @@ export const InPlaceEditor = <
const notify = useNotify();
const translate = useTranslate();
const [update] = useUpdate();
+ const isOffline = useIsOffine();
const {
meta: mutationMeta,
onSuccess = () => {
dispatch({ type: 'success' });
if (mutationMode !== 'undoable' && !notifyOnSuccess) return;
- notify(`resources.${resource}.notifications.updated`, {
- type: 'info',
- messageArgs: {
- smart_count: 1,
- _: translate('ra.notification.updated', {
+ notify(
+ isOffline
+ ? `resources.${resource}.notifications.pending_update`
+ : `resources.${resource}.notifications.updated`,
+ {
+ type: 'info',
+ messageArgs: {
smart_count: 1,
- }),
- },
- undoable: mutationMode === 'undoable',
- });
+ _: translate(
+ isOffline
+ ? 'ra.notification.pending_update'
+ : 'ra.notification.updated',
+ {
+ smart_count: 1,
+ }
+ ),
+ },
+ undoable: mutationMode === 'undoable',
+ }
+ );
},
onError = error => {
notify('ra.notification.http_error', {
diff --git a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx
index 19830fb0f19..8205dcdb5a4 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx
@@ -1,10 +1,5 @@
import * as React from 'react';
-import {
- DataProvider,
- Form,
- testDataProvider,
- TestMemoryRouter,
-} from 'ra-core';
+import { DataProvider, Form, TestMemoryRouter } from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import { Admin, Resource } from 'react-admin';
@@ -19,6 +14,7 @@ import { ReferenceArrayInput } from './ReferenceArrayInput';
import { AutocompleteArrayInput } from './AutocompleteArrayInput';
import { SelectArrayInput } from './SelectArrayInput';
import { CheckboxGroupInput } from './CheckboxGroupInput';
+import { Typography } from '@mui/material';
export default { title: 'ra-ui-materialui/input/ReferenceArrayInput' };
@@ -28,23 +24,21 @@ const tags = [
{ id: 2, name: 'Design' },
{ id: 3, name: 'Painting' },
{ id: 4, name: 'Photography' },
+ { id: 5, name: 'Sculpture' },
+ { id: 6, name: 'Urbanism' },
+ { id: 7, name: 'Video' },
+ { id: 8, name: 'Web' },
+ { id: 9, name: 'Writing' },
+ { id: 10, name: 'Other' },
];
-const dataProvider = testDataProvider({
- // @ts-ignore
- getList: () =>
- Promise.resolve({
- data: tags,
- total: tags.length,
- }),
- // @ts-ignore
- getMany: (resource, params) => {
- console.log('getMany', resource, params);
- return Promise.resolve({
- data: params.ids.map(id => tags.find(tag => tag.id === id)),
- });
+const dataProvider = fakeRestProvider(
+ {
+ tags,
},
-});
+ process.env.NODE_ENV !== 'test',
+ process.env.NODE_ENV !== 'test' ? 300 : 0
+);
const i18nProvider = polyglotI18nProvider(() => englishMessages);
@@ -74,6 +68,51 @@ export const Basic = () => (
);
+const LoadChildrenOnDemand = ({ children }: { children: React.ReactNode }) => {
+ const [showChildren, setShowChildren] = React.useState(false);
+ const handleClick = () => {
+ setShowChildren(true);
+ };
+ return showChildren ? (
+ children
+ ) : (
+
+
+ Don't forget to go offline first
+
+
+
+ );
+};
+
+export const Offline = () => (
+
+
+
+ (
+
+
+
+
+
+
+
+ )}
+ />
+
+
+);
+
export const WithAutocompleteInput = () => (
{
reference,
sort,
filter = defaultFilter,
+ offline,
} = props;
if (React.Children.count(children) !== 1) {
throw new Error(
@@ -94,8 +97,17 @@ export const ReferenceArrayInput = (props: ReferenceArrayInputProps) => {
sort,
filter,
});
+ const { isPaused, allChoices } = controllerProps;
- return (
+ return isPaused && allChoices == null ? (
+
+ {offline ?? }
+
+ ) : (
{children}
@@ -110,7 +122,8 @@ const defaultFilter = {};
export interface ReferenceArrayInputProps
extends InputProps,
UseReferenceArrayInputParams {
- children?: ReactElement;
+ children?: ReactNode;
label?: string;
+ offline?: ReactNode;
[key: string]: any;
}
diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx
index fda31da9ad9..30229ea8fe5 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx
@@ -109,6 +109,94 @@ export const Basic = ({ dataProvider = dataProviderWithAuthors }) => (
);
+const LoadChildrenOnDemand = ({ children }: { children: React.ReactNode }) => {
+ const [showChildren, setShowChildren] = React.useState(false);
+ const handleClick = () => {
+ setShowChildren(true);
+ };
+ return showChildren ? (
+ children
+ ) : (
+
+
+ Don't forget to go offline first
+
+
+
+ );
+};
+
+export const Offline = ({ dataProvider = dataProviderWithAuthors }) => (
+
+
+
+ `${record.first_name} ${record.last_name}`
+ }
+ />
+ {
+ console.log(data);
+ },
+ }}
+ >
+
+
+
+
+
+
+ }
+ />
+
+
+);
+
+export const CustomOffline = ({ dataProvider = dataProviderWithAuthors }) => (
+
+
+
+ `${record.first_name} ${record.last_name}`
+ }
+ />
+ {
+ console.log(data);
+ },
+ }}
+ >
+
+
+ You're offline
}
+ />
+
+
+
+ }
+ />
+
+
+);
+
const tags = [
{ id: 5, name: 'lorem' },
{ id: 6, name: 'ipsum' },
diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
index 0394f49622c..384e18d8021 100644
--- a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
+++ b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx
@@ -1,7 +1,9 @@
-import React from 'react';
+import React, { ReactNode } from 'react';
import { ReferenceInputBase, ReferenceInputBaseProps } from 'ra-core';
import { AutocompleteInput } from './AutocompleteInput';
+import { Offline } from '../Offline';
+import { Labeled } from '../Labeled';
/**
* An Input component for choosing a reference record. Useful for foreign keys.
@@ -64,7 +66,7 @@ import { AutocompleteInput } from './AutocompleteInput';
* a `setFilters` function. You can call this function to filter the results.
*/
export const ReferenceInput = (props: ReferenceInputProps) => {
- const { children = defaultChildren, ...rest } = props;
+ const { children = defaultChildren, offline, ...rest } = props;
if (props.validate && process.env.NODE_ENV !== 'production') {
throw new Error(
@@ -72,7 +74,22 @@ export const ReferenceInput = (props: ReferenceInputProps) => {
);
}
- return {children};
+ return (
+
+ {offline ?? }
+
+ }
+ >
+ {children}
+
+ );
};
const defaultChildren = ;
@@ -82,5 +99,6 @@ export interface ReferenceInputProps extends ReferenceInputBaseProps {
* Call validate on the child component instead
*/
validate?: never;
+ offline?: ReactNode;
[key: string]: any;
}
diff --git a/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx b/packages/ra-ui-materialui/src/layout/LoadingIndicator.tsx
index 99007ab5bf7..4bde713a0e3 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,53 @@ 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}
);
};
diff --git a/packages/ra-ui-materialui/src/layout/Title.tsx b/packages/ra-ui-materialui/src/layout/Title.tsx
index 057d9849a1e..d8c8e8a928e 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, ReactNode } 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?: ReactNode;
preferenceKey?: string | false;
}
diff --git a/packages/ra-ui-materialui/src/list/List.stories.tsx b/packages/ra-ui-materialui/src/list/List.stories.tsx
index 90189f61f78..6e1296d610f 100644
--- a/packages/ra-ui-materialui/src/list/List.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/List.stories.tsx
@@ -147,10 +147,7 @@ const data = {
const defaultDataProvider = fakeRestDataProvider(data);
const BookList = () => {
- const { error, isPending } = useListContext();
- if (isPending) {
- return Loading...
;
- }
+ const { error } = useListContext();
if (error) {
return Error: {error.message}
;
}
diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx
index e6cd323b79f..2fc4eb65fd6 100644
--- a/packages/ra-ui-materialui/src/list/ListView.tsx
+++ b/packages/ra-ui-materialui/src/list/ListView.tsx
@@ -42,7 +42,9 @@ export const ListView = (
defaultTitle,
data,
error,
+ isPaused,
isPending,
+ isPlaceholderData,
filterValues,
resource,
total,
@@ -72,6 +74,8 @@ export const ListView = (
empty !== false && {empty}
;
const shouldRenderEmptyPage =
+ !isPaused &&
+ !isPlaceholderData &&
!error &&
// the list is not loading data for the first time
!isPending &&
diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx
index afb4c1d69d8..3d7da1ad806 100644
--- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx
+++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx
@@ -23,7 +23,7 @@ import {
useTranslate,
} from 'ra-core';
import * as React from 'react';
-import { isValidElement, type ReactElement } from 'react';
+import { isValidElement, type ReactNode } from 'react';
import { ListNoResults } from '../ListNoResults';
import { SimpleListLoading } from './SimpleListLoading';
@@ -33,6 +33,7 @@ import {
SimpleListItem,
type SimpleListItemProps,
} from './SimpleListItem';
+import { Offline } from '../../Offline';
/**
* The component renders a list of records as a Material UI .
@@ -85,6 +86,7 @@ export const SimpleList = (
leftAvatar,
leftIcon,
linkType,
+ offline = DefaultOffline,
rowClick,
primaryText,
rightAvatar,
@@ -97,10 +99,10 @@ export const SimpleList = (
resource,
...rest
} = props;
- const { data, isPending, total } =
+ const { data, isPaused, isPending, isPlaceholderData, total } =
useListContextWithProps(props);
- if (isPending === true) {
+ if (isPending === true && !isPaused) {
return (
(
);
}
- if (data == null || data.length === 0 || total === 0) {
+ if (
+ (data == null || data.length === 0 || total === 0) &&
+ !isPaused &&
+ !isPlaceholderData
+ ) {
if (empty) {
return empty;
}
@@ -120,9 +126,17 @@ export const SimpleList = (
return null;
}
+ if (isPaused && (isPlaceholderData || data == null)) {
+ if (offline) {
+ return offline;
+ }
+
+ return null;
+ }
+
return (
- {data.map((record, rowIndex) => (
+ {data?.map((record, rowIndex) => (
extends SimpleListBaseProps,
Omit {
className?: string;
- empty?: ReactElement;
+ empty?: ReactNode;
+ offline?: ReactNode;
hasBulkActions?: boolean;
// can be injected when using the component without context
resource?: string;
@@ -285,6 +300,7 @@ const Root = styled(List, {
});
const DefaultEmpty = ;
+const DefaultOffline = ;
declare module '@mui/material/styles' {
interface ComponentNameToClassKey {
diff --git a/packages/ra-ui-materialui/src/list/SingleFieldList.stories.tsx b/packages/ra-ui-materialui/src/list/SingleFieldList.stories.tsx
index b990b1666db..5b20cce8548 100644
--- a/packages/ra-ui-materialui/src/list/SingleFieldList.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/SingleFieldList.stories.tsx
@@ -5,6 +5,7 @@ import {
ResourceDefinitionContextProvider,
useList,
TestMemoryRouter,
+ ListControllerResult,
} from 'ra-core';
import { Typography, Divider as MuiDivider } from '@mui/material';
@@ -29,11 +30,14 @@ export default {
const Wrapper = ({
children,
- data = [bookGenres[2], bookGenres[4], bookGenres[1]],
+ listContext = {
+ data: [bookGenres[2], bookGenres[4], bookGenres[1]],
+ },
+}: {
+ children: React.ReactNode;
+ listContext?: Partial;
}) => {
- const listContextValue = useList({
- data,
- });
+ const listContextValue = useList(listContext);
return (
(
);
export const NoData = () => (
-
+
);
export const Empty = ({ listContext = { data: [] } }) => (
-
+
No genres} />
-
+
);
export const Loading = () => (
-
+
+
+
+);
+
+export const Offline = () => (
+
-
+
);
export const Direction = () => (
diff --git a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
index de5e681b4a3..96b33f9c194 100644
--- a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
+++ b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx
@@ -19,6 +19,7 @@ import {
import { LinearProgress } from '../layout/LinearProgress';
import { Link } from '../Link';
+import { Offline } from '../Offline';
/**
* Iterator component to be used to display a list of entities, using a single field
@@ -63,26 +64,39 @@ export const SingleFieldList = (inProps: SingleFieldListProps) => {
className,
children,
empty,
+ offline = DefaultOffline,
linkType = 'edit',
gap = 1,
direction = 'row',
...rest
} = props;
- const { data, total, isPending } = useListContextWithProps(props);
+ const { data, total, isPaused, isPending, isPlaceholderData } =
+ useListContextWithProps(props);
const resource = useResourceContext(props);
const createPath = useCreatePath();
- if (isPending === true) {
+ if (isPending && !isPaused) {
return ;
}
- if (data == null || data.length === 0 || total === 0) {
+ if (
+ (data == null || data.length === 0 || total === 0) &&
+ !isPaused &&
+ !isPlaceholderData
+ ) {
if (empty) {
return empty;
}
return null;
}
+ if (isPaused && (isPlaceholderData || total == null)) {
+ if (offline) {
+ return offline;
+ }
+
+ return null;
+ }
return (
{
className={className}
{...sanitizeListRestProps(rest)}
>
- {data.map((record, rowIndex) => {
+ {data?.map((record, rowIndex) => {
const resourceLinkPath = !linkType
? false
: createPath({
@@ -135,7 +149,8 @@ export const SingleFieldList = (inProps: SingleFieldListProps) => {
export interface SingleFieldListProps
extends StackProps {
className?: string;
- empty?: React.ReactElement;
+ empty?: React.ReactNode;
+ offline?: React.ReactNode;
linkType?: string | false;
children?: React.ReactNode;
// can be injected when using the component without context
@@ -193,3 +208,5 @@ declare module '@mui/material/styles' {
};
}
}
+
+const DefaultOffline = ;
diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
index 4d6f2fe95b2..beca48005d6 100644
--- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
+++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
@@ -39,6 +39,7 @@ import {
import { BulkActionsToolbar } from '../BulkActionsToolbar';
import { BulkDeleteButton } from '../../button';
import { ListNoResults } from '../ListNoResults';
+import { Offline } from '../../Offline';
const defaultBulkActionButtons = ;
@@ -146,6 +147,7 @@ export const Datagrid: React.ForwardRefExoticComponent<
hover,
isRowSelectable,
isRowExpandable,
+ offline = DefaultOffline,
resource,
rowClick,
rowSx,
@@ -159,7 +161,9 @@ export const Datagrid: React.ForwardRefExoticComponent<
const {
sort,
data,
+ isPaused,
isPending,
+ isPlaceholderData,
onSelect,
onToggleItem,
selectedIds,
@@ -217,7 +221,7 @@ export const Datagrid: React.ForwardRefExoticComponent<
[data, isRowSelectable, onSelect, onToggleItem, selectedIds]
);
- if (isPending === true) {
+ if (isPending && !isPaused) {
return (
*/
empty?: ReactElement;
+ /**
+ * The component used to render the empty table when user are offline.
+ *
+ * @see https://marmelab.com/react-admin/Datagrid.html#offline
+ * @example
+ * import { List, Datagrid } from 'react-admin';
+ *
+ * const CustomOffline = () => We couldn't fetch book as you are offline
;
+ *
+ * const PostList = () => (
+ *
+ * }>
+ * ...
+ *
+ *
+ * );
+ */
+ offline?: ReactElement;
+
/**
* A function that returns whether the row for a record is expandable.
*
@@ -610,3 +644,4 @@ const sanitizeRestProps = props =>
Datagrid.displayName = 'Datagrid';
const DefaultEmpty = ;
+const DefaultOffline = ;
diff --git a/packages/ra-ui-materialui/src/list/datatable/DataTable.tsx b/packages/ra-ui-materialui/src/list/datatable/DataTable.tsx
index 91c7c7059f5..90f4a62f21a 100644
--- a/packages/ra-ui-materialui/src/list/datatable/DataTable.tsx
+++ b/packages/ra-ui-materialui/src/list/datatable/DataTable.tsx
@@ -38,8 +38,10 @@ import { DataTableColumn } from './DataTableColumn';
import { DataTableNumberColumn } from './DataTableNumberColumn';
import { ColumnsSelector } from './ColumnsSelector';
import { DataTableRowSxContext } from './DataTableRowSxContext';
+import { Offline } from '../../Offline';
const DefaultEmpty = ;
+const DefaultOffline = ;
const DefaultFoot = (_props: { children: ReactNode }) => null;
const PREFIX = 'RaDataTable';
@@ -149,6 +151,7 @@ export const DataTable = React.forwardRef(function DataTable<
children,
className,
empty = DefaultEmpty,
+ offline = DefaultOffline,
expand,
bulkActionsToolbar,
bulkActionButtons = canDelete ? defaultBulkActionButtons : false,
@@ -184,6 +187,7 @@ export const DataTable = React.forwardRef(function DataTable<
hasBulkActions={hasBulkActions}
loading={loading}
empty={empty}
+ offline={offline}
>
*/
empty?: ReactNode;
+ /**
+ * The component used to render the offline table.
+ *
+ * @see https://marmelab.com/react-admin/DataTable.html#offline
+ * @example
+ * import { List, DataTable } from 'react-admin';
+ *
+ * const CustomOffline = () => No books found
;
+ *
+ * const PostList = () => (
+ *
+ * }>
+ * ...
+ *
+ *
+ * );
+ */
+ offline?: ReactNode;
+
/**
* A function that returns whether the row for a record is expandable.
*
diff --git a/yarn.lock b/yarn.lock
index cd71595216b..f6e8876baa0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -32,7 +32,43 @@ __metadata:
languageName: node
linkType: hard
-"@apollo/client@npm:^3.12.4, @apollo/client@npm:^3.3.19, @apollo/client@npm:^3.9.11":
+"@apollo/client@npm:^3.12.11":
+ version: 3.13.8
+ resolution: "@apollo/client@npm:3.13.8"
+ dependencies:
+ "@graphql-typed-document-node/core": "npm:^3.1.1"
+ "@wry/caches": "npm:^1.0.0"
+ "@wry/equality": "npm:^0.5.6"
+ "@wry/trie": "npm:^0.5.0"
+ graphql-tag: "npm:^2.12.6"
+ hoist-non-react-statics: "npm:^3.3.2"
+ optimism: "npm:^0.18.0"
+ prop-types: "npm:^15.7.2"
+ rehackt: "npm:^0.1.0"
+ symbol-observable: "npm:^4.0.0"
+ ts-invariant: "npm:^0.10.3"
+ tslib: "npm:^2.3.0"
+ zen-observable-ts: "npm:^1.2.5"
+ peerDependencies:
+ graphql: ^15.0.0 || ^16.0.0
+ graphql-ws: ^5.5.5 || ^6.0.3
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc
+ subscriptions-transport-ws: ^0.9.0 || ^0.11.0
+ peerDependenciesMeta:
+ graphql-ws:
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ subscriptions-transport-ws:
+ optional: true
+ checksum: 0e5032c1ae1dbef72a01f87af06b84bf505d60e71eba7cb9f20f8284778d8ead65fc1b7eacc570eccb8d045577d7194e38401fbfbdf56c197e159ca91ef11755
+ languageName: node
+ linkType: hard
+
+"@apollo/client@npm:^3.12.4, @apollo/client@npm:^3.3.19":
version: 3.12.4
resolution: "@apollo/client@npm:3.12.4"
dependencies:
@@ -2010,43 +2046,43 @@ __metadata:
languageName: node
linkType: hard
-"@graphql-tools/merge@npm:^9.0.3":
- version: 9.0.3
- resolution: "@graphql-tools/merge@npm:9.0.3"
+"@graphql-tools/merge@npm:^9.0.24":
+ version: 9.0.24
+ resolution: "@graphql-tools/merge@npm:9.0.24"
dependencies:
- "@graphql-tools/utils": "npm:^10.0.13"
+ "@graphql-tools/utils": "npm:^10.8.6"
tslib: "npm:^2.4.0"
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
- checksum: ce2a6763488dbeeb778824780037ce5a00fd8c4a6337078d52c4fb4bcac28759b801ede280014d281472ee92416114e4c0eca621c618db617cb351df7d751570
+ checksum: 04e2b402bfc05f844a66bd2c687b7aac1c61e321dceb655e698b11044247bd5940ba9d684ff518924b697b943c1f0785ac8d1ac864397dd8f59e8c823efa5376
languageName: node
linkType: hard
-"@graphql-tools/schema@npm:^10.0.3":
- version: 10.0.3
- resolution: "@graphql-tools/schema@npm:10.0.3"
+"@graphql-tools/schema@npm:^10.0.18":
+ version: 10.0.23
+ resolution: "@graphql-tools/schema@npm:10.0.23"
dependencies:
- "@graphql-tools/merge": "npm:^9.0.3"
- "@graphql-tools/utils": "npm:^10.0.13"
+ "@graphql-tools/merge": "npm:^9.0.24"
+ "@graphql-tools/utils": "npm:^10.8.6"
tslib: "npm:^2.4.0"
- value-or-promise: "npm:^1.0.12"
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
- checksum: 420bfa29d00927da085a3e521d7d6de5694f3abcdf5ba18655cc2a6b6145816d74503b13ba3ea15c7c65411023c9d81cfb73e7d49aa35ccfb91943f16ab9db8f
+ checksum: f8b4dcc4751bde2e41e2fd7cafc0b01c6d69e0eee7022918fbb372695358138a95582fd6cf83dff13f98665b19f9ad234d88ffcd4e6969cb70ec2884eb4c805c
languageName: node
linkType: hard
-"@graphql-tools/utils@npm:^10.0.13":
- version: 10.1.3
- resolution: "@graphql-tools/utils@npm:10.1.3"
+"@graphql-tools/utils@npm:^10.8.6":
+ version: 10.8.6
+ resolution: "@graphql-tools/utils@npm:10.8.6"
dependencies:
"@graphql-typed-document-node/core": "npm:^3.1.1"
- cross-inspect: "npm:1.0.0"
- dset: "npm:^3.1.2"
+ "@whatwg-node/promise-helpers": "npm:^1.0.0"
+ cross-inspect: "npm:1.0.1"
+ dset: "npm:^3.1.4"
tslib: "npm:^2.4.0"
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
- checksum: 657e0758b3cfcbccbaa0c5bf81277c03e02bda32070e71e9f7f728ad692893ef0a0c4bc873b6972edf0b96d0d6397df6e55a8db3e2050bd9c00f6a5bf8881858
+ checksum: 17f727eb85415c15c5920ab9ef4648e0d205e1ca8b7d8539ac84f55da04ed60464313792456ebbde30bb883c0abde8df81919fd22f2ed5096b873920e84bef4b
languageName: node
linkType: hard
@@ -2743,7 +2779,7 @@ __metadata:
languageName: node
linkType: hard
-"@mui/private-theming@npm:^5.16.14":
+"@mui/private-theming@npm:^5.17.1":
version: 5.17.1
resolution: "@mui/private-theming@npm:5.17.1"
dependencies:
@@ -2862,14 +2898,14 @@ __metadata:
linkType: hard
"@mui/system@npm:^5.16.12, @mui/system@npm:^5.16.14":
- version: 5.16.14
- resolution: "@mui/system@npm:5.16.14"
+ version: 5.17.1
+ resolution: "@mui/system@npm:5.17.1"
dependencies:
"@babel/runtime": "npm:^7.23.9"
- "@mui/private-theming": "npm:^5.16.14"
+ "@mui/private-theming": "npm:^5.17.1"
"@mui/styled-engine": "npm:^5.16.14"
- "@mui/types": "npm:^7.2.15"
- "@mui/utils": "npm:^5.16.14"
+ "@mui/types": "npm:~7.2.15"
+ "@mui/utils": "npm:^5.17.1"
clsx: "npm:^2.1.0"
csstype: "npm:^3.1.3"
prop-types: "npm:^15.8.1"
@@ -2885,7 +2921,7 @@ __metadata:
optional: true
"@types/react":
optional: true
- checksum: d7ab8dfd9fbecbde4423a0d432e63f45cd8c96bb4e48116f9f9b46cb001c2e32df3a1f09727f8b30c1bc182774cc33e338b1475287a2985dba795ee5486fc4cb
+ checksum: ab74424e536164b720126ddd31ff0ceea4fb51d72f8d18f9be5621b33f8bbdf7fa8c96f8d1d2c4544ddacbaa84df1a197667f10cbe8915e00df103930e40f56e
languageName: node
linkType: hard
@@ -3815,142 +3851,142 @@ __metadata:
languageName: node
linkType: hard
-"@rollup/rollup-android-arm-eabi@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-android-arm-eabi@npm:4.40.1"
+"@rollup/rollup-android-arm-eabi@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-android-arm-eabi@npm:4.39.0"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
-"@rollup/rollup-android-arm64@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-android-arm64@npm:4.40.1"
+"@rollup/rollup-android-arm64@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-android-arm64@npm:4.39.0"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
-"@rollup/rollup-darwin-arm64@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-darwin-arm64@npm:4.40.1"
+"@rollup/rollup-darwin-arm64@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-darwin-arm64@npm:4.39.0"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
-"@rollup/rollup-darwin-x64@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-darwin-x64@npm:4.40.1"
+"@rollup/rollup-darwin-x64@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-darwin-x64@npm:4.39.0"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
-"@rollup/rollup-freebsd-arm64@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-freebsd-arm64@npm:4.40.1"
+"@rollup/rollup-freebsd-arm64@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-freebsd-arm64@npm:4.39.0"
conditions: os=freebsd & cpu=arm64
languageName: node
linkType: hard
-"@rollup/rollup-freebsd-x64@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-freebsd-x64@npm:4.40.1"
+"@rollup/rollup-freebsd-x64@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-freebsd-x64@npm:4.39.0"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm-gnueabihf@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.40.1"
+"@rollup/rollup-linux-arm-gnueabihf@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.39.0"
conditions: os=linux & cpu=arm & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm-musleabihf@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.40.1"
+"@rollup/rollup-linux-arm-musleabihf@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.39.0"
conditions: os=linux & cpu=arm & libc=musl
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm64-gnu@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.40.1"
+"@rollup/rollup-linux-arm64-gnu@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.39.0"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm64-musl@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-arm64-musl@npm:4.40.1"
+"@rollup/rollup-linux-arm64-musl@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-arm64-musl@npm:4.39.0"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
-"@rollup/rollup-linux-loongarch64-gnu@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.40.1"
+"@rollup/rollup-linux-loongarch64-gnu@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.39.0"
conditions: os=linux & cpu=loong64 & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.1"
+"@rollup/rollup-linux-powerpc64le-gnu@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.39.0"
conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-riscv64-gnu@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.40.1"
+"@rollup/rollup-linux-riscv64-gnu@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.39.0"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-riscv64-musl@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.40.1"
+"@rollup/rollup-linux-riscv64-musl@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.39.0"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
-"@rollup/rollup-linux-s390x-gnu@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.40.1"
+"@rollup/rollup-linux-s390x-gnu@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.39.0"
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-x64-gnu@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-x64-gnu@npm:4.40.1"
+"@rollup/rollup-linux-x64-gnu@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-x64-gnu@npm:4.39.0"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-x64-musl@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-linux-x64-musl@npm:4.40.1"
+"@rollup/rollup-linux-x64-musl@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-linux-x64-musl@npm:4.39.0"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
-"@rollup/rollup-win32-arm64-msvc@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.40.1"
+"@rollup/rollup-win32-arm64-msvc@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.39.0"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
-"@rollup/rollup-win32-ia32-msvc@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.40.1"
+"@rollup/rollup-win32-ia32-msvc@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.39.0"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
-"@rollup/rollup-win32-x64-msvc@npm:4.40.1":
- version: 4.40.1
- resolution: "@rollup/rollup-win32-x64-msvc@npm:4.40.1"
+"@rollup/rollup-win32-x64-msvc@npm:4.39.0":
+ version: 4.39.0
+ resolution: "@rollup/rollup-win32-x64-msvc@npm:4.39.0"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@@ -4312,10 +4348,10 @@ __metadata:
languageName: node
linkType: hard
-"@tanstack/query-core@npm:5.47.0":
- version: 5.47.0
- resolution: "@tanstack/query-core@npm:5.47.0"
- checksum: 2d2378dbde2b0610b6356fcdb56904aa9d41c140c17ceb55e257b918c8555484ff36743a6a37768575631be96b9291eedc53723cd80326783095499cb97db049
+"@tanstack/query-core@npm:5.47.0, @tanstack/query-core@npm:5.81.2":
+ version: 5.81.2
+ resolution: "@tanstack/query-core@npm:5.81.2"
+ checksum: 36a6bddec2e7512015bcfbb0d7b0876fab418de9e0ef21ad403598276960e0b7d53efd62832ce462738ad22d9883e31cb5403eafc65dfd9b2f6744c22a9d8e42
languageName: node
linkType: hard
@@ -4326,6 +4362,25 @@ __metadata:
languageName: node
linkType: hard
+"@tanstack/query-persist-client-core@npm:5.81.2":
+ version: 5.81.2
+ resolution: "@tanstack/query-persist-client-core@npm:5.81.2"
+ dependencies:
+ "@tanstack/query-core": "npm:5.81.2"
+ checksum: 65aebf52678cbadae81ec8bdf2764781cf975ed0eaf7ee37f77c536139da9f8c9220a1006c0ded50c1abd2d70ff0b76ed5e352dd4c6d3da782df240a2a3d3cbc
+ languageName: node
+ linkType: hard
+
+"@tanstack/query-sync-storage-persister@npm:^5.47.0":
+ version: 5.81.2
+ resolution: "@tanstack/query-sync-storage-persister@npm:5.81.2"
+ dependencies:
+ "@tanstack/query-core": "npm:5.81.2"
+ "@tanstack/query-persist-client-core": "npm:5.81.2"
+ checksum: cba3d0c0bf032c5a4aac5c49f2ed18f2660a2ae3640741a299a71097682cd15847a58c986827f0a01310d5da407591159cc1ed0b1a8b258100644a98aa554b8a
+ languageName: node
+ linkType: hard
+
"@tanstack/react-query-devtools@npm:^5.21.7":
version: 5.47.0
resolution: "@tanstack/react-query-devtools@npm:5.47.0"
@@ -4338,6 +4393,18 @@ __metadata:
languageName: node
linkType: hard
+"@tanstack/react-query-persist-client@npm:^5.47.0":
+ version: 5.81.2
+ resolution: "@tanstack/react-query-persist-client@npm:5.81.2"
+ dependencies:
+ "@tanstack/query-persist-client-core": "npm:5.81.2"
+ peerDependencies:
+ "@tanstack/react-query": ^5.81.2
+ react: ^18 || ^19
+ checksum: cd4176744c6a96295d9c6441e212420b34f83f10f82c59de87aaba0d918de27964fb1d3cd0d91d2cff2e94624fe978a9eb33a348980bf35a624ccb8e5cec0a2f
+ languageName: node
+ linkType: hard
+
"@tanstack/react-query@npm:^5.21.7":
version: 5.47.0
resolution: "@tanstack/react-query@npm:5.47.0"
@@ -5747,6 +5814,15 @@ __metadata:
languageName: node
linkType: hard
+"@whatwg-node/promise-helpers@npm:^1.0.0":
+ version: 1.3.1
+ resolution: "@whatwg-node/promise-helpers@npm:1.3.1"
+ dependencies:
+ tslib: "npm:^2.6.3"
+ checksum: bb47e71cb588c2bdc45cc78044f8e472b1600d3022f10d59bb70a68bd6a2d2f749f5bbe5858a9b0dccbc93c4c96092acc28e870ace1174d328e14fa8e8db14fa
+ languageName: node
+ linkType: hard
+
"@wry/caches@npm:^1.0.0":
version: 1.0.1
resolution: "@wry/caches@npm:1.0.1"
@@ -5886,15 +5962,6 @@ __metadata:
languageName: node
linkType: hard
-"acorn-dynamic-import@npm:^4.0.0":
- version: 4.0.0
- resolution: "acorn-dynamic-import@npm:4.0.0"
- peerDependencies:
- acorn: ^6.0.0
- checksum: 5450c917d28f39cabf64495928a711f446cb6a4731d45fcd8f160cc3ceb6fee3e1b4a8cb308b5ba4e9a0e450742f67d7295322033ffaa378a355af6cd2232693
- languageName: node
- linkType: hard
-
"acorn-globals@npm:^7.0.0":
version: 7.0.1
resolution: "acorn-globals@npm:7.0.1"
@@ -5930,15 +5997,6 @@ __metadata:
languageName: node
linkType: hard
-"acorn@npm:^6.1.1":
- version: 6.4.2
- resolution: "acorn@npm:6.4.2"
- bin:
- acorn: bin/acorn
- checksum: 52a72d5d785fa64a95880f2951021a38954f8f69a4944dfeab6fb1449b0f02293eae109a56d55b58ff31a90a00d16a804658a12db8ef834c20b3d1201fe5ba5b
- languageName: node
- linkType: hard
-
"acorn@npm:^8.1.0, acorn@npm:^8.14.0, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2":
version: 8.14.1
resolution: "acorn@npm:8.14.1"
@@ -7285,13 +7343,20 @@ __metadata:
languageName: node
linkType: hard
-"cli-spinners@npm:2.6.1, cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.7.0":
+"cli-spinners@npm:2.6.1, cli-spinners@npm:^2.7.0":
version: 2.9.2
resolution: "cli-spinners@npm:2.9.2"
checksum: 907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3
languageName: node
linkType: hard
+"cli-spinners@npm:^2.5.0":
+ version: 2.7.0
+ resolution: "cli-spinners@npm:2.7.0"
+ checksum: 5c781ace5c8f304ae4d138837f19cf88f03a97de3c3e388f9d1d6434146f06f6ce2a161d6237b3bb86448a05fbcbb20084f3fea96077e42a655b273e39c6f08d
+ languageName: node
+ linkType: hard
+
"cli-table3@npm:~0.6.1":
version: 0.6.1
resolution: "cli-table3@npm:0.6.1"
@@ -7846,12 +7911,12 @@ __metadata:
languageName: node
linkType: hard
-"cross-inspect@npm:1.0.0":
- version: 1.0.0
- resolution: "cross-inspect@npm:1.0.0"
+"cross-inspect@npm:1.0.1":
+ version: 1.0.1
+ resolution: "cross-inspect@npm:1.0.1"
dependencies:
tslib: "npm:^2.4.0"
- checksum: 53530865c357c69a5a0543e2f2c61d3d46c9c316a19169372f5094cfb0a7c7e674f2daf2d5253a6731dfd9a8538aa4a4e13c6b4613b6f72b48bb0c41d2015ff4
+ checksum: 2493ee47a801b46ede1c42ca6242b8d2059f7319b5baf23887bbaf46a6ea8e536d2e271d0990176c05092f67b32d084ffd8c93e7c1227eff4a06cceadb49af47
languageName: node
linkType: hard
@@ -8686,7 +8751,7 @@ __metadata:
languageName: node
linkType: hard
-"dset@npm:^3.1.2":
+"dset@npm:^3.1.4":
version: 3.1.4
resolution: "dset@npm:3.1.4"
checksum: b67bbd28dd8a539e90c15ffb61100eb64ef995c5270a124d4f99bbb53f4d82f55a051b731ba81f3215dd9dce2b4c8d69927dc20b3be1c5fc88bab159467aa438
@@ -9741,7 +9806,7 @@ __metadata:
languageName: node
linkType: hard
-"express@npm:^4.17.3, express@npm:^4.20.0":
+"express@npm:^4.20.0, express@npm:^4.21.2":
version: 4.21.2
resolution: "express@npm:4.21.2"
dependencies:
@@ -10889,12 +10954,12 @@ __metadata:
languageName: node
linkType: hard
-"graphql-http@npm:^1.22.1":
- version: 1.22.1
- resolution: "graphql-http@npm:1.22.1"
+"graphql-http@npm:^1.22.4":
+ version: 1.22.4
+ resolution: "graphql-http@npm:1.22.4"
peerDependencies:
graphql: ">=0.11 <=16"
- checksum: 969b65dbebbdb6616632e9278d050cc71ba2ae4ff8038b4d83be26d46fc83a4ae54545a0ead052cab0ddfae92d2ddff6aceaef877e74a33f4c7d7e3acc1fab89
+ checksum: 039e55545fda36ba9bae566ae5d528c4dd9d5972ce3799413741f57309107849930343449b024874041cdcbb7242c0562a19c48d83fccabbf0c7d9f0d3d5a43a
languageName: node
linkType: hard
@@ -10925,10 +10990,10 @@ __metadata:
languageName: node
linkType: hard
-"graphql@npm:^16.8.1":
- version: 16.8.1
- resolution: "graphql@npm:16.8.1"
- checksum: 129c318156b466f440914de80dbf7bc67d17f776f2a088a40cb0da611d19a97c224b1c6d2b13cbcbc6e5776e45ed7468b8432f9c3536724e079b44f1a3d57a8a
+"graphql@npm:^16.10.0":
+ version: 16.11.0
+ resolution: "graphql@npm:16.11.0"
+ checksum: 124da7860a2292e9acf2fed0c71fc0f6a9b9ca865d390d112bdd563c1f474357141501c12891f4164fe984315764736ad67f705219c62f7580681d431a85db88
languageName: node
linkType: hard
@@ -11422,7 +11487,7 @@ __metadata:
languageName: node
linkType: hard
-"inflection@npm:^3.0.0":
+"inflection@npm:^3.0.0, inflection@npm:^3.0.2":
version: 3.0.2
resolution: "inflection@npm:3.0.2"
checksum: ac6b635f029b27834313ce30188d74607fe9751c729bf91698675b2fd82489e0195e884d8a9455676064a74b2db77b407d35b56ada0978d0e8194e72202bf7af
@@ -12918,24 +12983,22 @@ __metadata:
linkType: hard
"json-graphql-server@npm:^3.0.1":
- version: 3.1.2
- resolution: "json-graphql-server@npm:3.1.2"
+ version: 3.2.1
+ resolution: "json-graphql-server@npm:3.2.1"
dependencies:
- "@apollo/client": "npm:^3.9.11"
- "@graphql-tools/schema": "npm:^10.0.3"
+ "@apollo/client": "npm:^3.12.11"
+ "@graphql-tools/schema": "npm:^10.0.18"
cors: "npm:^2.8.5"
- express: "npm:^4.17.3"
- graphql: "npm:^16.8.1"
- graphql-http: "npm:^1.22.1"
- graphql-tag: "npm:^2.12.6"
+ express: "npm:^4.21.2"
+ graphql: "npm:^16.10.0"
+ graphql-http: "npm:^1.22.4"
graphql-type-json: "npm:^0.3.2"
- inflection: "npm:^3.0.0"
+ inflection: "npm:^3.0.2"
lodash.merge: "npm:^4.6.2"
- reify: "npm:^0.20.12"
xhr-mock: "npm:^2.5.1"
bin:
json-graphql-server: bin/json-graphql-server.cjs
- checksum: 384a34e0a59dc548492b8401087b7cd339b6e0ea52686dc64e9253e833da921d55e4258159cbe0e4ff36a28aef3ffe8aadd02af64a28025c950be544ea53daf0
+ checksum: 6dc48fc8ba44f871d1f2511bde2213a5df96f89f069ae94962e1856c3c05d5d80b845d8ed9ef293a1ac1c61760377cd415044f1f4dd9e198f2312404b6068fe9
languageName: node
linkType: hard
@@ -13616,15 +13679,6 @@ __metadata:
languageName: node
linkType: hard
-"magic-string@npm:^0.25.3":
- version: 0.25.7
- resolution: "magic-string@npm:0.25.7"
- dependencies:
- sourcemap-codec: "npm:^1.4.4"
- checksum: d5da35f01d5437d7d6c030fe8185285a78b97144d07944d62187bd985ee2f6dcc8c9a538ded6a3afe186f5d6f2e705b45f9f307b19020aff530447bd32f24375
- languageName: node
- linkType: hard
-
"magic-string@npm:^0.30.5":
version: 0.30.10
resolution: "magic-string@npm:0.30.10"
@@ -16743,14 +16797,14 @@ __metadata:
linkType: hard
"react-router-dom@npm:^7.1.1":
- version: 7.6.1
- resolution: "react-router-dom@npm:7.6.1"
+ version: 7.5.3
+ resolution: "react-router-dom@npm:7.5.3"
dependencies:
- react-router: "npm:7.6.1"
+ react-router: "npm:7.5.3"
peerDependencies:
react: ">=18"
react-dom: ">=18"
- checksum: 9d448d82d73c18475c8e3e2f93cf9dd16ca0488379e295f5a949aa849aaf770eb6257d48f2aef2c62ac1635e8ccddd0fa53173c2479525305eb656936f330812
+ checksum: 56c03d8c31c100db54029df82f7e2d350ec75d301b2ffa9512fbc7659faf492ef6d777115629da89c92d0826f3b8a98271dd8426fd3afd3aaf9ed92763f7deaa
languageName: node
linkType: hard
@@ -16765,7 +16819,24 @@ __metadata:
languageName: node
linkType: hard
-"react-router@npm:7.6.1, react-router@npm:^7.1.1":
+"react-router@npm:7.5.3":
+ version: 7.5.3
+ resolution: "react-router@npm:7.5.3"
+ dependencies:
+ cookie: "npm:^1.0.1"
+ set-cookie-parser: "npm:^2.6.0"
+ turbo-stream: "npm:2.4.0"
+ peerDependencies:
+ react: ">=18"
+ react-dom: ">=18"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ checksum: 1f98ab5974cbf6696944a3cbe3d5708add6cdb9c765e0952459eb912d388fe361914a94557546dcd45164413fd9bc2fde97302c8daf3951156644232a9e3ce16
+ languageName: node
+ linkType: hard
+
+"react-router@npm:^7.1.1":
version: 7.6.1
resolution: "react-router@npm:7.6.1"
dependencies:
@@ -17091,18 +17162,6 @@ __metadata:
languageName: node
linkType: hard
-"reify@npm:^0.20.12":
- version: 0.20.12
- resolution: "reify@npm:0.20.12"
- dependencies:
- acorn: "npm:^6.1.1"
- acorn-dynamic-import: "npm:^4.0.0"
- magic-string: "npm:^0.25.3"
- semver: "npm:^5.4.1"
- checksum: e8bbe083a06d4d99e649160e1a4ef7f0e40a87575e2af4b070bbae6f94399e7779659424db777d3b30e7aabff086e3e2fb11b50adf16e74fe77aed3de9503483
- languageName: node
- linkType: hard
-
"relateurl@npm:^0.2.7":
version: 0.2.7
resolution: "relateurl@npm:0.2.7"
@@ -17370,30 +17429,30 @@ __metadata:
languageName: node
linkType: hard
-"rollup@npm:^4.34.9":
- version: 4.40.1
- resolution: "rollup@npm:4.40.1"
- dependencies:
- "@rollup/rollup-android-arm-eabi": "npm:4.40.1"
- "@rollup/rollup-android-arm64": "npm:4.40.1"
- "@rollup/rollup-darwin-arm64": "npm:4.40.1"
- "@rollup/rollup-darwin-x64": "npm:4.40.1"
- "@rollup/rollup-freebsd-arm64": "npm:4.40.1"
- "@rollup/rollup-freebsd-x64": "npm:4.40.1"
- "@rollup/rollup-linux-arm-gnueabihf": "npm:4.40.1"
- "@rollup/rollup-linux-arm-musleabihf": "npm:4.40.1"
- "@rollup/rollup-linux-arm64-gnu": "npm:4.40.1"
- "@rollup/rollup-linux-arm64-musl": "npm:4.40.1"
- "@rollup/rollup-linux-loongarch64-gnu": "npm:4.40.1"
- "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.40.1"
- "@rollup/rollup-linux-riscv64-gnu": "npm:4.40.1"
- "@rollup/rollup-linux-riscv64-musl": "npm:4.40.1"
- "@rollup/rollup-linux-s390x-gnu": "npm:4.40.1"
- "@rollup/rollup-linux-x64-gnu": "npm:4.40.1"
- "@rollup/rollup-linux-x64-musl": "npm:4.40.1"
- "@rollup/rollup-win32-arm64-msvc": "npm:4.40.1"
- "@rollup/rollup-win32-ia32-msvc": "npm:4.40.1"
- "@rollup/rollup-win32-x64-msvc": "npm:4.40.1"
+"rollup@npm:^4.30.1":
+ version: 4.39.0
+ resolution: "rollup@npm:4.39.0"
+ dependencies:
+ "@rollup/rollup-android-arm-eabi": "npm:4.39.0"
+ "@rollup/rollup-android-arm64": "npm:4.39.0"
+ "@rollup/rollup-darwin-arm64": "npm:4.39.0"
+ "@rollup/rollup-darwin-x64": "npm:4.39.0"
+ "@rollup/rollup-freebsd-arm64": "npm:4.39.0"
+ "@rollup/rollup-freebsd-x64": "npm:4.39.0"
+ "@rollup/rollup-linux-arm-gnueabihf": "npm:4.39.0"
+ "@rollup/rollup-linux-arm-musleabihf": "npm:4.39.0"
+ "@rollup/rollup-linux-arm64-gnu": "npm:4.39.0"
+ "@rollup/rollup-linux-arm64-musl": "npm:4.39.0"
+ "@rollup/rollup-linux-loongarch64-gnu": "npm:4.39.0"
+ "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.39.0"
+ "@rollup/rollup-linux-riscv64-gnu": "npm:4.39.0"
+ "@rollup/rollup-linux-riscv64-musl": "npm:4.39.0"
+ "@rollup/rollup-linux-s390x-gnu": "npm:4.39.0"
+ "@rollup/rollup-linux-x64-gnu": "npm:4.39.0"
+ "@rollup/rollup-linux-x64-musl": "npm:4.39.0"
+ "@rollup/rollup-win32-arm64-msvc": "npm:4.39.0"
+ "@rollup/rollup-win32-ia32-msvc": "npm:4.39.0"
+ "@rollup/rollup-win32-x64-msvc": "npm:4.39.0"
"@types/estree": "npm:1.0.7"
fsevents: "npm:~2.3.2"
dependenciesMeta:
@@ -17441,7 +17500,7 @@ __metadata:
optional: true
bin:
rollup: dist/bin/rollup
- checksum: 11c44b5ef9b3fd521c5501b3f1c36af4ca07821aeff41d41f45336eee324d8f5b46c1a92189f5c8cd146bc21ac10418d57cb4571637ea09aced1ae831a2a4ae0
+ checksum: 2dc0c23ca04bd00295035b405c977261559aed8acc9902ee9ff44e4a6b54734fcb64999c32143c43804dcb543da7983032831b893a902633b006c21848a093ce
languageName: node
linkType: hard
@@ -17587,7 +17646,7 @@ __metadata:
languageName: node
linkType: hard
-"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0":
+"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0, semver@npm:^5.6.0":
version: 5.7.2
resolution: "semver@npm:5.7.2"
bin:
@@ -17849,8 +17908,10 @@ __metadata:
"@hookform/devtools": "npm:^4.3.3"
"@mui/icons-material": "npm:^5.16.12"
"@mui/material": "npm:^5.16.12"
+ "@tanstack/query-sync-storage-persister": "npm:^5.47.0"
"@tanstack/react-query": "npm:^5.21.7"
"@tanstack/react-query-devtools": "npm:^5.21.7"
+ "@tanstack/react-query-persist-client": "npm:^5.47.0"
"@vitejs/plugin-react": "npm:^4.3.4"
jsonexport: "npm:^3.2.0"
little-state-machine: "npm:^4.8.1"
@@ -18031,13 +18092,6 @@ __metadata:
languageName: node
linkType: hard
-"sourcemap-codec@npm:^1.4.4":
- version: 1.4.8
- resolution: "sourcemap-codec@npm:1.4.8"
- checksum: f099279fdaae070ff156df7414bbe39aad69cdd615454947ed3e19136bfdfcb4544952685ee73f56e17038f4578091e12b17b283ed8ac013882916594d95b9e6
- languageName: node
- linkType: hard
-
"spdx-correct@npm:^3.0.0":
version: 3.1.1
resolution: "spdx-correct@npm:3.1.1"
@@ -18670,7 +18724,7 @@ __metadata:
languageName: node
linkType: hard
-"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13":
+"tinyglobby@npm:^0.2.12":
version: 0.2.13
resolution: "tinyglobby@npm:0.2.13"
dependencies:
@@ -18900,7 +18954,7 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.1":
+"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.6.3, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@@ -18943,6 +18997,13 @@ __metadata:
languageName: node
linkType: hard
+"turbo-stream@npm:2.4.0":
+ version: 2.4.0
+ resolution: "turbo-stream@npm:2.4.0"
+ checksum: e68b2569f1f16e6e9633d090c6024b2ae9f0e97bfeacb572451ca3732e120ebbb546f3bc4afc717c46cb57b5aea6104e04ef497f9912eef6a7641e809518e98a
+ languageName: node
+ linkType: hard
+
"tutorial@workspace:examples/tutorial":
version: 0.0.0-use.local
resolution: "tutorial@workspace:examples/tutorial"
@@ -19523,13 +19584,6 @@ __metadata:
languageName: node
linkType: hard
-"value-or-promise@npm:^1.0.12":
- version: 1.0.12
- resolution: "value-or-promise@npm:1.0.12"
- checksum: b75657b74e4d17552bd88e0c2857020fbab34a4d091dc058db18c470e7da0336067e72c130b3358e3321ac0a6ff11c0b92b67a382318a3705ad5d57de7ff3262
- languageName: node
- linkType: hard
-
"vary@npm:^1, vary@npm:~1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"
@@ -19549,16 +19603,13 @@ __metadata:
linkType: hard
"vite@npm:^6.2.6":
- version: 6.3.4
- resolution: "vite@npm:6.3.4"
+ version: 6.2.6
+ resolution: "vite@npm:6.2.6"
dependencies:
esbuild: "npm:^0.25.0"
- fdir: "npm:^6.4.4"
fsevents: "npm:~2.3.3"
- picomatch: "npm:^4.0.2"
postcss: "npm:^8.5.3"
- rollup: "npm:^4.34.9"
- tinyglobby: "npm:^0.2.13"
+ rollup: "npm:^4.30.1"
peerDependencies:
"@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: ">=1.21.0"
@@ -19599,7 +19650,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
- checksum: f1534a3f42d14b30e11c58e5e451903d965d5f5ba18d8c81f9df208589e3d2c65535abaa3268d3963573174b8e056ea7bc445f567622c65fcdf98eb4acc1bf4e
+ checksum: 68a2ed3e61bdd654c59b817b4f3203065241c66d1739faa707499130f3007bc3a666c7a8320a4198e275e62b5e4d34d9b78a6533f69e321d366e76f5093b2071
languageName: node
linkType: hard