Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/LockOnMount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
layout: default
title: "LockOnMount"
---

# `<LockOnMount>`

`<LockOnMount>` is the component version of the [`useLockOnMount`](./useLockOnMount.md) hook. It locks the current record on mount and unlocks it on unmount. It relies on `authProvider.getIdentity()` to get the identity of the current user. It guesses the current `resource` and `recordId` from the context (or the route) if not provided.

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

## Usage

Use this hook e.g. in an `<Edit>` component to lock the record so that it only accepts updates from the current user.

```tsx
import { Edit, SimpleForm, TextInput } from 'react-admin';
import { LockOnMount } from '@react-admin/ra-realtime';

const PostEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="title" fullWidth />
<TextInput source="headline" fullWidth multiline />
<TextInput source="author" fullWidth />
<LockOnMount />
</SimpleForm>
</Edit>
);
```

**Note**: If users close their tab/browser when on a page with a locked record, `LockOnMount` will block the navigation and show a notification until the record is unlocked.

## Parameters

`<LockOnMount>` accepts the same props as the [`useLockOnMount`](./useLockOnMount.md) hook.

78 changes: 78 additions & 0 deletions docs/LockStatus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
layout: default
title: "LockStatus"
---

# `<LockStatus>`

`<LockStatus>` is an [Enterprise Edition](https://react-admin-ee.marmelab.com)<img class="icon" src="./img/premium.svg" alt="React Admin Enterprise Edition icon" /> component that displays the lock status of the current record. It allows to visually indicate whether the record is locked or not, by the current user or not, and provides an easy way to lock or unlock the record.

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

## Usage

Use `<LockStatus>` e.g. in a toolbar, to let the user know the lock status of the current record:

{% raw %}
```tsx
import { Toolbar, SaveButton } from 'react-admin';
import { LockStatus } from '@react-admin/ra-realtime';

const CustomToolbar = () => {
return (
<Toolbar>
<SaveButton sx={{ mr: 1 }} />
<LockStatus />
</Toolbar>
);
};
```
{% endraw %}

You can also use it in a DataTable to show the lock status of each record:

```tsx
import { List, DataTable } from 'react-admin';
import { LockStatus } from '@react-admin/ra-realtime';

const PostList = () => {
return (
<List>
<DataTable>
<DataTable.Col source="id" />
<DataTable.Col source="title" />
<DataTable.Col source="headline" />
<DataTable.Col source="author" />
<DataTable.Col label="Lock">
<LockStatus hideWhenUnlocked />
</DataTable.Col>
</DataTable>
</List>
);
};
```

**Tip:** You can use the `hideWhenUnlocked` prop to hide the lock status when the record is not locked. This is useful to avoid showing too many lock icons in the DataTable when most records are not locked.

## Props

| Name | Required | Type | Default Value | Description |
| ----------------------- | -------- | ------------ | --------------------------------- | --------------------------------------------------------------------------------------------- |
| `hideWhenUnlocked` | No | `boolean` | - | Set to true to hide the lock status when the record is not locked. |
| `identity` | No | `Identifier` | From `AuthProvider.getIdentity()` | An identifier for the user who owns the lock. |
| `resource` | No | `string` | From `ResourceContext` | The resource name (e.g. `'posts'`). |
| `id` | No | `Identifier` | From `RecordContext` | The record id (e.g. `123`). |
| `meta` | No | `object` | - | Additional metadata forwarded to the dataProvider `lock()`, `unlock()` and `getLock()` calls. |
| `lockMutationOptions` | No | `object` | - | `react-query` mutation options, used to customize the lock side-effects. |
| `unlockMutationOptions` | No | `object` | - | `react-query` mutation options, used to customize the unlock side-effects. |
| `queryOptions` | No | `object` | - | `react-query` query options, used to customize the lock query side-effects. |

## Customizing the Tooltip Messages

You can customize the tooltip messages displayed by `<LockStatus>` by overriding the following i18n keys in your translations:
- `ra-realtime.locks.status.locked_by_you`: The tooltip message when the record is locked by the current user.
- `ra-realtime.locks.status.locked_by_another_user`: The tooltip message when the record is locked by another user.
- `ra-realtime.locks.status.unlocked`: The tooltip message when the record is unlocked.
3 changes: 3 additions & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,14 @@
<li {% if page.path == 'useGetLockLive.md' %} class="active" {% endif %}><a class="nav-link" href="./useGetLockLive.html"><code>useGetLockLive</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'useGetLocks.md' %} class="active" {% endif %}><a class="nav-link" href="./useGetLocks.html"><code>useGetLocks</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'useGetLocksLive.md' %} class="active" {% endif %}><a class="nav-link" href="./useGetLocksLive.html"><code>useGetLocksLive</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'useLockCallbacks.md' %} class="active" {% endif %}><a class="nav-link" href="./useLockCallbacks.html"><code>useLockCallbacks</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'useLockOnMount.md' %} class="active" {% endif %}><a class="nav-link" href="./useLockOnMount.html"><code>useLockOnMount</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'useLockOnCall.md' %} class="active" {% endif %}><a class="nav-link" href="./useLockOnCall.html"><code>useLockOnCall</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'useGetListLive.md' %} class="active" {% endif %}><a class="nav-link" href="./useGetListLive.html"><code>useGetListLive</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'useGetOneLive.md' %} class="active" {% endif %}><a class="nav-link" href="./useGetOneLive.html"><code>useGetOneLive</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'ListLiveUpdate.md' %} class="active" {% endif %}><a class="nav-link" href="./ListLiveUpdate.html"><code>&lt;ListLiveUpdate&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'LockOnMount.md' %} class="active" {% endif %}><a class="nav-link" href="./LockOnMount.html"><code>&lt;LockOnMount&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'LockStatus.md' %} class="active" {% endif %}><a class="nav-link" href="./LockStatus.html"><code>&lt;LockStatus&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'EditLive.md' %} class="active" {% endif %}><a class="nav-link" href="./EditLive.html"><code>&lt;EditLive&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'ShowLive.md' %} class="active" {% endif %}><a class="nav-link" href="./ShowLive.html"><code>&lt;ShowLive&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'MenuLive.md' %} class="active" {% endif %}><a class="nav-link" href="./MenuLive.html"><code>&lt;MenuLive&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
Expand Down
140 changes: 140 additions & 0 deletions docs/useLockCallbacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
layout: default
title: "useLockCallbacks"
---

# `useLockCallbacks`

This [Enterprise Edition](https://react-admin-ee.marmelab.com)<img class="icon" src="./img/premium.svg" alt="React Admin Enterprise Edition icon" /> hook returns callbacks to **lock** and **unlock** a record, as well as the current **lock status**.

## Usage

Use this hook e.g. to build a lock button:

{% raw %}
```tsx
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import { CircularProgress, IconButton, Tooltip } from '@mui/material';
import { useLockCallbacks } from '@react-admin/ra-realtime';

export const LockButton = () => {
const {
lock,
isLocked,
isLockedByCurrentUser,
isPending,
isLocking,
isUnlocking,
doLock,
doUnlock,
} = useLockCallbacks();
if (isPending) {
return null;
}
return isLocked ? (
isLockedByCurrentUser ? (
<Tooltip title="Locked by you, click to unlock">
<IconButton
disabled={isUnlocking}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
doUnlock();
}}
>
{isUnlocking ? (
<CircularProgress size={24} />
) : (
<LockIcon />
)}
</IconButton>
</Tooltip>
) : (
<Tooltip title={`Locked by another user: ${lock?.identity}`}>
<LockIcon color="error" sx={{ mx: 1 }} />
</Tooltip>
)
) : (
<Tooltip title="Record is unlocked, click to lock">
<IconButton
disabled={isLocking}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
doLock();
}}
color="warning"
>
{isLocking ? <CircularProgress size={24} /> : <LockOpenIcon />}
</IconButton>
</Tooltip>
);
};
```
{% endraw %}

You can also leverage this hook as a quick way to access the lock status of the current record:

```tsx
import { useLockCallbacks } from '@react-admin/ra-realtime';
import { SaveButton, Toolbar } from 'react-admin';

export const MyToolbar = () => {
const { isLockedByCurrentUser } = useLockCallbacks();
return (
<Toolbar>
<SaveButton disabled={!isLockedByCurrentUser} />
</Toolbar>
);
};
```

## Parameters

`useLockCallbacks` accepts a single options parameter, with the following properties:

| Name | Required | Type | Default Value | Description |
| ----------------------- | -------- | ------------ | --------------------------------- | --------------------------------------------------------------------------------------------- |
| `identity` | No | `Identifier` | From `AuthProvider.getIdentity()` | An identifier for the user who owns the lock. |
| `resource` | No | `string` | From `ResourceContext` | The resource name (e.g. `'posts'`). |
| `id` | No | `Identifier` | From `RecordContext` | The record id (e.g. `123`). |
| `meta` | No | `object` | - | Additional metadata forwarded to the dataProvider `lock()`, `unlock()` and `getLock()` calls. |
| `lockMutationOptions` | No | `object` | - | `react-query` mutation options, used to customize the lock side-effects. |
| `unlockMutationOptions` | No | `object` | - | `react-query` mutation options, used to customize the unlock side-effects. |
| `queryOptions` | No | `object` | - | `react-query` query options, used to customize the lock query side-effects. |

You can call `useLockCallbacks` with no parameter, and it will guess the resource and record id from the context (or the route):

```tsx
const { isLocked, error, isLocking } = useLockCallbacks();
```

Or you can provide them explicitly:

```tsx
const { isLocked, error, isLocking } = useLockCallbacks({
resource: 'venues',
id: 123,
identity: 'John Doe',
});
```

## Return value

`useLockCallbacks` returns an object with the following properties:

| Name | Type | Description |
| ----------------------- | ---------- | ------------------------------------------------------------------------- |
| `isLocked` | `boolean` | Whether the record is currently locked (possibly by another user) or not. |
| `isLockedByCurrentUser` | `boolean` | Whether the record is locked by the current user or not. |
| `lock` | `object` | The lock data. |
| `error` | `object` | The error object if any of the mutations or the query fails. |
| `isPending` | `boolean` | Whether the lock query is in progress. |
| `isLocking` | `boolean` | Whether the lock mutation is in progress. |
| `isUnlocking` | `boolean` | Whether the unlock mutation is in progress. |
| `doLock` | `function` | A callback to manually lock the record. |
| `doUnlock` | `function` | A callback to manually unlock the record. |
| `doLockAsync` | `function` | A callback to manually lock the record asynchronously. |
| `doUnlockAsync` | `function` | A callback to manually unlock the record asynchronously. |
| `lockQuery` | `object` | The `react-query` query object for the lock status. |
| `lockMutation` | `object` | The `react-query` mutation object for the lock mutation. |
| `unlockMutation` | `object` | The `react-query` mutation object for the unlock mutation. |
6 changes: 4 additions & 2 deletions docs/useLockOnCall.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const PostAside = () => {
<AlertTitle>Post locked</AlertTitle> Only you can edit it.
</Alert>
) : (
<Button onClick={() => doLock()} fullWidth>
<Button onClick={() => { doLock(); }} fullWidth>
Lock post
</Button>
)}
Expand All @@ -61,6 +61,8 @@ const PostEdit = () => (
```
{% endraw %}

**Note**: If users close their tab/browser when on a page with a locked record, `useLockOnCall` will block the navigation and show a notification until the record is unlocked. Hence it's a good practice to give them a way to unlock the record manually, e.g. by using the `doUnlock` callback returned by the [`useLockCallbacks`](./useLockCallbacks.md) hook or the [`<LockStatus>`](./LockStatus.md) component.

## Parameters

`useLockOnCall` accepts a single options parameter, with the following properties (all optional):
Expand All @@ -76,7 +78,7 @@ const PostEdit = () => (
const LockButton = ({ resource, id, identity }) => {
const [doLock, lockMutation] = useLockOnCall({ resource, id, identity });
return (
<button onClick={() => doLock()} disabled={lockMutation.isLoading}>
<button onClick={() => { doLock(); }} disabled={lockMutation.isLoading}>
Lock
</button>
);
Expand Down
17 changes: 16 additions & 1 deletion docs/useLockOnMount.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const PostEdit = () => (
```
{% endraw %}

**Note**: If users close their tab/browser when on a page with a locked record, `useLockOnMount` will block the navigation until the record is unlocked and show a notification.
**Note**: If users close their tab/browser when on a page with a locked record, `useLockOnMount` will block the navigation and show a notification until the record is unlocked. Hence it's a good practice to give them a way to unlock the record manually, e.g. by using the `doUnlock` callback returned by the hook or the [`<LockStatus>`](./LockStatus.md) component.

## Parameters

Expand Down Expand Up @@ -98,3 +98,18 @@ const { isLocked, error, isLoading } = useLockOnMount({
},
});
```

## Return value

`useLockOnMount` returns an object with the following properties:

- `isLocked`: Whether the record is successfully locked by this hook or not.
- `isLockedByCurrentUser`: Whether the record is locked by the current user or not.
- `lock`: The lock data.
- `error`: The error object if the lock attempt failed.
- `isLocking`: Whether the lock mutation is in progress.
- `isUnlocking`: Whether the unlock mutation is in progress.
- `doLock`: A callback to manually lock the record.
- `doUnlock`: A callback to manually unlock the record.
- `doLockAsync`: A callback to manually lock the record asynchronously.
- `doUnlockAsync`: A callback to manually unlock the record asynchronously.
Loading