chore: adds webhook types (#4606)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
This commit is contained in:
Piyush Gupta
2025-01-24 17:53:07 +05:30
committed by GitHub
parent ad842e0e80
commit e691c076a1
35 changed files with 481 additions and 370 deletions

View File

@@ -0,0 +1,37 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { Webhook } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { AddWebhookModal } from "./add-webhook-modal";
interface AddWebhookButtonProps {
environment: TEnvironment;
surveys: TSurvey[];
}
export const AddWebhookButton = ({ environment, surveys }: AddWebhookButtonProps) => {
const t = useTranslations();
const [isAddWebhookModalOpen, setAddWebhookModalOpen] = useState(false);
return (
<>
<Button
size="sm"
onClick={() => {
setAddWebhookModalOpen(true);
}}>
<Webhook className="mr-2 h-5 w-5 text-white" />
{t("environments.integrations.webhooks.add_webhook")}
</Button>
<AddWebhookModal
environmentId={environment.id}
surveys={surveys}
open={isAddWebhookModalOpen}
setOpen={setAddWebhookModalOpen}
/>
</>
);
};

View File

@@ -0,0 +1,266 @@
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { SurveyCheckboxGroup } from "@/modules/integrations/webhooks/components/survey-checkbox-group";
import { TriggerCheckboxGroup } from "@/modules/integrations/webhooks/components/trigger-checkbox-group";
import { validWebHookURL } from "@/modules/integrations/webhooks/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { Modal } from "@/modules/ui/components/modal";
import { PipelineTriggers } from "@prisma/client";
import clsx from "clsx";
import { Webhook } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { TSurvey } from "@formbricks/types/surveys/types";
import { createWebhookAction, testEndpointAction } from "../actions";
import { TWebhookInput } from "../types/webhooks";
interface AddWebhookModalProps {
environmentId: string;
open: boolean;
surveys: TSurvey[];
setOpen: (v: boolean) => void;
}
export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWebhookModalProps) => {
const router = useRouter();
const {
handleSubmit,
reset,
register,
formState: { isSubmitting },
} = useForm();
const t = useTranslations();
const [testEndpointInput, setTestEndpointInput] = useState("");
const [hittingEndpoint, setHittingEndpoint] = useState<boolean>(false);
const [endpointAccessible, setEndpointAccessible] = useState<boolean>();
const [selectedTriggers, setSelectedTriggers] = useState<PipelineTriggers[]>([]);
const [selectedSurveys, setSelectedSurveys] = useState<string[]>([]);
const [selectedAllSurveys, setSelectedAllSurveys] = useState(false);
const [creatingWebhook, setCreatingWebhook] = useState(false);
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
try {
const { valid, error } = validWebHookURL(testEndpointInput);
if (!valid) {
toast.error(error ?? t("common.something_went_wrong_please_try_again"));
return;
}
setHittingEndpoint(true);
const testEndpointActionResult = await testEndpointAction({ url: testEndpointInput });
if (!testEndpointActionResult?.data) {
const errorMessage = getFormattedErrorMessage(testEndpointActionResult);
throw new Error(errorMessage);
}
setHittingEndpoint(false);
if (sendSuccessToast) toast.success(t("environments.integrations.webhooks.endpoint_pinged"));
setEndpointAccessible(true);
return true;
} catch (err) {
setHittingEndpoint(false);
toast.error(
`${t("environments.integrations.webhooks.endpoint_pinged_error")} \n ${
err.message.length < 250
? `${t("common.error")}: ${err.message}`
: t("environments.integrations.webhooks.please_check_console")
}`,
{ className: err.message.length < 250 ? "break-all" : "" }
);
console.error(t("environments.integrations.webhooks.webhook_test_failed_due_to"), err.message);
setEndpointAccessible(false);
return false;
}
};
const handleSelectAllSurveys = () => {
setSelectedAllSurveys(!selectedAllSurveys);
setSelectedSurveys([]);
};
const handleSelectedSurveyChange = (surveyId: string) => {
setSelectedSurveys((prevSelectedSurveys: string[]) =>
prevSelectedSurveys.includes(surveyId)
? prevSelectedSurveys.filter((id) => id !== surveyId)
: [...prevSelectedSurveys, surveyId]
);
};
const handleCheckboxChange = (selectedValue: PipelineTriggers) => {
setSelectedTriggers((prevValues) =>
prevValues.includes(selectedValue)
? prevValues.filter((value) => value !== selectedValue)
: [...prevValues, selectedValue]
);
};
const submitWebhook = async (data: TWebhookInput): Promise<void> => {
if (!isSubmitting) {
try {
setCreatingWebhook(true);
if (!testEndpointInput || testEndpointInput === "") {
throw new Error(t("environments.integrations.webhooks.please_enter_a_url"));
}
if (selectedTriggers.length === 0) {
throw new Error(t("common.please_select_at_least_one_trigger"));
}
if (!selectedAllSurveys && selectedSurveys.length === 0) {
throw new Error(t("common.please_select_at_least_one_survey"));
}
const endpointHitSuccessfully = await handleTestEndpoint(false);
if (!endpointHitSuccessfully) return;
const updatedData: TWebhookInput = {
name: data.name,
url: testEndpointInput,
source: "user",
triggers: selectedTriggers,
surveyIds: selectedSurveys,
};
const createWebhookActionResult = await createWebhookAction({
environmentId,
webhookInput: updatedData,
});
if (createWebhookActionResult?.data) {
router.refresh();
setOpenWithStates(false);
toast.success(t("environments.integrations.webhooks.webhook_added_successfully"));
} else {
const errorMessage = getFormattedErrorMessage(createWebhookActionResult);
toast.error(errorMessage);
}
} catch (e) {
toast.error(e.message);
} finally {
setCreatingWebhook(false);
}
}
};
const setOpenWithStates = (isOpen: boolean) => {
setOpen(isOpen);
reset();
setTestEndpointInput("");
setEndpointAccessible(undefined);
setSelectedSurveys([]);
setSelectedTriggers([]);
setSelectedAllSurveys(false);
};
return (
<Modal open={open} setOpen={setOpenWithStates} noPadding closeOnOutsideClick={true}>
<div className="flex h-full flex-col rounded-lg">
<div className="rounded-t-lg bg-slate-100">
<div className="flex w-full items-center justify-between p-6">
<div className="flex items-center space-x-2">
<div className="mr-1.5 h-6 w-6 text-slate-500">
<Webhook />
</div>
<div>
<div className="text-xl font-medium text-slate-700">
{t("environments.integrations.webhooks.add_webhook")}
</div>
<div className="text-sm text-slate-500">
{t("environments.integrations.webhooks.add_webhook_description")}
</div>
</div>
</div>
</div>
</div>
<form onSubmit={handleSubmit(submitWebhook)}>
<div className="flex justify-between rounded-lg p-6">
<div className="w-full space-y-4">
<div className="col-span-1">
<Label htmlFor="name">{t("common.name")}</Label>
<div className="mt-1 flex">
<Input
type="text"
id="name"
{...register("name")}
placeholder={t("environments.integrations.webhooks.webhook_name_placeholder")}
/>
</div>
</div>
<div className="col-span-1">
<Label htmlFor="URL">{t("common.url")}</Label>
<div className="mt-1 flex">
<Input
type="url"
id="URL"
value={testEndpointInput}
onChange={(e) => {
setTestEndpointInput(e.target.value);
}}
className={clsx(
endpointAccessible === true
? "border-green-500 bg-green-50"
: endpointAccessible === false
? "border-red-200 bg-red-50"
: endpointAccessible === undefined
? "border-slate-200 bg-white"
: null
)}
placeholder={t("environments.integrations.webhooks.webhook_url_placeholder")}
/>
<Button
type="button"
variant="secondary"
loading={hittingEndpoint}
className="ml-2 whitespace-nowrap"
disabled={testEndpointInput.trim() === ""}
onClick={() => {
handleTestEndpoint(true);
}}>
{t("environments.integrations.webhooks.test_endpoint")}
</Button>
</div>
</div>
<div>
<Label htmlFor="Triggers">{t("environments.integrations.webhooks.triggers")}</Label>
<TriggerCheckboxGroup
selectedTriggers={selectedTriggers}
onCheckboxChange={handleCheckboxChange}
allowChanges={true}
/>
</div>
<div>
<Label htmlFor="Surveys">{t("common.surveys")}</Label>
<SurveyCheckboxGroup
surveys={surveys}
selectedSurveys={selectedSurveys}
selectedAllSurveys={selectedAllSurveys}
onSelectAllSurveys={handleSelectAllSurveys}
onSelectedSurveyChange={handleSelectedSurveyChange}
allowChanges={true}
/>
</div>
</div>
</div>
<div className="flex justify-end border-t border-slate-200 p-6">
<div className="flex space-x-2">
<Button
type="button"
variant="ghost"
onClick={() => {
setOpenWithStates(false);
}}>
{t("common.cancel")}
</Button>
<Button type="submit" loading={creatingWebhook}>
{t("environments.integrations.webhooks.add_webhook")}
</Button>
</div>
</div>
</form>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,74 @@
import { Checkbox } from "@/modules/ui/components/checkbox";
import { useTranslations } from "next-intl";
import React from "react";
import { TSurvey } from "@formbricks/types/surveys/types";
interface SurveyCheckboxGroupProps {
surveys: TSurvey[];
selectedSurveys: string[];
selectedAllSurveys: boolean;
onSelectAllSurveys: () => void;
onSelectedSurveyChange: (surveyId: string) => void;
allowChanges: boolean;
}
export const SurveyCheckboxGroup: React.FC<SurveyCheckboxGroupProps> = ({
surveys,
selectedSurveys,
selectedAllSurveys,
onSelectAllSurveys,
onSelectedSurveyChange,
allowChanges,
}) => {
const t = useTranslations();
return (
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
<div className="my-1 flex items-center space-x-2">
<label
htmlFor="allSurveys"
className={`flex items-center ${selectedAllSurveys ? "font-semibold" : ""} ${
!allowChanges ? "cursor-not-allowed opacity-50" : "cursor-pointer"
}`}>
<Checkbox
type="button"
id="allSurveys"
className="bg-white"
value=""
checked={selectedAllSurveys}
onCheckedChange={onSelectAllSurveys}
disabled={!allowChanges}
/>
<span className="ml-2">
{t("environments.integrations.webhooks.all_current_and_new_surveys")}
</span>
</label>
</div>
{surveys.map((survey) => (
<div key={survey.id} className="my-1 flex items-center space-x-2">
<label
htmlFor={survey.id}
className={`flex items-center ${
selectedAllSurveys || !allowChanges ? "cursor-not-allowed opacity-50" : "cursor-pointer"
}`}>
<Checkbox
type="button"
id={survey.id}
value={survey.id}
className="bg-white"
checked={selectedSurveys.includes(survey.id) && !selectedAllSurveys}
disabled={selectedAllSurveys || !allowChanges}
onCheckedChange={() => {
if (allowChanges) {
onSelectedSurveyChange(survey.id);
}
}}
/>
<span className="ml-2">{survey.name}</span>
</label>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,66 @@
import { Checkbox } from "@/modules/ui/components/checkbox";
import { PipelineTriggers } from "@prisma/client";
import { useTranslations } from "next-intl";
import React from "react";
interface TriggerCheckboxGroupProps {
selectedTriggers: PipelineTriggers[];
onCheckboxChange: (selectedValue: PipelineTriggers) => void;
allowChanges: boolean;
}
const triggers: {
title: string;
value: PipelineTriggers;
}[] = [
{
title: "environments.integrations.webhooks.response_created",
value: "responseCreated",
},
{
title: "environments.integrations.webhooks.response_updated",
value: "responseUpdated",
},
{
title: "environments.integrations.webhooks.response_finished",
value: "responseFinished",
},
];
export const TriggerCheckboxGroup: React.FC<TriggerCheckboxGroupProps> = ({
selectedTriggers,
onCheckboxChange,
allowChanges,
}) => {
const t = useTranslations();
return (
<div className="mt-1 rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{triggers.map((trigger) => (
<div key={trigger.value} className="my-1 flex items-center space-x-2">
<label
htmlFor={trigger.value}
className={`flex ${
!allowChanges ? "cursor-not-allowed opacity-50" : "cursor-pointer"
} items-center`}>
<Checkbox
type="button"
id={trigger.value}
value={trigger.value}
className="bg-white"
checked={selectedTriggers.includes(trigger.value)}
onCheckedChange={() => {
if (allowChanges) {
onCheckboxChange(trigger.value);
}
}}
disabled={!allowChanges}
/>
<span className="ml-2">{t(trigger.title)}</span>
</label>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,44 @@
import { WebhookOverviewTab } from "@/modules/integrations/webhooks/components/webhook-overview-tab";
import { WebhookSettingsTab } from "@/modules/integrations/webhooks/components/webhook-settings-tab";
import { ModalWithTabs } from "@/modules/ui/components/modal-with-tabs";
import { Webhook } from "@prisma/client";
import { WebhookIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TSurvey } from "@formbricks/types/surveys/types";
interface WebhookModalProps {
open: boolean;
setOpen: (v: boolean) => void;
webhook: Webhook;
surveys: TSurvey[];
isReadOnly: boolean;
}
export const WebhookModal = ({ open, setOpen, webhook, surveys, isReadOnly }: WebhookModalProps) => {
const t = useTranslations();
const tabs = [
{
title: t("common.overview"),
children: <WebhookOverviewTab webhook={webhook} surveys={surveys} />,
},
{
title: t("common.settings"),
children: (
<WebhookSettingsTab webhook={webhook} surveys={surveys} setOpen={setOpen} isReadOnly={isReadOnly} />
),
},
];
return (
<>
<ModalWithTabs
open={open}
setOpen={setOpen}
tabs={tabs}
icon={<WebhookIcon />}
label={webhook.name ? webhook.name : webhook.url}
description={""}
/>
</>
);
};

View File

@@ -0,0 +1,95 @@
import { Label } from "@/modules/ui/components/label";
import { Webhook } from "@prisma/client";
import { useTranslations } from "next-intl";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TSurvey } from "@formbricks/types/surveys/types";
interface ActivityTabProps {
webhook: Webhook;
surveys: TSurvey[];
}
const getSurveyNamesForWebhook = (webhook: Webhook, allSurveys: TSurvey[]): string[] => {
if (webhook.surveyIds.length === 0) {
return allSurveys.map((survey) => survey.name);
} else {
return webhook.surveyIds.map((surveyId) => {
const survey = allSurveys.find((survey) => survey.id === surveyId);
return survey ? survey.name : "";
});
}
};
const convertTriggerIdToName = (triggerId: string, t: (key: string) => string): string => {
switch (triggerId) {
case "responseCreated":
return t("environments.integrations.webhooks.response_created");
case "responseUpdated":
return t("environments.integrations.webhooks.response_updated");
case "responseFinished":
return t("environments.integrations.webhooks.response_finished");
default:
return triggerId;
}
};
export const WebhookOverviewTab = ({ webhook, surveys }: ActivityTabProps) => {
const t = useTranslations();
return (
<div className="grid grid-cols-3 pb-2">
<div className="col-span-2 space-y-4 pr-6">
<div>
<Label className="text-slate-500">{t("common.name")}</Label>
<p className="truncate text-sm text-slate-900">{webhook.name ? webhook.name : "-"}</p>
</div>
<div>
<Label className="text-slate-500">
{t("environments.integrations.webhooks.created_by_third_party")}
</Label>
<p className="text-sm text-slate-900">
{webhook.source === "user" ? "No" : capitalizeFirstLetter(webhook.source)}
</p>
</div>
<div>
<Label className="text-slate-500">{t("common.url")}</Label>
<p className="text-sm text-slate-900">{webhook.url}</p>
</div>
<div>
<Label className="text-slate-500">{t("common.surveys")}</Label>
{getSurveyNamesForWebhook(webhook, surveys).map((surveyName, index) => (
<p key={index} className="text-sm text-slate-900">
{surveyName}
</p>
))}
</div>
<div>
<Label className="text-slate-500">{t("environments.integrations.webhooks.triggers")}</Label>
{webhook.triggers.map((triggerId) => (
<p key={triggerId} className="text-sm text-slate-900">
{convertTriggerIdToName(triggerId, t)}
</p>
))}
</div>
</div>
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
<div>
<Label className="text-xs font-normal text-slate-500">{t("common.created_at")}</Label>
<p className="text-xs text-slate-700">
{convertDateTimeStringShort(webhook.createdAt?.toString())}
</p>
</div>
<div>
<Label className="text-xs font-normal text-slate-500">{t("common.updated_at")}</Label>
<p className="text-xs text-slate-700">
{convertDateTimeStringShort(webhook.updatedAt?.toString())}
</p>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,97 @@
import { Badge } from "@/modules/ui/components/badge";
import { Webhook } from "@prisma/client";
import { useTranslations } from "next-intl";
import { timeSince } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
const renderSelectedSurveysText = (webhook: Webhook, allSurveys: TSurvey[]) => {
if (webhook.surveyIds.length === 0) {
const allSurveyNames = allSurveys.map((survey) => survey.name);
return <p className="text-slate-400">{allSurveyNames.join(", ")}</p>;
} else {
const selectedSurveyNames = webhook.surveyIds.map((surveyId) => {
const survey = allSurveys.find((survey) => survey.id === surveyId);
return survey ? survey.name : "";
});
return <p className="text-slate-400">{selectedSurveyNames.join(", ")}</p>;
}
};
const renderSelectedTriggersText = (webhook: Webhook, t: (key: string) => string) => {
if (webhook.triggers.length === 0) {
return <p className="text-slate-400">No Triggers</p>;
} else {
let cleanedTriggers = webhook.triggers.map((trigger) => {
if (trigger === "responseCreated") {
return t("environments.integrations.webhooks.response_created");
} else if (trigger === "responseUpdated") {
return t("environments.integrations.webhooks.response_updated");
} else if (trigger === "responseFinished") {
return t("environments.integrations.webhooks.response_finished");
} else {
return trigger;
}
});
return (
<p className="text-slate-400">
{cleanedTriggers
.sort((a, b) => {
const triggerOrder = {
"Response Created": 1,
"Response Updated": 2,
"Response Finished": 3,
};
return triggerOrder[a] - triggerOrder[b];
})
.join(", ")}
</p>
);
}
};
export const WebhookRowData = ({
webhook,
surveys,
locale,
}: {
webhook: Webhook;
surveys: TSurvey[];
locale: TUserLocale;
}) => {
const t = useTranslations();
return (
<div className="mt-2 grid h-auto grid-cols-12 content-center rounded-lg py-2 hover:bg-slate-100">
<div className="col-span-3 flex items-center truncate pl-6 text-sm">
<div className="flex items-center">
<div className="text-left">
{webhook.name ? (
<div className="text-left">
<div className="font-medium text-slate-900">{webhook.name}</div>
<div className="text-xs text-slate-400">{webhook.url}</div>
</div>
) : (
<div className="font-medium text-slate-900">{webhook.url}</div>
)}
</div>
</div>
</div>
<div className="col-span-1 my-auto text-center text-sm text-slate-800">
<Badge type="gray" size="tiny" text={capitalizeFirstLetter(webhook.source) || t("common.user")} />
</div>
<div className="col-span-4 my-auto text-center text-sm text-slate-800">
{renderSelectedSurveysText(webhook, surveys)}
</div>
<div className="col-span-2 my-auto text-center text-sm text-slate-800">
{renderSelectedTriggersText(webhook, t)}
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
{timeSince(webhook.createdAt.toString(), locale)}
</div>
<div className="text-center"></div>
</div>
);
};

View File

@@ -0,0 +1,268 @@
"use client";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { SurveyCheckboxGroup } from "@/modules/integrations/webhooks/components/survey-checkbox-group";
import { TriggerCheckboxGroup } from "@/modules/integrations/webhooks/components/trigger-checkbox-group";
import { validWebHookURL } from "@/modules/integrations/webhooks/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { PipelineTriggers, Webhook } from "@prisma/client";
import clsx from "clsx";
import { TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { TSurvey } from "@formbricks/types/surveys/types";
import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions";
import { TWebhookInput } from "../types/webhooks";
interface ActionSettingsTabProps {
webhook: Webhook;
surveys: TSurvey[];
setOpen: (v: boolean) => void;
isReadOnly: boolean;
}
export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: ActionSettingsTabProps) => {
const t = useTranslations();
const router = useRouter();
const { register, handleSubmit } = useForm({
defaultValues: {
name: webhook.name,
url: webhook.url,
triggers: webhook.triggers,
surveyIds: webhook.surveyIds,
},
});
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const [isUpdatingWebhook, setIsUpdatingWebhook] = useState(false);
const [selectedTriggers, setSelectedTriggers] = useState<PipelineTriggers[]>(webhook.triggers);
const [selectedSurveys, setSelectedSurveys] = useState<string[]>(webhook.surveyIds);
const [testEndpointInput, setTestEndpointInput] = useState(webhook.url);
const [endpointAccessible, setEndpointAccessible] = useState<boolean>();
const [hittingEndpoint, setHittingEndpoint] = useState<boolean>(false);
const [selectedAllSurveys, setSelectedAllSurveys] = useState(webhook.surveyIds.length === 0);
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
try {
const { valid, error } = validWebHookURL(testEndpointInput);
if (!valid) {
toast.error(error ?? t("common.something_went_wrong_please_try_again"));
return;
}
setHittingEndpoint(true);
const testEndpointActionResult = await testEndpointAction({ url: testEndpointInput });
if (!testEndpointActionResult?.data) {
const errorMessage = getFormattedErrorMessage(testEndpointActionResult);
throw new Error(errorMessage);
}
setHittingEndpoint(false);
if (sendSuccessToast) toast.success(t("environments.integrations.webhooks.endpoint_pinged"));
setEndpointAccessible(true);
return true;
} catch (err) {
setHittingEndpoint(false);
toast.error(
`${t("environments.integrations.webhooks.endpoint_pinged_error")} \n ${err.message.length < 250 ? `${t("common.error")}: ${err.message}` : t("environments.integrations.webhooks.please_check_console")}`,
{ className: err.message.length < 250 ? "break-all" : "" }
);
console.error(t("environments.integrations.webhooks.webhook_test_failed_due_to"), err.message);
setEndpointAccessible(false);
return false;
}
};
const handleSelectAllSurveys = () => {
setSelectedAllSurveys(!selectedAllSurveys);
setSelectedSurveys([]);
};
const handleSelectedSurveyChange = (surveyId) => {
setSelectedSurveys((prevSelectedSurveys) => {
if (prevSelectedSurveys.includes(surveyId)) {
return prevSelectedSurveys.filter((id) => id !== surveyId);
} else {
return [...prevSelectedSurveys, surveyId];
}
});
};
const handleCheckboxChange = (selectedValue) => {
setSelectedTriggers((prevValues) => {
if (prevValues.includes(selectedValue)) {
return prevValues.filter((value) => value !== selectedValue);
} else {
return [...prevValues, selectedValue];
}
});
};
const onSubmit = async (data) => {
if (selectedTriggers.length === 0) {
toast.error(t("common.please_select_at_least_one_trigger"));
return;
}
if (!selectedAllSurveys && selectedSurveys.length === 0) {
toast.error(t("common.please_select_at_least_one_survey"));
return;
}
const endpointHitSuccessfully = await handleTestEndpoint(false);
if (!endpointHitSuccessfully) {
return;
}
const updatedData: TWebhookInput = {
name: data.name,
url: data.url as string,
source: data.source,
triggers: selectedTriggers,
surveyIds: selectedSurveys,
};
setIsUpdatingWebhook(true);
const updateWebhookActionResult = await updateWebhookAction({
webhookId: webhook.id,
webhookInput: updatedData,
});
if (updateWebhookActionResult?.data) {
router.refresh();
toast.success(t("environments.integrations.webhooks.webhook_updated_successfully"));
} else {
const errorMessage = getFormattedErrorMessage(updateWebhookActionResult);
toast.error(errorMessage);
}
setIsUpdatingWebhook(false);
setOpen(false);
};
return (
<div>
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
<div className="col-span-1">
<Label htmlFor="Name">{t("common.name")}</Label>
<div className="mt-1 flex">
<Input
type="text"
id="name"
{...register("name")}
disabled={isReadOnly}
defaultValue={webhook.name ?? ""}
placeholder={t("environments.integrations.webhooks.webhook_name_placeholder")}
/>
</div>
</div>
<div className="col-span-1">
<Label htmlFor="URL">{t("common.url")}</Label>
<div className="mt-1 flex">
<Input
{...register("url", {
value: testEndpointInput,
})}
type="text"
value={testEndpointInput}
onChange={(e) => {
setTestEndpointInput(e.target.value);
}}
readOnly={webhook.source !== "user"}
className={clsx(
webhook.source === "user" ? null : "cursor-not-allowed bg-slate-100 text-slate-500",
endpointAccessible === true
? "border-green-500 bg-green-50"
: endpointAccessible === false
? "border-red-200 bg-red-50"
: endpointAccessible === undefined
? "border-slate-200 bg-white"
: null
)}
placeholder={t("environments.integrations.webhooks.webhook_url_placeholder")}
/>
<Button
type="button"
variant="secondary"
loading={hittingEndpoint}
className="ml-2 whitespace-nowrap"
onClick={() => {
handleTestEndpoint(true);
}}>
{t("environments.integrations.webhooks.test_endpoint")}
</Button>
</div>
</div>
<div>
<Label htmlFor="Triggers">{t("environments.integrations.webhooks.triggers")}</Label>
<TriggerCheckboxGroup
selectedTriggers={selectedTriggers}
onCheckboxChange={handleCheckboxChange}
allowChanges={webhook.source === "user" && !isReadOnly}
/>
</div>
<div>
<Label htmlFor="Surveys">{t("common.surveys")}</Label>
<SurveyCheckboxGroup
surveys={surveys}
selectedSurveys={selectedSurveys}
selectedAllSurveys={selectedAllSurveys}
onSelectAllSurveys={handleSelectAllSurveys}
onSelectedSurveyChange={handleSelectedSurveyChange}
allowChanges={webhook.source === "user" && !isReadOnly}
/>
</div>
<div className="flex justify-between border-t border-slate-200 py-6">
<div>
{webhook.source === "user" && !isReadOnly && (
<Button
type="button"
variant="destructive"
onClick={() => setOpenDeleteDialog(true)}
className="mr-3">
<TrashIcon />
{t("common.delete")}
</Button>
)}
<Button variant="secondary" asChild>
<Link href="https://formbricks.com/docs/api/management/webhooks" target="_blank">
{t("common.read_docs")}
</Link>
</Button>
</div>
{!isReadOnly && (
<div className="flex space-x-2">
<Button type="submit" loading={isUpdatingWebhook}>
{t("common.save_changes")}
</Button>
</div>
)}
</div>
</form>
<DeleteDialog
open={openDeleteDialog}
setOpen={setOpenDeleteDialog}
deleteWhat={t("common.webhook")}
text={t("environments.integrations.webhooks.webhook_delete_confirmation")}
onDelete={async () => {
setOpen(false);
const deleteWebhookActionResult = await deleteWebhookAction({ id: webhook.id });
if (deleteWebhookActionResult?.data) {
router.refresh();
toast.success(t("environments.integrations.webhooks.webhook_deleted_successfully"));
} else {
const errorMessage = getFormattedErrorMessage(deleteWebhookActionResult);
toast.error(errorMessage);
}
}}
/>
</div>
);
};

View File

@@ -0,0 +1,17 @@
import { getTranslations } from "next-intl/server";
export const WebhookTableHeading = async () => {
const t = await getTranslations();
return (
<>
<div className="grid h-12 grid-cols-12 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<span className="sr-only">{t("common.edit")}</span>
<div className="col-span-3 pl-6">{t("common.webhook")}</div>
<div className="col-span-1 text-center">{t("environments.integrations.webhooks.source")}</div>
<div className="col-span-4 text-center">{t("common.surveys")}</div>
<div className="col-span-2 text-center">{t("environments.integrations.webhooks.triggers")}</div>
<div className="col-span-2 text-center">{t("common.updated")}</div>
</div>
</>
);
};

View File

@@ -0,0 +1,81 @@
"use client";
import { WebhookModal } from "@/modules/integrations/webhooks/components/webhook-detail-modal";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { Webhook } from "@prisma/client";
import { useTranslations } from "next-intl";
import { type JSX, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
interface WebhookTableProps {
environment: TEnvironment;
webhooks: Webhook[];
surveys: TSurvey[];
children: [JSX.Element, JSX.Element[]];
isReadOnly: boolean;
}
export const WebhookTable = ({
environment,
webhooks,
surveys,
children: [TableHeading, webhookRows],
isReadOnly,
}: WebhookTableProps) => {
const [isWebhookDetailModalOpen, setWebhookDetailModalOpen] = useState(false);
const t = useTranslations();
const [activeWebhook, setActiveWebhook] = useState<Webhook>({
environmentId: environment.id,
id: "",
name: "",
url: "",
source: "user",
triggers: [],
surveyIds: [],
createdAt: new Date(),
updatedAt: new Date(),
});
const handleOpenWebhookDetailModalClick = (e, webhook: Webhook) => {
e.preventDefault();
setActiveWebhook(webhook);
setWebhookDetailModalOpen(true);
};
return (
<>
{webhooks.length === 0 ? (
<EmptySpaceFiller
type="table"
environment={environment}
noWidgetRequired={true}
emptyMessage={t("environments.integrations.webhooks.empty_webhook_message")}
/>
) : (
<div className="rounded-lg border border-slate-200">
{TableHeading}
<div className="grid-cols-7">
{webhooks.map((webhook, index) => (
<button
onClick={(e) => {
handleOpenWebhookDetailModalClick(e, webhook);
}}
className="w-full"
key={webhook.id}>
{webhookRows[index]}
</button>
))}
</div>
</div>
)}
<WebhookModal
open={isWebhookDetailModalOpen}
setOpen={setWebhookDetailModalOpen}
webhook={activeWebhook}
surveys={surveys}
isReadOnly={isReadOnly}
/>
</>
);
};