From 4ce56793497f91c977c3f68ca6e0a70112ff2b80 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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('', () => { 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('', () => { tags_ids: [1, 2], }, }), + getMany: () => Promise.resolve({ data: [] }), }; render( @@ -35,13 +41,15 @@ describe('', () => { }); 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 = () => ( - + + + From b8c394034672df0cbb37a0eb04ab54690eafc0c2 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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('', () => { 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 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 based on the result of the + * List component rendering a based on the result of the * dataProvider.getList() call. * * The result (choice and type of columns) isn't configurable, but the - * outputs the it has guessed to the console so that + * outputs the it has guessed to the console so that * developers can start from there. * * To be used as the list prop of a . 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 ; + return ; }, - representation: (_props, children) => ` + representation: (_props, children) => ` ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} - `, + `, }, array: { component: ({ children, ...props }: ArrayFieldProps) => { const childrenArray = React.Children.toArray(children); return ( - - - 0 && - React.isValidElement(childrenArray[0]) && - childrenArray[0].props.source - } - /> - - + + + + 0 && + React.isValidElement(childrenArray[0]) && + childrenArray[0].props.source + } + /> + + + ); }, representation: (props, children) => - ``, + }" />`, }, boolean: { - component: BooleanField, - representation: props => ``, + component: props => , + representation: props => + ``, }, date: { - component: DateField, - representation: props => ``, + component: props => , + representation: props => + ``, }, email: { - component: EmailField, - representation: props => ``, + component: props => , + representation: props => + ``, }, id: { - component: TextField, - representation: props => ``, + component: props => , + representation: props => ``, }, number: { - component: NumberField, - representation: props => ``, + component: DataTable.NumberCol, + representation: props => + ``, }, reference: { - component: ReferenceField, + component: props => ( + + + + ), representation: props => - ``, + ``, }, referenceChild: { - component: () => , - representation: () => ``, + component: () => , + representation: () => ``, }, referenceArray: { - component: ReferenceArrayField, + component: props => ( + + + + ), representation: props => - ``, + ``, }, referenceArrayChild: { component: () => ( - - - + + + + + ), representation: () => - ``, + ``, }, richText: undefined, // never display a rich text field in a datagrid string: { - component: TextField, - representation: props => ``, + component: DataTable.Col, + representation: props => ``, }, url: { - component: UrlField, - representation: props => ``, + component: props => , + representation: props => + ``, }, }; From 92b29679d0037c9dc80f2d784595629418aded89 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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')} ``, }, referenceChild: { - component: () => , - representation: () => ``, + component: () => , + representation: () => ``, }, referenceArray: { component: props => ( @@ -96,14 +97,12 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} }, referenceArrayChild: { component: () => ( - - - - - + + + ), representation: () => - ``, + ``, }, richText: undefined, // never display a rich text field in a datagrid string: { From ed24389de68c18540cbc77f5c73f40db1c33cdeb Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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 => ; + export const ManyResources = () => ( ( From 96899d312d346c53ff9dfc025ab4e7a012270987 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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('', () => { - 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( - - - - - - ); - await waitFor(() => { - screen.getByText('john doe'); - }); + render(); + 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 = () => ( + + + + + + + + + + + +);`); + 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 = () => ( + + + + + + + + +);`); + + 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 = () => ( - - - - - - - - - + + + + + );`); }); From ea937454ab0b0f5d781661162933c2bcf5bba064 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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) => - ``, + ` + + + + + + `, }, boolean: { - component: props => , + component: props => ( + + + + ), representation: props => - ``, + ` + + `, }, date: { - component: props => , + component: props => ( + + + + ), representation: props => - ``, + ` + + `, }, email: { - component: props => , + component: props => ( + + + + ), representation: props => - ``, + ` + + `, }, id: { component: props => , @@ -80,7 +100,9 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} ), representation: props => - ``, + ` + + `, }, referenceChild: { component: () => , @@ -93,7 +115,9 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} ), representation: props => - ``, + ` + + `, }, referenceArrayChild: { component: () => ( @@ -102,7 +126,9 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} ), representation: () => - ``, + ` + + `, }, 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 => ``, }, url: { - component: props => , + component: props => ( + + + + ), representation: props => - ``, + ` + + `, }, }; From 46dd322aeb82712f13c0ef68ba9d3e2e64c9341f Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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')} ), representation: props => - ` + ` `, }, @@ -69,7 +69,7 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} ), representation: props => - ` + ` `, }, @@ -80,7 +80,7 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} ), representation: props => - ` + ` `, }, @@ -142,7 +142,7 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')} ), representation: props => - ` + ` `, }, From 51dc3cf55947781bd20e98afc8a7be6b85f2b1ea Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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('', () => { it('should log the guessed List views based on the fetched records', async () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); render(); - 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 = () => ( @@ -18,10 +18,18 @@ export const ProductList = () => ( - - - - + + + + + + + + + + + + );`); @@ -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 = () => ( - - + + + + + + + + + + );`); @@ -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 = () => ( - + + + );`); From 6479209afdd890cb071c1d1855e816d26d29bb0b Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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('', () => { 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 = () => ( @@ -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 = () => ( @@ -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 = () => ( 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 = () => ( From f1d9dab7759b0a0b3236a42c4ef66cc783408664 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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 = () => ( - - - - - - - - - - + + + + + + + + + + ); ``` @@ -214,16 +214,16 @@ Let's take a closer look at the `` component: ```tsx export const UserList = () => ( - - - - - - - - - - + + + + + + + + + + ); ``` @@ -238,7 +238,7 @@ The root component, [``](./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*. `` handles data fetching, while rendering is delegated to its child—in this case, [``](./Datagrid.md). Essentially, the code composes the functionalities of `` and `` functionalities. +In most frameworks, "simple" often implies limited capabilities, making it challenging to extend beyond basic features. React-admin addresses this through *composition*. `` handles data fetching, while rendering is delegated to its child—in this case, [``](./DataTable.md). Essentially, the code composes the functionalities of `` and `` functionalities. This means we can compose `` with another component - for instance [``](./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. -However, `` has low information density on desktop. Let's modify `` to use `` on larger screens and `` on smaller screens. We can achieve this using [Material UI's `useMediaQuery` hook](https://mui.com/material-ui/react-use-media-query/): +However, `` has low information density on desktop. Let's modify `` to use `` on larger screens and `` 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.breakpoints.down("sm")); @@ -293,16 +293,16 @@ export const UserList = () => { tertiaryText={(record) => record.email} /> ) : ( - - - - - - - - - - + + + + + + + + + + )} ); @@ -321,22 +321,25 @@ The `` component's child can be anything—even a custom component with it ## Selecting Columns -Let's get back to ``. It reads the data fetched by ``, then renders a table with one row for each record. `` 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 ``. +It reads the data fetched by ``, then renders a table with one row for each record. `` uses its child components (here, a list of [Field component](./Fields.md)) to render the columns. +Each `` component renders one field of the current record, specified by the `source` prop. -`` 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 `` components from the Datagrid and see the effect: +`` 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 `` components from the DataTable and see the effect: ```diff // in src/users.tsx - - - -- - -- - - - - + + + +- + +- + + + + ``` [![Users List](./img/tutorial_users_list_selected_columns.png)](./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.md) and [``](./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.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.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"; // ... - - - - - -- -+ - - + + + + + +- ++ + + ``` [![Url Field](./img/tutorial_url_field.png)](./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 ``: +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 ``: ```tsx // in src/MyUrlField.tsx @@ -388,25 +394,27 @@ const MyUrlField = ({ source }: { source: string }) => { export default MyUrlField; ``` -For each row, `` 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, `` 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 `` component in `` instead of react-admin's `` 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'; // ... - - - - - -- -+ - - + + + + + +- ++ + + ``` 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 = () => ( [![Guessed Post List](./img/tutorial_guessed_post_list.png)](./img/tutorial_guessed_post_list.png) -The `ListGuesser` suggests using a [``](./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.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 = () => ( - - - - - - + + + + + + + + ); ``` @@ -521,26 +532,32 @@ When displaying the posts list, react-admin is smart enough to display the `name The `` 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 = () => ( -- -+ -+ - -- - -- -+ - +- ++ ++ + + + +- + +- ++ ++ ++ + ); ``` @@ -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 = () => ( - -- -+ - - - - + + +- ++ + + + + + ); ``` @@ -633,8 +652,7 @@ Copy the `` 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 `` component, the `` 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.md) component, which is responsible for the form layout, default values, and validation. Just like ``, `` uses its children to determine the form inputs to display. It expects [*input components*](./Inputs.md) as children. [``](./TextInput.md) and [``](./ReferenceInput.md) are such inputs. +If you've understood the `` component, the `` 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.md) component, which is responsible for the form layout, default values, and validation. Just like ``, `` uses its children to determine the form inputs to display. It expects [*input components*](./Inputs.md) as children. [``](./TextInput.md) and [``](./ReferenceInput.md) are such inputs. The `` takes the same props as the `` (used earlier in the `` page). `` 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.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 `` 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 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')} ), representation: (props: InputProps, children: InferredElement[]) => - `${children - .map(child => child.getRepresentation()) - .join('\n')}`, + ` + + ${children.map(child => child.getRepresentation()).join('\n ')} + + `, }, boolean: { component: BooleanField, From d09f190b9d0a392bfc84b299c38844531c4d530c Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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('', () => { 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( - - - - - - ); - await waitFor(() => { - screen.getByText('john doe'); - }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + render(); + 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 = () => ( - - - + + + + + + + + + + + );`); 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: `

War and Peace is a novel by the Russian author Leo Tolstoy, +published serially, then in its entirety in 1869.

+

It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature.

`, + 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 = () => ; + +export const ShowGuesser = () => ( + + + + + +); 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 => ; +const ListGuesserWithProdLogs = () => ; export const ManyResources = () => ( 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 = () => ( 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')}
`, }, array: { - component: ({ - children, - ...props - }: { children: ReactNode } & InputProps) => ( + component: ({ children, ...props }: { children } & InputProps) => ( - {children} + + {children && children.length > 0 + ? children.map(child => ( + + {child} + + )) + : children} + ), representation: (props: InputProps, children: InferredElement[]) => ` - - ${children.map(child => child.getRepresentation()).join('\n ')} - + + ${children + .map( + child => ` + ${child.getRepresentation()} + ` + ) + .join('\n ')} + `, }, 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 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')} {children && children.length > 0 - ? children.map(child => ( - + ? children.map((child, index) => ( + {child} )) From 66ce6d07c872d142c5d12e43833e9a9e3567991f Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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('', () => { 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 = () => ( - - - - - + + + + + + + + + + + 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 = () => ( - + + + @@ -218,7 +220,9 @@ export const UserList = () => ( - + + + @@ -297,7 +301,9 @@ export const UserList = () => { - + + + @@ -334,7 +340,9 @@ That's a bit too much for a usable grid, so let's remove a couple of ` - - + + + - @@ -361,10 +369,14 @@ For instance, instead of displaying the `website` field as plain text, you could - + + + - -+ ++ ++ ++ ``` 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 --- 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 = () => ( - + @@ -303,7 +303,7 @@ export const UserList = () => { - + @@ -328,7 +328,7 @@ The `` component's child can be anything—even a custom component with it ## Selecting Columns Let's get back to ``. -It reads the data fetched by ``, then renders a table with one row for each record. `` uses its child components (here, a list of [Field component](./Fields.md)) to render the columns. +It reads the data fetched by ``, then renders a table with one row for each record. `` uses its child components (a list of `` components) to render the columns. Each `` component renders one field of the current record, specified by the `source` prop. `` 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.md#datatablecol) and [`EmailField`](./EmailField.md) as [a `DataTable.Col` `field`](./DataTable.md#field). +So far, you've used [``](./DataTable.md#datatablecol) directly and [`EmailField`](./EmailField.md) as [a `` 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.md): From 85ed6e92169d2a95e288638de7ebe98f2f738c66 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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.md#datatablecol) directly and [`EmailField`](./EmailField.md) as [a `` 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.md): @@ -374,9 +375,7 @@ For instance, instead of displaying the `website` field as plain text, you could - -+ -+ -+ ++ ``` @@ -421,10 +420,12 @@ You can use the `` component in `` instead of react-admin' - + + + -- -+ +- ++ ``` From e4695e992bf9954f06f74ee0f4ee8a7587000d81 Mon Sep 17 00:00:00 2001 From: erwanMarmelab 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 = () => ( 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 = () => (