From 50a620aae017e7489ea040a6295f3a46a36d8722 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Fri, 24 May 2024 11:15:37 +0200 Subject: [PATCH 1/3] Install file saver for csv export --- packages/admin/package.json | 2 ++ pnpm-lock.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/admin/package.json b/packages/admin/package.json index 07da2050..302576c4 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -27,6 +27,7 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { + "file-saver": "^2.0.5", "lodash.isequal": "^4.0.0" }, "devDependencies": { @@ -48,6 +49,7 @@ "@mui/styles": "^5.8.6", "@mui/system": "^5.8.6", "@mui/x-data-grid": "^5.17.26", + "@types/file-saver": "^2.0.7", "@types/lodash.isequal": "^4.0.0", "@types/react": "^17.0", "@types/react-dom": "^17.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8394d0dd..a89169b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -801,6 +801,9 @@ importers: packages/admin: dependencies: + file-saver: + specifier: ^2.0.5 + version: 2.0.5 lodash.isequal: specifier: ^4.0.0 version: 4.5.0 @@ -859,6 +862,9 @@ importers: '@mui/x-data-grid': specifier: ^5.17.26 version: 5.17.26(@mui/material@5.15.5)(@mui/system@5.15.5)(@types/react@17.0.75)(react-dom@17.0.2)(react@17.0.2) + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 '@types/lodash.isequal': specifier: ^4.0.0 version: 4.5.8 @@ -8705,6 +8711,10 @@ packages: '@types/qs': 6.9.11 '@types/serve-static': 1.15.5 + /@types/file-saver@2.0.7: + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + dev: true + /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: From 274cc77f4b8e4432f687cb8a8a7c801af872b820 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Fri, 24 May 2024 12:28:45 +0200 Subject: [PATCH 2/3] Add export of contacts to target group grid rows --- .changeset/forty-pillows-hug.md | 17 +++ demo/admin/src/Routes.tsx | 4 + .../brevoContactsPageAttributesConfig.ts | 14 +++ .../src/targetGroups/TargetGroupsGrid.tsx | 100 +++++++++++++++++- .../src/targetGroups/TargetGroupsPage.tsx | 16 ++- 5 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 .changeset/forty-pillows-hug.md diff --git a/.changeset/forty-pillows-hug.md b/.changeset/forty-pillows-hug.md new file mode 100644 index 00000000..9495a238 --- /dev/null +++ b/.changeset/forty-pillows-hug.md @@ -0,0 +1,17 @@ +--- +"@comet/brevo-admin": minor +--- + +Add a download button in the target group grid to download a list of contacts as csv file. + +It is possible to configure additional contact attributes for the export in the `createTargetGroupsPage. + +```diff +createTargetGroupsPage({ + // .... ++ exportTargetGroupOptions: { ++ additionalAttributesFragment: brevoContactConfig.additionalAttributesFragment, ++ exportFields: brevoContactConfig.exportFields, ++ }, +}); +``` diff --git a/demo/admin/src/Routes.tsx b/demo/admin/src/Routes.tsx index bcb1db6b..b6dda603 100644 --- a/demo/admin/src/Routes.tsx +++ b/demo/admin/src/Routes.tsx @@ -36,6 +36,10 @@ export const Routes: React.FC = () => { const TargetGroupsPage = createTargetGroupsPage({ scopeParts: ["domain", "language"], additionalFormFields: additionalFormConfig.additionalFormFields, + exportTargetGroupOptions: { + additionalAttributesFragment: brevoContactConfig.additionalAttributesFragment, + exportFields: brevoContactConfig.exportFields, + }, nodeFragment: additionalFormConfig.nodeFragment, input2State: additionalFormConfig.input2State, }); diff --git a/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.ts b/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.ts index 59edabbc..a41e5c89 100644 --- a/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.ts +++ b/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.ts @@ -22,6 +22,10 @@ export const getBrevoContactConfig = ( fragment: DocumentNode; name: string; }; + exportFields: { + renderValue: (row: GQLBrevoContactAttributesFragmentFragment) => string; + headerName: string; + }[]; } => { return { additionalGridFields: [ @@ -46,5 +50,15 @@ export const getBrevoContactConfig = ( fragment: attributesFragment, name: "BrevoContactAttributesFragment", }, + exportFields: [ + { + renderValue: (row: GQLBrevoContactAttributesFragmentFragment) => row.attributes?.FIRSTNAME, + headerName: intl.formatMessage({ id: "brevoContact.firstName", defaultMessage: "First name" }), + }, + { + renderValue: (row: GQLBrevoContactAttributesFragmentFragment) => row.attributes?.LASTNAME, + headerName: intl.formatMessage({ id: "brevoContact.lastName", defaultMessage: "Last name" }), + }, + ], }; }; diff --git a/packages/admin/src/targetGroups/TargetGroupsGrid.tsx b/packages/admin/src/targetGroups/TargetGroupsGrid.tsx index 6886cd82..74dda0cd 100644 --- a/packages/admin/src/targetGroups/TargetGroupsGrid.tsx +++ b/packages/admin/src/targetGroups/TargetGroupsGrid.tsx @@ -15,10 +15,12 @@ import { useDataGridRemote, usePersistentColumnState, } from "@comet/admin"; -import { Add as AddIcon, Edit } from "@comet/admin-icons"; +import { Add as AddIcon, Download, Edit } from "@comet/admin-icons"; import { ContentScopeInterface } from "@comet/cms-admin"; import { Button, IconButton } from "@mui/material"; import { DataGrid, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid"; +import saveAs from "file-saver"; +import { DocumentNode } from "graphql"; import * as React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -27,11 +29,18 @@ import { GQLCreateTargetGroupMutationVariables, GQLDeleteTargetGroupMutation, GQLDeleteTargetGroupMutationVariables, + GQLTargetGroupContactItemFragment, + GQLTargetGroupContactsQuery, + GQLTargetGroupContactsQueryVariables, GQLTargetGroupsGridQuery, GQLTargetGroupsGridQueryVariables, GQLTargetGroupsListFragment, } from "./TargetGroupsGrid.generated"; +export type AdditionalContactAttributesType = Record; + +type ContactWithAdditionalAttributes = GQLTargetGroupContactItemFragment & AdditionalContactAttributesType; + const targetGroupsFragment = gql` fragment TargetGroupsList on TargetGroup { id @@ -42,6 +51,15 @@ const targetGroupsFragment = gql` } `; +const targetGroupContactItemFragment = gql` + fragment TargetGroupContactItem on BrevoContact { + id + email + emailBlacklisted + smsBlacklisted + } +`; + const targetGroupsQuery = gql` query TargetGroupsGrid( $offset: Int @@ -95,11 +113,86 @@ function TargetGroupsGridToolbar() { ); } -export function TargetGroupsGrid({ scope }: { scope: ContentScopeInterface }): React.ReactElement { +export function TargetGroupsGrid({ + scope, + exportTargetGroupOptions, +}: { + scope: ContentScopeInterface; + exportTargetGroupOptions?: { + additionalAttributesFragment: { name: string; fragment: DocumentNode }; + exportFields: { renderValue: (row: AdditionalContactAttributesType) => string; headerName: string }[]; + }; +}): React.ReactElement { const client = useApolloClient(); const intl = useIntl(); const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("TargetGroupsGrid") }; + const targetGroupContactsQuery = gql` + query TargetGroupContacts($targetGroupId: ID, $offset: Int, $limit: Int, $scope: EmailCampaignContentScopeInput!) { + brevoContacts(targetGroupId: $targetGroupId, offset: $offset, limit: $limit, scope: $scope) { + nodes { + ...TargetGroupContactItem + ${ + exportTargetGroupOptions?.additionalAttributesFragment + ? "...".concat(exportTargetGroupOptions.additionalAttributesFragment?.name) + : "" + } + } + totalCount + } + } + ${targetGroupContactItemFragment} + ${exportTargetGroupOptions?.additionalAttributesFragment?.fragment ?? ""} + `; + + const convertToCsv = (data: ContactWithAdditionalAttributes[]) => { + const header = [ + intl.formatMessage({ id: "cometBrevoModule.targetGroup.export.brevoId", defaultMessage: "Brevo ID" }), + intl.formatMessage({ id: "cometBrevoModule.targetGroup.export.email", defaultMessage: "Email" }), + intl.formatMessage({ id: "cometBrevoModule.targetGroup.export.emailBlacklisted", defaultMessage: "Email blacklisted" }), + intl.formatMessage({ id: "cometBrevoModule.targetGroup.export.smsBlacklisted", defaultMessage: "Sms blacklisted" }), + ].concat(exportTargetGroupOptions?.exportFields.map((field) => field?.headerName ?? "") ?? []); + + const csvData = data.map((contact) => [ + `="${contact.id}"`, + contact.email, + contact.emailBlacklisted, + contact.smsBlacklisted, + exportTargetGroupOptions?.exportFields.map((field) => field.renderValue(contact)), + ]); + + csvData.unshift(header); + + return csvData.map((row) => row.join(",")).join("\n"); + }; + + async function downloadTargetGroupContactsExportFile({ id, title }: { id: string; title: string }) { + let offset = 0; + let shouldContinue = true; + let allContacts: ContactWithAdditionalAttributes[] = []; + + while (shouldContinue) { + const { data: newContactsData } = await client.query({ + query: targetGroupContactsQuery, + variables: { + targetGroupId: id, + scope: scope, + offset: offset, + limit: 100, + }, + }); + + allContacts = allContacts.concat(newContactsData.brevoContacts.nodes as ContactWithAdditionalAttributes[]); + shouldContinue = allContacts.length < newContactsData.brevoContacts.totalCount; + offset += 100; + } + + const csvData = convertToCsv(allContacts); + + const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" }); + saveAs(blob, `${title}.csv`); + } + const columns: GridColDef[] = [ { field: "title", headerName: intl.formatMessage({ id: "cometBrevoModule.targetGroup.title", defaultMessage: "Title" }), flex: 1 }, { @@ -131,6 +224,9 @@ export function TargetGroupsGrid({ scope }: { scope: ContentScopeInterface }): R + downloadTargetGroupContactsExportFile({ id: row.id, title: row.title })}> + + { return { diff --git a/packages/admin/src/targetGroups/TargetGroupsPage.tsx b/packages/admin/src/targetGroups/TargetGroupsPage.tsx index c459c7a2..4cff2e18 100644 --- a/packages/admin/src/targetGroups/TargetGroupsPage.tsx +++ b/packages/admin/src/targetGroups/TargetGroupsPage.tsx @@ -5,17 +5,27 @@ import * as React from "react"; import { useIntl } from "react-intl"; import { EditTargetGroupFinalFormValues, TargetGroupForm } from "./TargetGroupForm"; -import { TargetGroupsGrid } from "./TargetGroupsGrid"; +import { AdditionalContactAttributesType, TargetGroupsGrid } from "./TargetGroupsGrid"; interface CreateContactsPageOptions { scopeParts: string[]; additionalFormFields?: React.ReactNode; + exportTargetGroupOptions?: { + additionalAttributesFragment: { name: string; fragment: DocumentNode }; + exportFields: { renderValue: (row: AdditionalContactAttributesType) => string; headerName: string }[]; + }; nodeFragment?: { name: string; fragment: DocumentNode }; input2State?: (values?: EditTargetGroupFinalFormValues) => EditTargetGroupFinalFormValues; valuesToOutput?: (values: EditTargetGroupFinalFormValues) => EditTargetGroupFinalFormValues; } -export function createTargetGroupsPage({ scopeParts, additionalFormFields, nodeFragment, input2State }: CreateContactsPageOptions) { +export function createTargetGroupsPage({ + scopeParts, + additionalFormFields, + nodeFragment, + input2State, + exportTargetGroupOptions, +}: CreateContactsPageOptions) { function TargetGroupsPage(): JSX.Element { const { scope: completeScope } = useContentScope(); const intl = useIntl(); @@ -29,7 +39,7 @@ export function createTargetGroupsPage({ scopeParts, additionalFormFields, nodeF - + Date: Mon, 27 May 2024 15:01:03 +0200 Subject: [PATCH 3/3] Add missing quotation in changeset --- .changeset/forty-pillows-hug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/forty-pillows-hug.md b/.changeset/forty-pillows-hug.md index 9495a238..40e8d4ec 100644 --- a/.changeset/forty-pillows-hug.md +++ b/.changeset/forty-pillows-hug.md @@ -4,7 +4,7 @@ Add a download button in the target group grid to download a list of contacts as csv file. -It is possible to configure additional contact attributes for the export in the `createTargetGroupsPage. +It is possible to configure additional contact attributes for the export in the `createTargetGroupsPage`. ```diff createTargetGroupsPage({