[WEB-1127] style: create and delete modals' consistency (#4345)

* style: update modals typography, alignment

* style: made the modal separator full width

* style: delete modals consistency

* style: update the remaining delete modals

* chore: delete modal secondary button text

* style: update the remaining create modals

* chore: update cancel button text

* chore: created modal core

* style: modals responsiveness
This commit is contained in:
Aaryan Khandelwal 2024-05-07 12:44:36 +05:30 committed by GitHub
parent 5ef51edad7
commit 20e7dc68e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1375 additions and 2161 deletions

View File

@ -1,15 +1,16 @@
import { useState, Fragment, FC } from "react"; import { useState, FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
import { IApiToken } from "@plane/types";
// services
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
import { APITokenService } from "@/services/api_token.service";
// ui
// types // types
import { IApiToken } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// fetch-keys // fetch-keys
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// services
import { APITokenService } from "@/services/api_token.service";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -32,12 +33,12 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
setDeleteLoading(false); setDeleteLoading(false);
}; };
const handleDeletion = () => { const handleDeletion = async () => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
setDeleteLoading(true); setDeleteLoading(true);
apiTokenService await apiTokenService
.deleteApiToken(workspaceSlug.toString(), tokenId) .deleteApiToken(workspaceSlug.toString(), tokenId)
.then(() => { .then(() => {
setToast({ setToast({
@ -65,58 +66,17 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDeletion}
as={Fragment} isDeleting={deleteLoading}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete API token"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Any application using this token will no longer have the access to Plane data. This action cannot be undone.
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={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 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-3 p-4">
<div className="flex w-full items-center justify-start">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
Are you sure you want to delete the token?
</h3>
</div>
<span>
<p className="text-sm text-custom-text-400">
Any application using this token will no longer have the access to Plane data. This action cannot
be undone.
</p>
</span>
<div className="mt-2 flex justify-end gap-2">
<Button variant="neutral-primary" onClick={handleClose} size="sm">
Cancel
</Button>
<Button variant="danger" onClick={handleDeletion} loading={deleteLoading} size="sm">
{deleteLoading ? "Deleting..." : "Delete"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}; };

View File

@ -1,21 +1,20 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react"; // types
import { IApiToken } from "@plane/types"; import { IApiToken } from "@plane/types";
// services // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { CreateApiTokenForm, GeneratedTokenDetails } from "@/components/api-token"; import { CreateApiTokenForm, GeneratedTokenDetails } from "@/components/api-token";
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
// fetch-keys
import { API_TOKENS_LIST } from "@/constants/fetch-keys"; import { API_TOKENS_LIST } from "@/constants/fetch-keys";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper";
import { csvDownload } from "@/helpers/download.helper"; import { csvDownload } from "@/helpers/download.helper";
// services
import { APITokenService } from "@/services/api_token.service"; import { APITokenService } from "@/services/api_token.service";
// ui
// components
// helpers
// types
// fetch-keys
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -86,47 +85,17 @@ export const CreateApiTokenModal: React.FC<Props> = (props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={() => {}} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={() => {}}> {generatedToken ? (
<Transition.Child <GeneratedTokenDetails handleClose={handleClose} tokenDetails={generatedToken} />
as={React.Fragment} ) : (
enter="ease-out duration-300" <CreateApiTokenForm
enterFrom="opacity-0" handleClose={handleClose}
enterTo="opacity-100" neverExpires={neverExpires}
leave="ease-in duration-200" toggleNeverExpires={() => setNeverExpires((prevData) => !prevData)}
leaveFrom="opacity-100" onSubmit={handleCreateToken}
leaveTo="opacity-0" />
> )}
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> </ModalCore>
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="grid h-full w-full place-items-center p-4 text-center">
<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 rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-2xl">
{generatedToken ? (
<GeneratedTokenDetails handleClose={handleClose} tokenDetails={generatedToken} />
) : (
<CreateApiTokenForm
handleClose={handleClose}
neverExpires={neverExpires}
toggleNeverExpires={() => setNeverExpires((prevData) => !prevData)}
onSubmit={handleCreateToken}
/>
)}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}; };

View File

@ -2,13 +2,15 @@ import { useState } from "react";
import { add } from "date-fns"; import { add } from "date-fns";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Calendar } from "lucide-react"; import { Calendar } from "lucide-react";
// types
import { IApiToken } from "@plane/types"; import { IApiToken } from "@plane/types";
// ui // ui
import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { DateDropdown } from "@/components/dropdowns"; import { DateDropdown } from "@/components/dropdowns";
// helpers // helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// types
type Props = { type Props = {
handleClose: () => void; handleClose: () => void;
@ -106,13 +108,14 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
const today = new Date(); const today = new Date();
const tomorrow = add(today, { days: 1 }); const tomorrow = add(today, { days: 1 });
const expiredAt = watch("expired_at");
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-4"> <div className="space-y-5 p-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Create token</h3> <h3 className="text-xl font-medium text-custom-text-200">Create token</h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div className="space-y-1">
<Controller <Controller
control={control} control={control}
name="label" name="label"
@ -130,8 +133,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
value={value} value={value}
onChange={onChange} onChange={onChange}
hasError={Boolean(errors.label)} hasError={Boolean(errors.label)}
placeholder="Token title" placeholder="Title"
className="w-full text-sm font-medium" className="w-full text-base"
/> />
)} )}
/> />
@ -145,8 +148,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
value={value} value={value}
onChange={onChange} onChange={onChange}
hasError={Boolean(errors.description)} hasError={Boolean(errors.description)}
placeholder="Token description" placeholder="Description"
className="min-h-24 w-full text-sm" className="w-full text-base resize-none min-h-24"
/> />
)} )}
/> />
@ -162,9 +165,12 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
<CustomSelect <CustomSelect
customButton={ customButton={
<div <div
className={`flex items-center gap-2 rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 ${ className={cn(
neverExpires ? "text-custom-text-400" : "" "h-7 flex items-center gap-2 rounded border-[0.5px] border-custom-border-300 px-2 py-0.5",
}`} {
"text-custom-text-400": neverExpires,
}
)}
> >
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
{value === "custom" {value === "custom"
@ -188,33 +194,35 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
); );
}} }}
/> />
{watch("expired_at") === "custom" && ( {expiredAt === "custom" && (
<DateDropdown <div className="h-7">
value={customDate} <DateDropdown
onChange={(date) => setCustomDate(date)} value={customDate}
minDate={tomorrow} onChange={(date) => setCustomDate(date)}
icon={<Calendar className="h-3 w-3" />} minDate={tomorrow}
buttonVariant="border-with-text" icon={<Calendar className="h-3 w-3" />}
placeholder="Set date" buttonVariant="border-with-text"
disabled={neverExpires} placeholder="Set date"
/> disabled={neverExpires}
/>
</div>
)} )}
</div> </div>
{!neverExpires && ( {!neverExpires && (
<span className="text-xs text-custom-text-400"> <span className="text-xs text-custom-text-400">
{watch("expired_at") === "custom" {expiredAt === "custom"
? customDate ? customDate
? `Expires ${renderFormattedDate(customDate)}` ? `Expires ${renderFormattedDate(customDate)}`
: null : null
: watch("expired_at") : expiredAt
? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` ? `Expires ${getExpiryDate(expiredAt ?? "")}`
: null} : null}
</span> </span>
)} )}
</div> </div>
</div> </div>
</div> </div>
<div className="mt-5 flex items-center justify-between gap-2"> <div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
<div className="flex cursor-pointer items-center gap-1.5" onClick={toggleNeverExpires}> <div className="flex cursor-pointer items-center gap-1.5" onClick={toggleNeverExpires}>
<div className="flex cursor-pointer items-center justify-center"> <div className="flex cursor-pointer items-center justify-center">
<ToggleSwitch value={neverExpires} onChange={() => {}} size="sm" /> <ToggleSwitch value={neverExpires} onChange={() => {}} size="sm" />
@ -223,10 +231,10 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}> <Button variant="neutral-primary" size="sm" onClick={handleClose}>
Discard Cancel
</Button> </Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}> <Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{isSubmitting ? "Generating..." : "Generate token"} {isSubmitting ? "Generating" : "Generate token"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -28,7 +28,7 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
}; };
return ( return (
<div className="w-full"> <div className="w-full p-5">
<div className="w-full space-y-3 text-wrap"> <div className="w-full space-y-3 text-wrap">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3> <h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3>
<p className="text-sm text-custom-text-400"> <p className="text-sm text-custom-text-400">

View File

@ -0,0 +1,90 @@
import { AlertTriangle, LucideIcon } from "lucide-react";
// ui
import { Button, TButtonVariant } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
// helpers
import { cn } from "@/helpers/common.helper";
export type TModalVariant = "danger";
type Props = {
content: React.ReactNode | string;
handleClose: () => void;
handleSubmit: () => Promise<void>;
hideIcon?: boolean;
isDeleting: boolean;
isOpen: boolean;
position?: EModalPosition;
primaryButtonText?: {
loading: string;
default: string;
};
secondaryButtonText?: string;
title: string;
variant?: TModalVariant;
width?: EModalWidth;
};
const VARIANT_ICONS: Record<TModalVariant, LucideIcon> = {
danger: AlertTriangle,
};
const BUTTON_VARIANTS: Record<TModalVariant, TButtonVariant> = {
danger: "danger",
};
const VARIANT_CLASSES: Record<TModalVariant, string> = {
danger: "bg-red-500/20 text-red-500",
};
export const AlertModalCore: React.FC<Props> = (props) => {
const {
content,
handleClose,
handleSubmit,
hideIcon = false,
isDeleting,
isOpen,
position = EModalPosition.CENTER,
primaryButtonText = {
loading: "Deleting",
default: "Delete",
},
secondaryButtonText = "Cancel",
title,
variant = "danger",
width = EModalWidth.XL,
} = props;
const Icon = VARIANT_ICONS[variant];
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={position} width={width}>
<div className="p-5 flex flex-col sm:flex-row items-center sm:items-start gap-4">
{!hideIcon && (
<span
className={cn(
"flex-shrink-0 grid place-items-center rounded-full size-12 sm:size-10",
VARIANT_CLASSES[variant]
)}
>
<Icon className="size-5" aria-hidden="true" />
</span>
)}
<div className="text-center sm:text-left">
<h3 className="text-lg font-medium">{title}</h3>
<p className="mt-1 text-sm text-custom-text-200">{content}</p>
</div>
</div>
<div className="px-5 py-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
{secondaryButtonText}
</Button>
<Button variant={BUTTON_VARIANTS[variant]} size="sm" tabIndex={1} onClick={handleSubmit} loading={isDeleting}>
{isDeleting ? primaryButtonText.loading : primaryButtonText.default}
</Button>
</div>
</ModalCore>
);
};

View File

@ -1,7 +1,9 @@
export * from "./alert-modal";
export * from "./bulk-delete-issues-modal"; export * from "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal"; export * from "./existing-issues-list-modal";
export * from "./gpt-assistant-popover"; export * from "./gpt-assistant-popover";
export * from "./link-modal"; export * from "./link-modal";
export * from "./modal-core";
export * from "./user-image-upload-modal"; export * from "./user-image-upload-modal";
export * from "./workspace-image-upload-modal"; export * from "./workspace-image-upload-modal";
export * from "./issue-search-modal-empty-state"; export * from "./issue-search-modal-empty-state";

View File

@ -0,0 +1,68 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
// helpers
import { cn } from "@/helpers/common.helper";
export enum EModalPosition {
TOP = "flex items-center justify-center text-center mx-4 my-10 md:my-20",
CENTER = "flex items-end sm:items-center justify-center p-4 min-h-full",
}
export enum EModalWidth {
XL = "sm:max-w-xl",
XXL = "sm:max-w-2xl",
XXXL = "sm:max-w-3xl",
XXXXL = "sm:max-w-4xl",
}
type Props = {
children: React.ReactNode;
handleClose: () => void;
isOpen: boolean;
position?: EModalPosition;
width?: EModalWidth;
};
export const ModalCore: React.FC<Props> = (props) => {
const { children, handleClose, isOpen, position = EModalPosition.CENTER, width = EModalWidth.XXL } = props;
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={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 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className={position}>
<Transition.Child
as={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={cn(
"relative transform rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all w-full",
width
)}
>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -1,16 +1,16 @@
import { Fragment, useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { ICycle } from "@plane/types";
// hooks
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { CYCLE_DELETED } from "@/constants/event-tracker";
import { useEventTracker, useCycle } from "@/hooks/store";
// components
// types // types
import { ICycle } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// constants // constants
import { CYCLE_DELETED } from "@/constants/event-tracker";
// hooks
import { useEventTracker, useCycle } from "@/hooks/store";
interface ICycleDelete { interface ICycleDelete {
cycle: ICycle; cycle: ICycle;
@ -24,12 +24,12 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
// states // states
const [loader, setLoader] = useState(false); const [loader, setLoader] = useState(false);
// router
const router = useRouter();
const { cycleId, peekCycle } = router.query;
// store hooks // store hooks
const { captureCycleEvent } = useEventTracker(); const { captureCycleEvent } = useEventTracker();
const { deleteCycle } = useCycle(); const { deleteCycle } = useCycle();
// router
const router = useRouter();
const { cycleId, peekCycle } = router.query;
const formSubmit = async () => { const formSubmit = async () => {
if (!cycle) return; if (!cycle) return;
@ -70,66 +70,19 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
}; };
return ( return (
<div> <AlertModalCore
<div> handleClose={handleClose}
<Transition.Root show={isOpen} as={Fragment}> handleSubmit={formSubmit}
<Dialog as="div" className="relative z-20" onClose={handleClose}> isDeleting={loader}
<Transition.Child isOpen={isOpen}
as={Fragment} title="Delete Cycle"
enter="ease-out duration-300" content={
enterFrom="opacity-0" <>
enterTo="opacity-100" Are you sure you want to delete cycle{' "'}
leave="ease-in duration-200" <span className="break-words font-medium text-custom-text-100">{cycle?.name}</span>
leaveFrom="opacity-100" {'"'}? All of the data related to the cycle will be permanently removed. This action cannot be undone.
leaveTo="opacity-0" </>
> }
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> />
</Transition.Child>
<div className="fixed inset-0 z-10 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={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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-4">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20">
<AlertTriangle width={16} strokeWidth={2} className="text-red-600" />
</div>
<div className="text-xl font-medium 2xl:text-2xl">Delete cycle</div>
</div>
<span>
<p className="text-sm text-custom-text-200">
Are you sure you want to delete cycle{' "'}
<span className="break-words font-medium text-custom-text-100">{cycle?.name}</span>
{'"'}? All of the data related to the cycle will be permanently removed. This action cannot be
undone.
</p>
</span>
<div className="flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={formSubmit} loading={loader}>
{loader ? "Deleting" : "Delete"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</div>
</div>
); );
}); });

View File

@ -1,12 +1,12 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// types
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
// ui
import { Button, Input, TextArea } from "@plane/ui"; import { Button, Input, TextArea } from "@plane/ui";
// components
import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns"; import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldRenderProject } from "@/helpers/project.helper"; import { shouldRenderProject } from "@/helpers/project.helper";
@ -53,121 +53,119 @@ export const CycleForm: React.FC<Props> = (props) => {
return ( return (
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData, dirtyFields))}> <form onSubmit={handleSubmit((formData) => handleFormSubmit(formData, dirtyFields))}>
<div className="space-y-5"> <div className="space-y-5 p-5">
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
{!status && ( {!status && (
<Controller <Controller
control={control} control={control}
name="project_id" name="project_id"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<ProjectDropdown <div className="h-7">
value={value} <ProjectDropdown
onChange={(val) => { value={value}
onChange(val); onChange={(val) => {
setActiveProject(val); onChange(val);
}} setActiveProject(val);
buttonVariant="background-with-text" }}
renderCondition={(project) => shouldRenderProject(project)} buttonVariant="border-with-text"
tabIndex={7} renderCondition={(project) => shouldRenderProject(project)}
/> tabIndex={7}
/>
</div>
)} )}
/> />
)} )}
<h3 className="text-xl font-medium leading-6 text-custom-text-200">{status ? "Update" : "New"} Cycle</h3> <h3 className="text-xl font-medium text-custom-text-200">{status ? "Update" : "Create"} Cycle</h3>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="mt-2 space-y-3"> <div className="space-y-1">
<div className="flex flex-col gap-1"> <Controller
<Controller name="name"
name="name" control={control}
control={control} rules={{
rules={{ required: "Title is required",
required: "Name is required", maxLength: {
maxLength: { value: 255,
value: 255, message: "Title should be less than 255 characters",
message: "Title should be less than 255 characters", },
}, }}
}} render={({ field: { value, onChange } }) => (
render={({ field: { value, onChange } }) => ( <Input
<Input name="name"
id="cycle_name" type="text"
name="name" placeholder="Title"
type="text" className="w-full text-base"
placeholder="Cycle Title" value={value}
className="w-full resize-none placeholder:text-sm placeholder:font-medium focus:border-blue-400" inputSize="md"
value={value} onChange={onChange}
inputSize="md" hasError={Boolean(errors?.name)}
onChange={onChange} tabIndex={1}
hasError={Boolean(errors?.name)} autoFocus
tabIndex={1} />
/> )}
)} />
/> <span className="text-xs text-red-500">{errors?.name?.message}</span>
<span className="text-xs text-red-500">{errors?.name?.message}</span> </div>
</div> <div>
<div> <Controller
<Controller name="description"
name="description" control={control}
control={control} render={({ field: { value, onChange } }) => (
render={({ field: { value, onChange } }) => ( <TextArea
<TextArea name="description"
id="cycle_description" placeholder="Description"
name="description" className="w-full text-base resize-none min-h-24"
placeholder="Description..." hasError={Boolean(errors?.description)}
className="w-full text-sm resize-none min-h-24" value={value}
hasError={Boolean(errors?.description)} onChange={onChange}
value={value} tabIndex={2}
onChange={onChange} />
tabIndex={2} )}
/> />
)} </div>
/> <div className="flex flex-wrap items-center gap-2">
</div> <Controller
control={control}
<div className="flex flex-wrap items-center gap-2"> name="start_date"
<Controller render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
control={control} <Controller
name="start_date" control={control}
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => ( name="end_date"
<Controller render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
control={control} <DateRangeDropdown
name="end_date" buttonVariant="border-with-text"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => ( className="h-7"
<DateRangeDropdown minDate={new Date()}
buttonVariant="border-with-text" value={{
className="h-7" from: getDate(startDateValue),
minDate={new Date()} to: getDate(endDateValue),
value={{ }}
from: getDate(startDateValue), onSelect={(val) => {
to: getDate(endDateValue), onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
}} onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
onSelect={(val) => { }}
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null); placeholder={{
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null); from: "Start date",
}} to: "End date",
placeholder={{ }}
from: "Start date", hideIcon={{
to: "End date", to: true,
}} }}
hideIcon={{ tabIndex={3}
to: true, />
}} )}
tabIndex={3} />
/> )}
)} />
/>
)}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 "> <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} tabIndex={4}> <Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}> <Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
{data ? (isSubmitting ? "Updating" : "Update cycle") : isSubmitting ? "Creating" : "Create cycle"} {data ? (isSubmitting ? "Updating" : "Update Cycle") : isSubmitting ? "Creating" : "Create Cycle"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -1,18 +1,18 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Dialog, Transition } from "@headlessui/react"; // types
import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types"; import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types";
// services // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { CycleForm } from "@/components/cycles"; import { CycleForm } from "@/components/cycles";
// constants
import { CYCLE_CREATED, CYCLE_UPDATED } from "@/constants/event-tracker"; import { CYCLE_CREATED, CYCLE_UPDATED } from "@/constants/event-tracker";
// hooks
import { useEventTracker, useCycle, useProject } from "@/hooks/store"; import { useEventTracker, useCycle, useProject } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
// services
import { CycleService } from "@/services/cycle.service"; import { CycleService } from "@/services/cycle.service";
// hooks
// components
// ui
// types
// constants
type CycleModalProps = { type CycleModalProps = {
isOpen: boolean; isOpen: boolean;
@ -166,45 +166,15 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
}, [activeProject, data, projectId, workspaceProjectIds, isOpen]); }, [activeProject, data, projectId, workspaceProjectIds, isOpen]);
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <CycleForm
<Transition.Child handleFormSubmit={handleFormSubmit}
as={React.Fragment} handleClose={handleClose}
enter="ease-out duration-300" status={data ? true : false}
enterFrom="opacity-0" projectId={activeProject ?? ""}
enterTo="opacity-100" setActiveProject={setActiveProject}
leave="ease-in duration-200" data={data}
leaveFrom="opacity-100" />
leaveTo="opacity-0" </ModalCore>
>
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<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 rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
<CycleForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}
status={data ? true : false}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
data={data}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}; };

View File

@ -2,15 +2,16 @@ import React, { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
import { IEstimate, IEstimateFormData } from "@plane/types";
// store hooks
import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui";
import { checkDuplicates } from "@/helpers/array.helper";
import { useEstimate } from "@/hooks/store";
// ui
// helpers
// types // types
import { IEstimate, IEstimateFormData } from "@plane/types";
// ui
import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
// helpers
import { checkDuplicates } from "@/helpers/array.helper";
// hooks
import { useEstimate } from "@/hooks/store";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -196,133 +197,96 @@ export const CreateUpdateEstimateModal: React.FC<Props> = observer((props) => {
}, [data, reset]); }, [data, reset]);
return ( return (
<> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<Transition.Root show={isOpen} as={React.Fragment}> <form onSubmit={handleSubmit(onSubmit)}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <div className="space-y-5 p-5">
<Transition.Child <div className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Create"} Estimate</div>
as={React.Fragment} <div className="space-y-3">
enter="ease-out duration-300" <div>
enterFrom="opacity-0" <Controller
enterTo="opacity-100" control={control}
leave="ease-in duration-200" name="name"
leaveFrom="opacity-100" render={({ field: { value, onChange, ref } }) => (
leaveTo="opacity-0" <Input
> id="name"
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> name="name"
</Transition.Child> type="name"
value={value}
<div className="fixed inset-0 z-20 overflow-y-auto"> onChange={onChange}
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> ref={ref}
<Transition.Child hasError={Boolean(errors.name)}
as={React.Fragment} placeholder="Title"
enter="ease-out duration-300" className="w-full text-base"
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" </div>
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" <div>
> <Controller
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> name="description"
<form onSubmit={handleSubmit(onSubmit)}> control={control}
<div className="space-y-3"> render={({ field: { value, onChange } }) => (
<div className="text-lg font-medium leading-6">{data ? "Update" : "Create"} Estimate</div> <TextArea
<div> id="description"
<Controller name="description"
control={control} value={value}
name="name" placeholder="Description"
render={({ field: { value, onChange, ref } }) => ( onChange={onChange}
<Input className="w-full text-base resize-none min-h-24"
id="name" hasError={Boolean(errors?.description)}
name="name" />
type="name" )}
value={value} />
onChange={onChange}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Title"
className="w-full resize-none text-xl"
/>
)}
/>
</div>
<div>
<Controller
name="description"
control={control}
render={({ field: { value, onChange } }) => (
<TextArea
id="description"
name="description"
value={value}
placeholder="Description"
onChange={onChange}
className="mt-3 min-h-32 resize-none text-sm"
hasError={Boolean(errors?.description)}
/>
)}
/>
</div>
{/* list of all the points */}
{/* since they are all the same, we can use a loop to render them */}
<div className="grid grid-cols-3 gap-3">
{Array(6)
.fill(0)
.map((_, i) => (
<div className="flex items-center" key={i}>
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
<span className="rounded-lg px-2 text-sm text-custom-text-200">{i + 1}</span>
<span className="rounded-r-lg bg-custom-background-100">
<Controller
control={control}
name={`value${i + 1}` as keyof FormValues}
rules={{
maxLength: {
value: 20,
message: "Estimate point must at most be of 20 characters",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
ref={ref}
type="text"
value={value}
onChange={onChange}
id={`value${i + 1}`}
name={`value${i + 1}`}
placeholder={`Point ${i + 1}`}
className="w-full rounded-l-none"
hasError={Boolean(errors[`value${i + 1}` as keyof FormValues])}
/>
)}
/>
</span>
</span>
</div>
))}
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{data
? isSubmitting
? "Updating Estimate..."
: "Update Estimate"
: isSubmitting
? "Creating Estimate..."
: "Create Estimate"}
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</div> </div>
</Dialog> {/* list of all the points */}
</Transition.Root> {/* since they are all the same, we can use a loop to render them */}
</> <div className="grid grid-cols-3 gap-3">
{Array(6)
.fill(0)
.map((_, i) => (
<div className="flex items-center" key={i}>
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
<span className="rounded-lg px-2 text-sm text-custom-text-200">{i + 1}</span>
<span className="rounded-r-lg bg-custom-background-100">
<Controller
control={control}
name={`value${i + 1}` as keyof FormValues}
rules={{
maxLength: {
value: 20,
message: "Estimate point must at most be of 20 characters",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
ref={ref}
type="text"
value={value}
onChange={onChange}
id={`value${i + 1}`}
name={`value${i + 1}`}
placeholder={`Point ${i + 1}`}
className="w-full rounded-l-none"
hasError={Boolean(errors[`value${i + 1}` as keyof FormValues])}
/>
)}
/>
</span>
</span>
</div>
))}
</div>
</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}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{data ? (isSubmitting ? "Updating" : "Update Estimate") : isSubmitting ? "Creating" : "Create Estimate"}
</Button>
</div>
</form>
</ModalCore>
); );
}); });

View File

@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { IEstimate } from "@plane/types";
// store hooks
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { useEstimate } from "@/hooks/store";
// types // types
import { IEstimate } from "@plane/types";
// ui // ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// hooks
import { useEstimate } from "@/hooks/store";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -26,15 +26,16 @@ export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
// store hooks // store hooks
const { deleteEstimate } = useEstimate(); const { deleteEstimate } = useEstimate();
const handleEstimateDelete = () => { const handleEstimateDelete = async () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
setIsDeleteLoading(true);
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
const estimateId = data?.id!; const estimateId = data?.id!;
deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) await deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId)
.then(() => { .then(() => {
setIsDeleteLoading(false);
handleClose(); handleClose();
}) })
.catch((err) => { .catch((err) => {
@ -46,7 +47,8 @@ export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: errorString ?? "Estimate could not be deleted. Please try again", message: errorString ?? "Estimate could not be deleted. Please try again",
}); });
}); })
.finally(() => setIsDeleteLoading(false));
}; };
useEffect(() => { useEffect(() => {
@ -59,72 +61,19 @@ export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={onClose}> handleClose={onClose}
<Transition.Child handleSubmit={handleEstimateDelete}
as={React.Fragment} isDeleting={isDeleteLoading}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete Estimate"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete estimate-{" "}
leaveTo="opacity-0" <span className="break-words font-medium text-custom-text-100">{data?.name}</span>
> {""}? All of the data related to the estiamte will be permanently removed. This action cannot be undone.
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> </>
</Transition.Child> }
/>
<div className="fixed inset-0 z-10 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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Estimate</h3>
</span>
</div>
<span>
<p className="break-words text-sm leading-7 text-custom-text-200">
Are you sure you want to delete estimate-{" "}
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>
{""}? All of the data related to the estiamte will be permanently removed. This action cannot be
undone.
</p>
</span>
<div className="flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Cancel
</Button>
<Button
variant="danger"
size="sm"
tabIndex={1}
onClick={() => {
setIsDeleteLoading(true);
handleEstimateDelete();
}}
loading={isDeleteLoading}
>
{isDeleteLoading ? "Deleting..." : "Delete Estimate"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,7 +1,9 @@
import { FC, FormEvent, useCallback, useRef, useState } from "react"; import { FC, FormEvent, useCallback, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// editor
import { EditorRefApi } from "@plane/rich-text-editor"; import { EditorRefApi } from "@plane/rich-text-editor";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
@ -120,31 +122,37 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
if (!workspaceSlug || !projectId || !workspaceId) return <></>; if (!workspaceSlug || !projectId || !workspaceId) return <></>;
return ( return (
<form className="relative space-y-4" onSubmit={handleFormSubmit}> <form onSubmit={handleFormSubmit}>
<InboxIssueTitle <div className="space-y-5 p-5">
data={formData} <h3 className="text-xl font-medium text-custom-text-200">Create Inbox Issue</h3>
handleData={handleFormData} <div className="space-y-3">
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character} <InboxIssueTitle
/> data={formData}
<InboxIssueDescription handleData={handleFormData}
workspaceSlug={workspaceSlug} isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
projectId={projectId} />
workspaceId={workspaceId} <InboxIssueDescription
data={formData} workspaceSlug={workspaceSlug}
handleData={handleFormData} projectId={projectId}
editorRef={descriptionEditorRef} workspaceId={workspaceId}
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]" data={formData}
/> handleData={handleFormData}
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} /> editorRef={descriptionEditorRef}
<div className="relative flex justify-between items-center gap-3"> containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
/>
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
</div>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
<div <div
className="flex cursor-pointer items-center gap-1.5" className="inline-flex items-center gap-1.5 cursor-pointer"
onClick={() => setCreateMore((prevData) => !prevData)} onClick={() => setCreateMore((prevData) => !prevData)}
role="button"
> >
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" /> <ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
<span className="text-xs">Create more</span> <span className="text-xs">Create more</span>
</div> </div>
<div className="relative flex items-center gap-3"> <div className="flex items-center gap-3">
<Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}> <Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
Discard Discard
</Button> </Button>
@ -155,7 +163,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
loading={formSubmitting} loading={formSubmitting}
disabled={isTitleLengthMoreThan255Character} disabled={isTitleLengthMoreThan255Character}
> >
{formSubmitting ? "Adding Issue..." : "Add Issue"} {formSubmitting ? "Creating" : "Create Issue"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,8 +1,11 @@
import { FC, useCallback, useEffect, useRef, useState } from "react"; import { FC, useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// editor
import { EditorRefApi } from "@plane/rich-text-editor"; import { EditorRefApi } from "@plane/rich-text-editor";
// types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { import {
@ -15,7 +18,7 @@ import { ISSUE_UPDATED } from "@/constants/event-tracker";
// helpers // helpers
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks // hooks
import { useEventTracker, useInboxIssues, useWorkspace } from "@/hooks/store"; import { useEventTracker, useInboxIssues, useProject, useWorkspace } from "@/hooks/store";
type TInboxIssueEditRoot = { type TInboxIssueEditRoot = {
workspaceSlug: string; workspaceSlug: string;
@ -31,8 +34,9 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
const router = useRouter(); const router = useRouter();
// refs // refs
const descriptionEditorRef = useRef<EditorRefApi>(null); const descriptionEditorRef = useRef<EditorRefApi>(null);
// hooks // store hooks
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const { currentProjectDetails } = useProject();
const { updateProjectIssue } = useInboxIssues(issueId); const { updateProjectIssue } = useInboxIssues(issueId);
const { getWorkspaceBySlug } = useWorkspace(); const { getWorkspaceBySlug } = useWorkspace();
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id; const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
@ -125,23 +129,30 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
if (!workspaceSlug || !projectId || !workspaceId || !formData) return <></>; if (!workspaceSlug || !projectId || !workspaceId || !formData) return <></>;
return ( return (
<div className="relative space-y-4"> <>
<InboxIssueTitle <div className="space-y-5 p-5">
data={formData} <h3 className="text-xl font-medium text-custom-text-200">
handleData={handleFormData} Move {currentProjectDetails?.identifier}-{issue?.sequence_id} to project issues
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character} </h3>
/> <div className="space-y-3">
<InboxIssueDescription <InboxIssueTitle
workspaceSlug={workspaceSlug} data={formData}
projectId={projectId} handleData={handleFormData}
workspaceId={workspaceId} isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
data={formData} />
handleData={handleFormData} <InboxIssueDescription
editorRef={descriptionEditorRef} workspaceSlug={workspaceSlug}
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]" projectId={projectId}
/> workspaceId={workspaceId}
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible /> data={formData}
<div className="relative flex justify-end items-center gap-3"> handleData={handleFormData}
editorRef={descriptionEditorRef}
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
/>
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
</div>
</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" type="button" onClick={handleModalClose}> <Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
Cancel Cancel
</Button> </Button>
@ -153,9 +164,9 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
disabled={isTitleLengthMoreThan255Character} disabled={isTitleLengthMoreThan255Character}
onClick={handleFormSubmit} onClick={handleFormSubmit}
> >
{formSubmitting ? "Adding..." : "Add to project"} {formSubmitting ? "Adding" : "Add to project"}
</Button> </Button>
</div> </div>
</div> </>
); );
}); });

View File

@ -32,19 +32,18 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
<Loader.Item width="100%" height="140px" /> <Loader.Item width="100%" height="140px" />
</Loader> </Loader>
); );
return ( return (
<div className="relative"> <RichTextEditor
<RichTextEditor initialValue={!data?.description_html || data?.description_html === "" ? "<p></p>" : data?.description_html}
initialValue={!data?.description_html || data?.description_html === "" ? "<p></p>" : data?.description_html} ref={editorRef}
ref={editorRef} workspaceSlug={workspaceSlug}
workspaceSlug={workspaceSlug} workspaceId={workspaceId}
workspaceId={workspaceId} projectId={projectId}
projectId={projectId} dragDropEnabled={false}
dragDropEnabled={false} onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)} placeholder={getDescriptionPlaceholder}
placeholder={getDescriptionPlaceholder} containerClassName={containerClassName}
containerClassName={containerClassName} />
/>
</div>
); );
}); });

View File

@ -13,7 +13,7 @@ export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
const { data, handleData, isTitleLengthMoreThan255Character } = props; const { data, handleData, isTitleLengthMoreThan255Character } = props;
return ( return (
<div className="relative flex flex-wrap gap-2 items-center"> <div className="space-y-1">
<Input <Input
id="name" id="name"
name="name" name="name"
@ -21,7 +21,7 @@ export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
value={data?.name} value={data?.name}
onChange={(e) => handleData("name", e.target.value)} onChange={(e) => handleData("name", e.target.value)}
placeholder="Title" placeholder="Title"
className="w-full resize-none text-xl" className="w-full text-base"
required required
/> />
{isTitleLengthMoreThan255Character && ( {isTitleLengthMoreThan255Character && (

View File

@ -1,11 +1,9 @@
import { FC, Fragment } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; // types
import { Transition, Dialog } from "@headlessui/react";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// components // components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { InboxIssueCreateRoot, InboxIssueEditRoot } from "@/components/inbox/modals/create-edit-modal"; import { InboxIssueCreateRoot, InboxIssueEditRoot } from "@/components/inbox/modals/create-edit-modal";
// hooks
import { useProject } from "@/hooks/store";
type TInboxIssueCreateEditModalRoot = { type TInboxIssueCreateEditModalRoot = {
workspaceSlug: string; workspaceSlug: string;
@ -16,69 +14,28 @@ type TInboxIssueCreateEditModalRoot = {
onSubmit?: () => void; onSubmit?: () => void;
}; };
export const InboxIssueCreateEditModalRoot: FC<TInboxIssueCreateEditModalRoot> = observer((props) => { export const InboxIssueCreateEditModalRoot: FC<TInboxIssueCreateEditModalRoot> = (props) => {
const { workspaceSlug, projectId, modalState, handleModalClose, issue, onSubmit } = props; const { workspaceSlug, projectId, modalState, handleModalClose, issue, onSubmit } = props;
// hooks
const { currentProjectDetails } = useProject();
return ( return (
<div> <ModalCore
<Transition.Root show={modalState} as={Fragment}> isOpen={modalState}
<Dialog as="div" className="relative z-20" onClose={handleModalClose}> handleClose={handleModalClose}
<Transition.Child position={EModalPosition.TOP}
as={Fragment} width={EModalWidth.XXXXL}
enter="ease-out duration-300" >
enterFrom="opacity-0" {issue && issue?.id ? (
enterTo="opacity-100" <InboxIssueEditRoot
leave="ease-in duration-200" workspaceSlug={workspaceSlug}
leaveFrom="opacity-100" projectId={projectId}
leaveTo="opacity-0" issueId={issue.id}
> issue={issue}
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> handleModalClose={handleModalClose}
</Transition.Child> onSubmit={onSubmit}
/>
<div className="fixed inset-0 z-10 overflow-y-auto"> ) : (
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20"> <InboxIssueCreateRoot workspaceSlug={workspaceSlug} projectId={projectId} handleModalClose={handleModalClose} />
<Transition.Child )}
as={Fragment} </ModalCore>
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 rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all w-full lg:max-w-4xl">
{issue && issue?.id ? (
<div className="space-y-4">
<h3 className="text-xl font-medium text-custom-text-100">
Move {currentProjectDetails?.identifier}-{issue?.sequence_id} to project issues
</h3>
<InboxIssueEditRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issue.id}
issue={issue}
handleModalClose={handleModalClose}
onSubmit={onSubmit}
/>
</div>
) : (
<div className="space-y-4">
<h3 className="text-xl font-medium text-custom-text-100">Create Inbox Issue</h3>
<InboxIssueCreateRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
handleModalClose={handleModalClose}
/>
</div>
)}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</div>
); );
}); };

View File

@ -1,11 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import type { TIssue } from "@plane/types";
// icons
// ui
import { Button } from "@plane/ui";
// types // types
import type { TIssue } from "@plane/types";
// components
import { AlertModalCore } from "@/components/core";
// hooks
import { useProject } from "@/hooks/store"; import { useProject } from "@/hooks/store";
type Props = { type Props = {
@ -15,81 +13,46 @@ type Props = {
onSubmit: () => Promise<void>; onSubmit: () => Promise<void>;
}; };
export const DeclineIssueModal: React.FC<Props> = ({ isOpen, onClose, data, onSubmit }) => { export const DeclineIssueModal: React.FC<Props> = (props) => {
const { isOpen, onClose, data, onSubmit } = props;
// states
const [isDeclining, setIsDeclining] = useState(false); const [isDeclining, setIsDeclining] = useState(false);
// hooks // store hooks
const { getProjectById } = useProject(); const { getProjectById } = useProject();
// derived values
const projectDetails = data.project_id ? getProjectById(data?.project_id) : undefined;
const handleClose = () => { const handleClose = () => {
setIsDeclining(false); setIsDeclining(false);
onClose(); onClose();
}; };
const handleDecline = () => { const handleDecline = async () => {
setIsDeclining(true); setIsDeclining(true);
onSubmit().finally(() => setIsDeclining(false)); await onSubmit().finally(() => setIsDeclining(false));
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDecline}
as={React.Fragment} isDeleting={isDeclining}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Decline Issue"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" {" "}
leaveTo="opacity-0" Are you sure you want to decline issue{" "}
> <span className="break-words font-medium text-custom-text-100">
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> {projectDetails?.identifier}-{data?.sequence_id}
</Transition.Child> </span>
{""}? This action cannot be undone.
<div className="fixed inset-0 z-10 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 primaryButtonText={{
as={React.Fragment} loading: "Declining",
enter="ease-out duration-300" default: "Decline",
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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Decline Issue</h3>
</span>
</div>
<span>
<p className="text-sm text-custom-text-200">
Are you sure you want to decline issue{" "}
<span className="break-words font-medium text-custom-text-100">
{(data && data?.project_id && getProjectById(data?.project_id)?.identifier) || ""}-
{data?.sequence_id}
</span>
{""}? This action cannot be undone.
</p>
</span>
<div className="flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDecline} loading={isDeclining}>
{isDeclining ? "Declining..." : "Decline Issue"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}; };

View File

@ -1,14 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// hooks
// icons
import type { TIssue } from "@plane/types";
// ui
import { Button } from "@plane/ui";
import { useProject } from "@/hooks/store";
// types // types
import type { TIssue } from "@plane/types";
// components
import { AlertModalCore } from "@/components/core";
// hooks
import { useProject } from "@/hooks/store";
type Props = { type Props = {
data: Partial<TIssue>; data: Partial<TIssue>;
@ -20,79 +17,37 @@ type Props = {
export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, onSubmit, data }) => { export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, onSubmit, data }) => {
// states // states
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
// store hooks
const { getProjectById } = useProject(); const { getProjectById } = useProject();
// derived values
const projectDetails = data.project_id ? getProjectById(data?.project_id) : undefined;
const handleClose = () => { const handleClose = () => {
setIsDeleting(false); setIsDeleting(false);
onClose(); onClose();
}; };
const handleDelete = () => { const handleDelete = async () => {
setIsDeleting(true); setIsDeleting(true);
onSubmit().finally(() => handleClose()); await onSubmit().finally(() => handleClose());
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDelete}
as={React.Fragment} isDeleting={isDeleting}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete Issue"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete issue{" "}
leaveTo="opacity-0" <span className="break-words font-medium text-custom-text-100">
> {projectDetails?.identifier}-{data?.sequence_id}
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> </span>
</Transition.Child> {""}? The issue will only be deleted from the inbox and this action cannot be undone.
</>
<div className="fixed inset-0 z-10 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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
</span>
</div>
<span>
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{(data && data?.project_id && getProjectById(data?.project_id)?.identifier) || ""}-
{data?.sequence_id}
</span>
{""}? The issue will only be deleted from the inbox and this action cannot be undone.
</p>
</span>
<div className="flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDelete} loading={isDeleting}>
{isDeleting ? "Deleting..." : "Delete Issue"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,10 +1,7 @@
import { FC, Fragment, useState } from "react"; import { FC, useState } from "react";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import type { TIssueAttachment } from "@plane/types"; import type { TIssueAttachment } from "@plane/types";
// headless ui // components
// ui import { AlertModalCore } from "@/components/core";
import { Button } from "@plane/ui";
// helper // helper
import { getFileName } from "@/helpers/attachment.helper"; import { getFileName } from "@/helpers/attachment.helper";
// types // types
@ -35,74 +32,19 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
}; };
return ( return (
data && ( <AlertModalCore
<Transition.Root show={isOpen} as={Fragment}> handleClose={handleClose}
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleSubmit={() => handleDeletion(data.id)}
<Transition.Child isDeleting={loader}
as={Fragment} isOpen={isOpen}
enter="ease-out duration-300" title="Delete attachment"
enterFrom="opacity-0" content={
enterTo="opacity-100" <>
leave="ease-in duration-200" Are you sure you want to delete attachment-{" "}
leaveFrom="opacity-100" <span className="font-bold">{getFileName(data.attributes.name)}</span>? This attachment will be permanently
leaveTo="opacity-0" removed. This action cannot be undone.
> </>
<div className="fixed inset-0 bg-custom-backdrop 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={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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete attachment
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you want to delete attachment-{" "}
<span className="font-bold">{getFileName(data.attributes.name)}</span>? This attachment will
be permanently removed. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button
variant="danger"
size="sm"
tabIndex={1}
onClick={() => {
handleDeletion(data.id);
}}
disabled={loader}
>
{loader ? "Deleting" : "Delete"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
); );
}; };

View File

@ -78,7 +78,7 @@ export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
Cancel Cancel
</Button> </Button>
<Button variant="primary" size="sm" onClick={handleDeletion} loading={isLoading}> <Button variant="primary" size="sm" onClick={handleDeletion} loading={isLoading}>
{isLoading ? "Saving..." : "Save Draft"} {isLoading ? "Saving" : "Save draft"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,12 +1,12 @@
import { useEffect, useState, Fragment } from "react"; import { useEffect, useState } from "react";
import { AlertTriangle } from "lucide-react"; // types
import { Dialog, Transition } from "@headlessui/react";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// types // components
import { useIssues, useProject } from "@/hooks/store"; import { AlertModalCore } from "@/components/core";
// hooks // hooks
import { useIssues, useProject } from "@/hooks/store";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -18,12 +18,10 @@ type Props = {
export const DeleteIssueModal: React.FC<Props> = (props) => { export const DeleteIssueModal: React.FC<Props> = (props) => {
const { dataId, data, isOpen, handleClose, onSubmit } = props; const { dataId, data, isOpen, handleClose, onSubmit } = props;
// states
const { issueMap } = useIssues();
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
// store hooks
// hooks const { issueMap } = useIssues();
const { getProjectById } = useProject(); const { getProjectById } = useProject();
useEffect(() => { useEffect(() => {
@ -32,7 +30,9 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
if (!dataId && !data) return null; if (!dataId && !data) return null;
// derived values
const issue = data ? data : issueMap[dataId!]; const issue = data ? data : issueMap[dataId!];
const projectDetails = getProjectById(issue?.project_id);
const onClose = () => { const onClose = () => {
setIsDeleting(false); setIsDeleting(false);
@ -57,65 +57,21 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={onClose}> handleClose={onClose}
<Transition.Child handleSubmit={handleIssueDelete}
as={Fragment} isDeleting={isDeleting}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete Issue"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete issue{" "}
leaveTo="opacity-0" <span className="break-words font-medium text-custom-text-100">
> {projectDetails?.identifier}-{issue?.sequence_id}
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> </span>
</Transition.Child> {""}? All of the data related to the issue will be permanently removed. This action cannot be undone.
</>
<div className="fixed inset-0 z-10 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={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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
</span>
</div>
<span>
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{getProjectById(issue?.project_id)?.identifier}-{issue?.sequence_id}
</span>
{""}? All of the data related to the issue will be permanently removed. This action cannot be
undone.
</p>
</span>
<div className="flex justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleIssueDelete} loading={isDeleting}>
{isDeleting ? "Deleting" : "Delete"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}; };

View File

@ -1,4 +1,4 @@
import React, { FC, useState, useRef, useEffect, Fragment } from "react"; import React, { FC, useState, useRef, useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -296,8 +296,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
/> />
)} )}
<form onSubmit={handleSubmit((data) => handleFormSubmit(data))}> <form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
<div className="space-y-5"> <div className="space-y-5 p-5">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-3">
{/* Don't show project selection if editing an issue */} {/* Don't show project selection if editing an issue */}
{!data?.id && ( {!data?.id && (
<Controller <Controller
@ -322,16 +322,14 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
)} )}
/> />
)} )}
<h3 className="text-xl font-semibold leading-6 text-custom-text-100"> <h3 className="text-xl font-medium text-custom-text-200">{data?.id ? "Update" : "Create"} Issue</h3>
{data?.id ? "Update" : "Create"} issue
</h3>
</div> </div>
{watch("parent_id") && selectedParentIssue && ( {watch("parent_id") && selectedParentIssue && (
<Controller <Controller
control={control} control={control}
name="parent_id" name="parent_id"
render={({ field: { onChange } }) => ( render={({ field: { onChange } }) => (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs"> <div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-90 p-2 text-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className="block h-1.5 w-1.5 rounded-full" className="block h-1.5 w-1.5 rounded-full"
@ -361,7 +359,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
/> />
)} )}
<div className="space-y-3"> <div className="space-y-3">
<div className="mt-2 space-y-3"> <div className="space-y-1">
<Controller <Controller
control={control} control={control}
name="name" name="name"
@ -384,376 +382,372 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
}} }}
ref={issueTitleRef || ref} ref={issueTitleRef || ref}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder="Issue Title" placeholder="Title"
className="w-full resize-none text-xl" className="w-full text-base"
tabIndex={getTabIndex("name")} tabIndex={getTabIndex("name")}
autoFocus autoFocus
/> />
)} )}
/> />
<span className="text-xs text-red-500">{errors?.name?.message}</span> <span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
<div className="relative"> <div className="relative">
{data?.description_html === undefined ? ( {data?.description_html === undefined ? (
<Loader className="min-h-[7rem] space-y-2 overflow-hidden rounded-md border border-custom-border-200 p-2 py-2"> <Loader className="min-h-[7rem] space-y-2 overflow-hidden rounded-md border border-custom-border-200 p-2 py-2">
<Loader.Item width="100%" height="26px" /> <Loader.Item width="100%" height="26px" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader.Item width="26px" height="26px" /> <Loader.Item width="26px" height="26px" />
<Loader.Item width="400px" height="26px" /> <Loader.Item width="400px" height="26px" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader.Item width="26px" height="26px" /> <Loader.Item width="26px" height="26px" />
<Loader.Item width="400px" height="26px" /> <Loader.Item width="400px" height="26px" />
</div> </div>
<Loader.Item width="80%" height="26px" /> <Loader.Item width="80%" height="26px" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader.Item width="50%" height="26px" /> <Loader.Item width="50%" height="26px" />
</div> </div>
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2"> <div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2">
<Loader.Item width="100px" height="26px" /> <Loader.Item width="100px" height="26px" />
<Loader.Item width="50px" height="26px" /> <Loader.Item width="50px" height="26px" />
</div> </div>
</Loader> </Loader>
) : ( ) : (
<Fragment> <>
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2"> <div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2">
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && ( {issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
<button
type="button"
className={`flex items-center gap-1 rounded bg-custom-background-80 px-1.5 py-1 text-xs ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
tabIndex={getTabIndex("feeling_lucky")}
>
{iAmFeelingLucky ? (
"Generating response"
) : (
<>
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
</>
)}
</button>
)}
{envConfig?.has_openai_configured && (
<GptAssistantPopover
isOpen={gptAssistantModal}
projectId={projectId}
handleClose={() => {
setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
/>
)}
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => (
<RichTextEditor
initialValue={value}
value={data.description_html}
workspaceSlug={workspaceSlug?.toString() as string}
workspaceId={workspaceId}
projectId={projectId}
onChange={(_description: object, description_html: string) => {
onChange(description_html);
handleFormChange();
}}
ref={editorRef}
tabIndex={getTabIndex("description_html")}
placeholder={getDescriptionPlaceholder}
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
/>
)}
/>
</Fragment>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<Controller
control={control}
name="state_id"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<StateDropdown
value={value}
onChange={(stateId) => {
onChange(stateId);
handleFormChange();
}}
projectId={projectId}
buttonVariant="border-with-text"
tabIndex={getTabIndex("state_id")}
/>
</div>
)}
/>
<Controller
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<PriorityDropdown
value={value}
onChange={(priority) => {
onChange(priority);
handleFormChange();
}}
buttonVariant="border-with-text"
tabIndex={getTabIndex("priority")}
/>
</div>
)}
/>
<Controller
control={control}
name="assignee_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<MemberDropdown
projectId={projectId}
value={value}
onChange={(assigneeIds) => {
onChange(assigneeIds);
handleFormChange();
}}
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
placeholder="Assignees"
multiple
tabIndex={getTabIndex("assignee_ids")}
/>
</div>
)}
/>
<Controller
control={control}
name="label_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={(labelIds) => {
onChange(labelIds);
handleFormChange();
}}
projectId={projectId}
tabIndex={getTabIndex("label_ids")}
/>
</div>
)}
/>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
maxDate={maxDate ?? undefined}
placeholder="Start date"
tabIndex={getTabIndex("start_date")}
/>
</div>
)}
/>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
minDate={minDate ?? undefined}
placeholder="Due date"
tabIndex={getTabIndex("target_date")}
/>
</div>
)}
/>
{projectDetails?.cycle_view && (
<Controller
control={control}
name="cycle_id"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<CycleDropdown
projectId={projectId}
onChange={(cycleId) => {
onChange(cycleId);
handleFormChange();
}}
placeholder="Cycle"
value={value}
buttonVariant="border-with-text"
tabIndex={getTabIndex("cycle_id")}
/>
</div>
)}
/>
)}
{projectDetails?.module_view && workspaceSlug && (
<Controller
control={control}
name="module_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ModuleDropdown
projectId={projectId}
value={value ?? []}
onChange={(moduleIds) => {
onChange(moduleIds);
handleFormChange();
}}
placeholder="Modules"
buttonVariant="border-with-text"
tabIndex={getTabIndex("module_ids")}
multiple
showCount
/>
</div>
)}
/>
)}
{areEstimatesEnabledForProject(projectId) && (
<Controller
control={control}
name="estimate_point"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<EstimateDropdown
value={value}
onChange={(estimatePoint) => {
onChange(estimatePoint);
handleFormChange();
}}
projectId={projectId}
buttonVariant="border-with-text"
tabIndex={getTabIndex("estimate_point")}
placeholder="Estimate"
/>
</div>
)}
/>
)}
{watch("parent_id") ? (
<CustomMenu
customButton={
<button <button
type="button" type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80" className={`flex items-center gap-1 rounded bg-custom-background-90 px-1.5 py-1 text-xs ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
tabIndex={getTabIndex("feeling_lucky")}
> >
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" /> {iAmFeelingLucky ? (
<span className="whitespace-nowrap"> "Generating response"
{selectedParentIssue && ) : (
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`} <>
</span> <Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
</button> </>
}
placement="bottom-start"
tabIndex={getTabIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<Controller
control={control}
name="parent_id"
render={({ field: { onChange } }) => (
<CustomMenu.MenuItem
className="!p-1"
onClick={() => {
onChange(null);
handleFormChange();
}}
>
Remove parent issue
</CustomMenu.MenuItem>
)} )}
</button>
)}
{envConfig?.has_openai_configured && (
<GptAssistantPopover
isOpen={gptAssistantModal}
projectId={projectId}
handleClose={() => {
setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
/> />
</> )}
</CustomMenu> </div>
) : ( <Controller
<button name="description_html"
type="button" control={control}
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80" render={({ field: { value, onChange } }) => (
onClick={() => setParentIssueListModalOpen(true)} <RichTextEditor
> initialValue={value}
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" /> value={data.description_html}
<span className="whitespace-nowrap">Add parent</span> workspaceSlug={workspaceSlug?.toString() as string}
</button> workspaceId={workspaceId}
)} projectId={projectId}
<Controller onChange={(_description: object, description_html: string) => {
control={control} onChange(description_html);
name="parent_id" handleFormChange();
render={({ field: { onChange } }) => ( }}
<ParentIssuesListModal ref={editorRef}
isOpen={parentIssueListModalOpen} tabIndex={getTabIndex("description_html")}
handleClose={() => setParentIssueListModalOpen(false)} placeholder={getDescriptionPlaceholder}
onChange={(issue) => { containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
onChange(issue.id); />
)}
/>
</>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<Controller
control={control}
name="state_id"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<StateDropdown
value={value}
onChange={(stateId) => {
onChange(stateId);
handleFormChange(); handleFormChange();
setSelectedParentIssue(issue);
}} }}
projectId={projectId} projectId={projectId}
issueId={isDraft ? undefined : data?.id} buttonVariant="border-with-text"
tabIndex={getTabIndex("state_id")}
/> />
</div>
)}
/>
<Controller
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<PriorityDropdown
value={value}
onChange={(priority) => {
onChange(priority);
handleFormChange();
}}
buttonVariant="border-with-text"
tabIndex={getTabIndex("priority")}
/>
</div>
)}
/>
<Controller
control={control}
name="assignee_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<MemberDropdown
projectId={projectId}
value={value}
onChange={(assigneeIds) => {
onChange(assigneeIds);
handleFormChange();
}}
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
placeholder="Assignees"
multiple
tabIndex={getTabIndex("assignee_ids")}
/>
</div>
)}
/>
<Controller
control={control}
name="label_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={(labelIds) => {
onChange(labelIds);
handleFormChange();
}}
projectId={projectId}
tabIndex={getTabIndex("label_ids")}
/>
</div>
)}
/>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
maxDate={maxDate ?? undefined}
placeholder="Start date"
tabIndex={getTabIndex("start_date")}
/>
</div>
)}
/>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
minDate={minDate ?? undefined}
placeholder="Due date"
tabIndex={getTabIndex("target_date")}
/>
</div>
)}
/>
{projectDetails?.cycle_view && (
<Controller
control={control}
name="cycle_id"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<CycleDropdown
projectId={projectId}
onChange={(cycleId) => {
onChange(cycleId);
handleFormChange();
}}
placeholder="Cycle"
value={value}
buttonVariant="border-with-text"
tabIndex={getTabIndex("cycle_id")}
/>
</div>
)} )}
/> />
</div> )}
{projectDetails?.module_view && workspaceSlug && (
<Controller
control={control}
name="module_ids"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ModuleDropdown
projectId={projectId}
value={value ?? []}
onChange={(moduleIds) => {
onChange(moduleIds);
handleFormChange();
}}
placeholder="Modules"
buttonVariant="border-with-text"
tabIndex={getTabIndex("module_ids")}
multiple
showCount
/>
</div>
)}
/>
)}
{areEstimatesEnabledForProject(projectId) && (
<Controller
control={control}
name="estimate_point"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<EstimateDropdown
value={value}
onChange={(estimatePoint) => {
onChange(estimatePoint);
handleFormChange();
}}
projectId={projectId}
buttonVariant="border-with-text"
tabIndex={getTabIndex("estimate_point")}
placeholder="Estimate"
/>
</div>
)}
/>
)}
{watch("parent_id") ? (
<CustomMenu
customButton={
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">
{selectedParentIssue &&
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`}
</span>
</button>
}
placement="bottom-start"
tabIndex={getTabIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<Controller
control={control}
name="parent_id"
render={({ field: { onChange } }) => (
<CustomMenu.MenuItem
className="!p-1"
onClick={() => {
onChange(null);
handleFormChange();
}}
>
Remove parent issue
</CustomMenu.MenuItem>
)}
/>
</>
</CustomMenu>
) : (
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueListModalOpen(true)}
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add parent</span>
</button>
)}
<Controller
control={control}
name="parent_id"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={parentIssueListModalOpen}
handleClose={() => setParentIssueListModalOpen(false)}
onChange={(issue) => {
onChange(issue.id);
handleFormChange();
setSelectedParentIssue(issue);
}}
projectId={projectId}
issueId={isDraft ? undefined : data?.id}
/>
)}
/>
</div> </div>
</div> </div>
</div> </div>
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-100 px-5 pt-5"> <div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
<div> <div>
{!data?.id && ( {!data?.id && (
<div <div
className="inline-flex cursor-default items-center gap-1.5" className="inline-flex items-center gap-1.5 cursor-pointer"
onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)} onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled); if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
}} }}
tabIndex={getTabIndex("create_more")} tabIndex={getTabIndex("create_more")}
role="button"
> >
<div className="flex cursor-pointer items-center justify-center"> <ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
</div>
<span className="text-xs">Create more</span> <span className="text-xs">Create more</span>
</div> </div>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={getTabIndex("discard_button")}> <Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={getTabIndex("discard_button")}>
Discard Discard
</Button> </Button>
{isDraft && ( {isDraft && (
<Fragment> <>
{data?.id ? ( {data?.id ? (
<Button <Button
variant="neutral-primary" variant="neutral-primary"
@ -775,9 +769,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
{isSubmitting ? "Saving" : "Save as draft"} {isSubmitting ? "Saving" : "Save as draft"}
</Button> </Button>
)} )}
</Fragment> </>
)} )}
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
@ -785,7 +778,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
loading={isSubmitting} loading={isSubmitting}
tabIndex={isDraft ? getTabIndex("submit_button") : getTabIndex("draft_button")} tabIndex={isDraft ? getTabIndex("submit_button") : getTabIndex("draft_button")}
> >
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"} {data?.id ? (isSubmitting ? "Updating" : "Update Issue") : isSubmitting ? "Creating" : "Create Issue"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,13 +1,16 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react"; // types
import type { TIssue } from "@plane/types"; import type { TIssue } from "@plane/types";
// hooks // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
// constants
import { ISSUE_CREATED, ISSUE_UPDATED } from "@/constants/event-tracker"; import { ISSUE_CREATED, ISSUE_UPDATED } from "@/constants/event-tracker";
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
// hooks
import { import {
useApplication, useApplication,
useEventTracker, useEventTracker,
@ -22,9 +25,6 @@ import useLocalStorage from "@/hooks/use-local-storage";
// components // components
import { DraftIssueLayout } from "./draft-issue-layout"; import { DraftIssueLayout } from "./draft-issue-layout";
import { IssueFormRoot } from "./form"; import { IssueFormRoot } from "./form";
// ui
// types
// constants
export interface IssuesModalProps { export interface IssuesModalProps {
data?: Partial<TIssue>; data?: Partial<TIssue>;
@ -241,72 +241,47 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
if (!workspaceProjectIds || workspaceProjectIds.length === 0 || !activeProjectId) return null; if (!workspaceProjectIds || workspaceProjectIds.length === 0 || !activeProjectId) return null;
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore
<Dialog as="div" className="relative z-20" onClose={() => handleClose(true)}> isOpen={isOpen}
<Transition.Child handleClose={() => handleClose(true)}
as={React.Fragment} position={EModalPosition.TOP}
enter="ease-out duration-300" width={EModalWidth.XXXXL}
enterFrom="opacity-0" >
enterTo="opacity-100" {withDraftIssueWrapper ? (
leave="ease-in duration-200" <DraftIssueLayout
leaveFrom="opacity-100" changesMade={changesMade}
leaveTo="opacity-0" data={{
> ...data,
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" /> description_html: description,
</Transition.Child> cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
<div className="fixed inset-0 z-10 overflow-y-auto"> }}
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20"> issueTitleRef={issueTitleRef}
<Transition.Child onChange={handleFormChange}
as={React.Fragment} onClose={handleClose}
enter="ease-out duration-300" onSubmit={handleFormSubmit}
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" projectId={activeProjectId}
enterTo="opacity-100 translate-y-0 sm:scale-100" isCreateMoreToggleEnabled={createMore}
leave="ease-in duration-200" onCreateMoreToggleChange={handleCreateMoreToggleChange}
leaveFrom="opacity-100 translate-y-0 sm:scale-100" isDraft={isDraft}
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" />
> ) : (
<Dialog.Panel className="relative mx-4 transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-4xl"> <IssueFormRoot
{withDraftIssueWrapper ? ( issueTitleRef={issueTitleRef}
<DraftIssueLayout data={{
changesMade={changesMade} ...data,
data={{ description_html: description,
...data, cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
description_html: description, module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null, }}
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null, onClose={() => handleClose(false)}
}} isCreateMoreToggleEnabled={createMore}
issueTitleRef={issueTitleRef} onCreateMoreToggleChange={handleCreateMoreToggleChange}
onChange={handleFormChange} onSubmit={handleFormSubmit}
onClose={handleClose} projectId={activeProjectId}
onSubmit={handleFormSubmit} isDraft={isDraft}
projectId={activeProjectId} />
isCreateMoreToggleEnabled={createMore} )}
onCreateMoreToggleChange={handleCreateMoreToggleChange} </ModalCore>
isDraft={isDraft}
/>
) : (
<IssueFormRoot
issueTitleRef={issueTitleRef}
data={{
...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
}}
onClose={() => handleClose(false)}
isCreateMoreToggleEnabled={createMore}
onCreateMoreToggleChange={handleCreateMoreToggleChange}
onSubmit={handleFormSubmit}
projectId={activeProjectId}
isDraft={isDraft}
/>
)}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,15 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import type { IIssueLabel } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { useLabel } from "@/hooks/store";
// icons
// ui
// types // types
import type { IIssueLabel } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// hooks
import { useLabel } from "@/hooks/store";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -54,64 +53,18 @@ export const DeleteLabelModal: React.FC<Props> = observer((props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDeletion}
as={React.Fragment} isDeleting={isDeleteLoading}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete Label"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete <span className="font-medium text-custom-text-100">{data?.name}</span>? This
leaveTo="opacity-0" will remove the label from all the issue and from any views where the label is being filtered upon.
> </>
<div className="fixed inset-0 bg-custom-backdrop 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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete Label
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you wish to delete{" "}
<span className="font-medium text-custom-text-100">{data?.name}</span>? This will remove the
label from all the issue and from any views where the label is being filtered upon.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,17 +1,16 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import type { IModule } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { MODULE_DELETED } from "@/constants/event-tracker";
import { useEventTracker, useModule } from "@/hooks/store";
// ui
// icons
// types // types
import type { IModule } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// constants // constants
import { MODULE_DELETED } from "@/constants/event-tracker";
// hooks
import { useEventTracker, useModule } from "@/hooks/store";
type Props = { type Props = {
data: IModule; data: IModule;
@ -71,64 +70,19 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDeletion}
as={React.Fragment} isDeleting={isDeleteLoading}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete Module"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete module-{" "}
leaveTo="opacity-0" <span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the data related to
> the module will be permanently removed. This action cannot be undone.
<div className="fixed inset-0 bg-custom-backdrop 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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete Module
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you want to delete module-{" "}
<span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the
data related to the module will be permanently removed. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -64,7 +64,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
return ( return (
<form onSubmit={handleSubmit(handleCreateUpdateModule)}> <form onSubmit={handleSubmit(handleCreateUpdateModule)}>
<div className="space-y-5"> <div className="space-y-5 p-5">
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
{!status && ( {!status && (
<Controller <Controller
@ -86,11 +86,10 @@ export const ModuleForm: React.FC<Props> = (props) => {
)} )}
/> />
)} )}
<h3 className="text-xl font-medium leading-6 text-custom-text-200">{status ? "Update" : "New"} Module</h3> <h3 className="text-xl font-medium text-custom-text-200">{status ? "Update" : "Create"} Module</h3>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-col gap-1"> <div className="space-y-1">
<Controller <Controller
control={control} control={control}
name="name" name="name"
@ -101,18 +100,18 @@ export const ModuleForm: React.FC<Props> = (props) => {
message: "Title should be less than 255 characters", message: "Title should be less than 255 characters",
}, },
}} }}
render={({ field: { value, onChange, ref } }) => ( render={({ field: { value, onChange } }) => (
<Input <Input
id="name" id="name"
name="name" name="name"
type="text" type="text"
value={value} value={value}
onChange={onChange} onChange={onChange}
ref={ref}
hasError={Boolean(errors?.name)} hasError={Boolean(errors?.name)}
placeholder="Module Title" placeholder="Title"
className="w-full resize-none placeholder:text-sm placeholder:font-medium focus:border-blue-400" className="w-full text-base"
tabIndex={1} tabIndex={1}
autoFocus
/> />
)} )}
/> />
@ -128,8 +127,8 @@ export const ModuleForm: React.FC<Props> = (props) => {
name="description" name="description"
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder="Description..." placeholder="Description"
className="w-full text-sm resize-none min-h-24" className="w-full text-base resize-none min-h-24"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
tabIndex={2} tabIndex={2}
/> />
@ -211,12 +210,12 @@ export const ModuleForm: React.FC<Props> = (props) => {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-5 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200 pt-5"> <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} tabIndex={7}> <Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={7}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={8}> <Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={8}>
{status ? (isSubmitting ? "Updating" : "Update module") : isSubmitting ? "Creating" : "Create module"} {status ? (isSubmitting ? "Updating" : "Update Module") : isSubmitting ? "Creating" : "Create Module"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -1,17 +1,17 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react"; // types
import type { IModule } from "@plane/types"; import type { IModule } from "@plane/types";
// components // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { ModuleForm } from "@/components/modules"; import { ModuleForm } from "@/components/modules";
// constants
import { MODULE_CREATED, MODULE_UPDATED } from "@/constants/event-tracker"; import { MODULE_CREATED, MODULE_UPDATED } from "@/constants/event-tracker";
// hooks // hooks
import { useEventTracker, useModule, useProject } from "@/hooks/store"; import { useEventTracker, useModule, useProject } from "@/hooks/store";
// ui
// components
// types
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -140,45 +140,15 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
}, [activeProject, data, projectId, workspaceProjectIds, isOpen]); }, [activeProject, data, projectId, workspaceProjectIds, isOpen]);
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <ModuleForm
<Transition.Child handleFormSubmit={handleFormSubmit}
as={React.Fragment} handleClose={handleClose}
enter="ease-out duration-300" status={data ? true : false}
enterFrom="opacity-0" projectId={activeProject ?? ""}
enterTo="opacity-100" setActiveProject={setActiveProject}
leave="ease-in duration-200" data={data}
leaveFrom="opacity-100" />
leaveTo="opacity-0" </ModalCore>
>
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<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 rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
<ModuleForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}
status={data ? true : false}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
data={data}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,9 +1,9 @@
import { FC, Fragment, useState } from "react"; import { FC, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react";
// types // types
import { TPage } from "@plane/types"; import { TPage } from "@plane/types";
// components // components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { PageForm } from "@/components/pages"; import { PageForm } from "@/components/pages";
// constants // constants
import { PAGE_CREATED } from "@/constants/event-tracker"; import { PAGE_CREATED } from "@/constants/event-tracker";
@ -67,43 +67,18 @@ export const CreatePageModal: FC<Props> = (props) => {
}; };
return ( return (
<Transition.Root show={isModalOpen} as={Fragment}> <ModalCore
<Dialog as="div" className="relative z-20" onClose={handleModalClose}> isOpen={isModalOpen}
<Transition.Child handleClose={handleModalClose}
as={Fragment} position={EModalPosition.TOP}
enter="ease-out duration-300" width={EModalWidth.XXL}
enterFrom="opacity-0" >
enterTo="opacity-100" <PageForm
leave="ease-in duration-200" formData={pageFormData}
leaveFrom="opacity-100" handleFormData={handlePageFormData}
leaveTo="opacity-0" handleModalClose={handleStateClear}
> handleFormSubmit={handleFormSubmit}
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" /> />
</Transition.Child> </ModalCore>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={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 rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-2xl">
<PageForm
formData={pageFormData}
handleFormData={handlePageFormData}
handleModalClose={handleStateClear}
handleFormSubmit={handleFormSubmit}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}; };

View File

@ -1,9 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// constants // constants
import { PAGE_DELETED } from "@/constants/event-tracker"; import { PAGE_DELETED } from "@/constants/event-tracker";
// hooks // hooks
@ -71,64 +71,19 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDelete}
as={React.Fragment} isDeleting={isDeleting}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete Page"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete page-{" "}
leaveTo="opacity-0" <span className="break-words font-medium text-custom-text-100">{name}</span>? The Page will be deleted
> permanently. This action cannot be undone.
<div className="fixed inset-0 bg-custom-backdrop 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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete Page
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you want to delete page-{" "}
<span className="break-words font-medium text-custom-text-100">{name}</span>? The Page will be
deleted permanently. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDelete} loading={isDeleting}>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -39,28 +39,27 @@ export const PageForm: React.FC<Props> = (props) => {
return ( return (
<form onSubmit={handlePageFormSubmit}> <form onSubmit={handlePageFormSubmit}>
<div className="space-y-4"> <div className="space-y-5 p-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Create Page</h3> <h3 className="text-xl font-medium text-custom-text-200">Create Page</h3>
<div className="space-y-1">
<div className="space-y-2">
<Input <Input
id="name" id="name"
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => handleFormData("name", e.target.value)} onChange={(e) => handleFormData("name", e.target.value)}
placeholder="Title" placeholder="Title"
className="w-full resize-none text-lg" className="w-full resize-none text-base"
tabIndex={1} tabIndex={1}
required required
autoFocus
/> />
{isTitleLengthMoreThan255Character && ( {isTitleLengthMoreThan255Character && (
<span className="text-xs text-red-500">Max length of the name should be less than 255 characters</span> <span className="text-xs text-red-500">Max length of the name should be less than 255 characters</span>
)} )}
</div> </div>
</div> </div>
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
<div className="mt-5 relative flex items-center justify-between gap-2"> <div className="flex items-center gap-2">
<div className="relative flex items-center gap-2">
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1"> <div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
{PAGE_ACCESS_SPECIFIERS.map((access, index) => ( {PAGE_ACCESS_SPECIFIERS.map((access, index) => (
<Tooltip key={access.key} tooltipContent={access.label} isMobile={isMobile}> <Tooltip key={access.key} tooltipContent={access.label} isMobile={isMobile}>
@ -88,8 +87,7 @@ export const PageForm: React.FC<Props> = (props) => {
{PAGE_ACCESS_SPECIFIERS.find((access) => access.key === formData.access)?.label} {PAGE_ACCESS_SPECIFIERS.find((access) => access.key === formData.access)?.label}
</h6> </h6>
</div> </div>
<div className="flex items-center justify-end gap-2">
<div className="relative flex items-center gap-2 justify-end">
<Button variant="neutral-primary" size="sm" onClick={handleModalClose} tabIndex={4}> <Button variant="neutral-primary" size="sm" onClick={handleModalClose} tabIndex={4}>
Cancel Cancel
</Button> </Button>

View File

@ -1,16 +1,16 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AlertTriangle } from "lucide-react"; // types
import { Dialog, Transition } from "@headlessui/react";
import type { IState } from "@plane/types"; import type { IState } from "@plane/types";
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// constants // constants
import { STATE_DELETED } from "@/constants/event-tracker"; import { STATE_DELETED } from "@/constants/event-tracker";
// hooks // hooks
import { useEventTracker, useProjectState } from "@/hooks/store"; import { useEventTracker, useProjectState } from "@/hooks/store";
// types
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -78,64 +78,18 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDeletion}
as={React.Fragment} isDeleting={isDeleteLoading}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete State"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete state- <span className="font-medium text-custom-text-100">{data?.name}</span>?
leaveTo="opacity-0" All of the data related to the state will be permanently removed. This action cannot be undone.
> </>
<div className="fixed inset-0 bg-custom-backdrop 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 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-custom-background-100 px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete State
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you want to delete state-{" "}
<span className="font-medium text-custom-text-100">{data?.name}</span>? All of the data
related to the state will be permanently removed. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 bg-custom-background-100 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,14 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AlertTriangle } from "lucide-react"; // types
import { Dialog, Transition } from "@headlessui/react";
import { IProjectView } from "@plane/types"; import { IProjectView } from "@plane/types";
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// hooks // hooks
import { useProjectView } from "@/hooks/store"; import { useProjectView } from "@/hooks/store";
// types
type Props = { type Props = {
data: IProjectView; data: IProjectView;
@ -59,64 +59,19 @@ export const DeleteProjectViewModal: React.FC<Props> = observer((props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDeleteView}
as={React.Fragment} isDeleting={isDeleteLoading}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete View"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete view-{" "}
leaveTo="opacity-0" <span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the data related to
> the view will be permanently removed. This action cannot be undone.
<div className="fixed inset-0 bg-custom-backdrop 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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete View
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you want to delete view-{" "}
<span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the
data related to the view will be permanently removed. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeleteView}>
{isDeleteLoading ? "Deleting..." : "Delete"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,16 +1,16 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { IProjectView, IIssueFilterOptions } from "@plane/types";
// hooks
import { Button, Input, TextArea } from "@plane/ui";
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
// components
// ui
// types // types
import { IProjectView, IIssueFilterOptions } from "@plane/types";
// ui
import { Button, Input, TextArea } from "@plane/ui";
// components
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// hooks
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
type Props = { type Props = {
data?: IProjectView | null; data?: IProjectView | null;
@ -109,10 +109,10 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
return ( return (
<form onSubmit={handleSubmit(handleCreateUpdateView)}> <form onSubmit={handleSubmit(handleCreateUpdateView)}>
<div className="space-y-5"> <div className="space-y-5 p-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{data ? "Update" : "Create"} View</h3> <h3 className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Create"} View</h3>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-col gap-1"> <div className="space-y-1">
<Controller <Controller
control={control} control={control}
name="name" name="name"
@ -132,8 +132,9 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
onChange={onChange} onChange={onChange}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder="Title" placeholder="Title"
className="w-full resize-none text-xl focus:border-blue-400" className="w-full text-base"
tabIndex={1} tabIndex={1}
autoFocus
/> />
)} )}
/> />
@ -148,7 +149,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
id="description" id="description"
name="description" name="description"
placeholder="Description" placeholder="Description"
className="min-h-24 w-full resize-none text-sm" className="w-full text-base resize-none min-h-24"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -206,18 +207,12 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
)} )}
</div> </div>
</div> </div>
<div className="mt-5 flex justify-end gap-2"> <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} tabIndex={4}> <Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" size="sm" type="submit" tabIndex={5} disabled={isSubmitting}> <Button variant="primary" size="sm" type="submit" tabIndex={5} loading={isSubmitting}>
{data {data ? (isSubmitting ? "Updating" : "Update View") : isSubmitting ? "Creating" : "Create View"}
? isSubmitting
? "Updating View..."
: "Update View"
: isSubmitting
? "Creating View..."
: "Create View"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -1,14 +1,14 @@
import { FC, Fragment } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; // types
import { IProjectView } from "@plane/types"; import { IProjectView } from "@plane/types";
// ui // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { ProjectViewForm } from "@/components/views"; import { ProjectViewForm } from "@/components/views";
// hooks // hooks
import { useProjectView } from "@/hooks/store"; import { useProjectView } from "@/hooks/store";
// types
type Props = { type Props = {
data?: IProjectView | null; data?: IProjectView | null;
@ -65,43 +65,13 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <ProjectViewForm
<Transition.Child data={data}
as={Fragment} handleClose={handleClose}
enter="ease-out duration-300" handleFormSubmit={handleFormSubmit}
enterFrom="opacity-0" preLoadedData={preLoadedData}
enterTo="opacity-100" />
leave="ease-in duration-200" </ModalCore>
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={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 rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<ProjectViewForm
data={data}
handleClose={handleClose}
handleFormSubmit={handleFormSubmit}
preLoadedData={preLoadedData}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,18 +1,18 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// ui // types
import { Dialog, Transition } from "@headlessui/react";
import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
// helpers // helpers
import { csvDownload } from "@/helpers/download.helper"; import { csvDownload } from "@/helpers/download.helper";
// types // components
import { WebhookForm } from "./form"; import { WebhookForm } from "./form";
import { GeneratedHookDetails } from "./generated-hook-details"; import { GeneratedHookDetails } from "./generated-hook-details";
// utils // utils
import { getCurrentHookAsCSV } from "./utils"; import { getCurrentHookAsCSV } from "./utils";
// ui
interface ICreateWebhookModal { interface ICreateWebhookModal {
currentWorkspace: IWorkspace | null; currentWorkspace: IWorkspace | null;
@ -93,48 +93,19 @@ export const CreateWebhookModal: React.FC<ICreateWebhookModal> = (props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore
<Dialog isOpen={isOpen}
as="div" handleClose={() => {
className="relative z-20" if (!generatedWebhook) handleClose();
onClose={() => { }}
if (!generatedWebhook) handleClose(); position={EModalPosition.TOP}
}} width={EModalWidth.XXL}
> >
<Transition.Child {!generatedWebhook ? (
as={React.Fragment} <WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} />
enter="ease-out duration-300" ) : (
enterFrom="opacity-0" <GeneratedHookDetails webhookDetails={generatedWebhook} handleClose={handleClose} />
enterTo="opacity-100" )}
leave="ease-in duration-200" </ModalCore>
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 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>
); );
}; };

View File

@ -1,9 +1,9 @@
import React, { FC, useState } from "react"; import React, { FC, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// hooks // hooks
import { useWebhook } from "@/hooks/store"; import { useWebhook } from "@/hooks/store";
@ -52,59 +52,18 @@ export const DeleteWebhookModal: FC<IDeleteWebhook> = (props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDelete}
as={React.Fragment} isDeleting={isDeleting}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete webhook"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete this webhook? Future events will not be delivered to this webhook. This action
leaveTo="opacity-0" cannot be undone.
> </>
<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">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete webhook</h3>
</span>
</div>
<p className="mt-4 text-sm text-custom-text-200">
Are you sure you want to delete this webhook? Future events will not be delivered to this webhook.
This action cannot be undone.
</p>
<div className="flex justify-end gap-2">
<Button variant="neutral-primary" onClick={onClose}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete} loading={isDeleting}>
{isDeleting ? "Deleting..." : "Delete webhook"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}; };

View File

@ -58,11 +58,11 @@ export const WebhookForm: FC<Props> = observer((props) => {
}, [data]); }, [data]);
return ( return (
<div className="space-y-6"> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="text-xl font-medium">{data ? "Webhook details" : "Create webhook"}</div> <div className="space-y-5 p-5">
<form onSubmit={handleSubmit(handleFormSubmit)}> <div className="text-xl font-medium text-custom-text-200">{data ? "Webhook details" : "Create webhook"}</div>
<div className="space-y-8"> <div className="space-y-3">
<div> <div className="space-y-1">
<Controller <Controller
control={control} control={control}
name="url" name="url"
@ -76,34 +76,31 @@ export const WebhookForm: FC<Props> = observer((props) => {
{errors.url && <div className="text-xs text-red-500">{errors.url.message}</div>} {errors.url && <div className="text-xs text-red-500">{errors.url.message}</div>}
</div> </div>
{data && <WebhookToggle control={control} />} {data && <WebhookToggle control={control} />}
<div className="space-y-3"> <WebhookOptions value={webhookEventType} onChange={(val) => setWebhookEventType(val)} />
<WebhookOptions value={webhookEventType} onChange={(val) => setWebhookEventType(val)} />
</div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
{webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />} {webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />}
</div> </div>
{data ? ( </div>
<div className="mt-8 space-y-8"> {data ? (
<WebhookSecretKey data={data} /> <div className="p-5 pt-0 space-y-5">
<WebhookSecretKey data={data} />
<Button type="submit" loading={isSubmitting}> <Button type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update"} {isSubmitting ? "Updating" : "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}>
Cancel
</Button>
{!webhookSecretKey && (
<Button type="submit" variant="primary" size="sm" loading={isSubmitting}>
{isSubmitting ? "Creating" : "Create"}
</Button> </Button>
</div> )}
) : ( </div>
<div className="mt-4 flex justify-end gap-2"> )}
<Button variant="neutral-primary" onClick={handleClose}> </form>
Discard
</Button>
{!webhookSecretKey && (
<Button type="submit" variant="primary" loading={isSubmitting}>
{isSubmitting ? "Creating..." : "Create"}
</Button>
)}
</div>
)}
</form>
</div>
); );
}); });

View File

@ -14,20 +14,22 @@ export const GeneratedHookDetails: React.FC<Props> = (props) => {
const { handleClose, webhookDetails } = props; const { handleClose, webhookDetails } = props;
return ( return (
<div> <>
<div className="space-y-3 mb-3"> <div className="space-y-5 p-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3> <div className="space-y-3">
<p className="text-sm text-custom-text-400"> <h3 className="text-xl font-medium text-custom-text-200">Key created</h3>
Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file <p className="text-sm text-custom-text-400">
containing the key has been downloaded. Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file
</p> containing the key has been downloaded.
</p>
</div>
<WebhookSecretKey data={webhookDetails} />
</div> </div>
<WebhookSecretKey data={webhookDetails} /> <div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<div className="mt-6 flex justify-end">
<Button variant="neutral-primary" size="sm" onClick={handleClose}> <Button variant="neutral-primary" size="sm" onClick={handleClose}>
Close Close
</Button> </Button>
</div> </div>
</div> </>
); );
}; };

View File

@ -1,19 +1,16 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// ui // types
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// icons
import { IWorkspaceView } from "@plane/types"; import { IWorkspaceView } from "@plane/types";
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore } from "@/components/core";
// constants // constants
import { GLOBAL_VIEW_DELETED } from "@/constants/event-tracker"; import { GLOBAL_VIEW_DELETED } from "@/constants/event-tracker";
// store hooks // hooks
import { useGlobalView, useEventTracker } from "@/hooks/store"; import { useGlobalView, useEventTracker } from "@/hooks/store";
// ui
// types
type Props = { type Props = {
data: IWorkspaceView; data: IWorkspaceView;
@ -32,9 +29,7 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
const { deleteGlobalView } = useGlobalView(); const { deleteGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const handleClose = () => { const handleClose = () => onClose();
onClose();
};
const handleDeletion = async () => { const handleDeletion = async () => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
@ -69,64 +64,19 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <AlertModalCore
<Dialog as="div" className="relative z-20" onClose={handleClose}> handleClose={handleClose}
<Transition.Child handleSubmit={handleDeletion}
as={React.Fragment} isDeleting={isDeleteLoading}
enter="ease-out duration-300" isOpen={isOpen}
enterFrom="opacity-0" title="Delete View"
enterTo="opacity-100" content={
leave="ease-in duration-200" <>
leaveFrom="opacity-100" Are you sure you want to delete view-{" "}
leaveTo="opacity-0" <span className="break-words font-medium text-custom-text-100">{data?.name}</span>? All of the data related to
> the view will be permanently removed. This action cannot be undone.
<div className="fixed inset-0 bg-custom-backdrop 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 bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
Delete View
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you want to delete view-{" "}
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? All of the
data related to the view will be permanently removed. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -1,6 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// types
import { IIssueFilterOptions, IWorkspaceView } from "@plane/types"; import { IIssueFilterOptions, IWorkspaceView } from "@plane/types";
// ui // ui
import { Button, Input, TextArea } from "@plane/ui"; import { Button, Input, TextArea } from "@plane/ui";
@ -10,7 +11,6 @@ import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/componen
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// hooks // hooks
import { useLabel, useMember } from "@/hooks/store"; import { useLabel, useMember } from "@/hooks/store";
// types
type Props = { type Props = {
handleFormSubmit: (values: Partial<IWorkspaceView>) => Promise<void>; handleFormSubmit: (values: Partial<IWorkspaceView>) => Promise<void>;
@ -97,10 +97,10 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
return ( return (
<form onSubmit={handleSubmit(handleCreateUpdateView)}> <form onSubmit={handleSubmit(handleCreateUpdateView)}>
<div className="space-y-5"> <div className="space-y-5 p-5">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{data ? "Update" : "Create"} View</h3> <h3 className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Create"} View</h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div className="space-y-1">
<Controller <Controller
control={control} control={control}
name="name" name="name"
@ -121,10 +121,11 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
ref={ref} ref={ref}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder="Title" placeholder="Title"
className="w-full resize-none text-xl" className="w-full text-base"
/> />
)} )}
/> />
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div> </div>
<div> <div>
<Controller <Controller
@ -137,7 +138,7 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
value={value} value={value}
placeholder="Description" placeholder="Description"
onChange={onChange} onChange={onChange}
className="min-h-24 w-full resize-none text-sm" className="w-full text-base resize-none min-h-24"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
/> />
)} )}
@ -190,18 +191,12 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
)} )}
</div> </div>
</div> </div>
<div className="mt-5 flex justify-end gap-2"> <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}> <Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}> <Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{data {data ? (isSubmitting ? "Updating" : "Update View") : isSubmitting ? "Creating" : "Create View"}
? isSubmitting
? "Updating View..."
: "Update View"
: isSubmitting
? "Creating View..."
: "Create View"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -1,17 +1,17 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react"; // types
import { IWorkspaceView } from "@plane/types"; import { IWorkspaceView } from "@plane/types";
// ui // ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { WorkspaceViewForm } from "@/components/workspace"; import { WorkspaceViewForm } from "@/components/workspace";
// constants // constants
import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "@/constants/event-tracker"; import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "@/constants/event-tracker";
// store hooks // store hooks
import { useEventTracker, useGlobalView } from "@/hooks/store"; import { useEventTracker, useGlobalView } from "@/hooks/store";
// types
type Props = { type Props = {
data?: IWorkspaceView; data?: IWorkspaceView;
@ -120,43 +120,13 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <WorkspaceViewForm
<Transition.Child handleFormSubmit={handleFormSubmit}
as={React.Fragment} handleClose={handleClose}
enter="ease-out duration-300" data={data}
enterFrom="opacity-0" preLoadedData={preLoadedData}
enterTo="opacity-100" />
leave="ease-in duration-200" </ModalCore>
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-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 rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<WorkspaceViewForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}
data={data}
preLoadedData={preLoadedData}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View File

@ -96,7 +96,9 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => {
<PageHead title={pageTitle} /> <PageHead title={pageTitle} />
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} /> <DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
<div className="w-full space-y-8 overflow-y-auto md:py-8 py-4 md:pr-9 pr-4"> <div className="w-full space-y-8 overflow-y-auto md:py-8 py-4 md:pr-9 pr-4">
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} /> <div className="-m-5">
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
</div>
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />} {currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
</div> </div>
</> </>