Skip to content

Commit

Permalink
Merge pull request #43 from vivid-planet/export-contact-lists
Browse files Browse the repository at this point in the history
Export target group contacts
  • Loading branch information
RainbowBunchie authored May 27, 2024
2 parents 70b1c73 + 64c699f commit cc83a1f
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 5 deletions.
17 changes: 17 additions & 0 deletions .changeset/forty-pillows-hug.md
Original file line number Diff line number Diff line change
@@ -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,
+ },
});
```
4 changes: 4 additions & 0 deletions demo/admin/src/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export const getBrevoContactConfig = (
fragment: DocumentNode;
name: string;
};
exportFields: {
renderValue: (row: GQLBrevoContactAttributesFragmentFragment) => string;
headerName: string;
}[];
} => {
return {
additionalGridFields: [
Expand All @@ -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" }),
},
],
};
};
2 changes: 2 additions & 0 deletions packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
100 changes: 98 additions & 2 deletions packages/admin/src/targetGroups/TargetGroupsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -27,11 +29,18 @@ import {
GQLCreateTargetGroupMutationVariables,
GQLDeleteTargetGroupMutation,
GQLDeleteTargetGroupMutationVariables,
GQLTargetGroupContactItemFragment,
GQLTargetGroupContactsQuery,
GQLTargetGroupContactsQueryVariables,
GQLTargetGroupsGridQuery,
GQLTargetGroupsGridQueryVariables,
GQLTargetGroupsListFragment,
} from "./TargetGroupsGrid.generated";

export type AdditionalContactAttributesType = Record<string, unknown>;

type ContactWithAdditionalAttributes = GQLTargetGroupContactItemFragment & AdditionalContactAttributesType;

const targetGroupsFragment = gql`
fragment TargetGroupsList on TargetGroup {
id
Expand All @@ -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
Expand Down Expand Up @@ -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<GQLTargetGroupContactsQuery, GQLTargetGroupContactsQueryVariables>({
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<GQLTargetGroupsListFragment>[] = [
{ field: "title", headerName: intl.formatMessage({ id: "cometBrevoModule.targetGroup.title", defaultMessage: "Title" }), flex: 1 },
{
Expand Down Expand Up @@ -131,6 +224,9 @@ export function TargetGroupsGrid({ scope }: { scope: ContentScopeInterface }): R
<IconButton component={StackLink} pageName="edit" payload={row.id}>
<Edit color="primary" />
</IconButton>
<IconButton onClick={() => downloadTargetGroupContactsExportFile({ id: row.id, title: row.title })}>
<Download color="primary" />
</IconButton>
<CrudContextMenu
copyData={() => {
return {
Expand Down
16 changes: 13 additions & 3 deletions packages/admin/src/targetGroups/TargetGroupsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -29,7 +39,7 @@ export function createTargetGroupsPage({ scopeParts, additionalFormFields, nodeF
<Stack topLevelTitle={intl.formatMessage({ id: "cometBrevoModule.targetGroups.targetGroups", defaultMessage: "Target groups" })}>
<StackSwitch>
<StackPage name="grid">
<TargetGroupsGrid scope={scope} />
<TargetGroupsGrid scope={scope} exportTargetGroupOptions={exportTargetGroupOptions} />
</StackPage>
<StackPage
name="edit"
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit cc83a1f

Please sign in to comment.