From 7dea763a8de85d870140351bdaf40dc49bfc3d9f Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 13 Jul 2023 14:55:41 -0400 Subject: [PATCH 01/27] feat: wip, routing and boilerplate react component --- src/containers/AdminDashboard/index.jsx | 5 +++++ src/containers/AdminExportCampaigns/index.tsx | 12 ++++++++++++ .../CampaignList/components/CampaignList.tsx | 3 +++ src/routes.jsx | 5 +++++ 4 files changed, 25 insertions(+) create mode 100644 src/containers/AdminExportCampaigns/index.tsx diff --git a/src/containers/AdminDashboard/index.jsx b/src/containers/AdminDashboard/index.jsx index 703fd319e..b969f32dc 100644 --- a/src/containers/AdminDashboard/index.jsx +++ b/src/containers/AdminDashboard/index.jsx @@ -69,6 +69,11 @@ class AdminDashboard extends React.Component { const sections = [ { name: "Campaigns", path: "campaigns", role: "ADMIN" }, { name: "Template Campaigns", path: "template-campaigns", role: "ADMIN" }, + { + name: "Export Campaigns", + path: "export-campaigns", + role: "ADMIN" + }, { name: "Campaign Groups", path: "campaign-groups", role: "ADMIN" }, { name: "People", path: "people", role: "ADMIN" }, { name: "Teams", path: "teams", role: "ADMIN" }, diff --git a/src/containers/AdminExportCampaigns/index.tsx b/src/containers/AdminExportCampaigns/index.tsx new file mode 100644 index 000000000..dfbf8229f --- /dev/null +++ b/src/containers/AdminExportCampaigns/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { useHistory, useParams } from "react-router-dom"; + +const AdminExportCampaigns: React.FC = () => { + // const classes = useStyles(); + const _history = useHistory(); + const { organizationId: _orgId } = useParams<{ organizationId: string }>(); + + return
TODO
; +}; + +export default AdminExportCampaigns; diff --git a/src/containers/CampaignList/components/CampaignList.tsx b/src/containers/CampaignList/components/CampaignList.tsx index 88bc0e3e9..abbe0c9d0 100644 --- a/src/containers/CampaignList/components/CampaignList.tsx +++ b/src/containers/CampaignList/components/CampaignList.tsx @@ -18,6 +18,9 @@ export const CampaignList: React.FC = (props) => { return } />; } + // TODO: make verboseList = true? + // then can reuse in new component + return ( {props.campaigns.map((campaign) => ( diff --git a/src/routes.jsx b/src/routes.jsx index 5707234a8..e8a7386d0 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -12,6 +12,7 @@ import AdminCampaignGroupEditor from "./containers/AdminCampaignGroupEditor"; import AdminCampaignList from "./containers/AdminCampaignList"; import AdminCampaignStats from "./containers/AdminCampaignStats"; import AdminDashboard from "./containers/AdminDashboard"; +import AdminExportCampaigns from "./containers/AdminExportCampaigns"; import AdminExternalSystemDetail from "./containers/AdminExternalSystemDetail"; import AdminExternalSystems from "./containers/AdminExternalSystems"; import AdminIncomingMessageList from "./containers/AdminIncomingMessageList"; @@ -221,6 +222,10 @@ const AdminOrganizationRoutes = (props) => { path={`${organizationPath}/integrations/:systemId`} component={AdminExternalSystemDetail} /> + Date: Thu, 13 Jul 2023 17:25:48 -0400 Subject: [PATCH 02/27] feat: wip - add mutation, initial task --- libs/gql-schema/schema.ts | 7 + src/schema.graphql | 7 + src/server/api/root-mutations.ts | 21 +++ src/server/tasks/export-campaign.ts | 130 ++++++++++++------ src/server/tasks/export-multiple-campaigns.ts | 119 ++++++++++++++++ 5 files changed, 245 insertions(+), 39 deletions(-) create mode 100644 src/server/tasks/export-multiple-campaigns.ts diff --git a/libs/gql-schema/schema.ts b/libs/gql-schema/schema.ts index a1526a757..46e9c0abe 100644 --- a/libs/gql-schema/schema.ts +++ b/libs/gql-schema/schema.ts @@ -215,6 +215,12 @@ const rootSchema = ` vanOptions: ExportForVanInput } + input MultipleCampaignExportInput { + campaignIds: [String!] + exportType: CampaignExportType! + spokeOptions: ExportForSpokeInput + } + input QuestionResponseSyncConfigInput { id: String! } @@ -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! diff --git a/src/schema.graphql b/src/schema.graphql index cdee7ef2d..3556ff026 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -181,6 +181,12 @@ input CampaignExportInput { vanOptions: ExportForVanInput } + input MultipleCampaignExportInput { + campaignIds: [String!] + exportType: CampaignExportType! + spokeOptions: ExportForSpokeInput +} + input QuestionResponseSyncConfigInput { id: String! } @@ -253,6 +259,7 @@ type RootMutation { 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! diff --git a/src/server/api/root-mutations.ts b/src/server/api/root-mutations.ts index 50396e08b..aab792c9c 100644 --- a/src/server/api/root-mutations.ts +++ b/src/server/api/root-mutations.ts @@ -32,6 +32,7 @@ import { getUserById } from "../models/cacheable_queries"; import { Notifications, sendUserNotification } from "../notifications"; import { addExportCampaign } from "../tasks/export-campaign"; import { addExportForVan } from "../tasks/export-for-van"; +import { addExportMultipleCampaigns } from "../tasks/export-multiple-campaigns"; import { TASK_IDENTIFIER as exportOptOutsIdentifier } from "../tasks/export-opt-outs"; import { addFilterLandlines } from "../tasks/filter-landlines"; import { QUEUE_AUTOSEND_ORGANIZATION_INITIALS_TASK_IDENTIFIER } from "../tasks/queue-autosend-initials"; @@ -327,6 +328,26 @@ const rootMutations = { } }, + exportCampaigns: async (_root, { options }, { user, loaders }) => { + const { campaignIds, spokeOptions } = options; + + // TOOD - implement for VAN ? + + if (!spokeOptions) { + throw new Error("Input must include valid spokeOptions when exporting"); + } + // TODO - is this a safe assumption? could different orgs be part of same export action? + const campaignId = campaignIds[0]; + const campaign = await loaders.campaign.load(campaignId); + const organizationId = campaign.organization_id; + await accessRequired(user, organizationId, "ADMIN"); + return addExportMultipleCampaigns({ + campaignIds, + requesterId: user.id, + spokeOptions + }); + }, + editOrganizationMembership: async ( _root, { id, level, role }, diff --git a/src/server/tasks/export-campaign.ts b/src/server/tasks/export-campaign.ts index 90a915116..692042999 100644 --- a/src/server/tasks/export-campaign.ts +++ b/src/server/tasks/export-campaign.ts @@ -627,49 +627,28 @@ const processAndUploadFilteredContacts = async ({ return getDownloadUrl(`${filteredContactsKey}.csv`); }; -export interface ExportCampaignPayload { +export interface SpokeOptions { + campaign: boolean; + messages: boolean; + optOuts: boolean; + filteredContacts: boolean; +} + +export interface CampaignDataForExport { + fileNameKey: any; campaignId: number; - requesterId: number; - isAutomatedExport?: boolean; - spokeOptions: { - campaign: boolean; - messages: boolean; - optOuts: boolean; - filteredContacts: boolean; - }; + campaignTitle: any; + contactsCount: number; + helpers: ProgressTaskHelpers; + interactionSteps: any[]; + campaignVariableNames: string[]; } -export const exportCampaign: ProgressTask = async ( - payload, - helpers +// kicks off export processes and returns url for email +export const processExportData = async ( + campaignData: CampaignDataForExport, + spokeOptions: SpokeOptions ) => { - const { - campaignId, - requesterId, - isAutomatedExport = false, - spokeOptions - } = payload; - const { - campaignTitle, - notificationEmail, - interactionSteps, - campaignVariableNames - } = await fetchExportData(campaignId, requesterId); - - const countQueryResult = await r - .reader("campaign_contact") - .count("*") - .where({ campaign_id: campaignId }); - const contactsCount = countQueryResult[0].count as number; - - // Attempt upload to cloud storage - let fileNameKey = campaignTitle.replace(/ /g, "_").replace(/\//g, "_"); - - if (!isAutomatedExport) { - const timestamp = DateTime.local().toFormat("y-mm-d-hh-mm-ss"); - fileNameKey = `${fileNameKey}-${timestamp}`; - } - const { campaign: shouldExportCampaign, filteredContacts: shouldExportFilteredContacts, @@ -677,6 +656,16 @@ export const exportCampaign: ProgressTask = async ( optOuts: shouldExportOptOuts } = spokeOptions; + const { + fileNameKey, + campaignId, + campaignTitle, + contactsCount, + helpers, + interactionSteps, + campaignVariableNames + } = campaignData; + const campaignExportUrl = shouldExportCampaign ? await processAndUploadCampaignContacts({ fileNameKey, @@ -720,6 +709,69 @@ export const exportCampaign: ProgressTask = async ( }) : null; + return { + campaignExportUrl, + campaignFilteredContactsExportUrl, + campaignOptOutsExportUrl, + campaignMessagesExportUrl + }; +}; + +export interface ExportCampaignPayload { + campaignId: number; + requesterId: number; + isAutomatedExport?: boolean; + spokeOptions: SpokeOptions; +} + +export const exportCampaign: ProgressTask = async ( + payload, + helpers +) => { + const { + campaignId, + requesterId, + isAutomatedExport = false, + spokeOptions + } = payload; + const { + campaignTitle, + notificationEmail, + interactionSteps, + campaignVariableNames + } = await fetchExportData(campaignId, requesterId); + + const countQueryResult = await r + .reader("campaign_contact") + .count("*") + .where({ campaign_id: campaignId }); + const contactsCount = countQueryResult[0].count as number; + + // Attempt upload to cloud storage + let fileNameKey = campaignTitle.replace(/ /g, "_").replace(/\//g, "_"); + + if (!isAutomatedExport) { + const timestamp = DateTime.local().toFormat("y-mm-d-hh-mm-ss"); + fileNameKey = `${fileNameKey}-${timestamp}`; + } + + const campaignData = { + fileNameKey, + campaignId, + campaignTitle, + contactsCount, + helpers, + interactionSteps, + campaignVariableNames + }; + + const { + campaignExportUrl, + campaignFilteredContactsExportUrl, + campaignOptOutsExportUrl, + campaignMessagesExportUrl + } = await processExportData(campaignData, spokeOptions); + helpers.logger.debug("Waiting for streams to finish"); try { diff --git a/src/server/tasks/export-multiple-campaigns.ts b/src/server/tasks/export-multiple-campaigns.ts new file mode 100644 index 000000000..9de972fd1 --- /dev/null +++ b/src/server/tasks/export-multiple-campaigns.ts @@ -0,0 +1,119 @@ +import { DateTime } from "luxon"; + +import getExportCampaignContent from "../api/export-campaign"; +import { r } from "../models"; +import { fetchExportData, processExportData } from "./export-campaign"; +import type { ProgressTask } from "./utils"; +import { addProgressJob } from "./utils"; + +export const TASK_IDENTIFIER = "export-multiple-campaigns"; + +export interface ExportMultipleCampaignsPayload { + campaignIds: [number]; + requesterId: number; + spokeOptions: { + campaign: boolean; + messages: boolean; + optOuts: boolean; + filteredContacts: boolean; + }; +} + +// eslint-disable-next-line max-len +export const exportMultipleCampaigns: ProgressTask = async ( + payload, + helpers +) => { + // multiple jobs in progress + // handle exporting data + // send one email for all exports + + const { campaignIds, requesterId, spokeOptions } = payload; + const exportsContent = []; + for (const campaignId of campaignIds) { + const { + campaignTitle, + // TODO - see below + notificationEmail: _email, + interactionSteps, + campaignVariableNames + } = await fetchExportData(campaignId, requesterId); + + const countQueryResult = await r + .reader("campaign_contact") + .count("*") + .where({ campaign_id: campaignId }); + const contactsCount = countQueryResult[0].count as number; + + // Attempt upload to cloud storage + let fileNameKey = campaignTitle.replace(/ /g, "_").replace(/\//g, "_"); + + const timestamp = DateTime.local().toFormat("y-mm-d-hh-mm-ss"); + fileNameKey = `${fileNameKey}-${timestamp}`; + + const campaignData = { + fileNameKey, + campaignId, + campaignTitle, + contactsCount, + helpers, + interactionSteps, + campaignVariableNames + }; + + const { + campaignExportUrl, + campaignFilteredContactsExportUrl, + campaignOptOutsExportUrl, + campaignMessagesExportUrl + } = await processExportData(campaignData, spokeOptions); + + helpers.logger.debug("Waiting for streams to finish"); + + try { + const exportContent = await getExportCampaignContent( + { + campaignExportUrl, + campaignFilteredContactsExportUrl, + campaignOptOutsExportUrl, + campaignMessagesExportUrl + }, + campaignTitle + ); + exportsContent.push(exportContent); + + helpers.logger.info(`Successfully exported ${campaignId}`); + } catch (e) { + exportsContent.push(e); + } finally { + helpers.logger.info("Finishing individual export"); + } + } + // TODO - send one email with all export data + // await sendEmail({ + // to: notificationEmail, + // subject: `Export ready for ${campaignTitle}`, + // html: exportContent + // }); + helpers.logger.info("Finishing export process"); +}; + +// add jobs for each campaign export to the job_requests table +export const addExportMultipleCampaigns = async ( + payload: ExportMultipleCampaignsPayload +) => { + const { campaignIds, ...rest } = payload; + const jobResults = []; + for (const campaignId of campaignIds) { + const innerPayload = { ...rest, campaignId }; + const result = await addProgressJob({ + identifier: TASK_IDENTIFIER, + payload: innerPayload, + taskSpec: { + queueName: "campaign-exports" + } + }); + jobResults.push(result); + } + return jobResults; +}; From bb4ea782ee5f4b04930dfe408246beb05bb9dd52 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Tue, 18 Jul 2023 11:58:41 -0400 Subject: [PATCH 03/27] feat: graphql mutation --- .../src/graphql/campaign-stats.graphql | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/libs/spoke-codegen/src/graphql/campaign-stats.graphql b/libs/spoke-codegen/src/graphql/campaign-stats.graphql index 9576c7187..746cbd438 100644 --- a/libs/spoke-codegen/src/graphql/campaign-stats.graphql +++ b/libs/spoke-codegen/src/graphql/campaign-stats.graphql @@ -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 } } @@ -82,7 +96,6 @@ query GetCampaignSyncConfigs($campaignId: String!) { } } - query GetSyncTargets($campaignId: String!) { campaign(id: $campaignId) { id From a79b4ef403189321ab989daa6f3f1ebcff4fbc11 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Tue, 18 Jul 2023 12:11:29 -0400 Subject: [PATCH 04/27] feat: wip, frontend - use CampaignListMenu to select for export --- .../ExportMultipleCampaignDataDialog.tsx | 128 ++++++++++++++++++ src/containers/AdminCampaignList.jsx | 32 ++++- .../CampaignList/components/CampaignList.tsx | 6 +- .../components/CampaignListLoader.tsx | 16 ++- .../components/CampaignListMenu.tsx | 7 +- .../components/CampaignListRow.tsx | 8 +- src/containers/CampaignList/index.jsx | 6 +- 7 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/components/ExportMultipleCampaignDataDialog.tsx diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx new file mode 100644 index 000000000..416c3ff88 --- /dev/null +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -0,0 +1,128 @@ +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 FormControlLabel from "@material-ui/core/FormControlLabel"; +import Switch from "@material-ui/core/Switch"; +import { + CampaignExportType, + useExportCampaignsMutation +} from "@spoke/spoke-codegen"; +import React, { useState } from "react"; + +interface Props { + campaignIds: string[]; + open: boolean; + onClose: () => void; +} + +const ExportMultipleCampaignDataDialog = (props: Props) => { + const { campaignIds, open, onClose } = props; + const [exportCampaign, setExportCampaign] = useState(true); + const [exportMessages, setExportMessages] = useState(true); + const [exportOptOut, setExportOptOut] = useState(false); + const [exportFiltered, setExportFiltered] = useState(false); + + const [exportCampaignsMutation] = useExportCampaignsMutation(); + + const handleExportClick = async () => { + 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(", "); + console.log("MESSAGE", message); + // onError(message); + // return; + } + console.log("finished", result); + // onComplete(); + }; + + return ( + + + Export Multiple Campaign Data + + +
+ setExportCampaign(!exportCampaign)} + /> + } + /> +
+
+ setExportMessages(!exportMessages)} + /> + } + /> +
+ setExportOptOut(!exportOptOut)} + /> + } + /> +
+ setExportFiltered(!exportFiltered)} + /> + } + /> +
+ Exporting data for: + {/* {error && ( + + Error fetching templates: {error.message} + + )} */} +
+ + + + +
+ ); +}; + +export default ExportMultipleCampaignDataDialog; diff --git a/src/containers/AdminCampaignList.jsx b/src/containers/AdminCampaignList.jsx index be0f6cd3e..1680df238 100644 --- a/src/containers/AdminCampaignList.jsx +++ b/src/containers/AdminCampaignList.jsx @@ -6,6 +6,9 @@ import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; import CreateIcon from "@material-ui/icons/Create"; import FileCopyIcon from "@material-ui/icons/FileCopyOutlined"; +// import UpgradeIcon from "@material-ui/icons/Upgrade"; +// TODO - icon +import OpenInNewIcon from "@material-ui/icons/OpenInNew"; import SpeedDial from "@material-ui/lab/SpeedDial"; import SpeedDialAction from "@material-ui/lab/SpeedDialAction"; import SpeedDialIcon from "@material-ui/lab/SpeedDialIcon"; @@ -18,6 +21,7 @@ import { withRouter } from "react-router-dom"; import { compose } from "recompose"; import CreateCampaignFromTemplateDialog from "../components/CreateCampaignFromTemplateDialog"; +import ExportMultipleCampaignDataDialog from "../components/ExportMultipleCampaignDataDialog"; import LoadingIndicator from "../components/LoadingIndicator"; import theme from "../styles/theme"; import { withAuthzContext } from "./AuthzProvider"; @@ -47,7 +51,9 @@ class AdminCampaignList extends React.Component { releasingInProgress: false, releasingAllReplies: false, releaseAllRepliesError: undefined, - releaseAllRepliesResult: undefined + releaseAllRepliesResult: undefined, + campaignIdsForExport: [], + isExportCampaignData: false }; handleClickNewButton = async () => { @@ -133,6 +139,14 @@ class AdminCampaignList extends React.Component { }); }; + handleSelectForExport = (id) => { + const currentIds = this.state.campaignIdsForExport; + this.setState({ + ...this.state, + campaignIdsForExport: currentIds.concat(id) + }); + }; + renderFilters() { return ( )} @@ -289,6 +306,12 @@ class AdminCampaignList extends React.Component { tooltipTitle="Create from Template" onClick={() => this.setState({ createFromTemplateOpen: true })} /> + } + tooltipTitle="Export Campaign Data" + onClick={() => this.setState({ isExportCampaignData: true })} + disabled={campaignIdsForExport.length < 1} + /> ) : null} this.setState({ createFromTemplateOpen: false })} /> + this.setState({ isExportCampaignData: false })} + /> ); } diff --git a/src/containers/CampaignList/components/CampaignList.tsx b/src/containers/CampaignList/components/CampaignList.tsx index abbe0c9d0..706997192 100644 --- a/src/containers/CampaignList/components/CampaignList.tsx +++ b/src/containers/CampaignList/components/CampaignList.tsx @@ -11,6 +11,7 @@ interface Props extends CampaignOperations { organizationId: string; campaigns: CampaignListEntryFragment[]; isAdmin: boolean; + campaignIdsForExport: string[]; } export const CampaignList: React.FC = (props) => { @@ -18,9 +19,6 @@ export const CampaignList: React.FC = (props) => { return } />; } - // TODO: make verboseList = true? - // then can reuse in new component - return ( {props.campaigns.map((campaign) => ( @@ -32,6 +30,8 @@ export const CampaignList: React.FC = (props) => { startOperation={props.startOperation} archiveCampaign={props.archiveCampaign} unarchiveCampaign={props.unarchiveCampaign} + selectForExport={props.selectForExport} + campaignIdsForExport={props.campaignIdsForExport} /> ))} diff --git a/src/containers/CampaignList/components/CampaignListLoader.tsx b/src/containers/CampaignList/components/CampaignListLoader.tsx index 3cd664528..ed9de46b3 100644 --- a/src/containers/CampaignList/components/CampaignListLoader.tsx +++ b/src/containers/CampaignList/components/CampaignListLoader.tsx @@ -11,15 +11,17 @@ import LoadingIndicator from "../../../components/LoadingIndicator"; import { useAuthzContext } from "../../AuthzProvider"; import { isCampaignGroupsPermissionError } from "../utils"; import CampaignList from "./CampaignList"; +import type { CampaignOperations } from "./CampaignListMenu"; -interface Props { +interface Props extends CampaignOperations { organizationId: string; pageSize: number; campaignsFilter: CampaignsFilter; isAdmin: boolean; - startOperation: (...args: any[]) => any; - archiveCampaign: (...args: any[]) => any; - unarchiveCampaign: (...args: any[]) => any; + campaignIdsForExport: string[]; + // startOperation: (...args: any[]) => any; + // archiveCampaign: (...args: any[]) => any; + // unarchiveCampaign: (...args: any[]) => any; } const CampaignListLoader: React.FC = (props) => { @@ -30,7 +32,9 @@ const CampaignListLoader: React.FC = (props) => { isAdmin, startOperation, archiveCampaign, - unarchiveCampaign + unarchiveCampaign, + selectForExport, + campaignIdsForExport } = props; const { data, loading, error, fetchMore } = useGetAdminCampaignsQuery({ variables: { organizationId, limit: pageSize, filter: campaignsFilter }, @@ -105,6 +109,8 @@ const CampaignListLoader: React.FC = (props) => { startOperation={startOperation} archiveCampaign={archiveCampaign} unarchiveCampaign={unarchiveCampaign} + selectForExport={selectForExport} + campaignIdsForExport={campaignIdsForExport} /> {loading && }
diff --git a/src/containers/CampaignList/components/CampaignListMenu.tsx b/src/containers/CampaignList/components/CampaignListMenu.tsx index 68f7d1d51..e5d0764ac 100644 --- a/src/containers/CampaignList/components/CampaignListMenu.tsx +++ b/src/containers/CampaignList/components/CampaignListMenu.tsx @@ -18,6 +18,7 @@ export interface CampaignOperations { ) => ClickHandler; archiveCampaign: (campaignId: string) => ClickHandler; unarchiveCampaign: (campaignId: string) => ClickHandler; + selectForExport: (campaignId: string) => void; } interface Props extends CampaignOperations { @@ -31,7 +32,8 @@ export const CampaignListMenu: React.FC = (props) => { startOperation, archiveCampaign, unarchiveCampaign, - campaign + campaign, + selectForExport } = props; const handleClickMenu = useCallback( @@ -109,6 +111,9 @@ export const CampaignListMenu: React.FC = (props) => { ? "Turn auto-assign OFF" : "Turn auto-assign ON"} + selectForExport(campaign.id)}> + Select for export +
); diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index f6c84ed54..f6e815118 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -35,12 +35,13 @@ interface Props extends CampaignOperations { organizationId: string; isAdmin: boolean; campaign: CampaignListEntryFragment; + campaignIdsForExport: string[]; } export const CampaignListRow: React.FC = (props) => { const theme = useTheme(); const history = useHistory(); - const { organizationId, isAdmin, campaign } = props; + const { organizationId, isAdmin, campaign, campaignIdsForExport } = props; const { isStarted, isArchived, @@ -107,6 +108,10 @@ export const CampaignListRow: React.FC = (props) => { tags.push({ title: "Autoassign eligible" }); } + if (campaignIdsForExport.includes(campaign.id)) { + tags.push({ title: "Selected for export" }); + } + tags = tags.concat(teams.map(({ title }) => ({ title }))); if (campaignGroups) { tags = tags.concat( @@ -169,6 +174,7 @@ export const CampaignListRow: React.FC = (props) => { startOperation={props.startOperation} archiveCampaign={props.archiveCampaign} unarchiveCampaign={props.unarchiveCampaign} + selectForExport={props.selectForExport} /> )} diff --git a/src/containers/CampaignList/index.jsx b/src/containers/CampaignList/index.jsx index ed24c6a24..5c57bb503 100644 --- a/src/containers/CampaignList/index.jsx +++ b/src/containers/CampaignList/index.jsx @@ -50,7 +50,9 @@ export class CampaignList extends React.Component { campaignsFilter, isAdmin, data, - mutations + mutations, + selectForExport, + campaignIdsForExport } = this.props; const { currentAssignmentTargets } = data.organization; const { archiveCampaign, unarchiveCampaign } = mutations; @@ -77,6 +79,8 @@ export class CampaignList extends React.Component { startOperation={this.start} archiveCampaign={archiveCampaign} unarchiveCampaign={unarchiveCampaign} + selectForExport={selectForExport} + campaignIdsForExport={campaignIdsForExport} /> ); From 3aae8c5c31c6d67e5dbc28621ba4c934b23f9d32 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 20 Jul 2023 13:08:09 -0400 Subject: [PATCH 05/27] feat(export-multiple-campaigns): simple backend solution --- src/server/api/root-mutations.ts | 3 - src/server/tasks/export-multiple-campaigns.ts | 90 +------------------ 2 files changed, 2 insertions(+), 91 deletions(-) diff --git a/src/server/api/root-mutations.ts b/src/server/api/root-mutations.ts index aab792c9c..0ca41010d 100644 --- a/src/server/api/root-mutations.ts +++ b/src/server/api/root-mutations.ts @@ -331,12 +331,9 @@ const rootMutations = { exportCampaigns: async (_root, { options }, { user, loaders }) => { const { campaignIds, spokeOptions } = options; - // TOOD - implement for VAN ? - if (!spokeOptions) { throw new Error("Input must include valid spokeOptions when exporting"); } - // TODO - is this a safe assumption? could different orgs be part of same export action? const campaignId = campaignIds[0]; const campaign = await loaders.campaign.load(campaignId); const organizationId = campaign.organization_id; diff --git a/src/server/tasks/export-multiple-campaigns.ts b/src/server/tasks/export-multiple-campaigns.ts index 9de972fd1..6c6031b2b 100644 --- a/src/server/tasks/export-multiple-campaigns.ts +++ b/src/server/tasks/export-multiple-campaigns.ts @@ -1,13 +1,6 @@ -import { DateTime } from "luxon"; - -import getExportCampaignContent from "../api/export-campaign"; -import { r } from "../models"; -import { fetchExportData, processExportData } from "./export-campaign"; -import type { ProgressTask } from "./utils"; +import { TASK_IDENTIFIER } from "./export-campaign"; import { addProgressJob } from "./utils"; -export const TASK_IDENTIFIER = "export-multiple-campaigns"; - export interface ExportMultipleCampaignsPayload { campaignIds: [number]; requesterId: number; @@ -19,85 +12,6 @@ export interface ExportMultipleCampaignsPayload { }; } -// eslint-disable-next-line max-len -export const exportMultipleCampaigns: ProgressTask = async ( - payload, - helpers -) => { - // multiple jobs in progress - // handle exporting data - // send one email for all exports - - const { campaignIds, requesterId, spokeOptions } = payload; - const exportsContent = []; - for (const campaignId of campaignIds) { - const { - campaignTitle, - // TODO - see below - notificationEmail: _email, - interactionSteps, - campaignVariableNames - } = await fetchExportData(campaignId, requesterId); - - const countQueryResult = await r - .reader("campaign_contact") - .count("*") - .where({ campaign_id: campaignId }); - const contactsCount = countQueryResult[0].count as number; - - // Attempt upload to cloud storage - let fileNameKey = campaignTitle.replace(/ /g, "_").replace(/\//g, "_"); - - const timestamp = DateTime.local().toFormat("y-mm-d-hh-mm-ss"); - fileNameKey = `${fileNameKey}-${timestamp}`; - - const campaignData = { - fileNameKey, - campaignId, - campaignTitle, - contactsCount, - helpers, - interactionSteps, - campaignVariableNames - }; - - const { - campaignExportUrl, - campaignFilteredContactsExportUrl, - campaignOptOutsExportUrl, - campaignMessagesExportUrl - } = await processExportData(campaignData, spokeOptions); - - helpers.logger.debug("Waiting for streams to finish"); - - try { - const exportContent = await getExportCampaignContent( - { - campaignExportUrl, - campaignFilteredContactsExportUrl, - campaignOptOutsExportUrl, - campaignMessagesExportUrl - }, - campaignTitle - ); - exportsContent.push(exportContent); - - helpers.logger.info(`Successfully exported ${campaignId}`); - } catch (e) { - exportsContent.push(e); - } finally { - helpers.logger.info("Finishing individual export"); - } - } - // TODO - send one email with all export data - // await sendEmail({ - // to: notificationEmail, - // subject: `Export ready for ${campaignTitle}`, - // html: exportContent - // }); - helpers.logger.info("Finishing export process"); -}; - // add jobs for each campaign export to the job_requests table export const addExportMultipleCampaigns = async ( payload: ExportMultipleCampaignsPayload @@ -110,7 +24,7 @@ export const addExportMultipleCampaigns = async ( identifier: TASK_IDENTIFIER, payload: innerPayload, taskSpec: { - queueName: "campaign-exports" + queueName: "multiple-campaign-exports" } }); jobResults.push(result); From 506b02d0b739543ef311e1556b19a62b5c244884 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 20 Jul 2023 14:09:21 -0400 Subject: [PATCH 06/27] feat(campaignexportmodal): share ModalContent between components --- .../ExportMultipleCampaignDataDialog.tsx | 84 ++++------- .../components/CampaignExportModal.tsx | 131 ++++++++++++------ 2 files changed, 114 insertions(+), 101 deletions(-) diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx index 416c3ff88..a3b79aee2 100644 --- a/src/components/ExportMultipleCampaignDataDialog.tsx +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -4,22 +4,24 @@ 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 FormControlLabel from "@material-ui/core/FormControlLabel"; -import Switch from "@material-ui/core/Switch"; import { CampaignExportType, useExportCampaignsMutation } from "@spoke/spoke-codegen"; import React, { useState } from "react"; +import { CampaignExportModalContent } from "../containers/AdminCampaignStats/components/CampaignExportModal"; + interface Props { campaignIds: string[]; open: boolean; onClose: () => void; + onError: (errorMessage: string) => void; + onComplete(): void; } -const ExportMultipleCampaignDataDialog = (props: Props) => { - const { campaignIds, open, onClose } = props; +const ExportMultipleCampaignDataDialog: React.FC = (props) => { + const { campaignIds, open, onClose, onError, onComplete } = props; const [exportCampaign, setExportCampaign] = useState(true); const [exportMessages, setExportMessages] = useState(true); const [exportOptOut, setExportOptOut] = useState(false); @@ -27,6 +29,12 @@ const ExportMultipleCampaignDataDialog = (props: Props) => { const [exportCampaignsMutation] = useExportCampaignsMutation(); + const handleChange = (setStateFunction: any) => ( + event: React.ChangeEvent + ) => { + setStateFunction(event.target.checked); + }; + const handleExportClick = async () => { const result = await exportCampaignsMutation({ variables: { @@ -44,12 +52,10 @@ const ExportMultipleCampaignDataDialog = (props: Props) => { }); if (result.errors) { const message = result.errors.map((e) => e.message).join(", "); - console.log("MESSAGE", message); - // onError(message); - // return; + onError(message); + return; } - console.log("finished", result); - // onComplete(); + onComplete(); }; return ( @@ -59,57 +65,21 @@ const ExportMultipleCampaignDataDialog = (props: Props) => { open={open} > - Export Multiple Campaign Data + Export Multiple Campaigns Data + -
- setExportCampaign(!exportCampaign)} - /> - } - /> -
-
- setExportMessages(!exportMessages)} - /> - } - /> -
- setExportOptOut(!exportOptOut)} - /> - } - /> -
- setExportFiltered(!exportFiltered)} - /> - } - /> -
Exporting data for: - {/* {error && ( - - Error fetching templates: {error.message} - - )} */}
diff --git a/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx b/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx index 1b3cf4457..67a0a4965 100644 --- a/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx +++ b/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx @@ -19,6 +19,82 @@ interface CampaignExportModalProps { onComplete(): void; } +interface CampaignExportModalContentProps { + exportCampaign: boolean; + exportMessages: boolean; + exportOptOut: boolean; + exportFiltered: boolean; + handleChange: ( + setStateFunction: any + ) => (event: React.ChangeEvent) => void; + setExportCampaign: React.Dispatch>; + setExportMessages: React.Dispatch>; + setExportOptOut: React.Dispatch>; + setExportFiltered: React.Dispatch>; +} +// console.log('TODO') +export const CampaignExportModalContent: React.FC = ( + props: CampaignExportModalContentProps +) => { + const { + exportCampaign, + exportMessages, + exportOptOut, + exportFiltered, + handleChange, + setExportCampaign, + setExportMessages, + setExportOptOut, + setExportFiltered + } = props; + return ( + +
+ + } + /> +
+
+ + } + /> +
+ + } + /> +
+ + } + /> +
+
+ ); +}; + const CampaignExportModal: React.FC = (props) => { const { campaignId, open, onClose, onComplete, onError } = props; const [exportCampaign, setExportCampaign] = useState(true); @@ -60,50 +136,17 @@ const CampaignExportModal: React.FC = (props) => { return ( Export Campaign - -
- - } - /> -
-
- - } - /> -
- - } - /> -
- - } - /> -
-
+ + + ); +}; + +export default CampaignListHeader; diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index 2ae26018c..161e6c9de 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -1,36 +1,22 @@ -import Avatar from "@material-ui/core/Avatar"; -import Chip from "@material-ui/core/Chip"; -import { blue, green } from "@material-ui/core/colors"; +import { Card } from "@material-ui/core"; +import Checkbox from "@material-ui/core/Checkbox"; import ListItem from "@material-ui/core/ListItem"; -import ListItemAvatar from "@material-ui/core/ListItemAvatar"; +import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; import ListItemText from "@material-ui/core/ListItemText"; import { useTheme } from "@material-ui/core/styles"; -import WarningIcon from "@material-ui/icons/Warning"; import type { CampaignListEntryFragment } from "@spoke/spoke-codegen"; import React from "react"; import { useHistory } from "react-router-dom"; import { dataTest } from "../../../lib/attributes"; import { DateTime } from "../../../lib/datetime"; +import { makeCampaignHeaderTags } from "../utils"; +import CampaignDetails from "./CampaignDetails"; +import CampaignHeader from "./CampaignHeader"; import type { CampaignOperations } from "./CampaignListMenu"; import CampaignListMenu from "./CampaignListMenu"; -const inlineStyles = { - chipWrapper: { - display: "flex", - flexWrap: "wrap", - alignItems: "center" - }, - chip: { margin: "4px" }, - past: { - opacity: 0.6 - }, - secondaryText: { - whiteSpace: "pre-wrap" - } -}; - interface Props extends CampaignOperations { organizationId: string; isAdmin: boolean; @@ -54,132 +40,74 @@ export const CampaignListRow: React.FC = (props) => { externalSystem } = campaign; - let listItemStyle = {}; - let leftIcon; - if (isArchived) { - listItemStyle = inlineStyles.past; - } else if (!isStarted || hasUnassignedContacts) { - listItemStyle = { - color: theme.palette.warning.dark - }; - leftIcon = ; - } else if (hasUnsentInitialMessages) { - listItemStyle = { - color: theme.palette.info.dark - }; - } else { - listItemStyle = { - color: theme.palette.success.dark - }; - } const dueBy = DateTime.fromISO(campaign.dueBy || ""); const creatorName = campaign.creator ? campaign.creator.displayName : null; - let tags = []; - if (DateTime.local() >= dueBy) { - tags.push({ - title: "Overdue", - color: theme.palette.grey[900], - backgroundColor: theme.palette.error.main - }); - } - - if (externalSystem) { - const title = `${externalSystem.type}: ${externalSystem.name}`; - tags.push({ title, backgroundColor: blue[300] }); - } - - if (!isStarted) { - tags.push({ title: "Not started" }); - } - - if (hasUnassignedContacts) { - tags.push({ title: "Unassigned contacts" }); - } - - if (isStarted && hasUnsentInitialMessages) { - tags.push({ title: "Unsent initial messages" }); - } - - if (isStarted && hasUnhandledMessages) { - tags.push({ title: "Unhandled replies" }); - } - - if (isStarted && !isArchived && isAutoassignEnabled) { - tags.push({ title: "Autoassign eligible" }); - } - - if (campaignIdsForExport.includes(campaign.id)) { - tags.push({ title: "Selected for export", backgroundColor: green[300] }); - } - tags = tags.concat(teams.map(({ title }) => ({ title }))); - if (campaignGroups) { - tags = tags.concat( - campaignGroups.edges.map(({ node }) => ({ title: node.name })) - ); - } - - const primaryText = ( -
- {campaign.title} - {tags.map((tag) => ( - - ))} -
- ); - const secondaryText = ( - - - Campaign ID: {campaign.id} -
- {campaign.description} - {creatorName ? — Created by {creatorName} : null} -
- {dueBy.isValid ? dueBy.toFormat("DD") : "No due date set"} -
-
- ); + const headerTags = makeCampaignHeaderTags({ + isStarted, + hasUnassignedContacts, + hasUnsentInitialMessages, + hasUnhandledMessages, + theme + }); const campaignUrl = `/admin/${organizationId}/campaigns/${campaign.id}${ isStarted ? "" : "/edit" }`; return ( - history.push(campaignUrl)} + - {leftIcon && ( - - {leftIcon} - - )} - - {isAdmin && ( - - + + console.log("TODO")} /> - - )} - + + history.push(campaignUrl)} + /> + } + secondary={ + + } + secondaryTypographyProps={{ color: "textPrimary" }} + /> + {isAdmin && ( + + + + )} + + ); }; diff --git a/src/containers/CampaignList/index.jsx b/src/containers/CampaignList/index.jsx index 5c57bb503..1de9bd6fc 100644 --- a/src/containers/CampaignList/index.jsx +++ b/src/containers/CampaignList/index.jsx @@ -4,6 +4,7 @@ import React from "react"; import { loadData } from "../hoc/with-operations"; import AssignmentHUD from "./components/AssignmentHUD"; +import CampaignListHeader from "./components/CampaignListHeader"; import CampaignListLoader from "./components/CampaignListLoader"; import { OperationDialog, operations } from "./components/OperationDialog"; @@ -71,6 +72,7 @@ export class CampaignList extends React.Component { /> )} + { return ( gqlError.path && @@ -8,3 +11,68 @@ export const isCampaignGroupsPermissionError = (gqlError: GraphQLError) => { gqlError.extensions.code === "FORBIDDEN" ); }; + +type MakeCampaignTagsFn = (props: { + isStarted: boolean | null | undefined; + hasUnassignedContacts: boolean | null | undefined; + hasUnsentInitialMessages: boolean | null | undefined; + hasUnhandledMessages: boolean | null | undefined; + theme: Theme; +}) => Tag[]; + +export const makeCampaignHeaderTags: MakeCampaignTagsFn = ({ + isStarted, + hasUnassignedContacts, + hasUnsentInitialMessages, + hasUnhandledMessages, + theme +}) => { + const tags = []; + + // display 'Started' or 'Not started' first + if (isStarted) { + tags.push({ + title: "Started", + backgroundColor: theme.palette.success.light + }); + } else { + tags.push({ + title: "Not started", + backgroundColor: theme.palette.error.main + }); + } + + if (hasUnassignedContacts) { + tags.push({ + title: "Unassigned contacts", + backgroundColor: theme.palette.error.main + }); + } else { + tags.push({ + title: "All contacts assigned", + backgroundColor: theme.palette.success.light + }); + } + + if (isStarted) { + const tag = hasUnsentInitialMessages + ? { + title: "Unsent initial messages", + backgroundColor: theme.palette.error.main + } + : { + title: "All initials sent", + backgroundColor: theme.palette.success.light + }; + tags.push(tag); + } + + if (isStarted && hasUnhandledMessages) { + tags.push({ + title: "Unhandled replies", + backgroundColor: theme.palette.error.main + }); + } + + return tags; +}; From bef6de31d7598e6f15c8b2fb49bea5ec279256e5 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Tue, 25 Jul 2023 16:31:14 -0400 Subject: [PATCH 13/27] feat(campaign-list): hook up export campaign button --- src/containers/AdminCampaignList.jsx | 16 ++++----- .../components/CampaignListHeader.tsx | 34 ++++++++++++------- .../components/CampaignListRow.tsx | 11 ++++-- src/containers/CampaignList/index.jsx | 8 +++-- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/containers/AdminCampaignList.jsx b/src/containers/AdminCampaignList.jsx index 05d78cca6..801df8d65 100644 --- a/src/containers/AdminCampaignList.jsx +++ b/src/containers/AdminCampaignList.jsx @@ -6,7 +6,6 @@ import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; import CreateIcon from "@material-ui/icons/Create"; import FileCopyIcon from "@material-ui/icons/FileCopyOutlined"; -import OpenInNewIcon from "@material-ui/icons/OpenInNew"; import SpeedDial from "@material-ui/lab/SpeedDial"; import SpeedDialAction from "@material-ui/lab/SpeedDialAction"; import SpeedDialIcon from "@material-ui/lab/SpeedDialIcon"; @@ -153,6 +152,12 @@ class AdminCampaignList extends React.Component { }); }; + handleClickExportButton = () => { + this.setState({ + shouldShowExportModal: true + }); + }; + handleErrorCampaignExport = (exportErrorMessage) => { this.setState({ exportErrorMessage, @@ -298,8 +303,9 @@ class AdminCampaignList extends React.Component { campaignsFilter={campaignsFilter} pageSize={DEFAULT_PAGE_SIZE} isAdmin={isAdmin} - selectForExport={this.handleSelectForExport} campaignIdsForExport={campaignIdsForExport} + selectForExport={this.handleSelectForExport} + handleClickExportButton={this.handleClickExportButton} /> )} @@ -323,12 +329,6 @@ class AdminCampaignList extends React.Component { tooltipTitle="Create from Template" onClick={() => this.setState({ createFromTemplateOpen: true })} /> - } - tooltipTitle="Export Campaign Data" - onClick={() => this.setState({ shouldShowExportModal: true })} - disabled={campaignIdsForExport.length < 1} - /> ) : null} { - // const [anchorEl, setAnchorEl] = useState(null); +interface Props { + campaignIdsForExport: string[]; + onClick: () => void; +} - const handleClick = (_event: Event) => { - // TODO - // setAnchorEl(event.currentTarget); - }; - - // TODO - // const handleClose = () => { - // setAnchorEl(null); - // }; +const CampaignListHeader = (props: Props) => { + const { campaignIdsForExport, onClick } = props; + const isCampaignSelected = campaignIdsForExport.length > 0; return (
{
); diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index 161e6c9de..a0f64013f 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -22,12 +22,19 @@ interface Props extends CampaignOperations { isAdmin: boolean; campaign: CampaignListEntryFragment; campaignIdsForExport: string[]; + selectForExport: (id: string) => void; } export const CampaignListRow: React.FC = (props) => { const theme = useTheme(); const history = useHistory(); - const { organizationId, isAdmin, campaign, campaignIdsForExport } = props; + const { + organizationId, + isAdmin, + campaign, + campaignIdsForExport, + selectForExport + } = props; const { isStarted, isArchived, @@ -66,7 +73,7 @@ export const CampaignListRow: React.FC = (props) => { checked={campaignIdsForExport.includes(campaign.id)} tabIndex={-1} disableRipple - onClick={() => console.log("TODO")} + onClick={() => selectForExport(campaign.id)} /> )} - + Date: Tue, 25 Jul 2023 16:51:25 -0400 Subject: [PATCH 14/27] refactor: rm mvp solution --- src/containers/AdminDashboard/index.jsx | 5 ----- src/containers/AdminExportCampaigns/index.tsx | 12 ------------ src/routes.jsx | 5 ----- 3 files changed, 22 deletions(-) delete mode 100644 src/containers/AdminExportCampaigns/index.tsx diff --git a/src/containers/AdminDashboard/index.jsx b/src/containers/AdminDashboard/index.jsx index b969f32dc..703fd319e 100644 --- a/src/containers/AdminDashboard/index.jsx +++ b/src/containers/AdminDashboard/index.jsx @@ -69,11 +69,6 @@ class AdminDashboard extends React.Component { const sections = [ { name: "Campaigns", path: "campaigns", role: "ADMIN" }, { name: "Template Campaigns", path: "template-campaigns", role: "ADMIN" }, - { - name: "Export Campaigns", - path: "export-campaigns", - role: "ADMIN" - }, { name: "Campaign Groups", path: "campaign-groups", role: "ADMIN" }, { name: "People", path: "people", role: "ADMIN" }, { name: "Teams", path: "teams", role: "ADMIN" }, diff --git a/src/containers/AdminExportCampaigns/index.tsx b/src/containers/AdminExportCampaigns/index.tsx deleted file mode 100644 index dfbf8229f..000000000 --- a/src/containers/AdminExportCampaigns/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { useHistory, useParams } from "react-router-dom"; - -const AdminExportCampaigns: React.FC = () => { - // const classes = useStyles(); - const _history = useHistory(); - const { organizationId: _orgId } = useParams<{ organizationId: string }>(); - - return
TODO
; -}; - -export default AdminExportCampaigns; diff --git a/src/routes.jsx b/src/routes.jsx index e8a7386d0..5707234a8 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -12,7 +12,6 @@ import AdminCampaignGroupEditor from "./containers/AdminCampaignGroupEditor"; import AdminCampaignList from "./containers/AdminCampaignList"; import AdminCampaignStats from "./containers/AdminCampaignStats"; import AdminDashboard from "./containers/AdminDashboard"; -import AdminExportCampaigns from "./containers/AdminExportCampaigns"; import AdminExternalSystemDetail from "./containers/AdminExternalSystemDetail"; import AdminExternalSystems from "./containers/AdminExternalSystems"; import AdminIncomingMessageList from "./containers/AdminIncomingMessageList"; @@ -222,10 +221,6 @@ const AdminOrganizationRoutes = (props) => { path={`${organizationPath}/integrations/:systemId`} component={AdminExternalSystemDetail} /> - Date: Wed, 26 Jul 2023 14:07:46 -0400 Subject: [PATCH 15/27] feat(admin-campaign-list): styling, search field --- src/containers/AdminCampaignList.jsx | 36 +++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/containers/AdminCampaignList.jsx b/src/containers/AdminCampaignList.jsx index 801df8d65..c97efce15 100644 --- a/src/containers/AdminCampaignList.jsx +++ b/src/containers/AdminCampaignList.jsx @@ -4,6 +4,7 @@ import Dialog from "@material-ui/core/Dialog"; import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; +import Typography from "@material-ui/core/Typography"; import CreateIcon from "@material-ui/icons/Create"; import FileCopyIcon from "@material-ui/icons/FileCopyOutlined"; import SpeedDial from "@material-ui/lab/SpeedDial"; @@ -32,6 +33,10 @@ const styles = { alignItems: "baseline", justifyContent: "space-between", padding: 5 + }, + filterWrapper: { + display: "flex", + alignIems: "baseline" } }; @@ -44,7 +49,8 @@ class AdminCampaignList extends React.Component { createFromTemplateOpen: false, isCreating: false, campaignsFilter: { - isArchived: false + isArchived: false, + campaignTitle: "" }, releasingInProgress: false, releasingAllReplies: false, @@ -80,10 +86,22 @@ class AdminCampaignList extends React.Component { ); }; - handleFilterChange = (event, index, value) => { + handleFilterChangeCurrentOrArchived = (_event, _index, value) => { + const { campaignTitle } = this.state.campaignsFilter; + this.setState({ + campaignsFilter: { + isArchived: value, + campaignTitle + } + }); + }; + + handleFilterCampaignTitle = (campaignTitle) => { + const { isArchived } = this.state.campaignsFilter; this.setState({ campaignsFilter: { - isArchived: value + isArchived, + campaignTitle } }); }; @@ -166,11 +184,11 @@ class AdminCampaignList extends React.Component { }); }; - renderFilters() { + renderCurrentCampaignFilter() { return ( @@ -197,7 +215,12 @@ class AdminCampaignList extends React.Component { return (
- {this.renderFilters()} +
+ + Campaigns + + {this.renderCurrentCampaignFilter()} +
+ + + + ) + }} + onChange={(ev) => debounceSearchTerm(ev.target.value)} + />
); }; diff --git a/src/containers/CampaignList/components/CampaignListLoader.tsx b/src/containers/CampaignList/components/CampaignListLoader.tsx index ed9de46b3..053d3dd17 100644 --- a/src/containers/CampaignList/components/CampaignListLoader.tsx +++ b/src/containers/CampaignList/components/CampaignListLoader.tsx @@ -19,9 +19,6 @@ interface Props extends CampaignOperations { campaignsFilter: CampaignsFilter; isAdmin: boolean; campaignIdsForExport: string[]; - // startOperation: (...args: any[]) => any; - // archiveCampaign: (...args: any[]) => any; - // unarchiveCampaign: (...args: any[]) => any; } const CampaignListLoader: React.FC = (props) => { diff --git a/src/containers/CampaignList/index.jsx b/src/containers/CampaignList/index.jsx index 9f8f29bb6..6480f969d 100644 --- a/src/containers/CampaignList/index.jsx +++ b/src/containers/CampaignList/index.jsx @@ -54,6 +54,7 @@ export class CampaignList extends React.Component { mutations, selectForExport, campaignIdsForExport, + filterByCampaignTitle, handleClickExportButton } = this.props; const { currentAssignmentTargets } = data.organization; @@ -75,6 +76,7 @@ export class CampaignList extends React.Component { { - const { after, first, organizationId, campaignId, isArchived } = options; + const { + after, + first, + organizationId, + campaignId, + isArchived, + campaignTitle + } = options; const query = r.reader("campaign").select("*"); @@ -69,6 +76,12 @@ export const doGetCampaigns: DoGetCampaigns = async ( query.where({ is_archived: isArchived }); } + if (campaignTitle) { + query.whereRaw(`concat("id", ': ', "title") ilike ?`, [ + `%${campaignTitle}%` + ]); + } + const pagerOptions = { first, after }; return formatPage(query, pagerOptions); }; From ebd991a222519b18ef8145989b958ea3ee02ea0b Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Wed, 26 Jul 2023 21:21:37 -0400 Subject: [PATCH 17/27] feat(campaign-export-modal): display camaign details for export, related plumbing N --- .../ExportMultipleCampaignDataDialog.tsx | 48 +++++++++++++++++-- src/containers/AdminCampaignList.jsx | 27 ++++++----- .../components/CampaignExportModal.tsx | 1 - .../CampaignList/components/CampaignList.tsx | 5 +- .../components/CampaignListHeader.tsx | 12 +++-- .../components/CampaignListLoader.tsx | 7 +-- .../components/CampaignListMenu.tsx | 16 +------ .../components/CampaignListRow.tsx | 20 +++++--- src/containers/CampaignList/index.jsx | 8 ++-- 9 files changed, 92 insertions(+), 52 deletions(-) diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx index 73252a99e..34d148f8d 100644 --- a/src/components/ExportMultipleCampaignDataDialog.tsx +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -4,6 +4,8 @@ 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 Typography from "@material-ui/core/Typography"; +import CheckBoxOutlinedIcon from "@material-ui/icons/CheckBoxOutlined"; import { CampaignExportType, useExportCampaignsMutation @@ -12,8 +14,12 @@ import React, { useState } from "react"; import { CampaignExportModalContent } from "../containers/AdminCampaignStats/components/CampaignExportModal"; +export type CampaignDetailsForExport = { + id: string; + title: string; +}; interface Props { - campaignIds: string[]; + campaignDetailsForExport: CampaignDetailsForExport[]; open: boolean; onClose: () => void; onError: (errorMessage: string) => void; @@ -21,7 +27,13 @@ interface Props { } const ExportMultipleCampaignDataDialog: React.FC = (props) => { - const { campaignIds, open, onClose, onError, onComplete } = props; + const { + campaignDetailsForExport, + open, + onClose, + onError, + onComplete + } = props; const [exportCampaign, setExportCampaign] = useState(true); const [exportMessages, setExportMessages] = useState(true); const [exportOptOut, setExportOptOut] = useState(false); @@ -36,6 +48,9 @@ const ExportMultipleCampaignDataDialog: React.FC = (props) => { }; const handleExportClick = async () => { + const campaignIds = campaignDetailsForExport.map( + (campaign: CampaignDetailsForExport) => campaign.id + ); const result = await exportCampaignsMutation({ variables: { options: { @@ -64,7 +79,9 @@ const ExportMultipleCampaignDataDialog: React.FC = (props) => { open={open} > - Export Multiple Campaigns Data + + Export Campaigns + = (props) => { setExportFiltered={setExportFiltered} /> - Exporting data for: + + Exporting data for: + {campaignDetailsForExport.map((campaign) => { + return ( +
+ + + {campaign.title} + + + id: {campaign.id} + +
+ ); + })} +
= (props) => { @@ -31,7 +32,7 @@ const CampaignListLoader: React.FC = (props) => { archiveCampaign, unarchiveCampaign, selectForExport, - campaignIdsForExport + campaignDetailsForExport } = props; const { data, loading, error, fetchMore } = useGetAdminCampaignsQuery({ variables: { organizationId, limit: pageSize, filter: campaignsFilter }, @@ -107,7 +108,7 @@ const CampaignListLoader: React.FC = (props) => { archiveCampaign={archiveCampaign} unarchiveCampaign={unarchiveCampaign} selectForExport={selectForExport} - campaignIdsForExport={campaignIdsForExport} + campaignDetailsForExport={campaignDetailsForExport} /> {loading && }
diff --git a/src/containers/CampaignList/components/CampaignListMenu.tsx b/src/containers/CampaignList/components/CampaignListMenu.tsx index 3acc88ab5..b155045be 100644 --- a/src/containers/CampaignList/components/CampaignListMenu.tsx +++ b/src/containers/CampaignList/components/CampaignListMenu.tsx @@ -21,21 +21,14 @@ export interface CampaignOperations { selectForExport: (campaignId: string) => void; } -interface Props extends CampaignOperations { - campaign: CampaignListEntryFragment; - campaignIdsForExport: string[]; -} - -export const CampaignListMenu: React.FC = (props) => { +export const CampaignListMenu: React.FC = (props) => { const [menuAnchor, setMenuAnchor] = useState(null); const { startOperation, archiveCampaign, unarchiveCampaign, - campaign, - selectForExport, - campaignIdsForExport + campaign } = props; const handleClickMenu = useCallback( @@ -113,11 +106,6 @@ export const CampaignListMenu: React.FC = (props) => { ? "Turn auto-assign OFF" : "Turn auto-assign ON"} - selectForExport(campaign.id)}> - {campaignIdsForExport.includes(campaign.id) - ? "De-select for export" - : "Select for export"} -
); diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index a0f64013f..4ac350bf0 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -9,6 +9,7 @@ import type { CampaignListEntryFragment } from "@spoke/spoke-codegen"; import React from "react"; import { useHistory } from "react-router-dom"; +import type { CampaignDetailsForExport } from "../../../components/ExportMultipleCampaignDataDialog"; import { dataTest } from "../../../lib/attributes"; import { DateTime } from "../../../lib/datetime"; import { makeCampaignHeaderTags } from "../utils"; @@ -21,8 +22,8 @@ interface Props extends CampaignOperations { organizationId: string; isAdmin: boolean; campaign: CampaignListEntryFragment; - campaignIdsForExport: string[]; - selectForExport: (id: string) => void; + campaignDetailsForExport: CampaignDetailsForExport[]; + selectForExport: (details: CampaignDetailsForExport) => void; } export const CampaignListRow: React.FC = (props) => { @@ -32,7 +33,7 @@ export const CampaignListRow: React.FC = (props) => { organizationId, isAdmin, campaign, - campaignIdsForExport, + campaignDetailsForExport, selectForExport } = props; const { @@ -58,6 +59,11 @@ export const CampaignListRow: React.FC = (props) => { theme }); + const isCampaignSelected = campaignDetailsForExport.find( + (selectedCampaign: CampaignDetailsForExport) => + selectedCampaign.id === campaign.id + ); + const campaignUrl = `/admin/${organizationId}/campaigns/${campaign.id}${ isStarted ? "" : "/edit" }`; @@ -70,10 +76,12 @@ export const CampaignListRow: React.FC = (props) => { selectForExport(campaign.id)} + onClick={() => + selectForExport({ id: campaign.id, title: campaign.title }) + } /> = (props) => { startOperation={props.startOperation} archiveCampaign={props.archiveCampaign} unarchiveCampaign={props.unarchiveCampaign} - selectForExport={props.selectForExport} - campaignIdsForExport={props.campaignIdsForExport} /> )} diff --git a/src/containers/CampaignList/index.jsx b/src/containers/CampaignList/index.jsx index 6480f969d..9897579b9 100644 --- a/src/containers/CampaignList/index.jsx +++ b/src/containers/CampaignList/index.jsx @@ -53,7 +53,7 @@ export class CampaignList extends React.Component { data, mutations, selectForExport, - campaignIdsForExport, + campaignDetailsForExport, filterByCampaignTitle, handleClickExportButton } = this.props; @@ -75,7 +75,7 @@ export class CampaignList extends React.Component { )} @@ -88,7 +88,7 @@ export class CampaignList extends React.Component { archiveCampaign={archiveCampaign} unarchiveCampaign={unarchiveCampaign} selectForExport={selectForExport} - campaignIdsForExport={campaignIdsForExport} + campaignDetailsForExport={campaignDetailsForExport} />
); @@ -100,7 +100,7 @@ CampaignList.propTypes = { campaignsFilter: PropTypes.object.isRequired, pageSize: PropTypes.number.isRequired, isAdmin: PropTypes.bool.isRequired, - campaignIdsForExport: PropTypes.array.isRequired, + campaignDetailsForExport: PropTypes.array.isRequired, filterByCampaignTitle: PropTypes.func.isRequired, selectForExport: PropTypes.func.isRequired, handleClickExportButton: PropTypes.func.isRequired, From d14b20c1bdcb3fb076059826c70a0642013d2bbb Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 27 Jul 2023 16:32:34 -0400 Subject: [PATCH 18/27] feat(campaign-list): improve card styling --- .../components/CampaignDetails.tsx | 38 +++++++++++------- .../components/CampaignHeader.tsx | 39 ++++++++++++++----- .../components/CampaignListRow.tsx | 5 +-- src/containers/CampaignList/utils.ts | 28 +++++-------- 4 files changed, 64 insertions(+), 46 deletions(-) diff --git a/src/containers/CampaignList/components/CampaignDetails.tsx b/src/containers/CampaignList/components/CampaignDetails.tsx index f65222815..0ccd59f69 100644 --- a/src/containers/CampaignList/components/CampaignDetails.tsx +++ b/src/containers/CampaignList/components/CampaignDetails.tsx @@ -1,5 +1,5 @@ import Chip from "@material-ui/core/Chip"; -import { blue, yellow } from "@material-ui/core/colors"; +import { blue, orange } from "@material-ui/core/colors"; import Divider from "@material-ui/core/Divider"; import type { Theme } from "@material-ui/core/styles"; import { useTheme } from "@material-ui/core/styles"; @@ -22,7 +22,11 @@ const inlineStyles = { whiteSpace: "pre-wrap", alignItems: "center" }, - chip: { margin: "4px", padding: "4px" } + chip: { + margin: "4px", + padding: "4px", + border: "1px white" + } }; interface CampaignDetailsProps { @@ -41,29 +45,36 @@ interface DueByIconProps { theme: Theme; } +const makeDueByLabel = (dueBy: DateTime, isPastDue: boolean): string => { + if (!dueBy.isValid) { + return "No due date set"; + } + return isPastDue + ? `Past due ${dueBy.toFormat("DD")}` + : `Due ${dueBy.toFormat("DD")}`; +}; + const DueByIcon: React.FC = (props) => { const { dueBy, theme } = props; - const pastDue = DateTime.local() >= dueBy; + const isPastDue = DateTime.local() >= dueBy; - const label = dueBy.isValid - ? pastDue - ? `past due ${dueBy.toFormat("DD")}` - : `due ${dueBy.toFormat("DD")}` - : "No due date set"; - const style = pastDue + const label = makeDueByLabel(dueBy, isPastDue); + const chipStyle = isPastDue ? { margin: "4px", - color: theme.palette.grey[900], - backgroundColor: yellow[300] + color: orange[300] } : { margin: "4px", color: theme.palette.grey[900] }; + const iconStyle = isPastDue + ? { color: orange[300] } + : { color: theme.palette.grey[900] }; return ( } + icon={} label={label} style={{ ...inlineStyles.chip, - ...style + ...chipStyle }} variant="outlined" /> @@ -82,6 +93,7 @@ const CampaignDetails: React.FC = (props) => { } = props; const theme = useTheme(); + // display van configuration and auto assign eligible tags in divided section const shouldShowExtraTags = externalSystem || isAutoAssignEligible; return ( diff --git a/src/containers/CampaignList/components/CampaignHeader.tsx b/src/containers/CampaignList/components/CampaignHeader.tsx index c82c07b41..0d3d7bab6 100644 --- a/src/containers/CampaignList/components/CampaignHeader.tsx +++ b/src/containers/CampaignList/components/CampaignHeader.tsx @@ -1,7 +1,8 @@ import { Divider, Typography } from "@material-ui/core"; import Chip from "@material-ui/core/Chip"; -import CheckIcon from "@material-ui/icons/Check"; -import WarningIcon from "@material-ui/icons/Warning"; +import { green, orange } from "@material-ui/core/colors"; +import CheckCircleOutlineRoundedIcon from "@material-ui/icons/CheckCircleOutlineRounded"; +import WarningRoundedIcon from "@material-ui/icons/WarningRounded"; import React from "react"; type IconMap = { @@ -9,11 +10,17 @@ type IconMap = { }; const CHIP_ICONS: IconMap = { - "Not started": , - "Unassigned contacts": , - "Unsent initial messages": , - "All contacts assigned": , - "All initials sent": + "Not Started": , + "Unassigned Contacts": , + "Unsent Initial Messages": ( + + ), + "All Contacts Assigned": ( + + ), + "All Initials Sent": ( + + ) }; const inlineStyles = { @@ -22,7 +29,11 @@ const inlineStyles = { flexWrap: "wrap", alignItems: "center" }, - chip: { margin: "4px", padding: "4px" } + chip: { + margin: "4px", + padding: "4px", + borderRadius: "2px" + } }; export type Tag = { @@ -59,6 +70,15 @@ const CampaignHeader = (props: CampaignHeaderProps) => { /> {tags.map((tag) => { const Icon = CHIP_ICONS[tag.title] ?? null; + // "Started" tag should have green background + const tagStyle = + tag.title === "Started" + ? { + backgroundColor: green[100] + } + : { + border: "1px white" + }; return ( { icon={Icon} style={{ ...inlineStyles.chip, - color: tag.color, - backgroundColor: tag.backgroundColor + ...tagStyle }} variant="outlined" /> diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index 4ac350bf0..cf2f4d1d3 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -4,7 +4,6 @@ import ListItem from "@material-ui/core/ListItem"; import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; import ListItemText from "@material-ui/core/ListItemText"; -import { useTheme } from "@material-ui/core/styles"; import type { CampaignListEntryFragment } from "@spoke/spoke-codegen"; import React from "react"; import { useHistory } from "react-router-dom"; @@ -27,7 +26,6 @@ interface Props extends CampaignOperations { } export const CampaignListRow: React.FC = (props) => { - const theme = useTheme(); const history = useHistory(); const { organizationId, @@ -55,8 +53,7 @@ export const CampaignListRow: React.FC = (props) => { isStarted, hasUnassignedContacts, hasUnsentInitialMessages, - hasUnhandledMessages, - theme + hasUnhandledMessages }); const isCampaignSelected = campaignDetailsForExport.find( diff --git a/src/containers/CampaignList/utils.ts b/src/containers/CampaignList/utils.ts index 5459be88f..8da1d7f98 100644 --- a/src/containers/CampaignList/utils.ts +++ b/src/containers/CampaignList/utils.ts @@ -1,5 +1,4 @@ /* eslint-disable import/prefer-default-export */ -import type { Theme } from "@material-ui/core"; import type { GraphQLError } from "graphql"; import type { Tag } from "./components/CampaignHeader"; @@ -17,60 +16,51 @@ type MakeCampaignTagsFn = (props: { hasUnassignedContacts: boolean | null | undefined; hasUnsentInitialMessages: boolean | null | undefined; hasUnhandledMessages: boolean | null | undefined; - theme: Theme; }) => Tag[]; export const makeCampaignHeaderTags: MakeCampaignTagsFn = ({ isStarted, hasUnassignedContacts, hasUnsentInitialMessages, - hasUnhandledMessages, - theme + hasUnhandledMessages }) => { const tags = []; - // display 'Started' or 'Not started' first + // display 'Started' or 'Not Started' first if (isStarted) { tags.push({ - title: "Started", - backgroundColor: theme.palette.success.light + title: "Started" }); } else { tags.push({ - title: "Not started", - backgroundColor: theme.palette.error.main + title: "Not Started" }); } if (hasUnassignedContacts) { tags.push({ - title: "Unassigned contacts", - backgroundColor: theme.palette.error.main + title: "Unassigned Contacts" }); } else { tags.push({ - title: "All contacts assigned", - backgroundColor: theme.palette.success.light + title: "All Contacts Assigned" }); } if (isStarted) { const tag = hasUnsentInitialMessages ? { - title: "Unsent initial messages", - backgroundColor: theme.palette.error.main + title: "Unsent Initial Messages" } : { - title: "All initials sent", - backgroundColor: theme.palette.success.light + title: "All Initials Sent" }; tags.push(tag); } if (isStarted && hasUnhandledMessages) { tags.push({ - title: "Unhandled replies", - backgroundColor: theme.palette.error.main + title: "Unhandled Replies" }); } From e847c341180060613397a2434a22e5dae8112a49 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Fri, 28 Jul 2023 17:34:53 -0400 Subject: [PATCH 19/27] feat(campaign-list): use rounded icons, improve export dialog styling --- .../ExportMultipleCampaignDataDialog.tsx | 49 +++++++++++-------- .../components/CampaignDetails.tsx | 18 +++---- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx index 34d148f8d..5ac98386e 100644 --- a/src/components/ExportMultipleCampaignDataDialog.tsx +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -4,8 +4,9 @@ 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 CheckBoxOutlinedIcon from "@material-ui/icons/CheckBoxOutlined"; +import AssignmentRoundedIcon from "@material-ui/icons/AssignmentRounded"; import { CampaignExportType, useExportCampaignsMutation @@ -77,6 +78,8 @@ const ExportMultipleCampaignDataDialog: React.FC = (props) => { onClose={onClose} aria-labelledby="export-multiple-campaign-data" open={open} + fullWidth + maxWidth="sm" > @@ -94,28 +97,34 @@ const ExportMultipleCampaignDataDialog: React.FC = (props) => { setExportOptOut={setExportOptOut} setExportFiltered={setExportFiltered} /> + - Exporting data for: - {campaignDetailsForExport.map((campaign) => { - return ( -
- - + Selected campaigns: + + {campaignDetailsForExport.map( + (campaign: CampaignDetailsForExport) => { + return ( +
- {campaign.title} - - - id: {campaign.id} - -
- ); - })} + + + {campaign.title} + + + id: {campaign.id} + +
+ ); + } + )}
diff --git a/src/containers/CampaignList/components/CampaignDetails.tsx b/src/containers/CampaignList/components/CampaignDetails.tsx index 0ccd59f69..4a83f52b6 100644 --- a/src/containers/CampaignList/components/CampaignDetails.tsx +++ b/src/containers/CampaignList/components/CampaignDetails.tsx @@ -3,11 +3,11 @@ import { blue, orange } from "@material-ui/core/colors"; import Divider from "@material-ui/core/Divider"; import type { Theme } from "@material-ui/core/styles"; import { useTheme } from "@material-ui/core/styles"; -import GroupWorkOutlinedIcon from "@material-ui/icons/GroupWorkOutlined"; +import AssignmentRoundedIcon from "@material-ui/icons/AssignmentRounded"; import LocalOfferOutlinedIcon from "@material-ui/icons/LocalOfferOutlined"; -import PeopleAltOutlinedIcon from "@material-ui/icons/PeopleAltOutlined"; -import PersonOutlineIcon from "@material-ui/icons/PersonOutline"; -import ScheduleIcon from "@material-ui/icons/Schedule"; +import PeopleOutlineRoundedIcon from "@material-ui/icons/PeopleOutlineRounded"; +import PersonOutlineRoundedIcon from "@material-ui/icons/PersonOutlineRounded"; +import ScheduleRoundedIcon from "@material-ui/icons/ScheduleRounded"; import type { CampaignListEntryFragment, ExternalSystem @@ -70,7 +70,7 @@ const DueByIcon: React.FC = (props) => { : { color: theme.palette.grey[900] }; return ( } + icon={} label={label} style={{ ...inlineStyles.chip, @@ -101,7 +101,7 @@ const CampaignDetails: React.FC = (props) => {
{creatorName ? ( } + icon={} label={creatorName} style={inlineStyles.chip} variant="outlined" @@ -110,7 +110,7 @@ const CampaignDetails: React.FC = (props) => { {teams.length > 0 ? ( } + icon={} label={teams .map((team: Record) => team.title) .join(", ")} @@ -118,9 +118,9 @@ const CampaignDetails: React.FC = (props) => { variant="outlined" /> ) : null} - {campaignGroups.length > 0 ? ( + {campaignGroups?.edges.length > 0 ? ( } + icon={} label={campaignGroups.edges .map(({ node }: { node: Record }) => node.name) .join(", ")} From abb78ff28dc77ddde7cdd13a4837df0a2dde9407 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 3 Aug 2023 15:50:05 -0400 Subject: [PATCH 20/27] refactor: require campaign ids, spoke options, rm export type from mutation input --- libs/gql-schema/schema.ts | 5 ++--- src/components/ExportMultipleCampaignDataDialog.tsx | 6 +----- src/schema.graphql | 5 ++--- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/libs/gql-schema/schema.ts b/libs/gql-schema/schema.ts index 46e9c0abe..4e5a4754d 100644 --- a/libs/gql-schema/schema.ts +++ b/libs/gql-schema/schema.ts @@ -216,9 +216,8 @@ const rootSchema = ` } input MultipleCampaignExportInput { - campaignIds: [String!] - exportType: CampaignExportType! - spokeOptions: ExportForSpokeInput + campaignIds: [String!]! + spokeOptions: ExportForSpokeInput! } input QuestionResponseSyncConfigInput { diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx index 5ac98386e..daf672955 100644 --- a/src/components/ExportMultipleCampaignDataDialog.tsx +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -7,10 +7,7 @@ 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 { useExportCampaignsMutation } from "@spoke/spoke-codegen"; import React, { useState } from "react"; import { CampaignExportModalContent } from "../containers/AdminCampaignStats/components/CampaignExportModal"; @@ -56,7 +53,6 @@ const ExportMultipleCampaignDataDialog: React.FC = (props) => { variables: { options: { campaignIds, - exportType: CampaignExportType.Spoke, spokeOptions: { campaign: exportCampaign, messages: exportMessages, diff --git a/src/schema.graphql b/src/schema.graphql index 3556ff026..69f9bf1f0 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -182,9 +182,8 @@ input CampaignExportInput { } input MultipleCampaignExportInput { - campaignIds: [String!] - exportType: CampaignExportType! - spokeOptions: ExportForSpokeInput + campaignIds: [String!]! + spokeOptions: ExportForSpokeInput! } input QuestionResponseSyncConfigInput { From 7ef2246004b3f1a9a7dca7024858dfac39c4f24c Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 3 Aug 2023 16:02:01 -0400 Subject: [PATCH 21/27] refactor: improve var names --- src/containers/AdminCampaignList.jsx | 26 +++++++++---------- .../components/CampaignListHeader.tsx | 4 +-- .../components/CampaignListRow.tsx | 4 +-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/containers/AdminCampaignList.jsx b/src/containers/AdminCampaignList.jsx index 5081aa4d9..72f1e99fa 100644 --- a/src/containers/AdminCampaignList.jsx +++ b/src/containers/AdminCampaignList.jsx @@ -57,8 +57,8 @@ class AdminCampaignList extends React.Component { releaseAllRepliesError: undefined, releaseAllRepliesResult: undefined, campaignDetailsForExport: [], - shouldShowExportModal: false, - shouldShowExportSnackbar: false, + showExportModal: false, + showExportSnackbar: false, exportErrorMessage: null }; @@ -175,15 +175,15 @@ class AdminCampaignList extends React.Component { handleClickExportButton = () => { this.setState({ - shouldShowExportModal: true + showExportModal: true }); }; handleErrorCampaignExport = (exportErrorMessage) => { this.setState({ exportErrorMessage, - shouldShowExportModal: false, - shouldShowExportSnackbar: true + showExportModal: false, + showExportSnackbar: true }); }; @@ -205,8 +205,8 @@ class AdminCampaignList extends React.Component { releasingAllReplies, releasingInProgress, campaignDetailsForExport, - shouldShowExportModal, - shouldShowExportSnackbar, + showExportModal, + showExportSnackbar, exportErrorMessage } = this.state; @@ -365,10 +365,10 @@ class AdminCampaignList extends React.Component { /> this.setState({ - shouldShowExportModal: false, + showExportModal: false, campaignDetailsForExport: [] }) } @@ -376,17 +376,17 @@ class AdminCampaignList extends React.Component { onComplete={() => this.setState({ campaignDetailsForExport: [], - shouldShowExportModal: false, - shouldShowExportSnackbar: true + showExportModal: false, + showExportSnackbar: true }) } /> { this.setState({ - shouldShowExportSnackbar: false, + showExportSnackbar: false, exportErrorMessage: null }); }} diff --git a/src/containers/CampaignList/components/CampaignListHeader.tsx b/src/containers/CampaignList/components/CampaignListHeader.tsx index 041f855bc..b30f3ae5f 100644 --- a/src/containers/CampaignList/components/CampaignListHeader.tsx +++ b/src/containers/CampaignList/components/CampaignListHeader.tsx @@ -18,7 +18,7 @@ const styles = { } }; -const FIVE_HUNDRED = 500; +const DEBOUNCE_INTERVAL = 500; interface Props { campaignDetailsForExport: CampaignDetailsForExport[]; @@ -31,7 +31,7 @@ const CampaignListHeader = (props: Props) => { const debounceSearchTerm = useDebouncedCallback((str: string) => { filterByCampaignTitle(str); - }, FIVE_HUNDRED); + }, DEBOUNCE_INTERVAL); const campaignIds = campaignDetailsForExport.map((campaign) => campaign.id); diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index cf2f4d1d3..ba4b4ffa7 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -56,7 +56,7 @@ export const CampaignListRow: React.FC = (props) => { hasUnhandledMessages }); - const isCampaignSelected = campaignDetailsForExport.find( + const isCampaignSelected = !!campaignDetailsForExport.find( (selectedCampaign: CampaignDetailsForExport) => selectedCampaign.id === campaign.id ); @@ -73,7 +73,7 @@ export const CampaignListRow: React.FC = (props) => { From b5bc27a9655e97104a8cb830b6ed42a02e9f5ff1 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 3 Aug 2023 16:12:17 -0400 Subject: [PATCH 22/27] feat: improve typing --- src/components/ExportMultipleCampaignDataDialog.tsx | 6 +++--- src/server/tasks/export-campaign.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx index daf672955..819cc720e 100644 --- a/src/components/ExportMultipleCampaignDataDialog.tsx +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -39,9 +39,9 @@ const ExportMultipleCampaignDataDialog: React.FC = (props) => { const [exportCampaignsMutation] = useExportCampaignsMutation(); - const handleChange = (setStateFunction: any) => ( - event: React.ChangeEvent - ) => { + const handleChange = ( + setStateFunction: React.Dispatch> + ) => (event: React.ChangeEvent) => { setStateFunction(event.target.checked); }; diff --git a/src/server/tasks/export-campaign.ts b/src/server/tasks/export-campaign.ts index 692042999..a1e3a2a22 100644 --- a/src/server/tasks/export-campaign.ts +++ b/src/server/tasks/export-campaign.ts @@ -387,7 +387,7 @@ export const processMessagesChunk = async ( interface UploadCampaignContacts { campaignTitle: string; - interactionSteps: Array; + interactionSteps: InteractionStepRecord[]; contactsCount: number; campaignId: number; helpers: ProgressTaskHelpers; @@ -635,12 +635,12 @@ export interface SpokeOptions { } export interface CampaignDataForExport { - fileNameKey: any; + fileNameKey: string; campaignId: number; - campaignTitle: any; + campaignTitle: string; contactsCount: number; helpers: ProgressTaskHelpers; - interactionSteps: any[]; + interactionSteps: InteractionStepRecord[]; campaignVariableNames: string[]; } @@ -755,7 +755,7 @@ export const exportCampaign: ProgressTask = async ( fileNameKey = `${fileNameKey}-${timestamp}`; } - const campaignData = { + const campaignMetaData = { fileNameKey, campaignId, campaignTitle, @@ -770,7 +770,7 @@ export const exportCampaign: ProgressTask = async ( campaignFilteredContactsExportUrl, campaignOptOutsExportUrl, campaignMessagesExportUrl - } = await processExportData(campaignData, spokeOptions); + } = await processExportData(campaignMetaData, spokeOptions); helpers.logger.debug("Waiting for streams to finish"); From 45ea9516d88ef1af11f1c42988263b3c7060e3f1 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 3 Aug 2023 16:27:21 -0400 Subject: [PATCH 23/27] feat: destructure props in component declaration --- src/components/ExportCampaignDataSnackbar.tsx | 8 +++--- .../ExportMultipleCampaignDataDialog.tsx | 15 +++++------ .../components/CampaignExportModal.tsx | 26 +++++++++---------- .../components/CampaignHeader.tsx | 8 ++++-- .../components/CampaignListHeader.tsx | 8 +++--- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/components/ExportCampaignDataSnackbar.tsx b/src/components/ExportCampaignDataSnackbar.tsx index 2e67d44fc..3aaabb9b7 100644 --- a/src/components/ExportCampaignDataSnackbar.tsx +++ b/src/components/ExportCampaignDataSnackbar.tsx @@ -8,9 +8,11 @@ interface Props { onClose: () => void; } -const ExportCampaignDataSnackbar: React.FC = (props) => { - const { open, errorMessage, onClose } = props; - +const ExportCampaignDataSnackbar: React.FC = ({ + open, + errorMessage, + onClose +}) => { return errorMessage ? ( {errorMessage} diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx index 819cc720e..8a14b837a 100644 --- a/src/components/ExportMultipleCampaignDataDialog.tsx +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -24,14 +24,13 @@ interface Props { onComplete(): void; } -const ExportMultipleCampaignDataDialog: React.FC = (props) => { - const { - campaignDetailsForExport, - open, - onClose, - onError, - onComplete - } = props; +const ExportMultipleCampaignDataDialog: React.FC = ({ + campaignDetailsForExport, + open, + onClose, + onError, + onComplete +}) => { const [exportCampaign, setExportCampaign] = useState(true); const [exportMessages, setExportMessages] = useState(true); const [exportOptOut, setExportOptOut] = useState(false); diff --git a/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx b/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx index 71416096d..1d7bf29f5 100644 --- a/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx +++ b/src/containers/AdminCampaignStats/components/CampaignExportModal.tsx @@ -32,20 +32,18 @@ interface CampaignExportModalContentProps { setExportOptOut: React.Dispatch>; setExportFiltered: React.Dispatch>; } -export const CampaignExportModalContent: React.FC = ( - props: CampaignExportModalContentProps -) => { - const { - exportCampaign, - exportMessages, - exportOptOut, - exportFiltered, - handleChange, - setExportCampaign, - setExportMessages, - setExportOptOut, - setExportFiltered - } = props; +// eslint-disable-next-line max-len +export const CampaignExportModalContent: React.FC = ({ + exportCampaign, + exportMessages, + exportOptOut, + exportFiltered, + handleChange, + setExportCampaign, + setExportMessages, + setExportOptOut, + setExportFiltered +}) => { return (
diff --git a/src/containers/CampaignList/components/CampaignHeader.tsx b/src/containers/CampaignList/components/CampaignHeader.tsx index 0d3d7bab6..c7cafb696 100644 --- a/src/containers/CampaignList/components/CampaignHeader.tsx +++ b/src/containers/CampaignList/components/CampaignHeader.tsx @@ -49,8 +49,12 @@ interface CampaignHeaderProps { onClick: () => void; } -const CampaignHeader = (props: CampaignHeaderProps) => { - const { campaignTitle, campaignId, tags, onClick } = props; +const CampaignHeader: React.FC = ({ + campaignTitle, + campaignId, + tags, + onClick +}) => { return (
void; } -const CampaignListHeader = (props: Props) => { - const { campaignDetailsForExport, onClick, filterByCampaignTitle } = props; - +const CampaignListHeader: React.FC = ({ + campaignDetailsForExport, + onClick, + filterByCampaignTitle +}) => { const debounceSearchTerm = useDebouncedCallback((str: string) => { filterByCampaignTitle(str); }, DEBOUNCE_INTERVAL); From 338b169579187a0a21186cbf270f1fe8d25b3d22 Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 3 Aug 2023 16:40:55 -0400 Subject: [PATCH 24/27] fix: revert campaign list menu changes --- src/containers/CampaignList/components/CampaignListMenu.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/containers/CampaignList/components/CampaignListMenu.tsx b/src/containers/CampaignList/components/CampaignListMenu.tsx index b155045be..1fdadb2c8 100644 --- a/src/containers/CampaignList/components/CampaignListMenu.tsx +++ b/src/containers/CampaignList/components/CampaignListMenu.tsx @@ -18,10 +18,12 @@ export interface CampaignOperations { ) => ClickHandler; archiveCampaign: (campaignId: string) => ClickHandler; unarchiveCampaign: (campaignId: string) => ClickHandler; - selectForExport: (campaignId: string) => void; +} +interface Props extends CampaignOperations { + campaign: CampaignListEntryFragment; } -export const CampaignListMenu: React.FC = (props) => { +export const CampaignListMenu: React.FC = (props) => { const [menuAnchor, setMenuAnchor] = useState(null); const { From dd9336528abdf51eeed877c70591f467b2aa6d1f Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Thu, 3 Aug 2023 16:42:04 -0400 Subject: [PATCH 25/27] feat(campaign-list-row): improve typing --- .../components/CampaignDetails.tsx | 29 ++++++++++--------- .../components/CampaignListRow.tsx | 11 +++++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/containers/CampaignList/components/CampaignDetails.tsx b/src/containers/CampaignList/components/CampaignDetails.tsx index 4a83f52b6..9ade5ff70 100644 --- a/src/containers/CampaignList/components/CampaignDetails.tsx +++ b/src/containers/CampaignList/components/CampaignDetails.tsx @@ -34,7 +34,7 @@ interface CampaignDetailsProps { description: string; creatorName: string | null; dueBy: DateTime; - isAutoassignEligible: boolean; + isAutoAssignEligible: boolean; teams: CampaignListEntryFragment["teams"]; campaignGroups: CampaignListEntryFragment["campaignGroups"]; externalSystem: Pick | null | undefined; @@ -54,8 +54,7 @@ const makeDueByLabel = (dueBy: DateTime, isPastDue: boolean): string => { : `Due ${dueBy.toFormat("DD")}`; }; -const DueByIcon: React.FC = (props) => { - const { dueBy, theme } = props; +const DueByIcon: React.FC = ({ dueBy, theme }) => { const isPastDue = DateTime.local() >= dueBy; const label = makeDueByLabel(dueBy, isPastDue); @@ -81,21 +80,23 @@ const DueByIcon: React.FC = (props) => { ); }; -const CampaignDetails: React.FC = (props) => { - const { - description, - creatorName, - dueBy, - externalSystem, - isAutoAssignEligible, - teams, - campaignGroups - } = props; +const CampaignDetails: React.FC = ({ + description, + creatorName, + dueBy, + externalSystem, + isAutoAssignEligible, + teams, + campaignGroups +}) => { const theme = useTheme(); // display van configuration and auto assign eligible tags in divided section const shouldShowExtraTags = externalSystem || isAutoAssignEligible; + const showCampaignGroupsTags = + campaignGroups?.edges && campaignGroups.edges?.length > 0; + return ( <>
@@ -118,7 +119,7 @@ const CampaignDetails: React.FC = (props) => { variant="outlined" /> ) : null} - {campaignGroups?.edges.length > 0 ? ( + {showCampaignGroupsTags ? ( } label={campaignGroups.edges diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index ba4b4ffa7..d57b4ddb7 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -64,6 +64,13 @@ export const CampaignListRow: React.FC = (props) => { const campaignUrl = `/admin/${organizationId}/campaigns/${campaign.id}${ isStarted ? "" : "/edit" }`; + + // satisfy typescript (boolean | null | undefined possible for these vars) + const isAutoAssignEligible = !!( + isStarted && + !isArchived && + isAutoassignEnabled + ); return ( = (props) => { teams={teams} campaignGroups={campaignGroups} externalSystem={externalSystem} - isAutoAssignEligible={ - isStarted && !isArchived && isAutoassignEnabled - } + isAutoAssignEligible={isAutoAssignEligible} /> } secondaryTypographyProps={{ color: "textPrimary" }} From b8348d0fc37b3d995b60480892b235b48ab3ad5d Mon Sep 17 00:00:00 2001 From: henryk1229 Date: Fri, 4 Aug 2023 14:37:55 -0400 Subject: [PATCH 26/27] refactor(campaign-list-row): styling changes --- .../components/CampaignDetails.tsx | 128 +++++++----------- .../components/CampaignHeader.tsx | 62 ++++----- .../components/CampaignListRow.tsx | 3 - src/containers/CampaignList/utils.ts | 31 +++-- 4 files changed, 93 insertions(+), 131 deletions(-) diff --git a/src/containers/CampaignList/components/CampaignDetails.tsx b/src/containers/CampaignList/components/CampaignDetails.tsx index 9ade5ff70..e2013b45a 100644 --- a/src/containers/CampaignList/components/CampaignDetails.tsx +++ b/src/containers/CampaignList/components/CampaignDetails.tsx @@ -1,21 +1,18 @@ import Chip from "@material-ui/core/Chip"; -import { blue, orange } from "@material-ui/core/colors"; +import { blue } from "@material-ui/core/colors"; import Divider from "@material-ui/core/Divider"; -import type { Theme } from "@material-ui/core/styles"; -import { useTheme } from "@material-ui/core/styles"; -import AssignmentRoundedIcon from "@material-ui/icons/AssignmentRounded"; +import Tooltip from "@material-ui/core/Tooltip"; +import DescriptionOutlinedIcon from "@material-ui/icons/DescriptionOutlined"; import LocalOfferOutlinedIcon from "@material-ui/icons/LocalOfferOutlined"; import PeopleOutlineRoundedIcon from "@material-ui/icons/PeopleOutlineRounded"; import PersonOutlineRoundedIcon from "@material-ui/icons/PersonOutlineRounded"; -import ScheduleRoundedIcon from "@material-ui/icons/ScheduleRounded"; +import RecordVoiceOverIcon from "@material-ui/icons/RecordVoiceOver"; import type { CampaignListEntryFragment, ExternalSystem } from "@spoke/spoke-codegen"; import React from "react"; -import { DateTime } from "../../../lib/datetime"; - const inlineStyles = { wrapper: { display: "flex", @@ -25,7 +22,7 @@ const inlineStyles = { chip: { margin: "4px", padding: "4px", - border: "1px white" + color: "#666666" } }; @@ -33,66 +30,22 @@ interface CampaignDetailsProps { id: string; description: string; creatorName: string | null; - dueBy: DateTime; isAutoAssignEligible: boolean; teams: CampaignListEntryFragment["teams"]; campaignGroups: CampaignListEntryFragment["campaignGroups"]; externalSystem: Pick | null | undefined; } -interface DueByIconProps { - dueBy: DateTime; - theme: Theme; -} - -const makeDueByLabel = (dueBy: DateTime, isPastDue: boolean): string => { - if (!dueBy.isValid) { - return "No due date set"; - } - return isPastDue - ? `Past due ${dueBy.toFormat("DD")}` - : `Due ${dueBy.toFormat("DD")}`; -}; - -const DueByIcon: React.FC = ({ dueBy, theme }) => { - const isPastDue = DateTime.local() >= dueBy; - - const label = makeDueByLabel(dueBy, isPastDue); - const chipStyle = isPastDue - ? { - margin: "4px", - color: orange[300] - } - : { margin: "4px", color: theme.palette.grey[900] }; - const iconStyle = isPastDue - ? { color: orange[300] } - : { color: theme.palette.grey[900] }; - return ( - } - label={label} - style={{ - ...inlineStyles.chip, - ...chipStyle - }} - variant="outlined" - /> - ); -}; - const CampaignDetails: React.FC = ({ description, creatorName, - dueBy, externalSystem, isAutoAssignEligible, teams, campaignGroups }) => { - const theme = useTheme(); - // display van configuration and auto assign eligible tags in divided section - const shouldShowExtraTags = externalSystem || isAutoAssignEligible; + const showExtraTags = externalSystem || isAutoAssignEligible; const showCampaignGroupsTags = campaignGroups?.edges && campaignGroups.edges?.length > 0; @@ -101,51 +54,66 @@ const CampaignDetails: React.FC = ({ <>
{creatorName ? ( - } - label={creatorName} - style={inlineStyles.chip} - variant="outlined" - /> + + } + label={creatorName} + style={inlineStyles.chip} + variant="outlined" + /> + ) : null} - {teams.length > 0 ? ( - } - label={teams - .map((team: Record) => team.title) - .join(", ")} - style={inlineStyles.chip} - variant="outlined" - /> + + } + label={teams + .map((team: Record) => team.title) + .join(", ")} + style={inlineStyles.chip} + variant="outlined" + /> + ) : null} {showCampaignGroupsTags ? ( - } - label={campaignGroups.edges - .map(({ node }: { node: Record }) => node.name) - .join(", ")} - style={inlineStyles.chip} - variant="outlined" - /> + + } + label={campaignGroups.edges + .map(({ node }: { node: Record }) => node.name) + .join(", ")} + style={inlineStyles.chip} + variant="outlined" + /> + ) : null}
-
Description: {description}
- {shouldShowExtraTags ? ( +
+ +
+ + {description} +
+
+
+ {showExtraTags ? ( <>
{isAutoAssignEligible ? ( } + icon={} label="Auto-Assign Eligible" style={inlineStyles.chip} /> ) : null} {externalSystem ? ( } + icon={} label={`${externalSystem.type}: ${externalSystem.name}`} style={{ ...inlineStyles.chip, backgroundColor: blue[300] }} /> diff --git a/src/containers/CampaignList/components/CampaignHeader.tsx b/src/containers/CampaignList/components/CampaignHeader.tsx index c7cafb696..c24571443 100644 --- a/src/containers/CampaignList/components/CampaignHeader.tsx +++ b/src/containers/CampaignList/components/CampaignHeader.tsx @@ -1,28 +1,9 @@ import { Divider, Typography } from "@material-ui/core"; import Chip from "@material-ui/core/Chip"; -import { green, orange } from "@material-ui/core/colors"; -import CheckCircleOutlineRoundedIcon from "@material-ui/icons/CheckCircleOutlineRounded"; -import WarningRoundedIcon from "@material-ui/icons/WarningRounded"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import ErrorIcon from "@material-ui/icons/Error"; import React from "react"; -type IconMap = { - [key: string]: JSX.Element; -}; - -const CHIP_ICONS: IconMap = { - "Not Started": , - "Unassigned Contacts": , - "Unsent Initial Messages": ( - - ), - "All Contacts Assigned": ( - - ), - "All Initials Sent": ( - - ) -}; - const inlineStyles = { wrapper: { display: "flex", @@ -31,15 +12,13 @@ const inlineStyles = { }, chip: { margin: "4px", - padding: "4px", - borderRadius: "2px" + padding: "4px" } }; export type Tag = { title: string; - backgroundColor?: string; - color?: string; + status: string; }; interface CampaignHeaderProps { @@ -58,14 +37,17 @@ const CampaignHeader: React.FC = ({ return (
{campaignTitle} - - id: {campaignId} + + ID: {campaignId} = ({ style={{ margin: "4px 8px 4px 8px" }} /> {tags.map((tag) => { - const Icon = CHIP_ICONS[tag.title] ?? null; - // "Started" tag should have green background - const tagStyle = - tag.title === "Started" + // display check or alert icon + const Icon = + tag.status === "success" ? ( + + ) : ( + + ); + // display green or orange background + const backgroundColor = + tag.status === "success" ? { - backgroundColor: green[100] + backgroundColor: "#DFF0DF" } : { - border: "1px white" + backgroundColor: "#FFF2E9" }; return ( ); })} diff --git a/src/containers/CampaignList/components/CampaignListRow.tsx b/src/containers/CampaignList/components/CampaignListRow.tsx index d57b4ddb7..8b2b413b1 100644 --- a/src/containers/CampaignList/components/CampaignListRow.tsx +++ b/src/containers/CampaignList/components/CampaignListRow.tsx @@ -10,7 +10,6 @@ import { useHistory } from "react-router-dom"; import type { CampaignDetailsForExport } from "../../../components/ExportMultipleCampaignDataDialog"; import { dataTest } from "../../../lib/attributes"; -import { DateTime } from "../../../lib/datetime"; import { makeCampaignHeaderTags } from "../utils"; import CampaignDetails from "./CampaignDetails"; import CampaignHeader from "./CampaignHeader"; @@ -46,7 +45,6 @@ export const CampaignListRow: React.FC = (props) => { externalSystem } = campaign; - const dueBy = DateTime.fromISO(campaign.dueBy || ""); const creatorName = campaign.creator ? campaign.creator.displayName : null; const headerTags = makeCampaignHeaderTags({ @@ -101,7 +99,6 @@ export const CampaignListRow: React.FC = (props) => { Date: Fri, 4 Aug 2023 15:08:53 -0400 Subject: [PATCH 27/27] refactor(multi-export-dialog): styling changes --- src/components/ExportMultipleCampaignDataDialog.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/ExportMultipleCampaignDataDialog.tsx b/src/components/ExportMultipleCampaignDataDialog.tsx index 8a14b837a..4e90ac443 100644 --- a/src/components/ExportMultipleCampaignDataDialog.tsx +++ b/src/components/ExportMultipleCampaignDataDialog.tsx @@ -6,7 +6,6 @@ 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 { useExportCampaignsMutation } from "@spoke/spoke-codegen"; import React, { useState } from "react"; @@ -109,12 +108,14 @@ const ExportMultipleCampaignDataDialog: React.FC = ({ margin: "4px" }} > - - + {campaign.title} - - id: {campaign.id} + + ID: {campaign.id}
);