snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { useParams } from "next/navigation";
// types
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 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 function CreateWebhookModal(props: ICreateWebhookModal) {
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 }) => {
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) => {
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,72 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { useParams } from "next/navigation";
// ui
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { AlertModalCore } from "@plane/ui";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
import { useAppRouter } from "@/hooks/use-app-router";
interface IDeleteWebhook {
isOpen: boolean;
onClose: () => void;
}
export function DeleteWebhookModal(props: IDeleteWebhook) {
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);
try {
await removeWebhook(workspaceSlug.toString(), webhookId.toString());
router.replace(`/${workspaceSlug}/settings/webhooks/`);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Webhook deleted successfully.",
});
} catch (_error) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Webhook could not be deleted. Please try again.",
});
}
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,33 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React from "react";
// ui
import { Button } from "@plane/propel/button";
// assets
import EmptyWebhook from "@/app/assets/empty-state/web-hook.svg?url";
type Props = {
onClick: () => void;
};
export function WebhooksEmptyState(props: Props) {
const { onClick } = props;
return (
<div
className={`mx-auto flex w-full items-center justify-center rounded-xs border border-subtle bg-surface-2 px-16 py-10 lg:w-3/4`}
>
<div className="flex w-full flex-col items-center text-center">
<img src={EmptyWebhook} className="w-52 object-cover sm:w-60" alt="empty" />
<h6 className="mt-6 mb-3 text-18 font-semibold sm:mt-8">No webhooks</h6>
<p className="mb-7 text-tertiary 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 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Disclosure, Transition } from "@headlessui/react";
import { Button } from "@plane/propel/button";
import { ChevronDownOutline, ChevronUpOutline } from "@makeplane/propel/icons";
type Props = {
openDeleteModal: () => void;
};
export function WebhookDeleteSection(props: Props) {
const { openDeleteModal } = props;
return (
<Disclosure as="div" className="border-t border-subtle">
{({ open }) => (
<div className="w-full">
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
<span className="text-16 tracking-tight">Danger zone</span>
{open ? <ChevronUpOutline className="h-5 w-5" /> : <ChevronDownOutline className="h-5 w-5" />}
</Disclosure.Button>
<Transition
as="div"
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-13 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="error-fill" size="lg" onClick={openDeleteModal}>
Delete webhook
</Button>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
);
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// 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 function WebhookOptions(props: Props) {
const { value, onChange } = props;
const { t } = useTranslation();
return (
<>
<h6 className="text-13 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-13" htmlFor={option.key}>
{t(option.i18n_label)}
</label>
</div>
))}
</div>
</>
);
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
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 = observer(function WebhookForm(props: 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-18 font-medium text-secondary">
{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-11 text-danger-primary">{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="space-y-5 pt-0">
<WebhookSecretKey data={data} />
<Button size="lg" type="submit" loading={isSubmitting}>
{isSubmitting ? t("updating") : t("update")}
</Button>
</div>
) : (
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4">
<Button variant="secondary" size="lg" onClick={handleClose}>
{t("cancel")}
</Button>
{!webhookSecretKey && (
<Button type="submit" variant="primary" size="lg" loading={isSubmitting} className="capitalize">
{isSubmitting ? t("common.creating") : t("common.create")}
</Button>
)}
</div>
)}
</form>
);
});

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
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,71 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
import { CheckboxField } from "@makeplane/propel/components/checkbox-field";
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 function WebhookIndividualEventOptions({ control }: Props) {
return (
<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 } }) => (
<CheckboxField
name={option.key}
label={option.label}
size="lg"
description={option.description}
stretch="full"
checked={value === true}
onCheckedChange={onChange}
/>
)}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Field } from "@makeplane/propel/components/field";
import { Input, InputGroup } from "@makeplane/propel/components/input";
import { useTranslation } from "@plane/i18n";
type Props = {
value: string;
onChange: (value: string) => void;
hasError: boolean;
};
export function WebhookInput(props: Props) {
const { value, onChange, hasError } = props;
const { t } = useTranslation();
return (
<>
<h6 className="text-13 font-medium">{t("workspace_settings.settings.webhooks.modal.payload")}</h6>
<Field name="payload" invalid={hasError}>
<InputGroup size="2xl">
<Input
size="2xl"
type="url"
onChange={(e) => onChange(e.target.value)}
value={value}
autoComplete="off"
placeholder="https://example.com/post"
autoFocus
aria-label={t("workspace_settings.settings.webhooks.modal.payload")}
/>
</InputGroup>
</Field>
</>
);
}

View File

@@ -0,0 +1,151 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { range } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { CopyOutline, HideOutline, RefreshOutline, ShowOutline } from "@makeplane/propel/icons";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@makeplane/propel/components/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 = observer(function WebhookSecretKey(props: 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 ? HideOutline : ShowOutline, onClick: toggleShowKey, key: "eye" },
{ label: "Copy secret key", Icon: CopyOutline, onClick: handleCopySecretKey, key: "copy" },
];
return (
<>
{(data || webhookSecretKey) && (
<div className="space-y-2">
{webhookId && (
<div className="text-13 font-medium">{t("workspace_settings.settings.webhooks.secret_key.title")}</div>
)}
<div className="text-11 text-placeholder">{t("workspace_settings.settings.webhooks.secret_key.message")}</div>
<div className="flex flex-col gap-4 md:flex-row md:items-center">
<div className="flex h-8 max-w-lg flex-grow items-center justify-between self-stretch rounded-sm border border-subtle px-2">
<div className="overflow-hidden font-medium select-none">
{shouldShowKey ? (
<p className="text-11">{webhookSecretKey}</p>
) : (
<div className="mr-2 flex items-center gap-1.5 overflow-hidden">
{range(30).map((index) => (
<div key={index} className="h-1 w-1 flex-shrink-0 rounded-full bg-(--text-color-disabled)" />
))}
</div>
)}
</div>
{webhookSecretKey && (
<div className="flex items-center gap-2">
{SECRET_KEY_OPTIONS.map((option) => (
<Tooltip key={option.key} label={option.label} layout="stacked" disabled={isMobile}>
<button type="button" className="grid flex-shrink-0 place-items-center" onClick={option.onClick}>
<option.Icon className="h-3 w-3 text-placeholder" />
</button>
</Tooltip>
))}
</div>
)}
</div>
{data && (
<div>
<Button
onClick={handleRegenerateSecretKey}
variant="secondary"
size="lg"
loading={isRegenerating}
prependIcon={<RefreshOutline />}
>
{isRegenerating ? `${t("re_generating")}...` : t("re_generate_key")}
</Button>
</div>
)}
</div>
</div>
)}
</>
);
});

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
// constants
import type { IWebhook } from "@plane/types";
import { Switch } from "@makeplane/propel/components/switch";
interface IWebHookToggle {
control: Control<IWebhook, any>;
}
export function WebhookToggle({ control }: IWebHookToggle) {
return (
<div className="flex gap-6">
<div className="text-13 font-medium">Enable webhook</div>
<Controller
control={control}
name="is_active"
render={({ field: { onChange, value } }) => (
<Switch size="sm" checked={value} onCheckedChange={onChange} aria-label="Enable webhook" />
)}
/>
</div>
);
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// 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 function GeneratedHookDetails(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-18 font-medium text-secondary">{t("workspace_settings.key_created")}</h3>
<p className="text-13 text-placeholder">{t("workspace_settings.copy_key")}</p>
</div>
<WebhookSecretKey data={webhookDetails} />
</div>
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4">
<Button variant="secondary" size="lg" onClick={handleClose}>
Close
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
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,29 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// 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,59 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import Link from "next/link";
import { useParams } from "next/navigation";
// Plane imports
import type { IWebhook } from "@plane/types";
import { Switch } from "@makeplane/propel/components/switch";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
interface IWebhookListItem {
webhook: IWebhook;
}
export function WebhooksListItem(props: IWebhookListItem) {
const { webhook } = props;
// router
const { workspaceSlug } = useParams();
// store hooks
const { updateWebhook } = useWebhook();
const handleToggle = async () => {
if (!workspaceSlug || !webhook.id) return;
await updateWebhook(workspaceSlug.toString(), webhook.id, { is_active: !webhook.is_active });
};
return (
<div className="rounded-lg border border-subtle bg-layer-2 px-4 py-3">
<Link
href={`/${workspaceSlug}/settings/webhooks/${webhook?.id}`}
className="flex items-center justify-between gap-4"
>
<h5 className="truncate text-body-sm-medium">{webhook.url}</h5>
<div
className="shrink-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onKeyDown={(e) => e.stopPropagation()}
role="presentation"
>
<Switch
size="sm"
checked={webhook.is_active}
onCheckedChange={() => {
void handleToggle();
}}
aria-label="Toggle webhook"
/>
</div>
</Link>
</div>
);
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
// components
import { WebhooksListItem } from "./webhooks-list-item";
export const WebhooksList = observer(function WebhooksList() {
// store hooks
const { webhooks } = useWebhook();
return (
<div className="flex size-full flex-col gap-y-2 overflow-y-auto rounded-lg border border-subtle bg-layer-1 p-3">
{Object.values(webhooks ?? {}).map((webhook) => (
<WebhooksListItem key={webhook.id} webhook={webhook} />
))}
</div>
);
});