From 4ce56793497f91c977c3f68ca6e0a70112ff2b80 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Tue, 20 May 2025 16:30:59 +0200 Subject: [PATCH 01/20] update show test to display the datagrid --- .../src/detail/ShowGuesser.spec.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx b/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx index 10c40540cba..1cfbd6a606e 100644 --- a/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx @@ -8,13 +8,18 @@ import { ThemeProvider } from '../theme/ThemeProvider'; describe('<ShowGuesser />', () => { it('should log the guessed Show view based on the fetched record', async () => { - const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + const logSpy = jest + .spyOn(console, 'log') + .mockImplementation(console.warn); const dataProvider = { getOne: () => Promise.resolve({ data: { id: 123, - author: 'john doe', + authors: [ + { id: 1, name: 'john doe', dob: '1990-01-01' }, + { id: 2, name: 'jane doe', dob: '1992-01-01' }, + ], post_id: 6, score: 3, body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.", @@ -22,6 +27,7 @@ describe('<ShowGuesser />', () => { tags_ids: [1, 2], }, }), + getMany: () => Promise.resolve({ data: [] }), }; render( <ThemeProvider> @@ -35,13 +41,15 @@ describe('<ShowGuesser />', () => { }); expect(logSpy).toHaveBeenCalledWith(`Guessed Show: -import { DateField, NumberField, ReferenceArrayField, ReferenceField, Show, SimpleShowLayout, TextField } from 'react-admin'; +import { ArrayField, Datagrid, DateField, NumberField, ReferenceArrayField, ReferenceField, Show, SimpleShowLayout, TextField } from 'react-admin'; export const CommentShow = () => ( <Show> <SimpleShowLayout> <TextField source="id" /> - <TextField source="author" /> + <ArrayField source="authors"><Datagrid><TextField source="id" /> +<TextField source="name" /> +<DateField source="dob" /></Datagrid></ArrayField> <ReferenceField source="post_id" reference="posts" /> <NumberField source="score" /> <TextField source="body" /> From b8c394034672df0cbb37a0eb04ab54690eafc0c2 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Tue, 20 May 2025 16:34:30 +0200 Subject: [PATCH 02/20] hide ts error in tests --- packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx index 1f898e5ea73..60f81ab4a85 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx @@ -10,6 +10,7 @@ describe('<ListGuesser />', () => { it('should log the guessed List view based on the fetched records', async () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); const dataProvider = testDataProvider({ + // @ts-ignore getList: () => Promise.resolve({ data: [ From 26acc5df753ba4396a28d8ebd049efd1044d2360 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Wed, 21 May 2025 10:31:22 +0200 Subject: [PATCH 03/20] Update ListGuesser with DataTable --- .../ra-ui-materialui/src/list/ListGuesser.tsx | 4 +- .../src/list/listFieldTypes.tsx | 101 ++++++++++-------- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.tsx index 595194e7c4f..fb57f87c7c1 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.tsx @@ -18,11 +18,11 @@ import { listFieldTypes } from './listFieldTypes'; import { capitalize, singularize } from 'inflection'; /** - * List component rendering a <Datagrid> based on the result of the + * List component rendering a <DataTable> based on the result of the * dataProvider.getList() call. * * The result (choice and type of columns) isn't configurable, but the - * <ListGuesser> outputs the <Datagrid> it has guessed to the console so that + * <ListGuesser> outputs the <DataTable> it has guessed to the console so that * developers can start from there. * * To be used as the list prop of a <Resource>. diff --git a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx index 69b1592236c..c0e01fa8489 100644 --- a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Datagrid } from './datagrid'; +import { DataTable } from './datatable'; import { SingleFieldList } from './SingleFieldList'; import { ArrayField, @@ -7,10 +7,8 @@ import { ChipField, DateField, EmailField, - NumberField, ReferenceField, ReferenceArrayField, - TextField, UrlField, ArrayFieldProps, } from '../field'; @@ -18,86 +16,103 @@ import { export const listFieldTypes = { table: { component: props => { - return <Datagrid {...props} />; + return <DataTable {...props} />; }, - representation: (_props, children) => ` <Datagrid> + representation: (_props, children) => ` <DataTable> ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} - </Datagrid>`, + </DataTable>`, }, array: { component: ({ children, ...props }: ArrayFieldProps) => { const childrenArray = React.Children.toArray(children); return ( - <ArrayField {...props}> - <SingleFieldList> - <ChipField - source={ - childrenArray.length > 0 && - React.isValidElement(childrenArray[0]) && - childrenArray[0].props.source - } - /> - </SingleFieldList> - </ArrayField> + <DataTable.Col {...props}> + <ArrayField {...props}> + <SingleFieldList> + <ChipField + source={ + childrenArray.length > 0 && + React.isValidElement(childrenArray[0]) && + childrenArray[0].props.source + } + /> + </SingleFieldList> + </ArrayField> + </DataTable.Col> ); }, representation: (props, children) => - `<ArrayField source="${ + `<DataTable.Col source="${props.source}"><ArrayField source="${ props.source }"><SingleFieldList><ChipField source="${ children.length > 0 && children[0].getProps().source - }" /></SingleFieldList></ArrayField>`, + }" /></SingleFieldList></ArrayField></DataTable.Col>`, }, boolean: { - component: BooleanField, - representation: props => `<BooleanField source="${props.source}" />`, + component: props => <DataTable.Col {...props} field={BooleanField} />, + representation: props => + `<DataTable.Col source="${props.source}" field={BooleanField} />`, }, date: { - component: DateField, - representation: props => `<DateField source="${props.source}" />`, + component: props => <DataTable.Col {...props} field={DateField} />, + representation: props => + `<DataTable.Col source="${props.source}" field={DateField} />`, }, email: { - component: EmailField, - representation: props => `<EmailField source="${props.source}" />`, + component: props => <DataTable.Col {...props} field={EmailField} />, + representation: props => + `<DataTable.Col source="${props.source}" field={EmailField} />`, }, id: { - component: TextField, - representation: props => `<TextField source="${props.source}" />`, + component: props => <DataTable.Col {...props} />, + representation: props => `<DataTable.Col source="${props.source}" />`, }, number: { - component: NumberField, - representation: props => `<NumberField source="${props.source}" />`, + component: DataTable.NumberCol, + representation: props => + `<DataTable.NumberCol source="${props.source}" />`, }, reference: { - component: ReferenceField, + component: props => ( + <DataTable.Col {...props}> + <ReferenceField {...props} /> + </DataTable.Col> + ), representation: props => - `<ReferenceField source="${props.source}" reference="${props.reference}" />`, + `<DataTable.Col source="${props.source}"><ReferenceField source="${props.source}" reference="${props.reference}" /></DataTable.Col>`, }, referenceChild: { - component: () => <TextField source="id" />, - representation: () => `<TextField source="id" />`, + component: () => <DataTable.Col source="id" />, + representation: () => `<DataTable.Col source="id" />`, }, referenceArray: { - component: ReferenceArrayField, + component: props => ( + <DataTable.Col {...props}> + <ReferenceArrayField {...props} /> + </DataTable.Col> + ), representation: props => - `<ReferenceArrayField source="${props.source}" reference="${props.reference}" />`, + `<DataTable.Col source="${props.source}"><ReferenceArrayField source="${props.source}" reference="${props.reference}" /></DataTable.Col>`, }, referenceArrayChild: { component: () => ( - <SingleFieldList> - <ChipField source="id" /> - </SingleFieldList> + <DataTable.Col> + <SingleFieldList> + <ChipField source="id" /> + </SingleFieldList> + </DataTable.Col> ), representation: () => - `<SingleFieldList><ChipField source="id" /></SingleFieldList>`, + `<DataTable.Col><SingleFieldList><ChipField source="id" /></SingleFieldList></DataTable.Col>`, }, richText: undefined, // never display a rich text field in a datagrid string: { - component: TextField, - representation: props => `<TextField source="${props.source}" />`, + component: DataTable.Col, + representation: props => `<DataTable.Col source="${props.source}" />`, }, url: { - component: UrlField, - representation: props => `<UrlField source="${props.source}" />`, + component: props => <DataTable.Col {...props} field={UrlField} />, + representation: props => + `<DataTable.Col source="${props.source}" field={UrlField} />`, }, }; From 92b29679d0037c9dc80f2d784595629418aded89 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Wed, 21 May 2025 16:22:07 +0200 Subject: [PATCH 04/20] fix child types --- .../ra-ui-materialui/src/list/listFieldTypes.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx index c0e01fa8489..1712fc5524e 100644 --- a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx @@ -11,6 +11,7 @@ import { ReferenceArrayField, UrlField, ArrayFieldProps, + TextField, } from '../field'; export const listFieldTypes = { @@ -82,8 +83,8 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} `<DataTable.Col source="${props.source}"><ReferenceField source="${props.source}" reference="${props.reference}" /></DataTable.Col>`, }, referenceChild: { - component: () => <DataTable.Col source="id" />, - representation: () => `<DataTable.Col source="id" />`, + component: () => <TextField source="id" />, + representation: () => `<TextField source="id" />`, }, referenceArray: { component: props => ( @@ -96,14 +97,12 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} }, referenceArrayChild: { component: () => ( - <DataTable.Col> - <SingleFieldList> - <ChipField source="id" /> - </SingleFieldList> - </DataTable.Col> + <SingleFieldList> + <ChipField source="id" /> + </SingleFieldList> ), representation: () => - `<DataTable.Col><SingleFieldList><ChipField source="id" /></SingleFieldList></DataTable.Col>`, + `<SingleFieldList><ChipField source="id" /></SingleFieldList>`, }, richText: undefined, // never display a rich text field in a datagrid string: { From ed24389de68c18540cbc77f5c73f40db1c33cdeb Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Wed, 21 May 2025 16:22:50 +0200 Subject: [PATCH 05/20] improve the story to display every possibility of ListGuesser --- .../src/list/ListGuesser.stories.tsx | 117 ++++++++++++++---- 1 file changed, 95 insertions(+), 22 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx index 61c93ec4168..2a9172699f1 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx @@ -21,6 +21,8 @@ const data = { price: 45.99, category_id: 1, tags_ids: [1], + last_update: new Date('2023-10-01').toISOString(), + email: 'office.jeans@myshop.com', }, { id: 2, @@ -28,6 +30,8 @@ const data = { price: 69.99, category_id: 1, tags_ids: [2, 3], + last_update: new Date('2023-11-01').toISOString(), + email: 'black.elegance.jeans@myshop.com', }, { id: 3, @@ -35,6 +39,8 @@ const data = { price: 55.99, category_id: 1, tags_ids: [2, 4], + last_update: new Date('2023-12-01').toISOString(), + email: 'slim.fit.jeans@myshop.com', }, { id: 4, @@ -42,6 +48,8 @@ const data = { price: 15.99, category_id: 2, tags_ids: [1, 4, 3], + last_update: new Date('2023-10-15').toISOString(), + email: 'basic.t.shirt@myshop.com', }, { id: 5, @@ -49,30 +57,93 @@ const data = { price: 19.99, category_id: 6, tags_ids: [1, 4, 3], + last_update: new Date('2023-10-15').toISOString(), + email: 'basic.cap@myshop.com', }, ], categories: [ - { id: 1, name: 'Jeans' }, - { id: 2, name: 'T-Shirts' }, - { id: 3, name: 'Jackets' }, - { id: 4, name: 'Shoes' }, - { id: 5, name: 'Accessories' }, - { id: 6, name: 'Hats' }, - { id: 7, name: 'Socks' }, - { id: 8, name: 'Shirts' }, - { id: 9, name: 'Sweaters' }, - { id: 10, name: 'Trousers' }, - { id: 11, name: 'Coats' }, - { id: 12, name: 'Dresses' }, - { id: 13, name: 'Skirts' }, - { id: 14, name: 'Swimwear' }, - { id: 15, name: 'Bags' }, + { + id: 1, + name: 'Jeans', + alternativeName: [{ name: 'denims' }, { name: 'pants' }], + isVeganProduction: true, + }, + { + id: 2, + name: 'T-Shirts', + alternativeName: [{ name: 'polo' }, { name: 'tee shirt' }], + isVeganProduction: false, + }, + { + id: 3, + name: 'Jackets', + alternativeName: [{ name: 'coat' }, { name: 'blazers' }], + isVeganProduction: false, + }, + { + id: 4, + name: 'Shoes', + alternativeName: [{ name: 'sneakers' }, { name: 'moccasins' }], + isVeganProduction: false, + }, + { + id: 5, + name: 'Accessories', + alternativeName: [{ name: 'jewelry' }, { name: 'belts' }], + isVeganProduction: true, + }, + { + id: 6, + name: 'Hats', + alternativeName: [{ name: 'caps' }, { name: 'headwear' }], + isVeganProduction: true, + }, + { + id: 7, + name: 'Socks', + alternativeName: [{ name: 'stockings' }, { name: 'hosiery' }], + isVeganProduction: false, + }, + { + id: 8, + name: 'Bags', + alternativeName: [{ name: 'handbags' }, { name: 'purses' }], + isVeganProduction: false, + }, + { + id: 9, + name: 'Dresses', + alternativeName: [{ name: 'robes' }, { name: 'gowns' }], + isVeganProduction: false, + }, + { + id: 10, + name: 'Skirts', + alternativeName: [{ name: 'tutus' }, { name: 'kilts' }], + isVeganProduction: false, + }, ], tags: [ - { id: 1, name: 'top seller' }, - { id: 2, name: 'new' }, - { id: 3, name: 'sale' }, - { id: 4, name: 'promotion' }, + { + id: 1, + name: 'top seller', + url: 'https://www.myshop.com/tags/top-seller', + }, + { + id: 2, + name: 'new', + url: 'https://www.myshop.com/tags/new', + }, + { + id: 3, + name: 'sale', + url: 'https://www.myshop.com/tags/sale', + }, + { + id: 4, + name: 'promotion', + url: 'https://www.myshop.com/tags/promotion', + }, ], }; @@ -119,6 +190,8 @@ const delayedDataProvider = fakeRestProvider( 300 ); +const ListGuesserWithProdLogs = props => <ListGuesser {...props} enableLog />; + export const ManyResources = () => ( <AdminContext dataProvider={delayedDataProvider} @@ -127,17 +200,17 @@ export const ManyResources = () => ( <AdminUI> <Resource name="products" - list={ListGuesser} + list={ListGuesserWithProdLogs} recordRepresentation="name" /> <Resource name="categories" - list={ListGuesser} + list={ListGuesserWithProdLogs} recordRepresentation="name" /> <Resource name="tags" - list={ListGuesser} + list={ListGuesserWithProdLogs} recordRepresentation="name" /> </AdminUI> From 96899d312d346c53ff9dfc025ab4e7a012270987 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Wed, 21 May 2025 16:53:25 +0200 Subject: [PATCH 06/20] adapt test to the new ListGuesser --- .../src/list/ListGuesser.spec.tsx | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx index 60f81ab4a85..0df39ca51ef 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx @@ -1,58 +1,63 @@ import * as React from 'react'; import expect from 'expect'; -import { render, screen, waitFor } from '@testing-library/react'; -import { CoreAdminContext, testDataProvider } from 'ra-core'; - -import { ListGuesser } from './ListGuesser'; -import { ThemeProvider } from '../theme/ThemeProvider'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { ManyResources } from './ListGuesser.stories'; describe('<ListGuesser />', () => { - it('should log the guessed List view based on the fetched records', async () => { + it('should log the guessed List views based on the fetched records', async () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - const dataProvider = testDataProvider({ - // @ts-ignore - getList: () => - Promise.resolve({ - data: [ - { - id: 123, - author: 'john doe', - post_id: 6, - score: 3, - body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.", - created_at: new Date('2012-08-02'), - tags_ids: [1, 2], - }, - ], - total: 1, - }), - getMany: () => Promise.resolve({ data: [], total: 0 }), - }); - render( - <ThemeProvider> - <CoreAdminContext dataProvider={dataProvider as any}> - <ListGuesser resource="comments" enableLog /> - </CoreAdminContext> - </ThemeProvider> - ); - await waitFor(() => { - screen.getByText('john doe'); - }); + render(<ManyResources />); + await screen.findAllByText('top seller'); + expect(logSpy).toHaveBeenCalledWith(`Guessed List: + +import { DataTable, DataTable.Col, DataTable.NumberCol, List, ReferenceArrayField, ReferenceField } from 'react-admin'; + +export const ProductList = () => ( + <List> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.NumberCol source="price" /> + <DataTable.Col source="category_id"><ReferenceField source="category_id" reference="categories" /></DataTable.Col> + <DataTable.Col source="tags_ids"><ReferenceArrayField source="tags_ids" reference="tags" /></DataTable.Col> + <DataTable.Col source="last_update" field={DateField} /> + <DataTable.Col source="email" field={EmailField} /> + </DataTable> + </List> +);`); + logSpy.mockClear(); + + fireEvent.click(screen.getByText('Categories')); + await screen.findByText('Jeans'); + expect(logSpy).toHaveBeenCalledWith(`Guessed List: + +import { ArrayField, ChipField, DataTable, DataTable.Col, List, SingleFieldList } from 'react-admin'; + +export const CategoryList = () => ( + <List> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.Col source="alternativeName"><ArrayField source="alternativeName"><SingleFieldList><ChipField source="name" /></SingleFieldList></ArrayField></DataTable.Col> + <DataTable.Col source="isVeganProduction" field={BooleanField} /> + </DataTable> + </List> +);`); + + logSpy.mockClear(); + fireEvent.click(screen.getByText('Tags')); + await screen.findByText('top seller'); expect(logSpy).toHaveBeenCalledWith(`Guessed List: -import { Datagrid, DateField, List, NumberField, ReferenceArrayField, ReferenceField, TextField } from 'react-admin'; +import { DataTable, DataTable.Col, List } from 'react-admin'; -export const CommentList = () => ( +export const TagList = () => ( <List> - <Datagrid> - <TextField source="id" /> - <TextField source="author" /> - <ReferenceField source="post_id" reference="posts" /> - <NumberField source="score" /> - <TextField source="body" /> - <DateField source="created_at" /> - <ReferenceArrayField source="tags_ids" reference="tags" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.Col source="url" field={UrlField} /> + </DataTable> </List> );`); }); From ea937454ab0b0f5d781661162933c2bcf5bba064 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 10:44:56 +0200 Subject: [PATCH 07/20] make the representation prettier with indentation + use children instead of field prop --- .../src/list/listFieldTypes.tsx | 64 ++++++++++++++----- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx index 1712fc5524e..670ed800e7a 100644 --- a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx @@ -43,26 +43,46 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} ); }, representation: (props, children) => - `<DataTable.Col source="${props.source}"><ArrayField source="${ - props.source - }"><SingleFieldList><ChipField source="${ - children.length > 0 && children[0].getProps().source - }" /></SingleFieldList></ArrayField></DataTable.Col>`, + `<DataTable.Col source="${props.source}"> + <ArrayField source="${props.source}"> + <SingleFieldList> + <ChipField source="${children.length > 0 && children[0].getProps().source}" /> + </SingleFieldList> + </ArrayField> + </DataTable.Col>`, }, boolean: { - component: props => <DataTable.Col {...props} field={BooleanField} />, + component: props => ( + <DataTable.Col {...props}> + <BooleanField {...props} /> + </DataTable.Col> + ), representation: props => - `<DataTable.Col source="${props.source}" field={BooleanField} />`, + `<DataTable.Col source="${props.source}" /> + <BooleanField source="${props.source}" /> + </DataTable.Col>`, }, date: { - component: props => <DataTable.Col {...props} field={DateField} />, + component: props => ( + <DataTable.Col {...props}> + <DateField {...props} /> + </DataTable.Col> + ), representation: props => - `<DataTable.Col source="${props.source}" field={DateField} />`, + `<DataTable.Col source="${props.source}" /> + <DateField source="${props.source}" /> + </DataTable.Col>`, }, email: { - component: props => <DataTable.Col {...props} field={EmailField} />, + component: props => ( + <DataTable.Col {...props}> + <EmailField {...props} /> + </DataTable.Col> + ), representation: props => - `<DataTable.Col source="${props.source}" field={EmailField} />`, + `<DataTable.Col source="${props.source}" /> + <EmailField source="${props.source}" /> + </DataTable.Col>`, }, id: { component: props => <DataTable.Col {...props} />, @@ -80,7 +100,9 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </DataTable.Col> ), representation: props => - `<DataTable.Col source="${props.source}"><ReferenceField source="${props.source}" reference="${props.reference}" /></DataTable.Col>`, + `<DataTable.Col source="${props.source}"> + <ReferenceField source="${props.source}" reference="${props.reference}" /> + </DataTable.Col>`, }, referenceChild: { component: () => <TextField source="id" />, @@ -93,7 +115,9 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </DataTable.Col> ), representation: props => - `<DataTable.Col source="${props.source}"><ReferenceArrayField source="${props.source}" reference="${props.reference}" /></DataTable.Col>`, + `<DataTable.Col source="${props.source}"> + <ReferenceArrayField source="${props.source}" reference="${props.reference}" /> + </DataTable.Col>`, }, referenceArrayChild: { component: () => ( @@ -102,7 +126,9 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </SingleFieldList> ), representation: () => - `<SingleFieldList><ChipField source="id" /></SingleFieldList>`, + `<SingleFieldList> + <ChipField source="id" /> + </SingleFieldList>`, }, richText: undefined, // never display a rich text field in a datagrid string: { @@ -110,8 +136,14 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} representation: props => `<DataTable.Col source="${props.source}" />`, }, url: { - component: props => <DataTable.Col {...props} field={UrlField} />, + component: props => ( + <DataTable.Col {...props}> + <UrlField {...props} /> + </DataTable.Col> + ), representation: props => - `<DataTable.Col source="${props.source}" field={UrlField} />`, + `<DataTable.Col source="${props.source}" /> + <UrlField source="${props.source}" /> + </DataTable.Col>`, }, }; From 46dd322aeb82712f13c0ef68ba9d3e2e64c9341f Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 10:57:39 +0200 Subject: [PATCH 08/20] fix representation --- packages/ra-ui-materialui/src/list/listFieldTypes.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx index 670ed800e7a..dcb4745ff10 100644 --- a/packages/ra-ui-materialui/src/list/listFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/list/listFieldTypes.tsx @@ -58,7 +58,7 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </DataTable.Col> ), representation: props => - `<DataTable.Col source="${props.source}" /> + `<DataTable.Col source="${props.source}"> <BooleanField source="${props.source}" /> </DataTable.Col>`, }, @@ -69,7 +69,7 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </DataTable.Col> ), representation: props => - `<DataTable.Col source="${props.source}" /> + `<DataTable.Col source="${props.source}"> <DateField source="${props.source}" /> </DataTable.Col>`, }, @@ -80,7 +80,7 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </DataTable.Col> ), representation: props => - `<DataTable.Col source="${props.source}" /> + `<DataTable.Col source="${props.source}"> <EmailField source="${props.source}" /> </DataTable.Col>`, }, @@ -142,7 +142,7 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </DataTable.Col> ), representation: props => - `<DataTable.Col source="${props.source}" /> + `<DataTable.Col source="${props.source}"> <UrlField source="${props.source}" /> </DataTable.Col>`, }, From 51dc3cf55947781bd20e98afc8a7be6b85f2b1ea Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 10:57:55 +0200 Subject: [PATCH 09/20] adapt tests to this new representation --- .../src/list/ListGuesser.spec.tsx | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx index 0df39ca51ef..7fb035e51b6 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx @@ -7,10 +7,10 @@ describe('<ListGuesser />', () => { it('should log the guessed List views based on the fetched records', async () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); render(<ManyResources />); - await screen.findAllByText('top seller'); + await screen.findAllByText('top seller', undefined, { timeout: 2000 }); expect(logSpy).toHaveBeenCalledWith(`Guessed List: -import { DataTable, DataTable.Col, DataTable.NumberCol, List, ReferenceArrayField, ReferenceField } from 'react-admin'; +import { DataTable, DataTable.Col, DataTable.NumberCol, DateField, EmailField, List, ReferenceArrayField, ReferenceField } from 'react-admin'; export const ProductList = () => ( <List> @@ -18,10 +18,18 @@ export const ProductList = () => ( <DataTable.Col source="id" /> <DataTable.Col source="name" /> <DataTable.NumberCol source="price" /> - <DataTable.Col source="category_id"><ReferenceField source="category_id" reference="categories" /></DataTable.Col> - <DataTable.Col source="tags_ids"><ReferenceArrayField source="tags_ids" reference="tags" /></DataTable.Col> - <DataTable.Col source="last_update" field={DateField} /> - <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="category_id"> + <ReferenceField source="category_id" reference="categories" /> + </DataTable.Col> + <DataTable.Col source="tags_ids"> + <ReferenceArrayField source="tags_ids" reference="tags" /> + </DataTable.Col> + <DataTable.Col source="last_update"> + <DateField source="last_update" /> + </DataTable.Col> + <DataTable.Col source="email"> + <EmailField source="email" /> + </DataTable.Col> </DataTable> </List> );`); @@ -31,15 +39,23 @@ export const ProductList = () => ( await screen.findByText('Jeans'); expect(logSpy).toHaveBeenCalledWith(`Guessed List: -import { ArrayField, ChipField, DataTable, DataTable.Col, List, SingleFieldList } from 'react-admin'; +import { ArrayField, BooleanField, ChipField, DataTable, DataTable.Col, List, SingleFieldList } from 'react-admin'; export const CategoryList = () => ( <List> <DataTable> <DataTable.Col source="id" /> <DataTable.Col source="name" /> - <DataTable.Col source="alternativeName"><ArrayField source="alternativeName"><SingleFieldList><ChipField source="name" /></SingleFieldList></ArrayField></DataTable.Col> - <DataTable.Col source="isVeganProduction" field={BooleanField} /> + <DataTable.Col source="alternativeName"> + <ArrayField source="alternativeName"> + <SingleFieldList> + <ChipField source="name" /> + </SingleFieldList> + </ArrayField> + </DataTable.Col> + <DataTable.Col source="isVeganProduction"> + <BooleanField source="isVeganProduction" /> + </DataTable.Col> </DataTable> </List> );`); @@ -49,14 +65,16 @@ export const CategoryList = () => ( await screen.findByText('top seller'); expect(logSpy).toHaveBeenCalledWith(`Guessed List: -import { DataTable, DataTable.Col, List } from 'react-admin'; +import { DataTable, DataTable.Col, List, UrlField } from 'react-admin'; export const TagList = () => ( <List> <DataTable> <DataTable.Col source="id" /> <DataTable.Col source="name" /> - <DataTable.Col source="url" field={UrlField} /> + <DataTable.Col source="url"> + <UrlField source="url" /> + </DataTable.Col> </DataTable> </List> );`); From 6479209afdd890cb071c1d1855e816d26d29bb0b Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 11:03:28 +0200 Subject: [PATCH 10/20] Do not import `DataTable.Col` or `DataTable.NumberCol` --- packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx | 6 +++--- packages/ra-ui-materialui/src/list/ListGuesser.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx index 7fb035e51b6..0c8f0fcd04c 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx @@ -10,7 +10,7 @@ describe('<ListGuesser />', () => { await screen.findAllByText('top seller', undefined, { timeout: 2000 }); expect(logSpy).toHaveBeenCalledWith(`Guessed List: -import { DataTable, DataTable.Col, DataTable.NumberCol, DateField, EmailField, List, ReferenceArrayField, ReferenceField } from 'react-admin'; +import { DataTable, DateField, EmailField, List, ReferenceArrayField, ReferenceField } from 'react-admin'; export const ProductList = () => ( <List> @@ -39,7 +39,7 @@ export const ProductList = () => ( await screen.findByText('Jeans'); expect(logSpy).toHaveBeenCalledWith(`Guessed List: -import { ArrayField, BooleanField, ChipField, DataTable, DataTable.Col, List, SingleFieldList } from 'react-admin'; +import { ArrayField, BooleanField, ChipField, DataTable, List, SingleFieldList } from 'react-admin'; export const CategoryList = () => ( <List> @@ -65,7 +65,7 @@ export const CategoryList = () => ( await screen.findByText('top seller'); expect(logSpy).toHaveBeenCalledWith(`Guessed List: -import { DataTable, DataTable.Col, List, UrlField } from 'react-admin'; +import { DataTable, List, UrlField } from 'react-admin'; export const TagList = () => ( <List> diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.tsx index fb57f87c7c1..bb69d4e718c 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.tsx @@ -132,10 +132,15 @@ const ListViewGuesser = ( .sort(); if (enableLog) { + const importsToLog = components.includes('DataTable') + ? components.filter( + component => !component.startsWith('DataTable.') + ) + : components; console.log( `Guessed List: -import { ${components.join(', ')} } from 'react-admin'; +import { ${importsToLog.join(', ')} } from 'react-admin'; export const ${capitalize(singularize(resource))}List = () => ( <List> From f1d9dab7759b0a0b3236a42c4ef66cc783408664 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 11:50:55 +0200 Subject: [PATCH 11/20] use DataTable in Tutorial instead of Datagrid --- docs/Tutorial.md | 231 +++++++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 107 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 4b45b42960f..8cba51b5a80 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -26,9 +26,9 @@ The final result is a web application that allows you to list, create, edit, and React-admin is built on React. To start, we'll use [create-react-admin](./CreateReactAdmin.md) to bootstrap a new web application: ```sh -npm create react-admin@latest test-admin +npm create react-admin@latest test-admin -- --interactive # or -yarn create react-admin@latest test-admin +yarn create react-admin test-admin --interactive ``` When prompted, choose **JSON Server** as the data provider, then **None** as the auth provider. Do not add any resources for now and press **Enter**. Next, choose either `npm` or `yarn` and press **Enter**. Once everything is installed, run the following commands: @@ -168,20 +168,20 @@ Copy this code and create a new `UserList` component in a new file called `users ```tsx // in src/users.tsx -import { List, Datagrid, TextField, EmailField } from "react-admin"; +import { List, DataTable, EmailField } from "react-admin"; export const UserList = () => ( <List> - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> - <TextField source="username" /> - <EmailField source="email" /> - <TextField source="address.street" /> - <TextField source="phone" /> - <TextField source="website" /> - <TextField source="company.name" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.Col source="username" /> + <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="address.street" /> + <DataTable.Col source="phone" /> + <DataTable.Col source="website" /> + <DataTable.Col source="company.name" /> + </DataTable> </List> ); ``` @@ -214,16 +214,16 @@ Let's take a closer look at the `<UserList>` component: ```tsx export const UserList = () => ( <List> - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> - <TextField source="username" /> - <EmailField source="email" /> - <TextField source="address.street" /> - <TextField source="phone" /> - <TextField source="website" /> - <TextField source="company.name" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.Col source="username" /> + <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="address.street" /> + <DataTable.Col source="phone" /> + <DataTable.Col source="website" /> + <DataTable.Col source="company.name" /> + </DataTable> </List> ); ``` @@ -238,7 +238,7 @@ The root component, [`<List>`](./List.md), reads the query parameters, fetches d This demonstrates the goal of react-admin: helping developers build sophisticated applications with simple syntax. -In most frameworks, "simple" often implies limited capabilities, making it challenging to extend beyond basic features. React-admin addresses this through *composition*. `<List>` handles data fetching, while rendering is delegated to its child—in this case, [`<Datagrid>`](./Datagrid.md). Essentially, the code composes the functionalities of `<List>` and `<Datagrid>` functionalities. +In most frameworks, "simple" often implies limited capabilities, making it challenging to extend beyond basic features. React-admin addresses this through *composition*. `<List>` handles data fetching, while rendering is delegated to its child—in this case, [`<DataTable>`](./DataTable.md). Essentially, the code composes the functionalities of `<List>` and `<DataTable>` functionalities. This means we can compose `<List>` with another component - for instance [`<SimpleList>`](./SimpleList.md): @@ -275,12 +275,12 @@ React-admin's layout is responsive by default. Try resizing your browser, and yo Your browser does not support the video tag. </video> -However, `<SimpleList>` has low information density on desktop. Let's modify `<UserList>` to use `<Datagrid>` on larger screens and `<SimpleList>` on smaller screens. We can achieve this using [Material UI's `useMediaQuery` hook](https://mui.com/material-ui/react-use-media-query/): +However, `<SimpleList>` has low information density on desktop. Let's modify `<UserList>` to use `<DataTable>` on larger screens and `<SimpleList>` on smaller screens. We can achieve this using [Material UI's `useMediaQuery` hook](https://mui.com/material-ui/react-use-media-query/): ```tsx // in src/users.tsx import { useMediaQuery, Theme } from "@mui/material"; -import { List, SimpleList, Datagrid, TextField, EmailField } from "react-admin"; +import { List, SimpleList, DataTable, EmailField } from "react-admin"; export const UserList = () => { const isSmall = useMediaQuery<Theme>((theme) => theme.breakpoints.down("sm")); @@ -293,16 +293,16 @@ export const UserList = () => { tertiaryText={(record) => record.email} /> ) : ( - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> - <TextField source="username" /> - <EmailField source="email" /> - <TextField source="address.street" /> - <TextField source="phone" /> - <TextField source="website" /> - <TextField source="company.name" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.Col source="username" /> + <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="address.street" /> + <DataTable.Col source="phone" /> + <DataTable.Col source="website" /> + <DataTable.Col source="company.name" /> + </DataTable> )} </List> ); @@ -321,22 +321,25 @@ The `<List>` component's child can be anything—even a custom component with it ## Selecting Columns -Let's get back to `<Datagrid>`. It reads the data fetched by `<List>`, then renders a table with one row for each record. `<Datagrid>` uses its child components (here, a list of [Field component](./Fields.md)) to render the columns. Each Field component renders one field of the current record, specified by the `source` prop. +Let's get back to `<DataTable>`. +It reads the data fetched by `<List>`, then renders a table with one row for each record. `<DataTable>` uses its child components (here, a list of [Field component](./Fields.md)) to render the columns. +Each `<DataTable.Col>` component renders one field of the current record, specified by the `source` prop. -`<ListGuesser>` created one column for every field in the API response. That's a bit too much for a usable grid, so let's remove a couple of `<TextField>` components from the Datagrid and see the effect: +`<ListGuesser>` created one column for every field in the API response. +That's a bit too much for a usable grid, so let's remove a couple of `<DataTable.Col>` components from the DataTable and see the effect: ```diff // in src/users.tsx - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> -- <TextField source="username" /> - <EmailField source="email" /> -- <TextField source="address.street" /> - <TextField source="phone" /> - <TextField source="website" /> - <TextField source="company.name" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> +- <DataTable.Col source="username" /> + <DataTable.Col source="email" field={EmailField} /> +- <DataTable.Col source="address.street" /> + <DataTable.Col source="phone" /> + <DataTable.Col source="website" /> + <DataTable.Col source="company.name" /> + </DataTable> ``` [](./img/tutorial_users_list_selected_columns.png) @@ -345,24 +348,25 @@ In react-admin, most configuration is done through components. Instead of using ## Using Field Types -So far, you've used [`<TextField>`](./TextField.md) and [`<EmailField>`](./EmailField.md). React-admin provides [many more Field components](./Fields.md) to handle different data types—numbers, dates, images, arrays, and more. +So far, you've used simples [`<DataTable.Col>`](.//DataTable.md#datatablecol) and [`EmailField`](./EmailField.md) as [a `DataTable.Col` `field`](./DataTable.md#field). +React-admin provides [many more Field components](./Fields.md) to handle different data types—numbers, dates, images, arrays, and more. For instance, instead of displaying the `website` field as plain text, you could make it a clickable link using [`<UrlField>`](./UrlField.md): ```diff // in src/users.tsx --import { List, SimpleList, Datagrid, TextField, EmailField } from "react-admin"; -+import { List, SimpleList, Datagrid, TextField, EmailField, UrlField } from "react-admin"; +-import { List, SimpleList, DataTable, EmailField } from "react-admin"; ++import { List, SimpleList, DataTable, EmailField, UrlField } from "react-admin"; // ... - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> - <EmailField source="email" /> - <TextField source="phone" /> -- <TextField source="website" /> -+ <UrlField source="website" /> - <TextField source="company.name" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="phone" /> +- <DataTable.Col source="website" /> ++ <DataTable.Col source="website" field={UrlField} /> + <DataTable.Col source="company.name" /> + </DataTable> ``` [](./img/tutorial_url_field.png) @@ -371,9 +375,11 @@ This is typical of the early stages of development with react-admin: use a guess ## Writing A Custom Field -In react-admin, fields are just React components. When rendered, they grab the `record` fetched from the API (e.g. `{ "id": 2, "name": "Ervin Howell", "website": "anastasia.net", ... }`) using a custom hook, and use the `source` prop (e.g. `website`) to get the value they should display (e.g. "anastasia.net"). +In react-admin, fields are just React components. +When rendered, they grab the `record` fetched from the API (e.g. `{ "id": 2, "name": "Ervin Howell", "website": "anastasia.net", ... }`) using a custom hook, and use the `source` prop (e.g. `website`) to get the value they should display (e.g. "anastasia.net"). -That means you can do the same to [write a custom field](./Fields.md#writing-your-own-field-component). For instance, here is a simplified version of the `<UrlField>`: +That means you can do the same to [write a custom field](./Fields.md#writing-your-own-field-component). +For instance, here is a simplified version of the `<UrlField>`: ```tsx // in src/MyUrlField.tsx @@ -388,25 +394,27 @@ const MyUrlField = ({ source }: { source: string }) => { export default MyUrlField; ``` -For each row, `<Datagrid>` creates a `RecordContext` and stores the current record in it. [`useRecordContext`](./useRecordContext.md) allows you to read that record. It's one of the 50+ headless hooks that react-admin exposes to let you build your own components without forcing a particular UI. +For each row, `<DataTable>` creates a `RecordContext` and stores the current record in it. +[`useRecordContext`](./useRecordContext.md) allows you to read that record. +It's one of the 50+ headless hooks that react-admin exposes to let you build your own components without forcing a particular UI. You can use the `<MyUrlField>` component in `<UserList>` instead of react-admin's `<UrlField>` component, and it will work just the same. ```diff // in src/users.tsx --import { List, SimpleList, Datagrid, TextField, EmailField, UrlField } from "react-admin"; -+import { List, SimpleList, Datagrid, TextField, EmailField } from "react-admin"; +-import { List, SimpleList, DataTable, EmailField, UrlField } from "react-admin"; ++import { List, SimpleList, DataTable, EmailField } from "react-admin"; +import MyUrlField from './MyUrlField'; // ... - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> - <EmailField source="email" /> - <TextField source="phone" /> -- <UrlField source="website" /> -+ <MyUrlField source="website" /> - <TextField source="company.name" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id" /> + <DataTable.Col source="name" /> + <DataTable.Col source="email" source={EmailField} /> + <DataTable.Col source="phone" /> +- <DataTable.Col source="website" source={UrlField} /> ++ <DataTable.Col source="website" source={MyUrlField} /> + <DataTable.Col source="company.name" /> + </DataTable> ``` This means react-admin never blocks you: if one react-admin component doesn't perfectly suit your needs, you can just swap it with your own version. @@ -478,20 +486,23 @@ export const App = () => ( [](./img/tutorial_guessed_post_list.png) -The `ListGuesser` suggests using a [`<ReferenceField>`](./ReferenceField.md) for the `userId` field. Let's play with this new field by creating the `PostList` component based on the code dumped by the guesser: +The `ListGuesser` suggests using a [`<ReferenceField>`](./ReferenceField.md) for the `userId` field. +Let's play with this new field by creating the `PostList` component based on the code dumped by the guesser: ```tsx // in src/posts.tsx -import { List, Datagrid, TextField, ReferenceField } from "react-admin"; +import { List, DataTable, ReferenceField } from "react-admin"; export const PostList = () => ( <List> - <Datagrid> - <ReferenceField source="userId" reference="users" /> - <TextField source="id" /> - <TextField source="title" /> - <TextField source="body" /> - </Datagrid> + <DataTable> + <DataTable.Col source="userId"> + <ReferenceField source="userId" reference="users" /> + </DataTable.Col> + <DataTable.Col source="id" /> + <DataTable.Col source="title" /> + <DataTable.Col source="body" /> + </DataTable> </List> ); ``` @@ -521,26 +532,32 @@ When displaying the posts list, react-admin is smart enough to display the `name The `<ReferenceField>` component fetches the reference data, creates a `RecordContext` with the result, and renders the record representation (or its children). -**Tip**: Look at the network tab of your browser again: react-admin deduplicates requests for users and aggregates them in order to make only *one* HTTP request to the `/users` endpoint for the whole Datagrid. That's one of many optimizations that keep the UI fast and responsive. +**Tip**: Look at the network tab of your browser again: react-admin deduplicates requests for users and aggregates them in order to make only *one* HTTP request to the `/users` endpoint for the whole DataTable. That's one of many optimizations that keep the UI fast and responsive. -To finish the post list, place the post `id` field as the first column, and remove the `body` field. From a UX point of view, fields containing large chunks of text should not appear in a Datagrid, only in detail views. Also, to make the Edit action stand out, let's replace the default `rowClick` action with an explicit action button: +To finish the post list, place the post `id` field as the first column, and remove the `body` field. +From a UX point of view, fields containing large chunks of text should not appear in a DataTable, only in detail views. +Also, to make the Edit action stand out, let's replace the default `rowClick` action with an explicit action button: ```diff // in src/posts.tsx --import { List, Datagrid, TextField, ReferenceField } from "react-admin"; -+import { List, Datagrid, TextField, ReferenceField, EditButton } from "react-admin"; +-import { List, DataTable, ReferenceField } from "react-admin"; ++import { List, DataTable, ReferenceField, EditButton } from "react-admin"; export const PostList = () => ( <List> -- <Datagrid> -+ <Datagrid rowClick={false}> -+ <TextField source="id" /> - <ReferenceField source="userId" reference="users" /> -- <TextField source="id" /> - <TextField source="title" /> -- <TextField source="body" /> -+ <EditButton /> - </Datagrid> +- <DataTable> ++ <DataTable rowClick={false}> ++ <DataTable.Col source="id" /> + <DataTable.Col source="userId"> + <ReferenceField source="userId" reference="users" /> + </DataTable.Col source="userId"> +- <DataTable.Col source="id" /> + <DataTable.Col source="title" /> +- <DataTable.Col source="body" /> ++ <DataTable.Col> ++ <EditButton /> ++ </DataTable.Col> + </DataTable> </List> ); ``` @@ -583,13 +600,15 @@ Now that the `users` resource has a `show` view, you can also link to it from th // in src/posts.tsx export const PostList = () => ( <List> - <Datagrid> -- <ReferenceField source="userId" reference="users" /> -+ <ReferenceField source="userId" reference="users" link="show" /> - <TextField source="id" /> - <TextField source="title" /> - <TextField source="body" /> - </Datagrid> + <DataTable> + <DataTable.Col> +- <ReferenceField source="userId" reference="users" /> ++ <ReferenceField source="userId" reference="users" link="show" /> + </DataTable.Col> + <DataTable.Col source="id" /> + <DataTable.Col source="title" /> + <DataTable.Col source="body" /> + </DataTable> </List> ); ``` @@ -633,8 +652,7 @@ Copy the `<PostEdit>` code dumped by the guesser in the console to the `posts.ts // in src/posts.tsx import { List, - Datagrid, - TextField, + DataTable, ReferenceField, EditButton, Edit, @@ -699,7 +717,7 @@ export const PostEdit = () => ( ``` {% endraw %} -If you've understood the `<List>` component, the `<Edit>` component will be no surprise. It's responsible for fetching the record and displaying the page title. It passes the record down to the [`<SimpleForm>`](./SimpleForm.md) component, which is responsible for the form layout, default values, and validation. Just like `<Datagrid>`, `<SimpleForm>` uses its children to determine the form inputs to display. It expects [*input components*](./Inputs.md) as children. [`<TextInput>`](./TextInput.md) and [`<ReferenceInput>`](./ReferenceInput.md) are such inputs. +If you've understood the `<List>` component, the `<Edit>` component will be no surprise. It's responsible for fetching the record and displaying the page title. It passes the record down to the [`<SimpleForm>`](./SimpleForm.md) component, which is responsible for the form layout, default values, and validation. Just like `<DataTable>`, `<SimpleForm>` uses its children to determine the form inputs to display. It expects [*input components*](./Inputs.md) as children. [`<TextInput>`](./TextInput.md) and [`<ReferenceInput>`](./ReferenceInput.md) are such inputs. The `<ReferenceInput>` takes the same props as the `<ReferenceField>` (used earlier in the `<PostList>` page). `<ReferenceInput>` uses these props to fetch the API for possible references related to the current record (in this case, possible `users` for the current `post`). It then creates a context with the possible choices and renders an [`<AutocompleteInput>`](./AutocompleteInput.md), which is responsible for displaying the choices and letting the user select one. @@ -711,8 +729,7 @@ Let's allow users to create posts, too. Copy the `<PostEdit>` component into a ` // in src/posts.tsx import { List, - Datagrid, - TextField, + DataTable, ReferenceField, EditButton, Edit, From 2431f0f79b1b74ccb946270ac7e90867796c1551 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 14:55:06 +0200 Subject: [PATCH 12/20] make representation prettier --- packages/ra-ui-materialui/src/detail/showFieldTypes.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx b/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx index 7e39fdc5996..2ad60289748 100644 --- a/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx @@ -39,9 +39,11 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </ArrayField> ), representation: (props: InputProps, children: InferredElement[]) => - `<ArrayField source="${props.source}"><Datagrid>${children - .map(child => child.getRepresentation()) - .join('\n')}</Datagrid></ArrayField>`, + `<ArrayField source="${props.source}"> + <Datagrid> + ${children.map(child => child.getRepresentation()).join('\n ')} + </Datagrid> + </ArrayField>`, }, boolean: { component: BooleanField, From d09f190b9d0a392bfc84b299c38844531c4d530c Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 15:00:43 +0200 Subject: [PATCH 13/20] Add story + adapt test --- .../src/detail/ShowGuesser.spec.tsx | 58 ++++++------------- .../src/detail/ShowGuesser.stories.tsx | 48 +++++++++++++++ .../src/list/ListGuesser.stories.tsx | 2 +- 3 files changed, 67 insertions(+), 41 deletions(-) create mode 100644 packages/ra-ui-materialui/src/detail/ShowGuesser.stories.tsx diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx b/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx index 1cfbd6a606e..b23bcc43179 100644 --- a/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx @@ -1,60 +1,38 @@ import * as React from 'react'; import expect from 'expect'; -import { render, screen, waitFor } from '@testing-library/react'; -import { CoreAdminContext } from 'ra-core'; +import { render, screen } from '@testing-library/react'; -import { ShowGuesser } from './ShowGuesser'; -import { ThemeProvider } from '../theme/ThemeProvider'; +import { ShowGuesser } from './ShowGuesser.stories'; describe('<ShowGuesser />', () => { it('should log the guessed Show view based on the fetched record', async () => { - const logSpy = jest - .spyOn(console, 'log') - .mockImplementation(console.warn); - const dataProvider = { - getOne: () => - Promise.resolve({ - data: { - id: 123, - authors: [ - { id: 1, name: 'john doe', dob: '1990-01-01' }, - { id: 2, name: 'jane doe', dob: '1992-01-01' }, - ], - post_id: 6, - score: 3, - body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.", - created_at: new Date('2012-08-02'), - tags_ids: [1, 2], - }, - }), - getMany: () => Promise.resolve({ data: [] }), - }; - render( - <ThemeProvider> - <CoreAdminContext dataProvider={dataProvider as any}> - <ShowGuesser resource="comments" id={123} enableLog /> - </CoreAdminContext> - </ThemeProvider> - ); - await waitFor(() => { - screen.getByText('john doe'); - }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + render(<ShowGuesser />); + await screen.findByText('john doe'); expect(logSpy).toHaveBeenCalledWith(`Guessed Show: -import { ArrayField, Datagrid, DateField, NumberField, ReferenceArrayField, ReferenceField, Show, SimpleShowLayout, TextField } from 'react-admin'; +import { ArrayField, BooleanField, Datagrid, DateField, EmailField, NumberField, ReferenceArrayField, ReferenceField, RichTextField, Show, SimpleShowLayout, TextField, UrlField } from 'react-admin'; -export const CommentShow = () => ( +export const BookShow = () => ( <Show> <SimpleShowLayout> <TextField source="id" /> - <ArrayField source="authors"><Datagrid><TextField source="id" /> -<TextField source="name" /> -<DateField source="dob" /></Datagrid></ArrayField> + <ArrayField source="authors"> + <Datagrid> + <TextField source="id" /> + <TextField source="name" /> + <DateField source="dob" /> + </Datagrid> + </ArrayField> <ReferenceField source="post_id" reference="posts" /> <NumberField source="score" /> <TextField source="body" /> + <RichTextField source="description" /> <DateField source="created_at" /> <ReferenceArrayField source="tags_ids" reference="tags" /> + <UrlField source="url" /> + <EmailField source="email" /> + <BooleanField source="isAlreadyPublished" /> </SimpleShowLayout> </Show> );`); diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.stories.tsx b/packages/ra-ui-materialui/src/detail/ShowGuesser.stories.tsx new file mode 100644 index 00000000000..80d9bb66be0 --- /dev/null +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.stories.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { Admin } from 'react-admin'; +import { Resource, TestMemoryRouter } from 'ra-core'; +import fakeRestProvider from 'ra-data-fakerest'; + +import { ShowGuesser as RAShowGuesser } from './ShowGuesser'; + +export default { title: 'ra-ui-materialui/detail/ShowGuesser' }; + +const data = { + books: [ + { + id: 123, + authors: [ + { id: 1, name: 'john doe', dob: '1990-01-01' }, + { id: 2, name: 'jane doe', dob: '1992-01-01' }, + ], + post_id: 6, + score: 3, + body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.", + description: `<p><strong>War and Peace</strong> is a novel by the Russian author <a href="https://en.wikipedia.org/wiki/Leo_Tolstoy">Leo Tolstoy</a>, +published serially, then in its entirety in 1869.</p> +<p>It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature.</p>`, + created_at: new Date('2012-08-02'), + tags_ids: [1, 2], + url: 'https://www.myshop.com/tags/top-seller', + email: 'doe@production.com', + isAlreadyPublished: true, + }, + ], + tags: [ + { id: 1, name: 'top seller' }, + { id: 2, name: 'new' }, + ], + posts: [ + { id: 6, title: 'War and Peace', body: 'A great novel by Leo Tolstoy' }, + ], +}; + +const ShowGuesserWithProdLogs = () => <RAShowGuesser enableLog />; + +export const ShowGuesser = () => ( + <TestMemoryRouter initialEntries={['/books/123/show']}> + <Admin dataProvider={fakeRestProvider(data)}> + <Resource name="books" show={ShowGuesserWithProdLogs} /> + </Admin> + </TestMemoryRouter> +); diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx index 2a9172699f1..165844e50e2 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.stories.tsx @@ -190,7 +190,7 @@ const delayedDataProvider = fakeRestProvider( 300 ); -const ListGuesserWithProdLogs = props => <ListGuesser {...props} enableLog />; +const ListGuesserWithProdLogs = () => <ListGuesser enableLog />; export const ManyResources = () => ( <AdminContext From 500fa3a6e0ff12a665e6bc64ec44848b091e4ce9 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 15:59:19 +0200 Subject: [PATCH 14/20] use DataTable in ShowGuesser instead of Datagrid --- .../src/detail/ShowGuesser.tsx | 8 ++++- .../src/detail/showFieldTypes.tsx | 32 ++++++++++++------- .../ra-ui-materialui/src/list/ListGuesser.tsx | 1 + 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx b/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx index e21d81be0cc..d3ecc4fc15a 100644 --- a/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx @@ -72,10 +72,16 @@ const ShowViewGuesser = ( ) .sort(); + const importsToLog = components.includes('DataTable') + ? components.filter( + component => !component.startsWith('DataTable.') + ) + : components; + console.log( `Guessed Show: -import { ${components.join(', ')} } from 'react-admin'; +import { ${importsToLog.join(', ')} } from 'react-admin'; export const ${capitalize(singularize(resource))}Show = () => ( <Show> diff --git a/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx b/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx index 2ad60289748..ee2fd158ab4 100644 --- a/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ReactNode } from 'react'; -import { Datagrid } from '../list/datagrid/Datagrid'; +import type { InferredElement, InferredTypeMap, InputProps } from 'ra-core'; import { ArrayField, BooleanField, @@ -17,8 +17,7 @@ import { ChipField, } from '../field'; import { SimpleShowLayout, SimpleShowLayoutProps } from './SimpleShowLayout'; -import { InferredElement, InferredTypeMap, InputProps } from 'ra-core'; -import { SingleFieldList } from '../list'; +import { DataTable, SingleFieldList } from '../list'; export const showFieldTypes: InferredTypeMap = { show: { @@ -30,19 +29,30 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} </SimpleShowLayout>`, }, array: { - component: ({ - children, - ...props - }: { children: ReactNode } & InputProps) => ( + component: ({ children, ...props }: { children } & InputProps) => ( <ArrayField {...props}> - <Datagrid>{children}</Datagrid> + <DataTable> + {children && children.length > 0 + ? children.map(child => ( + <DataTable.Col source={child.props.source}> + {child} + </DataTable.Col> + )) + : children} + </DataTable> </ArrayField> ), representation: (props: InputProps, children: InferredElement[]) => `<ArrayField source="${props.source}"> - <Datagrid> - ${children.map(child => child.getRepresentation()).join('\n ')} - </Datagrid> + <DataTable> + ${children + .map( + child => `<DataTable.Col source="${child.getProps().source}"> + ${child.getRepresentation()} + </DataTable.Col>` + ) + .join('\n ')} + </DataTable> </ArrayField>`, }, boolean: { diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.tsx index bb69d4e718c..14a0b34b9cf 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.tsx @@ -137,6 +137,7 @@ const ListViewGuesser = ( component => !component.startsWith('DataTable.') ) : components; + console.log( `Guessed List: From b3bdd8595cb6b558431c192b4ad908cf8566bd63 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 16:02:24 +0200 Subject: [PATCH 15/20] fix missing `key` prop --- packages/ra-ui-materialui/src/detail/showFieldTypes.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx b/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx index ee2fd158ab4..ce894912d73 100644 --- a/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/detail/showFieldTypes.tsx @@ -33,8 +33,8 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} <ArrayField {...props}> <DataTable> {children && children.length > 0 - ? children.map(child => ( - <DataTable.Col source={child.props.source}> + ? children.map((child, index) => ( + <DataTable.Col key={index} {...child.props}> {child} </DataTable.Col> )) From 66ce6d07c872d142c5d12e43833e9a9e3567991f Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Mon, 26 May 2025 16:02:37 +0200 Subject: [PATCH 16/20] adapt test --- .../src/detail/ShowGuesser.spec.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx b/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx index b23bcc43179..ec2d2b6a7b0 100644 --- a/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx @@ -11,18 +11,24 @@ describe('<ShowGuesser />', () => { await screen.findByText('john doe'); expect(logSpy).toHaveBeenCalledWith(`Guessed Show: -import { ArrayField, BooleanField, Datagrid, DateField, EmailField, NumberField, ReferenceArrayField, ReferenceField, RichTextField, Show, SimpleShowLayout, TextField, UrlField } from 'react-admin'; +import { ArrayField, BooleanField, DataTable, DateField, EmailField, NumberField, ReferenceArrayField, ReferenceField, RichTextField, Show, SimpleShowLayout, TextField, UrlField } from 'react-admin'; export const BookShow = () => ( <Show> <SimpleShowLayout> <TextField source="id" /> <ArrayField source="authors"> - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> - <DateField source="dob" /> - </Datagrid> + <DataTable> + <DataTable.Col source="id"> + <TextField source="id" /> + </DataTable.Col> + <DataTable.Col source="name"> + <TextField source="name" /> + </DataTable.Col> + <DataTable.Col source="dob"> + <DateField source="dob" /> + </DataTable.Col> + </DataTable> </ArrayField> <ReferenceField source="post_id" reference="posts" /> <NumberField source="score" /> From a912bca51cd5f497fcb9315d88c27566270d4c0c Mon Sep 17 00:00:00 2001 From: erwanMarmelab <131013150+erwanMarmelab@users.noreply.github.com> Date: Tue, 27 May 2025 18:18:04 +0200 Subject: [PATCH 17/20] Use children instead of field --- docs/Tutorial.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 8cba51b5a80..5a7a89bc092 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -176,7 +176,9 @@ export const UserList = () => ( <DataTable.Col source="id" /> <DataTable.Col source="name" /> <DataTable.Col source="username" /> - <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="email"> + <EmailField source="email" /> + </DataTable.Col> <DataTable.Col source="address.street" /> <DataTable.Col source="phone" /> <DataTable.Col source="website" /> @@ -218,7 +220,9 @@ export const UserList = () => ( <DataTable.Col source="id" /> <DataTable.Col source="name" /> <DataTable.Col source="username" /> - <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="email"> + <EmailField source="email" /> + </DataTable.Col> <DataTable.Col source="address.street" /> <DataTable.Col source="phone" /> <DataTable.Col source="website" /> @@ -297,7 +301,9 @@ export const UserList = () => { <DataTable.Col source="id" /> <DataTable.Col source="name" /> <DataTable.Col source="username" /> - <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="email"> + <EmailField source="email" /> + </DataTable.Col> <DataTable.Col source="address.street" /> <DataTable.Col source="phone" /> <DataTable.Col source="website" /> @@ -334,7 +340,9 @@ That's a bit too much for a usable grid, so let's remove a couple of `<DataTable <DataTable.Col source="id" /> <DataTable.Col source="name" /> - <DataTable.Col source="username" /> - <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="email"> + <EmailField source="email" /> + </DataTable.Col> - <DataTable.Col source="address.street" /> <DataTable.Col source="phone" /> <DataTable.Col source="website" /> @@ -361,10 +369,14 @@ For instance, instead of displaying the `website` field as plain text, you could <DataTable> <DataTable.Col source="id" /> <DataTable.Col source="name" /> - <DataTable.Col source="email" field={EmailField} /> + <DataTable.Col source="email"> + <EmailField source="email" /> + </DataTable.Col> <DataTable.Col source="phone" /> - <DataTable.Col source="website" /> -+ <DataTable.Col source="website" field={UrlField} /> ++ <DataTable.Col source="website"> ++ <UrlField source="website" /> ++ </DataTable.Col> <DataTable.Col source="company.name" /> </DataTable> ``` From c0745dd2261313150546fcc7b767c80edd191c5d Mon Sep 17 00:00:00 2001 From: erwanMarmelab <131013150+erwanMarmelab@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:03:27 +0200 Subject: [PATCH 18/20] Apply suggestions from code review Co-authored-by: Jean-Baptiste Kaiser <jb@marmelab.com> --- docs/Tutorial.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 5a7a89bc092..f55b9d4fa01 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -222,7 +222,7 @@ export const UserList = () => ( <DataTable.Col source="username" /> <DataTable.Col source="email"> <EmailField source="email" /> - </DataTable.Col> + </DataTable.Col> <DataTable.Col source="address.street" /> <DataTable.Col source="phone" /> <DataTable.Col source="website" /> @@ -303,7 +303,7 @@ export const UserList = () => { <DataTable.Col source="username" /> <DataTable.Col source="email"> <EmailField source="email" /> - </DataTable.Col> + </DataTable.Col> <DataTable.Col source="address.street" /> <DataTable.Col source="phone" /> <DataTable.Col source="website" /> @@ -328,7 +328,7 @@ The `<List>` component's child can be anything—even a custom component with it ## Selecting Columns Let's get back to `<DataTable>`. -It reads the data fetched by `<List>`, then renders a table with one row for each record. `<DataTable>` uses its child components (here, a list of [Field component](./Fields.md)) to render the columns. +It reads the data fetched by `<List>`, then renders a table with one row for each record. `<DataTable>` uses its child components (a list of `<DataTable.Col>` components) to render the columns. Each `<DataTable.Col>` component renders one field of the current record, specified by the `source` prop. `<ListGuesser>` created one column for every field in the API response. @@ -356,7 +356,7 @@ In react-admin, most configuration is done through components. Instead of using ## Using Field Types -So far, you've used simples [`<DataTable.Col>`](.//DataTable.md#datatablecol) and [`EmailField`](./EmailField.md) as [a `DataTable.Col` `field`](./DataTable.md#field). +So far, you've used [`<DataTable.Col>`](./DataTable.md#datatablecol) directly and [`EmailField`](./EmailField.md) as [a `<DataTable.Col>` child](./DataTable.md#children-1). React-admin provides [many more Field components](./Fields.md) to handle different data types—numbers, dates, images, arrays, and more. For instance, instead of displaying the `website` field as plain text, you could make it a clickable link using [`<UrlField>`](./UrlField.md): From 85ed6e92169d2a95e288638de7ebe98f2f738c66 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Tue, 3 Jun 2025 15:24:23 +0200 Subject: [PATCH 19/20] improve and fix the tutorial --- docs/Tutorial.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index f55b9d4fa01..351ce5832b3 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -358,6 +358,7 @@ In react-admin, most configuration is done through components. Instead of using So far, you've used [`<DataTable.Col>`](./DataTable.md#datatablecol) directly and [`EmailField`](./EmailField.md) as [a `<DataTable.Col>` child](./DataTable.md#children-1). React-admin provides [many more Field components](./Fields.md) to handle different data types—numbers, dates, images, arrays, and more. +You can directly specify a field in your `DataTable.Col` using [the `field` prop](./DataTable.md#field), which is useful when no custom props are needed for that field. For instance, instead of displaying the `website` field as plain text, you could make it a clickable link using [`<UrlField>`](./UrlField.md): @@ -374,9 +375,7 @@ For instance, instead of displaying the `website` field as plain text, you could </DataTable.Col> <DataTable.Col source="phone" /> - <DataTable.Col source="website" /> -+ <DataTable.Col source="website"> -+ <UrlField source="website" /> -+ </DataTable.Col> ++ <DataTable.Col source="website" field={UrlField} /> <DataTable.Col source="company.name" /> </DataTable> ``` @@ -421,10 +420,12 @@ You can use the `<MyUrlField>` component in `<UserList>` instead of react-admin' <DataTable> <DataTable.Col source="id" /> <DataTable.Col source="name" /> - <DataTable.Col source="email" source={EmailField} /> + <DataTable.Col source="email"> + <EmailField source="email" /> + </DataTable.Col> <DataTable.Col source="phone" /> -- <DataTable.Col source="website" source={UrlField} /> -+ <DataTable.Col source="website" source={MyUrlField} /> +- <DataTable.Col source="website" field={UrlField} /> ++ <DataTable.Col source="website" field={MyUrlField} /> <DataTable.Col source="company.name" /> </DataTable> ``` From e4695e992bf9954f06f74ee0f4ee8a7587000d81 Mon Sep 17 00:00:00 2001 From: erwanMarmelab <erwan@marmelab.com> Date: Tue, 3 Jun 2025 15:38:17 +0200 Subject: [PATCH 20/20] Match `Xxx.Xxx` instead of starting with `DataTable.` --- packages/ra-ui-materialui/src/detail/ShowGuesser.tsx | 9 ++------- packages/ra-ui-materialui/src/list/ListGuesser.tsx | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx b/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx index d3ecc4fc15a..c2304283b56 100644 --- a/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.tsx @@ -70,18 +70,13 @@ const ShowViewGuesser = ( ) ) ) + .filter(component => !component.match(/[A-Za-z]+\.[A-Za-z]+/i)) .sort(); - const importsToLog = components.includes('DataTable') - ? components.filter( - component => !component.startsWith('DataTable.') - ) - : components; - console.log( `Guessed Show: -import { ${importsToLog.join(', ')} } from 'react-admin'; +import { ${components.join(', ')} } from 'react-admin'; export const ${capitalize(singularize(resource))}Show = () => ( <Show> diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.tsx index 14a0b34b9cf..42f1f56e802 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.tsx @@ -129,19 +129,14 @@ const ListViewGuesser = ( ) ) ) + .filter(component => !component.match(/[A-Za-z]+\.[A-Za-z]+/i)) .sort(); if (enableLog) { - const importsToLog = components.includes('DataTable') - ? components.filter( - component => !component.startsWith('DataTable.') - ) - : components; - console.log( `Guessed List: -import { ${importsToLog.join(', ')} } from 'react-admin'; +import { ${components.join(', ')} } from 'react-admin'; export const ${capitalize(singularize(resource))}List = () => ( <List>