From 7f544798dc86bc16e6fbc6ede44310d53eae3d0a Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 1 Jul 2024 14:42:26 +0200 Subject: [PATCH 1/6] [TypeScript] Improve List exporter type --- examples/crm/package.json | 1 + examples/crm/src/contacts/ContactList.tsx | 63 ++++++++++++++++--- examples/simple/src/comments/CommentList.tsx | 3 +- .../ra-core/src/export/fetchRelatedRecords.ts | 10 ++- packages/ra-core/src/types.ts | 12 ++-- yarn.lock | 10 +++ 6 files changed, 81 insertions(+), 18 deletions(-) diff --git a/examples/crm/package.json b/examples/crm/package.json index fb82ed971d2..416cfe6957d 100644 --- a/examples/crm/package.json +++ b/examples/crm/package.json @@ -27,6 +27,7 @@ "@testing-library/user-event": "^14.5.2", "@types/faker": "^5.1.7", "@types/jest": "^29.5.2", + "@types/jsonexport": "^3.0.5", "@types/lodash": "~4.14.168", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/examples/crm/src/contacts/ContactList.tsx b/examples/crm/src/contacts/ContactList.tsx index 5b0ece6c472..0ea32e8bc25 100644 --- a/examples/crm/src/contacts/ContactList.tsx +++ b/examples/crm/src/contacts/ContactList.tsx @@ -1,21 +1,24 @@ /* eslint-disable import/no-anonymous-default-export */ import * as React from 'react'; import { + BulkActionsToolbar, + BulkDeleteButton, + CreateButton, + downloadCSV, + ExportButton, List as RaList, - SimpleListLoading, + Pagination, + RecordContextProvider, ReferenceField, - TextField, - useListContext, - ExportButton, + SimpleListLoading, SortButton, + TextField, TopToolbar, - CreateButton, - Pagination, useGetIdentity, - BulkActionsToolbar, - BulkDeleteButton, - RecordContextProvider, + useListContext, + number, } from 'react-admin'; +import type { FetchRelatedRecords, DataProvider } from 'react-admin'; import { List, ListItem, @@ -28,12 +31,13 @@ import { } from '@mui/material'; import { Link } from 'react-router-dom'; import { formatDistance } from 'date-fns'; +import jsonExport from 'jsonexport/dist'; import { Avatar } from './Avatar'; import { Status } from '../misc/Status'; import { TagsList } from './TagsList'; import { ContactListFilter } from './ContactListFilter'; -import { Contact } from '../types'; +import { Contact, Company, Sale, Tag } from '../types'; const ContactListContent = () => { const { @@ -140,6 +144,44 @@ const ContactListActions = () => ( ); +const exporter = async ( + records: Contact[], + fetchRelatedRecords: FetchRelatedRecords, + dataProvider: DataProvider +) => { + const companies = await fetchRelatedRecords( + records, + 'company_id', + 'companies' + ); + const sales = await fetchRelatedRecords(records, 'sales_id', 'sales'); + const tagIds = records.reduce( + (acc, contact) => acc.concat(contact.tags as number[]), + [] + ); + const { data: tags } = await dataProvider.getMany('tags', { + ids: Array.from(new Set(tagIds)), + }); + const tagsById = tags.reduce<{ [key: number]: Tag }>((acc, tag) => { + acc[tag.id as number] = tag; + return acc; + }, {}); + + const contacts = records.map(contact => ({ + ...contact, + company: companies[contact.company_id as number].name, + sales: `${sales[contact.sales_id as number].first_name} ${ + sales[contact.sales_id as number].last_name + }`, + tags: contact.tags + .map(tagId => tagsById[tagId as number].name) + .join(', '), + })); + return jsonExport(contacts, {}, (_err: any, csv: string) => { + downloadCSV(csv, 'contacts'); + }); +}; + export const ContactList = () => { const { identity } = useGetIdentity(); return identity ? ( @@ -150,6 +192,7 @@ export const ContactList = () => { pagination={} filterDefaultValues={{ sales_id: identity?.id }} sort={{ field: 'last_seen', order: 'DESC' }} + exporter={exporter} > diff --git a/examples/simple/src/comments/CommentList.tsx b/examples/simple/src/comments/CommentList.tsx index 2aa7f61cc69..df50f31c395 100644 --- a/examples/simple/src/comments/CommentList.tsx +++ b/examples/simple/src/comments/CommentList.tsx @@ -18,6 +18,7 @@ import { ListActions, DateField, EditButton, + FetchRelatedRecords, Pagination, ReferenceField, ReferenceInput, @@ -36,7 +37,7 @@ const commentFilters = [ , ]; -const exporter = (records, fetchRelatedRecords) => +const exporter = (records, fetchRelatedRecords: FetchRelatedRecords) => fetchRelatedRecords(records, 'post_id', 'posts').then(posts => { const data = records.map(record => { const { author, ...recordForExport } = record; // omit author diff --git a/packages/ra-core/src/export/fetchRelatedRecords.ts b/packages/ra-core/src/export/fetchRelatedRecords.ts index bec29c242ae..f935f4069fd 100644 --- a/packages/ra-core/src/export/fetchRelatedRecords.ts +++ b/packages/ra-core/src/export/fetchRelatedRecords.ts @@ -1,4 +1,9 @@ -import { RaRecord, Identifier, DataProvider } from '../types'; +import { + RaRecord, + Identifier, + DataProvider, + FetchRelatedRecords, +} from '../types'; /** * Helper function for calling the dataProvider.getMany() method, @@ -13,7 +18,8 @@ import { RaRecord, Identifier, DataProvider } from '../types'; * ); */ const fetchRelatedRecords = - (dataProvider: DataProvider) => (data, field, resource) => + (dataProvider: DataProvider): FetchRelatedRecords => + (data, field, resource) => dataProvider .getMany(resource, { ids: getRelatedIds(data, field) }) .then(({ data }) => diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index bfe22acddc4..4e152986916 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -368,15 +368,17 @@ export interface ResourceProps { export type Exporter = ( data: any, - fetchRelatedRecords: ( - data: any, - field: string, - resource: string - ) => Promise, + fetchRelatedRecords: FetchRelatedRecords, dataProvider: DataProvider, resource?: string ) => void | Promise; +export type FetchRelatedRecords = ( + data: any[], + field: string, + resource: string +) => Promise<{ [key: Identifier]: RecordType }>; + export type SetOnSave = ( onSave?: (values: object, redirect: any) => void ) => void; diff --git a/yarn.lock b/yarn.lock index 26f10013cd2..d62fe3d05cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5975,6 +5975,15 @@ __metadata: languageName: node linkType: hard +"@types/jsonexport@npm:^3.0.5": + version: 3.0.5 + resolution: "@types/jsonexport@npm:3.0.5" + dependencies: + "@types/node": "npm:*" + checksum: 5234efbccb2d28632bf5f47272c838d1f011a1e34c9bf36f329edafe17c51b81d12ceb455c919adf7f5c6cd946ba9049c2a9782d7caf7cb0ff60aa0e5e2fa63b + languageName: node + linkType: hard + "@types/lodash@npm:^4.14.167, @types/lodash@npm:^4.14.168, @types/lodash@npm:^4.14.175, @types/lodash@npm:~4.14.168": version: 4.14.194 resolution: "@types/lodash@npm:4.14.194" @@ -18058,6 +18067,7 @@ __metadata: "@testing-library/user-event": "npm:^14.5.2" "@types/faker": "npm:^5.1.7" "@types/jest": "npm:^29.5.2" + "@types/jsonexport": "npm:^3.0.5" "@types/lodash": "npm:~4.14.168" "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18.3.0" From f254f21550392cab67e1d4cca27b276b802387f0 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 1 Jul 2024 14:50:50 +0200 Subject: [PATCH 2/6] Improve documentation --- docs/List.md | 29 ++++++++++++----------- examples/crm/src/contacts/ContactList.tsx | 22 ++++------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/docs/List.md b/docs/List.md index bd2a4a09bdf..616a9caacd6 100644 --- a/docs/List.md +++ b/docs/List.md @@ -607,23 +607,24 @@ In many cases, you'll need more than simple object manipulation. You'll need to Here is an example for a Comments exporter, fetching related Posts: -```jsx +```tsx // in CommentList.js import { List, downloadCSV } from 'react-admin'; +import type { FetchRelatedRecords } from 'react-admin'; import jsonExport from 'jsonexport/dist'; -const exporter = (records, fetchRelatedRecords) => { - // will call dataProvider.getMany('posts', { ids: records.map(record => record.post_id) }), ignoring duplicate and empty post_id - fetchRelatedRecords(records, 'post_id', 'posts').then(posts => { - const data = records.map(record => ({ - ...record, - post_title: posts[record.post_id].title, - })); - return jsonExport(data, { - headers: ['id', 'post_id', 'post_title', 'body'], - }, (err, csv) => { - downloadCSV(csv, 'comments'); - }); +const exporter = async (comments: Comments[], fetchRelatedRecords: FetchRelatedRecords) => { + // will call dataProvider.getMany('posts', { ids: records.map(record => record.post_id) }), + // ignoring duplicate and empty post_id + const posts = await fetchRelatedRecords(comments, 'post_id', 'posts') + const commentsWithPostTitle = comments.map(comment => ({ + ...comment, + post_title: posts[comment.post_id].title, + })); + return jsonExport(commentsWithPostTitle, { + headers: ['id', 'post_id', 'post_title', 'body'], + }, (err, csv) => { + downloadCSV(csv, 'comments'); }); }; @@ -631,7 +632,7 @@ const CommentList = () => ( ... -) +); ``` **Tip**: If you need to call another verb in the exporter, take advantage of the third parameter passed to the function: it's the `dataProvider` function. diff --git a/examples/crm/src/contacts/ContactList.tsx b/examples/crm/src/contacts/ContactList.tsx index 0ea32e8bc25..c881a421d31 100644 --- a/examples/crm/src/contacts/ContactList.tsx +++ b/examples/crm/src/contacts/ContactList.tsx @@ -16,9 +16,8 @@ import { TopToolbar, useGetIdentity, useListContext, - number, } from 'react-admin'; -import type { FetchRelatedRecords, DataProvider } from 'react-admin'; +import type { FetchRelatedRecords } from 'react-admin'; import { List, ListItem, @@ -146,8 +145,7 @@ const ContactListActions = () => ( const exporter = async ( records: Contact[], - fetchRelatedRecords: FetchRelatedRecords, - dataProvider: DataProvider + fetchRelatedRecords: FetchRelatedRecords ) => { const companies = await fetchRelatedRecords( records, @@ -155,17 +153,7 @@ const exporter = async ( 'companies' ); const sales = await fetchRelatedRecords(records, 'sales_id', 'sales'); - const tagIds = records.reduce( - (acc, contact) => acc.concat(contact.tags as number[]), - [] - ); - const { data: tags } = await dataProvider.getMany('tags', { - ids: Array.from(new Set(tagIds)), - }); - const tagsById = tags.reduce<{ [key: number]: Tag }>((acc, tag) => { - acc[tag.id as number] = tag; - return acc; - }, {}); + const tags = await fetchRelatedRecords(records, 'tags', 'tags'); const contacts = records.map(contact => ({ ...contact, @@ -173,9 +161,7 @@ const exporter = async ( sales: `${sales[contact.sales_id as number].first_name} ${ sales[contact.sales_id as number].last_name }`, - tags: contact.tags - .map(tagId => tagsById[tagId as number].name) - .join(', '), + tags: contact.tags.map(tagId => tags[tagId as number].name).join(', '), })); return jsonExport(contacts, {}, (_err: any, csv: string) => { downloadCSV(csv, 'contacts'); From 7231ad8d0181b9dfdb3934892194136a420b898d Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 1 Jul 2024 14:59:17 +0200 Subject: [PATCH 3/6] Add link to example in demo --- docs/Demos.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/Demos.md b/docs/Demos.md index 07580529e0c..846042b9b1f 100644 --- a/docs/Demos.md +++ b/docs/Demos.md @@ -280,13 +280,14 @@ A complete CRM app allowing to manage contacts, companies, deals, notes, tasks, The source shows how to implement the following features: -- [Horizontal navigation](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/Layout.tsx) -- [Trello-like Kanban board for the deals pipeline](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/deals/DealListContent.tsx) -- [Custom d3.js / Nivo Chart in the dashboard](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/dashboard/DealsChart.tsx) -- [Add or remove tags to a contact](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/contacts/TagsListEdit.tsx) -- [Use dataProvider hooks to update notes](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/notes/Note.tsx) -- [Custom grid layout for companies](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/companies/GridList.tsx) -- [Filter by "my favorites" in the company list](https://github.com/marmelab/react-admin/blob/7c60db09aea34a90607a4e7560e9e4b51bd7b9a3/examples/crm/src/deals/OnlyMineInput.tsx) +- [Horizontal navigation](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/Layout.tsx) +- [Custom exporter](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/contacts/ContactList.tsx) +- [Trello-like Kanban board for the deals pipeline](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/deals/DealListContent.tsx) +- [Custom d3.js / Nivo Chart in the dashboard](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/dashboard/DealsChart.tsx) +- [Add or remove tags to a contact](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/contacts/TagsListEdit.tsx) +- [Use dataProvider hooks to update notes](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/notes/Note.tsx) +- [Custom grid layout for companies](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/companies/GridList.tsx) +- [Filter by "my favorites" in the company list](https://github.com/marmelab/react-admin/blob/master/examples/crm/src/deals/OnlyMineInput.tsx) ## Help Desk From bd1bbea8ef9114f973f17d1e2a0462bdc06bcdcb Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 1 Jul 2024 21:48:49 +0200 Subject: [PATCH 4/6] Convert default to named exports --- .../ra-core/src/export/ExporterContext.ts | 6 +-- .../ra-core/src/export/defaultExporter.ts | 6 +-- packages/ra-core/src/export/downloadCSV.ts | 2 +- .../src/export/fetchRelatedRecords.spec.ts | 32 ------------- .../ra-core/src/export/fetchRelatedRecords.ts | 47 ++----------------- .../ra-core/src/export/getRelatedIds.spec.ts | 30 ++++++++++++ packages/ra-core/src/export/getRelatedIds.ts | 36 ++++++++++++++ packages/ra-core/src/export/index.ts | 10 ++-- 8 files changed, 78 insertions(+), 91 deletions(-) delete mode 100644 packages/ra-core/src/export/fetchRelatedRecords.spec.ts create mode 100644 packages/ra-core/src/export/getRelatedIds.spec.ts create mode 100644 packages/ra-core/src/export/getRelatedIds.ts diff --git a/packages/ra-core/src/export/ExporterContext.ts b/packages/ra-core/src/export/ExporterContext.ts index 81048937b0d..55c07293b38 100644 --- a/packages/ra-core/src/export/ExporterContext.ts +++ b/packages/ra-core/src/export/ExporterContext.ts @@ -1,10 +1,8 @@ import { createContext } from 'react'; import { Exporter } from '../types'; -import defaultExporter from './defaultExporter'; +import { defaultExporter } from './defaultExporter'; -const ExporterContext = createContext(defaultExporter); +export const ExporterContext = createContext(defaultExporter); ExporterContext.displayName = 'ExporterContext'; - -export default ExporterContext; diff --git a/packages/ra-core/src/export/defaultExporter.ts b/packages/ra-core/src/export/defaultExporter.ts index 7b8d42697c5..55d0ed4721e 100644 --- a/packages/ra-core/src/export/defaultExporter.ts +++ b/packages/ra-core/src/export/defaultExporter.ts @@ -1,9 +1,7 @@ import jsonExport from 'jsonexport/dist'; -import downloadCSV from './downloadCSV'; +import { downloadCSV } from './downloadCSV'; import { Exporter } from '../types'; -const defaultExporter: Exporter = (data, _, __, resource) => +export const defaultExporter: Exporter = (data, _, __, resource) => jsonExport(data, (err, csv) => downloadCSV(csv, resource)); - -export default defaultExporter; diff --git a/packages/ra-core/src/export/downloadCSV.ts b/packages/ra-core/src/export/downloadCSV.ts index f435ebc836d..bfe5a0c32b6 100644 --- a/packages/ra-core/src/export/downloadCSV.ts +++ b/packages/ra-core/src/export/downloadCSV.ts @@ -1,4 +1,4 @@ -export default (csv: string, filename: string = 'export'): void => { +export const downloadCSV = (csv: string, filename: string = 'export'): void => { const fakeLink = document.createElement('a'); fakeLink.style.display = 'none'; document.body.appendChild(fakeLink); diff --git a/packages/ra-core/src/export/fetchRelatedRecords.spec.ts b/packages/ra-core/src/export/fetchRelatedRecords.spec.ts deleted file mode 100644 index c67064e6b75..00000000000 --- a/packages/ra-core/src/export/fetchRelatedRecords.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import expect from 'expect'; - -import { getRelatedIds } from './fetchRelatedRecords'; - -describe('fetchRelatedRecords', () => { - describe('getRelatedIds', () => { - it('should ignore null or undefined values', () => { - const books = [ - { id: 1, author_id: 123, title: 'Pride and Prejudice' }, - { id: 2, author_id: null }, - { id: 3 }, - ]; - expect(getRelatedIds(books, 'author_id')).toEqual([123]); - }); - it('should aggregate scalar related ids', () => { - const books = [ - { id: 1, author_id: 123, title: 'Pride and Prejudice' }, - { id: 2, author_id: 123, title: 'Sense and Sensibility' }, - { id: 3, author_id: 456, title: 'War and Peace' }, - ]; - expect(getRelatedIds(books, 'author_id')).toEqual([123, 456]); - }); - it('should aggregate arrays of related ids', () => { - const books = [ - { id: 1, tag_ids: [1, 2], title: 'Pride and Prejudice' }, - { id: 2, tag_ids: [2, 3], title: 'Sense and Sensibility' }, - { id: 3, tag_ids: [4], title: 'War and Peace' }, - ]; - expect(getRelatedIds(books, 'tag_ids')).toEqual([1, 2, 3, 4]); - }); - }); -}); diff --git a/packages/ra-core/src/export/fetchRelatedRecords.ts b/packages/ra-core/src/export/fetchRelatedRecords.ts index f935f4069fd..c8792a37116 100644 --- a/packages/ra-core/src/export/fetchRelatedRecords.ts +++ b/packages/ra-core/src/export/fetchRelatedRecords.ts @@ -1,9 +1,5 @@ -import { - RaRecord, - Identifier, - DataProvider, - FetchRelatedRecords, -} from '../types'; +import { DataProvider, FetchRelatedRecords } from '../types'; +import { getRelatedIds } from './getRelatedIds'; /** * Helper function for calling the dataProvider.getMany() method, @@ -17,7 +13,7 @@ import { * })) * ); */ -const fetchRelatedRecords = +export const fetchRelatedRecords = (dataProvider: DataProvider): FetchRelatedRecords => (data, field, resource) => dataProvider @@ -28,40 +24,3 @@ const fetchRelatedRecords = return acc; }, {}) ); - -/** - * Extracts, aggregates and deduplicates the ids of related records - * - * @example - * const books = [ - * { id: 1, author_id: 123, title: 'Pride and Prejudice' }, - * { id: 2, author_id: 123, title: 'Sense and Sensibility' }, - * { id: 3, author_id: 456, title: 'War and Peace' }, - * ]; - * getRelatedIds(books, 'author_id'); => [123, 456] - * - * @example - * const books = [ - * { id: 1, tag_ids: [1, 2], title: 'Pride and Prejudice' }, - * { id: 2, tag_ids: [2, 3], title: 'Sense and Sensibility' }, - * { id: 3, tag_ids: [4], title: 'War and Peace' }, - * ]; - * getRelatedIds(records, 'tag_ids'); => [1, 2, 3, 4] - * - * @param {Object[]} records An array of records - * @param {string} field the identifier of the record field to use - */ -export const getRelatedIds = ( - records: RaRecord[], - field: string -): Identifier[] => - Array.from( - new Set( - records - .filter(record => record[field] != null) - .map(record => record[field]) - .reduce((ids, value) => ids.concat(value), []) - ) - ); - -export default fetchRelatedRecords; diff --git a/packages/ra-core/src/export/getRelatedIds.spec.ts b/packages/ra-core/src/export/getRelatedIds.spec.ts new file mode 100644 index 00000000000..fa024e4eebf --- /dev/null +++ b/packages/ra-core/src/export/getRelatedIds.spec.ts @@ -0,0 +1,30 @@ +import expect from 'expect'; + +import { getRelatedIds } from './getRelatedIds'; + +describe('getRelatedIds', () => { + it('should ignore null or undefined values', () => { + const books = [ + { id: 1, author_id: 123, title: 'Pride and Prejudice' }, + { id: 2, author_id: null }, + { id: 3 }, + ]; + expect(getRelatedIds(books, 'author_id')).toEqual([123]); + }); + it('should aggregate scalar related ids', () => { + const books = [ + { id: 1, author_id: 123, title: 'Pride and Prejudice' }, + { id: 2, author_id: 123, title: 'Sense and Sensibility' }, + { id: 3, author_id: 456, title: 'War and Peace' }, + ]; + expect(getRelatedIds(books, 'author_id')).toEqual([123, 456]); + }); + it('should aggregate arrays of related ids', () => { + const books = [ + { id: 1, tag_ids: [1, 2], title: 'Pride and Prejudice' }, + { id: 2, tag_ids: [2, 3], title: 'Sense and Sensibility' }, + { id: 3, tag_ids: [4], title: 'War and Peace' }, + ]; + expect(getRelatedIds(books, 'tag_ids')).toEqual([1, 2, 3, 4]); + }); +}); diff --git a/packages/ra-core/src/export/getRelatedIds.ts b/packages/ra-core/src/export/getRelatedIds.ts new file mode 100644 index 00000000000..6c1ad95ddf4 --- /dev/null +++ b/packages/ra-core/src/export/getRelatedIds.ts @@ -0,0 +1,36 @@ +import { RaRecord, Identifier } from '../types'; + +/** + * Extracts, aggregates and deduplicates the ids of related records + * + * @example + * const books = [ + * { id: 1, author_id: 123, title: 'Pride and Prejudice' }, + * { id: 2, author_id: 123, title: 'Sense and Sensibility' }, + * { id: 3, author_id: 456, title: 'War and Peace' }, + * ]; + * getRelatedIds(books, 'author_id'); => [123, 456] + * + * @example + * const books = [ + * { id: 1, tag_ids: [1, 2], title: 'Pride and Prejudice' }, + * { id: 2, tag_ids: [2, 3], title: 'Sense and Sensibility' }, + * { id: 3, tag_ids: [4], title: 'War and Peace' }, + * ]; + * getRelatedIds(records, 'tag_ids'); => [1, 2, 3, 4] + * + * @param {Object[]} records An array of records + * @param {string} field the identifier of the record field to use + */ +export const getRelatedIds = ( + records: RaRecord[], + field: string +): Identifier[] => + Array.from( + new Set( + records + .filter(record => record[field] != null) + .map(record => record[field]) + .reduce((ids, value) => ids.concat(value), []) + ) + ); diff --git a/packages/ra-core/src/export/index.ts b/packages/ra-core/src/export/index.ts index 644aabe7086..6a666bfd9ba 100644 --- a/packages/ra-core/src/export/index.ts +++ b/packages/ra-core/src/export/index.ts @@ -1,6 +1,4 @@ -import defaultExporter from './defaultExporter'; -import downloadCSV from './downloadCSV'; -import ExporterContext from './ExporterContext'; -import fetchRelatedRecords from './fetchRelatedRecords'; - -export { defaultExporter, downloadCSV, ExporterContext, fetchRelatedRecords }; +export * from './defaultExporter'; +export * from './downloadCSV'; +export * from './ExporterContext'; +export * from './fetchRelatedRecords'; From 6ade3b90fbd87556caad8a953fee870c24438361 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:44:55 +0200 Subject: [PATCH 5/6] Make Exporter accept a generic parameter --- examples/crm/src/contacts/ContactList.tsx | 9 +++------ examples/simple/src/comments/CommentList.tsx | 4 ++-- .../ra-core/src/controller/list/useListController.ts | 2 +- packages/ra-core/src/types.ts | 4 ++-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/examples/crm/src/contacts/ContactList.tsx b/examples/crm/src/contacts/ContactList.tsx index c881a421d31..702d95256be 100644 --- a/examples/crm/src/contacts/ContactList.tsx +++ b/examples/crm/src/contacts/ContactList.tsx @@ -17,7 +17,7 @@ import { useGetIdentity, useListContext, } from 'react-admin'; -import type { FetchRelatedRecords } from 'react-admin'; +import type { Exporter } from 'react-admin'; import { List, ListItem, @@ -143,10 +143,7 @@ const ContactListActions = () => ( ); -const exporter = async ( - records: Contact[], - fetchRelatedRecords: FetchRelatedRecords -) => { +const exporter: Exporter = async (records, fetchRelatedRecords) => { const companies = await fetchRelatedRecords( records, 'company_id', @@ -171,7 +168,7 @@ const exporter = async ( export const ContactList = () => { const { identity } = useGetIdentity(); return identity ? ( - actions={} aside={} perPage={25} diff --git a/examples/simple/src/comments/CommentList.tsx b/examples/simple/src/comments/CommentList.tsx index df50f31c395..22da00c254a 100644 --- a/examples/simple/src/comments/CommentList.tsx +++ b/examples/simple/src/comments/CommentList.tsx @@ -18,7 +18,6 @@ import { ListActions, DateField, EditButton, - FetchRelatedRecords, Pagination, ReferenceField, ReferenceInput, @@ -30,6 +29,7 @@ import { downloadCSV, useListContext, useTranslate, + Exporter, } from 'react-admin'; // eslint-disable-line import/no-unresolved const commentFilters = [ @@ -37,7 +37,7 @@ const commentFilters = [ , ]; -const exporter = (records, fetchRelatedRecords: FetchRelatedRecords) => +const exporter: Exporter = (records, fetchRelatedRecords) => fetchRelatedRecords(records, 'post_id', 'posts').then(posts => { const data = records.map(record => { const { author, ...recordForExport } = record; // omit author diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index 2f2fdb41130..fbc04826645 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -274,7 +274,7 @@ export interface ListControllerProps { * * ) */ - exporter?: Exporter | false; + exporter?: Exporter | false; /** * Permanent filter applied to all getList queries, regardless of the user selected filters. diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index 4e152986916..994b39fa645 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -366,8 +366,8 @@ export interface ResourceProps { children?: ReactNode; } -export type Exporter = ( - data: any, +export type Exporter = ( + data: RecordType[], fetchRelatedRecords: FetchRelatedRecords, dataProvider: DataProvider, resource?: string From f4c4beeb14836889675fbd7713d07e5c15844f5f Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 2 Jul 2024 09:56:51 +0200 Subject: [PATCH 6/6] Simplify type --- examples/crm/src/contacts/ContactList.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/crm/src/contacts/ContactList.tsx b/examples/crm/src/contacts/ContactList.tsx index 702d95256be..e9b285a437d 100644 --- a/examples/crm/src/contacts/ContactList.tsx +++ b/examples/crm/src/contacts/ContactList.tsx @@ -154,11 +154,11 @@ const exporter: Exporter = async (records, fetchRelatedRecords) => { const contacts = records.map(contact => ({ ...contact, - company: companies[contact.company_id as number].name, - sales: `${sales[contact.sales_id as number].first_name} ${ - sales[contact.sales_id as number].last_name + company: companies[contact.company_id].name, + sales: `${sales[contact.sales_id].first_name} ${ + sales[contact.sales_id].last_name }`, - tags: contact.tags.map(tagId => tags[tagId as number].name).join(', '), + tags: contact.tags.map(tagId => tags[tagId].name).join(', '), })); return jsonExport(contacts, {}, (_err: any, csv: string) => { downloadCSV(csv, 'contacts');