Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(exports)!: export multiple campaigns #1641

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7dea763
feat: wip, routing and boilerplate react component
henryk1229 Jul 13, 2023
2977ef0
feat: wip - add mutation, initial task
henryk1229 Jul 13, 2023
bb4ea78
feat: graphql mutation
henryk1229 Jul 18, 2023
a79b4ef
feat: wip, frontend - use CampaignListMenu to select for export
henryk1229 Jul 18, 2023
3aae8c5
feat(export-multiple-campaigns): simple backend solution
henryk1229 Jul 20, 2023
506b02d
feat(campaignexportmodal): share ModalContent between components
henryk1229 Jul 20, 2023
10a567e
feat(admincampaignlist): show snackbar after kicking off exports
henryk1229 Jul 20, 2023
99abef1
feat(campaign-list-row): add color to export tag
henryk1229 Jul 20, 2023
ddc3d28
feat(export-campaign-snackbar): snackbar component for success and er…
henryk1229 Jul 20, 2023
7f19a3d
feat: handle de-selecting for export in CampaignListMenu
henryk1229 Jul 20, 2023
ee721c1
feat: reset idsForExport on complete
henryk1229 Jul 20, 2023
cea4766
feat(campaign-list): initial card approach, break out components and …
henryk1229 Jul 25, 2023
bef6de3
feat(campaign-list): hook up export campaign button
henryk1229 Jul 25, 2023
4add064
refactor: rm mvp solution
henryk1229 Jul 25, 2023
67ac4d3
feat(admin-campaign-list): styling, search field
henryk1229 Jul 26, 2023
4807092
feat(campaign-list): filter by campaign title
henryk1229 Jul 26, 2023
ebd991a
feat(campaign-export-modal): display camaign details for export, rela…
henryk1229 Jul 27, 2023
d14b20c
feat(campaign-list): improve card styling
henryk1229 Jul 27, 2023
e847c34
feat(campaign-list): use rounded icons, improve export dialog styling
henryk1229 Jul 28, 2023
abb78ff
refactor: require campaign ids, spoke options, rm export type from mu…
henryk1229 Aug 3, 2023
7ef2246
refactor: improve var names
henryk1229 Aug 3, 2023
b5bc27a
feat: improve typing
henryk1229 Aug 3, 2023
45ea951
feat: destructure props in component declaration
henryk1229 Aug 3, 2023
338b169
fix: revert campaign list menu changes
henryk1229 Aug 3, 2023
dd93365
feat(campaign-list-row): improve typing
henryk1229 Aug 3, 2023
b8348d0
refactor(campaign-list-row): styling changes
henryk1229 Aug 4, 2023
16c9467
refactor(multi-export-dialog): styling changes
henryk1229 Aug 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions libs/gql-schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ const rootSchema = `
vanOptions: ExportForVanInput
}

input MultipleCampaignExportInput {
campaignIds: [String!]
henryk1229 marked this conversation as resolved.
Show resolved Hide resolved
exportType: CampaignExportType!
henryk1229 marked this conversation as resolved.
Show resolved Hide resolved
spokeOptions: ExportForSpokeInput
henryk1229 marked this conversation as resolved.
Show resolved Hide resolved
}

input QuestionResponseSyncConfigInput {
id: String!
}
Expand Down Expand Up @@ -287,6 +293,7 @@ const rootSchema = `
copyCampaign(id: String!): Campaign
copyCampaigns(sourceCampaignId: String!, quantity: Int!, targetOrgId: String): [Campaign!]!
exportCampaign(options: CampaignExportInput!): JobRequest
exportCampaigns(options: MultipleCampaignExportInput!): [JobRequest]
createCannedResponse(cannedResponse:CannedResponseInput!): CannedResponse
createOrganization(name: String!, userId: String!, inviteId: String!): Organization
editOrganization(id: String! input: EditOrganizationInput!): Organization!
Expand Down
19 changes: 16 additions & 3 deletions libs/spoke-codegen/src/graphql/campaign-stats.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,22 @@ mutation ExportCampaign($options: CampaignExportInput!) {
}
}

mutation CopyCampaigns($templateId: String!, $quantity: Int!, $targetOrgId: String) {
copyCampaigns(sourceCampaignId: $templateId, quantity: $quantity, targetOrgId: $targetOrgId) {
mutation ExportCampaigns($options: MultipleCampaignExportInput!) {
exportCampaigns(options: $options) {
id
}
}

mutation CopyCampaigns(
$templateId: String!
$quantity: Int!
$targetOrgId: String
) {
copyCampaigns(
sourceCampaignId: $templateId
quantity: $quantity
targetOrgId: $targetOrgId
) {
id
}
}
Expand All @@ -82,7 +96,6 @@ query GetCampaignSyncConfigs($campaignId: String!) {
}
}


query GetSyncTargets($campaignId: String!) {
campaign(id: $campaignId) {
id
Expand Down
28 changes: 28 additions & 0 deletions src/components/ExportCampaignDataSnackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Snackbar from "@material-ui/core/Snackbar";
import Alert from "@material-ui/lab/Alert";
import React from "react";

interface Props {
open: boolean;
errorMessage: string | null;
onClose: () => void;
}

const ExportCampaignDataSnackbar: React.FC<Props> = (props) => {
const { open, errorMessage, onClose } = props;

return errorMessage ? (
<Snackbar open={open} autoHideDuration={5000} onClose={onClose}>
<Alert severity="error">{errorMessage}</Alert>
</Snackbar>
) : (
<Snackbar
open={open}
message="Exports started - we'll e-mail you when they're done"
autoHideDuration={5000}
onClose={onClose}
/>
);
};

export default ExportCampaignDataSnackbar;
144 changes: 144 additions & 0 deletions src/components/ExportMultipleCampaignDataDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import Divider from "@material-ui/core/Divider";
import Typography from "@material-ui/core/Typography";
import AssignmentRoundedIcon from "@material-ui/icons/AssignmentRounded";
import {
CampaignExportType,
useExportCampaignsMutation
} from "@spoke/spoke-codegen";
import React, { useState } from "react";

import { CampaignExportModalContent } from "../containers/AdminCampaignStats/components/CampaignExportModal";

export type CampaignDetailsForExport = {
id: string;
title: string;
};
interface Props {
campaignDetailsForExport: CampaignDetailsForExport[];
open: boolean;
onClose: () => void;
onError: (errorMessage: string) => void;
onComplete(): void;
henryk1229 marked this conversation as resolved.
Show resolved Hide resolved
}

const ExportMultipleCampaignDataDialog: React.FC<Props> = (props) => {
const {
campaignDetailsForExport,
open,
onClose,
onError,
onComplete
} = props;
henryk1229 marked this conversation as resolved.
Show resolved Hide resolved
const [exportCampaign, setExportCampaign] = useState<boolean>(true);
const [exportMessages, setExportMessages] = useState<boolean>(true);
const [exportOptOut, setExportOptOut] = useState<boolean>(false);
const [exportFiltered, setExportFiltered] = useState<boolean>(false);

const [exportCampaignsMutation] = useExportCampaignsMutation();

const handleChange = (setStateFunction: any) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
setStateFunction(event.target.checked);
};

const handleExportClick = async () => {
const campaignIds = campaignDetailsForExport.map(
(campaign: CampaignDetailsForExport) => campaign.id
);
const result = await exportCampaignsMutation({
variables: {
options: {
campaignIds,
exportType: CampaignExportType.Spoke,
spokeOptions: {
campaign: exportCampaign,
messages: exportMessages,
optOuts: exportOptOut,
filteredContacts: exportFiltered
}
}
}
});
if (result.errors) {
const message = result.errors.map((e) => e.message).join(", ");
return onError(message);
}
onComplete();
};

return (
<Dialog
onClose={onClose}
aria-labelledby="export-multiple-campaign-data"
open={open}
fullWidth
maxWidth="sm"
>
<DialogTitle id="export-multiple-campaign-data">
<Typography variant="h6" style={{ margin: "4px", cursor: "pointer" }}>
Export Campaigns
</Typography>
</DialogTitle>
<CampaignExportModalContent
exportCampaign={exportCampaign}
exportMessages={exportMessages}
exportOptOut={exportOptOut}
exportFiltered={exportFiltered}
handleChange={handleChange}
setExportCampaign={setExportCampaign}
setExportMessages={setExportMessages}
setExportOptOut={setExportOptOut}
setExportFiltered={setExportFiltered}
/>
<Divider variant="middle" />
<DialogContent>
<DialogContentText>
<Typography variant="subtitle1" style={{ margin: "4px" }}>
Selected campaigns:
</Typography>
{campaignDetailsForExport.map(
(campaign: CampaignDetailsForExport) => {
return (
<div
key={campaign.id}
style={{
display: "flex",
alignItems: "center",
margin: "4px"
}}
>
<AssignmentRoundedIcon />
<Typography variant="h6" style={{ margin: "4px" }}>
{campaign.title}
</Typography>
<Typography variant="subtitle1" style={{ marginLeft: "8px" }}>
id: {campaign.id}
</Typography>
</div>
);
}
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button
color="primary"
disabled={campaignDetailsForExport.length < 1}
onClick={handleExportClick}
>
Export data
</Button>
</DialogActions>
</Dialog>
);
};

export default ExportMultipleCampaignDataDialog;
Loading
Loading