Skip to content

Commit

Permalink
Merge pull request #9095 from marmelab/RecordRepresentation
Browse files Browse the repository at this point in the history
Add RecordRepresentation component
  • Loading branch information
slax57 authored Jul 12, 2023
2 parents 171593e + 9dd4c41 commit 63549d9
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 0 deletions.
67 changes: 67 additions & 0 deletions docs/RecordRepresentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
layout: default
title: "The RecordRepresentation Component"
---

Render the record representation, leveraging the [`recordRepresentation`](./Resource.md#recordrepresentation) prop of the parent `<Resource>` component.

You can also uses its hook version: [`<useGetRecordRepresentation>`](./useGetRecordRepresentation.md).

## Usage

```tsx
// in src/posts/PostBreadcrumbs.tsx
import * as React from 'react';
import { Breadcrumbs, Typography } from '@mui/material';
import { Link, RecordRepresentation } from 'react-admin';

export const PostBreadcrumbs = () => {
return (
<div role="presentation">
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" to="/">
Home
</Link>
<Link underline="hover" color="inherit" to="/posts">
Posts
</Link>
<Typography color="text.primary">
<RecordRepresentation />
</Typography>
</Breadcrumbs>
</div>
);
}

// in src/posts/PostEdit.tsx
import { EditBase, EditView, SimpleForm, TextInput } from 'react-admin';
import { PostBreadcrumbs } from './PostBreadcrumbs';

const PostEdit = () => (
<EditBase>
<PostBreadcrumbs />
<EditView>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</EditView>
</EditBase>
)
```

## Props

Here are all the props you can set on the `<RecordRepresentation>` component:

| Prop | Required | Type | Default | Description |
| ---------- | -------- | ---------- | ------------------------------------------ | ----------------------|
| `record` | Optional | `RaRecord` | Record from the parent `RecordContext` | The record to display |
| `resource` | Optional | `string` | Resource from the parent `ResourceContext` | The record's resource |

## `record`

The record to display. Defaults to the record from the parent `RecordContext`.

## `resource`

The record's resource. Defaults to the resource from the parent `ResourceContext`.
2 changes: 2 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ title: "Index"

**- R -**
* [`<RadioButtonGroupInput>`](./RadioButtonGroupInput.md)
* [`<RecordRepresentation>`](./RecordRepresentation.md)
* [`<ReferenceArrayField>`](./ReferenceArrayField.md)
* [`<ReferenceArrayInput>`](./ReferenceArrayInput.md)
* [`<ReferenceField>`](./ReferenceField.md)
Expand Down Expand Up @@ -275,6 +276,7 @@ title: "Index"

**- R -**
* [`useRecordContext`](./useRecordContext.md)
* [`useRecordRepresentation`](./useRecordRepresentation.md)
* [`useRedirect`](./useRedirect.md)
* [`useReference`](./useGetOne.md#aggregating-getone-calls)
* [`useRefresh`](./useRefresh.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ For instance, to change the default representation of "users" records to render
- a function (e.g. `(record) => record.title`) to specify a custom string representation
- a React component (e.g. `<MyCustomRecordRepresentation />`). In such components, use [`useRecordContext`](./useRecordContext.md) to access the record.

If you want to display this record representation somewhere, you can leverage the [`useGetRecordRepresentation`](./useGetRecordRepresentation.md) hook or the [`<RecordRepresentation>`](./RecordRepresentation.md) component.

## `hasCreate`, `hasEdit`, `hasShow`

Some components, like [`<CreateDialog>`](./CreateDialog.md), [`<EditDialog>`](./EditDialog.md) or [`<ShowDialog>`](./ShowDialog.md) need to declare the CRUD components outside of the `<Resource>` component. In such cases, you can use the `hasCreate`, `hasEdit` and `hasShow` props to tell react-admin which CRUD components are available for a given resource.
Expand Down
2 changes: 2 additions & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@
<li {% if page.path == 'Confirm.md' %} class="active" {% endif %}><a class="nav-link" href="./Confirm.html"><code>&lt;Confirm&gt;</code></a></li>
<li {% if page.path == 'Buttons.md' %} class="active" {% endif %}><a class="nav-link" href="./Buttons.html">Buttons</a></li>
<li {% if page.path == 'UpdateButton.md' %} class="active" {% endif %}><a class="nav-link" href="./UpdateButton.html"><code>&lt;UpdateButton&gt;</code></a></li>
<li {% if page.path == 'RecordRepresentation.md' %} class="active" {% endif %}><a class="nav-link" href="./RecordRepresentation.html"><code>&lt;RecordRepresentation&gt;</code></a></li>
<li {% if page.path == 'useGetRecordRepresentation.md' %} class="active" {% endif %}><a class="nav-link" href="./useGetRecordRepresentation.html"><code>useGetRecordRepresentation</code></a></li>
</ul>

<ul><div>Realtime</div>
Expand Down
65 changes: 65 additions & 0 deletions docs/useGetRecordRepresentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
layout: default
title: "The useGetRecordRepresentation Component"
---

Returns a function that get the record representation, leveraging the [`recordRepresentation`](./Resource.md#recordrepresentation) prop of the parent `<Resource>` component.

You can also uses its component version: [`<RecordRepresentation>`](./RecordRepresentation.md).

## Usage

```tsx
// in src/posts/PostBreadcrumbs.tsx
import * as React from 'react';
import { Breadcrumbs, Typography } from '@mui/material';
import { Link, useGetRecordRepresentation, useRecordContext, useResourceContext } from 'react-admin';

export const PostBreadcrumbs = () => {
const record = useRecordContext();
const resource = useResourceContext();
const getRecordRepresentation = useGetRecordRepresentation(resource);
return (
<div role="presentation">
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" to="/">
Home
</Link>
<Link underline="hover" color="inherit" to="/posts">
Posts
</Link>
<Typography color="text.primary">
{getRecordRepresentation(record)}
</Typography>
</Breadcrumbs>
</div>
);
}

// in src/posts/PostEdit.tsx
import { EditBase, EditView, SimpleForm, TextInput } from 'react-admin';
import { PostBreadcrumbs } from './PostBreadcrumbs';

const PostEdit = () => (
<EditBase>
<PostBreadcrumbs />
<EditView>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</EditView>
</EditBase>
)
```

## Options

Here are all the options you can set on the `useGetRecordRepresentation` hook:

| Prop | Required | Type | Default | Description |
| ---------- | -------- | ---------- | ------- | ----------------------|
| `resource` | Required | `string` | | The record's resource |

## `resource`

The record's resource.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import {
ComponentRecordRepresentation,
FunctionRecordRepresentation,
NoRecordRepresentation,
StringRecordRepresentation,
} from './RecordRepresentation.stories';

describe('RecordRepresentation', () => {
it('should render the record id when not provided on its parent <Resource>', async () => {
render(<NoRecordRepresentation />);
await screen.findByText('#1');
});
it('should render the record representation when provided as a field name on its parent <Resource>', async () => {
render(<StringRecordRepresentation />);
await screen.findByText("The Hitchhiker's Guide to the Galaxy");
});
it('should render the record representation when provided as a function on its parent <Resource>', async () => {
render(<FunctionRecordRepresentation />);
await screen.findByText(
"The Hitchhiker's Guide to the Galaxy by Douglas Adams"
);
});
it('should render the record representation when provided as a component on its parent <Resource>', async () => {
render(<ComponentRecordRepresentation />);
await screen.findByText(
(content, element) => {
return (
element?.textContent ===
"The Hitchhiker's Guide to the Galaxy (by Douglas Adams) - 1979"
);
},
{ selector: 'p' }
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as React from 'react';
import {
ResourceContextProvider,
ResourceDefinitionContextProvider,
} from '../../core';
import { RecordContextProvider } from './RecordContext';
import { RecordRepresentation } from './RecordRepresentation';
import { useRecordContext } from './useRecordContext';
export default {
title: 'ra-core/controller/record/RecordRepresentation',
};

const Book = {
id: 1,
title: "The Hitchhiker's Guide to the Galaxy",
author: 'Douglas Adams',
publishedAt: '1979-10-12',
};

export const NoRecordRepresentation = () => (
<ResourceContextProvider value="books">
<ResourceDefinitionContextProvider
definitions={{
books: {
name: 'books',
hasList: true,
hasEdit: true,
hasShow: true,
hasCreate: true,
},
}}
>
<RecordContextProvider value={Book}>
<RecordRepresentation />
</RecordContextProvider>
</ResourceDefinitionContextProvider>
</ResourceContextProvider>
);

export const StringRecordRepresentation = () => (
<ResourceContextProvider value="books">
<ResourceDefinitionContextProvider
definitions={{
books: {
name: 'books',
hasList: true,
hasEdit: true,
hasShow: true,
hasCreate: true,
recordRepresentation: 'title',
},
}}
>
<RecordContextProvider value={Book}>
<RecordRepresentation />
</RecordContextProvider>
</ResourceDefinitionContextProvider>
</ResourceContextProvider>
);

export const FunctionRecordRepresentation = () => (
<ResourceContextProvider value="books">
<ResourceDefinitionContextProvider
definitions={{
books: {
name: 'books',
hasList: true,
hasEdit: true,
hasShow: true,
hasCreate: true,
recordRepresentation: record =>
`${record.title} by ${record.author}`,
},
}}
>
<RecordContextProvider value={Book}>
<RecordRepresentation />
</RecordContextProvider>
</ResourceDefinitionContextProvider>
</ResourceContextProvider>
);

const BookRepresentation = () => {
const record = useRecordContext();

if (!record) return null;

return (
<p>
<b>{record.title}</b>{' '}
<i>
(by {record.author}) -{' '}
{new Date(record.publishedAt).getFullYear()}
</i>
</p>
);
};
export const ComponentRecordRepresentation = () => (
<ResourceContextProvider value="books">
<ResourceDefinitionContextProvider
definitions={{
books: {
name: 'books',
hasList: true,
hasEdit: true,
hasShow: true,
hasCreate: true,
recordRepresentation: <BookRepresentation />,
},
}}
>
<RecordContextProvider value={Book}>
<RecordRepresentation />
</RecordContextProvider>
</ResourceDefinitionContextProvider>
</ResourceContextProvider>
);
21 changes: 21 additions & 0 deletions packages/ra-core/src/controller/record/RecordRepresentation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';
import { useGetRecordRepresentation, useResourceContext } from '../../core';
import { RaRecord } from '../../types';
import { useRecordContext } from './useRecordContext';

/**
* Render the record representation as specified on its parent <Resource>.
* @param props The component props
* @param {string} props.resource The resource name
* @param {RaRecord} props.record The record to render
*/
export const RecordRepresentation = (props: {
record?: RaRecord;
resource?: string;
}) => {
const record = useRecordContext(props);
const resource = useResourceContext(props);
const getRecordRepresentation = useGetRecordRepresentation(resource);

return <>{getRecordRepresentation(record)}</>;
};
1 change: 1 addition & 0 deletions packages/ra-core/src/controller/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './RecordContext';
export * from './useRecordContext';
export * from './WithRecord';
export * from './OptionalRecordContextProvider';
export * from './RecordRepresentation';

0 comments on commit 63549d9

Please sign in to comment.