Skip to content

Commit

Permalink
feat: allow users to add auto reply triggers
Browse files Browse the repository at this point in the history
  • Loading branch information
ajohn25 committed Aug 12, 2023
1 parent 66eaebb commit 735f7ed
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 77 deletions.
15 changes: 15 additions & 0 deletions libs/gql-schema/interaction-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,20 @@ export const schema = `
createdAt: Date
interactionSteps: [InteractionStepInput]
}
type InteractionStepWithChildren {
id: ID!
question: Question
questionText: String
scriptOptions: [String]!
answerOption: String
parentInteractionId: String
autoReplyTokens: [String]
isDeleted: Boolean
answerActions: String
questionResponse(campaignContactId: String): QuestionResponse
createdAt: Date!
interactionSteps: [InteractionStep]
}
`;
export default schema;
99 changes: 99 additions & 0 deletions src/components/forms/GSAutoReplyTokensField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import uniqBy from "lodash/uniqBy";
import type { KeyboardEventHandler } from "react";
import React, { useEffect, useRef, useState } from "react";
import CreatableSelect from "react-select/creatable";

import { optOutTriggers } from "../../lib/opt-out-triggers";
import type { GSFormFieldProps } from "./GSFormField";

interface GSAutoReplyTokensFieldProps extends GSFormFieldProps {
selectedOptions?: Option[];
disabled: boolean;
onChange: (...args: any[]) => any;
}

interface Option {
readonly label: string;
readonly value: string;
}

const createOption = (label: string) => ({
label,
value: label
});

const GSAutoReplyTokensField: React.FC<GSAutoReplyTokensFieldProps> = ({
disabled,
onChange,
value: selectedOptions
}) => {
const initialOptionValue = selectedOptions.map((token: string) =>
createOption(token)
);
const [optionValue, setOptionValue] = useState<readonly Option[]>(
initialOptionValue
);

const [inputValue, setInputValue] = useState("");
const onChangeValue = optionValue.map((option) => option.value);
const initialRender = useRef(true);

useEffect(() => {
if (initialRender.current) initialRender.current = false;
else onChange(onChangeValue);
}, [optionValue]);

const handleInputChange = (newValue: string) => {
setInputValue(newValue);
};

const handleChange = (newValue: Option[]) => {
setOptionValue(newValue);
};

const handleKeyDown: KeyboardEventHandler = (event) => {
if (!inputValue) return;

const lowerInputValue = inputValue.toLowerCase().trim();
if (optOutTriggers.includes(lowerInputValue)) {
setInputValue("");
return;
}
switch (event.key) {
/* eslint-disable no-case-declarations */
case ",":
case "Enter":
case "Tab":
const newValue = [...optionValue, createOption(lowerInputValue)];
const uniqValue = uniqBy(newValue, "value");

setInputValue("");
setOptionValue(uniqValue);
event.preventDefault();
/* eslint-enable no-case-declarations */
// no default
}
};

return (
<div>
<br />
Auto Replies
<CreatableSelect
components={{ DropdownIndicator: null }}
inputValue={inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Words or phrases that Spoke should automatically respond to using this script, separated by commas"
value={optionValue}
isDisabled={disabled}
/>
</div>
);
};

export default GSAutoReplyTokensField;
3 changes: 3 additions & 0 deletions src/components/forms/SpokeFormField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import BaseForm from "react-formal";

import GSAutoReplyTokensField from "./GSAutoReplyTokensField";
import GSDateField from "./GSDateField";
import GSPasswordField from "./GSPasswordField";
import GSScriptField from "./GSScriptField";
Expand Down Expand Up @@ -51,6 +52,8 @@ const SpokeFormField = React.forwardRef<unknown, Props>(function Component(
Input = GSSelectField;
} else if (type === "password") {
Input = GSPasswordField;
} else if (type === "autoreplytokens") {
Input = GSAutoReplyTokensField;
} else {
Input = type || GSTextField;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import DeleteIcon from "@material-ui/icons/Delete";
import ExpandLess from "@material-ui/icons/ExpandLess";
import ExpandMore from "@material-ui/icons/ExpandMore";
import HelpIconOutline from "@material-ui/icons/HelpOutline";
import type { CampaignVariable } from "@spoke/spoke-codegen";
import type {
CampaignVariable,
InteractionStepWithChildren
} from "@spoke/spoke-codegen";
import isNil from "lodash/isNil";
import React, { useCallback, useState } from "react";
import * as yup from "yup";

import type {
InteractionStep,
InteractionStepWithChildren
} from "../../../../../api/interaction-step";
import { supportsClipboard } from "../../../../../client/lib";
import GSForm from "../../../../../components/forms/GSForm";
import SpokeFormField from "../../../../../components/forms/SpokeFormField";
Expand Down Expand Up @@ -47,7 +46,8 @@ const interactionStepSchema = yup.object({
scriptOptions: yup.array(yup.string()),
questionText: yup.string(),
answerOption: yup.string(),
answerActions: yup.string()
answerActions: yup.string(),
autoReplyTokens: yup.array(yup.string())
});

type BlockHandlerFactory = (stepId: string) => () => Promise<void> | void;
Expand All @@ -62,7 +62,7 @@ interface Props {
title?: string;
disabled?: boolean;
onFormChange(e: any): void;
onCopyBlock(interactionStep: InteractionStep): void;
onCopyBlock(interactionStep: InteractionStepWithChildren): void;
onRequestRootPaste(): void;
deleteStepFactory: BlockHandlerFactory;
addStepFactory: BlockHandlerFactory;
Expand Down Expand Up @@ -111,7 +111,7 @@ export const InteractionStepCard: React.FC<Props> = (props) => {
const stepCanHaveChildren = isRootStep || answerOption;
const isAbleToAddResponse =
stepHasQuestion && stepHasScript && stepCanHaveChildren;
const childStepsLength = childSteps?.length;
const childStepsLength = childSteps?.length ?? 0;

const clipboardEnabled = supportsClipboard();

Expand Down Expand Up @@ -251,6 +251,13 @@ export const InteractionStepCard: React.FC<Props> = (props) => {
multiLine
disabled={disabled}
/>
{window.ENABLE_AUTO_REPLIES && parentInteractionId && (
<SpokeFormField
name="autoReplyTokens"
type="autoreplytokens"
disabled={disabled}
/>
)}
<SpokeFormField
{...dataTest("questionText")}
name="questionText"
Expand Down Expand Up @@ -288,26 +295,32 @@ export const InteractionStepCard: React.FC<Props> = (props) => {
)}
{expanded &&
(childSteps ?? [])
.filter((is) => !is.isDeleted)
.map((childStep) => (
<InteractionStepCard
key={childStep.id}
title={`Question: ${questionText}`}
interactionStep={childStep}
customFields={customFields}
campaignVariables={campaignVariables}
integrationSourced={integrationSourced}
availableActions={availableActions}
hasBlockCopied={hasBlockCopied}
disabled={disabled}
onFormChange={onFormChange}
onCopyBlock={onCopyBlock}
onRequestRootPaste={onRequestRootPaste}
addStepFactory={addStepFactory}
deleteStepFactory={deleteStepFactory}
pasteBlockFactory={pasteBlockFactory}
/>
))}
.filter((is) => !is?.isDeleted)
.map((childStep) => {
if (childStep) {
const { __typename, ...childStepWithoutTypename } = childStep;
return (
<InteractionStepCard
key={childStep?.id}
title={`Question: ${questionText}`}
interactionStep={childStepWithoutTypename}
customFields={customFields}
campaignVariables={campaignVariables}
integrationSourced={integrationSourced}
availableActions={availableActions}
hasBlockCopied={hasBlockCopied}
disabled={disabled}
onFormChange={onFormChange}
onCopyBlock={onCopyBlock}
onRequestRootPaste={onRequestRootPaste}
addStepFactory={addStepFactory}
deleteStepFactory={deleteStepFactory}
pasteBlockFactory={pasteBlockFactory}
/>
);
}
return null;
})}
</div>
</div>
);
Expand Down
Loading

0 comments on commit 735f7ed

Please sign in to comment.