Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add <RecordRepresentation> to streamline rendering a record as string #9095

Merged
merged 5 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
1 change: 1 addition & 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)
djhi marked this conversation as resolved.
Show resolved Hide resolved
slax57 marked this conversation as resolved.
Show resolved Hide resolved
* [`<ReferenceArrayField>`](./ReferenceArrayField.md)
* [`<ReferenceArrayInput>`](./ReferenceArrayInput.md)
* [`<ReferenceField>`](./ReferenceField.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(props);
const resource = useResourceContext(props);
djhi marked this conversation as resolved.
Show resolved Hide resolved
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';
Loading