diff --git a/web/components/web-hooks/create-webhook-modal.tsx b/web/components/web-hooks/create-webhook-modal.tsx new file mode 100644 index 000000000..5bb8a2c76 --- /dev/null +++ b/web/components/web-hooks/create-webhook-modal.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { Dialog, Transition } from "@headlessui/react"; +// components +import { WebhookForm } from "./form"; +import { GeneratedHookDetails } from "./generated-hook-details"; +// hooks +import useToast from "hooks/use-toast"; +// helpers +import { csvDownload } from "helpers/download.helper"; +// utils +import { getCurrentHookAsCSV } from "./utils"; +// types +import { IWebhook, IWorkspace, TWebhookEventTypes } from "types"; + +interface WebhookWithKey { + webHook: IWebhook; + secretKey: string | undefined; +} +interface ICreateWebhookModal { + currentWorkspace: IWorkspace | null; + isOpen: boolean; + clearSecretKey: () => void; + createWebhook: (workspaceSlug: string, data: Partial) => Promise; + onClose: () => void; +} + +export const CreateWebhookModal: React.FC = (props) => { + const { isOpen, onClose, currentWorkspace, createWebhook, clearSecretKey } = props; + // states + const [generatedWebhook, setGeneratedKey] = useState(null); + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + // toast + const { setToastAlert } = useToast(); + + const handleCreateWebhook = async (formData: IWebhook, webhookEventType: TWebhookEventTypes) => { + if (!workspaceSlug) return; + + let payload: Partial = { + url: formData.url, + }; + + if (webhookEventType === "all") + payload = { + ...payload, + project: true, + cycle: true, + module: true, + issue: true, + issue_comment: true, + }; + else + payload = { + ...payload, + project: formData.project ?? false, + cycle: formData.cycle ?? false, + module: formData.module ?? false, + issue: formData.issue ?? false, + issue_comment: formData.issue_comment ?? false, + }; + + await createWebhook(workspaceSlug.toString(), payload) + .then(({ webHook, secretKey }) => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Webhook created successfully.", + }); + + setGeneratedKey(webHook); + + const csvData = getCurrentHookAsCSV(currentWorkspace, webHook, secretKey); + csvDownload(csvData, `webhook-secret-key-${Date.now()}`); + }) + .catch((error) => { + setToastAlert({ + type: "error", + title: "Error!", + message: error?.error ?? "Something went wrong. Please try again.", + }); + }); + }; + + const handleClose = () => { + onClose(); + setTimeout(() => { + clearSecretKey(); + setGeneratedKey(null); + }, 350); + }; + + return ( + + { + if (!generatedWebhook) handleClose(); + }} + > + +
+ + +
+
+ + + {!generatedWebhook ? ( + + ) : ( + + )} + + +
+
+
+
+ ); +}; diff --git a/web/components/web-hooks/empty-state.tsx b/web/components/web-hooks/empty-state.tsx index 234a4d0a2..015cf57bc 100644 --- a/web/components/web-hooks/empty-state.tsx +++ b/web/components/web-hooks/empty-state.tsx @@ -1,14 +1,16 @@ -// next -import { useRouter } from "next/router"; +import React from "react"; import Image from "next/image"; // ui import { Button } from "@plane/ui"; // assets import EmptyWebhook from "public/empty-state/web-hook.svg"; -export const WebhooksEmptyState = () => { - const router = useRouter(); +type Props = { + onClick: () => void; +}; +export const WebhooksEmptyState: React.FC = (props) => { + const { onClick } = props; return (
{

Create webhooks to receive real-time updates and automate actions

-
diff --git a/web/components/web-hooks/form/form.tsx b/web/components/web-hooks/form/form.tsx index 43600b04a..4d98c2b7f 100644 --- a/web/components/web-hooks/form/form.tsx +++ b/web/components/web-hooks/form/form.tsx @@ -1,11 +1,8 @@ import React, { FC, useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// hooks -import useToast from "hooks/use-toast"; // components import { WebhookIndividualEventOptions, @@ -13,17 +10,16 @@ import { WebhookOptions, WebhookSecretKey, WebhookToggle, - getCurrentHookAsCSV, } from "components/web-hooks"; // ui import { Button } from "@plane/ui"; -// helpers -import { csvDownload } from "helpers/download.helper"; // types import { IWebhook, TWebhookEventTypes } from "types"; type Props = { data?: Partial; + onSubmit: (data: IWebhook, webhookEventType: TWebhookEventTypes) => Promise; + handleClose?: () => void; }; const initialWebhookPayload: Partial = { @@ -36,18 +32,12 @@ const initialWebhookPayload: Partial = { }; export const WebhookForm: FC = observer((props) => { - const { data } = props; + const { data, onSubmit, handleClose } = props; // states const [webhookEventType, setWebhookEventType] = useState("all"); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - // toast - const { setToastAlert } = useToast(); // mobx store const { - webhook: { createWebhook, updateWebhook }, - workspace: { currentWorkspace }, + webhook: { webhookSecretKey }, } = useMobxStore(); // use form const { @@ -58,74 +48,8 @@ export const WebhookForm: FC = observer((props) => { defaultValues: { ...initialWebhookPayload, ...data }, }); - const handleCreateWebhook = async (formData: IWebhook) => { - if (!workspaceSlug) return; - - let payload: Partial = { - url: formData.url, - }; - - if (webhookEventType === "all") - payload = { - ...payload, - project: true, - cycle: true, - module: true, - issue: true, - issue_comment: true, - }; - else - payload = { - ...payload, - project: formData.project ?? false, - cycle: formData.cycle ?? false, - module: formData.module ?? false, - issue: formData.issue ?? false, - issue_comment: formData.issue_comment ?? false, - }; - - await createWebhook(workspaceSlug.toString(), payload) - .then(({ webHook, secretKey }) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Webhook created successfully.", - }); - - const csvData = getCurrentHookAsCSV(currentWorkspace, webHook, secretKey); - csvDownload(csvData, `webhook-secret-key-${Date.now()}`); - - if (webHook && webHook.id) - router.push({ pathname: `/${workspaceSlug}/settings/webhooks/${webHook.id}`, query: { isCreated: true } }); - }) - .catch((error) => { - setToastAlert({ - type: "error", - title: "Error!", - message: error?.error ?? "Something went wrong. Please try again.", - }); - }); - }; - - const handleUpdateWebhook = async (formData: IWebhook) => { - if (!workspaceSlug || !data || !data.id) return; - - const payload = { - url: formData?.url, - is_active: formData?.is_active, - project: formData?.project, - cycle: formData?.cycle, - module: formData?.module, - issue: formData?.issue, - issue_comment: formData?.issue_comment, - }; - - return await updateWebhook(workspaceSlug.toString(), data.id, payload); - }; - const handleFormSubmit = async (formData: IWebhook) => { - if (data) await handleUpdateWebhook(formData); - else await handleCreateWebhook(formData); + await onSubmit(formData, webhookEventType); }; useEffect(() => { @@ -161,12 +85,26 @@ export const WebhookForm: FC = observer((props) => {
{webhookEventType === "individual" && }
-
- {data && } - -
+ {data ? ( +
+ + + +
+ ) : ( +
+ + {!webhookSecretKey && ( + + )} +
+ )} ); diff --git a/web/components/web-hooks/form/secret-key.tsx b/web/components/web-hooks/form/secret-key.tsx index eecc7aa71..e3cc6e45f 100644 --- a/web/components/web-hooks/form/secret-key.tsx +++ b/web/components/web-hooks/form/secret-key.tsx @@ -56,11 +56,11 @@ export const WebhookSecretKey: FC = observer((props) => { }; const handleRegenerateSecretKey = () => { - if (!workspaceSlug || !webhookId) return; + if (!workspaceSlug || !data.id) return; setIsRegenerating(true); - regenerateSecretKey(workspaceSlug.toString(), webhookId.toString()) + regenerateSecretKey(workspaceSlug.toString(), data.id) .then(() => { setToastAlert({ type: "success", @@ -92,10 +92,10 @@ export const WebhookSecretKey: FC = observer((props) => { <> {(data || webhookSecretKey) && (
-
Secret key
+ {webhookId &&
Secret key
}
Generate a token to sign-in to the webhook payload
-
+
{shouldShowKey ? (

{webhookSecretKey}

diff --git a/web/components/web-hooks/generated-hook-details.tsx b/web/components/web-hooks/generated-hook-details.tsx new file mode 100644 index 000000000..60dfeda30 --- /dev/null +++ b/web/components/web-hooks/generated-hook-details.tsx @@ -0,0 +1,33 @@ +// components +import { WebhookSecretKey } from "./form"; +// ui +import { Button } from "@plane/ui"; +// types +import { IWebhook } from "types"; + +type Props = { + handleClose: () => void; + webhookDetails: IWebhook; +}; + +export const GeneratedHookDetails: React.FC = (props) => { + const { handleClose, webhookDetails } = props; + + return ( +
+
+

Key created

+

+ Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file + containing the key has been downloaded. +

+
+ +
+ +
+
+ ); +}; diff --git a/web/components/web-hooks/index.ts b/web/components/web-hooks/index.ts index 79fe4a520..fd23f4330 100644 --- a/web/components/web-hooks/index.ts +++ b/web/components/web-hooks/index.ts @@ -1,6 +1,8 @@ -export * from "./form"; export * from "./delete-webhook-modal"; export * from "./empty-state"; +export * from "./form"; +export * from "./generated-hook-details"; export * from "./utils"; +export * from "./create-webhook-modal"; export * from "./webhooks-list-item"; export * from "./webhooks-list"; diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx index b5e4ff64a..c8b5d2cce 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; @@ -7,6 +7,8 @@ import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; +// hooks +import useToast from "hooks/use-toast"; // components import { WorkspaceSettingHeader } from "components/headers"; import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; @@ -14,22 +16,21 @@ import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "component import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "types/app"; +import { IWebhook } from "types"; const WebhookDetailsPage: NextPageWithLayout = observer(() => { // states const [deleteWebhookModal, setDeleteWebhookModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, webhookId, isCreated } = router.query; + const { workspaceSlug, webhookId } = router.query; // mobx store const { - webhook: { currentWebhook, clearSecretKey, fetchWebhookById }, + webhook: { currentWebhook, fetchWebhookById, updateWebhook }, user: { currentWorkspaceRole }, } = useMobxStore(); - - useEffect(() => { - if (isCreated !== "true") clearSecretKey(); - }, [clearSecretKey, isCreated]); + // toast + const { setToastAlert } = useToast(); const isAdmin = currentWorkspaceRole === 20; @@ -40,6 +41,34 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { : null ); + const handleUpdateWebhook = async (formData: IWebhook) => { + if (!workspaceSlug || !formData || !formData.id) return; + const payload = { + url: formData?.url, + is_active: formData?.is_active, + project: formData?.project, + cycle: formData?.cycle, + module: formData?.module, + issue: formData?.issue, + issue_comment: formData?.issue_comment, + }; + await updateWebhook(workspaceSlug.toString(), formData.id, payload) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Webhook updated successfully.", + }); + }) + .catch((error) => { + setToastAlert({ + type: "error", + title: "Error!", + message: error?.error ?? "Something went wrong. Please try again.", + }); + }); + }; + if (!isAdmin) return (
@@ -58,7 +87,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { <> setDeleteWebhookModal(false)} />
- + await handleUpdateWebhook(data)} data={currentWebhook} /> {currentWebhook && setDeleteWebhookModal(true)} />}
diff --git a/web/pages/[workspaceSlug]/settings/webhooks/create.tsx b/web/pages/[workspaceSlug]/settings/webhooks/create.tsx deleted file mode 100644 index 6615d4c68..000000000 --- a/web/pages/[workspaceSlug]/settings/webhooks/create.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// layouts -import { AppLayout } from "layouts/app-layout"; -import { WorkspaceSettingLayout } from "layouts/settings-layout"; -// components -import { WorkspaceSettingHeader } from "components/headers"; -import { WebhookForm } from "components/web-hooks"; -// types -import { NextPageWithLayout } from "types/app"; - -const CreateWebhookPage: NextPageWithLayout = observer(() => { - const { - user: { currentWorkspaceRole }, - } = useMobxStore(); - - const isAdmin = currentWorkspaceRole === 20; - - if (!isAdmin) - return ( -
-

You are not authorized to access this page.

-
- ); - - return ( -
- -
- ); -}); - -CreateWebhookPage.getLayout = function getLayout(page: React.ReactElement) { - return ( - }> - {page} - - ); -}; - -export default CreateWebhookPage; diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index 0fe6e4e71..407c3ebab 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import Link from "next/link"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; @@ -10,18 +9,22 @@ import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components import { WorkspaceSettingHeader } from "components/headers"; -import { WebhooksList, WebhooksEmptyState } from "components/web-hooks"; +import { WebhooksList, WebhooksEmptyState, CreateWebhookModal } from "components/web-hooks"; // ui import { Button, Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "types/app"; const WebhooksListPage: NextPageWithLayout = observer(() => { + // states + const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false); + // router const router = useRouter(); const { workspaceSlug } = router.query; const { - webhook: { fetchWebhooks, webhooks }, + webhook: { fetchWebhooks, createWebhook, clearSecretKey, webhooks, webhookSecretKey }, + workspace: { currentWorkspace }, user: { currentWorkspaceRole }, } = useMobxStore(); @@ -32,6 +35,11 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null ); + // clear secret key when modal is closed. + useEffect(() => { + if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey(); + }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); + if (!isAdmin) return (
@@ -48,21 +56,28 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { return (
+ { + setShowCreateWebhookModal(false); + }} + /> {Object.keys(webhooks).length > 0 ? (
Webhooks
- - - +
) : (
- + setShowCreateWebhookModal(true)} />
)}