forked from github/plane
Merge pull request #3249 from makeplane/develop
promote: moving stable changes from develop to preview
This commit is contained in:
commit
ad20079e0c
139
web/components/web-hooks/create-webhook-modal.tsx
Normal file
139
web/components/web-hooks/create-webhook-modal.tsx
Normal file
@ -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<IWebhook>) => Promise<WebhookWithKey>;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateWebhookModal: React.FC<ICreateWebhookModal> = (props) => {
|
||||||
|
const { isOpen, onClose, currentWorkspace, createWebhook, clearSecretKey } = props;
|
||||||
|
// states
|
||||||
|
const [generatedWebhook, setGeneratedKey] = useState<IWebhook | null>(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<IWebhook> = {
|
||||||
|
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 (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="relative z-20"
|
||||||
|
onClose={() => {
|
||||||
|
if (!generatedWebhook) handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 p-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||||
|
{!generatedWebhook ? (
|
||||||
|
<WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} />
|
||||||
|
) : (
|
||||||
|
<GeneratedHookDetails webhookDetails={generatedWebhook} handleClose={handleClose} />
|
||||||
|
)}
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
@ -1,14 +1,16 @@
|
|||||||
// next
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// assets
|
// assets
|
||||||
import EmptyWebhook from "public/empty-state/web-hook.svg";
|
import EmptyWebhook from "public/empty-state/web-hook.svg";
|
||||||
|
|
||||||
export const WebhooksEmptyState = () => {
|
type Props = {
|
||||||
const router = useRouter();
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebhooksEmptyState: React.FC<Props> = (props) => {
|
||||||
|
const { onClick } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`mx-auto flex w-full items-center justify-center rounded-sm border border-custom-border-200 bg-custom-background-90 px-16 py-10 lg:w-3/4`}
|
className={`mx-auto flex w-full items-center justify-center rounded-sm border border-custom-border-200 bg-custom-background-90 px-16 py-10 lg:w-3/4`}
|
||||||
@ -19,7 +21,7 @@ export const WebhooksEmptyState = () => {
|
|||||||
<p className="mb-7 text-custom-text-300 sm:mb-8">
|
<p className="mb-7 text-custom-text-300 sm:mb-8">
|
||||||
Create webhooks to receive real-time updates and automate actions
|
Create webhooks to receive real-time updates and automate actions
|
||||||
</p>
|
</p>
|
||||||
<Button className="flex items-center gap-1.5" onClick={() => router.push(`${router.asPath}/create/`)}>
|
<Button className="flex items-center gap-1.5" onClick={onClick}>
|
||||||
Add webhook
|
Add webhook
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import React, { FC, useEffect, useState } from "react";
|
import React, { FC, useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
WebhookIndividualEventOptions,
|
WebhookIndividualEventOptions,
|
||||||
@ -13,17 +10,16 @@ import {
|
|||||||
WebhookOptions,
|
WebhookOptions,
|
||||||
WebhookSecretKey,
|
WebhookSecretKey,
|
||||||
WebhookToggle,
|
WebhookToggle,
|
||||||
getCurrentHookAsCSV,
|
|
||||||
} from "components/web-hooks";
|
} from "components/web-hooks";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// helpers
|
|
||||||
import { csvDownload } from "helpers/download.helper";
|
|
||||||
// types
|
// types
|
||||||
import { IWebhook, TWebhookEventTypes } from "types";
|
import { IWebhook, TWebhookEventTypes } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data?: Partial<IWebhook>;
|
data?: Partial<IWebhook>;
|
||||||
|
onSubmit: (data: IWebhook, webhookEventType: TWebhookEventTypes) => Promise<void>;
|
||||||
|
handleClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialWebhookPayload: Partial<IWebhook> = {
|
const initialWebhookPayload: Partial<IWebhook> = {
|
||||||
@ -36,18 +32,12 @@ const initialWebhookPayload: Partial<IWebhook> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const WebhookForm: FC<Props> = observer((props) => {
|
export const WebhookForm: FC<Props> = observer((props) => {
|
||||||
const { data } = props;
|
const { data, onSubmit, handleClose } = props;
|
||||||
// states
|
// states
|
||||||
const [webhookEventType, setWebhookEventType] = useState<TWebhookEventTypes>("all");
|
const [webhookEventType, setWebhookEventType] = useState<TWebhookEventTypes>("all");
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
// toast
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
// mobx store
|
// mobx store
|
||||||
const {
|
const {
|
||||||
webhook: { createWebhook, updateWebhook },
|
webhook: { webhookSecretKey },
|
||||||
workspace: { currentWorkspace },
|
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// use form
|
// use form
|
||||||
const {
|
const {
|
||||||
@ -58,74 +48,8 @@ export const WebhookForm: FC<Props> = observer((props) => {
|
|||||||
defaultValues: { ...initialWebhookPayload, ...data },
|
defaultValues: { ...initialWebhookPayload, ...data },
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateWebhook = async (formData: IWebhook) => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
let payload: Partial<IWebhook> = {
|
|
||||||
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) => {
|
const handleFormSubmit = async (formData: IWebhook) => {
|
||||||
if (data) await handleUpdateWebhook(formData);
|
await onSubmit(formData, webhookEventType);
|
||||||
else await handleCreateWebhook(formData);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -161,12 +85,26 @@ export const WebhookForm: FC<Props> = observer((props) => {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />}
|
{webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />}
|
||||||
</div>
|
</div>
|
||||||
|
{data ? (
|
||||||
<div className="mt-8 space-y-8">
|
<div className="mt-8 space-y-8">
|
||||||
{data && <WebhookSecretKey data={data} />}
|
<WebhookSecretKey data={data} />
|
||||||
|
|
||||||
<Button type="submit" loading={isSubmitting}>
|
<Button type="submit" loading={isSubmitting}>
|
||||||
{data ? (isSubmitting ? "Updating..." : "Update") : isSubmitting ? "Creating..." : "Create"}
|
{isSubmitting ? "Updating..." : "Update"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
|
<Button variant="neutral-primary" onClick={handleClose}>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
{!webhookSecretKey && (
|
||||||
|
<Button type="submit" variant="primary" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Creating..." : "Create"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -56,11 +56,11 @@ export const WebhookSecretKey: FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRegenerateSecretKey = () => {
|
const handleRegenerateSecretKey = () => {
|
||||||
if (!workspaceSlug || !webhookId) return;
|
if (!workspaceSlug || !data.id) return;
|
||||||
|
|
||||||
setIsRegenerating(true);
|
setIsRegenerating(true);
|
||||||
|
|
||||||
regenerateSecretKey(workspaceSlug.toString(), webhookId.toString())
|
regenerateSecretKey(workspaceSlug.toString(), data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -92,10 +92,10 @@ export const WebhookSecretKey: FC<Props> = observer((props) => {
|
|||||||
<>
|
<>
|
||||||
{(data || webhookSecretKey) && (
|
{(data || webhookSecretKey) && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium">Secret key</div>
|
{webhookId && <div className="text-sm font-medium">Secret key</div>}
|
||||||
<div className="text-xs text-custom-text-400">Generate a token to sign-in to the webhook payload</div>
|
<div className="text-xs text-custom-text-400">Generate a token to sign-in to the webhook payload</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex min-w-[30rem] max-w-lg items-center justify-between self-stretch rounded border border-custom-border-200 px-2 py-1.5">
|
<div className="flex flex-grow max-w-lg items-center justify-between self-stretch rounded border border-custom-border-200 px-2 py-1.5">
|
||||||
<div className="select-none overflow-hidden font-medium">
|
<div className="select-none overflow-hidden font-medium">
|
||||||
{shouldShowKey ? (
|
{shouldShowKey ? (
|
||||||
<p className="text-xs">{webhookSecretKey}</p>
|
<p className="text-xs">{webhookSecretKey}</p>
|
||||||
|
33
web/components/web-hooks/generated-hook-details.tsx
Normal file
33
web/components/web-hooks/generated-hook-details.tsx
Normal file
@ -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> = (props) => {
|
||||||
|
const { handleClose, webhookDetails } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="space-y-3 mb-3">
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3>
|
||||||
|
<p className="text-sm text-custom-text-400">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<WebhookSecretKey data={webhookDetails} />
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,8 @@
|
|||||||
export * from "./form";
|
|
||||||
export * from "./delete-webhook-modal";
|
export * from "./delete-webhook-modal";
|
||||||
export * from "./empty-state";
|
export * from "./empty-state";
|
||||||
|
export * from "./form";
|
||||||
|
export * from "./generated-hook-details";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
|
export * from "./create-webhook-modal";
|
||||||
export * from "./webhooks-list-item";
|
export * from "./webhooks-list-item";
|
||||||
export * from "./webhooks-list";
|
export * from "./webhooks-list";
|
||||||
|
@ -60,6 +60,7 @@ const calculateShades = (hexValue: string): TShades => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const applyTheme = (palette: string, isDarkPalette: boolean) => {
|
export const applyTheme = (palette: string, isDarkPalette: boolean) => {
|
||||||
|
if (!palette) return;
|
||||||
const dom = document?.querySelector<HTMLElement>("[data-theme='custom']");
|
const dom = document?.querySelector<HTMLElement>("[data-theme='custom']");
|
||||||
// palette: [bg, text, primary, sidebarBg, sidebarText]
|
// palette: [bg, text, primary, sidebarBg, sidebarText]
|
||||||
const values: string[] = palette.split(",");
|
const values: string[] = palette.split(",");
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -7,6 +7,8 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks";
|
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks";
|
||||||
@ -14,22 +16,21 @@ import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "component
|
|||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
|
import { IWebhook } from "types";
|
||||||
|
|
||||||
const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
||||||
// states
|
// states
|
||||||
const [deleteWebhookModal, setDeleteWebhookModal] = useState(false);
|
const [deleteWebhookModal, setDeleteWebhookModal] = useState(false);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, webhookId, isCreated } = router.query;
|
const { workspaceSlug, webhookId } = router.query;
|
||||||
// mobx store
|
// mobx store
|
||||||
const {
|
const {
|
||||||
webhook: { currentWebhook, clearSecretKey, fetchWebhookById },
|
webhook: { currentWebhook, fetchWebhookById, updateWebhook },
|
||||||
user: { currentWorkspaceRole },
|
user: { currentWorkspaceRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
// toast
|
||||||
useEffect(() => {
|
const { setToastAlert } = useToast();
|
||||||
if (isCreated !== "true") clearSecretKey();
|
|
||||||
}, [clearSecretKey, isCreated]);
|
|
||||||
|
|
||||||
const isAdmin = currentWorkspaceRole === 20;
|
const isAdmin = currentWorkspaceRole === 20;
|
||||||
|
|
||||||
@ -40,6 +41,34 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
: null
|
: 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)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
@ -58,7 +87,7 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
||||||
<div className="w-full space-y-8 overflow-y-auto py-8 pr-9">
|
<div className="w-full space-y-8 overflow-y-auto py-8 pr-9">
|
||||||
<WebhookForm data={currentWebhook} />
|
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
|
||||||
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
|
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -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 (
|
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
|
||||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full overflow-y-auto py-8 pl-1 pr-9">
|
|
||||||
<WebhookForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CreateWebhookPage.getLayout = function getLayout(page: React.ReactElement) {
|
|
||||||
return (
|
|
||||||
<AppLayout header={<WorkspaceSettingHeader title="Webhook settings" />}>
|
|
||||||
<WorkspaceSettingLayout>{page}</WorkspaceSettingLayout>
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateWebhookPage;
|
|
@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
@ -10,18 +9,22 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
import { WorkspaceSettingHeader } from "components/headers";
|
import { WorkspaceSettingHeader } from "components/headers";
|
||||||
import { WebhooksList, WebhooksEmptyState } from "components/web-hooks";
|
import { WebhooksList, WebhooksEmptyState, CreateWebhookModal } from "components/web-hooks";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Spinner } from "@plane/ui";
|
import { Button, Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
|
|
||||||
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||||
|
// states
|
||||||
|
const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false);
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
webhook: { fetchWebhooks, webhooks },
|
webhook: { fetchWebhooks, createWebhook, clearSecretKey, webhooks, webhookSecretKey },
|
||||||
|
workspace: { currentWorkspace },
|
||||||
user: { currentWorkspaceRole },
|
user: { currentWorkspaceRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
@ -32,6 +35,11 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
|||||||
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// clear secret key when modal is closed.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
|
||||||
|
}, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]);
|
||||||
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex h-full w-full justify-center p-4">
|
<div className="mt-10 flex h-full w-full justify-center p-4">
|
||||||
@ -48,21 +56,28 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
<div className="h-full w-full overflow-hidden py-8 pr-9">
|
||||||
|
<CreateWebhookModal
|
||||||
|
createWebhook={createWebhook}
|
||||||
|
clearSecretKey={clearSecretKey}
|
||||||
|
currentWorkspace={currentWorkspace}
|
||||||
|
isOpen={showCreateWebhookModal}
|
||||||
|
onClose={() => {
|
||||||
|
setShowCreateWebhookModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{Object.keys(webhooks).length > 0 ? (
|
{Object.keys(webhooks).length > 0 ? (
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||||
<div className="text-xl font-medium">Webhooks</div>
|
<div className="text-xl font-medium">Webhooks</div>
|
||||||
<Link href={`/${workspaceSlug}/settings/webhooks/create`}>
|
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||||
<Button variant="primary" size="sm">
|
|
||||||
Add webhook
|
Add webhook
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<WebhooksList />
|
<WebhooksList />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<WebhooksEmptyState />
|
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user