Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled

Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
chuan
2025-11-07 00:00:52 +08:00
commit 8ebde8aa05
4886 changed files with 462270 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
"use client";
import React, { useState } from "react";
import { useParams } from "next/navigation";
// types
import { WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// helpers
import { csvDownload } from "@plane/utils";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import useKeypress from "@/hooks/use-keypress";
// components
import { WebhookForm } from "./form";
import { GeneratedHookDetails } from "./generated-hook-details";
// utils
import { getCurrentHookAsCSV } from "./utils";
interface ICreateWebhookModal {
currentWorkspace: IWorkspace | null;
isOpen: boolean;
clearSecretKey: () => void;
createWebhook: (
workspaceSlug: string,
data: Partial<IWebhook>
) => Promise<{
webHook: IWebhook;
secretKey: string | null;
}>;
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 { workspaceSlug } = useParams();
const { t } = useTranslation();
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 }) => {
captureSuccess({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_created,
payload: {
webhook: formData?.url,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("workspace_settings.settings.webhooks.toasts.created.title"),
message: t("workspace_settings.settings.webhooks.toasts.created.message"),
});
setGeneratedKey(webHook);
const csvData = getCurrentHookAsCSV(currentWorkspace, webHook, secretKey ?? undefined);
csvDownload(csvData, `webhook-secret-key-${Date.now()}`);
})
.catch((error) => {
captureError({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_created,
payload: {
webhook: formData?.url,
},
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("workspace_settings.settings.webhooks.toasts.not_created.title"),
message: error?.error ?? t("workspace_settings.settings.webhooks.toasts.not_created.message"),
});
});
};
const handleClose = () => {
onClose();
setTimeout(() => {
clearSecretKey();
setGeneratedKey(null);
}, 350);
};
useKeypress("Escape", () => {
if (isOpen && !generatedWebhook) handleClose();
});
return (
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL} className="p-4 pb-0">
{!generatedWebhook ? (
<WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} />
) : (
<GeneratedHookDetails webhookDetails={generatedWebhook} handleClose={handleClose} />
)}
</ModalCore>
);
};

View File

@@ -0,0 +1,87 @@
"use client";
import type { FC } from "react";
import React, { useState } from "react";
import { useParams } from "next/navigation";
// ui
import { WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { AlertModalCore } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useWebhook } from "@/hooks/store/use-webhook";
import { useAppRouter } from "@/hooks/use-app-router";
interface IDeleteWebhook {
isOpen: boolean;
onClose: () => void;
}
export const DeleteWebhookModal: FC<IDeleteWebhook> = (props) => {
const { isOpen, onClose } = props;
// states
const [isDeleting, setIsDeleting] = useState(false);
// router
const router = useAppRouter();
// store hooks
const { removeWebhook } = useWebhook();
const { workspaceSlug, webhookId } = useParams();
const handleClose = () => {
onClose();
};
const handleDelete = async () => {
if (!workspaceSlug || !webhookId) return;
setIsDeleting(true);
removeWebhook(workspaceSlug.toString(), webhookId.toString())
.then(() => {
captureSuccess({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_deleted,
payload: {
webhook: webhookId,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Webhook deleted successfully.",
});
router.replace(`/${workspaceSlug}/settings/webhooks/`);
})
.catch((error) => {
captureError({
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_deleted,
payload: {
webhook: webhookId,
},
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: error?.error ?? "Something went wrong. Please try again.",
});
})
.finally(() => setIsDeleting(false));
};
return (
<AlertModalCore
handleClose={handleClose}
handleSubmit={handleDelete}
isSubmitting={isDeleting}
isOpen={isOpen}
title="Delete webhook"
content={
<>
Are you sure you want to delete this webhook? Future events will not be delivered to this webhook. This action
cannot be undone.
</>
}
/>
);
};

View File

@@ -0,0 +1,32 @@
"use client";
import React from "react";
import Image from "next/image";
// ui
import { Button } from "@plane/propel/button";
// assets
import EmptyWebhook from "@/app/assets/empty-state/web-hook.svg?url";
type Props = {
onClick: () => void;
};
export const WebhooksEmptyState: React.FC<Props> = (props) => {
const { onClick } = props;
return (
<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`}
>
<div className="flex w-full flex-col items-center text-center">
<Image src={EmptyWebhook} className="w-52 sm:w-60" alt="empty" />
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">No webhooks</h6>
<p className="mb-7 text-custom-text-300 sm:mb-8">
Create webhooks to receive real-time updates and automate actions
</p>
<Button className="flex items-center gap-1.5" onClick={onClick}>
Add webhook
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,55 @@
"use client";
import { Disclosure, Transition } from "@headlessui/react";
import { WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons";
type Props = {
openDeleteModal: () => void;
};
export const WebhookDeleteSection: React.FC<Props> = (props) => {
const { openDeleteModal } = props;
return (
<Disclosure as="div" className="border-t border-custom-border-200">
{({ open }) => (
<div className="w-full">
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
<span className="text-lg tracking-tight">Danger zone</span>
{open ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />}
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
Once a webhook is deleted, it cannot be restored. Future events will no longer be delivered to this
webhook.
</span>
<div>
<Button
variant="danger"
onClick={openDeleteModal}
data-ph-element={WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_DELETE_BUTTON}
>
Delete webhook
</Button>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
);
};

View File

@@ -0,0 +1,46 @@
// types
import { useTranslation } from "@plane/i18n";
import type { TWebhookEventTypes } from "@plane/types";
type Props = {
value: string;
onChange: (value: TWebhookEventTypes) => void;
};
const WEBHOOK_EVENT_TYPES: { key: TWebhookEventTypes; i18n_label: string }[] = [
{
key: "all",
i18n_label: "workspace_settings.settings.webhooks.options.all",
},
{
key: "individual",
i18n_label: "workspace_settings.settings.webhooks.options.individual",
},
];
export const WebhookOptions: React.FC<Props> = (props) => {
const { value, onChange } = props;
const { t } = useTranslation();
return (
<>
<h6 className="text-sm font-medium">{t("workspace_settings.settings.webhooks.modal.question")}</h6>
<div className="space-y-3">
{WEBHOOK_EVENT_TYPES.map((option) => (
<div key={option.key} className="flex items-center gap-2">
<input
id={option.key}
type="radio"
value={option.key}
checked={value == option.key}
onChange={() => onChange(option.key)}
/>
<label className="text-sm" htmlFor={option.key}>
{t(option.i18n_label)}
</label>
</div>
))}
</div>
</>
);
};

View File

@@ -0,0 +1,120 @@
"use client";
import type { FC } from "react";
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import type { IWebhook, TWebhookEventTypes } from "@plane/types";
// hooks
import {
WebhookIndividualEventOptions,
WebhookInput,
WebhookOptions,
WebhookSecretKey,
WebhookToggle,
} from "@/components/web-hooks";
import { useWebhook } from "@/hooks/store/use-webhook";
// components
// ui
// types
type Props = {
data?: Partial<IWebhook>;
onSubmit: (data: IWebhook, webhookEventType: TWebhookEventTypes) => Promise<void>;
handleClose?: () => void;
};
const initialWebhookPayload: Partial<IWebhook> = {
cycle: true,
issue: true,
issue_comment: true,
module: true,
project: true,
url: "",
};
export const WebhookForm: FC<Props> = observer((props) => {
const { data, onSubmit, handleClose } = props;
// states
const [webhookEventType, setWebhookEventType] = useState<TWebhookEventTypes>("all");
// store hooks
const { webhookSecretKey } = useWebhook();
const { t } = useTranslation();
// use form
const {
handleSubmit,
control,
formState: { isSubmitting, errors },
} = useForm<IWebhook>({
defaultValues: { ...initialWebhookPayload, ...data },
});
const handleFormSubmit = async (formData: IWebhook) => {
await onSubmit(formData, webhookEventType);
};
useEffect(() => {
if (!data) return;
if (data.project && data.cycle && data.module && data.issue && data.issue_comment) setWebhookEventType("all");
else setWebhookEventType("individual");
}, [data]);
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-5 ">
<div className="text-xl font-medium text-custom-text-200">
{data
? t("workspace_settings.settings.webhooks.modal.details")
: t("workspace_settings.settings.webhooks.modal.title")}
</div>
<div className="space-y-3">
<div className="space-y-1">
<Controller
control={control}
name="url"
rules={{
required: t("workspace_settings.settings.webhooks.modal.error"),
}}
render={({ field: { onChange, value } }) => (
<WebhookInput value={value} onChange={onChange} hasError={Boolean(errors.url)} />
)}
/>
{errors.url && <div className="text-xs text-red-500">{errors.url.message}</div>}
</div>
{data && <WebhookToggle control={control} />}
<WebhookOptions value={webhookEventType} onChange={(val) => setWebhookEventType(val)} />
</div>
<div className="mt-4">
{webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />}
</div>
</div>
{data ? (
<div className="pt-0 space-y-5">
<WebhookSecretKey data={data} />
<Button
type="submit"
loading={isSubmitting}
data-ph-element={WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_UPDATE_BUTTON}
>
{isSubmitting ? t("updating") : t("update")}
</Button>
</div>
) : (
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
{t("cancel")}
</Button>
{!webhookSecretKey && (
<Button type="submit" variant="primary" size="sm" loading={isSubmitting} className="capitalize">
{isSubmitting ? t("common.creating") : t("common.create")}
</Button>
)}
</div>
)}
</form>
);
});

View File

@@ -0,0 +1,7 @@
export * from "./delete-section";
export * from "./event-types";
export * from "./form";
export * from "./individual-event-options";
export * from "./input";
export * from "./secret-key";
export * from "./toggle";

View File

@@ -0,0 +1,68 @@
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
import type { IWebhook } from "@plane/types";
export const INDIVIDUAL_WEBHOOK_OPTIONS: {
key: keyof IWebhook;
label: string;
description: string;
}[] = [
{
key: "project",
label: "Projects",
description: "Project created, updated, or deleted",
},
{
key: "cycle",
label: "Cycles",
description: "Cycle created, updated, or deleted",
},
{
key: "issue",
label: "Work items",
description: "Work item created, updated, deleted, added to a cycle or module",
},
{
key: "module",
label: "Modules",
description: "Module created, updated, or deleted",
},
{
key: "issue_comment",
label: "Work item comments",
description: "Comment posted, updated, or deleted",
},
];
type Props = {
control: Control<IWebhook, any>;
};
export const WebhookIndividualEventOptions = ({ control }: Props) => (
<div className="grid grid-cols-1 gap-x-4 gap-y-8 px-6 lg:grid-cols-2">
{INDIVIDUAL_WEBHOOK_OPTIONS.map((option) => (
<Controller
key={option.key}
control={control}
name={option.key}
render={({ field: { onChange, value } }) => (
<div>
<div className="flex items-center gap-2">
<input
id={option.key}
onChange={() => onChange(!value)}
type="checkbox"
name="selectIndividualEvents"
checked={value === true}
/>
<label className="text-sm" htmlFor={option.key}>
{option.label}
</label>
</div>
<p className="ml-6 mt-0.5 text-xs text-custom-text-300">{option.description}</p>
</div>
)}
/>
))}
</div>
);

View File

@@ -0,0 +1,30 @@
"use client";
import { useTranslation } from "@plane/i18n";
import { Input } from "@plane/ui";
type Props = {
value: string;
onChange: (value: string) => void;
hasError: boolean;
};
export const WebhookInput: React.FC<Props> = (props) => {
const { value, onChange, hasError } = props;
const { t } = useTranslation();
return (
<>
<h6 className="text-sm font-medium">{t("workspace_settings.settings.webhooks.modal.payload")}</h6>
<Input
type="url"
className="h-11 w-full"
onChange={(e) => onChange(e.target.value)}
value={value}
autoComplete="off"
hasError={hasError}
placeholder="https://example.com/post"
autoFocus
/>
</>
);
};

View File

@@ -0,0 +1,145 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { range } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import type { IWebhook } from "@plane/types";
// ui
import { csvDownload, copyTextToClipboard } from "@plane/utils";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace";
// types
import { usePlatformOS } from "@/hooks/use-platform-os";
// utils
import { getCurrentHookAsCSV } from "../utils";
// hooks
type Props = {
data: Partial<IWebhook>;
};
export const WebhookSecretKey: FC<Props> = observer((props) => {
const { data } = props;
// states
const [isRegenerating, setIsRegenerating] = useState(false);
const [shouldShowKey, setShouldShowKey] = useState(false);
// router
const { workspaceSlug, webhookId } = useParams();
// store hooks
const { currentWorkspace } = useWorkspace();
const { currentWebhook, regenerateSecretKey, webhookSecretKey } = useWebhook();
const { isMobile } = usePlatformOS();
const { t } = useTranslation();
const handleCopySecretKey = () => {
if (!webhookSecretKey) return;
copyTextToClipboard(webhookSecretKey)
.then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: `${t("success")!}`,
message: t("workspace_settings.settings.webhooks.toasts.secret_key_copied.message"),
})
)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: `${t("error")}!`,
message: t("workspace_settings.settings.webhooks.toasts.secret_key_not_copied.message"),
})
);
};
const handleRegenerateSecretKey = () => {
if (!workspaceSlug || !data.id) return;
setIsRegenerating(true);
regenerateSecretKey(workspaceSlug.toString(), data.id)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: `${t("success")!}`,
message: "New key regenerated successfully.",
});
if (currentWebhook && webhookSecretKey) {
const csvData = getCurrentHookAsCSV(currentWorkspace, currentWebhook, webhookSecretKey);
csvDownload(csvData, `webhook-secret-key-${Date.now()}`);
}
})
.catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: `${t("error")}!`,
message: err?.error ?? t("something_went_wrong_please_try_again"),
})
)
.finally(() => setIsRegenerating(false));
};
const toggleShowKey = () => setShouldShowKey((prevState) => !prevState);
const SECRET_KEY_OPTIONS = [
{ label: "View secret key", Icon: shouldShowKey ? EyeOff : Eye, onClick: toggleShowKey, key: "eye" },
{ label: "Copy secret key", Icon: Copy, onClick: handleCopySecretKey, key: "copy" },
];
return (
<>
{(data || webhookSecretKey) && (
<div className="space-y-2">
{webhookId && (
<div className="text-sm font-medium">{t("workspace_settings.settings.webhooks.secret_key.title")}</div>
)}
<div className="text-xs text-custom-text-400">
{t("workspace_settings.settings.webhooks.secret_key.message")}
</div>
<div className="flex flex-col md:flex-row md:items-center gap-4">
<div className="flex flex-grow max-w-lg items-center justify-between self-stretch rounded border border-custom-border-200 px-2 h-8">
<div className="select-none overflow-hidden font-medium">
{shouldShowKey ? (
<p className="text-xs">{webhookSecretKey}</p>
) : (
<div className="flex items-center gap-1.5 overflow-hidden mr-2">
{range(30).map((index) => (
<div key={index} className="h-1 w-1 rounded-full bg-custom-text-400 flex-shrink-0" />
))}
</div>
)}
</div>
{webhookSecretKey && (
<div className="flex items-center gap-2">
{SECRET_KEY_OPTIONS.map((option) => (
<Tooltip key={option.key} tooltipContent={option.label} isMobile={isMobile}>
<button type="button" className="grid flex-shrink-0 place-items-center" onClick={option.onClick}>
<option.Icon className="h-3 w-3 text-custom-text-400" />
</button>
</Tooltip>
))}
</div>
)}
</div>
{data && (
<div>
<Button onClick={handleRegenerateSecretKey} variant="accent-primary" loading={isRegenerating}>
<RefreshCw className="h-3 w-3" />
{isRegenerating ? `${t("re_generating")}...` : t("re_generate_key")}
</Button>
</div>
)}
</div>
</div>
)}
</>
);
});

View File

@@ -0,0 +1,37 @@
"use client";
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
// constants
import { WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import type { IWebhook } from "@plane/types";
// ui
import { ToggleSwitch } from "@plane/ui";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
interface IWebHookToggle {
control: Control<IWebhook, any>;
}
export const WebhookToggle = ({ control }: IWebHookToggle) => (
<div className="flex gap-6">
<div className="text-sm font-medium">Enable webhook</div>
<Controller
control={control}
name="is_active"
render={({ field: { onChange, value } }) => (
<ToggleSwitch
value={value}
onChange={(val: boolean) => {
captureClick({
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_DETAILS_PAGE_TOGGLE_SWITCH,
});
onChange(val);
}}
size="sm"
/>
)}
/>
</div>
);

View File

@@ -0,0 +1,36 @@
"use client";
// components
// ui
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import type { IWebhook } from "@plane/types";
// types
import { WebhookSecretKey } from "./form";
type Props = {
handleClose: () => void;
webhookDetails: IWebhook;
};
export const GeneratedHookDetails: React.FC<Props> = (props) => {
const { handleClose, webhookDetails } = props;
const { t } = useTranslation();
return (
<>
<div className="space-y-5 p-5">
<div className="space-y-3">
<h3 className="text-xl font-medium text-custom-text-200">{t("workspace_settings.key_created")}</h3>
<p className="text-sm text-custom-text-400">{t("workspace_settings.copy_key")}</p>
</div>
<WebhookSecretKey data={webhookDetails} />
</div>
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Close
</Button>
</div>
</>
);
};

View File

@@ -0,0 +1,8 @@
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";

View File

@@ -0,0 +1,23 @@
// helpers
import type { IWebhook, IWorkspace } from "@plane/types";
import { renderFormattedPayloadDate } from "@plane/utils";
// types
export const getCurrentHookAsCSV = (
currentWorkspace: IWorkspace | null,
webhook: IWebhook | undefined,
secretKey: string | undefined
) => ({
id: webhook?.id || "",
url: webhook?.url || "",
created_at: renderFormattedPayloadDate(webhook?.created_at || "") ?? "",
updated_at: renderFormattedPayloadDate(webhook?.updated_at || "") ?? "",
is_active: webhook?.is_active?.toString() || "",
secret_key: secretKey || "",
project: webhook?.project?.toString() || "",
issue: webhook?.issue?.toString() || "",
module: webhook?.module?.toString() || "",
cycle: webhook?.cycle?.toString() || "",
issue_comment: webhook?.issue_comment?.toString() || "",
workspace: currentWorkspace?.name || "",
});

View File

@@ -0,0 +1,69 @@
"use client";
import type { FC } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { WORKSPACE_SETTINGS_TRACKER_ELEMENTS, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import type { IWebhook } from "@plane/types";
// hooks
import { ToggleSwitch } from "@plane/ui";
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useWebhook } from "@/hooks/store/use-webhook";
// ui
// types
interface IWebhookListItem {
webhook: IWebhook;
}
export const WebhooksListItem: FC<IWebhookListItem> = (props) => {
const { webhook } = props;
// router
const { workspaceSlug } = useParams();
// store hooks
const { updateWebhook } = useWebhook();
const handleToggle = () => {
if (!workspaceSlug || !webhook.id) return;
updateWebhook(workspaceSlug.toString(), webhook.id, { is_active: !webhook.is_active })
.then(() => {
captureElementAndEvent({
element: {
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_LIST_ITEM_TOGGLE_SWITCH,
},
event: {
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_toggled,
state: "SUCCESS",
payload: {
webhook: webhook.url,
},
},
});
})
.catch(() => {
captureElementAndEvent({
element: {
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_LIST_ITEM_TOGGLE_SWITCH,
},
event: {
eventName: WORKSPACE_SETTINGS_TRACKER_EVENTS.webhook_toggled,
state: "ERROR",
payload: {
webhook: webhook.url,
},
},
});
});
};
return (
<div className="border-b border-custom-border-200">
<Link href={`/${workspaceSlug}/settings/webhooks/${webhook?.id}`}>
<span className="flex items-center justify-between gap-4 py-[18px]">
<h5 className="truncate text-base font-medium">{webhook.url}</h5>
<ToggleSwitch value={webhook.is_active} onChange={handleToggle} />
</span>
</Link>
</div>
);
};

View File

@@ -0,0 +1,18 @@
import { observer } from "mobx-react";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
// components
import { WebhooksListItem } from "./webhooks-list-item";
export const WebhooksList = observer(() => {
// store hooks
const { webhooks } = useWebhook();
return (
<div className="h-full w-full overflow-y-auto">
{Object.values(webhooks ?? {}).map((webhook) => (
<WebhooksListItem key={webhook.id} webhook={webhook} />
))}
</div>
);
});