Compare commits

..
Author SHA1 Message Date
LAKHAN BAHETI 6c3c3c3aa3 style: onboarding ui improvements 2023-12-26 12:15:58 +05:30
23 changed files with 241 additions and 330 deletions
+1 -6
View File
@@ -23,8 +23,6 @@
/* Custom image styles */
.ProseMirror img {
transition: filter 0.1s ease-in-out;
margin-top: 0 !important;
margin-bottom: 0 !important;
&:hover {
cursor: pointer;
@@ -141,8 +139,6 @@ 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: "";
@@ -169,8 +165,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
table {
border-collapse: collapse;
table-layout: fixed;
margin: 0.5em 0 0.5em 0;
margin: 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, placeholder, {
const deco = Decoration.widget(pos + 1, placeholder, {
id,
});
set = set.add(tr.doc, [deco]);
@@ -131,8 +131,7 @@ 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.insert(pos - 1, node).setMeta(uploadKey, { remove: { id } });
const transaction = view.state.tr.replaceWith(pos, pos, 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, { SuggestionOptions } from "@tiptap/suggestion";
import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react";
import tippy from "tippy.js";
import {
@@ -40,11 +40,7 @@ interface CommandItemProps {
icon: ReactNode;
}
export type SlashCommandOptions = {
suggestion: Omit<SuggestionOptions, "editor">;
};
const Command = Extension.create<SlashCommandOptions>({
const Command = Extension.create({
name: "slash-command",
addOptions() {
return {
@@ -53,10 +49,6 @@ const Command = Extension.create<SlashCommandOptions>({
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
props.command({ editor, range });
},
allow({ editor }: { editor: Editor }) {
return !editor.isActive("table");
},
allowSpaces: true,
},
};
},
@@ -64,6 +56,9 @@ const Command = Extension.create<SlashCommandOptions>({
return [
Suggestion({
editor: this.editor,
allow({ editor }) {
return !editor.isActive("table");
},
...this.options.suggestion,
}),
];
@@ -180,7 +175,7 @@ const getSuggestionItems =
key: "image",
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["img", "photo", "picture", "media"],
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
@@ -317,7 +312,7 @@ const renderItems = () => {
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
component = new ReactRenderer(CommandList, {
props,
// @ts-ignore
@@ -335,7 +330,7 @@ const renderItems = () => {
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
component?.updateProps(props);
popup &&
@@ -1,6 +1,6 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { XCircle, X } from "lucide-react";
// services
import { AuthService } from "services/auth.service";
// hooks
@@ -27,6 +27,8 @@ const authService = new AuthService();
export const EmailForm: React.FC<Props> = (props) => {
const { handleStepChange, updateEmail } = props;
// states
const [emailErrorToast, setEmailErrorToast] = useState(false);
const { setToastAlert } = useToast();
@@ -91,23 +93,51 @@ export const EmailForm: React.FC<Props> = (props) => {
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="orville.wright@frstflt.com"
className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => onChange("")}
<div className="flex transition-all">
<div
className={`relative flex-grow flex items-center rounded-[4.5px] bg-onboarding-background-200 border-0 ${
errors.email
? "!border-2 border-red-200"
: "focus-within:!border-2 focus-within:border-custom-primary-30 "
}`}
>
<Input
id="email"
name="email"
type="email"
value={value}
onChange={(event) => {
if (errors.email && !emailErrorToast) setEmailErrorToast(true);
onChange(event.target.value);
}}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="orville.wright@frstflt.com"
className={`h-[46px] w-full !rounded-[3.5px] !border ${
errors.email ? "!border-red-400" : "focus:border-custom-primary-100"
} pr-12 placeholder:text-onboarding-text-400`}
/>
{value.length > 0 && (
<XCircle
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => onChange("")}
/>
)}
</div>
{errors.email && emailErrorToast && (
<div className="absolute ml-[400px] w-full transition-all">
<div className="relative px-2 gap-x-2 flex w-fit h-10 bg-red-50 rounded-[4px] border border-red-200">
{"🤥"}
<div>
<X
className="right-1 top-1 absolute h-2.5 w-2.5 hover:cursor-pointer stroke-custom-text-400"
onClick={() => setEmailErrorToast(false)}
/>
<p className="mt-1 text-[10px]">That doesn{""}t look like an email address.</p>
<p className="mt-1 text-[10px]">Sometimes, we miss an @ or a . Make sure you have it right.</p>
</div>
</div>
</div>
)}
</div>
)}
@@ -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 your project."
description="Create labels to help organize and filter issues in you project"
image={emptyLabel}
primaryButton={{
text: "Add label",
+9 -1
View File
@@ -244,7 +244,15 @@ const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
<button
type="button"
className="ml-3 hidden place-items-center self-center rounded group-hover:grid"
onClick={() => remove(index)}
onClick={() => {
if (fields.length > 3) {
remove(index);
} else {
setValue(`emails.${index}.email`, "");
setValue(`emails.${index}.role`, 15);
setValue(`emails.${index}.role_active`, false);
}
}}
>
<XCircle className="h-3.5 w-3.5 text-custom-text-400" />
</button>
+1 -1
View File
@@ -14,7 +14,7 @@ import { IUser } from "types";
// services
import { FileService } from "services/file.service";
// assets
import IssuesSvg from "public/onboarding/onboarding-issues.webp";
import IssuesSvg from "public/onboarding/onboarding-issues.png";
const defaultValues: Partial<IUser> = {
first_name: "",
+1 -1
View File
@@ -43,7 +43,7 @@ export const SignInView = observer(() => {
</div>
</div>
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
<div className="mx-auto relative h-full rounded-t-md border-x border-t border-custom-border-200 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3 ">
<div className="h-full overflow-auto rounded-t-md bg-onboarding-gradient-200 px-7 pb-56 pt-24 sm:px-0">
{!envConfig ? (
<div className="mx-auto flex justify-center pt-10">
@@ -182,7 +182,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
setValue(
"identifier",
e.target.value
.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "")
.replace(/[^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(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "");
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
setIsChangeInIdentifierRequired(false);
onChange(alphanumericValue.toUpperCase());
};
@@ -301,9 +301,8 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
name="identifier"
rules={{
required: "Identifier is required",
// allow only alphanumeric & non-latin characters
validate: (value) =>
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.",
/^[A-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.",
minLength: {
value: 1,
message: "Identifier must at least be of 1 character",
@@ -1,139 +0,0 @@
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>
);
};
+5 -7
View File
@@ -1,16 +1,14 @@
import React from "react";
// next
import { useRouter } from "next/router";
import Image from "next/image";
// ui
import { Button } from "@plane/ui";
// assets
import EmptyWebhook from "public/empty-state/web-hook.svg";
type Props = {
onClick: () => void;
};
export const WebhooksEmptyState = () => {
const router = useRouter();
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`}
@@ -21,7 +19,7 @@ export const WebhooksEmptyState: React.FC<Props> = (props) => {
<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}>
<Button className="flex items-center gap-1.5" onClick={() => router.push(`${router.asPath}/create/`)}>
Add webhook
</Button>
</div>
+87 -25
View File
@@ -1,8 +1,11 @@
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,
@@ -10,16 +13,17 @@ 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> = {
@@ -32,12 +36,18 @@ const initialWebhookPayload: Partial<IWebhook> = {
};
export const WebhookForm: FC<Props> = observer((props) => {
const { data, onSubmit, handleClose } = props;
const { data } = props;
// states
const [webhookEventType, setWebhookEventType] = useState<TWebhookEventTypes>("all");
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// toast
const { setToastAlert } = useToast();
// mobx store
const {
webhook: { webhookSecretKey },
webhook: { createWebhook, updateWebhook },
workspace: { currentWorkspace },
} = useMobxStore();
// use form
const {
@@ -48,8 +58,74 @@ 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) => {
await onSubmit(formData, webhookEventType);
if (data) await handleUpdateWebhook(formData);
else await handleCreateWebhook(formData);
};
useEffect(() => {
@@ -85,26 +161,12 @@ export const WebhookForm: FC<Props> = observer((props) => {
<div className="mt-4">
{webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />}
</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>
)}
<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>
</form>
</div>
);
+4 -4
View File
@@ -56,11 +56,11 @@ export const WebhookSecretKey: FC<Props> = observer((props) => {
};
const handleRegenerateSecretKey = () => {
if (!workspaceSlug || !data.id) return;
if (!workspaceSlug || !webhookId) return;
setIsRegenerating(true);
regenerateSecretKey(workspaceSlug.toString(), data.id)
regenerateSecretKey(workspaceSlug.toString(), webhookId.toString())
.then(() => {
setToastAlert({
type: "success",
@@ -92,10 +92,10 @@ export const WebhookSecretKey: FC<Props> = observer((props) => {
<>
{(data || webhookSecretKey) && (
<div className="space-y-2">
{webhookId && <div className="text-sm font-medium">Secret key</div>}
<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 flex-grow max-w-lg items-center justify-between self-stretch rounded border border-custom-border-200 px-2 py-1.5">
<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="select-none overflow-hidden font-medium">
{shouldShowKey ? (
<p className="text-xs">{webhookSecretKey}</p>
@@ -1,33 +0,0 @@
// 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 -3
View File
@@ -1,8 +1,6 @@
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,7 +60,6 @@ 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(",");
+3 -2
View File
@@ -47,7 +47,8 @@ const PosthogWrapper: FC<IPosthogWrapper> = (props) => {
capture_pageview: false, // Disable automatic pageview capture, as we capture manually
});
}
}, [posthogAPIKey, posthogHost]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
// Track page views
@@ -59,7 +60,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 { useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
@@ -7,8 +7,6 @@ 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";
@@ -16,21 +14,22 @@ 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 } = router.query;
const { workspaceSlug, webhookId, isCreated } = router.query;
// mobx store
const {
webhook: { currentWebhook, fetchWebhookById, updateWebhook },
webhook: { currentWebhook, clearSecretKey, fetchWebhookById },
user: { currentWorkspaceRole },
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
useEffect(() => {
if (isCreated !== "true") clearSecretKey();
}, [clearSecretKey, isCreated]);
const isAdmin = currentWorkspaceRole === 20;
@@ -41,34 +40,6 @@ 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">
@@ -87,7 +58,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 onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
<WebhookForm data={currentWebhook} />
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
</div>
</>
@@ -0,0 +1,43 @@
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,4 +1,5 @@
import React, { useEffect, useState } from "react";
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
@@ -9,22 +10,18 @@ import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout";
// components
import { WorkspaceSettingHeader } from "components/headers";
import { WebhooksList, WebhooksEmptyState, CreateWebhookModal } from "components/web-hooks";
import { WebhooksList, WebhooksEmptyState } 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, createWebhook, clearSecretKey, webhooks, webhookSecretKey },
workspace: { currentWorkspace },
webhook: { fetchWebhooks, webhooks },
user: { currentWorkspaceRole },
} = useMobxStore();
@@ -35,11 +32,6 @@ 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">
@@ -56,28 +48,21 @@ 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>
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
Add webhook
</Button>
<Link href={`/${workspaceSlug}/settings/webhooks/create`}>
<Button variant="primary" size="sm">
Add webhook
</Button>
</Link>
</div>
<WebhooksList />
</div>
) : (
<div className="mx-auto">
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
<WebhooksEmptyState />
</div>
)}
</div>
+6 -6
View File
@@ -139,7 +139,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
<SwitchOrDeleteAccountModal isOpen={showDeleteAccountModal} onClose={() => setShowDeleteAccountModal(false)} />
{user && step !== null ? (
<div className={`fixed flex h-full w-full flex-col bg-onboarding-gradient-100`}>
<div className="flex items-center px-4 py-10 sm:px-7 sm:pb-8 sm:pt-14 md:px-14 lg:pl-28 lg:pr-24">
<div className="flex items-center px-4 py-10 sm:px-7 sm:pb-8 sm:pt-14 md:px-14 lg:pl-28 ">
<div className="flex w-full items-center justify-between font-semibold ">
<div className="flex items-center gap-x-1 text-3xl">
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" height={30} width={30} />
@@ -158,8 +158,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
currentUser?.first_name
? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}`
: value.length > 0
? value
: currentUser?.email
? value
: currentUser?.email
}
src={currentUser?.avatar}
size={35}
@@ -174,13 +174,13 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
{currentUser?.first_name
? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}`
: value.length > 0
? value
: null}
? value
: null}
</p>
)}
<Menu>
<Menu.Button className={"flex items-center gap-x-2"}>
<Menu.Button className={"flex items-center gap-x-2 min-w-[180px]"}>
<span className="text-base font-medium">{user.email}</span>
<ChevronDown className="h-4 w-4 text-custom-text-300" />
</Menu.Button>
Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB