diff --git a/docs/DeletedRecordsList.md b/docs/DeletedRecordsList.md index 43fcfa6381e..589c862d32e 100644 --- a/docs/DeletedRecordsList.md +++ b/docs/DeletedRecordsList.md @@ -36,24 +36,41 @@ That's enough to display the deleted records list, with functional simple filter ## Props -| Prop | Required | Type | Default | Description | -|----------------------------|----------|---------------------------------|------------------------------------------|--------------------------------------------------------------------------------------------------| -| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. | -| `children` | Optional | `Element` | `` | The component used to render the list of deleted records. | -| `detail Components` | Optional | `Record` | - | The custom show components for each resource in the deleted records list. | -| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. | -| `disable SyncWithLocation` | Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. | -| `filter` | Optional | `object` | - | The permanent filter values. | -| `filter DefaultValues` | Optional | `object` | - | The default filter values. | -| `mutation Mode` | Optional | `string` | `'undoable'` | Mutation mode (`'undoable'`, `'pessimistic'` or `'optimistic'`). | -| `pagination` | Optional | `ReactElement` | `` | The pagination component to use. | -| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. | -| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. | -| `resource` | Optional | `string` | - | The resource of deleted records to fetch and display | -| `sort` | Optional | `object` | `{ field: 'deleted_at', order: 'DESC' }` | The initial sort parameters. | -| `storeKey` | Optional | `string` or `false` | - | The key to use to store the current filter & sort. Pass `false` to disable store synchronization | -| `title` | Optional | `string | ReactElement | false` | - | The title to display in the App Bar. | -| `sx` | Optional | `object` | - | The CSS styles to apply to the component. | +| Prop | Required | Type | Default | Description | +|----------------------------|----------------|---------------------------------|------------------------------------------|--------------------------------------------------------------------------------------------------| +| `authLoading` | Optional | `ReactNode` | - | The component to render while checking for authentication and permissions. | +| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. | +| `children` | Optional | `Element` | `` | The component used to render the list of deleted records. | +| `detailComponents` | Optional | `Record` | - | The custom show components for each resource in the deleted records list. | +| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. | +| `disable SyncWithLocation` | Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. | +| `empty` | Optional | `ReactNode` | - | The component to display when the list is empty. | +| `error` | Optional | `ReactNode` | - | The component to render when failing to load the list of records. | +| `filter` | Optional | `object` | - | The permanent filter values. | +| `filter DefaultValues` | Optional | `object` | - | The default filter values. | +| `loading` | Optional | `ReactNode` | - | The component to render while loading the list of records. | +| `mutation Mode` | Optional | `string` | `'undoable'` | Mutation mode (`'undoable'`, `'pessimistic'` or `'optimistic'`). | +| `offline` | Optional | `ReactNode` | `` | The component to render when there is no connectivity and there is no data in the cache | +| `pagination` | Optional | `ReactElement` | `` | The pagination component to use. | +| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. | +| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. | +| `resource` | Optional | `string` | - | The resource of deleted records to fetch and display | +| `sort` | Optional | `object` | `{ field: 'deleted_at', order: 'DESC' }` | The initial sort parameters. | +| `storeKey` | Optional | `string` or `false` | - | The key to use to store the current filter & sort. Pass `false` to disable store synchronization | +| `title` | Optional | `string | ReactElement | false` | - | The title to display in the App Bar. | +| `sx` | Optional | `object` | - | The CSS styles to apply to the component. | + +## `authLoading` + +By default, `` renders `` while checking for authentication and permissions. You can display a custom component via the `authLoading` prop: + +```jsx +import { DeletedRecordsList } from '@react-admin/ra-soft-delete'; + +export const CustomDeletedRecords = () => ( + Checking for permissions...

} /> +); +``` ## `children` @@ -152,9 +169,21 @@ const DeletedRecordsWithoutSyncWithLocation = () => ; ``` +## `error` + +By default, `` renders the children when an error happens while loading the list of deleted records. You can render an error component via the `error` prop: + +```jsx +import { DeletedRecordsList } from '@react-admin/ra-soft-delete'; + +export const CustomDeletedRecords = () => ( + Something went wrong while loading your posts!

} /> +); +``` + ## `filter`: Permanent Filter -You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the data provider in the `filter` props: +You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the data provider in the `filter` prop: {% raw %} ```tsx @@ -184,6 +213,19 @@ const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser ``` {% endraw %} +## `loading` + +By default, `` renders the children while loading the list of deleted records. You can display a component during this time via the `loading` prop: + +```jsx +import { Loading } from 'react-admin'; +import { DeletedRecordsList } from '@react-admin/ra-soft-delete'; + +export const CustomDeletedRecords = () => ( + } /> +); +``` + ## `mutationMode` The `` list exposes restore and delete permanently buttons, which perform "mutations" (i.e. they alter the data). React-admin offers three modes for mutations. The mode determines when the side effects (redirection, notifications, etc.) are executed: @@ -212,6 +254,21 @@ const PessimisticDeletedRecords = () => ( **Tip**: When using any other mode than `undoable`, the `` and `` display a confirmation dialog before calling the dataProvider. +## `offline` + +By default, `` renders the `` component when there is no connectivity and there are no records in the cache yet for the current parameters (page, sort, etc.). You can provide your own component via the `offline` prop: + +```jsx +import { DeletedRecordsList } from '@react-admin/ra-soft-delete'; +import { Alert } from '@mui/material'; + +const offline = No network. Could not load the posts.; + +export const CustomDeletedRecords = () => ( + +); +``` + ## `pagination` By default, the `` view displays a set of pagination controls at the bottom of the list. diff --git a/docs/SoftDeleteDataProvider.md b/docs/SoftDeleteDataProvider.md index 75347ea9d54..4966cc45613 100644 --- a/docs/SoftDeleteDataProvider.md +++ b/docs/SoftDeleteDataProvider.md @@ -185,7 +185,7 @@ export const dataProvider = addSoftDeleteInPlace( **Note:** When using `addSoftDeleteInPlace`, avoid calling `getListDeleted` without a `resource` filter, as it uses a naive implementation combining multiple `getList` calls, which can lead to bad performance. It is recommended to use one list per resource in this case (see [`` property](./DeletedRecordsList.md#resource)). -You can also write your own implementation. Feel free to look at these builders source code for inspiration. You can find it under your `node_modules` folder, e.g. at `node_modules/@react-admin/ra-soft-delete/src/dataProvider/addSoftDeleteBasedOnResource.ts`. +You can also write your own implementation. Feel free to look at these builders source code for inspiration. You can find it under your `node_modules` folder, e.g. at `node_modules/@react-admin/ra-core-ee/src/soft-delete/dataProvider/addSoftDeleteBasedOnResource.ts`. ### Query and Mutation Hooks diff --git a/docs_headless/astro.config.mjs b/docs_headless/astro.config.mjs index b7c3148fe5f..01f651c1cdc 100644 --- a/docs_headless/astro.config.mjs +++ b/docs_headless/astro.config.mjs @@ -218,6 +218,7 @@ export default defineConfig({ { label: 'Realtime', items: [ + enterpriseEntry(''), enterpriseEntry('usePublish'), enterpriseEntry('useSubscribe'), enterpriseEntry('useSubscribeCallback'), @@ -234,7 +235,26 @@ export default defineConfig({ enterpriseEntry('useLockOnCall'), enterpriseEntry('useGetListLive'), enterpriseEntry('useGetOneLive'), - enterpriseEntry(''), + ], + }, + { + label: 'Soft Delete', + items: [ + enterpriseEntry('SoftDeleteDataProvider', 'Setting up'), + enterpriseEntry(''), + enterpriseEntry(''), + enterpriseEntry(''), + enterpriseEntry('addSoftDeleteBasedOnResource'), + enterpriseEntry('addSoftDeleteInPlace'), + enterpriseEntry('useSoftDelete'), + enterpriseEntry('useSoftDeleteMany'), + enterpriseEntry('useGetListDeleted'), + enterpriseEntry('useGetOneDeleted'), + enterpriseEntry('useRestoreOne'), + enterpriseEntry('useRestoreMany'), + enterpriseEntry('useHardDelete'), + enterpriseEntry('useHardDeleteMany'), + enterpriseEntry('useDeletedRecordsListController'), ], }, { @@ -279,10 +299,10 @@ export default defineConfig({ * @param {string} name * @returns {any} */ -function enterpriseEntry(name) { +function enterpriseEntry(name, label = name) { return { link: `${name.toLowerCase().replace(//g, '')}/`, - label: name, + label, attrs: { class: 'enterprise' }, badge: { text: 'React Admin Enterprise', diff --git a/docs_headless/src/content/docs/DeletedRecordRepresentation.md b/docs_headless/src/content/docs/DeletedRecordRepresentation.md new file mode 100644 index 00000000000..1f17bf66750 --- /dev/null +++ b/docs_headless/src/content/docs/DeletedRecordRepresentation.md @@ -0,0 +1,56 @@ +--- +title: "" +--- + +A component that renders the record representation of a deleted record. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +import { CoreAdmin, CustomRoutes, WithRecord } from 'react-admin'; +import { Route } from 'react-router-dom'; +import { DeletedRecordsListBase, ShowDeletedBase, type DeletedRecordType } from '@react-admin/ra-core-ee'; + +export const App = () => ( + + ... + + + isPending ? null : ( +
    + {data.map(record => ( +
  • +
    {record.resource}
    + +
  • + ))} +
+ )} + /> +
+ } + /> + + +); +``` + +## Props + +| Prop | Required | Type | Default | Description | +|------------|----------|------------|---------|---------------------------------------------------------------------------------------| +| `record` | Optional | `RaRecord` | | The deleted record. If not provided, the record from closest `RecordContext` is used. | diff --git a/docs_headless/src/content/docs/DeletedRecordsListBase.md b/docs_headless/src/content/docs/DeletedRecordsListBase.md new file mode 100644 index 00000000000..0696a1e51a6 --- /dev/null +++ b/docs_headless/src/content/docs/DeletedRecordsListBase.md @@ -0,0 +1,327 @@ +--- +title: "" +--- + +The `` component fetches a list of deleted records from the data provider. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +`` uses `dataProvider.getListDeleted()` to get the deleted records to display, so in general it doesn't need any property. +However, you need to define the route to reach this component manually using [``](./CustomRoutes.md). + +```tsx +// in src/App.js +import { CoreAdmin, CustomRoutes } from 'ra-core'; +import { Route } from 'react-router-dom'; +import { DeletedRecordsListBase, DeletedRecordRepresentation } from '@react-admin/ra-core-ee'; + +export const App = () => ( + + ... + + + isPending ? null : ( +
    + {data.map(record => ( +
  • +
    {record.resource}
    + +
  • + ))} +
+ )} + /> +
+ } + /> + + +); +``` + +That's enough to display the deleted records list, with functional simple filters, sort and pagination. + +## Props + +| Prop | Required | Type | Default | Description | +|----------------------------|----------------|---------------------------------|------------------------------------------|--------------------------------------------------------------------------------------------------| +| `children` | Required | `Element` | | The component used to render the list of deleted records. | +| `authLoading` | Optional | `ReactNode` | - | The component to render while checking for authentication and permissions. | +| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. | +| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. | +| `disable SyncWithLocation` | Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. | +| `empty` | Optional | `ReactNode` | - | The component to display when the list is empty. | +| `error` | Optional | `ReactNode` | - | The component to render when failing to load the list of records. | +| `filter` | Optional | `object` | - | The permanent filter values. | +| `filter DefaultValues` | Optional | `object` | - | The default filter values. | +| `loading` | Optional | `ReactNode` | - | The component to render while loading the list of records. | +| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and there is no data in the cache | +| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. | +| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. | +| `resource` | Optional | `string` | - | The resource of deleted records to fetch and display | +| `sort` | Optional | `object` | `{ field: 'deleted_at', order: 'DESC' }` | The initial sort parameters. | +| `storeKey` | Optional | `string` or `false` | - | The key to use to store the current filter & sort. Pass `false` to disable store synchronization | + +## `authLoading` + +By default, `` renders its children while checking for authentication and permissions. You can display a custom component via the `authLoading` prop: + +```jsx +export const CustomDeletedRecords = () => ( + Checking for permissions...

} /> +); +``` + +## `children` + +A component that uses `ListContext` to render the deleted records: + +```tsx +import { DeletedRecordsListBase, DeletedRecordRepresentation } from '@react-admin/ra-core-ee'; + +export const CustomDeletedRecords = () => ( + + isPending ? null : ( +
    + {data.map(record => ( +
  • +
    {record.resource}
    + +
  • + ))} +
+ )} + /> +
+); +``` + +## `debounce` + +By default, `` does not refresh the data as soon as the user enters data in the filter form. Instead, it waits for half a second of user inactivity (via `lodash.debounce`) before calling the dataProvider on filter change. This is to prevent repeated (and useless) calls to the API. + +You can customize the debounce duration in milliseconds - or disable it completely - by passing a `debounce` prop to the `` component: + +```tsx +// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider +const DeletedRecordsWithDebounce = () => ; +``` + +## `disableAuthentication` + +By default, `` requires the user to be authenticated - any anonymous access redirects the user to the login page. + +If you want to allow anonymous access to the deleted records list page, set the `disableAuthentication` prop to `true`. + +```tsx +const AnonymousDeletedRecords = () => ; +``` + +## `disableSyncWithLocation` + +By default, react-admin synchronizes the `` parameters (sort, pagination, filters) with the query string in the URL (using `react-router` location) and the [Store](./Store.md). + +You may want to disable this synchronization to keep the parameters in a local state, independent for each `` instance. To do so, pass the `disableSyncWithLocation` prop. The drawback is that a hit on the "back" button doesn't restore the previous parameters. + +```tsx +const DeletedRecordsWithoutSyncWithLocation = () => ; +``` + +**Tip**: `disableSyncWithLocation` also disables the persistence of the deleted records list parameters in the Store by default. To enable the persistence of the deleted records list parameters in the Store, you can pass a custom `storeKey` prop. + +```tsx +const DeletedRecordsSyncWithStore = () => ; +``` + +## `empty` + +By default, `` renders the children when there are no deleted records to show. You can render a custom component via the `empty` prop: + +```jsx +export const CustomDeletedRecords = () => ( + The trash is empty!

} /> +); +``` + +## `error` + +By default, `` renders the children when an error happens while loading the list of deleted records. You can render an error component via the `error` prop: + +```jsx +export const CustomDeletedRecords = () => ( + Something went wrong while loading your posts!

} /> +); +``` + +## `filter`: Permanent Filter + +You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the data provider in the `filter` prop: + +```tsx +const DeletedPostsList = () => ( + +); +``` + +The actual filter parameter sent to the data provider is the result of the combination of the *user* filters (the ones set through the `filters` component form), and the *permanent* filter. The user cannot override the permanent filters set by way of `filter`. + +## `filterDefaultValues` + +To set default values to filters, you can pass an object literal as the `filterDefaultValues` prop of the `` element. + +```tsx +const CustomDeletedRecords = () => ( + +); +``` + +**Tip**: The `filter` and `filterDefaultValues` props have one key difference: the `filterDefaultValues` can be overridden by the user, while the `filter` values are always sent to the data provider. Or, to put it otherwise: + +```js +const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser, ...filter }; +``` + +## `loading` + +By default, `` renders the children while loading the list of deleted records. You can display a component during this time via the `loading` prop: + +```jsx +export const CustomDeletedRecords = () => ( + Loading...

} /> +); +``` + +## `offline` + +By default, `` renders the children when there is no connectivity and there are no records in the cache yet for the current parameters (page, sort, etc.). You can provide your own component via the `offline` prop: + +```jsx +export const CustomDeletedRecords = () => ( + No network. Could not load the posts.

} /> +); +``` + +## `perPage` + +By default, the deleted records list paginates results by groups of 10. You can override this setting by specifying the `perPage` prop: + +```tsx +const DeletedRecordsWithCustomPagination = () => ; +``` + +## `queryOptions` + +`` accepts a `queryOptions` prop to pass query options to the react-query client. Check [react-query's useQuery documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery) for the list of available options. + +This can be useful e.g. to pass a custom `meta` to the `dataProvider.getListDeleted()` call. + +```tsx +const CustomDeletedRecords = () => ( + +); +``` + +With this option, react-admin will call `dataProvider.getListDeleted()` on mount with the `meta: { foo: 'bar' }` option. + +You can also use the `queryOptions` prop to override the default error side effect. By default, when the `dataProvider.getListDeleted()` call fails, react-admin shows an error notification. Here is how to show a custom notification instead: + +```tsx +import { useNotify, useRedirect } from 'ra-core'; +import { DeletedRecordsListBase } from '@react-admin/ra-core-ee'; + +const CustomDeletedRecords = () => { + const notify = useNotify(); + const redirect = useRedirect(); + + const onError = (error) => { + notify(`Could not load list: ${error.message}`, { type: 'error' }); + redirect('/dashboard'); + }; + + return ( + + ); +} +``` + +The `onError` function receives the error from the dataProvider call (`dataProvider.getListDeleted()`), which is a JavaScript Error object (see [the dataProvider documentation for details](./DataProviderWriting.md#error-format)). + +## `resource` + +`` fetches the deleted records from the data provider using the `dataProvider.getListDeleted()` method. When no resource is specified, it will fetch all deleted records from all resources and display a filter. + +If you want to display only the deleted records of a specific resource, you can pass the `resource` prop: + +```tsx +const DeletedPosts = () => ( + +); +``` + +When a resource is specified, the filter will not be displayed, and the list will only show deleted records of that resource. + +The title is also updated accordingly. Its translation key is `ra-soft-delete.deleted_records_list.resource_title`. + +## `sort` + +Pass an object literal as the `sort` prop to determine the default `field` and `order` used for sorting: + +```tsx +const PessimisticDeletedRecords = () => ( + +); +``` + +`sort` defines the *default* sort order ; the list remains sortable by clicking on column headers. + +For more details on list sort, see the [Sorting The List](./ListTutorial.md#sorting-the-list) section. + +## `storeKey` + +By default, react-admin stores the list parameters (sort, pagination, filters) in localStorage so that users can come back to the list and find it in the same state as when they left it. +The `` component uses a specific identifier to store the list parameters under the key `ra-soft-delete.listParams`. + +If you want to use multiple `` and keep distinct store states for each of them (filters, sorting and pagination), you must give each list a unique `storeKey` property. You can also disable the persistence of list parameters and selection in the store by setting the `storeKey` prop to `false`. + +In the example below, the deleted records lists store their list parameters separately (under the store keys `'deletedBooks'` and `'deletedAuthors'`). This allows to use both components in the same app, each having its own state (filters, sorting and pagination). + +```tsx +import { CoreAdmin, CustomRoutes } from 'ra-core'; +import { Route } from 'react-router-dom'; +import { DeletedRecordsListBase } from '@react-admin/ra-core-ee'; + +const Admin = () => { + return ( + + + + } /> + + } /> + + + + ); +}; +``` + +**Tip:** The `storeKey` is actually passed to the underlying `useDeletedRecordsListController` hook, which you can use directly for more complex scenarios. See the [`useDeletedRecordsListController` doc](./useDeletedRecordsListController.md) for more info. + +**Note:** *Selection state* will remain linked to a global key regardless of the specified `storeKey` string. This is a design choice because if row selection is not stored globally, then when a user permanently deletes or restores a record it may remain selected without any ability to unselect it. If you want to allow custom `storeKey`'s for managing selection state, you will have to implement your own `useDeletedRecordsListController` hook and pass a custom key to the `useRecordSelection` hook. You will then need to implement your own delete buttons to manually unselect rows when deleting or restoring records. You can still opt out of all store interactions including selection if you set it to `false`. diff --git a/docs_headless/src/content/docs/ShowDeletedBase.md b/docs_headless/src/content/docs/ShowDeletedBase.md new file mode 100644 index 00000000000..968b2767736 --- /dev/null +++ b/docs_headless/src/content/docs/ShowDeletedBase.md @@ -0,0 +1,78 @@ +--- +title: "" +--- + +The `` component replaces the [``](./ShowBase.md) component when displaying a deleted record. + +It provides the same `ShowContext` as `` so that you can use the same children components. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +import { CoreAdmin, CustomRoutes, WithRecord } from 'ra-core'; +import { Route } from 'react-router-dom'; +import { DeletedRecordsListBase, DeletedRecordRepresentation, ShowDeletedBase, type DeletedRecordType } from '@react-admin/ra-core-ee'; + +export const App = () => ( + + ... + + + isPending ? null : ( +
    + {data.map(record => ( +
  • + +
  • + ))} +
+ )} + /> +
+ } + /> + + +); + +const DeletedItem = ({ record }: { record: DeletedRecordType }) => { + const [showDetails, setShowDetails] = React.useState(false); + return ( + <> +
{record.resource}
+ +
+ +
+ {showDetails ? ( + +

{record.title}

} /> +

{record.description}

} /> + +
+ ) : null} + + ) +} +``` + +## Props + +| Prop | Required | Type | Default | Description | +|------------|----------|------------|---------|---------------------------------------------------------------------------------------| +| `children` | Required | `Element` | | The component used to render the deleted record. | +| `record` | Optional | `RaRecord` | | The deleted record. If not provided, the record from closest `RecordContext` is used. | diff --git a/docs_headless/src/content/docs/SoftDeleteDataProvider.md b/docs_headless/src/content/docs/SoftDeleteDataProvider.md new file mode 100644 index 00000000000..e28171f2a08 --- /dev/null +++ b/docs_headless/src/content/docs/SoftDeleteDataProvider.md @@ -0,0 +1,213 @@ +--- +layout: default +title: "Soft Delete Setup" +--- + +The soft delete feature is an [Enterprise Edition add-on](https://react-admin-ee.marmelab.com/documentation/ra-core-ee) that allows you to "delete" records without actually removing them from your database. + +Use it to: + +- Archive records safely instead of permanent deletion +- Browse and filter all deleted records in a dedicated interface +- Restore archived items individually or in bulk +- Track who deleted what and when + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +You will need an active Enterprise Edition license to use this package. Please refer to the [Enterprise Edition documentation](https://react-admin-ee.marmelab.com) for more details. + +## Data Provider + +### Methods + +The Soft Delete features of `ra-core-ee` rely on the `dataProvider` to soft-delete, restore or view deleted records. +In order to use those features, you must add a few new methods to your data provider: + +- `softDelete` performs the soft deletion of the provided record. +- `softDeleteMany` performs the soft deletion of the provided records. +- `getOneDeleted` gets one deleted record by its ID. +- `getListDeleted` gets a list of deleted records with filters and sort. +- `restoreOne` restores a deleted record. +- `restoreMany` restores deleted records. +- `hardDelete` permanently deletes a record. +- `hardDeleteMany` permanently deletes many records. +- (OPTIONAL) [`createMany`](#createmany) creates multiple records at once. This method is used internally by some data provider implementations to delete or restore multiple records at once. As it is optional, a default implementation is provided that simply calls `create` multiple times. + +### Signature + +Here is the full `SoftDeleteDataProvider` interface: + +```tsx +const dataProviderWithSoftDelete: SoftDeleteDataProvider = { + ...dataProvider, + + softDelete: (resource, params: SoftDeleteParams): SoftDeleteResult => { + const { id, authorId } = params; + // ... + return { data: deletedRecord }; + }, + softDeleteMany: (resource, params: SoftDeleteManyParams): SoftDeleteManyResult => { + const { ids, authorId } = params; + // ... + return { data: deletedRecords }; + }, + + getOneDeleted: (params: GetOneDeletedParams): GetOneDeletedResult => { + const { id } = params; + // ... + return { data: deletedRecord }; + }, + getListDeleted: (params: GetListDeletedParams): GetListDeletedResult => { + const { filter, sort, pagination } = params; + // ... + return { data: deletedRecords, total: deletedRecords.length }; + }, + + restoreOne: (params: RestoreOneParams): RestoreOneResult => { + const { id } = params; + // ... + return { data: deletedRecord }; + }, + restoreMany: (params: RestoreManyParams): RestoreManyResult => { + const { ids } = params; + // ... + return { data: deletedRecords }; + }, + + hardDelete: (params: HardDeleteParams): HardDeleteResult => { + const { id } = params; + // ... + return { data: deletedRecordId }; + }, + hardDeleteMany: (params: HardDeleteManyParams): HardDeleteManyResult => { + const { ids } = params; + // ... + return { data: deletedRecordsIds }; + }, +}; +``` + +**Tip**: `ra-core-ee` automatically populates the `authorId` parameter using `authProvider.getIdentity()` if it is implemented. It will use the `id` field of the returned identity object. Otherwise this field will be left blank. + +**Tip**: Deleted records are immutable, so you don't need to implement an `updateDeleted` method. + +Once your provider has all soft-delete methods, pass it to the [``](./CoreAdmin.md) component and you're ready to start using the Soft Delete feature. + +```tsx +// in src/App.tsx +import { CoreAdmin } from 'ra-core'; +import { dataProvider } from './dataProvider'; + +const App = () => {/* ... */}; +``` + +### Deleted Record Structure + +A _deleted record_ is an object with the following properties: + +- `id`: The identifier of the deleted record. +- `resource`: The resource name of the deleted record. +- `deleted_at`: The date and time when the record was deleted, in ISO 8601 format. +- `deleted_by`: (optional) The identifier of the user who deleted the record. +- `data`: The original record data before deletion. + +Here is an example of a deleted record: + +```js +{ + id: 123, + resource: "products", + deleted_at: "2025-06-06T15:32:22Z", + deleted_by: "johndoe", + data: { + id: 456, + title: "Lorem ipsum", + teaser: "Lorem ipsum dolor sit amet", + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + }, +} +``` + +### Builders + +`ra-core-ee` comes with two built-in implementations that will add soft delete capabilities to your data provider without any specific backend requirements. You can choose the one that best fits your needs: + +- [`addSoftDeleteBasedOnResource`](./addSoftDeleteBasedOnResource.md) stores the deleted records for all resources in a single resource. This resource is named `deleted_records` by default. + + With this builder, all deleted records disappear from their original resource when soft-deleted, and are recreated in the `deleted_records` resource. + +```tsx +// in src/dataProvider.ts +import { addSoftDeleteBasedOnResource } from '@react-admin/ra-core-ee'; +import baseDataProvider from './baseDataProvider'; + +export const dataProvider = addSoftDeleteBasedOnResource( + baseDataProvider, + { deletedRecordsResourceName: 'deleted_records' } +); +``` + +- [`addSoftDeleteInPlace`](./addSoftDeleteInPlace.md) keeps the deleted records in the same resource, but marks them as deleted. + + With this builder, all deleted records remain in their original resource when soft-deleted, but are marked with the `deleted_at` and `deleted_by` fields. The query methods (`getList`, `getOne`, etc.) automatically filter out deleted records. + + You'll need to pass a configuration object with all soft deletable resources as key so that `getListDeleted` knows where to look for deleted records. + +```tsx +// in src/dataProvider.ts +import { addSoftDeleteInPlace } from '@react-admin/ra-core-ee'; +import baseDataProvider from './baseDataProvider'; + +export const dataProvider = addSoftDeleteInPlace( + baseDataProvider, + { + posts: {}, + comments: { + deletedAtFieldName: 'deletion_date', + }, + accounts: { + deletedAtFieldName: 'disabled_at', + deletedByFieldName: 'disabled_by', + } + } +); +``` + +**Note:** When using `addSoftDeleteInPlace`, avoid calling `getListDeleted` without a `resource` filter, as it uses a naive implementation combining multiple `getList` calls, which can lead to bad performance. It is recommended to use one list per resource in this case (see [`` property](./DeletedRecordsListBase.md#resource)). + +You can also write your own implementation. Feel free to look at these builders source code for inspiration. You can find it under your `node_modules` folder, e.g. at `node_modules/@react-admin/ra-core-ee/src/soft-delete/dataProvider/addSoftDeleteBasedOnResource.ts`. + +### Query and Mutation Hooks + +Each data provider verb has its own hook so you can use them in custom components: + +- `softDelete`: [`useSoftDelete`](./useSoftDelete.md) +- `softDeleteMany`: [`useSoftDeleteMany`](./useSoftDeleteMany.md) +- `getListDeleted`: [`useGetListDeleted`](./useGetListDeleted.md) +- `getOneDeleted`: [`useGetOneDeleted`](./useGetOneDeleted.md) +- `restoreOne`: [`useRestoreOne`](./useRestoreOne.md) +- `restoreMany`: [`useRestoreMany`](./useRestoreMany.md) +- `hardDelete`: [`useHardDelete`](./useHardDelete.md) +- `hardDeleteMany`: [`useHardDeleteMany`](./useHardDeleteMany.md) + + +## `createMany` + +`ra-core-ee` provides a default implementation of the `createMany` method that simply calls `create` multiple times. However, some data providers may be able to create multiple records at once, which can greatly improve performances. + +```tsx +const dataProviderWithCreateMany = { + ...dataProvider, + createMany: (resource, params: CreateManyParams): CreateManyResult => { + const {data} = params; // data is an array of records. + // ... + return {data: createdRecords}; + }, +}; +``` diff --git a/docs_headless/src/content/docs/addSoftDeleteBasedOnResource.md b/docs_headless/src/content/docs/addSoftDeleteBasedOnResource.md new file mode 100644 index 00000000000..cf62ca51b44 --- /dev/null +++ b/docs_headless/src/content/docs/addSoftDeleteBasedOnResource.md @@ -0,0 +1,29 @@ +--- +title: "addSoftDeleteBasedOnResource" +--- + +This helper function wraps an existing [`dataProvider`](./DataProviders.md) to add the soft delete capabilities, storing all deleted records in a single `deleted_records` (configurable) resource. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +// in src/dataProvider.ts +import { addSoftDeleteBasedOnResource } from '@react-admin/ra-core-ee'; +import baseDataProvider from './baseDataProvider'; + +export const dataProvider = addSoftDeleteBasedOnResource( + baseDataProvider, + { deletedRecordsResourceName: 'deleted_records' } +); +``` + diff --git a/docs_headless/src/content/docs/addSoftDeleteInPlace.md b/docs_headless/src/content/docs/addSoftDeleteInPlace.md new file mode 100644 index 00000000000..67648e51aeb --- /dev/null +++ b/docs_headless/src/content/docs/addSoftDeleteInPlace.md @@ -0,0 +1,41 @@ +--- +title: "addSoftDeleteInPlace" +--- + +This helper function wraps an existing [`dataProvider`](./DataProviders.md) to add the soft delete capabilities, keeping the deleted records in the same resource. This implementation will simply fill the `deleted_at` (configurable) and `deleted_by` (configurable) fields. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +You'll need to pass an object with all your resources as key so that `getListDeleted` knows where to look for deleted records. + +> **Note on performances:** Avoid calling `getListDeleted` without a `resource` filter, as it uses a naive implementation combining multiple `getList` calls, which can lead to bad performances. It is recommended to use one list per resource in this case (see [`resource` property](./useGetListDeleted.md#resource)). + +```tsx +// in src/dataProvider.ts +import { addSoftDeleteInPlace } from '@react-admin/ra-core-ee'; +import baseDataProvider from './baseDataProvider'; + +export const dataProvider = addSoftDeleteInPlace( + baseDataProvider, + { + posts: {}, + comments: { + deletedAtFieldName: 'deletion_date', + }, + accounts: { + deletedAtFieldName: 'disabled_at', + deletedByFieldName: 'disabled_by', + } + } +); +``` diff --git a/docs_headless/src/content/docs/useDeletedRecordsListController.md b/docs_headless/src/content/docs/useDeletedRecordsListController.md new file mode 100644 index 00000000000..06b237bc3d3 --- /dev/null +++ b/docs_headless/src/content/docs/useDeletedRecordsListController.md @@ -0,0 +1,349 @@ +--- +title: "useDeletedRecordsListController" +--- + +`useDeletedRecordsListController` contains the headless logic to create a list of deleted records. + +`useDeletedRecordsListController` reads the deleted records list parameters from the URL, calls `dataProvider.getListDeleted()`, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them together with the data. Its return value matches the [`ListContext`](./useListContext.md) shape. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +`useDeletedRecordsListController` expects a parameters object defining the deleted records list sorting, pagination, and filters. It returns an object with the fetched data, and callbacks to modify the deleted records list parameters. + +You can call `useDeletedRecordsListController()` without parameters, and then put the result in a `ListContext` to make it available to the rest of the component tree. + +```tsx +import { ListContextProvider } from 'ra-core'; +import { useDeletedRecordsListController } from '@react-admin/ra-core-ee'; + +const MyDeletedRecords = ({children}: { children: React.ReactNode }) => { + const deletedRecordsListController = useDeletedRecordsListController(); + return ( + + {children} + + ); +}; +``` + +## Parameters + +`useDeletedRecordsListController` expects an object as parameter. All keys are optional. + +- [`debounce`](#debounce): Debounce time in ms for the `setFilters` callbacks. +- [`disableAuthentication`](#disableauthentication): Set to true to allow anonymous access to the list +- [`disableSyncWithLocation`](#disablesyncwithlocation): Set to true to have more than one list per page +- [`filter`](#filter-permanent-filter): Permanent filter, forced over the user filter +- [`filterDefaultValues`](#filterdefaultvalues): Default values for the filter form +- [`perPage`](#perpage): Number of results per page +- [`queryOptions`](#queryoptions): React-query options for the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) call. +- [`resource`](#resource): The resource of deleted records to fetch and display (used as filter when calling `getListDeleted`) +- [`sort`](#sort): Current sort value, e.g. `{ field: 'deleted_at', order: 'ASC' }` +- [`storeKey`](#storekey): Key used to differentiate the list from another, in store managed states + +Here are their default values: + +```tsx +import { ListContextProvider } from 'ra-core'; +import { useDeletedRecordsListController } from '@react-admin/ra-core-ee'; + +const CustomDeletedRecords = ({ + debounce = 500, + disableAuthentication = false, + disableSyncWithLocation = false, + filter = undefined, + filterDefaultValues = undefined, + perPage = 10, + queryOptions = undefined, + sort = { field: 'deleted_at', order: 'DESC' }, + storeKey = undefined, +}) => { + const deletedRecordsListController = useDeletedRecordsListController({ + debounce, + disableAuthentication, + disableSyncWithLocation, + filter, + filterDefaultValues, + perPage, + queryOptions, + sort, + storeKey, + }); + return ( + + {children} + + ); +}; +``` + +## `debounce` + +By default, `useDeletedRecordsListController` does not refresh the data as soon as the user enters data in the filter form. Instead, it waits for half a second of user inactivity (via `lodash.debounce`) before calling the dataProvider on filter change. This is to prevent repeated (and useless) calls to the API. + +You can customize the debounce duration in milliseconds - or disable it completely - by passing a `debounce` parameter: + +```tsx +// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider +const deletedRecordsListController = useDeletedRecordsListController({ debounce: 1000 }); +``` + +## `disableAuthentication` + +By default, `useDeletedRecordsListController` requires the user to be authenticated - any anonymous access redirects the user to the login page. + +If you want to allow anonymous access to the deleted records list page, set the `disableAuthentication` parameter to `true`. + +```tsx +const anonymousDeletedRecordsListController = useDeletedRecordsListController({ disableAuthentication: true }); +``` + +## `disableSyncWithLocation` + +By default, ra-core-ee synchronizes the `useDeletedRecordsListController` parameters (sort, pagination, filters) with the query string in the URL (using `react-router` location) and the [Store](./Store.md). + +You may want to disable this synchronization to keep the parameters in a local state, independent for each `useDeletedRecordsListController` call. To do so, pass the `disableSyncWithLocation` parameter. The drawback is that a hit on the "back" button doesn't restore the previous parameters. + +```tsx +const deletedRecordsListController = useDeletedRecordsListController({ disableSyncWithLocation: true }); +``` + +**Tip**: `disableSyncWithLocation` also disables the persistence of the deleted records list parameters in the Store by default. To enable the persistence of the deleted records list parameters in the Store, you can pass a custom `storeKey` parameter. + +```tsx +const deletedRecordsListController = useDeletedRecordsListController({ + disableSyncWithLocation: true, + storeKey: 'deletedRecordsListParams', +}); +``` + +## `filter`: Permanent Filter + +You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the data provider in the `filter` parameter: + +```tsx +const deletedRecordsListController = useDeletedRecordsListController({ + filter: { deleted_by: 'test' }, +}); +``` + +The actual filter parameter sent to the data provider is the result of the combination of the *user* filters (the ones set through the `filters` component form), and the *permanent* filter. The user cannot override the permanent filters set by way of `filter`. + +## `filterDefaultValues` + +To set default values to filters, you can pass an object literal as the `filterDefaultValues` parameter of `useDeletedRecordsListController`. + +```tsx +const deletedRecordsListController = useDeletedRecordsListController({ + filterDefaultValues: { deleted_by: 'test' }, +}); +``` + +**Tip**: The `filter` and `filterDefaultValues` props have one key difference: the `filterDefaultValues` can be overridden by the user, while the `filter` values are always sent to the data provider. Or, to put it otherwise: + +```js +const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser, ...filter }; +``` + +## `perPage` + +By default, the deleted records list paginates results by groups of 10. You can override this setting by specifying the `perPage` parameter: + +```tsx +const deletedRecordsListController = useDeletedRecordsListController({ perPage: 25 }); +``` + +## `queryOptions` + +`useDeletedRecordsListController` accepts a `queryOptions` parameter to pass query options to the react-query client. Check [react-query's useQuery documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery) for the list of available options. + +This can be useful e.g. to pass a custom `meta` to the `dataProvider.getListDeleted()` call. + +```tsx +const deletedRecordsListController = useDeletedRecordsListController({ + queryOptions: { meta: { foo: 'bar' } }, +}); +``` + +With this option, ra-core-ee will call `dataProvider.getListDeleted()` on mount with the `meta: { foo: 'bar' }` option. + +You can also use the `queryOptions` parameter to override the default error side effect. By default, when the `dataProvider.getListDeleted()` call fails, ra-core-ee shows an error notification. Here is how to show a custom notification instead: + +```tsx +import { useNotify, useRedirect } from 'ra-core'; +import { useDeletedRecordsListController } from '@react-admin/ra-core-ee'; + +const CustomDeletedRecords = () => { + const notify = useNotify(); + const redirect = useRedirect(); + + const onError = (error) => { + notify(`Could not load list: ${error.message}`, { type: 'error' }); + redirect('/dashboard'); + }; + + const deletedRecordsListController = useDeletedRecordsListController({ + queryOptions: { onError }, + }); + + return ( + <>{/* ... */} + ); +} +``` + +The `onError` function receives the error from the dataProvider call (`dataProvider.getListDeleted()`), which is a JavaScript Error object (see [the dataProvider documentation for details](./DataProviderWriting.md/#error-format)). + +## `resource` + +`useDeletedRecordsListController` fetches the deleted records from the data provider using the `dataProvider.getListDeleted()` method. When no resource is specified, it will fetch all deleted records from all resources and display a filter. + +If you want to display only the deleted records of a specific resource, you can pass the `resource` parameter: + +```tsx +const deletedRecordsListController = useDeletedRecordsListController({ resource: 'posts' }); +``` + +The title is also updated accordingly. Its translation key is `ra-soft-delete.deleted_records_list.resource_title`. + +## `sort` + +Pass an object literal as the `sort` parameter to determine the default `field` and `order` used for sorting: + +```tsx +const PessimisticDeletedRecords = () => ( + +); +``` + +`sort` defines the *default* sort order ; it can still be changed by using the `setSort` function returned by the controller. + +For more details on list sort, see the [Sorting The List](./ListTutorial.md/#sorting-the-list) section. + +## `storeKey` + +To display multiple deleted records lists and keep distinct store states for each of them (filters, sorting and pagination), specify unique keys with the `storeKey` property. + +In case no `storeKey` is provided, the states will be stored with the following key: `ra-soft-delete.listParams`. + +**Note**: Please note that selection state will remain linked to a constant key (`ra-soft-delete.selectedIds`) as described [here](#storekey). + +If you want to disable the storage of list parameters altogether for a given list, you can use [the `disableSyncWithLocation` prop](#disablesyncwithlocation). + +In the example below, the controller states of `NewestDeletedRecords` and `OldestDeletedRecords` are stored separately (under the store keys 'newest' and 'oldest' respectively). + +```tsx +import { useDeletedRecordsListController } from '@react-admin/ra-core-ee'; + +const OrderedDeletedRecords = ({ + storeKey, + sort, +}) => { + const params = useDeletedRecordsListController({ + sort, + storeKey, + }); + return ( +
    + {!params.isPending && + params.data.map(deletedRecord => ( +
  • + [{deletedRecord.deleted_at}] Deleted by {deletedRecord.deleted_by}: {JSON.stringify(deletedRecord.data)} +
  • + ))} +
+ ); +}; + +const NewestDeletedRecords = ( + +); +const OldestDeletedRecords = ( + +); +``` + +You can disable this feature by setting the `storeKey` prop to `false`. When disabled, parameters will not be persisted in the store. + +## Return value + +`useDeletedRecordsListController` returns an object with the following keys: + +```tsx +const { + // Data + data, // Array of the deleted records, e.g. [{ id: 123, resource: 'posts', deleted_at: '2025-03-25T12:32:22Z', deleted_by: 'test', data: { ... } }, { ... }, ...] + total, // Total number of deleted records for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23 + isPending, // Boolean, true until the data is available + isFetching, // Boolean, true while the data is being fetched, false once the data is fetched + isLoading, // Boolean, true until the data is fetched for the first time + // Pagination + page, // Current page. Starts at 1 + perPage, // Number of results per page. Defaults to 25 + setPage, // Callback to change the page, e.g. setPage(3) + setPerPage, // Callback to change the number of results per page, e.g. setPerPage(25) + hasPreviousPage, // Boolean, true if the current page is not the first one + hasNextPage, // Boolean, true if the current page is not the last one + // Sorting + sort, // Sort object { field, order }, e.g. { field: 'deleted_at', order: 'DESC' } + setSort, // Callback to change the sort, e.g. setSort({ field: 'id', order: 'ASC' }) + // Filtering + filterValues, // Dictionary of filter values, e.g. { resource: 'posts', deleted_by: 'test' } + setFilters, // Callback to update the filters, e.g. setFilters(filters) + // Record selection + selectedIds, // Array listing the ids of the selected deleted records, e.g. [123, 456] + onSelect, // Callback to change the list of selected deleted records, e.g. onSelect([456, 789]) + onToggleItem, // Callback to toggle the deleted record selection for a given id, e.g. onToggleItem(456) + onUnselectItems, // Callback to clear the deleted records selection, e.g. onUnselectItems(); + // Misc + defaultTitle, // Translated title, e.g. 'Archives' + refetch, // Callback for fetching the deleted records again +} = useDeletedRecordsListController(); +``` + +## Security + +`useDeletedRecordsListController` requires authentication and will redirect anonymous users to the login page. If you want to allow anonymous access, use the [`disableAuthentication`](#disableauthentication) property. + +If your `authProvider` implements [Access Control](./Permissions.md/#access-control), `useDeletedRecordsListController` will only render if the user has the `deleted_records` access on a virtual `ra-soft-delete` resource. + +For instance, for the `` page below: + +```tsx +import { useDeletedRecordsListController } from '@react-admin/ra-core-ee'; + +const CustomDeletedRecords = () => { + const { isPending, error, data, total } = useDeletedRecordsListController({ filter: { resource: 'posts' } }) + if (error) return
Error!
; + if (isPending) return
Loading...
; + return ( +
    + {data.map(deletedRecord => ( +
  • + {deletedRecord.data.title} deleted by {deletedRecord.deleted_by} +
  • + ))} +
+ ); +} +``` + +`useDeletedRecordsListController` will call `authProvider.canAccess()` using the following parameters: + +```tsx +{ resource: 'ra-soft-delete', action: 'list_deleted_records' } +``` + +Users without access will be redirected to the [Access Denied page](./CoreAdmin.md/#accessdenied). + +Note: Access control is disabled when you use [the disableAuthentication property](#disableauthentication). diff --git a/docs_headless/src/content/docs/useGetListDeleted.md b/docs_headless/src/content/docs/useGetListDeleted.md new file mode 100644 index 00000000000..7a46df1ff6a --- /dev/null +++ b/docs_headless/src/content/docs/useGetListDeleted.md @@ -0,0 +1,98 @@ +--- +title: "useGetListDeleted" +--- + +This hook calls `dataProvider.getListDeleted()` when the component mounts. It's ideal for getting a list of deleted records. It supports filtering, sorting and pagination. + +```tsx +const { data, total, isPending, error, refetch, meta } = useGetListDeleted( + { + pagination: { page, perPage }, + sort: { field, order }, + filter, + meta + }, + options +); +``` + +The `meta` argument is optional. It can be anything you want to pass to the data provider, e.g. a list of fields to show in the result. It is distinct from the `meta` property of the response, which may contain additional metadata returned by the data provider. + +The options parameter is optional, and is passed to react-query's `useQuery` hook. Check [react-query's `useQuery` hook documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery) for details on all available option. + +The react-query [query key](https://tanstack.com/query/v5/docs/framework/react/guides/query-keys) for this hook is `['getListDeleted', { pagination, sort, filter, meta }]`. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +Call the `useGetListDeleted` hook when you need to fetch a list of deleted records from the data provider. + +```tsx +import { useGetListDeleted } from '@react-admin/ra-core-ee'; + +const LatestDeletedPosts = () => { + const { data, total, isPending, error } = useGetListDeleted( + { + filter: { resource: "posts" }, + pagination: { page: 1, perPage: 10 }, + sort: { field: 'deleted_at', order: 'DESC' } + } + ); + if (isPending) { return ; } + if (error) { return

ERROR

; } + return ( + <> +

Latest deleted posts

+
    + {data.map(deletedRecord => +
  • {deletedRecord.data.title}
  • + )} +
+

{data.length} / {total} deleted posts

+ + ); +}; +``` + +If you need to learn more about pagination, sort or filter, please refer to [`useGetList` documentation](./useGetList.md), as `useGetListDeleted` implements these parameters the same way. + +## TypeScript + +The `useGetListDeleted` hook accepts a generic parameter for the record type: + +```tsx +import { useGetListDeleted } from '@react-admin/ra-core-ee'; + +const LatestDeletedPosts = () => { + const { data, total, isPending, error } = useGetListDeleted( + { + filter: { resource: "posts" }, + pagination: { page: 1, perPage: 10 }, + sort: { field: 'deleted_at', order: 'DESC' } + } + ); + if (isPending) { return ; } + if (error) { return

ERROR

; } + return ( + <> +

Latest deleted posts

+
    + {/* TypeScript knows that data is of type DeletedRecordType[] */} + {data.map(deletedRecord => +
  • {deletedRecord.data.title}
  • + )} +
+

{data.length} / {total} deleted posts

+ + ); +}; +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useGetOneDeleted.md b/docs_headless/src/content/docs/useGetOneDeleted.md new file mode 100644 index 00000000000..6f0b681b455 --- /dev/null +++ b/docs_headless/src/content/docs/useGetOneDeleted.md @@ -0,0 +1,59 @@ +--- +title: "useGetOneDeleted" +--- + +This hook calls `dataProvider.getOneDeleted()` when the component mounts. It queries the data provider for a single deleted record, based on its id. + +```tsx +const { data, isPending, error, refetch } = useGetOne( + { id, meta }, + options +); +``` + +The `meta` argument is optional. It can be anything you want to pass to the data provider, e.g. a list of fields to show in the result. + +The options parameter is optional, and is passed to react-query's `useQuery` hook. Check [react-query's `useQuery` hook documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery) for details on all available option. + +The react-query [query key](https://tanstack.com/query/v5/docs/framework/react/guides/query-keys) for this hook is `['getOneDeleted', { id: String(id), meta }]`. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +Call `useGetOneDeleted` in a component to query the data provider for a single deleted record, based on its id. + +```tsx +import { useGetOneDeleted } from '@react-admin/ra-core-ee'; + +const DeletedUser = ({ deletedUserId }) => { + const { data: deletedUser, isPending, error } = useGetOneDeleted({ id: deletedUserId }); + if (isPending) { return ; } + if (error) { return

ERROR

; } + return
User {deletedUser.data.username} (deleted by {deletedUser.deleted_by})
; +}; +``` + +## TypeScript + +The `useGetOneDeleted` hook accepts a generic parameter for the record type: + +```tsx +import { useGetOneDeleted } from '@react-admin/ra-core-ee'; + +const DeletedUser = ({ deletedUserId }) => { + const { data: deletedUser, isPending, error } = useGetOneDeleted({ id: deletedUserId }); + if (isPending) { return ; } + if (error) { return

ERROR

; } + // TypeScript knows that deletedUser.data is of type User + return
User {deletedUser.data.username} (deleted by {deletedUser.deleted_by})
; +}; +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useHardDelete.md b/docs_headless/src/content/docs/useHardDelete.md new file mode 100644 index 00000000000..99fe4a6d550 --- /dev/null +++ b/docs_headless/src/content/docs/useHardDelete.md @@ -0,0 +1,91 @@ +--- +title: "useHardDelete" +--- + +This hook allows calling `dataProvider.hardDelete()` when the callback is executed and deleting a single deleted record based on its `id`. + +**Warning**: The `id` here is the ID of the *deleted record*, and **not** the ID of the actual record that has been deleted. + +```tsx +const [hardDeleteOne, { data, isPending, error }] = useHardDelete( + { id, previousData, meta }, + options, +); +``` + +The `hardDeleteOne()` method can be called with the same parameters as the hook: + +```tsx +const [hardDeleteOne, { data, isPending, error }] = useHardDelete(); + +// ... + +hardDeleteOne( + { id, previousData, meta }, + options, +); +``` + +So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the `hardDeleteOne` callback (second example). + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +// set params when calling the hook +import { useRecordContext } from 'ra-core'; +import { useHardDelete } from '@react-admin/ra-core-ee'; + +const HardDeleteButton = () => { + const deletedRecord = useRecordContext(); + const [hardDeleteOne, { isPending, error }] = useHardDelete( + { id: deletedRecord.id, previousData: record } + ); + const handleClick = () => { + hardDeleteOne(); + } + if (error) { return

ERROR

; } + return ; +}; + +// set params when calling the hardDeleteOne callback +import { useRecordContext } from 'ra-core'; +import { useHardDelete } from '@react-admin/ra-core-ee'; + +const HardDeleteButton = () => { + const deletedRecord = useRecordContext(); + const [hardDeleteOne, { isPending, error }] = useHardDelete(); + const handleClick = () => { + hardDeleteOne( + { id: deletedRecord.id, previousData: record } + ); + } + if (error) { return

ERROR

; } + return ; +}; +``` + +## TypeScript + +The `useHardDelete` hook accepts a generic parameter for the record type and another for the error type: + +```tsx +useHardDelete(undefined, undefined, { + onError: (error) => { + // TypeScript knows that error is of type Error + }, + onSettled: (data, error) => { + // TypeScript knows that data is of type DeletedRecordType + // TypeScript knows that error is of type Error + }, +}); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useHardDeleteMany.md b/docs_headless/src/content/docs/useHardDeleteMany.md new file mode 100644 index 00000000000..11ba54c0de0 --- /dev/null +++ b/docs_headless/src/content/docs/useHardDeleteMany.md @@ -0,0 +1,91 @@ +--- +title: "useHardDeleteMany" +--- + +This hook allows calling `dataProvider.hardDeleteMany()` when the callback is executed and deleting an array of deleted records based on their `ids`. + +**Warning**: The `ids` here are the IDs of the *deleted records*, and **not** the IDs of the actual records that have been deleted. + +```tsx +const [hardDeleteMany, { data, isPending, error }] = useHardDeleteMany( + { ids, meta }, + options, +); +``` + +The `hardDeleteMany()` method can be called with the same parameters as the hook: + +```tsx +const [hardDeleteMany, { data, isPending, error }] = useHardDeleteMany(); + +// ... + +hardDeleteMany( + { ids, meta }, + options, +); +``` + +So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the `hardDeleteMany` callback (second example). + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +// set params when calling the hook +import { useListContext } from 'ra-core'; +import { useHardDeleteMany } from '@react-admin/ra-core-ee'; + +const BulkHardDeletePostsButton = () => { + const { selectedIds } = useListContext(); + const [hardDeleteMany, { isPending, error }] = useHardDeleteMany( + { ids: selectedIds } + ); + const handleClick = () => { + hardDeleteMany(); + } + if (error) { return

ERROR

; } + return ; +}; + +// set params when calling the hardDeleteMany callback +import { useListContext } from 'ra-core'; +import { useHardDeleteMany } from '@react-admin/ra-core-ee'; + +const BulkHardDeletePostsButton = () => { + const { selectedIds } = useListContext(); + const [hardDeleteMany, { isPending, error }] = useHardDeleteMany(); + const handleClick = () => { + hardDeleteMany( + { ids: seletedIds } + ); + } + if (error) { return

ERROR

; } + return ; +}; +``` + +## TypeScript + +The `useHardDeleteMany` hook accepts a generic parameter for the record type and another for the error type: + +```tsx +useHardDeleteMany(undefined, undefined, { + onError: (error) => { + // TypeScript knows that error is of type Error + }, + onSettled: (data, error) => { + // TypeScript knows that data is of type Product['id'][] + // TypeScript knows that error is of type Error + }, +}); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useRestoreMany.md b/docs_headless/src/content/docs/useRestoreMany.md new file mode 100644 index 00000000000..a12aa3abcbc --- /dev/null +++ b/docs_headless/src/content/docs/useRestoreMany.md @@ -0,0 +1,101 @@ +--- +title: "useRestoreMany" +--- + +This hook allows calling `dataProvider.restoreMany()` when the callback is executed and restoring an array of deleted records based on their `ids`. + +**Warning**: The `ids` here are the IDs of the *deleted records*, and **not** the IDs of the actual records that have been deleted. + +```tsx +const [restoreMany, { data, isPending, error }] = useRestoreMany( + { ids, meta }, + options, +); +``` + +The `restoreMany()` method can be called with the same parameters as the hook: + +```tsx +const [restoreMany, { data, isPending, error }] = useRestoreMany(); + +// ... + +restoreMany( + { ids, meta }, + options, +); +``` + +So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the `restoreMany` callback (second example). + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +// set params when calling the hook +import { useListContext } from 'ra-core'; +import { useRestoreMany } from '@react-admin/ra-core-ee'; + +const BulkRestorePostsButton = () => { + const { selectedIds } = useListContext(); + const [restoreMany, { isPending, error }] = useRestoreMany( + { ids: selectedIds } + ); + const handleClick = () => { + restoreMany(); + } + if (error) { return

ERROR

; } + return ; +}; + +// set params when calling the restoreMany callback +import { useListContext } from 'ra-core'; +import { useRestoreMany } from '@react-admin/ra-core-ee'; + +const BulkRestorePostsButton = () => { + const { selectedIds } = useListContext(); + const [restoreMany, { isPending, error }] = useRestoreMany(); + const handleClick = () => { + restoreMany( + { ids: seletedIds } + ); + } + if (error) { return

ERROR

; } + return ; +}; +``` + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## TypeScript + +The `useRestoreMany` hook accepts a generic parameter for the record type and another for the error type: + +```tsx +useRestoreMany(undefined, undefined, { + onError: (error) => { + // TypeScript knows that error is of type Error + }, + onSettled: (data, error) => { + // TypeScript knows that data is of type DeletedRecordType[] + // TypeScript knows that error is of type Error + }, +}); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useRestoreOne.md b/docs_headless/src/content/docs/useRestoreOne.md new file mode 100644 index 00000000000..bab2707f76d --- /dev/null +++ b/docs_headless/src/content/docs/useRestoreOne.md @@ -0,0 +1,91 @@ +--- +title: "useRestoreOne" +--- + +This hook allows calling `dataProvider.restoreOne()` when the callback is executed and restoring a single deleted record based on its `id`. + +**Warning**: The `id` here is the ID of the *deleted record*, and **not** the ID of the actual record that has been deleted. + +```tsx +const [restoreOne, { data, isPending, error }] = useRestoreOne( + { id, meta }, + options, +); +``` + +The `restoreOne()` method can be called with the same parameters as the hook: + +```tsx +const [restoreOne, { data, isPending, error }] = useRestoreOne(); + +// ... + +restoreOne( + { id, meta }, + options, +); +``` + +So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the `restoreOne` callback (second example). + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +// set params when calling the hook +import { useRecordContext } from 'ra-core'; +import { useRestoreOne } from '@react-admin/ra-core-ee'; + +const RestoreButton = () => { + const deletedRecord = useRecordContext(); + const [restoreOne, { isPending, error }] = useRestoreOne( + { id: deletedRecord.id } + ); + const handleClick = () => { + restoreOne(); + } + if (error) { return

ERROR

; } + return ; +}; + +// set params when calling the restoreOne callback +import { useRecordContext } from 'ra-core'; +import { useRestoreOne } from '@react-admin/ra-core-ee'; + +const HardDeleteButton = () => { + const deletedRecord = useRecordContext(); + const [restoreOne, { isPending, error }] = useRestoreOne(); + const handleClick = () => { + restoreOne( + { id: deletedRecord.id } + ); + } + if (error) { return

ERROR

; } + return ; +}; +``` + +## TypeScript + +The `useRestoreOne` hook accepts a generic parameter for the record type and another for the error type: + +```tsx +useRestoreOne(undefined, undefined, { + onError: (error) => { + // TypeScript knows that error is of type Error + }, + onSettled: (data, error) => { + // TypeScript knows that data is of type DeletedRecordType + // TypeScript knows that error is of type Error + }, +}); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useSoftDelete.md b/docs_headless/src/content/docs/useSoftDelete.md new file mode 100644 index 00000000000..8e6a4823c0a --- /dev/null +++ b/docs_headless/src/content/docs/useSoftDelete.md @@ -0,0 +1,95 @@ +--- +title: "useSoftDelete" +--- + +This hook allows calling `dataProvider.softDelete()` when the callback is executed and deleting a single record based on its `id`. + +```tsx +const [softDeleteOne, { data, isPending, error }] = useSoftDelete( + resource, + { id, authorId, previousData, meta }, + options, +); +``` + +The `softDeleteOne()` method can be called with the same parameters as the hook: + +```tsx +const [softDeleteOne, { data, isPending, error }] = useSoftDelete(); + +// ... + +softDeleteOne( + resource, + { id, authorId, previousData, meta }, + options, +); +``` + +So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the `softDeleteOne` callback (second example). + +**Tip**: If it's not provided, `useSoftDelete` will automatically populate the `authorId` using your `authProvider`'s `getIdentity` method if there is one. It will use the `id` field of the returned identity object. Otherwise this field will be left blank. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +// set params when calling the hook +import { useRecordContext } from 'ra-core'; +import { useSoftDelete } from '@react-admin/ra-core-ee'; + +const SoftDeleteButton = () => { + const record = useRecordContext(); + const [softDeleteOne, { isPending, error }] = useSoftDelete( + 'likes', + { id: record.id, previousData: record } + ); + const handleClick = () => { + softDeleteOne(); + } + if (error) { return

ERROR

; } + return ; +}; + +// set params when calling the softDeleteOne callback +import { useRecordContext } from 'ra-core'; +import { useSoftDelete } from '@react-admin/ra-core-ee'; + +const SoftDeleteButton = () => { + const record = useRecordContext(); + const [softDeleteOne, { isPending, error }] = useSoftDelete(); + const handleClick = () => { + softDeleteOne( + 'likes', + { id: record.id, previousData: record } + ); + } + if (error) { return

ERROR

; } + return ; +}; +``` + +## TypeScript + +The `useSoftDelete` hook accepts a generic parameter for the record type and another for the error type: + +```tsx +useSoftDelete(undefined, undefined, { + onError: (error) => { + // TypeScript knows that error is of type Error + }, + onSettled: (data, error) => { + // TypeScript knows that data is of type Product + // TypeScript knows that error is of type Error + }, +}); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useSoftDeleteMany.md b/docs_headless/src/content/docs/useSoftDeleteMany.md new file mode 100644 index 00000000000..9257ce007b9 --- /dev/null +++ b/docs_headless/src/content/docs/useSoftDeleteMany.md @@ -0,0 +1,95 @@ +--- +title: "useSoftDeleteMany" +--- + +This hook allows calling `dataProvider.softDeleteMany()` when the callback is executed and deleting an array of records based on their `ids`. + +```tsx +const [softDeleteMany, { data, isPending, error }] = useSoftDeleteMany( + resource, + { ids, authorId, meta }, + options, +); +``` + +The `softDeleteMany()` method can be called with the same parameters as the hook: + +```tsx +const [softDeleteMany, { data, isPending, error }] = useSoftDeleteMany(); + +// ... + +softDeleteMany( + resource, + { ids, authorId, meta }, + options, +); +``` + +So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the `softDeleteMany` callback (second example). + +**Tip**: If it's not provided, `useSoftDeleteMany` will automatically populate the `authorId` using your `authProvider`'s `getIdentity` method if there is one. It will use the `id` field of the returned identity object. Otherwise this field will be left blank. + +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. + +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` + +## Usage + +```tsx +// set params when calling the hook +import { useListContext } from 'ra-core'; +import { useSoftDeleteMany } from '@react-admin/ra-core-ee'; + +const BulkSoftDeletePostsButton = () => { + const { selectedIds } = useListContext(); + const [softDeleteMany, { isPending, error }] = useSoftDeleteMany( + 'posts', + { ids: selectedIds } + ); + const handleClick = () => { + softDeleteMany(); + } + if (error) { return

ERROR

; } + return ; +}; + +// set params when calling the softDeleteMany callback +import { useListContext } from 'ra-core'; +import { useSoftDeleteMany } from '@react-admin/ra-core-ee'; + +const BulkSoftDeletePostsButton = () => { + const { selectedIds } = useListContext(); + const [softDeleteMany, { isPending, error }] = useSoftDeleteMany(); + const handleClick = () => { + softDeleteMany( + 'posts', + { ids: seletedIds } + ); + } + if (error) { return

ERROR

; } + return ; +}; +``` + +## TypeScript + +The `useSoftDeleteMany` hook accepts a generic parameter for the record type and another for the error type: + +```tsx +useSoftDeleteMany(undefined, undefined, { + onError: (error) => { + // TypeScript knows that error is of type Error + }, + onSettled: (data, error) => { + // TypeScript knows that data is of type Product[] + // TypeScript knows that error is of type Error + }, +}); +``` \ No newline at end of file