ra-history

react-admin ≥ 4.14.0

Track the changes made in your admin. See the history of revisions, compare differences between any two versions, and revert to a previous state if needed.

Test it live on the Enterprise Edition Storybook and in the e-commerce demo.

Installation

npm install --save @react-admin/ra-history
# or
yarn add @react-admin/ra-history

Tip: ra-history is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

Data Provider Methods

ra-history relies on the dataProvider to read, create and delete revisions. In order to use the ra-history package, you must add 3 new methods to your data provider: getRevisions, addRevision and deleteRevisions.

const dataProviderWithRevisions = {
    ...dataProvider,
    getRevisions: async (resource, params) => {
        const { recordId } = params;
        // ...
        return { data: revisions };
    },
    addRevision: async (resource, params) => {
        const { recordId, data, authorId, message, description } = params;
        // ...
        return { data: revision };
    },
    deleteRevisions: async resource => {
        const { recordId } = params;
        // ...
        return { data: deletedRevisionIds };
    },
};
const dataProviderWithRevisions = {
    ...dataProvider,
    getRevisions: async (resource, params) => {
        const { recordId } = params;
        // ...
        return { data: revisions };
    },
    addRevision: async (resource, params) => {
        const { recordId, data, authorId, message, description } = params;
        // ...
        return { data: revision };
    },
    deleteRevisions: async resource => {
        const { recordId } = params;
        // ...
        return { data: deletedRevisionIds };
    },
};

A revision is an object with the following properties:

{
    id: 123, // the revision id
    resource: 'products', // the resource name
    recordId: 456, // the id of the record
    data: {
        id: 456,
        title: 'Lorem ipsum',
        teaser: 'Lorem ipsum dolor sit amet',
        body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
    }, // the data of the record
    // metadata
    authorId: 789, // the id of the author
    date: '2021-10-01T00:00:00.000Z', // the date of the revision
    message: 'Updated title, teaser, body', // the commit message
    description: 'Added a teaser', // the commit description
}

You can read an example data provider implementation in the package source at src/dataProvider/builder/addRevisionMethodsBasedOnSingleResource.ts.

Instead of implementing these new methods yourself, you can use one of the provided builders to generate them:

  • addRevisionMethodsBasedOnSingleResource stores the revisions for all resources in a single revisions resource:
// in src/dataProvider.ts
import { addRevisionMethodsBasedOnSingleResource } from '@react-admin/ra-history';
import baseDataProvider from './baseDataProvider';

export const dataProvider = addRevisionMethodsBasedOnSingleResource(
    baseDataProvider,
    { resourceName: 'revisions' }
);
// in src/dataProvider.ts
import { addRevisionMethodsBasedOnSingleResource } from '@react-admin/ra-history';
import baseDataProvider from './baseDataProvider';

export const dataProvider = addRevisionMethodsBasedOnSingleResource(
    baseDataProvider,
    { resourceName: 'revisions' }
);
  • addRevisionMethodsBasedOnRelatedResource stores the revisions of each resource in a related resource (e.g. store the revisions of products in products_history):
// in src/dataProvider.ts
import { addRevisionMethodsBasedOnRelatedResource } from '@react-admin/ra-history';
import baseDataProvider from './baseDataProvider';

export const dataProvider = addRevisionMethodsBasedOnRelatedResource(
    baseDataProvider,
    { getRevisionResourceName: resource => `${resource}_history` }
);
// in src/dataProvider.ts
import { addRevisionMethodsBasedOnRelatedResource } from '@react-admin/ra-history';
import baseDataProvider from './baseDataProvider';

export const dataProvider = addRevisionMethodsBasedOnRelatedResource(
    baseDataProvider,
    { getRevisionResourceName: resource => `${resource}_history` }
);

Once your provider has the three revisions methods, pass it to the <Admin> component and you're ready to start using ra-history.

// in src/App.tsx
import { Admin } from 'react-admin';
import { dataProvider } from './dataProvider';

const App = () => <Admin dataProvider={dataProvider}>{/* ... */}</Admin>;
// in src/App.tsx
import { Admin } from 'react-admin';
import { dataProvider } from './dataProvider';

const App = () => <Admin dataProvider={dataProvider}>{/* ... */}</Admin>;

Tip: Revisions are immutable, so you don't need to implement an updateRevision method.

Usage

First, update your Creation and Edition components to save a new revision each time the user submits the form. To do so, replace:

  • <SimpleForm> with <SimpleFormWithRevision>
  • <TabbedForm> with <TabbedFormWithRevision>
// in src/products/ProductCreate.js
import { Create, TextInput, SelectInput } from 'react-admin';
import { SimpleFormWithRevision } from '@react-admin/ra-history';
import categories from './categories';

export const ProductCreate = () => (
    <Create>
        <SimpleFormWithRevision>
            <TextInput source="reference" />
            <TextInput multiline source="description" />
            <TextInput source="image" />
            <SelectInput source="category" choices={categories} />
        </SimpleFormWithRevision>
    </Create>
);
// in src/products/ProductCreate.js
import { Create, TextInput, SelectInput } from 'react-admin';
import { SimpleFormWithRevision } from '@react-admin/ra-history';
import categories from './categories';

export const ProductCreate = () => (
    <Create>
        <SimpleFormWithRevision>
            <TextInput source="reference" />
            <TextInput multiline source="description" />
            <TextInput source="image" />
            <SelectInput source="category" choices={categories} />
        </SimpleFormWithRevision>
    </Create>
);

When they submit a form, users will see a dialog inviting them to define a commit message and description. The commit message is mandatory, and will be displayed in the list of revisions. The description is optional, and will be displayed in the diff view. Once they click on the "Save" button, the form will be submitted, and a new revision will be created.

Next, let users see the history of a record, and revert to a previous version. To do so, add a <RevisionsButton> to the actions toolbar of a detail view:

// in src/products/ProductEdit.js
import { Edit, SelectInput, TextInput, TopToolbar } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionsButton,
} from '@react-admin/ra-history';
import categories from './categories';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton allowRevert />
    </TopToolbar>
);

export const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>
            <TextInput source="reference" />
            <TextInput multiline source="description" />
            <TextInput source="image" />
            <SelectInput source="category" choices={categories} />
        </SimpleFormWithRevision>
    </Edit>
);
// in src/products/ProductEdit.js
import { Edit, SelectInput, TextInput, TopToolbar } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionsButton,
} from '@react-admin/ra-history';
import categories from './categories';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton allowRevert />
    </TopToolbar>
);

export const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>
            <TextInput source="reference" />
            <TextInput multiline source="description" />
            <TextInput source="image" />
            <SelectInput source="category" choices={categories} />
        </SimpleFormWithRevision>
    </Edit>
);

The <RevisionsButton> will open a menu listing all the revisions of the current record. Clicking on a revision will open a diff view, allowing the user to see the changes between the current version and the selected revision. The user can then revert to the selected revision by clicking on the "Revert" button.

ra-history provides alternative ways to visualize the history of a record:

  • <TabbedFormWithRevision> includes a tab with the list of revisions, so you don't need to add a <RevisionsButton> to the actions toolbar.
  • You can use the aside prop to display the list of revisions in a sidebar. Leverage the <RevisionListWithDetailsInDialog> component to display the diff view in a dialog.

Check the documentation of each of these components to learn more.

<SimpleFormWithRevision>

A replacement for <SimpleForm> that adds a new revision after save.

SimpleFormWithRevision

It saves a new revision each time the user submits the form, and applies the changes from the selected revision when the user reverts to a previous version. In addition, when users click on the Delete button, it deletes all the revisions of the current record.

Usage

Use <SimpleFormWithRevision> as a child of <Create> and <Edit>, as you would with <SimpleForm>. It accepts Input components as children.

// in src/posts/PostCreate.js
import { Create, TextInput } from 'react-admin';
import { SimpleFormWithRevision } from '@react-admin/ra-history';

export const PostCreate = () => (
    <Create>
        <SimpleFormWithRevision>
            <TextInput source="title" />
            <TextInput source="teaser" />
            <TextInput multiline source="body" />
        </SimpleFormWithRevision>
    </Create>
);
// in src/posts/PostCreate.js
import { Create, TextInput } from 'react-admin';
import { SimpleFormWithRevision } from '@react-admin/ra-history';

export const PostCreate = () => (
    <Create>
        <SimpleFormWithRevision>
            <TextInput source="title" />
            <TextInput source="teaser" />
            <TextInput multiline source="body" />
        </SimpleFormWithRevision>
    </Create>
);

<SimpleFormWithRevision> calls dataProvider.addRevision() after the form changes have been saved.

<SimpleFormWithRevision> doesn't display the list of revisions. For that, you'll have to use <RevisionsButton> in the page actions, or <RevisionListWithDetailsInDialog> in an aside.

// in src/posts/PostEdit.js
import { Edit, TextInput, TopToolbar } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionsButton,
} from '@react-admin/ra-history';

const PostEditActions = () => (
    <TopToolbar>
        <RevisionsButton />
    </TopToolbar>
);

export const PostEdit = () => (
    <Edit actions={<PostEditActions />}>
        <SimpleFormWithRevision>
            <TextInput source="title" />
            <TextInput source="teaser" />
            <TextInput multiline source="body" />
        </SimpleFormWithRevision>
    </Edit>
);
// in src/posts/PostEdit.js
import { Edit, TextInput, TopToolbar } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionsButton,
} from '@react-admin/ra-history';

const PostEditActions = () => (
    <TopToolbar>
        <RevisionsButton />
    </TopToolbar>
);

export const PostEdit = () => (
    <Edit actions={<PostEditActions />}>
        <SimpleFormWithRevision>
            <TextInput source="title" />
            <TextInput source="teaser" />
            <TextInput multiline source="body" />
        </SimpleFormWithRevision>
    </Edit>
);

Props

<SimpleFormWithRevision> accepts the same props as <SimpleForm>, and adds the following:

Prop Required Type Default Description
skipUserDetails Optional Boolean false If true, users won't be prompted to enter a commit message and description when they submit the form.

skipUserDetails

By default, <TabbedFormWithRevision> prompts the user to enter a commit message and description when they submit the form. You can skip this step by setting the skipUserDetails prop to true.

export const PostEdit = () => (
    <Edit>
        <SimpleFormWithRevision skipUserDetails>
            {/* ... */}
        </SimpleFormWithRevision>
    </Edit>
);
export const PostEdit = () => (
    <Edit>
        <SimpleFormWithRevision skipUserDetails>
            {/* ... */}
        </SimpleFormWithRevision>
    </Edit>
);

With this prop set, revisions will be saved with the current user as author, and an auto-generated commit message based on the updated fields (e.g. "Updated title, teaser, body").

<TabbedFormWithRevision>

<TabbedFormWithRevision> is a drop-in replacement for <TabbedForm>. It saves a new revision each time the user submits the form, and applies the changes from the selected revision when the user reverts to a previous version. It also adds a new tab with the list of revisions. Finally, when users click on the Delete button, it deletes all the revisions of the current record.

TabbedFormWithRevision

Usage

Use <TabbedFormWithRevision> as a child of <Create> and <Edit>, as you would with <TabbedForm>. It expects <TabbedFormWithRevision.Tab> components as children.

// in src/posts/PostEdit.js
import { Edit, TextInput } from 'react-admin';
import { TabbedFormWithRevision } from '@react-admin/ra-history';

export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision>
            <TabbedFormWithRevision.Tab label="Main">
                <TextInput source="title" />
                <TextInput source="teaser" />
                <TextInput multiline source="body" />
            </TabbedFormWithRevision.Tab>
        </TabbedFormWithRevision>
    </Edit>
);
// in src/posts/PostEdit.js
import { Edit, TextInput } from 'react-admin';
import { TabbedFormWithRevision } from '@react-admin/ra-history';

export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision>
            <TabbedFormWithRevision.Tab label="Main">
                <TextInput source="title" />
                <TextInput source="teaser" />
                <TextInput multiline source="body" />
            </TabbedFormWithRevision.Tab>
        </TabbedFormWithRevision>
    </Edit>
);

<TabbedFormWithRevision> calls dataProvider.addRevision() after the form changes have been saved.

Props

<TabbedFormWithRevision> accepts the same props as <TabbedForm>, and adds the following:

Prop Required Type Default Description
allowRevert Optional Boolean true If true, users will be able to revert to a previous version of the record.
diff Optional Element <DefaultDiff Element /> The element used to represent the diff between two versions.
renderName Optional Function A function to render the author name based on its id
skipUser Details Optional Boolean false If true, users won't be prompted to enter a commit message and description when they submit the form.

allowRevert

By default, the "Revisions" tab of <TabbedFormWithRevision> includes a button to revert to a previous version of the record. You can disable this button by setting the allowRevert prop to false.

export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision allowRevert={false}>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);
export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision allowRevert={false}>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);

diff

By default, the "Revisions" tab of <TabbedFormWithRevision> includes a diff view to compare the current version of the record with a previous version. You can customize the diff view by setting the diff prop to a React element.

This element can grab the current record using useRecordContext, and the record from the revision selected by the user using useReferenceRecordContext. But instead of doing the diff by hand, you can use the two field diff components provided by ra-history:

  • <FieldDiff> displays the diff of a given field. It accepts a react-admin Field component as child.
  • <SmartFieldDiff> displays the diff of a string field, and uses a word-by-word diffing algorithm to highlight the changes.

So a custom diff view is usually a layout component with <FieldDiff> and <SmartFieldDiff> components as children:

import { Stack } from '@mui/material';
import {
    FieldDiff,
    SmartFieldDiff,
    TabbedFormWithRevision,
} from '@react-admin/ra-history';
import { Edit, NumberField } from 'react-admin';

const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="image" />
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="width" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="height" />
            </FieldDiff>
        </Stack>
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="price" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="stock" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="sales" />
            </FieldDiff>
        </Stack>
    </Stack>
);

export const ProductEdit = () => (
    <Edit>
        <TabbedFormWithRevision diff={<ProductDiff />}>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);
import { Stack } from '@mui/material';
import {
    FieldDiff,
    SmartFieldDiff,
    TabbedFormWithRevision,
} from '@react-admin/ra-history';
import { Edit, NumberField } from 'react-admin';

const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="image" />
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="width" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="height" />
            </FieldDiff>
        </Stack>
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="price" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="stock" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="sales" />
            </FieldDiff>
        </Stack>
    </Stack>
);

export const ProductEdit = () => (
    <Edit>
        <TabbedFormWithRevision diff={<ProductDiff />}>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);

renderName

Revisions keep an authorId, but not the name of the revision author. You can use the renderName prop to display the name of the author in the list of revisions based on your user data. It expects a function that accepts the authorId and returns a React element.

For instance, if the users are stored in a users resource, you can use the following:

const UserName = ({ id }) => {
    const { data: user } = useGetOne('users', { id });
    if (!user) return null;
    return (
        <>
            {user.firstName} {user.lastName}
        </>
    );
};

export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision renderName={id => <UserName id={id} />}>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);
const UserName = ({ id }) => {
    const { data: user } = useGetOne('users', { id });
    if (!user) return null;
    return (
        <>
            {user.firstName} {user.lastName}
        </>
    );
};

export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision renderName={id => <UserName id={id} />}>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);

skipUserDetails

By default, <TabbedFormWithRevision> prompts the user to enter a commit message and description when they submit the form. You can skip this step by setting the skipUserDetails prop to true.

export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision skipUserDetails>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);
export const PostEdit = () => (
    <Edit>
        <TabbedFormWithRevision skipUserDetails>
            {/* ... */}
        </TabbedFormWithRevision>
    </Edit>
);

With this prop set, revisions will be saved with the current user as author, and an auto-generated commit message based on the updated fields (e.g. "Updated title, teaser, body").

<RevisionsButton>

<RevisionsButton> is a button that opens a menu with the list of revisions of the current record. When users select a revision, it opens a diff view, allowing them to see the changes between the current version and the selected revision. The user can then revert to the selected revision by clicking on the "Revert" button.

Usage

<RevisionsButton> is usually used in the page actions of an <Edit> component.

import { Edit, SelectInput, TextInput, TopToolbar } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionsButton,
} from '@react-admin/ra-history';
import categories from './categories';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton />
    </TopToolbar>
);

export const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>
            <TextInput source="reference" />
            <TextInput multiline source="description" />
            <TextInput source="image" />
            <SelectInput source="category" choices={categories} />
        </SimpleFormWithRevision>
    </Edit>
);
import { Edit, SelectInput, TextInput, TopToolbar } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionsButton,
} from '@react-admin/ra-history';
import categories from './categories';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton />
    </TopToolbar>
);

export const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>
            <TextInput source="reference" />
            <TextInput multiline source="description" />
            <TextInput source="image" />
            <SelectInput source="category" choices={categories} />
        </SimpleFormWithRevision>
    </Edit>
);

It reads the current record from the RecordContext, and the current resource from the ResourceContext. It calls dataProvider.getRevisions() to fetch the list of revisions of the current record.

Props

Prop Required Type Default Description
allowRevert Optional Boolean false If true, users will be able to revert to a previous version of the record.
diff Optional Element <DefaultDiff Element /> The element used to represent the diff between two versions.
onSelect Optional Function A function to call when the user selects a revision. It receives the revision as argument.
renderName Optional Function A function to render the author name based on its id

allowRevert

By default, the detail view of a revision rendered in the dialog is read-only. You can include a button to revert to a previous version of the record by setting the allowRevert prop.

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton allowRevert />
    </TopToolbar>
);
const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton allowRevert />
    </TopToolbar>
);

diff

The detail view of a revision includes a diff view to compare the current version of the record with a previous version. You can customize the diff view by setting the diff prop to a React element.

This element can grab the current record using useRecordContext, and the record from the revision selected by the user using useReferenceRecordContext. But instead of doing the diff by hand, you can use the two field diff components provided by ra-history:

  • <FieldDiff> displays the diff of a given field. It accepts a react-admin Field component as child.
  • <SmartFieldDiff> displays the diff of a string field, and uses a word-by-word diffing algorithm to highlight the changes.

So a custom diff view is usually a layout component with <FieldDiff> and <SmartFieldDiff> components as children:

import { Stack } from '@mui/material';
import {
    FieldDiff,
    SmartFieldDiff,
    RevisionsButton,
} from '@react-admin/ra-history';
import { Edit, NumberField } from 'react-admin';

const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="image" />
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="width" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="height" />
            </FieldDiff>
        </Stack>
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="price" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="stock" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="sales" />
            </FieldDiff>
        </Stack>
    </Stack>
);

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton diff={<ProductDiff />} />
    </TopToolbar>
);
import { Stack } from '@mui/material';
import {
    FieldDiff,
    SmartFieldDiff,
    RevisionsButton,
} from '@react-admin/ra-history';
import { Edit, NumberField } from 'react-admin';

const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="image" />
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="width" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="height" />
            </FieldDiff>
        </Stack>
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="price" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="stock" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="sales" />
            </FieldDiff>
        </Stack>
    </Stack>
);

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton diff={<ProductDiff />} />
    </TopToolbar>
);

onSelect

If you want to do something when users select a given revision, you can use the onSelect prop. It receives the selected revision as argument.

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton onSelect={revision => console.log(revision)} />
    </TopToolbar>
);
const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton onSelect={revision => console.log(revision)} />
    </TopToolbar>
);

renderName

Revisions keep an authorId, but not the name of the revision author. You can use the renderName prop to display the name of the author in the list of revisions based on your user data. It expects a function that accepts the authorId and returns a React element.

For instance, if the users are stored in a users resource, you can use the following:

const UserName = ({ id }) => {
    const { data: user } = useGetOne('users', { id });
    if (!user) return null;
    return (
        <>
            {user.firstName} {user.lastName}
        </>
    );
};

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton renderName={id => <UserName id={id} />} />
    </TopToolbar>
);
const UserName = ({ id }) => {
    const { data: user } = useGetOne('users', { id });
    if (!user) return null;
    return (
        <>
            {user.firstName} {user.lastName}
        </>
    );
};

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton renderName={id => <UserName id={id} />} />
    </TopToolbar>
);

<RevisionListWithDetailsInDialog>

<RevisionListWithDetailsInDialog> renders the list of revisions of the current record. When users select a revision, it opens a diff view in a dialog, allowing them to see the changes between the current version and the selected revision. The user can then revert to the selected revision by clicking on the "Revert" button.

Usage

<RevisionListWithDetailsInDialog> is usually used in an <Edit aside> or a <Show aside>.

import { Edit } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionListWithDetailsInDialog,
} from '@react-admin/ra-history';
import { Box, Typography } from '@mui/material';

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog allowRevert />
    </Box>
);

export const ProductEdit = () => (
    <Edit aside={<ProductAside />}>
        <SimpleFormWithRevision>{/* ... */}</SimpleFormWithRevision>
    </Edit>
);
import { Edit } from 'react-admin';
import {
    SimpleFormWithRevision,
    RevisionListWithDetailsInDialog,
} from '@react-admin/ra-history';
import { Box, Typography } from '@mui/material';

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog allowRevert />
    </Box>
);

export const ProductEdit = () => (
    <Edit aside={<ProductAside />}>
        <SimpleFormWithRevision>{/* ... */}</SimpleFormWithRevision>
    </Edit>
);

It reads the current record from the RecordContext, and the current resource from the ResourceContext. It calls dataProvider.getRevisions() to fetch the list of revisions of the current record.

Props

Prop Required Type Default Description
allowRevert Optional Boolean false If true, users will be able to revert to a previous version of the record.
diff Optional Element <DefaultDiff Element /> The element used to represent the diff between two versions.
onSelect Optional Function A function to call when the user selects a revision. It receives the revision as argument.
renderName Optional Function A function to render the author name based on its id

allowRevert

By default, the detail view of a revision rendered in the dialog is read-only. You can include a button to revert to a previous version of the record by setting the allowRevert prop.

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog allowRevert />
    </Box>
);
const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog allowRevert />
    </Box>
);

diff

The detail view of a revision includes a diff view to compare the current version of the record with a previous version. You can customize the diff view by setting the diff prop to a React element.

This element can grab the current record using useRecordContext, and the record from the revision selected by the user using useReferenceRecordContext. But instead of doing the diff by hand, you can use the two field diff components provided by ra-history:

  • <FieldDiff> displays the diff of a given field. It accepts a react-admin Field component as child.
  • <SmartFieldDiff> displays the diff of a string field, and uses a word-by-word diffing algorithm to highlight the changes.

So a custom diff view is usually a layout component with <FieldDiff> and <SmartFieldDiff> components as children:

import { Stack } from '@mui/material';
import {
    FieldDiff,
    SmartFieldDiff,
    RevisionListWithDetailsInDialog,
} from '@react-admin/ra-history';
import { Edit, NumberField } from 'react-admin';

const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="image" />
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="width" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="height" />
            </FieldDiff>
        </Stack>
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="price" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="stock" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="sales" />
            </FieldDiff>
        </Stack>
    </Stack>
);

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog diff={<ProductDiff />} />
    </Box>
);
import { Stack } from '@mui/material';
import {
    FieldDiff,
    SmartFieldDiff,
    RevisionListWithDetailsInDialog,
} from '@react-admin/ra-history';
import { Edit, NumberField } from 'react-admin';

const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="image" />
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="width" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="height" />
            </FieldDiff>
        </Stack>
        <Stack direction="row" gap={2}>
            <FieldDiff inline>
                <NumberField source="price" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="stock" />
            </FieldDiff>
            <FieldDiff inline>
                <NumberField source="sales" />
            </FieldDiff>
        </Stack>
    </Stack>
);

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog diff={<ProductDiff />} />
    </Box>
);

onSelect

If you want to do something when users select a given revision, you can use the onSelect prop. It receives the selected revision as argument.

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog
            onSelect={revision => console.log(revision)}
        />
    </Box>
);
const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog
            onSelect={revision => console.log(revision)}
        />
    </Box>
);

renderName

Revisions keep an authorId, but not the name of the revision author. You can use the renderName prop to display the name of the author in the list of revisions based on your user data. It expects a function that accepts the authorId and returns a React element.

For instance, if the users are stored in a users resource, you can use the following:

const UserName = ({ id }) => {
    const { data: user } = useGetOne('users', { id });
    if (!user) return null;
    return (
        <>
            {user.firstName} {user.lastName}
        </>
    );
};

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog
            renderName={id => <UserName id={id} />}
        />
    </Box>
);
const UserName = ({ id }) => {
    const { data: user } = useGetOne('users', { id });
    if (!user) return null;
    return (
        <>
            {user.firstName} {user.lastName}
        </>
    );
};

const ProductAside = () => (
    <Box width={300} px={2}>
        <Typography variant="h6" gutterBottom>
            Revisions
        </Typography>
        <RevisionListWithDetailsInDialog
            renderName={id => <UserName id={id} />}
        />
    </Box>
);

<FieldDiff>

<FieldDiff> displays the diff of a given field. It accepts a react-admin Field component as child.

FieldDiff

Usage

import { Stack } from '@mui/material';
import { FieldDiff } from '@react-admin/ra-history';
import { NumberField } from 'react-admin';

export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <FieldDiff source="description" />
        <FieldDiff inline>
            <NumberField source="stock" />
        </FieldDiff>
    </Stack>
);
import { Stack } from '@mui/material';
import { FieldDiff } from '@react-admin/ra-history';
import { NumberField } from 'react-admin';

export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <FieldDiff source="description" />
        <FieldDiff inline>
            <NumberField source="stock" />
        </FieldDiff>
    </Stack>
);

By default, <FieldDiff> displays the diff using a <TextField>. When used with a child component (like <NumberField> in the example above), it renders the field value using the child component. If the two values are different, <FieldDiff> renders the old value in a <del> tag, and the new value in an <ins> tag. If the two values are the same, it renders the value without any tag.

<FieldDiff> must be used in a diff element like the <ProductDiff> above. This element must be passed in the diff prop, for instance in <RevisionsButton>:

import { TopToolbar, Edit } from 'react-admin';
import {
    RevisionsButton,
    SimpleFormWithRevision,
} from '@react-admin/ra-history';
import { ProductDiff } from './ProductDiff';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton diff={<ProductDiff />} />
    </TopToolbar>
);

const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>{/* ... */}</SimpleFormWithRevision>
    </Edit>
);
import { TopToolbar, Edit } from 'react-admin';
import {
    RevisionsButton,
    SimpleFormWithRevision,
} from '@react-admin/ra-history';
import { ProductDiff } from './ProductDiff';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton diff={<ProductDiff />} />
    </TopToolbar>
);

const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>{/* ... */}</SimpleFormWithRevision>
    </Edit>
);

Tip: If you need a more advanced diffing algorithm, use <SmartFieldDiff> instead.

Props

Prop Required Type Default Description
children Optional ReactElement <TextField> The component used to render the field value
inline Optional Boolean false If true, the diff will be displayed inline, instead of in a new line.
label Optional String The label of the field. Inferred from the child source if not provided.
source Optional String The name of the field to diff. Inferred from the child if not provided.
sx Optional Object The style of the field.

children

By default, <FieldDiff> displays the diff using a <TextField>.

export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="stock" />
    </Stack>
);
export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="stock" />
    </Stack>
);

You can customize the component used to display the field value by passing it as child. Use any of react-admin's Field components for that. And since the child requires a source prop, you can omit the source prop on <FieldDiff>:

export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff>
            <NumberField source="stock" />
        </FieldDiff>
    </Stack>
);
export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff>
            <NumberField source="stock" />
        </FieldDiff>
    </Stack>
);

inline

By default, <FieldDiff> displays the diff in two lines: one for the previous value, and one for the new value. You can display it inline by setting the inline prop to true.

<FieldDiff source="reference" inline />
<FieldDiff source="reference" inline />

label

By default, <FieldDiff> uses the humanized source prop as label. You can customize the label by setting the label prop. It accepts a string or a React element. If it's a string, react-admin will pass it to the translate function.

<FieldDiff source="reference" label="Ref." />
<FieldDiff source="reference" label="Ref." />

source

By default, <FieldDiff> uses the source prop of its child as source. You can customize the source by setting the source prop.

export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <FieldDiff source="description" />
        <FieldDiff source="stock" />
    </Stack>
);
export const ProductDiff = () => (
    <Stack gap={1}>
        <FieldDiff source="reference" />
        <FieldDiff source="description" />
        <FieldDiff source="stock" />
    </Stack>
);

sx

You can customize the style of the field by passing a style object to the sx prop. This allows to change the style of the diff, implemented using <ins> and <del> tags.

<FieldDiff
    source="reference"
    sx={{
        '& ins': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'underline',
        },
        '& del': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'line-through',
        },
    }}
/>
<FieldDiff
    source="reference"
    sx={{
        '& ins': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'underline',
        },
        '& del': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'line-through',
        },
    }}
/>

<SmartFieldDiff>

<SmartFieldDiff> displays the diff of a string field, and uses a word-by-word diffing algorithm to highlight the changes. If the value isn't a string, it stringifies it before diffing.

SmartFieldDiff

Usage

import { SmartFieldDiff } from '@react-admin/ra-history';
import { Stack } from '@mui/material';

export const ProductDiff = () => (
    <Stack gap={1}>
        <SmartFieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="stock" />
    </Stack>
);
import { SmartFieldDiff } from '@react-admin/ra-history';
import { Stack } from '@mui/material';

export const ProductDiff = () => (
    <Stack gap={1}>
        <SmartFieldDiff source="reference" />
        <SmartFieldDiff source="description" />
        <SmartFieldDiff source="stock" />
    </Stack>
);

<SmartFieldDiff> must be used in a diff element like the <ProductDiff> above. This element must be passed in the diff prop, for instance in <RevisionsButton>:

import { TopToolbar, Edit } from 'react-admin';
import {
    RevisionsButton,
    SimpleFormWithRevision,
} from '@react-admin/ra-history';
import { ProductDiff } from './ProductDiff';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton diff={<ProductDiff />} />
    </TopToolbar>
);

const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>{/* ... */}</SimpleFormWithRevision>
    </Edit>
);
import { TopToolbar, Edit } from 'react-admin';
import {
    RevisionsButton,
    SimpleFormWithRevision,
} from '@react-admin/ra-history';
import { ProductDiff } from './ProductDiff';

const ProductEditActions = () => (
    <TopToolbar>
        <RevisionsButton diff={<ProductDiff />} />
    </TopToolbar>
);

const ProductEdit = () => (
    <Edit actions={<ProductEditActions />}>
        <SimpleFormWithRevision>{/* ... */}</SimpleFormWithRevision>
    </Edit>
);

Props

Prop Required Type Default Description
source Required String The name of the field to diff. Inferred from the child if not provided.
label Optional String The label of the field. Inferred from the child source if not provided.
sx Optional Object The style of the field.

label

By default, <SmartFieldDiff> uses the humanized source prop as label. You can customize the label by setting the label prop. It accepts a string or a React element. If it's a string, react-admin will pass it to the translate function.

<SmartFieldDiff source="description" label="Desc." />
<SmartFieldDiff source="description" label="Desc." />

source

You must set the source prop to the name of the field to diff.

<SmartFieldDiff source="description" />
<SmartFieldDiff source="description" />

sx

You can customize the style of the field by passing a style object to the sx prop. This allows to change the style of the diff, implemented using <ins> and <del> tags.

<SmartFieldDiff
    source="description"
    sx={{
        '& ins': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'underline',
        },
        '& del': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'line-through',
        },
    }}
/>
<SmartFieldDiff
    source="description"
    sx={{
        '& ins': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'underline',
        },
        '& del': {
            color: 'inherit',
            backgroundColor: 'inherit',
            textDecoration: 'line-through',
        },
    }}
/>

CHANGELOG

v4.0.0

2024-01-09

  • First release