Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add canned responses to message review #1142

Merged
merged 6 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions libs/spoke-codegen/src/graphql/canned-responses.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
fragment CannedResponseInfo on CannedResponse {
id
title
text
}

query GetCampaignCannedResponses($campaignId: String!) {
campaign(id: $campaignId) {
id
cannedResponses {
...CannedResponseInfo
}
}
}
2 changes: 1 addition & 1 deletion src/api/campaign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export const schema = `
hasUnsentInitialMessages: Boolean
hasUnhandledMessages: Boolean
customFields: [String]
cannedResponses(userId: String): [CannedResponse]
cannedResponses(userId: String): [CannedResponse!]!
stats: CampaignStats,
pendingJobs(jobTypes: [String]): [JobRequest]!
datawarehouseAvailable: Boolean
Expand Down
6 changes: 3 additions & 3 deletions src/api/canned-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export const schema = `
}

type CannedResponse {
id: ID
title: String
text: String
id: ID!
title: String!
text: String!
isUserCreated: Boolean
}
`;
98 changes: 0 additions & 98 deletions src/components/CannedResponseMenu.jsx

This file was deleted.

62 changes: 62 additions & 0 deletions src/components/CannedResponseMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import ListItemText from "@material-ui/core/ListItemText";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import { useGetCampaignCannedResponsesQuery } from "@spoke/spoke-codegen";
import React from "react";

interface CannedResponseMenuProps {
bchrobot marked this conversation as resolved.
Show resolved Hide resolved
anchorEl?: Element;
campaignId?: string;
bchrobot marked this conversation as resolved.
Show resolved Hide resolved
onSelectCannedResponse?: (script: string) => Promise<void> | void;
onRequestClose?: () => Promise<void> | void;
}

const CannedResponseMenu: React.FC<CannedResponseMenuProps> = (props) => {
const { campaignId, anchorEl } = props;
const { data, loading, error } = useGetCampaignCannedResponsesQuery({
variables: { campaignId: campaignId! },
skip: campaignId === undefined
});

const handleOnClickFactory = (script: string) => () =>
props.onSelectCannedResponse?.(script);

const cannedResponses = data?.campaign?.cannedResponses ?? [];

return (
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={props?.onRequestClose}
>
{loading && (
<MenuItem disabled>
<ListItemText primary="Loading..." />
</MenuItem>
)}
{error && (
<MenuItem disabled>
<ListItemText primary={`Error: ${error.message}`} />
</MenuItem>
)}
{cannedResponses.length === 0 && (
<MenuItem disabled>
<ListItemText primary="No canned responses for this campaign" />
</MenuItem>
)}
{cannedResponses.map((response) => {
return (
<MenuItem
key={response.id}
onClick={handleOnClickFactory(response.text)}
>
<ListItemText primary={response.title} secondary={response.text} />
</MenuItem>
);
})}
</Menu>
);
};

export default CannedResponseMenu;
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ import SpokeFormField from "../../forms/SpokeFormField";
import MessageLengthInfo from "../../MessageLengthInfo";
import SendButton from "../../SendButton";

interface MessageFormValue {
messageText: string;
}

interface InnerProps {
conversation: Conversation;
value?: string;
onChange?: (value: string) => Promise<void> | void;
messagesChanged(messages: Message[]): Promise<void> | void;
}

Expand All @@ -39,14 +37,12 @@ interface HocProps {
interface Props extends InnerProps, HocProps {}

interface State {
messageText: string;
isSending: boolean;
sendError: string;
}

class MessageResponse extends Component<Props, State> {
state: State = {
messageText: "",
isSending: false,
sendError: ""
};
Expand All @@ -65,7 +61,7 @@ class MessageResponse extends Component<Props, State> {
};

handleMessageFormChange = ({ messageText }: MessageFormValue) =>
this.setState({ messageText });
this.props.onChange?.(messageText);

handleMessageFormSubmit = async ({ messageText }: MessageFormValue) => {
const { contact } = this.props.conversation;
Expand All @@ -82,7 +78,7 @@ class MessageResponse extends Component<Props, State> {
);
const { messages } = response.data.sendMessage;
this.props.messagesChanged(messages);
this.setState({ messageText: "" });
this.props.onChange?.("");
} catch (e) {
this.setState({ sendError: e.message });
} finally {
Expand All @@ -106,8 +102,8 @@ class MessageResponse extends Component<Props, State> {
.max(window.MAX_MESSAGE_LENGTH)
});

const { messageText, isSending } = this.state;
const isSendDisabled = isSending || messageText.trim() === "";
const { isSending } = this.state;
const isSendDisabled = isSending || this.props.value?.trim() === "";

const errorActions = [
<FlatButton
Expand All @@ -125,7 +121,7 @@ class MessageResponse extends Component<Props, State> {
this.messageForm = ref;
}}
schema={messageSchema}
value={{ messageText: this.state.messageText }}
value={{ messageText: this.props.value ?? "" }}
onSubmit={this.handleMessageFormSubmit}
onChange={this.handleMessageFormChange}
>
Expand All @@ -140,7 +136,7 @@ class MessageResponse extends Component<Props, State> {
rowsMax={6}
style={{ flexGrow: "1" }}
/>
<MessageLengthInfo messageText={messageText} />
<MessageLengthInfo messageText={this.props.value} />
</div>
<SendButton
threeClickEnabled={false}
Expand Down
77 changes: 62 additions & 15 deletions src/components/IncomingMessageList/MessageColumn/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import Button from "@material-ui/core/Button";
import Grid from "@material-ui/core/Grid";
import {
ConversationInfoFragment,
ConversationMessageFragment
} from "@spoke/spoke-codegen";
import isNil from "lodash/isNil";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import CannedResponseMenu from "src/components/CannedResponseMenu";

import MessageList from "./MessageList";
import MessageOptOut from "./MessageOptOut";
Expand All @@ -17,6 +20,8 @@ const styles: Record<string, React.CSSProperties> = {
}
};

type ClickButtonHandler = React.MouseEventHandler<HTMLButtonElement>;

interface Props {
organizationId: string;
conversation: ConversationInfoFragment;
Expand All @@ -26,6 +31,8 @@ const MessageColumn: React.FC<Props> = (props) => {
const { organizationId, conversation } = props;
const { contact } = conversation;

const [messageText, setMessageText] = useState("");
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const [isOptedOut, setIsOptedOut] = useState(
!isNil(conversation.contact.optOut?.cell)
);
Expand All @@ -36,22 +43,62 @@ const MessageColumn: React.FC<Props> = (props) => {
setMessages(conversation.contact.messages);
}, [setMessages]);

const handleOpenCannedResponse: ClickButtonHandler = useCallback(
(event) => {
setAnchorEl(event.currentTarget);
},
[setAnchorEl]
);

const handleScriptSelected = useCallback(
(script: string) => {
setAnchorEl(null);
setMessageText(script);
},
[setAnchorEl]
);

const handleRequestClose = useCallback(() => setAnchorEl(null), [
setAnchorEl
]);

return (
<div style={styles.container}>
<h4>Messages</h4>
<MessageList messages={messages} organizationId={organizationId} />
{!isOptedOut && (
<MessageResponse
conversation={conversation}
messagesChanged={setMessages}
/>
)}
<MessageOptOut
contact={contact}
isOptedOut={isOptedOut}
optOutChanged={setIsOptedOut}
<>
<div style={styles.container}>
<h4>Messages</h4>
<MessageList messages={messages} organizationId={organizationId} />
{!isOptedOut && (
<MessageResponse
value={messageText}
conversation={conversation}
messagesChanged={setMessages}
onChange={setMessageText}
/>
)}
<Grid container spacing={2} justify="flex-end">
<Grid item>
<MessageOptOut
contact={contact}
isOptedOut={isOptedOut}
optOutChanged={setIsOptedOut}
/>
</Grid>
{!isOptedOut && (
<Grid item>
<Button variant="contained" onClick={handleOpenCannedResponse}>
Canned Responses
</Button>
</Grid>
)}
</Grid>
</div>
<CannedResponseMenu
campaignId={conversation.campaign.id ?? undefined}
anchorEl={anchorEl ?? undefined}
onSelectCannedResponse={handleScriptSelected}
onRequestClose={handleRequestClose}
/>
</div>
</>
);
};

Expand Down
Loading