forked from github/plane
[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:
parent
5ef51edad7
commit
20e7dc68e6
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
90
web/components/core/modals/alert-modal.tsx
Normal file
90
web/components/core/modals/alert-modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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";
|
||||||
|
68
web/components/core/modals/modal-core.tsx
Normal file
68
web/components/core/modals/modal-core.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
Loading…
Reference in New Issue
Block a user