add basic pipeline view to hq

This commit is contained in:
Matthias Nannt
2022-11-29 21:02:13 +01:00
parent e0d0458d7f
commit 69985f5f39
12 changed files with 981 additions and 17 deletions
@@ -0,0 +1,95 @@
"use client";
/* This example requires Tailwind CSS v2.0+ */
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { createPipeline, usePipelines } from "@/lib/pipelines";
import { webhook } from "./webhook";
import { emailNotification } from "./emailNotification";
import PipelineSettings from "./PipelineSettings";
import Modal from "@/components/Modal";
import { Button } from "@formbricks/ui";
const availablePipelines = [webhook, emailNotification];
const getEmptyPipeline = () => {
return { label: "", type: null, events: [], config: {} };
};
export default function AddPipelineModal({ open, setOpen, params }) {
const [typeId, setTypeId] = useState(null);
const [pipeline, setPipeline] = useState(getEmptyPipeline());
const { pipelines, mutatePipelines } = usePipelines(params.formId, params.teamId);
useEffect(() => {
if (typeId !== pipeline.type) {
setPipeline({ ...pipeline, type: typeId });
}
}, [typeId, pipeline]);
useEffect(() => {
if (!open) {
setPipeline(getEmptyPipeline());
setTypeId(null);
}
}, [open]);
const handleSubmit = async (e) => {
e.preventDefault();
const newPipeline = await createPipeline(params.formId, params.teamId, pipeline);
const newPipelines = JSON.parse(JSON.stringify(pipelines));
newPipelines.push(newPipeline);
mutatePipelines(newPipelines);
setOpen(false);
};
return (
<Modal open={open} setOpen={setOpen}>
<>
{typeId === null ? (
<>
<h2 className="text-ui-gray-dark mb-6 text-xl font-bold">
Please choose a pipeline you want to add
</h2>
{availablePipelines.map((pipeline) => (
<div
className="border-ui-gray-light mb-5 w-full border bg-white shadow sm:rounded"
key={pipeline.title}>
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium leading-6 text-gray-900">{pipeline.title}</h3>
<div className="mt-2 sm:flex sm:items-start sm:justify-between">
<div className="max-w-xl text-sm text-gray-500">
<p>{pipeline.description}</p>
</div>
<div className="mt-5 sm:mt-0 sm:ml-6 sm:flex sm:flex-shrink-0 sm:items-center">
<Button
onClick={() => {
setTypeId(pipeline.typeId);
}}>
Select
</Button>
</div>
</div>
</div>
</div>
))}
</>
) : (
<form className="w-full space-y-8 divide-y divide-gray-200" onSubmit={handleSubmit}>
<PipelineSettings typeId={typeId} pipeline={pipeline} setPipeline={setPipeline} />
<div className="pt-5">
<div className="flex justify-end">
<Button variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit" className="ml-2">
Create
</Button>
</div>
</div>
</form>
)}
</>
</Modal>
);
}
@@ -0,0 +1,18 @@
"use client";
import { EmailNotificationSettings } from "./emailNotification";
import { WebhookSettings } from "./webhook";
const PipelineSettings = ({ typeId, pipeline, setPipeline }) => {
switch (typeId) {
case "WEBHOOK":
return <WebhookSettings pipeline={pipeline} setPipeline={setPipeline} />;
break;
case "EMAIL_NOTIFICATION":
return <EmailNotificationSettings pipeline={pipeline} setPipeline={setPipeline} />;
default:
return <></>;
}
};
export default PipelineSettings;
@@ -0,0 +1,56 @@
import LoadingSpinner from "@/app/LoadingSpinner";
import Modal from "@/components/Modal";
import { persistPipeline, usePipeline, usePipelines } from "@/lib/pipelines";
import PipelineSettings from "./PipelineSettings";
export default function UpdatePipelineModal({ open, setOpen, params, pipelineId }) {
const { pipeline, isLoadingPipeline, mutatePipeline } = usePipeline(
params.formId,
params.teamId,
pipelineId
);
const { pipelines, mutatePipelines } = usePipelines(params.formId, params.teamId);
const handleSubmit = async (e) => {
e.preventDefault();
await persistPipeline(params.formId, params.teamId, pipeline);
const newPipelines = JSON.parse(JSON.stringify(pipelines));
const pipelineIdx = pipelines.findIndex((p) => p.id === pipelineId);
if (pipelineIdx > -1) {
newPipelines[pipelineIdx] = pipeline;
mutatePipelines(newPipelines);
}
setOpen(false);
};
return (
<Modal open={open} setOpen={setOpen}>
{isLoadingPipeline ? (
<LoadingSpinner />
) : (
<form className="w-full space-y-8 divide-y divide-gray-200" onSubmit={handleSubmit}>
<PipelineSettings
typeId={pipeline.type}
pipeline={pipeline}
setPipeline={(p) => mutatePipeline(p, false)}
/>
<div className="pt-5">
<div className="flex justify-end">
<button
type="button"
onClick={() => setOpen(false)}
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Cancel
</button>
<button
type="submit"
className="ml-3 inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Update
</button>
</div>
</div>
</form>
)}
</Modal>
);
}
@@ -0,0 +1,149 @@
export const emailNotification = {
typeId: "EMAIL_NOTIFICATION",
title: "Email Notification",
description: "Get an email notification when your form is completed",
};
const eventTypes = [
{
id: "PAGE_SUBMISSION",
name: "Page Submission",
description: "every time a form page is submitted (partial submission)",
},
{
id: "FORM_COMPLETED",
name: "Form completed",
description: "each time the form is fully completed (total submission)",
},
];
export function EmailNotificationSettings({ pipeline, setPipeline }) {
const toggleEvent = (eventId) => {
const newPipeline = JSON.parse(JSON.stringify(pipeline));
const eventIdx = newPipeline.events.indexOf(eventId);
if (eventIdx !== -1) {
newPipeline.events.splice(eventIdx, 1);
} else {
newPipeline.events.push(eventId);
}
setPipeline(newPipeline);
};
const updateField = (field, value, parent = null) => {
const newPipeline = JSON.parse(JSON.stringify(pipeline));
if (parent) {
newPipeline[parent][field] = value;
} else {
newPipeline[field] = value;
}
setPipeline(newPipeline);
};
return (
<div className="space-y-8 divide-y divide-gray-200">
<div>
<h2 className="text-ui-gray-dark mb-3 text-xl font-bold">Configure Email notifications</h2>
<p className="mt-1 text-sm text-gray-500">
Configure Email notifications. To learn more about how email notifications work, please check out
our docs.
</p>
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Pipeline Name
</label>
<div className="mt-1">
<input
type="text"
name="name"
id="name"
value={pipeline.name || ""}
onChange={(e) => updateField("name", e.target.value)}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm"
required
/>
</div>
</div>
<div className="sm:col-span-4">
<label htmlFor="endpointUrl" className="block text-sm font-medium text-gray-700">
Email address
</label>
<div className="mt-1">
<input
type="email"
// pattern="/^[^@]+@[^.]+\..+$/"
onInvalid={(e: any) => e.target.setCustomValidity("please provide a valid email")}
onInput={(e: any) => e.target.setCustomValidity("")}
name="email"
id="email"
value={pipeline.data.email || ""}
onChange={(e) => updateField("email", e.target.value, "data")}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm"
required
/>
</div>
<p className="mt-2 text-xs text-gray-500" id="email-description">
The email address that will receive notifications when the form/page is completed
</p>
</div>
</div>
</div>
<div className="pt-8">
<div>
<h3 className="text-lg font-medium leading-6 text-gray-900">Advanced Settings</h3>
<p className="mt-1 text-sm text-gray-500">Set up this webhook to fit your needs.</p>
</div>
<div className="mt-6">
<fieldset>
<legend className="sr-only">Events</legend>
<div className="text-base font-medium text-gray-900" aria-hidden="true">
Events
</div>
<div className="mt-4 space-y-4">
{eventTypes.map((eventType) => (
<div key={eventType.id}>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
id={eventType.id}
name={eventType.name}
type="checkbox"
checked={pipeline.events.includes(eventType.id)}
onChange={() => toggleEvent(eventType.id)}
className="h-4 w-4 rounded-sm border-gray-300 text-red-600 focus:ring-red-500"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor={eventType.id} className="font-medium text-gray-700">
{eventType.name}
</label>
<p className="text-gray-500">{eventType.description}</p>
</div>
</div>
</div>
))}
</div>
</fieldset>
</div>
<div className="mt-6">
<fieldset>
<legend className="sr-only">Conditions</legend>
<div className="text-base font-medium text-gray-900" aria-hidden="true">
Conditions
</div>
<div className="mt-4 space-y-4">
<div className="rounded-sm border border-gray-100 bg-gray-50 px-2 py-5">
<p className="flex justify-center text-xs text-gray-600">
conditional data piping coming soon
</p>
</div>
</div>
</fieldset>
</div>
</div>
</div>
);
}
@@ -1,14 +1,127 @@
"use client";
import LoadingSpinner from "@/app/LoadingSpinner";
import EmptyPageFiller from "@/components/EmptyPageFiller";
import { useForm } from "@/lib/forms";
import { deletePipeline, persistPipeline, usePipelines } from "@/lib/pipelines";
import { useTeam } from "@/lib/teams";
import { Button } from "@formbricks/ui";
import { Switch } from "@headlessui/react";
import { BoltIcon, Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid";
import { CodeBracketSquareIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import { useState } from "react";
import { AiOutlineMail } from "react-icons/ai";
import { SiAirtable, SiGoogle, SiNotion, SiSlack, SiZapier } from "react-icons/si";
import AddPipelineModal from "./AddPipelineModal";
import UpdatePipelineModal from "./UpdatePipelineModal";
const integrations = [
{
id: "webhook",
name: "Webhook",
href: "#",
comingSoon: false,
bgColor: "bg-slate-500",
icon: CodeBracketSquareIcon,
action: () => {},
},
{
id: "email notification",
name: "Email Notification",
href: "#",
comingSoon: false,
bgColor: "bg-slate-500",
icon: AiOutlineMail,
action: () => {},
},
{
id: "Notion",
name: "Notion",
comingSoon: true,
href: "#",
bgColor: "bg-slate-500",
icon: SiNotion,
action: () => {},
},
{
id: "googleSheets",
name: "Google Sheets",
comingSoon: true,
href: "#",
bgColor: "bg-slate-500",
icon: SiGoogle,
action: () => {},
},
{
id: "zapier",
name: "Zapier",
comingSoon: true,
href: "#",
bgColor: "bg-slate-500",
icon: SiZapier,
action: () => {},
},
{
id: "airtable",
name: "Airtable",
comingSoon: true,
href: "#",
bgColor: "bg-slate-500",
icon: SiAirtable,
action: () => {},
},
{
id: "slack",
name: "Slack",
comingSoon: true,
href: "#",
bgColor: "bg-slate-500",
icon: SiSlack,
action: () => {},
},
];
export default function PipelinesPage({ params }) {
const { form, isLoadingForm, isErrorForm } = useForm(params.formId, params.teamId);
const { team, isLoadingTeam, isErrorTeam } = useTeam(params.teamId);
const { pipelines, isLoadingPipelines, isErrorPipelines, mutatePipelines } = usePipelines(
params.formId,
params.teamId
);
if (isLoadingForm || isLoadingTeam) {
const [openAddModal, setOpenAddModal] = useState(false);
const [updatePipelineId, setUpdatePipelineId] = useState(null);
const [openUpdateModal, setOpenUpdateModal] = useState(false);
const toggleEnabled = async (pipeline) => {
const newPipeline = JSON.parse(JSON.stringify(pipeline));
newPipeline.enabled = !newPipeline.enabled;
await persistPipeline(params.formId, params.teamId, newPipeline);
const pipelineIdx = pipelines.findIndex((p) => p.id === pipeline.id);
if (pipelineIdx !== -1) {
const newPipelines = JSON.parse(JSON.stringify(pipelines));
newPipelines[pipelineIdx] = newPipeline;
mutatePipelines(newPipelines);
}
};
const openSettings = (pipeline) => {
setUpdatePipelineId(pipeline.id);
setOpenUpdateModal(true);
};
const deletePipelineAction = async (pipelineId) => {
await deletePipeline(params.formId, params.teamId, pipelineId);
const newPipelines = JSON.parse(JSON.stringify(pipelines));
const pipelineIdx = newPipelines.findIndex((p) => p.id === pipelineId);
if (pipelineIdx > -1) {
newPipelines.splice(pipelineIdx, 1);
mutatePipelines(newPipelines);
}
};
if (isLoadingForm || isLoadingTeam || isLoadingPipelines) {
return (
<div className="flex h-full w-full items-center justify-center">
<LoadingSpinner />
@@ -16,19 +129,155 @@ export default function PipelinesPage({ params }) {
);
}
if (isErrorForm || isErrorTeam) {
if (isErrorForm || isErrorTeam || isErrorPipelines) {
return <div>Error loading ressources. Maybe you don&lsquo;t have enough access rights</div>;
}
return (
<div className="mx-auto py-8 sm:px-6 lg:px-8">
<header className="mb-8">
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
Pipelines - {form.label}
<span className="text-brand-dark ml-4 inline-flex items-center rounded-md border border-teal-100 bg-teal-50 px-2.5 py-0.5 text-sm font-medium">
{team.name}
</span>
</h1>
<div className="flex justify-between">
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
Pipelines - {form.label}
<span className="text-brand-dark ml-4 inline-flex items-center rounded-md border border-teal-100 bg-teal-50 px-2.5 py-0.5 text-sm font-medium">
{team.name}
</span>
</h1>
<Button onClick={() => setOpenAddModal(true)}>Add Pipeline</Button>
</div>
</header>
<div className="my-4">
<p className="text-slate-800">
Pipe your data exactly where you need it. Add conditions for variable data piping.
</p>
</div>
{pipelines.length > 0 ? (
<>
<div className="overflow-hidden bg-white shadow sm:rounded-md">
<ul role="list" className="divide-y divide-gray-200">
{pipelines.map((pipeline) => (
<li key={pipeline.id}>
<div className="block">
<div className="flex items-center px-4 py-4 sm:px-6">
<div className="flex min-w-0 flex-1 items-center">
<div className="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4">
<div>
<p className="truncate text-sm font-medium text-slate-800">{pipeline.label}</p>
<p className="mt-2 flex items-center text-sm text-gray-500">
<BoltIcon
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
aria-hidden="true"
/>
<span className="truncate">{pipeline.type}</span>
</p>
</div>
<div className="hidden md:block">
<div>
{<p className="mb-1 text-xs text-slate-800">Active</p>}
<Switch
checked={pipeline.enabled}
onChange={() => toggleEnabled(pipeline)}
className={clsx(
pipeline.enabled ? "bg-brand-dark" : "bg-gray-200",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
)}>
<span className="sr-only">Use setting</span>
<span
className={clsx(
pipeline.enabled ? "translate-x-5" : "translate-x-0",
"pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
)}>
<span
className={clsx(
pipeline.enabled
? "opacity-0 duration-100 ease-out"
: "opacity-100 duration-200 ease-in",
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)}
aria-hidden="true">
<svg className="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span
className={clsx(
pipeline.enabled
? "opacity-100 duration-200 ease-in"
: "opacity-0 duration-100 ease-out",
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)}
aria-hidden="true">
<svg
className="text-brand-dark h-3 w-3"
fill="currentColor"
viewBox="0 0 12 12">
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
</svg>
</span>
</span>
</Switch>
</div>
</div>
</div>
</div>
<div className="inline-flex">
<button onClick={() => openSettings(pipeline)}>
<Cog6ToothIcon className="mx-2 h-4 w-4 text-gray-400" aria-hidden="true" />
</button>
<button
onClick={() => {
if (confirm("Are you sure you want to delete this pipeline?")) {
deletePipelineAction(pipeline.id);
}
}}>
<TrashIcon className="mx-2 h-4 w-4 text-gray-400" aria-hidden="true" />
</button>
</div>
</div>
</div>
</li>
))}
</ul>
</div>
</>
) : (
<EmptyPageFiller
alertText={`No active pipelines for '${form.label}'`}
hintText="Add a pipeline to get started.">
<div className="mx-10 mb-5 grid grid-cols-3 gap-3">
{integrations.map((integration) => (
<div className="col-span-1 my-1 flex" key={integration.id}>
<div className="text-ui-gray-medium relative col-span-1 flex w-full">
<integration.icon className="text-ui-gray-medium h-6 w-6" />
<div className="inline-flex items-center truncate px-4 text-sm">
<p className="">{integration.name}</p>
{integration.comingSoon && (
<div className="ml-3 rounded-sm border border-teal-100 bg-teal-50 p-0.5 px-2">
<p className="text-xs text-teal-600">coming soon</p>
</div>
)}
</div>
</div>
</div>
))}
</div>
<hr />
</EmptyPageFiller>
)}
<AddPipelineModal open={openAddModal} setOpen={setOpenAddModal} params={params} />
{openUpdateModal && (
<UpdatePipelineModal
open={openUpdateModal}
setOpen={setOpenUpdateModal}
params={params}
pipelineId={updatePipelineId}
/>
)}
</div>
);
}
@@ -0,0 +1,185 @@
import crypto from "crypto";
export const webhook = {
typeId: "WEBHOOK",
title: "Webhook",
description: "Notify an external endpoint when events happen in your form (e.g. a new submission).",
};
const eventTypes = [
{
id: "SUBMISSION_CREATED",
name: "Submission Created",
description: "Every time a new submission is created",
},
];
export function WebhookSettings({ pipeline, setPipeline }) {
const toggleEvent = (eventId) => {
const newPipeline = JSON.parse(JSON.stringify(pipeline));
const eventIdx = newPipeline.events.indexOf(eventId);
if (eventIdx !== -1) {
newPipeline.events.splice(eventIdx, 1);
} else {
newPipeline.events.push(eventId);
}
setPipeline(newPipeline);
};
const updateField = (field, value, parent = null) => {
const newPipeline = JSON.parse(JSON.stringify(pipeline));
if (parent) {
newPipeline[parent][field] = value;
} else {
newPipeline[field] = value;
}
setPipeline(newPipeline);
};
return (
<div className="space-y-8 divide-y divide-gray-200">
<div>
<h2 className="text-ui-gray-dark mb-3 text-xl font-bold">Configure Webhook</h2>
<p className="mt-1 text-sm text-gray-500">
Configure your webhook. To learn more about how webhooks work, please check out our docs.
</p>
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-4">
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
Webhook Label
</label>
<div className="mt-1">
<input
type="text"
name="label"
id="label"
value={pipeline.label || ""}
onChange={(e) => updateField("label", e.target.value)}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-teal-500 focus:ring-teal-500 sm:text-sm"
required
/>
</div>
</div>
<div className="sm:col-span-4">
<label htmlFor="endpointUrl" className="block text-sm font-medium text-gray-700">
Endpoint URL
</label>
<div className="mt-1">
<input
type="url"
pattern="^https:\/\/(.*)"
onInvalid={(e: any) =>
e.target.setCustomValidity("please provide a valid website address with https")
}
onInput={(e: any) => e.target.setCustomValidity("")}
name="endpointUrl"
id="endpointUrl"
value={pipeline.config.endpointUrl || ""}
onChange={(e) => updateField("endpointUrl", e.target.value, "config")}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-teal-500 focus:ring-teal-500 sm:text-sm"
required
/>
</div>
<p className="mt-2 text-xs text-gray-500" id="email-description">
Your server URL to which the data should be sent (https requiteal)
</p>
</div>
<div className="sm:col-span-4">
<label htmlFor="secret" className="block text-sm font-medium text-gray-700">
Secret
</label>
<div className="mt-1">
<input
type="text"
name="secret"
id="secret"
value={pipeline.config.secret || ""}
onChange={(e) => updateField("secret", e.target.value, "config")}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-teal-500 focus:ring-teal-500 sm:text-sm"
/>
</div>
<p className="mt-2 text-xs text-gray-500" id="email-description">
We sign all event notification payloads with a SHA256 signature using this secret
</p>
</div>
</div>
</div>
<div className="pt-8">
<div>
<h3 className="text-lg font-medium leading-6 text-gray-900">Advanced Settings</h3>
<p className="mt-1 text-sm text-gray-500">Set up this webhook to fit your needs.</p>
</div>
<div className="mt-6">
<fieldset>
<legend className="sr-only">Events</legend>
<div className="text-base font-medium text-gray-900" aria-hidden="true">
Events
</div>
<div className="mt-4 space-y-4">
{eventTypes.map((eventType) => (
<div key={eventType.id}>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
id="comments"
name="comments"
type="checkbox"
checked={pipeline.events.includes(eventType.id)}
onChange={() => toggleEvent(eventType.id)}
className="h-4 w-4 rounded-sm border-gray-300 text-teal-600 focus:ring-teal-500"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="comments" className="font-medium text-gray-700">
{eventType.name}
</label>
<p className="text-gray-500">{eventType.description}</p>
</div>
</div>
</div>
))}
</div>
</fieldset>
</div>
<div className="mt-6">
<fieldset>
<legend className="sr-only">Conditions</legend>
<div className="text-base font-medium text-gray-900" aria-hidden="true">
Conditions
</div>
<div className="mt-4 space-y-4">
<div className="rounded-sm border border-gray-100 bg-gray-50 px-2 py-5">
<p className="flex justify-center text-xs text-gray-600">
conditional data piping coming soon
</p>
</div>
</div>
</fieldset>
</div>
</div>
</div>
);
}
export async function handleWebhook(pipeline, event) {
if (pipeline.config.hasOwnProperty("endpointUrl") && pipeline.config.hasOwnProperty("secret")) {
if (event.type === "pageSubmission" && pipeline.events.includes("PAGE_SUBMISSION")) {
const webhookData = pipeline.config;
const body = { time: Math.floor(Date.now() / 1000), event };
fetch(webhookData.endpointUrl.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Hub-Signature-256": `sha256=${crypto
.createHmac("sha256", webhookData.secret.toString())
.update(JSON.stringify(body))
.digest("base64")}`,
},
body: JSON.stringify(body),
});
}
}
}
+61
View File
@@ -0,0 +1,61 @@
import useSWR from "swr";
import { fetcher } from "./utils";
export const usePipelines = (formId: string, teamId: string) => {
const { data, error, mutate } = useSWR(`/api/teams/${teamId}/forms/${formId}/pipelines`, fetcher);
return {
pipelines: data,
isLoadingPipelines: !error && !data,
isErrorPipelines: error,
mutatePipelines: mutate,
};
};
export const usePipeline = (id: string, formId: string, teamId: string) => {
const { data, error, mutate } = useSWR(`/api/teams/${teamId}/forms/${formId}/pipelines/${id}`, fetcher);
return {
pipeline: data,
isLoadingPipeline: !error && !data,
isErrorPipeline: error,
mutatePipeline: mutate,
};
};
export const persistPipeline = async (formId, teamId, pipeline) => {
try {
await fetch(`/api/teams/${teamId}/forms/${formId}/pipelines/${pipeline.id}/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(pipeline),
});
} catch (error) {
console.error(error);
}
};
export const createPipeline = async (formId: string, teamId: string, pipeline = {}) => {
try {
const res = await fetch(`/api/teams/${teamId}/forms/${formId}/pipelines`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(pipeline),
});
return await res.json();
} catch (error) {
console.error(error);
throw Error(`createPipeline: unable to create pipeline: ${error.message}`);
}
};
export const deletePipeline = async (formId: string, teamId: string, pipelineId: string) => {
try {
await fetch(`/api/teams/${teamId}/forms/${formId}/pipelines/${pipelineId}`, {
method: "DELETE",
});
} catch (error) {
console.error(error);
throw Error(`deletePipeline: unable to delete pipeline: ${error.message}`);
}
};
@@ -29,9 +29,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
// GET /api/teams[teamId]/forms/[formId]
// Get a specific team
if (req.method === "GET") {
const forms = await prisma.form.findUnique({
const forms = await prisma.form.findFirst({
where: {
id: formId,
teamId,
},
});
@@ -42,7 +43,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
// Deletes a single form
else if (req.method === "DELETE") {
const prismaRes = await prisma.form.delete({
where: { id: formId },
where: { id: formId, teamId },
});
return res.json(prismaRes);
}
@@ -0,0 +1,66 @@
import { getSessionOrUser } from "@/lib/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const teamId = req.query.teamId.toString();
const formId = req.query.formId.toString();
const pipelineId = req.query.pipelineId.toString();
// check team permission
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId: user.id,
teamId,
},
},
});
if (membership === null) {
return res.status(403).json({ message: "You don't have access to this team or this team doesn't exist" });
}
// GET /api/teams[teamId]/forms/[formId]/pipelines/[pipelineId]
// Get a specific pipeline
if (req.method === "GET") {
const forms = await prisma.pipeline.findUnique({
where: {
id: pipelineId,
formId: formId,
},
});
return res.json(forms);
}
// POST /api/teams[teamId]/forms/[formId]/pipelines/[pipelineId]
// Replace a specific pipeline
else if (req.method === "POST") {
const data = { ...req.body, updatedAt: new Date() };
const prismaRes = await prisma.pipeline.update({
where: { id: pipelineId },
data,
});
return res.json(prismaRes);
}
// Delete /api/teams[teamId]/forms/[formId]/pipelines/[pipelineId]
// Deletes a single form
else if (req.method === "DELETE") {
const prismaRes = await prisma.pipeline.delete({
where: { id: pipelineId, formId },
});
return res.json(prismaRes);
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}
@@ -0,0 +1,65 @@
import { getSessionOrUser } from "@/lib/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const teamId = req.query.teamId.toString();
const formId = req.query.formId.toString();
// check team permission
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId: user.id,
teamId,
},
},
});
if (membership === null) {
return res.status(403).json({ message: "You don't have access to this team or this team doesn't exist" });
}
// GET /api/teams[teamId]/forms/[formId]/pipelines
// Get pipelines
if (req.method === "GET") {
// get submission
const pipelines = await prisma.pipeline.findMany({
where: {
form: {
id: formId,
teamId,
},
},
});
return res.json(pipelines);
}
// POST /api/teams[teamId]/forms/[formId]/pipelines
// Create a new pipeline
// Required fields in body: name, type
// Optional fields in body: enabled, config
else if (req.method === "POST") {
const pipeline = req.body;
// create form in db
const result = await prisma.pipeline.create({
data: {
...pipeline,
form: { connect: { id: formId } },
},
});
res.json(result);
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}
@@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the column `name` on the `Pipeline` table. All the data in the column will be lost.
- Added the required column `label` to the `Pipeline` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "PipelineEvent" AS ENUM ('SUBMISSION_CREATED');
-- AlterTable
ALTER TABLE "Pipeline" DROP COLUMN "name",
ADD COLUMN "events" "PipelineEvent"[],
ADD COLUMN "label" TEXT NOT NULL;
+12 -7
View File
@@ -18,16 +18,21 @@ enum PipelineType {
EMAIL_NOTIFICATION
}
enum PipelineEvent {
SUBMISSION_CREATED
}
model Pipeline {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
label String
type PipelineType
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
events PipelineEvent[]
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String
enabled Boolean @default(false)
config Json @default("{}")
enabled Boolean @default(false)
config Json @default("{}")
}
model Customer {