From 8c7c847c0f16e22879684b7b0d6bef530c21c014 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:25:07 +0200 Subject: [PATCH 1/6] Add documentation for headless enterprise features in ra-core documentation --- docs_headless/astro.config.mjs | 40 ++++ docs_headless/public/premium-dark.svg | 1 + docs_headless/public/premium-light.svg | 1 + .../src/content/docs/LockStatusBase.md | 84 +++++++ .../content/docs/canAccessWithPermissions.md | 77 +++++++ .../content/docs/getPermissionsFromRoles.md | 110 +++++++++ .../src/content/docs/useGetListLive.md | 36 +++ docs_headless/src/content/docs/useGetLock.md | 48 ++++ .../src/content/docs/useGetLockLive.md | 27 +++ docs_headless/src/content/docs/useGetLocks.md | 66 ++++++ .../src/content/docs/useGetLocksLive.md | 29 +++ .../src/content/docs/useGetOneLive.md | 30 +++ docs_headless/src/content/docs/useLock.md | 25 ++ .../src/content/docs/useLockCallbacks.md | 132 +++++++++++ .../src/content/docs/useLockOnCall.md | 80 +++++++ .../src/content/docs/useLockOnMount.md | 107 +++++++++ docs_headless/src/content/docs/usePublish.md | 104 +++++++++ .../src/content/docs/useSubscribe.md | 155 +++++++++++++ .../src/content/docs/useSubscribeCallback.md | 185 +++++++++++++++ .../src/content/docs/useSubscribeToRecord.md | 215 ++++++++++++++++++ .../content/docs/useSubscribeToRecordList.md | 159 +++++++++++++ docs_headless/src/content/docs/useUnlock.md | 23 ++ docs_headless/src/styles/global.css | 58 ++++- 23 files changed, 1790 insertions(+), 2 deletions(-) create mode 100644 docs_headless/public/premium-dark.svg create mode 100644 docs_headless/public/premium-light.svg create mode 100644 docs_headless/src/content/docs/LockStatusBase.md create mode 100644 docs_headless/src/content/docs/canAccessWithPermissions.md create mode 100644 docs_headless/src/content/docs/getPermissionsFromRoles.md create mode 100644 docs_headless/src/content/docs/useGetListLive.md create mode 100644 docs_headless/src/content/docs/useGetLock.md create mode 100644 docs_headless/src/content/docs/useGetLockLive.md create mode 100644 docs_headless/src/content/docs/useGetLocks.md create mode 100644 docs_headless/src/content/docs/useGetLocksLive.md create mode 100644 docs_headless/src/content/docs/useGetOneLive.md create mode 100644 docs_headless/src/content/docs/useLock.md create mode 100644 docs_headless/src/content/docs/useLockCallbacks.md create mode 100644 docs_headless/src/content/docs/useLockOnCall.md create mode 100644 docs_headless/src/content/docs/useLockOnMount.md create mode 100644 docs_headless/src/content/docs/usePublish.md create mode 100644 docs_headless/src/content/docs/useSubscribe.md create mode 100644 docs_headless/src/content/docs/useSubscribeCallback.md create mode 100644 docs_headless/src/content/docs/useSubscribeToRecord.md create mode 100644 docs_headless/src/content/docs/useSubscribeToRecordList.md create mode 100644 docs_headless/src/content/docs/useUnlock.md diff --git a/docs_headless/astro.config.mjs b/docs_headless/astro.config.mjs index 303a69a391f..519f2e4fd3c 100644 --- a/docs_headless/astro.config.mjs +++ b/docs_headless/astro.config.mjs @@ -106,6 +106,8 @@ export default defineConfig({ 'usepermissions', 'addrefreshauthtoauthprovider', 'addrefreshauthtodataprovider', + enterpriseEntry('canAccessWithPermissions'), + enterpriseEntry('getPermissionsFromRoles'), ], }, { @@ -203,6 +205,28 @@ export default defineConfig({ 'usegetrecordrepresentation', ], }, + { + label: 'Realtime', + items: [ + enterpriseEntry('usePublish'), + enterpriseEntry('useSubscribe'), + enterpriseEntry('useSubscribeCallback'), + enterpriseEntry('useSubscribeToRecord'), + enterpriseEntry('useSubscribeToRecordList'), + enterpriseEntry('useLock'), + enterpriseEntry('useUnlock'), + enterpriseEntry('useGetLock'), + enterpriseEntry('useGetLockLive'), + enterpriseEntry('useGetLocks'), + enterpriseEntry('useGetLocksLive'), + enterpriseEntry('useLockCallbacks'), + enterpriseEntry('useLockOnMount'), + enterpriseEntry('useLockOnCall'), + enterpriseEntry('useGetListLive'), + enterpriseEntry('useGetOneLive'), + enterpriseEntry(''), + ], + }, { label: 'Recipes', items: ['caching', 'unittesting'], @@ -240,3 +264,19 @@ export default defineConfig({ assets: 'assets', }, }); + +/** + * @param {string} name + * @returns {any} + */ +function enterpriseEntry(name) { + return { + link: name.toLowerCase().replace(//g, ''), + label: name, + attrs: { class: 'enterprise' }, + badge: { + text: 'React Admin Enterprise', + variant: 'default', + }, + }; +} diff --git a/docs_headless/public/premium-dark.svg b/docs_headless/public/premium-dark.svg new file mode 100644 index 00000000000..44c86b7b6d6 --- /dev/null +++ b/docs_headless/public/premium-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs_headless/public/premium-light.svg b/docs_headless/public/premium-light.svg new file mode 100644 index 00000000000..f3a80fe9b52 --- /dev/null +++ b/docs_headless/public/premium-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs_headless/src/content/docs/LockStatusBase.md b/docs_headless/src/content/docs/LockStatusBase.md new file mode 100644 index 00000000000..4955553fa1c --- /dev/null +++ b/docs_headless/src/content/docs/LockStatusBase.md @@ -0,0 +1,84 @@ +--- +title: "" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Use the `` component to display the lock status of the record in the nearest `RecordContext`: + +```tsx +import React from 'react'; +import { Lock, LockOpen, LoaderCircle } from 'lucide-react'; +import { LockStatusBase } from '@react-admin/ra-core-ee'; + +export const LockStatus = () => { + return ( + { + if (isPending) { + return null; + } + + if (lockStatus === 'lockedByUser') { + return ( + + ); + } + if (lockStatus === 'lockedByAnotherUser') { + return ( + + ); + } + if (lockStatus === 'unlocked') { + return ( + + ); + } + return null; + }} + /> + ); +}; +``` + +In addition to the [`useLockCallbacks`](#uselockcallbacks) parameters, `` accepts a `render` prop. The function passed to the `render` prop will be called with the result of the `useLockCallbacks` hook. \ No newline at end of file diff --git a/docs_headless/src/content/docs/canAccessWithPermissions.md b/docs_headless/src/content/docs/canAccessWithPermissions.md new file mode 100644 index 00000000000..1a6422cb56b --- /dev/null +++ b/docs_headless/src/content/docs/canAccessWithPermissions.md @@ -0,0 +1,77 @@ +--- +title: "canAccessWithPermissions" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +`canAccessWithPermissions` is a helper function that facilitates the `authProvider.canAccess()` method implementation: + +## Usage + +The user roles and permissions should be returned upon login. The `authProvider` should store the permissions in memory, or in localStorage. This allows `authProvider.canAccess()` to read the permissions from localStorage. + +```tsx +// in roleDefinitions.ts +export const roleDefinitions = { + admin: [ + { action: '*', resource: '*' } + ], + reader: [ + { action: ['list', 'show', 'export'], resource: '*' } + { action: 'read', resource: 'posts.*' } + { action: 'read', resource: 'comments.*' } + ], + accounting: [ + { action: '*', resource: 'sales' }, + ], +}; + +// in authProvider.ts +import { canAccessWithPermissions, getPermissionsFromRoles } from '@react-admin/ra-core-ee'; +import { roleDefinitions } from './roleDefinitions'; + +const authProvider = { + login: async ({ username, password }) => { + const request = new Request('https://mydomain.com/authenticate', { + method: 'POST', + body: JSON.stringify({ username, password }), + headers: new Headers({ 'Content-Type': 'application/json' }), + }); + const response = await fetch(request); + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + const { user: { roles, permissions }} = await response.json(); + // merge the permissions from the roles with the extra permissions + const permissions = getPermissionsFromRoles({ + roleDefinitions, + userPermissions, + userRoles + }); + localStorage.setItem('permissions', JSON.stringify(permissions)); + }, + canAccess: async ({ action, resource, record }) => { + const permissions = JSON.parse(localStorage.getItem('permissions')); + return canAccessWithPermissions({ + permissions, + action, + resource, + record, + }); + } + // ... +}; +``` + +## Parameters + +This function takes an object as argument with the following fields: + +| Name | Optional | Type | Description +| - | - | - | - | +| `permissions` | Required | `Array` | An array of permissions for the current user +| `action` | Required | `string` | The action for which to check users has the execution right +| `resource` | Required | `string` | The resource for which to check users has the execution right +| `record` | Required | `string` | The record for which to check users has the execution right + +`canAccessWithPermissions` expects the `permissions` to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the [`getPermissionsFromRoles`](#getpermissionsfromroles) function. \ No newline at end of file diff --git a/docs_headless/src/content/docs/getPermissionsFromRoles.md b/docs_headless/src/content/docs/getPermissionsFromRoles.md new file mode 100644 index 00000000000..ab92a585c1d --- /dev/null +++ b/docs_headless/src/content/docs/getPermissionsFromRoles.md @@ -0,0 +1,110 @@ +--- +title: "getPermissionsFromRoles" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +This function returns an array of user permissions based on a role definition, a list of roles, and a list of user permissions. It merges the permissions defined in `roleDefinitions` for the current user's roles (`userRoles`) with the extra `userPermissions`. + +```ts +import { getPermissionsFromRoles } from '@react-admin/ra-core-ee'; + +// static role definitions (usually in the app code) +const roleDefinitions = { + admin: [ + { action: '*', resource: '*' } + ], + reader: [ + { action: ['list', 'show', 'export'], resource: '*' } + { action: 'read', resource: 'posts.*' } + { action: 'read', resource: 'comments.*' } + ], + accounting: [ + { action: '*', resource: 'sales' }, + ], +}; + +const permissions = getPermissionsFromRoles({ + roleDefinitions, + // roles of the current user (usually returned by the server upon login) + userRoles: ['reader'], + // extra permissions for the current user (usually returned by the server upon login) + userPermissions: [ + { action: 'list', resource: 'sales'}, + ], +}); +// permissions = [ +// { action: ['list', 'show', 'export'], resource: '*' }, +// { action: 'read', resource: 'posts.*' }, +// { action: 'read', resource: 'comments.*' }, +// { action: 'list', resource: 'sales' }, +// ]; +``` + +## Usage + +The user roles and permissions should be returned upon login. The `authProvider` should store the permissions in memory, or in localStorage. This allows `authProvider.canAccess()` to read the permissions from localStorage. + +```tsx +// in roleDefinitions.ts +export const roleDefinitions = { + admin: [ + { action: '*', resource: '*' } + ], + reader: [ + { action: ['list', 'show', 'export'], resource: '*' } + { action: 'read', resource: 'posts.*' } + { action: 'read', resource: 'comments.*' } + ], + accounting: [ + { action: '*', resource: 'sales' }, + ], +}; + +// in authProvider.ts +import { canAccessWithPermissions, getPermissionsFromRoles } from '@react-admin/ra-core-ee'; +import { roleDefinitions } from './roleDefinitions'; + +const authProvider = { + login: async ({ username, password }) => { + const request = new Request('https://mydomain.com/authenticate', { + method: 'POST', + body: JSON.stringify({ username, password }), + headers: new Headers({ 'Content-Type': 'application/json' }), + }); + const response = await fetch(request); + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + const { user: { roles, permissions }} = await response.json(); + // merge the permissions from the roles with the extra permissions + const permissions = getPermissionsFromRoles({ + roleDefinitions, + userPermissions, + userRoles + }); + localStorage.setItem('permissions', JSON.stringify(permissions)); + }, + canAccess: async ({ action, resource, record }) => { + const permissions = JSON.parse(localStorage.getItem('permissions')); + return canAccessWithPermissions({ + permissions, + action, + resource, + record, + }); + } + // ... +}; +``` + +## Parameters + +This function takes an object as argument with the following fields: + +| Name | Optional | Type | Description +| - | - | - | - | +| `roleDefinitions` | Required | `Record` | A dictionary containing the role definition for each role +| `userRoles` | Optional | `Array` | An array of roles (admin, reader...) for the current user +| `userPermissions` | Optional | `Array` | An array of permissions for the current user + diff --git a/docs_headless/src/content/docs/useGetListLive.md b/docs_headless/src/content/docs/useGetListLive.md new file mode 100644 index 00000000000..f185bffa594 --- /dev/null +++ b/docs_headless/src/content/docs/useGetListLive.md @@ -0,0 +1,36 @@ +--- +title: "useGetListLive" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Alternative to `useGetList` that subscribes to live updates on the record list. + +```tsx +import { useGetListLive } from '@react-admin/ra-core-ee'; + +const LatestNews = () => { + const { data, total, isLoading, error } = useGetListLive('posts', { + pagination: { page: 1, perPage: 10 }, + sort: { field: 'published_at', order: 'DESC' }, + }); + if (isLoading) { + return
Loading...
; + } + if (error) { + return

ERROR

; + } + + return ( +
    + {data.map(item => ( +
  • {item.title}
  • + ))} +
+ ); +}; +``` + +The hook will subscribe to live updates on the list of records (topic: `resource/[resource]`) and will refetch the list when a new record is created, or an existing record is updated or deleted. + +See the [useGetList](https://marmelab.com/react-admin/useGetList.html) documentation for the full list of parameters and return type. \ No newline at end of file diff --git a/docs_headless/src/content/docs/useGetLock.md b/docs_headless/src/content/docs/useGetLock.md new file mode 100644 index 00000000000..9a7c9c83206 --- /dev/null +++ b/docs_headless/src/content/docs/useGetLock.md @@ -0,0 +1,48 @@ +--- +title: "useGetLock" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Gets the lock status for a record. It calls `dataProvider.getLock()` on mount. + +```tsx +const { data, isLoading } = useGetLock(resource, { id }); +``` + +Parameters description: + +- `resource`: the resource name (e.g. `'posts'`) +- `params`: an object with the following properties: + - `id`: the record id (e.g. `123`) + - `meta`: Optional. an object that will be forwarded to the dataProvider (optional) + +Here is a form toolbar that displays the lock status of the current record: + +```tsx +const FormToolbar = () => { + const resource = useResourceContext(); + const record = useRecordContext(); + const { isLoading: identityLoading, identity } = useGetIdentity(); + const { isLoading: lockLoading, data: lock } = useGetLock(resource, { + id: record.id, + }); + + if (identityLoading || lockLoading) { + return null; + } + + const isLockedByOtherUser = lock?.identity !== identity.id; + + return ( +
+ + {isLockedByOtherUser && ( + + {`This record is locked by another user: ${lock?.dentity}.`} + + )} +
+ ); +}; +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useGetLockLive.md b/docs_headless/src/content/docs/useGetLockLive.md new file mode 100644 index 00000000000..cd44605d809 --- /dev/null +++ b/docs_headless/src/content/docs/useGetLockLive.md @@ -0,0 +1,27 @@ +--- +title: "useGetLockLive" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Use the `useGetLockLive()` hook to get the lock status in real time. This hook calls `dataProvider.getLock()` for the current record on mount, and subscribes to live updates on the `lock/[resource]/[id]` topic. + +This means that if the lock is acquired or released by another user while the current user is on the page, the return value will be updated. + +```tsx +import { useGetLockLive } from '@react-admin/ra-core-ee'; + +const LockStatus = () => { + const { data: lock } = useGetLockLive(); + const { identity } = useGetIdentity(); + if (!lock) return No lock; + if (lock.identity === identity?.id) return Locked by you; + return Locked by {lock.identity}; +}; +``` + +`useGetLockLive` reads the current resource and record id from the `ResourceContext` and `RecordContext`. You can provide them explicitly if you are not in such a context: + +```tsx +const { data: lock } = useGetLockLive('posts', { id: 123 }); +``` diff --git a/docs_headless/src/content/docs/useGetLocks.md b/docs_headless/src/content/docs/useGetLocks.md new file mode 100644 index 00000000000..109641a7075 --- /dev/null +++ b/docs_headless/src/content/docs/useGetLocks.md @@ -0,0 +1,66 @@ +--- +title: "useGetLocks" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Get all the locks for a given resource. Calls `dataProvider.getLocks()` on mount. + +```tsx +// simple Usage +const { data } = useGetLocks('posts'); +``` + +Here is how to use it in a custom list, to disable edit and delete buttons for locked records: + +```tsx +import { WithListContext, useRecordContext } from 'ra-core'; +import { useGetLocks, type Lock } from '@react-admin/ra-core-ee'; +import { DeleteButton } from '@components/ui/DeleteButton'; +import { LockableEditButton } from '@components/ui/DeleteButton'; + +const MyPostGrid = () => { + const resource = useResourceContext(); + const { data: locks } = useGetLocks(resource); + return ( +
    + isPending ? null : ( +
  • + + +
  • + )} + /> +
+ ); +}; + +const MyPostTitle = ({ locks }: { locks: Lock[] }) => { + const record = useRecordContext(); + const lock = locks.find(l => l.recordId === record.id); + + return ( +
+ {record.title}} />} /> + {lock && ( + + {` (Locked by ${lock.identity})`} + + )} +
+ ); +}; + +const MyPostActions = ({ locks }: { locks: Lock[] }) => { + const record = useRecordContext(); + const locked = locks.find(l => l.recordId === record.id); + + return ( +
+ + +
+ ); +}; +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useGetLocksLive.md b/docs_headless/src/content/docs/useGetLocksLive.md new file mode 100644 index 00000000000..60c46e07968 --- /dev/null +++ b/docs_headless/src/content/docs/useGetLocksLive.md @@ -0,0 +1,29 @@ +--- +title: "useGetLocksLive" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Use the `useGetLocksLive` hook to get the locks in real time. This hook calls `dataProvider.getLocks()` for the current resource on mount, and subscribes to live updates on the `lock/[resource]` topic. + +This means that if a lock is acquired or released by another user while the current user is on the page, the return value will be updated. + +```tsx +import { useRecordContext } from 'ra-core'; +import { useGetLocksLive } from '@react-admin/ra-core-ee'; +import { Lock } from 'lucide-react'; + +export const LockField = ({ locks }) => { + const record = useRecordContext(); + if (!record) return null; + const lock = locks?.find(lock => lock.recordId === record?.id); + if (!lock) return ; + return ; +}; +``` + +`useGetLocksLive` reads the current resource from the `ResourceContext`. You can provide it explicitly if you are not in such a context: + +```tsx +const { data: locks } = useGetLocksLive('posts'); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useGetOneLive.md b/docs_headless/src/content/docs/useGetOneLive.md new file mode 100644 index 00000000000..feb2cc6f815 --- /dev/null +++ b/docs_headless/src/content/docs/useGetOneLive.md @@ -0,0 +1,30 @@ +--- +title: "useGetOneLive" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Alternative to `useGetOne()` that subscribes to live updates on the record + +```tsx +import { useRecordContext } from 'ra-core'; +import { useGetOneLive } from '@react-admin/ra-core-ee'; + +const UserProfile = () => { + const record = useRecordContext(); + const { data, isLoading, error } = useGetOneLive('users', { + id: record.id, + }); + if (isLoading) { + return
Loading...
; + } + if (error) { + return

ERROR

; + } + return
User {data.username}
; +}; +``` + +The hook will subscribe to live updates on the record (topic: `resource/[resource]/[id]`) and will refetch the record when it is updated or deleted. + +See the [useGetOne](https://marmelab.com/react-admin/useGetOne.html) documentation for the full list of parameters and return type. \ No newline at end of file diff --git a/docs_headless/src/content/docs/useLock.md b/docs_headless/src/content/docs/useLock.md new file mode 100644 index 00000000000..05b3219d3b2 --- /dev/null +++ b/docs_headless/src/content/docs/useLock.md @@ -0,0 +1,25 @@ +--- +title: "useLock" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +`useLock` is a low-level hook that returns a callback to call `dataProvider.lock()`, leveraging react-query's `useMutation`. + +```tsx +const [lock, { isLoading, error }] = useLock( + resource, + { id, identity, meta }, + options +); +``` + +The payload is an object with the following properties: + +- `id`: the record id (e.g. `123`) +- `identity`: an identifier (string or number) corresponding to the identity of the locker (e.g. `'julien'`). This usually comes from `authProvider.getIdentity()`. +- `meta`: an object that will be forwarded to the dataProvider (optional) + +The optional `options` argument is passed to react-query's `useMutation` hook. + +For most use cases, you won't need to call the `useLock` hook directly. Instead, you should use the `useLockOnMount` or `useLockOnCall` orchestration hooks, which are responsible for calling `useLock` and `useUnlock`. \ No newline at end of file diff --git a/docs_headless/src/content/docs/useLockCallbacks.md b/docs_headless/src/content/docs/useLockCallbacks.md new file mode 100644 index 00000000000..a7e2b0e49b0 --- /dev/null +++ b/docs_headless/src/content/docs/useLockCallbacks.md @@ -0,0 +1,132 @@ +--- +title: "useLockCallbacks" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +This utility hook allows to easily get the 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: + +```tsx +import { useLockCallbacks } from '@react-admin/ra-core-ee'; +import { LoaderCircle, Lock } from 'lucide-react'; + +export const LockButton = () => { + const { + lock, + isLocked, + isLockedByCurrentUser, + isPending, + isLocking, + isUnlocking, + doLock, + doUnlock, + } = useLockCallbacks(); + + if (isPending) { + return null; + } + + return isLocked ? ( + isLockedByCurrentUser ? ( + + ) : ( + + ) + ) : ( + + ); +}; +``` + +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-core-ee'; + +export const MyToolbar = () => { + const { isLockedByCurrentUser } = useLockCallbacks(); + + return ( +
+ +
+ ); +}; +``` + +## 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. | \ No newline at end of file diff --git a/docs_headless/src/content/docs/useLockOnCall.md b/docs_headless/src/content/docs/useLockOnCall.md new file mode 100644 index 00000000000..f6df5877c9b --- /dev/null +++ b/docs_headless/src/content/docs/useLockOnCall.md @@ -0,0 +1,80 @@ +--- +title: "useLockOnCall" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Get a callback to lock a record and get a mutation state. + +`useLockOnCall` calls `dataProvider.lock()` when the callback is called. 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. It releases the lock when the component unmounts by calling `dataProvider.unlock()`. + + + +## Usage + +Use this hook in a toolbar, to let the user lock the record manually. + +```tsx +import { EditBase } from 'ra-core'; +import { useLockOnMount } from '@react-admin/ra-core-ee'; +import { Alert, AlertTitle, Box, Button } from '@material-ui/core'; + +const LockStatus = () => { + const [doLock, { data, error, isLoading }] = useLockOnCall(); + return ( +
+ {isLoading ? ( +
Locking post...
+ ) : error ? ( +
+
Failed to lock
+
Someone else is probably already locking it.
+
+ ) : data ? ( +
+
Post locked
+
Only you can edit it.
+
+ ) : ( + + )} +
+ ); +}; + +const PostEdit = () => ( + + + {/* The edit form*/} + +); +``` + +**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) hook or the [``](#lockstatusbase) component. + +## Parameters + +`useLockOnCall` accepts a single options parameter, with the following properties (all optional): + +- `identity`: An identifier (string or number) corresponding to the identity of the locker (e.g. `'julien'`). This could be an authentication token for instance. Falls back to the identifier of the identity returned by the `AuthProvider.getIdentity()` function. +- `resource`: The resource name (e.g. `'posts'`). The hook uses the `ResourceContext` if not provided. +- `id`: The record id (e.g. `123`). The hook uses the `RecordContext` if not provided. +- `meta`: An object that will be forwarded to the `dataProvider.lock()` call +- `lockMutationOptions`: `react-query` mutation options, used to customize the lock side-effects for instance +- `unlockMutationOptions`: `react-query` mutation options, used to customize the unlock side-effects for instance + +```tsx +const LockButton = ({ resource, id, identity }) => { + const [doLock, lockMutation] = useLockOnCall({ resource, id, identity }); + return ( + + ); +}; +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useLockOnMount.md b/docs_headless/src/content/docs/useLockOnMount.md new file mode 100644 index 00000000000..d54c3dc4136 --- /dev/null +++ b/docs_headless/src/content/docs/useLockOnMount.md @@ -0,0 +1,107 @@ +--- +title: "useLockOnMount" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +This hook locks the current record on mount. + +`useLockOnMount` calls `dataProvider.lock()` on mount and `dataProvider.unlock()` on unmount to lock and unlock the record. 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. + + + +## Usage + +Use this hook e.g. in an `` component to lock the record so that it only accepts updates from the current user. + +```tsx +import { EditBase, Form } from 'ra-core'; +import { useLockOnMount } from '@react-admin/ra-core-ee'; + +const LockStatus = () => { + const { isLocked, error, isLoading } = useLockOnMount(); + return ( +
+ {isLoading &&

Locking post...

} + {error && ( +

+

Failed to lock
+
Someone else is probably already locking it.
+

+ )} + {isLocked && ( +

+

Post locked
+
Only you can edit it.
+

+ )} +
+ ); +}; + +const PostEdit = () => ( + + + {/* The edit form*/} + +); +``` + +**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 [``](#lockstatusbase) component. + +## Parameters + +`useLockOnMount` accepts a single options parameter, with the following properties (all optional): + +- `identity`: An identifier (string or number) corresponding to the identity of the locker (e.g. `'julien'`). This could be an authentication token for instance. Falls back to the identifier of the identity returned by the `AuthProvider.getIdentity()` function. +- `resource`: The resource name (e.g. `'posts'`). The hook uses the `ResourceContext` if not provided. +- `id`: The record id (e.g. `123`). The hook uses the `RecordContext` if not provided. +- `meta`: An object that will be forwarded to the `dataProvider.lock()` call +- `lockMutationOptions`: `react-query` mutation options, used to customize the lock side-effects for instance +- `unlockMutationOptions`: `react-query` mutation options, used to customize the unlock side-effects for instance + +You can call `useLockOnMount` with no parameter, and it will guess the resource and record id from the context (or the route): + +```tsx +const { isLocked, error, isLoading } = useLockOnMount(); +``` + +Or you can provide them explicitly: + +```tsx +const { isLocked, error, isLoading } = useLockOnMount({ + resource: 'venues', + id: 123, + identity: 'John Doe', +}); +``` + +**Tip**: If the record can't be locked because another user is already locking it, you can use [`react-query`'s retry feature](https://react-query-v3.tanstack.com/guides/mutations#retry) to try again later: + +```tsx +const { isLocked, error, isLoading } = useLockOnMount({ + lockMutationOptions: { + // retry every 5 seconds, until the lock is acquired + retry: true, + retryDelay: 5000, + }, +}); +``` + +## 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. diff --git a/docs_headless/src/content/docs/usePublish.md b/docs_headless/src/content/docs/usePublish.md new file mode 100644 index 00000000000..51568af64e0 --- /dev/null +++ b/docs_headless/src/content/docs/usePublish.md @@ -0,0 +1,104 @@ +--- +title: "usePublish" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Get a callback to publish an event on a topic. The callback returns a promise that resolves when the event is published. + +`usePublish` calls `dataProvider.publish()` to publish the event. It leverages react-query's `useMutation` hook to provide a callback. + +**Note**: Events should generally be published by the server, in reaction to an action by an end user. They should seldom be published directly by the client. This hook is provided mostly for testing purposes, but you may use it in your own custom components if you know what you're doing. + +## Usage + +`usePublish` returns a callback with the following signature: + +```tsx +const publish = usePublish(); +publish(topic, event, options); +``` + +For instance, in a chat application, when a user is typing a message, the following component publishes a `typing` event to the `chat/[channel]` topic: + +```tsx +import { useInput, useGetIdentity } from 'ra-core'; +import { usePublish } from '@react-admin/ra-core-ee'; + +const MessageInput = ({ channel }) => { + const [publish, { isLoading }] = usePublish(); + const { id, field, fieldState } = useInput({ source: 'message' }); + const { identity } = useGetIdentity(); + + const handleUserInput = event => { + publish(`chat/${channel}`, { + type: 'typing', + payload: { user: identity }, + }); + }; + + return ( + + ); +}; +``` + +The event format is up to you. It should at least contain a `type` property and may contain a `payload` property. The `payload` property can contain any data you want to send to the subscribers. + +Some hooks and components in this package are specialized to handle "CRUD" events, which are events with a `type` property set to `created`, `updated` or `deleted`. For instance: + +```js +{ + topic: `resource/${resource}/id`, + event: { + type: 'deleted', + payload: { ids: [id]}, + }, +} +``` + +See the [CRUD events](#crud-events) section for more details. + +## Return Value + +`usePublish` returns an array with the following values: + +- `publish`: The callback to publish an event to a topic. +- `state`: The state of the mutation ([see react-query documentation](https://react-query-v3.tanstack.com/reference/useMutation)). Notable properties: + - `isLoading`: Whether the mutation is loading. + - `error`: The error if the mutation failed. + - `data`: The published event if the mutation succeeded. + +```tsx +const [publish, { isLoading, error, data }] = usePublish(); +``` + +## Callback Parameters + +The `publish` callback accepts the following parameters: + +- `topic`: The topic to publish the event on. +- `event`: The event to publish. It must contain a `type` property. +- `options`: `useMutation` options ([see react-query documentation](https://react-query-v3.tanstack.com/reference/useMutation)). Notable properties: + - `onSuccess`: A callback to call when the event is published. It receives the published event as its first argument. + - `onError`: A callback to call when the event could not be published. It receives the error as its first argument. + - `retry`: Whether to retry on failure. Defaults to `0`. + +```tsx +const [publish] = usePublish(); +publish( + 'chat/general', + { + type: 'message', + payload: { user: 'John', message: 'Hello!' }, + }, + { + onSuccess: event => console.log('Event published', event), + onError: error => console.log('Could not publish event', error), + retry: 3, + } +); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useSubscribe.md b/docs_headless/src/content/docs/useSubscribe.md new file mode 100644 index 00000000000..0b5b8958d15 --- /dev/null +++ b/docs_headless/src/content/docs/useSubscribe.md @@ -0,0 +1,155 @@ +--- +title: "useSubscribe" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Subscribe to the events from a topic on mount (and unsubscribe on unmount). + + + +## Usage + +The following component subscribes to the `messages/{channelName}` topic and displays a badge with the number of unread messages: + +```tsx +import { useState, useCallback } from 'react'; +import { useSubscribe } from '@react-admin/ra-core-ee'; + +const ChannelName = ({ name }) => { + const [nbMessages, setNbMessages] = useState(0); + + const callback = useCallback( + event => { + if (event.type === 'created') { + setNbMessages(count => count + 1); + } + }, + [setNbMessages] + ); + + useSubscribe(`messages/${name}`, callback); + + return nbMessages > 0 ? ( +

#{name} ({nbMessages} new messages)

+ ) : ( +

#{name}

+ ); +}; +``` + +## Parameters + +| Prop | Required | Type | Default | Description | +| ---------- | -------- | ---------- | ------- | ------------------------------------------------------------------ | +| `topic` | Optional | `string` | - | The topic to subscribe to. When empty, no subscription is created. | +| `callback` | Optional | `function` | - | The callback to execute when an event is received. | +| `options` | Optional | `object` | - | Options to modify the subscription / unsubscription behavior. | + +## `callback` + +This function will be called with the event as its first argument, so you can use it to update the UI. + +```tsx +useSubscribe(`messages/${name}`, event => { + if (event.type === 'created') { + setNbMessages(count => count + 1); + } +}); +``` + +**Tip**: Memoize the callback using `useCallback` to avoid unnecessary subscriptions/unsubscriptions. + +```tsx +const callback = useCallback( + event => { + if (event.type === 'created') { + setNbMessages(count => count + 1); + } + }, + [setNbMessages] +); +useSubscribe(`messages/${name}`, callback); +``` + +The callback function receives an `unsubscribe` callback as its second argument. You can call it to unsubscribe from the topic after receiving a specific event. + +```tsx +import { useState, useCallback } from 'react'; +import { useSubscribe } from '@react-admin/ra-core-ee'; + +const JobProgress = ({ jobId }) => { + const [progress, setProgress] = useState(0); + const callback = useCallback( + (event, unsubscribe) => { + if (event.type === 'progress') { + setProgress(event.payload.progress); + } + if (event.type === 'completed') { + unsubscribe(); + } + }, + [setColor] + ); + useSubscribe(`jobs/${jobId}`, callback); + return ( +
{progress}%
+ ); +}; +``` + + + +## `options` + +The `options` object can contain the following properties: + +- `enabled`: Whether to subscribe or not. Defaults to `true` +- `once`: Whether to unsubscribe after the first event. Defaults to `false`. +- `unsubscribeOnUnmount`: Whether to unsubscribe on unmount. Defaults to `true`. + +You can use the `once` option to subscribe to a topic only once, and then unsubscribe. + +For instance, the following component subscribes to the `office/restart` topic and changes the message when the office is open, then unsubscribes from the topic: + +```tsx +import { useState } from 'react'; +import { useSubscribe } from '@react-admin/ra-core-ee'; + +const OfficeClosed = () => { + const [state, setState] = useState('closed'); + + useSubscribe('office/restart', () => setState('open'), { once: true }); + + return ( +
+ {state === 'closed' + ? 'Sorry, the office is closed for maintenance.' + : 'Welcome! The office is open.'} +
+ ); +}; +``` + + + +## `topic` + +The first argument of `useSubscribe` is the topic to subscribe to. It can be an arbitrary string. + +```tsx +useSubscribe('messages', event => { + // ... +}); +``` + +If you want to subscribe to CRUD events, instead of writing the topic manually like `resource/[resource]`, you can use the `useSubscribeToRecord` or `useSubscribeToRecordList` hooks. \ No newline at end of file diff --git a/docs_headless/src/content/docs/useSubscribeCallback.md b/docs_headless/src/content/docs/useSubscribeCallback.md new file mode 100644 index 00000000000..99be01551e3 --- /dev/null +++ b/docs_headless/src/content/docs/useSubscribeCallback.md @@ -0,0 +1,185 @@ +--- +title: "useSubscribeCallback" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +Get a callback to subscribe to events on a topic and optionally unsubscribe on unmount. + +This is useful to start a subscription from an event handler, like a button click. + + + +## Usage + +The following component subscribes to the `backgroundJobs/recompute` topic on click, and displays the progress of the background job: + +```tsx +import { useState, useCallback } from 'react'; +import { useDataProvider } from 'ra-core'; +import { useSubscribeCallback } from '@react-admin/ra-core-ee'; + +const LaunchBackgroundJob = () => { + const dataProvider = useDataProvider(); + const [progress, setProgress] = useState(0); + const callback = useCallback( + (event, unsubscribe) => { + setProgress(event.payload?.progress || 0); + if (event.payload?.progress === 100) { + unsubscribe(); + } + }, + [setProgress] + ); + const subscribe = useSubscribeCallback( + 'backgroundJobs/recompute', + callback + ); + + return ( +
+ +
+ ); +}; +``` + +## Parameters + +| Prop | Required | Type | Default | Description | +| ---------- | -------- | ---------- | ------- | ------------------------------------------------------------------ | +| `topic` | Optional | `string` | - | The topic to subscribe to. When empty, no subscription is created. | +| `callback` | Optional | `function` | - | The callback to execute when an event is received. | +| `options` | Optional | `object` | - | Options to modify the subscription / unsubscription behavior. | + +## `callback` + +Whenever an event is published on the `topic` passed as the first argument, the function passed as the second argument will be called with the event as a parameter. + +```tsx +const subscribe = useSubscribeCallback('backgroundJobs/recompute', event => { + if (event.type === 'progress') { + setProgress(event.payload.progress); + } +}); + +// later +subscribe(); +``` + +**Tip**: Memoize the callback using `useCallback` to avoid unnecessary subscriptions/unsubscriptions. + +```tsx +const callback = useCallback( + event => { + if (event.type === 'progress') { + setProgress(event.payload.progress); + } + }, + [setProgress] +); +``` + +The callback function receives an `unsubscribe` callback as its second argument. You can call it to unsubscribe from the topic after receiving a specific event. + +```tsx +const subscribe = useSubscribeCallback( + 'backgroundJobs/recompute', + (event, unsubscribe) => { + if (event.type === 'completed') { + setProgress(100); + unsubscribe(); + } + } +); +``` + +## `options` + +The `options` object can contain the following properties: + +- `enabled`: Whether to subscribe or not. Defaults to `true` +- `once`: Whether to unsubscribe after the first event. Defaults to `false`. +- `unsubscribeOnUnmount`: Whether to unsubscribe on unmount. Defaults to `true`. + +You can use the `once` option to subscribe to a topic only once, and then unsubscribe. + +For instance, the following component subscribes to the `backgroundJobs/recompute` topic on click, displays a notification when the background job is complete, then unsubscribes: + +```jsx +import { useDataProvider, useNotify } from 'ra-core'; +import { useSubscribeCallback } from '@react-admin/ra-core-ee'; + +const LaunchBackgroundJob = () => { + const dataProvider = useDataProvider(); + const notify = useNotify(); + + const subscribe = useSubscribeCallback( + 'backgroundJobs/recompute', + event => + notify('Recompute complete: %{summary}', { + type: 'success', + messageArgs: { + summary: event.payload?.summary, + }, + }), + { + unsubscribeOnUnmount: false, // show the notification even if the user navigates away + once: true, // unsubscribe after the first event + } + ); + + return ( + + ); +}; +``` + + + +You can use the `unsubscribeOnUnmount` option to keep the subscription alive after the component unmounts. + +This can be useful when you want the subscription to persist across multiple pages. + +```tsx +const subscribe = useSubscribeCallback( + 'backgroundJobs/recompute', + event => setProgress(event.payload?.progress || 0), + { + unsubscribeOnUnmount: false, // don't unsubscribe on unmount + } +); +``` + +## `topic` + +The first argument of `useSubscribeCallback` is the topic to subscribe to. It can be an arbitrary string. + +```tsx +const subscribe = useSubscribeCallback('backgroundJobs/recompute', event => { + // ... +}); + +// later +subscribe(); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useSubscribeToRecord.md b/docs_headless/src/content/docs/useSubscribeToRecord.md new file mode 100644 index 00000000000..a3ba9df841b --- /dev/null +++ b/docs_headless/src/content/docs/useSubscribeToRecord.md @@ -0,0 +1,215 @@ +--- +title: "useSubscribeToRecord" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +This specialized version of `useSubscribe` subscribes to events concerning a single record. + + + +## Usage + +The hook expects a callback function as its only argument, as it guesses the record and resource from the current context. The callback will be executed whenever an event is published on the `resource/[resource]/[recordId]` topic. + +For instance, the following component displays a message when the record is updated by someone else: + +```tsx +const WarnWhenUpdatedBySomeoneElse = () => { + const [open, setOpen] = useState(false); + const [author, setAuthor] = useState(null); + const handleClose = () => { + setOpen(false); + }; + const { refetch } = useEditContext(); + const refresh = () => { + refetch(); + handleClose(); + }; + const { + formState: { isDirty }, + } = useFormContext(); + + useSubscribeToRecord((event: Event) => { + if (event.type === 'edited') { + if (isDirty) { + setOpen(true); + setAuthor(event.payload.user); + } else { + refetch(); + } + } + }); + return open ? ( +
+

+ Post Updated by {author} +

+

+ Your changes and their changes may conflict. What do you + want to do? +

+
+ + +
+
+ ) : null; +}; + +const PostEdit = () => ( + +
+ {/* Inputs... */} + + +
+); +``` + +`useSubscribeToRecord` reads the current resource and record from the `ResourceContext` and `RecordContext` respectively. In the example above, the notification is displayed when the app receives an event on the `resource/books/123` topic. + +Just like `useSubscribe`, `useSubscribeToRecord` unsubscribes from the topic when the component unmounts. + +**Tip**: In the example above, `` creates the `RecordContext`- that's why the `useSubscribeToRecord` hook is used in its child component instead of in the `` component. + +You can provide the resource and record id explicitly if you are not in such contexts: + +```tsx +useSubscribeToRecord( + event => { + /* ... */ + }, + 'posts', + 123 +); +``` + +**Tip**: If your reason to subscribe to events on a record is to keep the record up to date, you should use [the `useGetOneLive` hook](#usegetonelive) instead. + +## Parameters + +| Prop | Required | Type | Default | Description | +| ---------- | -------- | ---------- | ------- | --------------------------------------------------------------------------------------- | +| `callback` | Required | `function` | - | The callback to execute when an event is received. | +| `resource` | Optional | `string` | - | The resource to subscribe to. Defaults to the resource in the `ResourceContext`. | +| `recordId` | Optional | `string` | - | The record id to subscribe to. Defaults to the id of the record in the `RecordContext`. | +| `options` | Optional | `object` | - | The subscription options. | + +## `callback` + +Whenever an event is published on the `resource/[resource]/[recordId]` topic, the function passed as the first argument will be called with the event as a parameter. + +```tsx +const [open, setOpen] = useState(false); +const [author, setAuthor] = useState(null); +const { refetch } = useEditContext(); +const { + formState: { isDirty }, +} = useFormContext(); +useSubscribeToRecord((event: Event) => { + if (event.type === 'edited') { + if (isDirty) { + setOpen(true); + setAuthor(event.payload.user); + } else { + refetch(); + } + } +}); +``` + +**Tip**: Memoize the callback using `useCallback` to avoid unnecessary subscriptions/unsubscriptions. + +```tsx +const [open, setOpen] = useState(false); +const [author, setAuthor] = useState(null); +const { refetch } = useEditContext(); +const { + formState: { isDirty }, +} = useFormContext(); + +const handleEvent = useCallback( + (event: Event) => { + if (event.type === 'edited') { + if (isDirty) { + setOpen(true); + setAuthor(event.payload.user); + } else { + refetch(); + } + } + }, + [isDirty, refetch, setOpen, setAuthor] +); + +useSubscribeToRecord(handleEvent); +``` + +Just like for `useSubscribe`, the callback function receives an `unsubscribe` callback as its second argument. You can call it to unsubscribe from the topic after receiving a specific event. + +```tsx +useSubscribeToRecord((event: Event, unsubscribe) => { + if (event.type === 'deleted') { + // do something + unsubscribe(); + } + if (event.type === 'edited') { + if (isDirty) { + setOpen(true); + setAuthor(event.payload.user); + } else { + refetch(); + } + } +}); +``` + +## `options` + +The `options` object can contain the following properties: + +- `enabled`: Whether to subscribe or not. Defaults to `true` +- `once`: Whether to unsubscribe after the first event. Defaults to `false`. +- `unsubscribeOnUnmount`: Whether to unsubscribe on unmount. Defaults to `true`. + +See [`useSubscribe`](#usesubscribe) for more details. + +## `recordId` + +The record id to subscribe to. By default, `useSubscribeToRecord` builds the topic it subscribes to using the id of the record in the `RecordContext`. But you can override this behavior by passing a record id as the third argument. + +```tsx +// will subscribe to the 'resource/posts/123' topic +useSubscribeToRecord( + event => { + /* ... */ + }, + 'posts', + 123 +); +``` + +Note that if you pass a null record id, the hook will not subscribe to any topic. + +## `resource` + +The resource to subscribe to. By default, `useSubscribeToRecord` builds the topic it subscribes to using the resource in the `ResourceContext`. But you can override this behavior by passing a resource name as the second argument. + +```tsx +// will subscribe to the 'resource/posts/123' topic +useSubscribeToRecord( + event => { + /* ... */ + }, + 'posts', + 123 +); +``` + +Note that if you pass an empty string as the resource name, the hook will not subscribe to any topic. \ No newline at end of file diff --git a/docs_headless/src/content/docs/useSubscribeToRecordList.md b/docs_headless/src/content/docs/useSubscribeToRecordList.md new file mode 100644 index 00000000000..9b1f3e810e3 --- /dev/null +++ b/docs_headless/src/content/docs/useSubscribeToRecordList.md @@ -0,0 +1,159 @@ +--- +title: "useSubscribeToRecordList" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +This specialized version of `useSubscribe` subscribes to events concerning a list of records. + + + +## Usage + +`useSubscribeToRecordList` expects a callback function as its first argument. It will be executed whenever an event is published on the `resource/[resource]` topic. + +For instance, the following component displays notifications when a record is created, updated, or deleted by someone else: + +```tsx +import React from 'react'; +import { useNotify, useListContext } from 'ra-core'; +import { useSubscribeToRecordList } from '@react-admin/ra-core-ee'; + +const ListWatcher = () => { + const notity = useNotify(); + const { refetch, data } = useListContext(); + useSubscribeToRecordList(event => { + switch (event.type) { + case 'created': { + notity('New movie created'); + refetch(); + break; + } + case 'updated': { + if (data.find(record => record.id === event.payload.ids[0])) { + notity(`Movie #${event.payload.ids[0]} updated`); + refetch(); + } + break; + } + case 'deleted': { + if (data.find(record => record.id === event.payload.ids[0])) { + notity(`Movie #${event.payload.ids[0]} deleted`); + refetch(); + } + break; + } + } + }); + return null; +}; + +const MovieList = () => ( + + {/* The list view*/} + + +); +``` + +## Parameters + +| Prop | Required | Type | Default | Description | +| ---------- | -------- | ---------- | ------- | -------------------------------------------------------------------------------- | +| `callback` | Required | `function` | - | The callback function to execute when an event is published on the topic. | +| `resource` | Optional | `string` | - | The resource to subscribe to. Defaults to the resource in the `ResourceContext`. | +| `options` | Optional | `object` | - | The subscription options. | + +## `callback` + +Whenever an event is published on the `resource/[resource]` topic, the function passed as the first argument will be called with the event as a parameter. + +```tsx +const notity = useNotify(); +const { refetch, data } = useListContext(); +useSubscribeToRecordList(event => { + switch (event.type) { + case 'created': { + notity('New movie created'); + refetch(); + break; + } + case 'updated': { + if (data.find(record => record.id === event.payload.ids[0])) { + notity(`Movie #${event.payload.ids[0]} updated`); + refetch(); + } + break; + } + case 'deleted': { + if (data.find(record => record.id === event.payload.ids[0])) { + notity(`Movie #${event.payload.ids[0]} deleted`); + refetch(); + } + break; + } + } +}); +``` + +**Tip**: Memoize the callback using `useCallback` to avoid unnecessary subscriptions/unsubscriptions. + +```tsx +const notity = useNotify(); +const { refetch, data } = useListContext(); +const callback = useCallback( + event => { + switch (event.type) { + case 'created': { + notity('New movie created'); + refetch(); + break; + } + case 'updated': { + if (data.find(record => record.id === event.payload.ids[0])) { + notity(`Movie #${event.payload.ids[0]} updated`); + refetch(); + } + break; + } + case 'deleted': { + if (data.find(record => record.id === event.payload.ids[0])) { + notity(`Movie #${event.payload.ids[0]} deleted`); + refetch(); + } + break; + } + } + }, + [data, refetch, notity] +); +useSubscribeToRecordList(callback); +``` + +Just like for `useSubscribe`, the callback function receives an `unsubscribe` callback as its second argument. You can call it to unsubscribe from the topic after receiving a specific event. + +## `options` + +The `options` object can contain the following properties: + +- `enabled`: Whether to subscribe or not. Defaults to `true` +- `once`: Whether to unsubscribe after the first event. Defaults to `false`. +- `unsubscribeOnUnmount`: Whether to unsubscribe on unmount. Defaults to `true`. + +See [`useSubscribe`](#usesubscribe) for more details. + +## `resource` + +`useSubscribeToRecordList` reads the current resource from the `ResourceContext`. You can provide the resource explicitly if you are not in such a context: + +```tsx +useSubscribeToRecordList(event => { + if (event.type === 'updated') { + notify('Post updated'); + refresh(); + } +}, 'posts'); +``` \ No newline at end of file diff --git a/docs_headless/src/content/docs/useUnlock.md b/docs_headless/src/content/docs/useUnlock.md new file mode 100644 index 00000000000..bdf043f2f7d --- /dev/null +++ b/docs_headless/src/content/docs/useUnlock.md @@ -0,0 +1,23 @@ +--- +title: "useUnlock" +--- + +**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. + +`useUnlock` is a low-level hook that returns a callback to call `dataProvider.unlock()`, leveraging react-query's `useMutation`. + +```tsx +const [unlock, { isLoading, error }] = useUnlock( + resource, + { id, identity, meta }, + options +); +``` + +The payload is an object with the following properties: + +- `id`: the record id (e.g. `123`) +- `identity`: an identifier (string or number) corresponding to the identity of the locker (e.g. `'julien'`). This usually comes from `authProvider.getIdentity()` +- `meta`: an object that will be forwarded to the dataProvider (optional) + +The optional `options` argument is passed to react-query's `useMutation` hook. \ No newline at end of file diff --git a/docs_headless/src/styles/global.css b/docs_headless/src/styles/global.css index 44a9b393722..0de57d232a3 100644 --- a/docs_headless/src/styles/global.css +++ b/docs_headless/src/styles/global.css @@ -13,7 +13,7 @@ --sl-color-accent-high: #ff78ac; } -img.icon { +img.icon { display: inline; box-shadow: none; margin: 0; @@ -44,7 +44,7 @@ img.icon { .expressive-code button.cb-fullscreen__button { color: var(--ec-frm-inlBtnFg); -} +} /* Auth and Data Provider icons */ @@ -96,3 +96,57 @@ img.icon { overflow-wrap: break-word; } +a.enterprise { + position: relative; + padding-right: 26px; +} + +a.enterprise .sl-badge { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +a.enterprise span:not(.sl-badge)::after { + content: ''; + background-repeat: no-repeat; + width: 16px; + height: 16px; + position: absolute; + top: 6px; + right: 6px; +} + +a.enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-dark.svg'); +} +a[aria-current='page'].enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-dark.svg'); +} + +[data-theme="light"] a.enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-light.svg'); +} +[data-theme="light"] a[aria-current='page'].enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-dark.svg'); +} + +[data-theme="dark"] a.enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-dark.svg'); +} +[data-theme="dark"] a[aria-current='page'].enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-light.svg'); +} + +@media (prefers-color-scheme: dark) { + a.enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-dark.svg'); + } + a[aria-current='page'].enterprise span:not(.sl-badge)::after { + background-image: url('/public/premium-light.svg'); + } +} From a41f30138fba42670ab397d3a0d26f5dde51aa62 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 1 Oct 2025 18:48:46 +0200 Subject: [PATCH 2/6] Improve aces control documentation --- .../content/docs/canAccessWithPermissions.md | 82 +++++++++++++++---- .../content/docs/getPermissionsFromRoles.md | 47 +++++++---- 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/docs_headless/src/content/docs/canAccessWithPermissions.md b/docs_headless/src/content/docs/canAccessWithPermissions.md index 1a6422cb56b..30f5cf4dfea 100644 --- a/docs_headless/src/content/docs/canAccessWithPermissions.md +++ b/docs_headless/src/content/docs/canAccessWithPermissions.md @@ -2,13 +2,76 @@ title: "canAccessWithPermissions" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +`canAccessWithPermissions` is a helper function that facilitates the implementation of [Access Control](./Permissions.md#access-control) policies based on an underlying list of user roles and permissions. -`canAccessWithPermissions` is a helper function that facilitates the `authProvider.canAccess()` method implementation: +It is a builder block to implement the `authProvider.canAccess()` method, which is called by ra-core to check whether the current user has the right to perform a given action on a given resource or 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 -The user roles and permissions should be returned upon login. The `authProvider` should store the permissions in memory, or in localStorage. This allows `authProvider.canAccess()` to read the permissions from localStorage. +`canAccessWithPermissions` is a pure function that you can call from your `authProvider.canAccess()` implementation. + +```tsx +import { canAccessWithPermissions } from '@react-admin/ra-core-ee'; + +const authProvider = { + // ... + canAccess: async ({ action, resource, record }) => { + const permissions = myGetPermissionsFunction(); + return canAccessWithPermissions({ + permissions, + action, + resource, + record, + }); + } + // ... +}; +``` + +The `permissions` parameter must be an array of permissions. A *permission* is an object that represents access to a subset of the application. It is defined by a `resource` (usually a noun) and an `action` (usually a verb), with sometimes an additional `record`. + +Here are a few examples of permissions: + +- `{ action: "*", resource: "*" }`: allow everything +- `{ action: "read", resource: "*" }`: allow read actions on all resources +- `{ action: "read", resource: ["companies", "people"] }`: allow read actions on a subset of resources +- `{ action: ["read", "create", "edit", "export"], resource: "companies" }`: allow all actions except delete on companies +- `{ action: ["write"], resource: "game.score", record: { "id": "123" } }`: allow write action on the score of the game with id 123 + +:::tip +When the `record` field is omitted, the permission is valid for all records. +::: + +In most cases, the permissions are derived from user roles, which are fetched at login and stored in memory or in localStorage. Check the [`getPermissionsFromRoles`](./getPermissionsFromRoles.md) function to merge the permissions from multiple roles into a single flat array of permissions. + +## Parameters + +This function takes an object as argument with the following fields: + +| Name | Optional | Type | Description +| - | - | - | - | +| `permissions` | Required | `Array` | An array of permissions for the current user +| `action` | Required | `string` | The action for which to check users has the execution right +| `resource` | Required | `string` | The resource for which to check users has the execution right +| `record` | Required | `string` | The record for which to check users has the execution right + +`canAccessWithPermissions` expects the `permissions` to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the [`getPermissionsFromRoles`](#getpermissionsfromroles) function. + +## Building RBAC + +The following example shows how to implement Role-based Access Control (RBAC) in `authProvider.canAccess()` using `canAccessWithPermissions` and `getPermissionsFromRoles`. The role permissions are defined in the code, and the user roles are returned by the authentication endpoint. Additional user permissions can also be returned by the authentication endpoint. + +The `authProvider` stores the permissions in `localStorage`, so that returning users can access their permissions without having to log in again. ```tsx // in roleDefinitions.ts @@ -62,16 +125,3 @@ const authProvider = { // ... }; ``` - -## Parameters - -This function takes an object as argument with the following fields: - -| Name | Optional | Type | Description -| - | - | - | - | -| `permissions` | Required | `Array` | An array of permissions for the current user -| `action` | Required | `string` | The action for which to check users has the execution right -| `resource` | Required | `string` | The resource for which to check users has the execution right -| `record` | Required | `string` | The record for which to check users has the execution right - -`canAccessWithPermissions` expects the `permissions` to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the [`getPermissionsFromRoles`](#getpermissionsfromroles) function. \ No newline at end of file diff --git a/docs_headless/src/content/docs/getPermissionsFromRoles.md b/docs_headless/src/content/docs/getPermissionsFromRoles.md index ab92a585c1d..e6beb9ea815 100644 --- a/docs_headless/src/content/docs/getPermissionsFromRoles.md +++ b/docs_headless/src/content/docs/getPermissionsFromRoles.md @@ -2,9 +2,25 @@ title: "getPermissionsFromRoles" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +`getPermissionsFromRoles` returns an array of user permissions based on a role definition, a list of roles, and a list of user permissions. It merges the permissions defined in `roleDefinitions` for the current user's roles (`userRoles`) with the extra `userPermissions`. -This function returns an array of user permissions based on a role definition, a list of roles, and a list of user permissions. It merges the permissions defined in `roleDefinitions` for the current user's roles (`userRoles`) with the extra `userPermissions`. +It is a builder block to implement the `authProvider.canAccess()` method, which is called by ra-core to check whether the current user has the right to perform a given action on a given resource or 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 + +`getPermissionsFromRoles` takes a configuration object as argument containing the role definitions, the user roles, and the user permissions. + +It returns an array of permissions that can be passed to [`canAccessWithPermissions`](./canAccessWithPermissions.md). ```ts import { getPermissionsFromRoles } from '@react-admin/ra-core-ee'; @@ -41,9 +57,21 @@ const permissions = getPermissionsFromRoles({ // ]; ``` -## Usage +## Parameters -The user roles and permissions should be returned upon login. The `authProvider` should store the permissions in memory, or in localStorage. This allows `authProvider.canAccess()` to read the permissions from localStorage. +This function takes an object as argument with the following fields: + +| Name | Optional | Type | Description +| - | - | - | - | +| `roleDefinitions` | Required | `Record` | A dictionary containing the role definition for each role +| `userRoles` | Optional | `Array` | An array of roles (admin, reader...) for the current user +| `userPermissions` | Optional | `Array` | An array of permissions for the current user + +## Building RBAC + +The following example shows how to implement Role-based Access Control (RBAC) in `authProvider.canAccess()` using `canAccessWithPermissions` and `getPermissionsFromRoles`. The role permissions are defined in the code, and the user roles are returned by the authentication endpoint. Additional user permissions can also be returned by the authentication endpoint. + +The `authProvider` stores the permissions in `localStorage`, so that returning users can access their permissions without having to log in again. ```tsx // in roleDefinitions.ts @@ -97,14 +125,3 @@ const authProvider = { // ... }; ``` - -## Parameters - -This function takes an object as argument with the following fields: - -| Name | Optional | Type | Description -| - | - | - | - | -| `roleDefinitions` | Required | `Record` | A dictionary containing the role definition for each role -| `userRoles` | Optional | `Array` | An array of roles (admin, reader...) for the current user -| `userPermissions` | Optional | `Array` | An array of permissions for the current user - From 9f9b365efad6c664c8de907393bea128781e5e85 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Wed, 1 Oct 2025 18:52:46 +0200 Subject: [PATCH 3/6] Fix syntax --- docs_headless/src/content/docs/canAccessWithPermissions.md | 6 +++--- docs_headless/src/content/docs/getPermissionsFromRoles.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs_headless/src/content/docs/canAccessWithPermissions.md b/docs_headless/src/content/docs/canAccessWithPermissions.md index 30f5cf4dfea..1dad8cd27ba 100644 --- a/docs_headless/src/content/docs/canAccessWithPermissions.md +++ b/docs_headless/src/content/docs/canAccessWithPermissions.md @@ -80,9 +80,9 @@ export const roleDefinitions = { { action: '*', resource: '*' } ], reader: [ - { action: ['list', 'show', 'export'], resource: '*' } - { action: 'read', resource: 'posts.*' } - { action: 'read', resource: 'comments.*' } + { action: ['list', 'show', 'export'], resource: '*' }, + { action: 'read', resource: 'posts.*' }, + { action: 'read', resource: 'comments.*' }, ], accounting: [ { action: '*', resource: 'sales' }, diff --git a/docs_headless/src/content/docs/getPermissionsFromRoles.md b/docs_headless/src/content/docs/getPermissionsFromRoles.md index e6beb9ea815..5e582f72eb7 100644 --- a/docs_headless/src/content/docs/getPermissionsFromRoles.md +++ b/docs_headless/src/content/docs/getPermissionsFromRoles.md @@ -31,9 +31,9 @@ const roleDefinitions = { { action: '*', resource: '*' } ], reader: [ - { action: ['list', 'show', 'export'], resource: '*' } - { action: 'read', resource: 'posts.*' } - { action: 'read', resource: 'comments.*' } + { action: ['list', 'show', 'export'], resource: '*' }, + { action: 'read', resource: 'posts.*' }, + { action: 'read', resource: 'comments.*' }, ], accounting: [ { action: '*', resource: 'sales' }, From 147ac5b90b2981c0aa63f203c0cbff7a731a92a8 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:34:56 +0200 Subject: [PATCH 4/6] Apply review suggestions --- docs/useSubscribeToRecordList.md | 26 ++++++++--------- docs_headless/astro.config.mjs | 2 +- .../src/content/docs/LockStatusBase.md | 2 +- .../content/docs/canAccessWithPermissions.md | 2 +- .../content/docs/getPermissionsFromRoles.md | 10 +++---- .../src/content/docs/useGetListLive.md | 2 +- docs_headless/src/content/docs/useGetLock.md | 2 +- .../src/content/docs/useGetOneLive.md | 2 +- .../content/docs/useSubscribeToRecordList.md | 28 +++++++++---------- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/docs/useSubscribeToRecordList.md b/docs/useSubscribeToRecordList.md index 4131eb10713..308a9c46bae 100644 --- a/docs/useSubscribeToRecordList.md +++ b/docs/useSubscribeToRecordList.md @@ -26,25 +26,25 @@ import { useNotify, useListContext } from 'react-admin'; import { useSubscribeToRecordList } from '@react-admin/ra-realtime'; const ListWatcher = () => { - const notity = useNotify(); + const notify = useNotify(); const { refetch, data } = useListContext(); useSubscribeToRecordList(event => { switch (event.type) { case 'created': { - notity('New movie created'); + notify('New movie created'); refetch(); break; } case 'updated': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} updated`); + notify(`Movie #${event.payload.ids[0]} updated`); refetch(); } break; } case 'deleted': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} deleted`); + notify(`Movie #${event.payload.ids[0]} deleted`); refetch(); } break; @@ -80,25 +80,25 @@ const MovieList = () => ( Whenever an event is published on the `resource/[resource]` topic, the function passed as the first argument will be called with the event as a parameter. ```jsx -const notity = useNotify(); +const notify = useNotify(); const { refetch, data } = useListContext(); useSubscribeToRecordList(event => { switch (event.type) { case 'created': { - notity('New movie created'); + notify('New movie created'); refetch(); break; } case 'updated': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} updated`); + notify(`Movie #${event.payload.ids[0]} updated`); refetch(); } break; } case 'deleted': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} deleted`); + notify(`Movie #${event.payload.ids[0]} deleted`); refetch(); } break; @@ -110,33 +110,33 @@ useSubscribeToRecordList(event => { **Tip**: Memoize the callback using `useCallback` to avoid unnecessary subscriptions/unsubscriptions. ```jsx -const notity = useNotify(); +const notify = useNotify(); const { refetch, data } = useListContext(); const callback = useCallback( event => { switch (event.type) { case 'created': { - notity('New movie created'); + notify('New movie created'); refetch(); break; } case 'updated': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} updated`); + notify(`Movie #${event.payload.ids[0]} updated`); refetch(); } break; } case 'deleted': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} deleted`); + notify(`Movie #${event.payload.ids[0]} deleted`); refetch(); } break; } } }, - [data, refetch, notity] + [data, refetch, notify] ); useSubscribeToRecordList(callback); ``` diff --git a/docs_headless/astro.config.mjs b/docs_headless/astro.config.mjs index 519f2e4fd3c..940197834de 100644 --- a/docs_headless/astro.config.mjs +++ b/docs_headless/astro.config.mjs @@ -271,7 +271,7 @@ export default defineConfig({ */ function enterpriseEntry(name) { return { - link: name.toLowerCase().replace(//g, ''), + link: `${name.toLowerCase().replace(//g, '')}/`, label: name, attrs: { class: 'enterprise' }, badge: { diff --git a/docs_headless/src/content/docs/LockStatusBase.md b/docs_headless/src/content/docs/LockStatusBase.md index 4955553fa1c..739602e12a4 100644 --- a/docs_headless/src/content/docs/LockStatusBase.md +++ b/docs_headless/src/content/docs/LockStatusBase.md @@ -81,4 +81,4 @@ export const LockStatus = () => { }; ``` -In addition to the [`useLockCallbacks`](#uselockcallbacks) parameters, `` accepts a `render` prop. The function passed to the `render` prop will be called with the result of the `useLockCallbacks` hook. \ No newline at end of file +In addition to the [`useLockCallbacks`](./useLockCallbacks.md) parameters, `` accepts a `render` prop. The function passed to the `render` prop will be called with the result of the `useLockCallbacks` hook. \ No newline at end of file diff --git a/docs_headless/src/content/docs/canAccessWithPermissions.md b/docs_headless/src/content/docs/canAccessWithPermissions.md index 1dad8cd27ba..c52857dee52 100644 --- a/docs_headless/src/content/docs/canAccessWithPermissions.md +++ b/docs_headless/src/content/docs/canAccessWithPermissions.md @@ -65,7 +65,7 @@ This function takes an object as argument with the following fields: | `resource` | Required | `string` | The resource for which to check users has the execution right | `record` | Required | `string` | The record for which to check users has the execution right -`canAccessWithPermissions` expects the `permissions` to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the [`getPermissionsFromRoles`](#getpermissionsfromroles) function. +`canAccessWithPermissions` expects the `permissions` to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the [`getPermissionsFromRoles`](./getPermissionsFromRoles.md) function. ## Building RBAC diff --git a/docs_headless/src/content/docs/getPermissionsFromRoles.md b/docs_headless/src/content/docs/getPermissionsFromRoles.md index 5e582f72eb7..18ed28ae578 100644 --- a/docs_headless/src/content/docs/getPermissionsFromRoles.md +++ b/docs_headless/src/content/docs/getPermissionsFromRoles.md @@ -28,7 +28,7 @@ import { getPermissionsFromRoles } from '@react-admin/ra-core-ee'; // static role definitions (usually in the app code) const roleDefinitions = { admin: [ - { action: '*', resource: '*' } + { action: '*', resource: '*' }, ], reader: [ { action: ['list', 'show', 'export'], resource: '*' }, @@ -77,12 +77,12 @@ The `authProvider` stores the permissions in `localStorage`, so that returning u // in roleDefinitions.ts export const roleDefinitions = { admin: [ - { action: '*', resource: '*' } + { action: '*', resource: '*' }, ], reader: [ - { action: ['list', 'show', 'export'], resource: '*' } - { action: 'read', resource: 'posts.*' } - { action: 'read', resource: 'comments.*' } + { action: ['list', 'show', 'export'], resource: '*' }, + { action: 'read', resource: 'posts.*' }, + { action: 'read', resource: 'comments.*' }, ], accounting: [ { action: '*', resource: 'sales' }, diff --git a/docs_headless/src/content/docs/useGetListLive.md b/docs_headless/src/content/docs/useGetListLive.md index f185bffa594..2181ec4d0e2 100644 --- a/docs_headless/src/content/docs/useGetListLive.md +++ b/docs_headless/src/content/docs/useGetListLive.md @@ -33,4 +33,4 @@ const LatestNews = () => { The hook will subscribe to live updates on the list of records (topic: `resource/[resource]`) and will refetch the list when a new record is created, or an existing record is updated or deleted. -See the [useGetList](https://marmelab.com/react-admin/useGetList.html) documentation for the full list of parameters and return type. \ No newline at end of file +See the [useGetList](./useGetList.md) documentation for the full list of parameters and return type. \ No newline at end of file diff --git a/docs_headless/src/content/docs/useGetLock.md b/docs_headless/src/content/docs/useGetLock.md index 9a7c9c83206..2d8eccc9c87 100644 --- a/docs_headless/src/content/docs/useGetLock.md +++ b/docs_headless/src/content/docs/useGetLock.md @@ -39,7 +39,7 @@ const FormToolbar = () => { {isLockedByOtherUser && ( - {`This record is locked by another user: ${lock?.dentity}.`} + {`This record is locked by another user: ${lock?.identity}.`} )} diff --git a/docs_headless/src/content/docs/useGetOneLive.md b/docs_headless/src/content/docs/useGetOneLive.md index feb2cc6f815..65704a3e470 100644 --- a/docs_headless/src/content/docs/useGetOneLive.md +++ b/docs_headless/src/content/docs/useGetOneLive.md @@ -27,4 +27,4 @@ const UserProfile = () => { The hook will subscribe to live updates on the record (topic: `resource/[resource]/[id]`) and will refetch the record when it is updated or deleted. -See the [useGetOne](https://marmelab.com/react-admin/useGetOne.html) documentation for the full list of parameters and return type. \ No newline at end of file +See the [useGetOne](./useGetOne.md) documentation for the full list of parameters and return type. \ No newline at end of file diff --git a/docs_headless/src/content/docs/useSubscribeToRecordList.md b/docs_headless/src/content/docs/useSubscribeToRecordList.md index 9b1f3e810e3..10cdbb4bc56 100644 --- a/docs_headless/src/content/docs/useSubscribeToRecordList.md +++ b/docs_headless/src/content/docs/useSubscribeToRecordList.md @@ -23,25 +23,25 @@ import { useNotify, useListContext } from 'ra-core'; import { useSubscribeToRecordList } from '@react-admin/ra-core-ee'; const ListWatcher = () => { - const notity = useNotify(); + const notify = useNotify(); const { refetch, data } = useListContext(); useSubscribeToRecordList(event => { switch (event.type) { case 'created': { - notity('New movie created'); + notify('New movie created'); refetch(); break; } case 'updated': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} updated`); + notify(`Movie #${event.payload.ids[0]} updated`); refetch(); } break; } case 'deleted': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} deleted`); + notify(`Movie #${event.payload.ids[0]} deleted`); refetch(); } break; @@ -72,25 +72,25 @@ const MovieList = () => ( Whenever an event is published on the `resource/[resource]` topic, the function passed as the first argument will be called with the event as a parameter. ```tsx -const notity = useNotify(); +const notify = useNotify(); const { refetch, data } = useListContext(); useSubscribeToRecordList(event => { switch (event.type) { case 'created': { - notity('New movie created'); + notify('New movie created'); refetch(); break; } case 'updated': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} updated`); + notify(`Movie #${event.payload.ids[0]} updated`); refetch(); } break; } case 'deleted': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} deleted`); + notify(`Movie #${event.payload.ids[0]} deleted`); refetch(); } break; @@ -102,33 +102,33 @@ useSubscribeToRecordList(event => { **Tip**: Memoize the callback using `useCallback` to avoid unnecessary subscriptions/unsubscriptions. ```tsx -const notity = useNotify(); +const notify = useNotify(); const { refetch, data } = useListContext(); const callback = useCallback( event => { switch (event.type) { case 'created': { - notity('New movie created'); + notify('New movie created'); refetch(); break; } case 'updated': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} updated`); + notify(`Movie #${event.payload.ids[0]} updated`); refetch(); } break; } case 'deleted': { if (data.find(record => record.id === event.payload.ids[0])) { - notity(`Movie #${event.payload.ids[0]} deleted`); + notify(`Movie #${event.payload.ids[0]} deleted`); refetch(); } break; } } }, - [data, refetch, notity] + [data, refetch, notify] ); useSubscribeToRecordList(callback); ``` @@ -143,7 +143,7 @@ The `options` object can contain the following properties: - `once`: Whether to unsubscribe after the first event. Defaults to `false`. - `unsubscribeOnUnmount`: Whether to unsubscribe on unmount. Defaults to `true`. -See [`useSubscribe`](#usesubscribe) for more details. +See [`useSubscribe`](./useSubscribe.md) for more details. ## `resource` From 7b7d989dfc196a1e8338a02f1606147c1785f81f Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:18:35 +0200 Subject: [PATCH 5/6] Add installation steps --- .../src/content/docs/LockStatusBase.md | 14 ++++++++-- .../src/content/docs/useGetListLive.md | 14 ++++++++-- docs_headless/src/content/docs/useGetLock.md | 28 +++++++++++-------- .../src/content/docs/useGetLockLive.md | 14 +++++++++- docs_headless/src/content/docs/useGetLocks.md | 15 ++++++---- .../src/content/docs/useGetLocksLive.md | 18 ++++++++++-- .../src/content/docs/useGetOneLive.md | 14 ++++++++-- docs_headless/src/content/docs/useLock.md | 14 ++++++++-- .../src/content/docs/useLockCallbacks.md | 11 ++++++-- .../src/content/docs/useLockOnCall.md | 15 +++++++--- .../src/content/docs/useLockOnMount.md | 12 ++++++-- docs_headless/src/content/docs/usePublish.md | 12 ++++++-- .../src/content/docs/useSubscribe.md | 12 ++++++-- .../src/content/docs/useSubscribeCallback.md | 12 ++++++-- .../src/content/docs/useSubscribeToRecord.md | 12 ++++++-- .../content/docs/useSubscribeToRecordList.md | 12 ++++++-- docs_headless/src/content/docs/useUnlock.md | 14 ++++++++-- 17 files changed, 195 insertions(+), 48 deletions(-) diff --git a/docs_headless/src/content/docs/LockStatusBase.md b/docs_headless/src/content/docs/LockStatusBase.md index 739602e12a4..0f73f4ad023 100644 --- a/docs_headless/src/content/docs/LockStatusBase.md +++ b/docs_headless/src/content/docs/LockStatusBase.md @@ -2,9 +2,19 @@ title: "" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +`` 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. -Use the `` component to display the lock status of the record in the nearest `RecordContext`: +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 React from 'react'; diff --git a/docs_headless/src/content/docs/useGetListLive.md b/docs_headless/src/content/docs/useGetListLive.md index 2181ec4d0e2..5f2e6aef406 100644 --- a/docs_headless/src/content/docs/useGetListLive.md +++ b/docs_headless/src/content/docs/useGetListLive.md @@ -2,9 +2,19 @@ title: "useGetListLive" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +`useGetListLive` is an alternative to `useGetList` that subscribes to live updates on the record list. -Alternative to `useGetList` that subscribes to live updates on the record list. +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 { useGetListLive } from '@react-admin/ra-core-ee'; diff --git a/docs_headless/src/content/docs/useGetLock.md b/docs_headless/src/content/docs/useGetLock.md index 2d8eccc9c87..00ecff38956 100644 --- a/docs_headless/src/content/docs/useGetLock.md +++ b/docs_headless/src/content/docs/useGetLock.md @@ -2,20 +2,19 @@ title: "useGetLock" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +A hook that gets the lock status for a record. It calls `dataProvider.getLock()` on mount. -Gets the lock status for a record. It calls `dataProvider.getLock()` on mount. +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. -```tsx -const { data, isLoading } = useGetLock(resource, { id }); -``` +## Installation -Parameters description: +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` -- `resource`: the resource name (e.g. `'posts'`) -- `params`: an object with the following properties: - - `id`: the record id (e.g. `123`) - - `meta`: Optional. an object that will be forwarded to the dataProvider (optional) +## Usage Here is a form toolbar that displays the lock status of the current record: @@ -45,4 +44,11 @@ const FormToolbar = () => { ); }; -``` \ No newline at end of file +``` + +## Parameters + +- `resource`: the resource name (e.g. `'posts'`) +- `params`: an object with the following properties: + - `id`: the record id (e.g. `123`) + - `meta`: Optional. an object that will be forwarded to the dataProvider (optional) diff --git a/docs_headless/src/content/docs/useGetLockLive.md b/docs_headless/src/content/docs/useGetLockLive.md index cd44605d809..cf53fb71574 100644 --- a/docs_headless/src/content/docs/useGetLockLive.md +++ b/docs_headless/src/content/docs/useGetLockLive.md @@ -2,7 +2,19 @@ title: "useGetLockLive" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +A hook that gets the lock status for a record in real time. + +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 Use the `useGetLockLive()` hook to get the lock status in real time. This hook calls `dataProvider.getLock()` for the current record on mount, and subscribes to live updates on the `lock/[resource]/[id]` topic. diff --git a/docs_headless/src/content/docs/useGetLocks.md b/docs_headless/src/content/docs/useGetLocks.md index 109641a7075..613731d9789 100644 --- a/docs_headless/src/content/docs/useGetLocks.md +++ b/docs_headless/src/content/docs/useGetLocks.md @@ -2,15 +2,20 @@ title: "useGetLocks" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +A hook that gets all the locks for a given resource. Calls `dataProvider.getLocks()` on mount. -Get all the locks for a given resource. Calls `dataProvider.getLocks()` on mount. +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. -```tsx -// simple Usage -const { data } = useGetLocks('posts'); +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee ``` +## Usage + Here is how to use it in a custom list, to disable edit and delete buttons for locked records: ```tsx diff --git a/docs_headless/src/content/docs/useGetLocksLive.md b/docs_headless/src/content/docs/useGetLocksLive.md index 60c46e07968..4918fb989dc 100644 --- a/docs_headless/src/content/docs/useGetLocksLive.md +++ b/docs_headless/src/content/docs/useGetLocksLive.md @@ -2,10 +2,21 @@ title: "useGetLocksLive" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +Use the `useGetLocksLive` hook to get all the locks for a resource in real time. -Use the `useGetLocksLive` hook to get the locks in real time. This hook calls `dataProvider.getLocks()` for the current resource on mount, and subscribes to live updates on the `lock/[resource]` topic. +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 + +This hook calls `dataProvider.getLocks()` for the current resource on mount, and subscribes to live updates on the `lock/[resource]` topic. This means that if a lock is acquired or released by another user while the current user is on the page, the return value will be updated. ```tsx @@ -13,8 +24,9 @@ import { useRecordContext } from 'ra-core'; import { useGetLocksLive } from '@react-admin/ra-core-ee'; import { Lock } from 'lucide-react'; -export const LockField = ({ locks }) => { +export const LockField = () => { const record = useRecordContext(); + const locks = useGetLocksLive(); if (!record) return null; const lock = locks?.find(lock => lock.recordId === record?.id); if (!lock) return ; diff --git a/docs_headless/src/content/docs/useGetOneLive.md b/docs_headless/src/content/docs/useGetOneLive.md index 65704a3e470..170ed6c0ae2 100644 --- a/docs_headless/src/content/docs/useGetOneLive.md +++ b/docs_headless/src/content/docs/useGetOneLive.md @@ -2,9 +2,19 @@ title: "useGetOneLive" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +An alternative to `useGetOne()` that subscribes to live updates on the record -Alternative to `useGetOne()` that subscribes to live updates on the 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 { useRecordContext } from 'ra-core'; diff --git a/docs_headless/src/content/docs/useLock.md b/docs_headless/src/content/docs/useLock.md index 05b3219d3b2..ba621d0b1a8 100644 --- a/docs_headless/src/content/docs/useLock.md +++ b/docs_headless/src/content/docs/useLock.md @@ -2,10 +2,20 @@ title: "useLock" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - `useLock` is a low-level hook that returns a callback to call `dataProvider.lock()`, leveraging react-query's `useMutation`. +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 const [lock, { isLoading, error }] = useLock( resource, diff --git a/docs_headless/src/content/docs/useLockCallbacks.md b/docs_headless/src/content/docs/useLockCallbacks.md index a7e2b0e49b0..bb1ed3debd7 100644 --- a/docs_headless/src/content/docs/useLockCallbacks.md +++ b/docs_headless/src/content/docs/useLockCallbacks.md @@ -1,10 +1,17 @@ --- title: "useLockCallbacks" --- +This utility hook allows to easily get the callbacks to **lock** and **unlock** a record, as well as the current **lock status**. -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. +This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription. -This utility hook allows to easily get the callbacks to **lock** and **unlock** a record, as well as the current **lock status**. +## Installation + +```bash +npm install --save @react-admin/ra-core-ee +# or +yarn add @react-admin/ra-core-ee +``` ## Usage diff --git a/docs_headless/src/content/docs/useLockOnCall.md b/docs_headless/src/content/docs/useLockOnCall.md index f6df5877c9b..e2928ea71e3 100644 --- a/docs_headless/src/content/docs/useLockOnCall.md +++ b/docs_headless/src/content/docs/useLockOnCall.md @@ -2,10 +2,7 @@ title: "useLockOnCall" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - -Get a callback to lock a record and get a mutation state. - +A hook that gets a callback to lock a record and its mutation state. `useLockOnCall` calls `dataProvider.lock()` when the callback is called. 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. It releases the lock when the component unmounts by calling `dataProvider.unlock()`. +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 Use this hook in a toolbar, to let the user lock the record manually. diff --git a/docs_headless/src/content/docs/useLockOnMount.md b/docs_headless/src/content/docs/useLockOnMount.md index d54c3dc4136..447e1c72a41 100644 --- a/docs_headless/src/content/docs/useLockOnMount.md +++ b/docs_headless/src/content/docs/useLockOnMount.md @@ -2,8 +2,6 @@ title: "useLockOnMount" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - This hook locks the current record on mount. `useLockOnMount` calls `dataProvider.lock()` on mount and `dataProvider.unlock()` on unmount to lock and unlock the record. 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. @@ -13,6 +11,16 @@ This hook locks the current record on mount. Your browser does not support the video tag. +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 Use this hook e.g. in an `` component to lock the record so that it only accepts updates from the current user. diff --git a/docs_headless/src/content/docs/usePublish.md b/docs_headless/src/content/docs/usePublish.md index 51568af64e0..00e70b4b23c 100644 --- a/docs_headless/src/content/docs/usePublish.md +++ b/docs_headless/src/content/docs/usePublish.md @@ -2,14 +2,22 @@ title: "usePublish" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - Get a callback to publish an event on a topic. The callback returns a promise that resolves when the event is published. `usePublish` calls `dataProvider.publish()` to publish the event. It leverages react-query's `useMutation` hook to provide a callback. **Note**: Events should generally be published by the server, in reaction to an action by an end user. They should seldom be published directly by the client. This hook is provided mostly for testing purposes, but you may use it in your own custom components if you know what you're doing. +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 `usePublish` returns a callback with the following signature: diff --git a/docs_headless/src/content/docs/useSubscribe.md b/docs_headless/src/content/docs/useSubscribe.md index 0b5b8958d15..e2ef052af03 100644 --- a/docs_headless/src/content/docs/useSubscribe.md +++ b/docs_headless/src/content/docs/useSubscribe.md @@ -2,8 +2,6 @@ title: "useSubscribe" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - Subscribe to the events from a topic on mount (and unsubscribe on unmount). +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 The following component subscribes to the `messages/{channelName}` topic and displays a badge with the number of unread messages: diff --git a/docs_headless/src/content/docs/useSubscribeCallback.md b/docs_headless/src/content/docs/useSubscribeCallback.md index 99be01551e3..fdfd4679d8d 100644 --- a/docs_headless/src/content/docs/useSubscribeCallback.md +++ b/docs_headless/src/content/docs/useSubscribeCallback.md @@ -2,8 +2,6 @@ title: "useSubscribeCallback" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - Get a callback to subscribe to events on a topic and optionally unsubscribe on unmount. This is useful to start a subscription from an event handler, like a button click. @@ -13,6 +11,16 @@ This is useful to start a subscription from an event handler, like a button clic Your browser does not support the video tag. +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 The following component subscribes to the `backgroundJobs/recompute` topic on click, and displays the progress of the background job: diff --git a/docs_headless/src/content/docs/useSubscribeToRecord.md b/docs_headless/src/content/docs/useSubscribeToRecord.md index a3ba9df841b..60860259236 100644 --- a/docs_headless/src/content/docs/useSubscribeToRecord.md +++ b/docs_headless/src/content/docs/useSubscribeToRecord.md @@ -2,8 +2,6 @@ title: "useSubscribeToRecord" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - This specialized version of `useSubscribe` subscribes to events concerning a single 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 The hook expects a callback function as its only argument, as it guesses the record and resource from the current context. The callback will be executed whenever an event is published on the `resource/[resource]/[recordId]` topic. diff --git a/docs_headless/src/content/docs/useSubscribeToRecordList.md b/docs_headless/src/content/docs/useSubscribeToRecordList.md index 10cdbb4bc56..9337325dba7 100644 --- a/docs_headless/src/content/docs/useSubscribeToRecordList.md +++ b/docs_headless/src/content/docs/useSubscribeToRecordList.md @@ -2,8 +2,6 @@ title: "useSubscribeToRecordList" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - This specialized version of `useSubscribe` subscribes to events concerning a list of records. +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 `useSubscribeToRecordList` expects a callback function as its first argument. It will be executed whenever an event is published on the `resource/[resource]` topic. diff --git a/docs_headless/src/content/docs/useUnlock.md b/docs_headless/src/content/docs/useUnlock.md index bdf043f2f7d..ec68350dd22 100644 --- a/docs_headless/src/content/docs/useUnlock.md +++ b/docs_headless/src/content/docs/useUnlock.md @@ -2,10 +2,20 @@ title: "useUnlock" --- -**Tip**: `ra-core-ee` is part of the [React-Admin Enterprise Edition](https://marmelab.com/ra-enterprise/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. - `useUnlock` is a low-level hook that returns a callback to call `dataProvider.unlock()`, leveraging react-query's `useMutation`. +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 const [unlock, { isLoading, error }] = useUnlock( resource, From a95edd0f3d9a534be9e37ad06dc79f57423a21b2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Thu, 9 Oct 2025 11:07:54 +0200 Subject: [PATCH 6/6] [no ci] fix more anchors --- docs_headless/src/content/docs/useLockOnCall.md | 2 +- docs_headless/src/content/docs/useLockOnMount.md | 2 +- docs_headless/src/content/docs/usePublish.md | 2 +- docs_headless/src/content/docs/useSubscribeToRecord.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs_headless/src/content/docs/useLockOnCall.md b/docs_headless/src/content/docs/useLockOnCall.md index e2928ea71e3..aac0e1f7b9f 100644 --- a/docs_headless/src/content/docs/useLockOnCall.md +++ b/docs_headless/src/content/docs/useLockOnCall.md @@ -62,7 +62,7 @@ const PostEdit = () => ( ); ``` -**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) hook or the [``](#lockstatusbase) component. +**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 [``](./LockStatusBase.md) component. ## Parameters diff --git a/docs_headless/src/content/docs/useLockOnMount.md b/docs_headless/src/content/docs/useLockOnMount.md index 447e1c72a41..d65a0a9b2e4 100644 --- a/docs_headless/src/content/docs/useLockOnMount.md +++ b/docs_headless/src/content/docs/useLockOnMount.md @@ -58,7 +58,7 @@ const PostEdit = () => ( ); ``` -**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 [``](#lockstatusbase) component. +**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 [``](./LockStatusBase.md) component. ## Parameters diff --git a/docs_headless/src/content/docs/usePublish.md b/docs_headless/src/content/docs/usePublish.md index 00e70b4b23c..530dadc71a5 100644 --- a/docs_headless/src/content/docs/usePublish.md +++ b/docs_headless/src/content/docs/usePublish.md @@ -68,7 +68,7 @@ Some hooks and components in this package are specialized to handle "CRUD" event } ``` -See the [CRUD events](#crud-events) section for more details. +See the [CRUD events](https://react-admin-ee.marmelab.com/documentation/ra-realtime#crud-events) section for more details. ## Return Value diff --git a/docs_headless/src/content/docs/useSubscribeToRecord.md b/docs_headless/src/content/docs/useSubscribeToRecord.md index 60860259236..70b1974363f 100644 --- a/docs_headless/src/content/docs/useSubscribeToRecord.md +++ b/docs_headless/src/content/docs/useSubscribeToRecord.md @@ -98,7 +98,7 @@ useSubscribeToRecord( ); ``` -**Tip**: If your reason to subscribe to events on a record is to keep the record up to date, you should use [the `useGetOneLive` hook](#usegetonelive) instead. +**Tip**: If your reason to subscribe to events on a record is to keep the record up to date, you should use [the `useGetOneLive` hook](./useGetOneLive.md) instead. ## Parameters @@ -186,7 +186,7 @@ The `options` object can contain the following properties: - `once`: Whether to unsubscribe after the first event. Defaults to `false`. - `unsubscribeOnUnmount`: Whether to unsubscribe on unmount. Defaults to `true`. -See [`useSubscribe`](#usesubscribe) for more details. +See [`useSubscribe`](./useSubscribe.md) for more details. ## `recordId`