Compare commits

...
Author SHA1 Message Date
NarayanBavisetti 2475097b4e Merge branch 'preview' of github.com:makeplane/plane into fix/external-api-context 2023-12-26 16:03:25 +05:30
sriram veeraghantaandGitHub ad20079e0c Merge pull request #3249 from makeplane/develop
promote: moving stable changes from develop to preview
2023-12-26 14:25:01 +05:30
Lakhan BahetiandGitHub e14baf17a7 style: create webhook page to modal (#3223)
* style: create webhook page to modal

* fix: create page removed

* fix: auto modal close on empty state

* fix: secret key heading removed from generated modal
2023-12-26 13:28:47 +05:30
Anmol Singh BhatiaandGitHub 542b18a585 fix: custom theme validation added (#3246) 2023-12-26 13:11:59 +05:30
sriram veeraghantaandGitHub 37d88cc05b Merge pull request #3233 from makeplane/fix/types-slash-command
fix: Fixed types for slash commands by inspecting generics
2023-12-22 19:02:20 +05:30
Palanikannan1437 b652d1a8f1 fix: Fixed types for slash commands by inspecting generics 2023-12-22 18:52:00 +05:30
sriram veeraghanta 98b7a941f9 fix: yarn lock fixes 2023-12-22 18:37:14 +05:30
sriram veeraghantaandGitHub dc131ee05b Merge pull request #3232 from makeplane/release-0.15
fix: moving patch fixes from release 0.15 to 0.14.2
2023-12-22 18:19:47 +05:30
M. Palanikannanandsriram veeraghanta 0e3d15215d [fix] Image insertion position at cursor position and Updated placeholder text. (#3224)
* Fix position bug in the UploadImagesPlugin widget decoration and adjust transaction to correctly insert image node and set meta data for image removal.

* Update CSS styles in editor.css to remove margin top and bottom on images and img placeholders and adjust the margin on the table in the editor container.

* Better typescript support for images extension.

Update the `Command` extension in `slash-commands.tsx` to include a `SlashCommandOptions` type for better TS support and allow spaces in the suggestion options and modify the `image` suggestion's search terms to include "img" in addition to "photo", "picture", and "media".
2023-12-22 18:10:21 +05:30
Lakhan Bahetiandsriram veeraghanta 816b6abf3b chore: allow non-latin characters to project identifier (#3226) 2023-12-22 18:10:21 +05:30
Lakhan Bahetiandsriram veeraghanta 6ecaa661a7 style: email placeholder changed across the platform (#3206)
* style: email placeholder changed across the platform

* fix: placeholder text
2023-12-22 18:10:21 +05:30
M. Palanikannanandsriram veeraghanta 0b1efb173f [refactor] Editor code refactoring (#3194)
* removed relative imports from editor core

* Update issue widget file paths and imports to use kebab case instead of camel case, to align with coding conventions and improve consistency.

* Update Tiptap core and extensions versions to 2.1.13 and Tiptap React version to 2.1.13. Update Tiptap table imports to use the new location in package @tiptap/pm/tables. Update AlertLabel component to use the new type definition for LucideIcon.

* updated lock file

* removed default exports from editor/core

* fixed injecting css into the core package itself

* seperated css code to have single source of origin wrt to the package

* removed default imports from document editor

* all instances using index as key while mapping fixed

* Update Lite Text Editor package.json to remove @plane/editor-types as a dependency.

Update Lite Text Editor index.ts to update the import of IMentionSuggestion and IMentionHighlight from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/index.tsx to update the import of UploadImage, DeleteImage, IMentionSuggestion, and RestoreImage from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/menus/fixed-menu/index.tsx to update the import of UploadImage from @plane/editor-types to @plane/editor-core.

Update turbo.json to remove @plane/editor-types#build as a dependency for @plane/lite-text-editor#build, @plane/rich-text-editor#build, and @plane/document-editor#build.

* Remove deprecated import and adjust tippy.js usage in the slash-commands.tsx file of the editor extensions package.

* Update dependencies in `rich-text-editor/package.json`, remove `@plane/editor-types` and add `@plane/editor-core` in `rich-text-editor/src/index.ts`, and update imports in `rich-text-editor/src/ui/extensions/index.tsx` and `rich-text-editor/src/ui/index.tsx` to use `@plane/editor-core` instead of `@plane/editor-types`.

* Update package.json dependencies and add new types for image deletion, upload, restore, mention highlight, mention suggestion, and slash command item.

* Update import statements in various files to use the new package "@plane/editor-core" instead of "@plane/editor-types".

* fixed document editor to follow conventions

* Refactor imports in the Rich Text Editor package to use relative paths instead of absolute paths.

- Updated imports in `index.ts`, `ui/index.tsx`, and `ui/menus/bubble-menu/index.tsx` to use relative paths.
- Updated `tsconfig.json` to include the `baseUrl` compiler option and adjust the `include` and `exclude` paths.

* Refactor Lite Text Editor code to use relative import paths instead of absolute import paths.

* Added LucideIconType to the exports in index.ts for use in other files.
Created a new file lucide-icon.ts which contains the type LucideIconType.
Updated the icon type in HeadingOneItem in menu-items/index.tsx to use LucideIconType.
Updated the Icon type in AlertLabel in alert-label.tsx to use LucideIconType.
Updated the Icon type in VerticalDropdownItemProps in vertical-dropdown-menu.tsx to use LucideIconType.
Updated the Icon type in BubbleMenuItem in fixed-menu/index.tsx to use LucideIconType.
Deleted the file tooltip.tsx since it is no longer used.
Updated the Icon type in BubbleMenuItem in bubble-menu/index.tsx to use LucideIconType.

* ♻️ refactor: simplify rendering logic in slash-commands.tsx

The rendering logic in the file "slash-commands.tsx" has been simplified. Previously, the code used inline positioning for the popup, but it has now been removed. Instead of appending the popup to the document body, it is now appended to the element with the ID "tiptap-container". The "flip" option has also been removed. These changes have improved the readability and maintainability of the code.

* fixed build errors caused due to core's internal imports

* regression: fixed pages not saving issue and not duplicating with proper content issue

* build: Update @tiptap dependencies

Updated the @tiptap dependencies in the package.json files of `document-editor`, `extensions`, and `rich-text-editor` packages to version 2.1.13.

* 🚑 fix: Correct appendTo selector in slash-commands.tsx

Update the `appendTo` function call in `slash-commands.tsx` to use the correct selector `#editor-container` instead of `#tiptap-container`. This ensures that the component is appended to the appropriate container in the editor extension.

Note: The commit message assumes that the change is a fix for an issue or error. If it's not a fix, please provide more context so that an appropriate commit type can be determined.
2023-12-22 18:10:18 +05:30
Lakhan Bahetiandsriram veeraghanta aceee7d2e2 fix: bugs & improvements (#3189)
* fix: workspace invitation modal form values reset

* fix: profile sidebar avatar letter
2023-12-22 18:09:41 +05:30
Anmol Singh Bhatiaandsriram veeraghanta 0a41eff435 chore: bug fixes & improvement (#3218)
* chore: draft issue validation added to prevent saving empty or whitespace title

* chore: resolve scrolling issue in page empty state

* chore: kanban layout quick add issue improvement
2023-12-22 18:09:41 +05:30
Prateek Shouryaandsriram veeraghanta 5f681973a0 style: instance admin email settings ui & ux update. (#3186) 2023-12-22 18:09:41 +05:30
9c65657a66 fix - file size limit not work on plane.settings.production (#3160)
* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production, move to common.py

---------

Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-12-22 18:09:39 +05:30
Manish Guptaandsriram veeraghanta 6fab75f9ab fix: branch build fix (#3214)
* branch build fix for release-* in case of space,backend,proxy

* fixes
2023-12-22 18:08:36 +05:30
sriram veeraghantaandGitHub 111070bdb9 Merge pull request #3231 from makeplane/release-0.14.2
fix: branch build custom docker repo with suffix (#3182)
2023-12-22 17:49:25 +05:30
David ChiangandGitHub d76840ff6f Fix spelling error in setting-label-list.tsx (#3210) 2023-12-22 17:24:02 +05:30
M. PalanikannanandGitHub 1a2186cca4 [fix] Image insertion position at cursor position and Updated placeholder text. (#3224)
* Fix position bug in the UploadImagesPlugin widget decoration and adjust transaction to correctly insert image node and set meta data for image removal.

* Update CSS styles in editor.css to remove margin top and bottom on images and img placeholders and adjust the margin on the table in the editor container.

* Better typescript support for images extension.

Update the `Command` extension in `slash-commands.tsx` to include a `SlashCommandOptions` type for better TS support and allow spaces in the suggestion options and modify the `image` suggestion's search terms to include "img" in addition to "photo", "picture", and "media".
2023-12-22 14:10:42 +05:30
sriram veeraghantaandGitHub d4b6f4faf1 Merge pull request #3227 from makeplane/develop
fix: posthog events trigger in staging (#3220)
2023-12-22 14:05:51 +05:30
Ramesh Kumar ChandraandGitHub fe9519314a fix: posthog events trigger in staging (#3220)
* fix: posthog events trigger in staging

* refactor: cleared comment lines for warnings, added dependancy values

* refactor: removed unnecessary dependency
2023-12-22 14:05:17 +05:30
Lakhan BahetiandGitHub ac39bb9b1d chore: allow non-latin characters to project identifier (#3226) 2023-12-22 14:04:47 +05:30
16 changed files with 303 additions and 176 deletions
+6 -1
View File
@@ -23,6 +23,8 @@
/* Custom image styles */
.ProseMirror img {
transition: filter 0.1s ease-in-out;
margin-top: 0 !important;
margin-bottom: 0 !important;
&:hover {
cursor: pointer;
@@ -139,6 +141,8 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
.img-placeholder {
position: relative;
width: 35%;
margin-top: 0 !important;
margin-bottom: 0 !important;
&:before {
content: "";
@@ -165,7 +169,8 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
table {
border-collapse: collapse;
table-layout: fixed;
margin: 0;
margin: 0.5em 0 0.5em 0;
border: 1px solid rgb(var(--color-border-200));
width: 100%;
@@ -43,7 +43,7 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
cancelButton.appendChild(svgElement);
placeholder.appendChild(cancelButton);
const deco = Decoration.widget(pos + 1, placeholder, {
const deco = Decoration.widget(pos, placeholder, {
id,
});
set = set.add(tr.doc, [deco]);
@@ -131,7 +131,8 @@ export async function startImageUpload(
const imageSrc = typeof src === "object" ? reader.result : src;
const node = schema.nodes.image.create({ src: imageSrc });
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
const transaction = view.state.tr.insert(pos - 1, node).setMeta(uploadKey, { remove: { id } });
view.dispatch(transaction);
} catch (error) {
console.error("Upload error: ", error);
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
import { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion";
import Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react";
import tippy from "tippy.js";
import {
@@ -40,7 +40,11 @@ interface CommandItemProps {
icon: ReactNode;
}
const Command = Extension.create({
export type SlashCommandOptions = {
suggestion: Omit<SuggestionOptions, "editor">;
};
const Command = Extension.create<SlashCommandOptions>({
name: "slash-command",
addOptions() {
return {
@@ -49,6 +53,10 @@ const Command = Extension.create({
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
props.command({ editor, range });
},
allow({ editor }: { editor: Editor }) {
return !editor.isActive("table");
},
allowSpaces: true,
},
};
},
@@ -56,9 +64,6 @@ const Command = Extension.create({
return [
Suggestion({
editor: this.editor,
allow({ editor }) {
return !editor.isActive("table");
},
...this.options.suggestion,
}),
];
@@ -175,7 +180,7 @@ const getSuggestionItems =
key: "image",
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
searchTerms: ["img", "photo", "picture", "media"],
icon: <ImageIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
@@ -312,7 +317,7 @@ const renderItems = () => {
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
component = new ReactRenderer(CommandList, {
props,
// @ts-ignore
@@ -330,7 +335,7 @@ const renderItems = () => {
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
component?.updateProps(props);
popup &&
@@ -217,7 +217,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
{projectLabels && projectLabels.length === 0 && (
<EmptyState
title="No labels yet"
description="Create labels to help organize and filter issues in you project"
description="Create labels to help organize and filter issues in your project."
image={emptyLabel}
primaryButton={{
text: "Add label",
@@ -182,7 +182,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
setValue(
"identifier",
e.target.value
.replace(/[^a-zA-Z0-9]/g, "")
.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "")
.toUpperCase()
.substring(0, 5)
);
@@ -191,7 +191,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
const alphanumericValue = value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "");
setIsChangeInIdentifierRequired(false);
onChange(alphanumericValue.toUpperCase());
};
@@ -301,8 +301,9 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
name="identifier"
rules={{
required: "Identifier is required",
// allow only alphanumeric & non-latin characters
validate: (value) =>
/^[A-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.",
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.",
minLength: {
value: 1,
message: "Identifier must at least be of 1 character",
@@ -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>
);
};
+7 -5
View File
@@ -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> = (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`}
@@ -19,7 +21,7 @@ export const WebhooksEmptyState = () => {
<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={() => router.push(`${router.asPath}/create/`)}>
<Button className="flex items-center gap-1.5" onClick={onClick}>
Add webhook
</Button>
</div>
+25 -87
View File
@@ -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<IWebhook>;
onSubmit: (data: IWebhook, webhookEventType: TWebhookEventTypes) => Promise<void>;
handleClose?: () => void;
};
const initialWebhookPayload: Partial<IWebhook> = {
@@ -36,18 +32,12 @@ const initialWebhookPayload: Partial<IWebhook> = {
};
export const WebhookForm: FC<Props> = observer((props) => {
const { data } = props;
const { data, onSubmit, handleClose } = props;
// states
const [webhookEventType, setWebhookEventType] = useState<TWebhookEventTypes>("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<Props> = observer((props) => {
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) => {
if (data) await handleUpdateWebhook(formData);
else await handleCreateWebhook(formData);
await onSubmit(formData, webhookEventType);
};
useEffect(() => {
@@ -161,12 +85,26 @@ export const WebhookForm: FC<Props> = observer((props) => {
<div className="mt-4">
{webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />}
</div>
<div className="mt-8 space-y-8">
{data && <WebhookSecretKey data={data} />}
<Button type="submit" loading={isSubmitting}>
{data ? (isSubmitting ? "Updating..." : "Update") : isSubmitting ? "Creating..." : "Create"}
</Button>
</div>
{data ? (
<div className="mt-8 space-y-8">
<WebhookSecretKey data={data} />
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update"}
</Button>
</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>
</div>
);
+4 -4
View File
@@ -56,11 +56,11 @@ export const WebhookSecretKey: FC<Props> = 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<Props> = observer((props) => {
<>
{(data || webhookSecretKey) && (
<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="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">
{shouldShowKey ? (
<p className="text-xs">{webhookSecretKey}</p>
@@ -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>
);
};
+3 -1
View File
@@ -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";
+1
View File
@@ -60,6 +60,7 @@ const calculateShades = (hexValue: string): TShades => {
};
export const applyTheme = (palette: string, isDarkPalette: boolean) => {
if (!palette) return;
const dom = document?.querySelector<HTMLElement>("[data-theme='custom']");
// palette: [bg, text, primary, sidebarBg, sidebarText]
const values: string[] = palette.split(",");
+2 -3
View File
@@ -47,8 +47,7 @@ const PosthogWrapper: FC<IPosthogWrapper> = (props) => {
capture_pageview: false, // Disable automatic pageview capture, as we capture manually
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [posthogAPIKey, posthogHost]);
useEffect(() => {
// Track page views
@@ -60,7 +59,7 @@ const PosthogWrapper: FC<IPosthogWrapper> = (props) => {
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (posthogAPIKey) {
@@ -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 (
<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)} />
<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)} />}
</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 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 (
<div className="mt-10 flex h-full w-full justify-center p-4">
@@ -48,21 +56,28 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
return (
<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 ? (
<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="text-xl font-medium">Webhooks</div>
<Link href={`/${workspaceSlug}/settings/webhooks/create`}>
<Button variant="primary" size="sm">
Add webhook
</Button>
</Link>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
</div>
<WebhooksList />
</div>
) : (
<div className="mx-auto">
<WebhooksEmptyState />
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
</div>
)}
</div>