Button to test a workflow action (#3873)
* add Test action button + UI improvements * add test action functionality * add confirmation dialog before sending SMS * code clean up * show error message if test action fails * disable test action button in edit mode * fixes SMS testing * use updated values * fix wrongly updated data in useEffect * fix typo * code clean up * fix UI issue in mobile view * small design fix Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
6c86317081
commit
b00402f429
|
@ -1044,6 +1044,14 @@
|
|||
"using_additional_inputs_as_variables": "How to use additional inputs as variables?",
|
||||
"download_desktop_app": "Download desktop app",
|
||||
"set_ping_link": "Set Ping link",
|
||||
"when_something_happens": "When something happens",
|
||||
"action_is_performed": "An action is performed",
|
||||
"test_action": "Test action",
|
||||
"notification_sent": "Notification sent",
|
||||
"no_input": "No input",
|
||||
"test_workflow_action": "Test workflow action",
|
||||
"send_sms": "Send SMS",
|
||||
"send_sms_to_number": "Are you sure you want to send a SMS to {{number}}?",
|
||||
"missing_connected_calendar": "No default calendar connected",
|
||||
"connect_your_calendar_and_link": "You can connect your calendar from <1>here</1>.",
|
||||
"default_calendar_selected": "Default calendar",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ArrowDownIcon } from "@heroicons/react/outline";
|
||||
import { WorkflowActions, WorkflowTemplates } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||
|
@ -168,7 +169,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
|||
</>
|
||||
)}
|
||||
<div className="flex justify-center">
|
||||
<div className="h-10 border-l-2 border-gray-400" />
|
||||
<ArrowDownIcon className="my-4 h-7 stroke-1 text-gray-500" />
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button type="button" onClick={() => setIsAddActionDialogOpen(true)} color="secondary">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ArrowDownIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
TimeUnit,
|
||||
WorkflowActions,
|
||||
|
@ -13,7 +14,12 @@ import "react-phone-number-input/style.css";
|
|||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
||||
import { Dialog } from "@calcom/ui/Dialog";
|
||||
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import Select from "@calcom/ui/form/Select";
|
||||
|
@ -49,6 +55,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
const [errorMessageNumber, setErrorMessageNumber] = useState("");
|
||||
const [errorMessageCustomInput, setErrorMessageCustomInput] = useState("");
|
||||
const [isInfoParagraphOpen, setIsInfoParagraphOpen] = useState(false);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [isTestActionDisabled, setIsTestActionDisabled] = useState(false);
|
||||
|
||||
const [translatedReminderBody, setTranslatedReminderBody] = useState(
|
||||
getTranslatedText((step ? form.getValues(`steps.${step.stepNumber - 1}.reminderBody`) : "") || "", {
|
||||
|
@ -114,6 +122,18 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const testActionMutation = trpc.useMutation("viewer.workflows.testAction", {
|
||||
onSuccess: async () => {
|
||||
showToast(t("notification_sent"), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
//trigger
|
||||
if (!step) {
|
||||
const trigger = form.getValues("trigger");
|
||||
|
@ -127,8 +147,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
return (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div className=" min-w-80 w-[50rem] rounded border-2 border-gray-400 bg-gray-50 px-10 pb-9 pt-5">
|
||||
<div className="font-bold">{t("triggers")}:</div>
|
||||
<div className=" min-w-80 w-[50rem] rounded border border-gray-200 bg-white pl-10 pr-12 pb-9 pt-5">
|
||||
<div className="text-base font-bold">{t("trigger")}</div>
|
||||
<div className="text-sm text-gray-600">{t("when_something_happens")}</div>
|
||||
<div className="my-7 border-t border-gray-200" />
|
||||
<Controller
|
||||
name="trigger"
|
||||
control={form.control}
|
||||
|
@ -206,13 +228,15 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div className="h-10 border-l-2 border-gray-400" />
|
||||
<div className="flex justify-center ">
|
||||
<ArrowDownIcon className="my-4 h-7 stroke-1 text-gray-500" />
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="min-w-80 flex w-[50rem] rounded border-2 border-gray-400 bg-gray-50 pl-10 pb-9 ">
|
||||
<div className=" min-w-80 flex w-[50rem] rounded border border-gray-200 bg-white px-6 pb-9 pt-5 pr-3 sm:px-10">
|
||||
<div className="w-full pt-5">
|
||||
<div className="font-bold">{t("action")}:</div>
|
||||
<div className="text-base font-bold">{t("action")}</div>
|
||||
<div className="text-sm text-gray-600">{t("action_is_performed")}</div>
|
||||
<div className="my-7 border-t border-gray-200" />
|
||||
<div>
|
||||
<Controller
|
||||
name={`steps.${step.stepNumber - 1}.action`}
|
||||
|
@ -229,19 +253,19 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
setIsPhoneNumberNeeded(true);
|
||||
setEditNumberMode(true);
|
||||
counter = counter + 1;
|
||||
setIsTestActionDisabled(true);
|
||||
} else {
|
||||
setIsPhoneNumberNeeded(false);
|
||||
setEditNumberMode(false);
|
||||
}
|
||||
|
||||
if (
|
||||
form.getValues(`steps.${step.stepNumber - 1}.template`) ===
|
||||
WorkflowTemplates.CUSTOM
|
||||
) {
|
||||
setEditEmailBodyMode(true);
|
||||
counter = counter + 1;
|
||||
setIsTestActionDisabled(true);
|
||||
}
|
||||
|
||||
if (
|
||||
val.value === WorkflowActions.EMAIL_ATTENDEE ||
|
||||
val.value === WorkflowActions.EMAIL_HOST
|
||||
|
@ -281,8 +305,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
onChange={(newValue) => {
|
||||
if (newValue) {
|
||||
setSendTo(newValue);
|
||||
setErrorMessageNumber("");
|
||||
} else {
|
||||
setSendTo("");
|
||||
}
|
||||
setErrorMessageNumber("");
|
||||
}}
|
||||
placeholder={t("enter_phone_number")}
|
||||
id="sendTo"
|
||||
|
@ -300,9 +326,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
className="-ml-3"
|
||||
onClick={() => {
|
||||
setEditNumberMode(true);
|
||||
setEditCounter(editCounter + 1);
|
||||
setIsTestActionDisabled(true);
|
||||
}}>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
|
@ -310,15 +338,21 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
className="-ml-3"
|
||||
onClick={async () => {
|
||||
if (sendTo) {
|
||||
form.setValue(`steps.${step.stepNumber - 1}.sendTo`, sendTo);
|
||||
if (isValidPhoneNumber(sendTo)) {
|
||||
form.setValue(`steps.${step.stepNumber - 1}.sendTo`, sendTo);
|
||||
setEditNumberMode(false);
|
||||
setEditCounter(editCounter - 1);
|
||||
if (!editEmailBodyMode) {
|
||||
setIsTestActionDisabled(false);
|
||||
}
|
||||
} else {
|
||||
setErrorMessageNumber(t("invalid_input"));
|
||||
}
|
||||
} else {
|
||||
setErrorMessageNumber(t("no_input"));
|
||||
}
|
||||
}}>
|
||||
{t("save")}
|
||||
|
@ -474,6 +508,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
onClick={() => {
|
||||
setEditEmailBodyMode(true);
|
||||
setEditCounter(editCounter + 1);
|
||||
setIsTestActionDisabled(true);
|
||||
}}>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
|
@ -482,8 +517,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
type="button"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
const reminderBody = form.getValues(`steps.${step.stepNumber - 1}.reminderBody`);
|
||||
const emailSubject = form.getValues(`steps.${step.stepNumber - 1}.emailSubject`);
|
||||
let isEmpty = false;
|
||||
let errorMessage = "";
|
||||
|
||||
|
@ -507,6 +540,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
if (!isEmpty) {
|
||||
setEditEmailBodyMode(false);
|
||||
setEditCounter(editCounter - 1);
|
||||
if (!editNumberMode) {
|
||||
setIsTestActionDisabled(false);
|
||||
}
|
||||
}
|
||||
setErrorMessageCustomInput(errorMessage);
|
||||
}}>
|
||||
|
@ -515,6 +551,29 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{form.getValues(`steps.${step.stepNumber - 1}.action`) !== WorkflowActions.SMS_ATTENDEE && (
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-7 w-full"
|
||||
disabled={isTestActionDisabled}
|
||||
onClick={() => {
|
||||
if (
|
||||
form.getValues(`steps.${step.stepNumber - 1}.action`) !== WorkflowActions.SMS_NUMBER
|
||||
) {
|
||||
testActionMutation.mutate({
|
||||
action: step.action,
|
||||
emailSubject: step.emailSubject || "",
|
||||
reminderBody: step.reminderBody || "",
|
||||
template: step.template,
|
||||
});
|
||||
} else {
|
||||
setConfirmationDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
color="secondary">
|
||||
<div className="w-full">{t("test_action")}</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown>
|
||||
|
@ -552,6 +611,25 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={confirmationDialogOpen} onOpenChange={setConfirmationDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
variety="warning"
|
||||
title={t("test_workflow_action")}
|
||||
confirmBtnText={t("send_sms")}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
testActionMutation.mutate({
|
||||
action: step.action,
|
||||
emailSubject: step.emailSubject || "",
|
||||
reminderBody: step.reminderBody || "",
|
||||
template: step.template,
|
||||
sendTo: step.sendTo || "",
|
||||
});
|
||||
setConfirmationDialogOpen(false);
|
||||
}}>
|
||||
{t("send_sms_to_number", { number: sendTo })}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ function WorkflowPage() {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (workflow) {
|
||||
if (workflow && !form.getValues("name")) {
|
||||
setSelectedEventTypes(
|
||||
workflow.activeOn.map((active) => ({
|
||||
value: String(active.eventType.id),
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
WORKFLOW_TEMPLATES,
|
||||
WORKFLOW_TRIGGER_EVENTS,
|
||||
|
@ -20,9 +21,11 @@ import {
|
|||
scheduleEmailReminder,
|
||||
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import {
|
||||
BookingInfo,
|
||||
deleteScheduledSMSReminder,
|
||||
scheduleSMSReminder,
|
||||
} from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
|
@ -709,4 +712,112 @@ export const workflowsRouter = createProtectedRouter()
|
|||
workflow,
|
||||
};
|
||||
},
|
||||
})
|
||||
.mutation("testAction", {
|
||||
input: z.object({
|
||||
action: z.enum(WORKFLOW_ACTIONS),
|
||||
emailSubject: z.string(),
|
||||
reminderBody: z.string(),
|
||||
template: z.enum(WORKFLOW_TEMPLATES),
|
||||
sendTo: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { action, emailSubject, reminderBody, template, sendTo } = input;
|
||||
try {
|
||||
const booking = await ctx.prisma.booking.findFirst({
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
let evt: BookingInfo;
|
||||
if (booking) {
|
||||
evt = {
|
||||
uid: booking?.uid,
|
||||
attendees:
|
||||
booking?.attendees.map((attendee) => {
|
||||
return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone };
|
||||
}) || [],
|
||||
organizer: {
|
||||
language: {
|
||||
locale: booking?.user?.locale || "",
|
||||
},
|
||||
name: booking?.user?.name || "",
|
||||
email: booking?.user?.email || "",
|
||||
timeZone: booking?.user?.timeZone || "",
|
||||
},
|
||||
startTime: booking?.startTime.toISOString() || "",
|
||||
endTime: booking?.endTime.toISOString() || "",
|
||||
title: booking?.title || "",
|
||||
location: booking?.location || null,
|
||||
additionalNotes: booking?.description || null,
|
||||
customInputs: booking?.customInputs,
|
||||
};
|
||||
} else {
|
||||
//if no booking exists create an example booking
|
||||
evt = {
|
||||
attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }],
|
||||
organizer: {
|
||||
language: {
|
||||
locale: ctx.user.locale,
|
||||
},
|
||||
name: ctx.user.name || "",
|
||||
email: ctx.user.email,
|
||||
timeZone: ctx.user.timeZone,
|
||||
},
|
||||
startTime: dayjs().add(10, "hour").toISOString(),
|
||||
endTime: dayjs().add(11, "hour").toISOString(),
|
||||
title: "Example Booking",
|
||||
location: "Office",
|
||||
additionalNotes: "These are additional notes",
|
||||
};
|
||||
}
|
||||
|
||||
if (action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.EMAIL_HOST) {
|
||||
scheduleEmailReminder(
|
||||
evt,
|
||||
WorkflowTriggerEvents.NEW_EVENT,
|
||||
action,
|
||||
{ time: null, timeUnit: null },
|
||||
ctx.user.email,
|
||||
emailSubject,
|
||||
reminderBody,
|
||||
0,
|
||||
template
|
||||
);
|
||||
return { message: "Notification sent" };
|
||||
} else if (action === WorkflowActions.SMS_NUMBER && sendTo) {
|
||||
scheduleSMSReminder(
|
||||
evt,
|
||||
sendTo,
|
||||
WorkflowTriggerEvents.NEW_EVENT,
|
||||
action,
|
||||
{ time: null, timeUnit: null },
|
||||
reminderBody,
|
||||
0,
|
||||
template
|
||||
);
|
||||
return { message: "Notification sent" };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
message: "Notification could not be sent",
|
||||
};
|
||||
} catch (_err) {
|
||||
const error = getErrorFromUnknown(_err);
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user