Skip to content

Commit

Permalink
Merge pull request #9584 from marmelab/history-doc
Browse files Browse the repository at this point in the history
[Doc] Add documentation for the versioning features
  • Loading branch information
djhi authored Jan 16, 2024
2 parents ba9f534 + acc363f commit e6edfe4
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 32 deletions.
34 changes: 33 additions & 1 deletion docs/Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,6 @@ You can also use the [`<SmartRichTextInput>`](./SmartRichTextInput.md) component

## Fast


React-admin takes advantage of the Single-Page-Application architecture, implementing various performance optimizations that make react-admin apps incredibly fast by default.

- **Non-Blocking Data Fetching**: Instead of waiting for API data before starting to render the UI, React-admin initiates the rendering process immediately. This strategy ensures a snappy application where user interactions receive instant feedback, outperforming Server-side Rendered apps by eliminating waiting times for server responses.
Expand Down Expand Up @@ -939,6 +938,31 @@ To learn more about authentication, roles, and permissions, check out the follow
- [`useCanAccess`](./useCanAccess.md)
- [`canAccess`](./canAccess.md)

## Revisions & Versioning

React-admin lets users **track the changes** made to any record. They can see the **history of revisions**, **compare differences** between any two versions, and **revert to a previous state** if needed.

<video controls autoplay playsinline muted loop>
<source src="https://marmelab.com/ra-enterprise/modules/assets/ra-history.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>

In detail, revision tracking lets you:

- Prevent data loss with robust version control
- Enhance transparency with detailed change logs
- Uncover insights with the 'diff' feature, a powerful tool for comparing versions
- Boost confidence in making changes with easy rollback options

These features are available through the following components:

- [`<SimpleFormWithRevision>`](https://marmelab.com/ra-enterprise/modules/ra-history#simpleformwithrevision)
- [`<TabbedFormWithRevision>`](https://marmelab.com/ra-enterprise/modules/ra-history#tabbedformwithrevision)
- [`<RevisionsButton>`](./RevisionsButton.md)
- [`<RevisionListWithDetailsInDialog>`](https://marmelab.com/ra-enterprise/modules/ra-history#revisionlistwithdetailsindialog)
- [`<FieldDiff>`](https://marmelab.com/ra-enterprise/modules/ra-history#fielddiff)
- [`<SmartFieldDiff>`](https://marmelab.com/ra-enterprise/modules/ra-history#smartfielddiff)

## Audit Log

Most admin and B2B apps require that user actions are recorded for audit purposes. React-admin provides templates for displaying such audit logs, and helpers to automatically **record user actions**.
Expand All @@ -964,6 +988,14 @@ const Dashboard = () => {
};
```

The Audit Log features let you:

- Comply with data and action traceability regulations
- Troubleshoot and resolve problems with a clear action trail
- Boost security by detecting unusual activity
- Improve accountability with detailed action records
- Monitor user activity with an aggregated timeline

These features are available through the following components:

- [`<Timeline>`](https://marmelab.com/ra-enterprise/modules/ra-audit-log#timeline) shows a list of all recent changes in the admin. It's a great component for dashboards.
Expand Down
6 changes: 6 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ title: "Index"
* [`<Empty>`](./List.md#empty)

**- F -**
* [`<FieldDiff>`](https://marmelab.com/ra-enterprise/modules/ra-history#fielddiff)<img class="icon" src="./img/premium.svg" />
* [`<FileField>`](./FileField.md)
* [`<FileInput>`](./FileInput.md)
* [`<Filter>`](./List.md#filters-filter-inputs)
Expand Down Expand Up @@ -140,6 +141,8 @@ title: "Index"
* [`<ReferenceOneField>`](./ReferenceOneField.md)
* [`<ReferenceOneInput>`](./ReferenceOneInput.md)
* [`<Resource>`](./Resource.md)
* [`<RevisionsButton>`](./RevisionsButton.md)<img class="icon" src="./img/premium.svg" />
* [`<RevisionListWithDetailsInDialog>`](https://marmelab.com/ra-enterprise/modules/ra-history#revisionlistwithdetailsindialog)<img class="icon" src="./img/premium.svg" />
* [`<RichTextField>`](./RichTextField.md)
* [`<RichTextInput>`](./RichTextInput.md)
* [`<RowForm>`](https://marmelab.com/ra-enterprise/modules/ra-editable-datagrid#rowform)<img class="icon" src="./img/premium.svg" />
Expand All @@ -164,9 +167,11 @@ title: "Index"
* [`<SidebarOpenPreferenceSync>`](https://marmelab.com/ra-enterprise/modules/ra-preferences#sidebaropenpreferencesync-store-the-sidebar-openclose-state-in-preferences)<img class="icon" src="./img/premium.svg" />
* [`<SimpleForm>`](./SimpleForm.md)
* [`<SimpleFormIterator>`](./SimpleFormIterator.md)
* [`<SimpleFormWithRevision>`](./SimpleForm.md#versioning)<img class="icon" src="./img/premium.svg" />
* [`<SimpleList>`](./SimpleList.md)
* [`<SimpleShowLayout>`](./SimpleShowLayout.md)
* [`<SingleFieldList>`](./SingleFieldList.md)
* [`<SmartFieldDiff>`](https://marmelab.com/ra-enterprise/modules/ra-history#smartfielddiff)<img class="icon" src="./img/premium.svg" />
* [`<SmartRichTextInput>`](./SmartRichTextInput.md)<img class="icon" src="./img/premium.svg" />
* [`<SolarLayout>`](./SolarLayout.md)<img class="icon" src="./img/premium.svg" />
* [`<SolarMenu>`](./SolarLayout.md#solarmenu)<img class="icon" src="./img/premium.svg" />
Expand All @@ -177,6 +182,7 @@ title: "Index"
**- T -**
* `<Tab>`
* [`<TabbedForm>`](./TabbedForm.md)
* [`<TabbedFormWithRevision>`](./TabbedForm.md#versioning)<img class="icon" src="./img/premium.svg" />
* [`<TabbedShowLayout>`](./TabbedShowLayout.md)
* [`<TextField>`](./TextField.md)
* [`<TextInput>`](./TextInput.md)
Expand Down
206 changes: 206 additions & 0 deletions docs/RevisionsButton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
layout: default
title: "The RevisionsButton Component"
---

# `<RevisionsButton>`

This button 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.

<video controls autoplay playsinline muted loop>
<source src="https://marmelab.com/ra-enterprise/modules/assets/RevisionsButton.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>

`<RevisionsButton>` is an [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> component, part of [`ra-history`](https://marmelab.com/ra-enterprise/modules/ra-history).

## Usage

First, install the `@react-admin/ra-history` package:

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

Tip: `ra-history` is hosted in a private npm registry. You need to subscribe to one of the [Enterprise Edition](https://marmelab.com/ra-enterprise/) plans to access this package.

`<RevisionsButton>` is usually used in the page `actions` of an `<Edit>` component, in conjunction with [`<SimpleFormWithRevision>`](./SimpleForm.md#versioning).

```tsx
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.

```tsx
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>`](https://marmelab.com/ra-enterprise/modules/ra-history#fielddiff) displays the diff of a given field. It accepts a react-admin Field component as child.
- [`<SmartFieldDiff>`](https://marmelab.com/ra-enterprise/modules/ra-history#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:

```tsx
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.

```tsx
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:

```tsx
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>
);
```

## Showing the List of Revisions

By default, the `<RevisionsButton>` component only shows the list of revisions when the user clicks on the button. If you want to always show the list of revisions, you can use the [`<RevisionListWithDetailsInDialog>`](https://marmelab.com/ra-enterprise/modules/ra-history#revisionlistwithdetailsindialog) component instead.

<video controls autoplay playsinline muted loop>
<source src="https://marmelab.com/ra-enterprise/modules/assets/RevisionListWithDetailsInDialog.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>


This component is usually used in an `<Edit aside>`.

```tsx
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>
);
```

Check the [`<RevisionListWithDetailsInDialog>`](https://marmelab.com/ra-enterprise/modules/ra-history#revisionlistwithdetailsindialog) documentation for more details.
61 changes: 61 additions & 0 deletions docs/SimpleForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,67 @@ const ProductEdit = () => (

Check [the RBAC `<SimpleForm>` component](./AuthRBAC.md#simpleform) documentation for more details.

## Versioning

By default, `<SimpleForm>` updates the current record (via `dataProvider.update()`), so the previous version of the record is lost. If you want to keep the previous version, you can use the [`<SimpleFormWithRevision>`](https://marmelab.com/ra-enterprise/modules/ra-history#simpleformwithrevision) component instead:

```diff
// in src/posts/PostCreate.js
-import { Create, SimpleForm, TextInput, RichTextInput, NumberInput } from 'react-admin';
+import { Create, TextInput, RichTextInput, NumberInput } from 'react-admin';
+import { SimpleFormWithRevision } from "@react-admin/ra-history";

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

This won't change the look and feel of the form. But when the user submits the form, they will see a dialog asking them for the reason of the change.

![SimpleFormWithRevision](https://marmelab.com/ra-enterprise/modules/assets/ra-history/latest/SimpleFormWithRevision.png)

After submitting this dialog, react-admin will update the main record and **create a new revision**. A revision represents the state of the record at a given point in time. It is immutable. A revision also records the date, author, and reason of the change. Past revisions can be accessed via the [`<RevisionsButton>`](https://marmelab.com/ra-enterprise/modules/ra-history#revisionsbutton) component.

<video controls autoplay playsinline muted loop>
<source src="https://marmelab.com/ra-enterprise/modules/assets/RevisionsButton.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>

```jsx
// 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>
);
```

Check the [`<SimpleFormWithRevision>`](https://marmelab.com/ra-enterprise/modules/ra-history#simpleformwithrevision) and [`<RevisionsButton>`](https://marmelab.com/ra-enterprise/modules/ra-history#revisionsbutton) documentation for more details.

## Linking Two Inputs

Edition forms often contain linked inputs, e.g. country and city (the choices of the latter depending on the value of the former).
Expand Down
Loading

0 comments on commit e6edfe4

Please sign in to comment.