Skip to content

Commit b6a6325

Browse files
authored
feat(autosend): add basic autosend page (#1350)
Reviewed-by: @bchrobot Reviewed-by: @hiemanshu
1 parent fcb6004 commit b6a6325

20 files changed

+612
-90
lines changed

libs/spoke-codegen/src/graphql/autosending.graphql

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
fragment AutosendingTarget on Campaign {
1+
fragment BasicAutosendingTarget on Campaign {
22
id
33
title
44
isStarted
55
autosendStatus
6+
}
7+
8+
fragment DetailedAutosendingTarget on Campaign {
69
contactsCount
710
stats {
811
optOutsCount
@@ -20,9 +23,15 @@ fragment AutosendingTarget on Campaign {
2023
}
2124
}
2225

26+
fragment AutosendingTarget on Campaign {
27+
...BasicAutosendingTarget
28+
...DetailedAutosendingTarget
29+
}
30+
2331
query CampaignsEligibleForAutosending(
2432
$organizationId: String!
2533
$isStarted: Boolean!
34+
$isBasic: Boolean!
2635
) {
2736
organization(id: $organizationId) {
2837
id
@@ -31,7 +40,8 @@ query CampaignsEligibleForAutosending(
3140
cursor: { offset: 0, limit: 5000 }
3241
) {
3342
campaigns {
34-
...AutosendingTarget
43+
...BasicAutosendingTarget
44+
...DetailedAutosendingTarget @skip (if: $isBasic)
3545
}
3646
}
3747
}

libs/spoke-codegen/src/graphql/general-settings.graphql

+23
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,26 @@ mutation UpdateScriptPreviewSettings(
5353
scriptPreviewForSupervolunteers
5454
}
5555
}
56+
57+
query GetAutosendingSettings($organizationId: String!) {
58+
organization(id: $organizationId) {
59+
id
60+
settings {
61+
id
62+
defaultAutosendingControlsMode
63+
}
64+
}
65+
}
66+
67+
mutation UpdateAutosendingSettings(
68+
$organizationId: String!
69+
$controlsMode: AutosendingControlsMode
70+
) {
71+
editOrganizationSettings(
72+
id: $organizationId
73+
input: { defaultAutosendingControlsMode: $controlsMode }
74+
) {
75+
id
76+
defaultAutosendingControlsMode
77+
}
78+
}

libs/spoke-codegen/src/graphql/spoke-context.graphql

+1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ fragment OrganizationSettingsInfo on OrganizationSettings {
1919
startCampaignRequiresApproval
2020
scriptPreviewForSupervolunteers
2121
defaultCampaignBuilderMode
22+
defaultAutosendingControlsMode
2223
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
exports.up = function up(knex) {
2+
return knex.schema.raw(
3+
`
4+
create or replace view sendable_campaigns as (
5+
select id, title, organization_id, limit_assignment_to_teams, autosend_status, is_autoassign_enabled
6+
from campaign
7+
where is_started and not is_archived
8+
);
9+
10+
create or replace view assignable_campaigns as (
11+
select id, title, organization_id, limit_assignment_to_teams, autosend_status
12+
from sendable_campaigns
13+
where is_autoassign_enabled
14+
);
15+
16+
create or replace view assignable_campaigns_with_needs_message as (
17+
select *
18+
from assignable_campaigns
19+
where
20+
exists (
21+
select 1
22+
from assignable_needs_message
23+
where campaign_id = assignable_campaigns.id
24+
)
25+
and not exists (
26+
select 1
27+
from campaign
28+
where campaign.id = assignable_campaigns.id
29+
and now() > date_trunc('day', (due_by + interval '24 hours') at time zone campaign.timezone)
30+
)
31+
and autosend_status <> 'sending'
32+
);
33+
34+
create or replace view assignable_campaigns_with_needs_reply as (
35+
select *
36+
from assignable_campaigns
37+
where exists (
38+
select 1
39+
from assignable_needs_reply
40+
where campaign_id = assignable_campaigns.id
41+
)
42+
);
43+
44+
create or replace view autosend_campaigns_to_send as (
45+
select *
46+
from sendable_campaigns
47+
where
48+
exists ( -- assignable contacts are valid for both autoassign and autosending
49+
select 1
50+
from assignable_needs_message
51+
where campaign_id = sendable_campaigns.id
52+
)
53+
and not exists (
54+
select 1
55+
from campaign
56+
where campaign.id = sendable_campaigns.id
57+
and now() > date_trunc('day', (due_by + interval '24 hours') at time zone campaign.timezone)
58+
)
59+
and autosend_status = 'sending'
60+
);
61+
`
62+
);
63+
};
64+
65+
exports.down = function down(knex) {
66+
return knex.schema.raw(
67+
`
68+
drop view autosend_campaigns_to_send;
69+
drop view assignable_campaigns_with_needs_reply;
70+
drop view assignable_campaigns_with_needs_message;
71+
drop view assignable_campaigns;
72+
drop view sendable_campaigns;
73+
74+
create or replace view assignable_campaigns as (
75+
select id, title, organization_id, limit_assignment_to_teams, autosend_status
76+
from campaign
77+
where is_started = true
78+
and is_archived = false
79+
and is_autoassign_enabled = true
80+
);
81+
82+
create or replace view assignable_campaigns_with_needs_message as (
83+
select *
84+
from assignable_campaigns
85+
where
86+
exists (
87+
select 1
88+
from assignable_needs_message
89+
where campaign_id = assignable_campaigns.id
90+
)
91+
and not exists (
92+
select 1
93+
from campaign
94+
where campaign.id = assignable_campaigns.id
95+
and now() > date_trunc('day', (due_by + interval '24 hours') at time zone campaign.timezone)
96+
)
97+
and autosend_status <> 'sending'
98+
);
99+
100+
create or replace view assignable_campaigns_with_needs_reply as (
101+
select *
102+
from assignable_campaigns
103+
where exists (
104+
select 1
105+
from assignable_needs_reply
106+
where campaign_id = assignable_campaigns.id
107+
)
108+
);
109+
`
110+
);
111+
};

schema-dump.sql

+44-5
Original file line numberDiff line numberDiff line change
@@ -1597,17 +1597,34 @@ CREATE VIEW public.assignable_campaign_contacts_with_escalation_tags AS
15971597
ALTER TABLE public.assignable_campaign_contacts_with_escalation_tags OWNER TO postgres;
15981598

15991599
--
1600-
-- Name: assignable_campaigns; Type: VIEW; Schema: public; Owner: postgres
1600+
-- Name: sendable_campaigns; Type: VIEW; Schema: public; Owner: postgres
16011601
--
16021602

1603-
CREATE VIEW public.assignable_campaigns AS
1603+
CREATE VIEW public.sendable_campaigns AS
16041604
SELECT campaign.id,
16051605
campaign.title,
16061606
campaign.organization_id,
16071607
campaign.limit_assignment_to_teams,
1608-
campaign.autosend_status
1608+
campaign.autosend_status,
1609+
campaign.is_autoassign_enabled
16091610
FROM public.campaign
1610-
WHERE ((campaign.is_started = true) AND (campaign.is_archived = false) AND (campaign.is_autoassign_enabled = true));
1611+
WHERE (campaign.is_started AND (NOT campaign.is_archived));
1612+
1613+
1614+
ALTER TABLE public.sendable_campaigns OWNER TO postgres;
1615+
1616+
--
1617+
-- Name: assignable_campaigns; Type: VIEW; Schema: public; Owner: postgres
1618+
--
1619+
1620+
CREATE VIEW public.assignable_campaigns AS
1621+
SELECT sendable_campaigns.id,
1622+
sendable_campaigns.title,
1623+
sendable_campaigns.organization_id,
1624+
sendable_campaigns.limit_assignment_to_teams,
1625+
sendable_campaigns.autosend_status
1626+
FROM public.sendable_campaigns
1627+
WHERE sendable_campaigns.is_autoassign_enabled;
16111628

16121629

16131630
ALTER TABLE public.assignable_campaigns OWNER TO postgres;
@@ -1670,7 +1687,8 @@ CREATE VIEW public.assignable_campaigns_with_needs_reply AS
16701687
SELECT assignable_campaigns.id,
16711688
assignable_campaigns.title,
16721689
assignable_campaigns.organization_id,
1673-
assignable_campaigns.limit_assignment_to_teams
1690+
assignable_campaigns.limit_assignment_to_teams,
1691+
assignable_campaigns.autosend_status
16741692
FROM public.assignable_campaigns
16751693
WHERE (EXISTS ( SELECT 1
16761694
FROM public.assignable_needs_reply
@@ -1776,6 +1794,27 @@ ALTER TABLE public.assignment_request_id_seq OWNER TO postgres;
17761794
ALTER SEQUENCE public.assignment_request_id_seq OWNED BY public.assignment_request.id;
17771795

17781796

1797+
--
1798+
-- Name: autosend_campaigns_to_send; Type: VIEW; Schema: public; Owner: postgres
1799+
--
1800+
1801+
CREATE VIEW public.autosend_campaigns_to_send AS
1802+
SELECT sendable_campaigns.id,
1803+
sendable_campaigns.title,
1804+
sendable_campaigns.organization_id,
1805+
sendable_campaigns.limit_assignment_to_teams,
1806+
sendable_campaigns.autosend_status,
1807+
sendable_campaigns.is_autoassign_enabled
1808+
FROM public.sendable_campaigns
1809+
WHERE ((EXISTS ( SELECT 1
1810+
FROM public.assignable_needs_message
1811+
WHERE (assignable_needs_message.campaign_id = sendable_campaigns.id))) AND (NOT (EXISTS ( SELECT 1
1812+
FROM public.campaign
1813+
WHERE ((campaign.id = sendable_campaigns.id) AND (now() > date_trunc('day'::text, timezone(campaign.timezone, (campaign.due_by + '24:00:00'::interval)))))))) AND (sendable_campaigns.autosend_status = 'sending'::text));
1814+
1815+
1816+
ALTER TABLE public.autosend_campaigns_to_send OWNER TO postgres;
1817+
17791818
--
17801819
-- Name: campaign_contact_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
17811820
--

src/api/organization-settings.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { AutosendingControlsMode } from "@spoke/spoke-codegen";
2+
13
import type { RequestAutoApproveType } from "./organization-membership";
24

35
export interface OrganizationSettingsInput {
@@ -12,6 +14,7 @@ export interface OrganizationSettingsInput {
1214
scriptPreviewForSupervolunteers: boolean | null;
1315
showDoNotAssignMessage: boolean | null;
1416
doNotAssignMessage: string | null;
17+
defaultAutosendingControlsMode: AutosendingControlsMode | null;
1518

1619
// Superadmin
1720
startCampaignRequiresApproval: boolean | null;
@@ -32,6 +35,9 @@ export interface OrganizationSettings {
3235
startCampaignRequiresApproval: boolean | null;
3336
scriptPreviewForSupervolunteers: boolean | null;
3437

38+
// Admin
39+
defaultAutosendingControlsMode: AutosendingControlsMode;
40+
3541
// Owner
3642
defaulTexterApprovalStatus: RequestAutoApproveType | null;
3743
numbersApiKey: string | null;
@@ -52,6 +58,7 @@ export const schema = `
5258
defaultCampaignBuilderMode: CampaignBuilderMode
5359
showDoNotAssignMessage: Boolean
5460
doNotAssignMessage: String
61+
defaultAutosendingControlsMode: AutosendingControlsMode
5562
5663
# Superadmin
5764
startCampaignRequiresApproval: Boolean
@@ -73,6 +80,9 @@ export const schema = `
7380
scriptPreviewForSupervolunteers: Boolean
7481
defaultCampaignBuilderMode: CampaignBuilderMode
7582
83+
# Admin
84+
defaultAutosendingControlsMode: AutosendingControlsMode
85+
7686
# Owner
7787
defaulTexterApprovalStatus: RequestAutoApprove
7888
numbersApiKey: String

src/api/schema.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ import { schema as trollbotSchema } from "./trollbot";
3232
import { schema as userSchema } from "./user";
3333

3434
const rootSchema = `
35+
enum AutosendingControlsMode {
36+
BASIC
37+
DETAILED
38+
}
39+
3540
enum CampaignBuilderMode {
3641
BASIC
3742
ADVANCED
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Button from "@material-ui/core/Button";
2+
import Chip from "@material-ui/core/Chip";
3+
import TableCell from "@material-ui/core/TableCell";
4+
import TableRow from "@material-ui/core/TableRow";
5+
import MoreIcon from "@material-ui/icons/ArrowForward";
6+
import PauseIcon from "@material-ui/icons/Pause";
7+
import PlayIcon from "@material-ui/icons/PlayArrow";
8+
import { AutosendingTargetFragment } from "@spoke/spoke-codegen";
9+
import React from "react";
10+
import { Link } from "react-router-dom";
11+
12+
import useChipStyles from "./chipStyles";
13+
14+
interface AutosendingTargetRowProps {
15+
target: AutosendingTargetFragment;
16+
organizationId: string;
17+
disabled?: boolean;
18+
onStart?: () => Promise<unknown> | unknown;
19+
onPause?: () => Promise<unknown> | unknown;
20+
}
21+
22+
export const AutosendingTargetRow: React.FC<AutosendingTargetRowProps> = (
23+
props
24+
) => {
25+
const { target, organizationId, disabled = false, onStart, onPause } = props;
26+
const chipClasses = useChipStyles();
27+
const statusChipDisplay = target.autosendStatus;
28+
29+
const chipRootClass =
30+
statusChipDisplay === "sending"
31+
? chipClasses.sending
32+
: statusChipDisplay === "paused"
33+
? chipClasses.paused
34+
: statusChipDisplay === "complete"
35+
? chipClasses.complete
36+
: chipClasses.unstarted;
37+
38+
return (
39+
<TableRow>
40+
<TableCell>
41+
{target.id}: {target.title}
42+
</TableCell>
43+
<TableCell>
44+
<Chip label={statusChipDisplay} classes={{ root: chipRootClass }} />
45+
</TableCell>
46+
<TableCell>
47+
{target.autosendStatus ===
48+
"complete" ? undefined : target.autosendStatus === "sending" ||
49+
target.autosendStatus === "holding" ? (
50+
<Button
51+
variant="contained"
52+
startIcon={<PauseIcon />}
53+
disabled={disabled}
54+
onClick={onPause}
55+
>
56+
Pause
57+
</Button>
58+
) : (
59+
<Button
60+
variant="contained"
61+
startIcon={<PlayIcon />}
62+
disabled={disabled}
63+
onClick={onStart}
64+
>
65+
{target.autosendStatus === "unstarted" ? "Queue" : "Resume"}
66+
</Button>
67+
)}
68+
</TableCell>
69+
<TableCell>
70+
<Link to={`/admin/${organizationId}/campaigns/${target.id}`}>
71+
<MoreIcon />
72+
</Link>
73+
</TableCell>
74+
</TableRow>
75+
);
76+
};
77+
78+
export default AutosendingTargetRow;

0 commit comments

Comments
 (0)