diff --git a/web/components/web-hooks/delete-webhook-modal.tsx b/web/components/web-hooks/delete-webhook-modal.tsx new file mode 100644 index 00000000000..79ce86fabf2 --- /dev/null +++ b/web/components/web-hooks/delete-webhook-modal.tsx @@ -0,0 +1,128 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Button, Input } from "@plane/ui"; +import { error } from "console"; +import useToast from "hooks/use-toast"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { AlertTriangle } from "lucide-react"; +import { useRouter } from "next/router"; +import React, { useState } from "react"; +import { FC } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { WebhookStore } from "store/webhook.store"; + +interface IDeleteWebhook { + isOpen: boolean; + webhook_url: string; + onClose: () => void; +} + +export const DeleteWebhookModal: FC = (props) => { + const { isOpen, onClose } = props; + + const router = useRouter(); + + const { webhook: webhookStore } = useMobxStore(); + + const { setToastAlert } = useToast(); + + const [deleting, setDelete] = useState(false); + + const { + control, + formState: { errors, isSubmitting }, + handleSubmit, + reset, + watch, + } = useForm({}); + + const { workspaceSlug, webhookId } = router.query; + + const handleClose = () => { + onClose(); + }; + + const handleDelete = async () => { + setDelete(true); + if (!workspaceSlug || !webhookId) return; + webhookStore + .remove(workspaceSlug.toString(), webhookId.toString()) + .then(() => { + setToastAlert({ + title: "Success", + type: "success", + message: "Successfully deleted", + }); + router.replace(`/${workspaceSlug}/settings/webhooks/`); + }) + .catch((error) => { + console.log(error); + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.error, + }); + }) + .finally(() => { + setDelete(false); + }); + }; + + return ( + + + +
+ + +
+
+ + +
+ + + +

Delete Webhook

+
+
+ + +

+ Are you sure you want to delete workspace ? All + of the data related to the workspace will be permanently removed. This action cannot be undone. +

+
+ +
+ + +
+
+
+
+
+
+
+ ); +}; diff --git a/web/components/web-hooks/empty-webhooks.tsx b/web/components/web-hooks/empty-webhooks.tsx new file mode 100644 index 00000000000..c27efcccf78 --- /dev/null +++ b/web/components/web-hooks/empty-webhooks.tsx @@ -0,0 +1,31 @@ +import { FC } from "react"; +import Link from "next/link"; +import { Button } from "@plane/ui"; +import Image from "next/image"; +import EmptyWebhookLogo from "public/empty-state/issue.svg"; + +interface IWebHookLists { + workspaceSlug: string; +} + + +export const EmptyWebhooks:FC = (props) => { + + const {workspaceSlug} = props; + + return ( +
+
+ empty-webhook image + +
No Webhooks
+

Create webhooks to receive real-time updates and automate actions

+ + + +
+
+ ); +}; \ No newline at end of file diff --git a/web/components/web-hooks/generate-key.tsx b/web/components/web-hooks/generate-key.tsx new file mode 100644 index 00000000000..4cbfe086343 --- /dev/null +++ b/web/components/web-hooks/generate-key.tsx @@ -0,0 +1,122 @@ +import { useState, FC } from "react"; +import { Button, Input } from "@plane/ui"; +import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; +import { generateRandomString } from "helpers/generate-random-string"; +import { observer } from "mobx-react-lite"; +import { RootStore } from "store/root"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { copyTextToClipboard, copyUrlToClipboard } from "helpers/string.helper"; +import { useRouter } from "next/router"; +import useToast from "hooks/use-toast"; +import { csvDownload } from "helpers/download.helper"; +import { stringify } from "querystring"; +import { renderDateFormat } from "helpers/date-time.helper"; + +interface IGenerateKey { + type: "create" | "edit"; +} + +export const GenerateKey: FC = observer((props) => { + const { type } = props; + const [regenerating, setRegenerate] = useState(false); + const router = useRouter(); + const { workspaceSlug, webhookId } = router.query as { workspaceSlug: string, webhookId: string }; + const { webhook: webhookStore, workspace: workspaceStore }: RootStore = useMobxStore(); + const { setToastAlert } = useToast(); + // const [showgenerateKey, setShowGenarateKey] = useState(false); + + function handleRegenerate() { + setRegenerate(true); + webhookStore.regenerate(workspaceSlug, webhookId).then( + () => { + // setShowGenarateKey(true); + setToastAlert({ + title: "Success", + type: "success", + message: "Successfully regenerated", + }); + csvDownload([[ + "id", + "url", + "created_at", + "updated_at", + "is_active", + "secret_key", + "project", + "issue", + "module", + "cycle", + "issue_comment", + "workspace" + ], [ + webhookStore.currentWebhook?.id!, + webhookStore.currentWebhook?.url!, + renderDateFormat(webhookStore.currentWebhook?.created_at!), + renderDateFormat(webhookStore.currentWebhook?.updated_at!), + String(webhookStore.currentWebhook?.is_active!), + webhookStore.currentWebhook?.secret_key!, + String(webhookStore.currentWebhook?.project!), + String(webhookStore.currentWebhook?.issue!), + String(webhookStore.currentWebhook?.module!), + String(webhookStore.currentWebhook?.cycle!), + String(webhookStore.currentWebhook?.issue_comment!), + workspaceStore.currentWorkspace?.name!, + ] + ], "Secret-key") + } + ).catch((err)=>{ + setToastAlert({ + title: "Oops!", + type: "error", + message: err?.error, + }); + }).finally(()=>{ + setRegenerate(false); + }) + } + console.log(webhookStore?.webhookSecretKey); + return ( + <> + {(type === 'edit' || (type === 'create' && webhookStore?.webhookSecretKey)) && ( +
+
Secret Key
+
Genarate a token to sign-in the webhook payload
+ +
+
+
+ {(webhookStore?.webhookSecretKey) ?
{webhookStore?.webhookSecretKey}
: +
+ {[...Array(41)].map((_, index) =>
)} +
+ } +
+ {webhookStore?.webhookSecretKey && ( +
copyTextToClipboard(webhookStore?.webhookSecretKey || '')}> + { + navigator.clipboard.writeText(webhookStore.webhookSecretKey!); + setToastAlert({ + title: "Success", + type: "success", + message: "Secret key copied", + }); + }} className="text-custom-text-400 w-4 h-4" /> +
+ )} +
+
+ {type != 'create' && ( + + )} +
+
+
+ )} + + ); +}); diff --git a/web/components/web-hooks/index.ts b/web/components/web-hooks/index.ts new file mode 100644 index 00000000000..91911ad9afb --- /dev/null +++ b/web/components/web-hooks/index.ts @@ -0,0 +1,4 @@ +export * from "./empty-webhooks"; +export * from "./webhooks-list"; +export * from "./webhooks-list-item"; +export * from "./webhook-form"; diff --git a/web/components/web-hooks/webhook-form.tsx b/web/components/web-hooks/webhook-form.tsx new file mode 100644 index 00000000000..d9e533ef764 --- /dev/null +++ b/web/components/web-hooks/webhook-form.tsx @@ -0,0 +1,286 @@ +import React, { FC, useEffect, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { Button, Input, ToggleSwitch } from "@plane/ui"; +import { IWebhook, IExtendedWebhook } from "types"; +import { GenerateKey } from "./generate-key"; +import { observer } from "mobx-react-lite"; +import { RootStore } from "store/root"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { Disclosure, Transition } from "@headlessui/react"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { DeleteWebhookModal } from "./delete-webhook-modal"; + +interface IWebhookDetails { + type: "create" | "edit"; + initialData: IWebhook; + onSubmit: (val: IExtendedWebhook) => void; +} + +export const WebhookDetails: FC = observer((props) => { + const { type, initialData, onSubmit } = props; + const { webhook: webhookStore }: RootStore = useMobxStore(); + const [openDeleteModal, setOpenDeleteModal] = useState(false); + const { + reset, + watch, + handleSubmit, + control, + getValues, + formState: { isSubmitting }, + } = useForm(); + + const checkWebhookEvent = (initialData: IWebhook) => { + const { project, module, cycle, issue, issue_comment } = initialData; + if (!project || !cycle || !module || !issue || !issue_comment) { + return "individual"; + } + return "all"; + } + + useEffect(() => { + if (initialData && reset) reset({ ...initialData, webhook_events: checkWebhookEvent(initialData) }); + }, [initialData, reset]); + + useEffect(() => { + if (watch("webhook_events")) { + if (watch("webhook_events") === "all") + reset({ ...getValues(), project: watch('project') ?? true, module: watch('module') ?? true, cycle: watch('cycle') ?? true, issue: watch('issue') ?? true, issue_comment: watch('issue_comment') ?? true }); + if (watch("webhook_events") === "individual") + reset({ ...getValues(), project: watch('project') ?? false, module: watch('module') ?? false, cycle: watch('cycle') ?? false, issue: watch('issue') ?? false, issue_comment: watch('issue_comment') ?? false }); + } + }, [watch && watch("webhook_events")]); + + return ( + <> + {setOpenDeleteModal(false)}} + /> +
+
+
+
URL
+ ( + + )} + /> +
+ +
+
Enable webhook
+ ( + { + onChange(val); + }} + size="sm" + /> + )} + /> +
+ +
+
Which events do you like to trigger this webhook
+ +
+ ( + onChange("all")} + /> + )} + /> + +
+ +
+ ( + onChange("individual")} + /> + )} + /> + +
+ + {watch("webhook_events") === "individual" && ( +
+
+ ( +
+ onChange(!value)} + type="checkbox" + name="selectIndividualEvents" + checked={value == true} + /> + +
+ )} + /> + ( +
+ onChange(!value)} + type="checkbox" + name="selectIndividualEvents" + checked={value == true} + /> + +
+ )} + /> + ( +
+ onChange(!value)} + type="checkbox" + name="selectIndividualEvents" + checked={value == true} + /> + +
+ )} + /> + ( +
+ onChange(!value)} + type="checkbox" + name="selectIndividualEvents" + checked={value == true} + /> + +
+ )} + /> + + ( +
+ onChange(!value)} + type="checkbox" + name="selectIndividualEvents" + checked={value == true} + /> + +
+ )} + /> +
+
+ )} +
+ + + +
+ +
+ {type === "edit" && + + {({ open }) => ( +
+ + Danger Zone + {open ? : } + + + + +
+ + The danger zone of the workspace delete page is a critical area that requires careful + consideration and attention. When deleting a workspace, all of the data and resources within + that workspace will be permanently removed and cannot be recovered. + +
+ +
+
+
+
+
+ )} +
} +
+
+ + ); +}); diff --git a/web/components/web-hooks/webhooks-list-item.tsx b/web/components/web-hooks/webhooks-list-item.tsx new file mode 100644 index 00000000000..7e590a9cd33 --- /dev/null +++ b/web/components/web-hooks/webhooks-list-item.tsx @@ -0,0 +1,34 @@ +import { FC, useState } from "react"; +import { ToggleSwitch } from "@plane/ui"; +import { Pencil, XCircle } from "lucide-react"; +import { IWebhook } from "types"; +import Link from "next/link"; + +interface IWebhookListItem { + workspaceSlug: string; + webhook: IWebhook; +} + +export const WebhooksListItem: FC = (props) => { + const { workspaceSlug, webhook } = props; + + const [toggle, setToggle] = useState(false); + + return ( +
+ +
+
+
{webhook?.url || "Webhook URL"}
+ {/*
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +
*/} +
+ {/*
+ setToggle(!toggle)} /> +
*/} +
+ +
+ ); +}; diff --git a/web/components/web-hooks/webhooks-list.tsx b/web/components/web-hooks/webhooks-list.tsx new file mode 100644 index 00000000000..5a58d7c4ebb --- /dev/null +++ b/web/components/web-hooks/webhooks-list.tsx @@ -0,0 +1,35 @@ +import { FC } from "react"; +import Link from "next/link"; +import { Button } from "@plane/ui"; +import { WebhooksListItem } from "./webhooks-list-item"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +import { observer } from "mobx-react-lite"; + +interface IWebHookLists { + workspaceSlug: string; +} + +export const WebhookLists: FC = observer((props) => { + const { workspaceSlug } = props; + const { webhook: webhookStore }: RootStore = useMobxStore(); + + return ( + <> +
+
Webhooks
+ + + +
+ +
+ {webhookStore.webhooks.map((item) => ( + + ))} +
+ + ); +}); diff --git a/web/helpers/download.helper.ts b/web/helpers/download.helper.ts new file mode 100644 index 00000000000..9a94ef9445d --- /dev/null +++ b/web/helpers/download.helper.ts @@ -0,0 +1,13 @@ +import { IApiToken } from "types/api_token"; +import { renderDateFormat } from "./date-time.helper"; + +export const csvDownload = (rows: Array>, name: string) => { + + let csvContent = "data:text/csv;charset=utf-8," + rows.map((e) => e.join(",")).join("\n"); + var encodedUri = encodeURI(csvContent); + var link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", `${name}.csv`); + document.body.appendChild(link); + link.click(); +}; diff --git a/web/helpers/generate-random-string.ts b/web/helpers/generate-random-string.ts new file mode 100644 index 00000000000..3d85b9b7f8a --- /dev/null +++ b/web/helpers/generate-random-string.ts @@ -0,0 +1,13 @@ + + +export const generateRandomString = (length: number) => { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + let counter = 0; + while (counter < length) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + counter += 1; + } + return result; +} \ No newline at end of file diff --git a/web/layouts/settings-layout/workspace/sidebar.tsx b/web/layouts/settings-layout/workspace/sidebar.tsx index 6e09c53fdde..867dd57e31a 100644 --- a/web/layouts/settings-layout/workspace/sidebar.tsx +++ b/web/layouts/settings-layout/workspace/sidebar.tsx @@ -26,6 +26,10 @@ export const WorkspaceSettingsSidebar = () => { label: "Integrations", href: `/${workspaceSlug}/settings/integrations`, }, + { + label: "Webhooks", + href: `/${workspaceSlug}/settings/webhooks`, + }, { label: "Api tokens", href: `/${workspaceSlug}/settings/api-tokens`, diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx new file mode 100644 index 00000000000..90a5a9909ae --- /dev/null +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -0,0 +1,70 @@ +import type { NextPage } from "next"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +import { observer } from "mobx-react-lite"; +// layout +import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; +// components +import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookDetails } from "components/web-hooks"; +// hooks +import { useMobxStore } from "lib/mobx/store-provider"; +// types +import { RootStore } from "store/root"; +import { IExtendedWebhook, IWebhook } from "types"; +import { Spinner } from "@plane/ui"; +import { useEffect } from "react"; + +const Webhooks: NextPage = observer(() => { + const router = useRouter(); + const { workspaceSlug, webhookId } = router.query as { workspaceSlug: string; webhookId: string }; + + const { webhook: webhookStore }: RootStore = useMobxStore(); + + useEffect(() => { + webhookStore.clearSecretKey(); + }, []); + + const { isLoading } = useSWR( + workspaceSlug && webhookId ? `WEBHOOKS_DETAIL_${workspaceSlug}_${webhookId}` : null, + workspaceSlug && webhookId + ? async () => { + await webhookStore.fetchById(workspaceSlug, webhookId); + } + : null + ); + + const onSubmit = (data: IExtendedWebhook): Promise => { + const payload = { + url: data?.url, + is_active: data?.is_active, + project: data?.project, + cycle: data?.cycle, + module: data?.module, + issue: data?.issue, + issue_comment: data?.issue_comment, + }; + return webhookStore.update(workspaceSlug, webhookId, payload); + }; + + const initialPayload = webhookStore.currentWebhook as IWebhook; + + return ( + }> + +
+ {isLoading ? +
+ +
+ : + + } +
+
+
+ ); +}); + +export default Webhooks; diff --git a/web/pages/[workspaceSlug]/settings/webhooks/create.tsx b/web/pages/[workspaceSlug]/settings/webhooks/create.tsx new file mode 100644 index 00000000000..a8e150ff5c3 --- /dev/null +++ b/web/pages/[workspaceSlug]/settings/webhooks/create.tsx @@ -0,0 +1,109 @@ +import React, { useEffect } from "react"; +import { useRouter } from "next/router"; +import type { NextPage } from "next"; +import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; +import { WebhookDetails } from "components/web-hooks"; +import { IWebhook, IExtendedWebhook } from "types"; +import { RootStore } from "store/root"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { renderDateFormat } from "helpers/date-time.helper"; +import { csvDownload } from "helpers/download.helper"; +import useToast from "hooks/use-toast"; + +const Webhooks: NextPage = () => { + const router = useRouter(); + + const { workspaceSlug } = router.query as { workspaceSlug: string }; + + const initialWebhookPayload: IWebhook = { + url: "", + is_active: true, + created_at: "", + updated_at: "", + secret_key: "", + project: true, + issue_comment: true, + cycle: true, + module: true, + issue: true, + workspace: "", + }; + + const { webhook: webhookStore, workspace: workspaceStore }: RootStore = useMobxStore(); + + const { setToastAlert } = useToast(); + + const onSubmit = async (data: IExtendedWebhook) => { + const payload = { + url: data?.url, + is_active: data?.is_active, + project: data?.project, + cycle: data?.cycle, + module: data?.module, + issue: data?.issue, + issue_comment: data?.issue_comment, + }; + + const response = await webhookStore.create(workspaceSlug, payload).then( + (webhook) => { + setToastAlert({ + title: "Success", + type: "success", + message: "Successfully created", + }); + csvDownload([[ + "id", + "url", + "created_at", + "updated_at", + "is_active", + "secret_key", + "project", + "issue", + "module", + "cycle", + "issue_comment", + "workspace" + ], [ + webhook.id!, + webhook.url!, + renderDateFormat(webhook.updated_at!), + renderDateFormat(webhook.created_at!), + webhookStore.webhookSecretKey!, + String(webhook.is_active!), + String(webhook.issue!), + String(webhook.project!), + String(webhook.module!), + String(webhook.cycle!), + String(webhook.issue_comment!), + workspaceStore.currentWorkspace?.name!, + ] + ], "Secret-key"); + } + ).catch((error) => { + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.error ?? "Something went wrong!", + }); + }) + }; + + useEffect(() => { + webhookStore.clearSecretKey(); + }, []); + + return ( + }> + +
+ +
+
+
+ ); +}; + +export default Webhooks; diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx new file mode 100644 index 00000000000..1b3fd1e1fdf --- /dev/null +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import type { NextPage } from "next"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +import { observer } from "mobx-react-lite"; +// layout +import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; +// components +import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookLists, EmptyWebhooks } from "components/web-hooks"; +// hooks +import { useMobxStore } from "lib/mobx/store-provider"; +// types +import { RootStore } from "store/root"; +import { Spinner } from "@plane/ui"; + +const Webhooks: NextPage = observer(() => { + const router = useRouter(); + const { workspaceSlug } = router.query as { workspaceSlug: string }; + + const { webhook: webhookStore }: RootStore = useMobxStore(); + + const { isLoading } = useSWR( + workspaceSlug ? `WEBHOOKS_LIST_${workspaceSlug}` : null, + workspaceSlug + ? async () => { + await webhookStore.fetchAll(workspaceSlug); + } + : null + ); + + return ( + }> + +
+ {webhookStore.webhooks.length > 0 ? isLoading ?
+ +
: ( + + ) : ( + + )} +
+
+
+ ); +}); + +export default Webhooks; diff --git a/web/services/webhook.service.ts b/web/services/webhook.service.ts new file mode 100644 index 00000000000..05f622aef21 --- /dev/null +++ b/web/services/webhook.service.ts @@ -0,0 +1,60 @@ +// api services +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import { IWebhook } from "types"; + +export class WebhookService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getAll(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/webhooks/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getById(workspaceSlug: string, webhook_id: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/webhooks/${webhook_id}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async create(workspaceSlug: string, data: {}): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/webhooks/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async update(workspaceSlug: string, webhook_id: string, data: {}): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/webhooks/${webhook_id}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async remove(workspaceSlug: string, webhook_id: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/webhooks/${webhook_id}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async regenerate(workspaceSlug: string, webhook_id: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/webhooks/${webhook_id}/regenerate/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/store/root.ts b/web/store/root.ts index 50c68e2a40e..36116041088 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -97,11 +97,9 @@ import { InboxIssuesStore, InboxStore, } from "store/inbox"; +import { IWebhookStore, WebhookStore } from "./webhook.store"; -import { - IMentionsStore, - MentionsStore -} from "store/editor" +import { IMentionsStore, MentionsStore } from "store/editor"; enableStaticRendering(typeof window === "undefined"); @@ -164,6 +162,8 @@ export class RootStore { inboxIssueDetails: IInboxIssueDetailsStore; inboxFilters: IInboxFiltersStore; + webhook: IWebhookStore; + mentionsStore: IMentionsStore; constructor() { @@ -225,6 +225,8 @@ export class RootStore { this.inboxIssueDetails = new InboxIssueDetailsStore(this); this.inboxFilters = new InboxFiltersStore(this); + this.webhook = new WebhookStore(this); + this.mentionsStore = new MentionsStore(this); } } diff --git a/web/store/webhook.store.ts b/web/store/webhook.store.ts new file mode 100644 index 00000000000..7e5bccb94a6 --- /dev/null +++ b/web/store/webhook.store.ts @@ -0,0 +1,195 @@ +// mobx +import { action, observable, makeObservable, computed, runInAction } from "mobx"; +import { IWebhook } from "types"; +import { WebhookService } from "services/webhook.service"; + +export interface IWebhookStore { + loader: boolean; + error: any | null; + + webhooks: IWebhook[] | []; + webhook_id: string | null; + webhook_detail: { + [webhook_id: string]: IWebhook; + } | null; + webhookSecretKey: string | null; + + // computed + currentWebhook: IWebhook | null; + + // actions + fetchAll: (workspaceSlug: string) => Promise; + fetchById: (workspaceSlug: string, webhook_id: string) => Promise; + create: (workspaceSlug: string, data: IWebhook) => Promise; + update: (workspaceSlug: string, webhook_id: string, data: IWebhook) => Promise; + remove: (workspaceSlug: string, webhook_id: string) => Promise; + regenerate: (workspaceSlug: string, webhook_id: string) => Promise; + clearSecretKey: () => void; +} + +export class WebhookStore implements IWebhookStore { + loader: boolean = false; + error: any | null = null; + + webhooks: IWebhook[] | [] = []; + webhook_id: string | null = null; + webhook_detail: { + [webhook_id: string]: IWebhook; + } | null = null; + webhookSecretKey: string | null = null; + + // root store + rootStore; + webhookService; + + constructor(_rootStore: any | null = null) { + makeObservable(this, { + loader: observable.ref, + error: observable.ref, + + webhooks: observable.ref, + webhook_id: observable.ref, + webhook_detail: observable.ref, + webhookSecretKey: observable.ref, + + currentWebhook: computed, + + fetchAll: action, + create: action, + fetchById: action, + update: action, + remove: action, + regenerate: action, + clearSecretKey: action + }); + this.rootStore = _rootStore; + this.webhookService = new WebhookService(); + } + + get currentWebhook() { + if (!this.webhook_id) return null; + const currentWebhook = this.webhook_detail ? this.webhook_detail[this.webhook_id] : null; + return currentWebhook; + } + + fetchAll = async (workspaceSlug: string) => { + try { + this.loader = true; + this.error = null; + const webhookResponse = await this.webhookService.getAll(workspaceSlug); + + runInAction(() => { + this.webhooks = webhookResponse; + this.loader = true; + this.error = null; + }); + + return webhookResponse; + } catch (error) { + this.loader = false; + this.error = error; + throw error; + } + }; + + create = async (workspaceSlug: string, data: IWebhook) => { + try { + const webhookResponse = await this.webhookService.create(workspaceSlug, data); + + const _secretKey = webhookResponse?.secret_key; + delete webhookResponse?.secret_key; + const _webhooks = [...this.webhooks, webhookResponse]; + runInAction(() => { + this.webhookSecretKey = _secretKey || null; + this.webhooks = _webhooks; + this.webhook_detail = {...this.webhook_detail, [webhookResponse.id!]: webhookResponse}; + this.webhook_id = webhookResponse.id!; + console.log(this.webhook_detail); + }); + + return webhookResponse; + } catch (error) { + console.log(error); + throw error; + } + }; + + fetchById = async (workspaceSlug: string, webhook_id: string) => { + try { + const webhookResponse = await this.webhookService.getById(workspaceSlug, webhook_id); + + const _webhook_detail = { + ...this.webhook_detail, + [webhook_id]: webhookResponse, + }; + runInAction(() => { + this.webhook_id = webhook_id; + this.webhook_detail = _webhook_detail; + }); + + return webhookResponse; + } catch (error) { + console.log(error); + throw error; + } + }; + + update = async (workspaceSlug: string, webhook_id: string, data: IWebhook) => { + try { + const webhookResponse = await this.webhookService.update(workspaceSlug, webhook_id, data); + + const _updatedWebhooks = this.webhooks.map((element) => { + if (element.id === webhook_id) { + return webhookResponse; + } else { + return element; + } + }); + const _webhookDetail = { ...this.webhook_detail, [webhook_id]: webhookResponse }; + runInAction(() => { + this.webhooks = _updatedWebhooks; + this.webhook_detail = _webhookDetail; + }); + + return webhookResponse; + } catch (error) { + console.log(error); + throw error; + } + }; + + remove = async (workspaceSlug: string, webhook_id: string) => { + try { + await this.webhookService.remove(workspaceSlug, webhook_id); + + const _webhooks = this.webhooks.filter((element) => element.id != webhook_id); + const _webhookDetail = { ...this.webhook_detail }; + delete _webhookDetail[webhook_id]; + runInAction(() => { + this.webhooks = _webhooks; + this.webhook_detail = _webhookDetail; + }); + } catch (error) { + console.log(error); + throw error; + } + }; + + regenerate = async (workspaceSlug: string, webhook_id: string) => { + try { + const webhookResponse = await this.webhookService.regenerate(workspaceSlug, webhook_id); + runInAction(() => { + this.webhookSecretKey = webhookResponse.secret_key!; + this.webhook_detail = {...this.webhook_detail, [webhook_id]: webhookResponse}; + }); + return webhookResponse; + } catch (error) { + console.log(error); + throw error; + } + }; + + clearSecretKey = () => { + this.webhookSecretKey = null; + } +} diff --git a/web/types/index.d.ts b/web/types/index.d.ts index b350901066f..9f27e818ca1 100644 --- a/web/types/index.d.ts +++ b/web/types/index.d.ts @@ -20,6 +20,7 @@ export * from "./waitlist"; export * from "./reaction"; export * from "./view-props"; export * from "./workspace-views"; +export * from "./webhook"; export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object diff --git a/web/types/webhook.d.ts b/web/types/webhook.d.ts new file mode 100644 index 00000000000..b1420c2af80 --- /dev/null +++ b/web/types/webhook.d.ts @@ -0,0 +1,19 @@ +export interface IWebhook { + id?: string; + secret_key?: string; + url: string; + created_at?: string; + updated_at?: string; + is_active: boolean; + project: boolean; + cycle: boolean; + module: boolean; + issue: boolean; + issue_comment?: boolean; + workspace?: string; +} + +// this interface is used to handle the webhook form state +interface IExtendedWebhook extends IWebhook { + webhook_events: string; +}