2022-08-31 03:41:23 +00:00
|
|
|
import { Webhook } from "@prisma/client";
|
2022-06-16 16:21:48 +00:00
|
|
|
import { useEffect, useState } from "react";
|
2022-03-02 16:24:57 +00:00
|
|
|
import { Controller, useForm } from "react-hook-form";
|
|
|
|
|
2022-04-04 20:26:14 +00:00
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
2022-03-16 23:36:43 +00:00
|
|
|
import showToast from "@calcom/lib/notification";
|
2022-07-22 17:27:06 +00:00
|
|
|
import { trpc } from "@calcom/trpc/react";
|
2022-03-16 23:36:43 +00:00
|
|
|
import Button from "@calcom/ui/Button";
|
|
|
|
import { DialogFooter } from "@calcom/ui/Dialog";
|
2022-03-11 00:26:42 +00:00
|
|
|
import Switch from "@calcom/ui/Switch";
|
2022-03-16 23:36:43 +00:00
|
|
|
import { FieldsetLegend, Form, InputGroupBox, TextArea, TextField } from "@calcom/ui/form/fields";
|
2022-03-11 00:26:42 +00:00
|
|
|
|
2022-07-20 18:30:57 +00:00
|
|
|
import { WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP } from "@lib/webhooks/constants";
|
2022-03-02 16:24:57 +00:00
|
|
|
import customTemplate, { hasTemplateIntegration } from "@lib/webhooks/integrationTemplate";
|
|
|
|
|
|
|
|
import { TWebhook } from "@components/webhook/WebhookListItem";
|
|
|
|
import WebhookTestDisclosure from "@components/webhook/WebhookTestDisclosure";
|
|
|
|
|
|
|
|
export default function WebhookDialogForm(props: {
|
|
|
|
eventTypeId?: number;
|
|
|
|
defaultValues?: TWebhook;
|
2022-07-20 18:30:57 +00:00
|
|
|
app?: string;
|
2022-03-02 16:24:57 +00:00
|
|
|
handleClose: () => void;
|
2022-08-31 03:41:23 +00:00
|
|
|
webhooks: Webhook[];
|
2022-03-02 16:24:57 +00:00
|
|
|
}) {
|
|
|
|
const { t } = useLocale();
|
|
|
|
const utils = trpc.useContext();
|
2022-07-20 18:30:57 +00:00
|
|
|
const appId = props.app;
|
2022-08-31 03:41:23 +00:00
|
|
|
const webhooks = props.webhooks;
|
2022-07-20 18:30:57 +00:00
|
|
|
|
|
|
|
const triggers = !appId
|
|
|
|
? WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP["core"]
|
|
|
|
: WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP[appId as keyof typeof WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP];
|
2022-03-02 16:24:57 +00:00
|
|
|
const {
|
|
|
|
defaultValues = {
|
|
|
|
id: "",
|
2022-07-20 18:30:57 +00:00
|
|
|
eventTriggers: triggers,
|
2022-03-02 16:24:57 +00:00
|
|
|
subscriberUrl: "",
|
|
|
|
active: true,
|
|
|
|
payloadTemplate: null,
|
2022-06-16 16:21:48 +00:00
|
|
|
secret: null,
|
2022-05-03 23:16:59 +00:00
|
|
|
} as Omit<TWebhook, "userId" | "createdAt" | "eventTypeId" | "appId">,
|
2022-03-02 16:24:57 +00:00
|
|
|
} = props;
|
|
|
|
|
|
|
|
const [useCustomPayloadTemplate, setUseCustomPayloadTemplate] = useState(!!defaultValues.payloadTemplate);
|
2022-06-16 16:21:48 +00:00
|
|
|
const [changeSecret, setChangeSecret] = useState(false);
|
|
|
|
const [newSecret, setNewSecret] = useState("");
|
|
|
|
const hasSecretKey = !!defaultValues.secret;
|
|
|
|
const currentSecret = defaultValues.secret;
|
2022-03-02 16:24:57 +00:00
|
|
|
|
2022-08-31 03:41:23 +00:00
|
|
|
const subscriberUrlReserved = (subscriberUrl: string, id: string): boolean => {
|
|
|
|
return !!webhooks.find((webhook) => webhook.subscriberUrl === subscriberUrl && webhook.id !== id);
|
|
|
|
};
|
|
|
|
|
2022-03-02 16:24:57 +00:00
|
|
|
const form = useForm({
|
|
|
|
defaultValues,
|
|
|
|
});
|
2022-06-16 16:21:48 +00:00
|
|
|
|
|
|
|
const handleInput = (event: React.FormEvent<HTMLInputElement>) => {
|
|
|
|
setNewSecret(event.currentTarget.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (changeSecret) {
|
|
|
|
form.unregister("secret", { keepDefaultValue: false });
|
|
|
|
}
|
|
|
|
}, [changeSecret]);
|
|
|
|
|
2022-03-02 16:24:57 +00:00
|
|
|
return (
|
|
|
|
<Form
|
|
|
|
data-testid="WebhookDialogForm"
|
|
|
|
form={form}
|
|
|
|
handleSubmit={async (event) => {
|
2022-08-31 03:41:23 +00:00
|
|
|
if (subscriberUrlReserved(event.subscriberUrl, event.id)) {
|
|
|
|
showToast(t("webhook_subscriber_url_reserved"), "error");
|
|
|
|
return;
|
|
|
|
}
|
2022-06-16 16:21:48 +00:00
|
|
|
const e = changeSecret
|
2022-07-20 18:30:57 +00:00
|
|
|
? { ...event, eventTypeId: props.eventTypeId, appId }
|
|
|
|
: { ...event, secret: currentSecret, eventTypeId: props.eventTypeId, appId };
|
2022-03-02 16:24:57 +00:00
|
|
|
if (!useCustomPayloadTemplate && event.payloadTemplate) {
|
|
|
|
event.payloadTemplate = null;
|
|
|
|
}
|
|
|
|
if (event.id) {
|
|
|
|
await utils.client.mutation("viewer.webhook.edit", e);
|
|
|
|
await utils.invalidateQueries(["viewer.webhook.list"]);
|
|
|
|
showToast(t("webhook_updated_successfully"), "success");
|
|
|
|
} else {
|
|
|
|
await utils.client.mutation("viewer.webhook.create", e);
|
|
|
|
await utils.invalidateQueries(["viewer.webhook.list"]);
|
|
|
|
showToast(t("webhook_created_successfully"), "success");
|
|
|
|
}
|
|
|
|
props.handleClose();
|
|
|
|
}}
|
|
|
|
className="space-y-4">
|
2022-05-06 21:44:33 +00:00
|
|
|
<div>
|
|
|
|
<input type="hidden" {...form.register("id")} />
|
|
|
|
</div>
|
2022-03-02 16:24:57 +00:00
|
|
|
<fieldset className="space-y-2">
|
|
|
|
<InputGroupBox className="border-0 bg-gray-50">
|
|
|
|
<Controller
|
|
|
|
control={form.control}
|
|
|
|
name="active"
|
|
|
|
render={({ field }) => (
|
|
|
|
<Switch
|
|
|
|
label={field.value ? t("webhook_enabled") : t("webhook_disabled")}
|
|
|
|
defaultChecked={field.value}
|
|
|
|
onCheckedChange={(isChecked) => {
|
|
|
|
form.setValue("active", isChecked);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
</InputGroupBox>
|
|
|
|
</fieldset>
|
2022-05-06 21:44:33 +00:00
|
|
|
<div>
|
|
|
|
<TextField
|
|
|
|
label={t("subscriber_url")}
|
|
|
|
{...form.register("subscriberUrl")}
|
|
|
|
required
|
|
|
|
type="url"
|
|
|
|
onChange={(e) => {
|
|
|
|
form.setValue("subscriberUrl", e.target.value);
|
|
|
|
if (hasTemplateIntegration({ url: e.target.value })) {
|
|
|
|
setUseCustomPayloadTemplate(true);
|
|
|
|
form.setValue("payloadTemplate", customTemplate({ url: e.target.value }));
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
2022-03-02 16:24:57 +00:00
|
|
|
<fieldset className="space-y-2">
|
|
|
|
<FieldsetLegend>{t("event_triggers")}</FieldsetLegend>
|
|
|
|
<InputGroupBox className="border-0 bg-gray-50">
|
2022-07-20 18:30:57 +00:00
|
|
|
{triggers.map((key) => (
|
2022-03-02 16:24:57 +00:00
|
|
|
<Controller
|
|
|
|
key={key}
|
|
|
|
control={form.control}
|
|
|
|
name="eventTriggers"
|
|
|
|
render={({ field }) => (
|
|
|
|
<Switch
|
|
|
|
label={t(key.toLowerCase())}
|
|
|
|
defaultChecked={field.value.includes(key)}
|
|
|
|
onCheckedChange={(isChecked) => {
|
|
|
|
const value = field.value;
|
|
|
|
const newValue = isChecked ? [...value, key] : value.filter((v) => v !== key);
|
|
|
|
|
|
|
|
form.setValue("eventTriggers", newValue, {
|
|
|
|
shouldDirty: true,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</InputGroupBox>
|
|
|
|
</fieldset>
|
2022-06-16 16:21:48 +00:00
|
|
|
<fieldset className="space-y-2">
|
|
|
|
{!!hasSecretKey && !changeSecret && (
|
|
|
|
<>
|
|
|
|
<FieldsetLegend>{t("secret")}</FieldsetLegend>
|
|
|
|
<div className="rounded-sm bg-gray-50 p-2 text-xs text-neutral-900">
|
|
|
|
{t("forgotten_secret_description")}
|
|
|
|
</div>
|
|
|
|
<Button
|
|
|
|
color="secondary"
|
|
|
|
type="button"
|
|
|
|
className="py-1 text-xs"
|
|
|
|
onClick={() => {
|
|
|
|
setChangeSecret(true);
|
|
|
|
}}>
|
|
|
|
{t("change_secret")}
|
|
|
|
</Button>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{!!hasSecretKey && changeSecret && (
|
|
|
|
<>
|
|
|
|
<TextField
|
|
|
|
autoComplete="off"
|
|
|
|
label={t("secret")}
|
|
|
|
{...form.register("secret")}
|
|
|
|
value={newSecret}
|
|
|
|
onChange={handleInput}
|
|
|
|
type="text"
|
|
|
|
placeholder={t("leave_blank_to_remove_secret")}
|
|
|
|
/>
|
|
|
|
<Button
|
|
|
|
color="secondary"
|
|
|
|
type="button"
|
|
|
|
className="py-1 text-xs"
|
|
|
|
onClick={() => {
|
|
|
|
setChangeSecret(false);
|
|
|
|
}}>
|
|
|
|
{t("cancel")}
|
|
|
|
</Button>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{!hasSecretKey && (
|
|
|
|
<TextField autoComplete="off" label={t("secret")} {...form.register("secret")} type="text" />
|
|
|
|
)}
|
|
|
|
</fieldset>
|
2022-03-02 16:24:57 +00:00
|
|
|
<fieldset className="space-y-2">
|
|
|
|
<FieldsetLegend>{t("payload_template")}</FieldsetLegend>
|
|
|
|
<div className="space-x-3 text-sm rtl:space-x-reverse">
|
|
|
|
<label>
|
|
|
|
<input
|
|
|
|
className="text-neutral-900 focus:ring-neutral-500"
|
|
|
|
type="radio"
|
|
|
|
name="useCustomPayloadTemplate"
|
|
|
|
onChange={(value) => setUseCustomPayloadTemplate(!value.target.checked)}
|
|
|
|
defaultChecked={!useCustomPayloadTemplate}
|
|
|
|
/>{" "}
|
|
|
|
Default
|
|
|
|
</label>
|
|
|
|
<label>
|
|
|
|
<input
|
|
|
|
className="text-neutral-900 focus:ring-neutral-500"
|
|
|
|
onChange={(value) => setUseCustomPayloadTemplate(value.target.checked)}
|
|
|
|
name="useCustomPayloadTemplate"
|
|
|
|
type="radio"
|
|
|
|
defaultChecked={useCustomPayloadTemplate}
|
|
|
|
/>{" "}
|
|
|
|
Custom
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
{useCustomPayloadTemplate && (
|
|
|
|
<TextArea
|
|
|
|
{...form.register("payloadTemplate")}
|
|
|
|
defaultValue={useCustomPayloadTemplate && (defaultValues.payloadTemplate || "")}
|
|
|
|
rows={3}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</fieldset>
|
|
|
|
<WebhookTestDisclosure />
|
|
|
|
<DialogFooter>
|
|
|
|
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
|
|
|
|
{t("cancel")}
|
|
|
|
</Button>
|
|
|
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
|
|
|
{t("save")}
|
|
|
|
</Button>
|
|
|
|
</DialogFooter>
|
|
|
|
</Form>
|
|
|
|
);
|
|
|
|
}
|