[WEB-5782]chore: migrated modals to @plane/ui (#8420)

* chore: migrated modal to @plane/ui

* chore: fixed spacings
This commit is contained in:
Vamsi Krishna 2025-12-24 19:45:55 +05:30 committed by GitHub
parent 39728d4cc4
commit 5b28327551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1530 additions and 2412 deletions

View file

@ -1,11 +1,11 @@
import React, { useState } from "react"; import { useState } from "react";
import { Trash2 } from "lucide-react"; import { Trash2 } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants"; import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// ui // ui
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
@ -47,6 +47,7 @@ export function DeactivateAccountModal(props: Props) {
signOut(); signOut();
router.push("/"); router.push("/");
handleClose(); handleClose();
return;
}) })
.catch((err: any) => { .catch((err: any) => {
captureError({ captureError({
@ -62,65 +63,30 @@ export function DeactivateAccountModal(props: Props) {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<Transition.Child <div className="">
as={React.Fragment} <div className="flex items-start gap-x-4">
enter="ease-out duration-300" <div className="mt-3 grid place-items-center rounded-full bg-red-500/20 p-2 sm:mt-3 sm:p-2 md:mt-0 md:p-4 lg:mt-0 lg:p-4 ">
enterFrom="opacity-0" <Trash2 className="h-4 w-4 text-red-600 sm:h-4 sm:w-4 md:h-6 md:w-6 lg:h-6 lg:w-6" aria-hidden="true" />
enterTo="opacity-100" </div>
leave="ease-in duration-200" <div>
leaveFrom="opacity-100" <h3 className="my-4 text-20 font-medium leading-6 text-primary">{t("deactivate_your_account")}</h3>
leaveTo="opacity-0" <p className="mt-6 list-disc pr-4 text-14 font-regular text-secondary">
> {t("deactivate_your_account_description")}
<div className="fixed inset-0 bg-backdrop transition-opacity" /> </p>
</Transition.Child> </div>
<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-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="">
<div className="flex items-start gap-x-4">
<div className="mt-3 grid place-items-center rounded-full bg-red-500/20 p-2 sm:mt-3 sm:p-2 md:mt-0 md:p-4 lg:mt-0 lg:p-4 ">
<Trash2
className="h-4 w-4 text-red-600 sm:h-4 sm:w-4 md:h-6 md:w-6 lg:h-6 lg:w-6"
aria-hidden="true"
/>
</div>
<div>
<Dialog.Title as="h3" className="my-4 text-20 font-medium leading-6 text-primary">
{t("deactivate_your_account")}
</Dialog.Title>
<p className="mt-6 list-disc pr-4 text-14 font-regular text-secondary">
{t("deactivate_your_account_description")}
</p>
</div>
</div>
</div>
</div>
<div className="mb-2 flex items-center justify-end gap-2 p-4 sm:px-6">
<Button variant="secondary" size="lg" onClick={onClose}>
{t("cancel")}
</Button>
<Button variant="error-fill" size="lg" onClick={handleDeleteAccount}>
{isDeactivating ? t("deactivating") : t("confirm")}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</div> </div>
</Dialog> </div>
</Transition.Root> <div className="mb-2 flex items-center justify-end gap-2 p-4 sm:px-6">
<Button variant="secondary" size="lg" onClick={handleClose}>
{t("cancel")}
</Button>
<Button variant="error-fill" size="lg" onClick={handleDeleteAccount}>
{isDeactivating ? t("deactivating") : t("confirm")}
</Button>
</div>
</ModalCore>
); );
} }

View file

@ -1,14 +1,10 @@
import React from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import type { IProject } from "@plane/types"; import type { IProject } from "@plane/types";
// ui // ui
import { Input } from "@plane/ui"; import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// types
// types // types
type Props = { type Props = {
@ -43,124 +39,92 @@ export function SelectMonthModal({ type, initialValues, isOpen, handleClose, han
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-30" onClose={onClose}> <form onSubmit={handleSubmit(onSubmit)}>
<Transition.Child <div>
as={React.Fragment} <h3 className="text-16 font-medium leading-6 text-primary">Customize time range</h3>
enter="ease-out duration-300" <div className="mt-8 flex items-center gap-2">
enterFrom="opacity-0" <div className="flex w-full flex-col justify-center gap-1">
enterTo="opacity-100" {type === "auto-close" ? (
leave="ease-in duration-200" <>
leaveFrom="opacity-100" <Controller
leaveTo="opacity-0" control={control}
> name="close_in"
<div className="fixed inset-0 bg-backdrop transition-opacity" /> rules={{
</Transition.Child> required: "Select a month between 1 and 12.",
min: 1,
<div className="fixed inset-0 z-10 overflow-y-auto"> max: 12,
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> }}
<Transition.Child render={({ field: { value, onChange, ref } }) => (
as={React.Fragment} <div className="relative flex w-full flex-col justify-center gap-1">
enter="ease-out duration-300" <Input
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" id="close_in"
enterTo="opacity-100 translate-y-0 sm:scale-100" name="close_in"
leave="ease-in duration-200" type="number"
leaveFrom="opacity-100 translate-y-0 sm:scale-100" value={value?.toString()}
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" onChange={onChange}
> ref={ref}
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 px-4 pb-4 pt-5 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> hasError={Boolean(errors.close_in)}
<form onSubmit={handleSubmit(onSubmit)}> placeholder="Enter Months"
<div> className="w-full border-subtle"
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary"> min={1}
Customize time range max={12}
</Dialog.Title> />
<div className="mt-8 flex items-center gap-2"> <span className="absolute right-8 top-2.5 text-13 text-secondary">Months</span>
<div className="flex w-full flex-col justify-center gap-1">
{type === "auto-close" ? (
<>
<Controller
control={control}
name="close_in"
rules={{
required: "Select a month between 1 and 12.",
min: 1,
max: 12,
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex w-full flex-col justify-center gap-1">
<Input
id="close_in"
name="close_in"
type="number"
value={value?.toString()}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.close_in)}
placeholder="Enter Months"
className="w-full border-subtle"
min={1}
max={12}
/>
<span className="absolute right-8 top-2.5 text-13 text-secondary">Months</span>
</div>
)}
/>
{errors.close_in && (
<span className="px-1 text-13 text-red-500">Select a month between 1 and 12.</span>
)}
</>
) : (
<>
<Controller
control={control}
name="archive_in"
rules={{
required: "Select a month between 1 and 12.",
min: 1,
max: 12,
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex w-full flex-col justify-center gap-1">
<Input
id="archive_in"
name="archive_in"
type="number"
value={value?.toString()}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.archive_in)}
placeholder="Enter Months"
className="w-full border-subtle"
min={1}
max={12}
/>
<span className="absolute right-8 top-2.5 text-13 text-secondary">Months</span>
</div>
)}
/>
{errors.archive_in && (
<span className="px-1 text-13 text-red-500">Select a month between 1 and 12.</span>
)}
</>
)}
</div> </div>
</div> )}
</div> />
<div className="mt-5 flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={onClose}> {errors.close_in && (
Cancel <span className="px-1 text-13 text-red-500">Select a month between 1 and 12.</span>
</Button> )}
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}> </>
{isSubmitting ? "Submitting..." : "Submit"} ) : (
</Button> <>
</div> <Controller
</form> control={control}
</Dialog.Panel> name="archive_in"
</Transition.Child> rules={{
required: "Select a month between 1 and 12.",
min: 1,
max: 12,
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex w-full flex-col justify-center gap-1">
<Input
id="archive_in"
name="archive_in"
type="number"
value={value?.toString()}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.archive_in)}
placeholder="Enter Months"
className="w-full border-subtle"
min={1}
max={12}
/>
<span className="absolute right-8 top-2.5 text-13 text-secondary">Months</span>
</div>
)}
/>
{errors.archive_in && (
<span className="px-1 text-13 text-red-500">Select a month between 1 and 12.</span>
)}
</>
)}
</div>
</div> </div>
</div> </div>
</Dialog> <div className="mt-5 flex justify-end gap-2">
</Transition.Root> <Button variant="secondary" size="lg" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</Button>
</div>
</form>
</ModalCore>
); );
} }

View file

@ -1,12 +1,8 @@
import { Fragment } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { Calendar } from "@plane/propel/calendar"; import { Calendar } from "@plane/propel/calendar";
import { CloseIcon } from "@plane/propel/icons"; import { CloseIcon } from "@plane/propel/icons";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@plane/utils"; import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@plane/utils";
import { DateFilterSelect } from "./date-filter-select"; import { DateFilterSelect } from "./date-filter-select";
type Props = { type Props = {
@ -49,118 +45,89 @@ export function DateFilterModal({ title, handleClose, isOpen, onSelect }: Props)
const isInvalid = watch("filterType") === "range" && date1 && date2 ? date1 > date2 : false; const isInvalid = watch("filterType") === "range" && date1 && date2 ? date1 > date2 : false;
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-30" onClose={handleClose}> <form className="space-y-4 px-5 py-8 sm:p-6">
<Transition.Child <div className="flex w-full justify-between">
as={Fragment} <Controller
enter="ease-out duration-300" control={control}
enterFrom="opacity-0" name="filterType"
enterTo="opacity-100" render={({ field: { value, onChange } }) => (
leave="ease-in duration-200" <DateFilterSelect title={title} value={value} onChange={onChange} />
leaveFrom="opacity-100" )}
leaveTo="opacity-0" />
> <CloseIcon className="h-4 w-4 cursor-pointer" onClick={handleClose} />
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 flex w-full justify-center overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-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 flex transform rounded-lg bg-surface-1 px-5 py-8 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form className="space-y-4">
<div className="flex w-full justify-between">
<Controller
control={control}
name="filterType"
render={({ field: { value, onChange } }) => (
<DateFilterSelect title={title} value={value} onChange={onChange} />
)}
/>
<CloseIcon className="h-4 w-4 cursor-pointer" onClick={handleClose} />
</div>
<div className="flex w-full justify-between gap-4">
<Controller
control={control}
name="date1"
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date2Value = getDate(watch("date2"));
return (
<Calendar
className="rounded-md border border-subtle p-3"
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date: Date | undefined) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date2Value ? [{ after: date2Value }] : undefined}
/>
);
}}
/>
{watch("filterType") === "range" && (
<Controller
control={control}
name="date2"
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date1Value = getDate(watch("date1"));
return (
<Calendar
className="rounded-md border border-subtle p-3"
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date: Date | undefined) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date1Value ? [{ before: date1Value }] : undefined}
/>
);
}}
/>
)}
</div>
{watch("filterType") === "range" && (
<h6 className="flex items-center gap-1 text-11">
<span className="text-secondary">After:</span>
<span>{renderFormattedDate(watch("date1"))}</span>
<span className="ml-1 text-secondary">Before:</span>
{!isInvalid && <span>{renderFormattedDate(watch("date2"))}</span>}
</h6>
)}
<div className="flex justify-end gap-4">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button
variant="primary"
size="lg"
type="button"
onClick={handleSubmit(handleFormSubmit)}
disabled={isInvalid}
>
Apply
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> <div className="flex w-full justify-between gap-4">
</Transition.Root> <Controller
control={control}
name="date1"
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date2Value = getDate(watch("date2"));
return (
<Calendar
className="rounded-md border border-subtle p-3"
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date: Date | undefined) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date2Value ? [{ after: date2Value }] : undefined}
/>
);
}}
/>
{watch("filterType") === "range" && (
<Controller
control={control}
name="date2"
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date1Value = getDate(watch("date1"));
return (
<Calendar
className="rounded-md border border-subtle p-3"
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date: Date | undefined) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date1Value ? [{ before: date1Value }] : undefined}
/>
);
}}
/>
)}
</div>
{watch("filterType") === "range" && (
<h6 className="flex items-center gap-1 text-11">
<span className="text-secondary">After:</span>
<span>{renderFormattedDate(watch("date1"))}</span>
<span className="ml-1 text-secondary">Before:</span>
{!isInvalid && <span>{renderFormattedDate(watch("date2"))}</span>}
</h6>
)}
<div className="flex justify-end gap-4">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button
variant="primary"
size="lg"
type="button"
onClick={handleSubmit(handleFormSubmit)}
disabled={isInvalid}
>
Apply
</Button>
</div>
</form>
</ModalCore>
); );
} }

View file

@ -1,18 +1,18 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import type { SubmitHandler } from "react-hook-form"; import type { SubmitHandler } from "react-hook-form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse, IUser } from "@plane/types"; import type { ISearchIssueResponse, IUser } from "@plane/types";
import { EIssuesStoreType } from "@plane/types"; import { EIssuesStoreType } from "@plane/types";
import { Loader } from "@plane/ui"; import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// assets // assets
import darkIssuesAsset from "@/app/assets/empty-state/search/issues-dark.webp?url"; import darkIssuesAsset from "@/app/assets/empty-state/search/issues-dark.webp?url";
import lightIssuesAsset from "@/app/assets/empty-state/search/issues-light.webp?url"; import lightIssuesAsset from "@/app/assets/empty-state/search/issues-light.webp?url";
@ -150,80 +150,57 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
); );
return ( return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <form>
<div className="fixed inset-0 z-20 overflow-y-auto bg-backdrop p-4 transition-opacity sm:p-6 md:p-20"> <Combobox
<Transition.Child onChange={(val: string) => {
as={React.Fragment} const selectedIssues = watch("delete_issue_ids");
enter="ease-out duration-300" if (selectedIssues.includes(val))
enterFrom="opacity-0 scale-95" setValue(
enterTo="opacity-100 scale-100" "delete_issue_ids",
leave="ease-in duration-200" selectedIssues.filter((i) => i !== val)
leaveFrom="opacity-100 scale-100" );
leaveTo="opacity-0 scale-95" else setValue("delete_issue_ids", [...selectedIssues, val]);
> }}
<Dialog.Panel className="relative flex w-full items-center justify-center "> >
<div className="w-full max-w-2xl transform divide-y divide-subtle-1 divide-opacity-10 rounded-lg bg-surface-1 shadow-raised-200 transition-all"> <div className="relative m-1">
<form> <Search
<Combobox className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40"
onChange={(val: string) => { aria-hidden="true"
const selectedIssues = watch("delete_issue_ids"); />
if (selectedIssues.includes(val)) <input
setValue( type="text"
"delete_issue_ids", className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-primary outline-none focus:ring-0 sm:text-13"
selectedIssues.filter((i) => i !== val) placeholder="Search..."
); onChange={(event) => setQuery(event.target.value)}
else setValue("delete_issue_ids", [...selectedIssues, val]); />
}} </div>
>
<div className="relative m-1">
<Search
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40"
aria-hidden="true"
/>
<input
type="text"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-primary outline-none focus:ring-0 sm:text-13"
placeholder="Search..."
onChange={(event) => setQuery(event.target.value)}
/>
</div>
<Combobox.Options static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto"> <Combobox.Options static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto">
{isSearching ? ( {isSearching ? (
<Loader className="space-y-3 p-3"> <Loader className="space-y-3 p-3">
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
) : ( ) : (
<>{issueList}</> <>{issueList}</>
)} )}
</Combobox.Options> </Combobox.Options>
</Combobox> </Combobox>
{issues.length > 0 && ( {issues.length > 0 && (
<div className="flex items-center justify-end gap-2 p-3"> <div className="flex items-center justify-end gap-2 p-3">
<Button variant="secondary" size="lg" onClick={handleClose}> <Button variant="secondary" size="lg" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button <Button variant="error-fill" size="lg" onClick={handleSubmit(handleDelete)} loading={isSubmitting}>
variant="error-fill" {isSubmitting ? "Deleting..." : "Delete selected work items"}
size="lg" </Button>
onClick={handleSubmit(handleDelete)} </div>
loading={isSubmitting} )}
> </form>
{isSubmitting ? "Deleting..." : "Delete selected work items"} </ModalCore>
</Button>
</div>
)}
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
); );
}); });

View file

@ -1,12 +1,11 @@
import React, { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Transition, Dialog } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Input } from "@plane/ui"; import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// helpers // helpers
import { authErrorHandler } from "@/helpers/authentication.helper"; import { authErrorHandler } from "@/helpers/authentication.helper";
@ -127,119 +126,82 @@ export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props)
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-30" onClose={handleClose}> <div className="py-4 space-y-0 px-4">
<Transition.Child <h3 className="text-16 font-medium leading-6 text-primary">{changeEmailT("title")}</h3>
as={React.Fragment} <p className="my-4 text-13 text-secondary">{changeEmailT("description")}</p>
enter="ease-out duration-300" </div>
enterFrom="opacity-0" <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 px-4" noValidate>
enterTo="opacity-100" <div className="flex flex-col gap-1">
leave="ease-in duration-200" {secondStep && <h4 className="text-13 font-medium text-secondary">{changeEmailT("form.email.label")}</h4>}
leaveFrom="opacity-100" <Controller
leaveTo="opacity-0" control={control}
> name="email"
<div className="fixed inset-0 transition-opacity bg-backdrop" /> rules={{
</Transition.Child> required: changeEmailT("form.email.errors.required"),
pattern: {
<div className="overflow-y-auto fixed inset-0 z-30"> value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
<div className="flex justify-center items-center p-4 min-h-full text-center sm:p-0"> message: changeEmailT("form.email.errors.invalid"),
<Transition.Child },
as={React.Fragment} }}
enter="ease-out duration-300" render={({ field: { value, onChange, ref } }) => (
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" <Input
enterTo="opacity-100 translate-y-0 sm:scale-100" id="email"
leave="ease-in duration-200" name="email"
leaveFrom="opacity-100 translate-y-0 sm:scale-100" type="email"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" value={value}
> onChange={onChange}
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 px-4 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]"> ref={ref}
<div className="py-4 space-y-0"> hasError={Boolean(errors.email)}
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary"> placeholder={changeEmailT("form.email.placeholder")}
{changeEmailT("title")} className={cn({ "border-red-500": errors.email }, { "cursor-not-allowed !bg-surface-2": secondStep })}
</Dialog.Title> disabled={secondStep}
<p className="my-4 text-13 text-secondary">{changeEmailT("description")}</p> />
</div> )}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate> />
<div className="flex flex-col gap-1"> {errors?.email && <span className="text-11 text-red-500">{errors?.email?.message}</span>}
{secondStep && (
<h4 className="text-13 font-medium text-secondary">{changeEmailT("form.email.label")}</h4>
)}
<Controller
control={control}
name="email"
rules={{
required: changeEmailT("form.email.errors.required"),
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: changeEmailT("form.email.errors.invalid"),
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder={changeEmailT("form.email.placeholder")}
className={cn(
{ "border-red-500": errors.email },
{ "cursor-not-allowed !bg-surface-2": secondStep }
)}
disabled={secondStep}
/>
)}
/>
{errors?.email && <span className="text-11 text-red-500">{errors?.email?.message}</span>}
</div>
{secondStep && (
<div className="flex flex-col gap-1">
<h4 className="text-13 font-medium text-secondary">{changeEmailT("form.code.label")}</h4>
<Controller
control={control}
name="code"
rules={{ required: changeEmailT("form.code.errors.required") }}
render={({ field: { value, onChange, ref } }) => (
<Input
id="code"
name="code"
value={value}
onChange={onChange}
ref={ref}
placeholder={changeEmailT("form.code.placeholder")}
className={cn({ "border-red-500": errors.code })}
autoFocus
/>
)}
/>
{errors?.code ? (
<span className="text-11 text-red-500">{errors?.code?.message}</span>
) : (
<span className="text-11 text-green-700">{changeEmailT("form.code.helper_text")}</span>
)}
</div>
)}
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle py-4">
<Button type="button" variant="secondary" size="lg" onClick={handleClose}>
{changeEmailT("actions.cancel")}
</Button>
<Button type="submit" variant="primary" size="lg" disabled={isSubmitting}>
{isSubmitting
? changeEmailT("states.sending")
: secondStep
? changeEmailT("actions.confirm")
: changeEmailT("actions.continue")}
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog>
</Transition.Root> {secondStep && (
<div className="flex flex-col gap-1">
<h4 className="text-13 font-medium text-secondary">{changeEmailT("form.code.label")}</h4>
<Controller
control={control}
name="code"
rules={{ required: changeEmailT("form.code.errors.required") }}
render={({ field: { value, onChange, ref } }) => (
<Input
id="code"
name="code"
value={value}
onChange={onChange}
ref={ref}
placeholder={changeEmailT("form.code.placeholder")}
className={cn({ "border-red-500": errors.code })}
autoFocus
/>
)}
/>
{errors?.code ? (
<span className="text-11 text-red-500">{errors?.code?.message}</span>
) : (
<span className="text-11 text-green-700">{changeEmailT("form.code.helper_text")}</span>
)}
</div>
)}
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle py-4">
<Button type="button" variant="secondary" size="lg" onClick={handleClose}>
{changeEmailT("actions.cancel")}
</Button>
<Button type="submit" variant="primary" size="lg" disabled={isSubmitting}>
{isSubmitting
? changeEmailT("states.sending")
: secondStep
? changeEmailT("actions.confirm")
: changeEmailT("actions.continue")}
</Button>
</div>
</form>
</ModalCore>
); );
}); });

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { Rocket, Search } from "lucide-react"; import { Rocket, Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// i18n // i18n
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
@ -10,7 +10,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
// ui // ui
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { generateWorkItemLink, getTabIndex } from "@plane/utils"; import { generateWorkItemLink, getTabIndex } from "@plane/utils";
// helpers // helpers
// hooks // hooks
@ -131,62 +131,136 @@ export function ExistingIssuesListModal(props: Props) {
const filteredIssues = issues.filter((issue) => !shouldHideIssue?.(issue)); const filteredIssues = issues.filter((issue) => !shouldHideIssue?.(issue));
return ( return (
<> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear> <Combobox
<Dialog as="div" className="relative z-30" onClose={handleClose}> as="div"
<Transition.Child onChange={(val: ISearchIssueResponse) => {
as={React.Fragment} if (selectedIssues.some((i) => i.id === val.id))
enter="ease-out duration-300" setSelectedIssues((prevData) => prevData.filter((i) => i.id !== val.id));
enterFrom="opacity-0" else setSelectedIssues((prevData) => [...prevData, val]);
enterTo="opacity-100" }}
leave="ease-in duration-200" >
leaveFrom="opacity-100" <div className="relative m-1">
leaveTo="opacity-0" <Search
> className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40"
<div className="fixed inset-0 bg-backdrop transition-opacity" /> aria-hidden="true"
</Transition.Child> />
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-13 text-primary outline-none placeholder:text-placeholder focus:ring-0"
placeholder={t("common.search.placeholder")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
tabIndex={baseTabIndex}
/>
</div>
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20"> <div className="flex flex-col-reverse gap-4 p-2 text-13 text-secondary sm:flex-row sm:items-center sm:justify-between">
<Transition.Child {selectedIssues.length > 0 ? (
as={React.Fragment} <div className="mt-1 flex flex-wrap items-center gap-2">
enter="ease-out duration-300" {selectedIssues.map((issue) => (
enterFrom="opacity-0 scale-95" <div
enterTo="opacity-100 scale-100" key={issue.id}
leave="ease-in duration-200" className="flex items-center gap-1 whitespace-nowrap rounded-md border border-subtle bg-layer-1 py-1 pl-2 text-11 text-primary"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-surface-1 shadow-raised-200 transition-all">
<Combobox
as="div"
onChange={(val: ISearchIssueResponse) => {
if (selectedIssues.some((i) => i.id === val.id))
setSelectedIssues((prevData) => prevData.filter((i) => i.id !== val.id));
else setSelectedIssues((prevData) => [...prevData, val]);
}}
> >
<div className="relative m-1"> <IssueIdentifier
<Search projectId={issue.project_id}
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40" issueTypeId={issue.type_id}
aria-hidden="true" projectIdentifier={issue.project__identifier}
/> issueSequenceId={issue.sequence_id}
<Combobox.Input size="xs"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-13 text-primary outline-none placeholder:text-placeholder focus:ring-0" variant="secondary"
placeholder={t("common.search.placeholder")} />
value={searchTerm} <button
onChange={(e) => setSearchTerm(e.target.value)} type="button"
tabIndex={baseTabIndex} className="group p-1"
/> onClick={() => setSelectedIssues((prevData) => prevData.filter((i) => i.id !== issue.id))}
</div> >
<CloseIcon className="h-3 w-3 text-secondary group-hover:text-primary" />
</button>
</div>
))}
</div>
) : (
<div className="w-min whitespace-nowrap rounded-md border border-subtle bg-layer-1 p-2 text-11">
{t("issue.select.empty")}
</div>
)}
{workspaceLevelToggle && (
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
<div
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-11 ${
isWorkspaceLevel ? "text-primary" : "text-secondary"
}`}
>
<ToggleSwitch value={isWorkspaceLevel} onChange={() => setIsWorkspaceLevel((prevData) => !prevData)} />
<button
type="button"
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
{t("common.workspace_level")}
</button>
</div>
</Tooltip>
)}
</div>
<div className="flex flex-col-reverse gap-4 p-2 text-13 text-secondary sm:flex-row sm:items-center sm:justify-between"> <Combobox.Options static className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto">
{selectedIssues.length > 0 ? ( {/* TODO: Translate here */}
<div className="mt-1 flex flex-wrap items-center gap-2"> {searchTerm !== "" && (
{selectedIssues.map((issue) => ( <h5 className="mx-2 text-13 text-secondary">
<div Search results for{" "}
key={issue.id} <span className="text-primary">
className="flex items-center gap-1 whitespace-nowrap rounded-md border border-subtle bg-layer-1 py-1 pl-2 text-11 text-primary" {'"'}
> {searchTerm}
{'"'}
</span>{" "}
in project:
</h5>
)}
{isSearching || isLoading ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
) : (
<>
{filteredIssues.length === 0 ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={filteredIssues}
searchTerm={searchTerm}
/>
) : (
<ul className={`text-13 text-primary ${filteredIssues.length > 0 ? "p-2" : ""}`}>
{filteredIssues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id);
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue}
className={({ active }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-secondary ${
active ? "bg-layer-1 text-primary" : ""
} ${selected ? "text-primary" : ""}`
}
>
<div className="flex items-center gap-2 truncate">
<input type="checkbox" checked={selected} readOnly />
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0">
<IssueIdentifier <IssueIdentifier
projectId={issue.project_id} projectId={issue.project_id}
issueTypeId={issue.type_id} issueTypeId={issue.type_id}
@ -195,169 +269,57 @@ export function ExistingIssuesListModal(props: Props) {
size="xs" size="xs"
variant="secondary" variant="secondary"
/> />
<button </span>
type="button" <span className="truncate">{issue.name}</span>
className="group p-1"
onClick={() => setSelectedIssues((prevData) => prevData.filter((i) => i.id !== issue.id))}
>
<CloseIcon className="h-3 w-3 text-secondary group-hover:text-primary" />
</button>
</div>
))}
</div>
) : (
<div className="w-min whitespace-nowrap rounded-md border border-subtle bg-layer-1 p-2 text-11">
{t("issue.select.empty")}
</div>
)}
{workspaceLevelToggle && (
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
<div
className={`flex flex-shrink-0 cursor-pointer items-center gap-1 text-11 ${
isWorkspaceLevel ? "text-primary" : "text-secondary"
}`}
>
<ToggleSwitch
value={isWorkspaceLevel}
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
/>
<button
type="button"
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
{t("common.workspace_level")}
</button>
</div> </div>
</Tooltip> <a
)} href={generateWorkItemLink({
</div> workspaceSlug,
projectId: issue?.project_id,
<Combobox.Options issueId: issue?.id,
static projectIdentifier: issue.project__identifier,
className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto" sequenceId: issue?.sequence_id,
> })}
{/* TODO: Translate here */} target="_blank"
{searchTerm !== "" && ( className="z-1 relative hidden flex-shrink-0 text-secondary hover:text-primary group-hover:block"
<h5 className="mx-2 text-13 text-secondary"> rel="noopener noreferrer"
Search results for{" "} onClick={(e) => e.stopPropagation()}
<span className="text-primary"> >
{'"'} <Rocket className="h-4 w-4" />
{searchTerm} </a>
{'"'} </Combobox.Option>
</span>{" "} );
in project: })}
</h5> </ul>
)} )}
</>
{isSearching || isLoading ? ( )}
<Loader className="space-y-3 p-3"> </Combobox.Options>
<Loader.Item height="40px" /> </Combobox>
<Loader.Item height="40px" /> <div className="flex justify-between items-center p-3">
<Loader.Item height="40px" /> <Button
<Loader.Item height="40px" /> variant="link"
</Loader> onClick={handleSelectIssues}
) : ( disabled={filteredIssues.length === 0}
<> className={filteredIssues.length === 0 ? "p-0" : ""}
{filteredIssues.length === 0 ? ( >
<IssueSearchModalEmptyState {selectedIssues.length === issues.length ? t("issue.select.deselect_all") : t("issue.select.select_all")}
debouncedSearchTerm={debouncedSearchTerm} </Button>
isSearching={isSearching} <div className="flex items-center justify-end gap-2">
issues={filteredIssues} <Button variant="secondary" size="lg" onClick={handleClose}>
searchTerm={searchTerm} {t("common.cancel")}
/> </Button>
) : ( <Button
<ul className={`text-13 text-primary ${filteredIssues.length > 0 ? "p-2" : ""}`}> variant="primary"
{filteredIssues.map((issue) => { size="lg"
const selected = selectedIssues.some((i) => i.id === issue.id); onClick={onSubmit}
loading={isSubmitting}
return ( disabled={isSubmitting || selectedIssues.length === 0}
<Combobox.Option >
key={issue.id} {isSubmitting ? t("common.adding") : t("issue.select.add_selected")}
as="label" </Button>
htmlFor={`issue-${issue.id}`} </div>
value={issue} </div>
className={({ active }) => </ModalCore>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-secondary ${
active ? "bg-layer-1 text-primary" : ""
} ${selected ? "text-primary" : ""}`
}
>
<div className="flex items-center gap-2 truncate">
<input type="checkbox" checked={selected} readOnly />
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0">
<IssueIdentifier
projectId={issue.project_id}
issueTypeId={issue.type_id}
projectIdentifier={issue.project__identifier}
issueSequenceId={issue.sequence_id}
size="xs"
variant="secondary"
/>
</span>
<span className="truncate">{issue.name}</span>
</div>
<a
href={generateWorkItemLink({
workspaceSlug,
projectId: issue?.project_id,
issueId: issue?.id,
projectIdentifier: issue.project__identifier,
sequenceId: issue?.sequence_id,
})}
target="_blank"
className="z-1 relative hidden flex-shrink-0 text-secondary hover:text-primary group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
);
})}
</ul>
)}
</>
)}
</Combobox.Options>
</Combobox>
<div className="flex justify-between items-center p-3">
<Button
variant="link"
onClick={handleSelectIssues}
disabled={filteredIssues.length === 0}
className={filteredIssues.length === 0 ? "p-0" : ""}
>
{selectedIssues.length === issues.length
? t("issue.select.deselect_all")
: t("issue.select.select_all")}
</Button>
<div className="flex items-center justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button
variant="primary"
size="lg"
onClick={onSubmit}
loading={isSubmitting}
disabled={isSubmitting || selectedIssues.length === 0}
>
{isSubmitting ? t("common.adding") : t("issue.select.add_selected")}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</>
); );
} }

View file

@ -1,13 +1,13 @@
import React, { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { Transition, Dialog } from "@headlessui/react";
// plane imports // plane imports
import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { UserCirclePropertyIcon } from "@plane/propel/icons"; import { UserCirclePropertyIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types"; import { EFileAssetType } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils"; import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils";
// services // services
import { FileService } from "@/services/file.service"; import { FileService } from "@/services/file.service";
@ -88,106 +88,68 @@ export const UserImageUploadModal = observer(function UserImageUploadModal(props
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XL}>
<Dialog as="div" className="relative z-30" onClose={handleClose}> <div className="space-y-5 px-5 py-8 sm:p-6">
<Transition.Child <h3 className="text-16 font-medium leading-6 text-primary">Upload Image</h3>
as={React.Fragment} <div className="space-y-3">
enter="ease-out duration-300" <div className="flex items-center justify-center gap-3">
enterFrom="opacity-0" <div
enterTo="opacity-100" {...getRootProps()}
leave="ease-in duration-200" className={`relative grid h-80 w-80 cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-accent-strong focus:ring-offset-2 ${
leaveFrom="opacity-100" (image === null && isDragActive) || !value
leaveTo="opacity-0" ? "border-2 border-dashed border-subtle hover:bg-surface-2"
> : ""
<div className="fixed inset-0 bg-backdrop transition-opacity" /> }`}
</Transition.Child>
<div className="fixed inset-0 z-30 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 overflow-hidden rounded-lg bg-surface-1 px-5 py-8 text-left shadow-raised-200 transition-all sm:w-full sm:max-w-xl sm:p-6"> {image !== null || (value && value !== "") ? (
<div className="space-y-5"> <>
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary"> <button
Upload Image type="button"
</Dialog.Title> className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded-sm bg-surface-2 px-2 py-0.5 text-11 font-medium text-secondary"
<div className="space-y-3"> >
<div className="flex items-center justify-center gap-3"> Edit
<div </button>
{...getRootProps()} <img
className={`relative grid h-80 w-80 cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-accent-strong focus:ring-offset-2 ${ src={image ? URL.createObjectURL(image) : value ? getFileURL(value) : ""}
(image === null && isDragActive) || !value alt="image"
? "border-2 border-dashed border-subtle hover:bg-surface-2" className="absolute left-0 top-0 h-full w-full rounded-md object-cover"
: "" />
}`} </>
> ) : (
{image !== null || (value && value !== "") ? ( <div>
<> <UserCirclePropertyIcon className="mx-auto h-16 w-16 text-secondary" />
<button <span className="mt-2 block text-13 font-medium text-secondary">
type="button" {isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded-sm bg-surface-2 px-2 py-0.5 text-11 font-medium text-secondary" </span>
> </div>
Edit )}
</button>
<img
src={image ? URL.createObjectURL(image) : value ? getFileURL(value) : ""}
alt="image"
className="absolute left-0 top-0 h-full w-full rounded-md object-cover"
/>
</>
) : (
<div>
<UserCirclePropertyIcon className="mx-auto h-16 w-16 text-secondary" />
<span className="mt-2 block text-13 font-medium text-secondary">
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
</span>
</div>
)}
<input {...getInputProps()} /> <input {...getInputProps()} />
</div> </div>
</div> </div>
{fileRejections.length > 0 && ( {fileRejections.length > 0 && (
<p className="text-13 text-red-500"> <p className="text-13 text-red-500">
{fileRejections[0].errors[0].code === "file-too-large" {fileRejections[0].errors[0].code === "file-too-large"
? "The image size cannot exceed 5 MB." ? "The image size cannot exceed 5 MB."
: "Please upload a file in a valid format."} : "Please upload a file in a valid format."}
</p> </p>
)} )}
</div> </div>
</div> <p className="my-4 text-13 text-secondary">File formats supported- .jpeg, .jpg, .png, .webp</p>
<p className="my-4 text-13 text-secondary">File formats supported- .jpeg, .jpg, .png, .webp</p> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <Button variant="error-fill" size="lg" onClick={handleImageRemove} disabled={!value}>
<Button variant="error-fill" size="lg" onClick={handleImageRemove} disabled={!value}> {isRemoving ? "Removing" : "Remove"}
{isRemoving ? "Removing" : "Remove"} </Button>
</Button> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <Button variant="secondary" size="lg" onClick={handleClose}>
<Button variant="secondary" size="lg" onClick={handleClose}> Cancel
Cancel </Button>
</Button> <Button variant="primary" size="lg" onClick={handleSubmit} disabled={!image} loading={isImageUploading}>
<Button {isImageUploading ? "Uploading" : "Upload & Save"}
variant="primary" </Button>
size="lg"
onClick={handleSubmit}
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading" : "Upload & Save"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</div> </div>
</Dialog> </div>
</Transition.Root> </ModalCore>
); );
}); });

View file

@ -1,14 +1,14 @@
import React, { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { Transition, Dialog } from "@headlessui/react";
// plane imports // plane imports
import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { UserCirclePropertyIcon } from "@plane/propel/icons"; import { UserCirclePropertyIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types"; import { EFileAssetType } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils"; import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils";
// hooks // hooks
import { useWorkspace } from "@/hooks/store/use-workspace"; import { useWorkspace } from "@/hooks/store/use-workspace";
@ -101,112 +101,68 @@ export const WorkspaceImageUploadModal = observer(function WorkspaceImageUploadM
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XL}>
<Dialog as="div" className="relative z-30" onClose={handleClose}> <div className="space-y-5 px-5 py-8 sm:p-6">
<Transition.Child <h3 className="text-16 font-medium leading-6 text-primary">Upload image</h3>
as={React.Fragment} <div className="space-y-3">
enter="ease-out duration-300" <div className="flex items-center justify-center gap-3">
enterFrom="opacity-0" <div
enterTo="opacity-100" {...getRootProps()}
leave="ease-in duration-200" className={`relative grid h-80 w-80 cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-accent-strong focus:ring-offset-2 ${
leaveFrom="opacity-100" (image === null && isDragActive) || !value
leaveTo="opacity-0" ? "border-2 border-dashed border-subtle hover:bg-surface-2"
> : ""
<div className="fixed inset-0 bg-backdrop transition-opacity" /> }`}
</Transition.Child>
<div className="fixed inset-0 z-30 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 overflow-hidden rounded-lg bg-surface-1 px-5 py-8 text-left shadow-raised-200 transition-all sm:w-full sm:max-w-xl sm:p-6"> {image !== null || (value && value !== "") ? (
<div className="space-y-5"> <>
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary"> <button
Upload image type="button"
</Dialog.Title> className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded-sm bg-surface-2 px-2 py-0.5 text-11 font-medium text-secondary"
<div className="space-y-3">
<div className="flex items-center justify-center gap-3">
<div
{...getRootProps()}
className={`relative grid h-80 w-80 cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-accent-strong focus:ring-offset-2 ${
(image === null && isDragActive) || !value
? "border-2 border-dashed border-subtle hover:bg-surface-2"
: ""
}`}
>
{image !== null || (value && value !== "") ? (
<>
<button
type="button"
className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded-sm bg-surface-2 px-2 py-0.5 text-11 font-medium text-secondary"
>
Edit
</button>
<img
src={image ? URL.createObjectURL(image) : value ? getFileURL(value) : ""}
alt="image"
className="absolute left-0 top-0 h-full w-full rounded-md object-cover"
/>
</>
) : (
<div>
<UserCirclePropertyIcon className="mx-auto h-16 w-16 text-secondary" />
<span className="mt-2 block text-13 font-medium text-secondary">
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
</span>
</div>
)}
<input {...getInputProps()} />
</div>
</div>
{fileRejections.length > 0 && (
<p className="text-13 text-red-500">
{fileRejections[0].errors[0].code === "file-too-large"
? "The image size cannot exceed 5 MB."
: "Please upload a file in a valid format."}
</p>
)}
</div>
</div>
<p className="my-4 text-13 text-secondary">File formats supported- .jpeg, .jpg, .png, .webp</p>
<div className="flex items-center justify-between">
<Button
variant="error-fill"
size="lg"
onClick={handleImageRemove}
disabled={!value}
loading={isRemoving}
> >
{isRemoving ? "Removing" : "Remove"} Edit
</Button> </button>
<div className="flex items-center gap-2"> <img
<Button variant="secondary" size="lg" onClick={handleClose}> src={image ? URL.createObjectURL(image) : value ? getFileURL(value) : ""}
Cancel alt="image"
</Button> className="absolute left-0 top-0 h-full w-full rounded-md object-cover"
<Button />
variant="primary" </>
size="lg" ) : (
onClick={handleSubmit} <div>
disabled={!image} <UserCirclePropertyIcon className="mx-auto h-16 w-16 text-secondary" />
loading={isImageUploading} <span className="mt-2 block text-13 font-medium text-secondary">
> {isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
{isImageUploading ? "Uploading" : "Upload & Save"} </span>
</Button>
</div>
</div> </div>
</Dialog.Panel> )}
</Transition.Child>
<input {...getInputProps()} />
</div>
</div>
{fileRejections.length > 0 && (
<p className="text-13 text-red-500">
{fileRejections[0].errors[0].code === "file-too-large"
? "The image size cannot exceed 5 MB."
: "Please upload a file in a valid format."}
</p>
)}
</div>
<p className="my-4 text-13 text-secondary">File formats supported- .jpeg, .jpg, .png, .webp</p>
<div className="flex items-center justify-between">
<Button variant="error-fill" size="lg" onClick={handleImageRemove} disabled={!value} loading={isRemoving}>
{isRemoving ? "Removing" : "Remove"}
</Button>
<div className="flex items-center gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" size="lg" onClick={handleSubmit} disabled={!image} loading={isImageUploading}>
{isImageUploading ? "Uploading" : "Upload & Save"}
</Button>
</div> </div>
</div> </div>
</Dialog> </div>
</Transition.Root> </ModalCore>
); );
}); });

View file

@ -1,9 +1,9 @@
import { useState, Fragment } from "react"; import { useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { CYCLE_TRACKER_EVENTS } from "@plane/constants"; import { CYCLE_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store/use-cycle"; import { useCycle } from "@/hooks/store/use-cycle";
@ -51,6 +51,7 @@ export function ArchiveCycleModal(props: Props) {
}); });
onClose(); onClose();
router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
return;
}) })
.catch(() => { .catch(() => {
setToast({ setToast({
@ -69,51 +70,21 @@ export function ArchiveCycleModal(props: Props) {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.LG}>
<Dialog as="div" className="relative z-20" onClose={onClose}> <div className="px-5 py-4">
<Transition.Child <h3 className="text-18 font-medium 2xl:text-20">Archive cycle {cycleName}</h3>
as={Fragment} <p className="mt-3 text-13 text-secondary">
enter="ease-out duration-300" Are you sure you want to archive the cycle? All your archives can be restored later.
enterFrom="opacity-0" </p>
enterTo="opacity-100" <div className="mt-3 flex justify-end gap-2">
leave="ease-in duration-200" <Button variant="secondary" size="lg" onClick={onClose}>
leaveFrom="opacity-100" Cancel
leaveTo="opacity-0" </Button>
> <Button variant="primary" size="lg" tabIndex={1} onClick={handleArchiveCycle} loading={isArchiving}>
<div className="fixed inset-0 bg-backdrop transition-opacity" /> {isArchiving ? "Archiving" : "Archive"}
</Transition.Child> </Button>
<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-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="px-5 py-4">
<h3 className="text-18 font-medium 2xl:text-20">Archive cycle {cycleName}</h3>
<p className="mt-3 text-13 text-secondary">
Are you sure you want to archive the cycle? All your archives can be restored later.
</p>
<div className="mt-3 flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" size="lg" tabIndex={1} onClick={handleArchiveCycle} loading={isArchiving}>
{isArchiving ? "Archiving" : "Archive"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> </div>
</Transition.Root> </ModalCore>
); );
} }

View file

@ -1,15 +1,13 @@
import React, { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { AlertCircle, Search } from "lucide-react"; import { AlertCircle, Search } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { CycleIcon, TransferIcon, CloseIcon } from "@plane/propel/icons"; import { CycleIcon, TransferIcon, CloseIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EIssuesStoreType } from "@plane/types"; import { EIssuesStoreType } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { useCycle } from "@/hooks/store/use-cycle"; import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues"; import { useIssues } from "@/hooks/store/use-issues";
//icons
// constants
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -72,109 +70,71 @@ export const TransferIssuesModal = observer(function TransferIssuesModal(props:
return cycleDetails?.name?.toLowerCase().includes(query?.toLowerCase()); return cycleDetails?.name?.toLowerCase().includes(query?.toLowerCase());
}); });
// useEffect(() => {
// const handleKeyDown = (e: KeyboardEvent) => {
// if (e.key === "Escape") {
// handleClose();
// }
// };
// }, [handleClose]);
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}> <div className="flex flex-col gap-4 py-5">
<Transition.Child <div className="flex items-center justify-between px-5">
as={React.Fragment} <div className="flex items-center gap-1">
enter="ease-out duration-300" <TransferIcon className="w-5 fill-primary" />
enterFrom="opacity-0" <h4 className="text-18 font-medium text-primary">Transfer work items</h4>
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10">
<div className="mt-10 flex min-h-full items-start justify-center p-4 text-center sm:p-0 md:mt-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-surface-1 py-5 text-left shadow-raised-200 transition-all sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between px-5">
<div className="flex items-center gap-1">
<TransferIcon className="w-5 fill-primary" />
<h4 className="text-18 font-medium text-primary">Transfer work items</h4>
</div>
<button onClick={handleClose}>
<CloseIcon className="h-4 w-4" />
</button>
</div>
<div className="flex items-center gap-2 border-b border-subtle px-5 pb-3">
<Search className="h-4 w-4 text-secondary" />
<input
className="outline-none text-13"
placeholder="Search for a cycle..."
onChange={(e) => setQuery(e.target.value)}
value={query}
/>
</div>
<div className="flex w-full flex-col items-start gap-2 px-5">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((optionId) => {
const cycleDetails = getCycleById(optionId);
if (!cycleDetails) return;
return (
<button
key={optionId}
className="flex w-full items-center gap-4 rounded-sm px-4 py-3 text-13 text-secondary hover:bg-surface-2"
onClick={() => {
transferIssue({
new_cycle_id: optionId,
});
handleClose();
}}
>
<CycleIcon className="h-5 w-5" />
<div className="flex w-full justify-between truncate">
<span className="truncate">{cycleDetails?.name}</span>
{cycleDetails.status && (
<span className="flex-shrink-0 flex items-center rounded-full bg-layer-1 px-2 capitalize">
{cycleDetails.status.toLocaleLowerCase()}
</span>
)}
</div>
</button>
);
})
) : (
<div className="flex w-full items-center justify-center gap-4 p-5 text-13">
<AlertCircle className="h-3.5 w-3.5 text-secondary" />
<span className="text-center text-secondary">
You dont have any current cycle. Please create one to transfer the work items.
</span>
</div>
)
) : (
<p className="text-center text-secondary">Loading...</p>
)}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div> </div>
<button onClick={handleClose}>
<CloseIcon className="h-4 w-4" />
</button>
</div> </div>
</Dialog> <div className="flex items-center gap-2 border-b border-subtle px-5 pb-3">
</Transition.Root> <Search className="h-4 w-4 text-secondary" />
<input
className="outline-none text-13"
placeholder="Search for a cycle..."
onChange={(e) => setQuery(e.target.value)}
value={query}
/>
</div>
<div className="flex w-full flex-col items-start gap-2 px-5">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((optionId) => {
const cycleDetails = getCycleById(optionId);
if (!cycleDetails) return;
return (
<button
key={optionId}
className="flex w-full items-center gap-4 rounded-sm px-4 py-3 text-13 text-secondary hover:bg-surface-2"
onClick={() => {
transferIssue({
new_cycle_id: optionId,
});
handleClose();
}}
>
<CycleIcon className="h-5 w-5" />
<div className="flex w-full justify-between truncate">
<span className="truncate">{cycleDetails?.name}</span>
{cycleDetails.status && (
<span className="flex-shrink-0 flex items-center rounded-full bg-layer-1 px-2 capitalize">
{cycleDetails.status.toLocaleLowerCase()}
</span>
)}
</div>
</button>
);
})
) : (
<div className="flex w-full items-center justify-center gap-4 p-5 text-13">
<AlertCircle className="h-3.5 w-3.5 text-secondary" />
<span className="text-center text-secondary">
You dont have any current cycle. Please create one to transfer the work items.
</span>
</div>
)
) : (
<p className="text-center text-secondary">Loading...</p>
)}
</div>
</div>
</ModalCore>
); );
}); });

View file

@ -2,14 +2,13 @@ import React, { useState } from "react";
import { intersection } from "lodash-es"; import { intersection } from "lodash-es";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Dialog, Transition } from "@headlessui/react";
// types // types
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IImporterService } from "@plane/types"; import type { IUser, IImporterService } from "@plane/types";
// ui // ui
import { Checkbox, CustomSearchSelect } from "@plane/ui"; import { Checkbox, CustomSearchSelect, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
@ -99,101 +98,64 @@ export const Exporter = observer(function Exporter(props: Props) {
} }
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore
<Dialog isOpen={isOpen}
as="div" handleClose={() => {
className="relative z-20" if (!isSelectOpen) handleClose();
onClose={() => { }}
if (!isSelectOpen) handleClose(); position={EModalPosition.CENTER}
}} width={EModalWidth.XL}
> >
<Transition.Child <div className="flex flex-col gap-6 gap-y-4 p-6">
as={React.Fragment} <div className="flex w-full items-center justify-start gap-6">
enter="ease-out duration-300" <span className="flex items-center justify-start">
enterFrom="opacity-0" <h3 className="text-18 font-medium 2xl:text-20">
enterTo="opacity-100" {t("workspace_settings.settings.exports.modal.title")}{" "}
leave="ease-in duration-200" {provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""}
leaveFrom="opacity-100" </h3>
leaveTo="opacity-0" </span>
> </div>
<div className="fixed inset-0 bg-backdrop transition-opacity" /> <div>
</Transition.Child> <CustomSearchSelect
value={value ?? []}
onChange={(val: string[]) => onChange(val)}
options={options}
input
label={
value && value.length > 0
? value
.map((projectId) => {
const projectDetails = getProjectById(projectId);
<div className="fixed inset-0 z-20 overflow-y-auto"> return projectDetails?.identifier;
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> })
<Transition.Child .join(", ")
as={React.Fragment} : "All projects"
enter="ease-out duration-300" }
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" onOpen={() => setIsSelectOpen(true)}
enterTo="opacity-100 translate-y-0 sm:scale-100" onClose={() => setIsSelectOpen(false)}
leave="ease-in duration-200" optionsClassName="max-w-48 sm:max-w-[532px]"
leaveFrom="opacity-100 translate-y-0 sm:scale-100" placement="bottom-end"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" multiple
> />
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-xl"> </div>
<div className="flex flex-col gap-6 gap-y-4 p-6"> <div className="flex max-w-min cursor-pointer items-center gap-2">
<div className="flex w-full items-center justify-start gap-6"> <Checkbox checked={multiple} onChange={() => setMultiple(!multiple)} />
<span className="flex items-center justify-start"> <div className="whitespace-nowrap text-13">
<h3 className="text-18 font-medium 2xl:text-20"> {t("workspace_settings.settings.exports.export_separate_files")}
{t("workspace_settings.settings.exports.modal.title")}{" "}
{provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""}
</h3>
</span>
</div>
<div>
<CustomSearchSelect
value={value ?? []}
onChange={(val: string[]) => onChange(val)}
options={options}
input
label={
value && value.length > 0
? value
.map((projectId) => {
const projectDetails = getProjectById(projectId);
return projectDetails?.identifier;
})
.join(", ")
: "All projects"
}
onOpen={() => setIsSelectOpen(true)}
onClose={() => setIsSelectOpen(false)}
optionsClassName="max-w-48 sm:max-w-[532px]"
placement="bottom-end"
multiple
/>
</div>
<div
onClick={() => setMultiple(!multiple)}
className="flex max-w-min cursor-pointer items-center gap-2"
>
<Checkbox checked={multiple} onChange={() => setMultiple(!multiple)} />
<div className="whitespace-nowrap text-13">
{t("workspace_settings.settings.exports.export_separate_files")}
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={handleClose}>
{t("cancel")}
</Button>
<Button
variant="primary"
onClick={ExportCSVToMail}
disabled={exportLoading}
loading={exportLoading}
>
{exportLoading
? `${t("workspace_settings.settings.exports.exporting")}...`
: t("workspace_settings.settings.exports.title")}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</div> </div>
</Dialog> <div className="flex justify-end gap-2">
</Transition.Root> <Button variant="secondary" onClick={handleClose}>
{t("cancel")}
</Button>
<Button variant="primary" onClick={ExportCSVToMail} disabled={exportLoading} loading={exportLoading}>
{exportLoading
? `${t("workspace_settings.settings.exports.exporting")}...`
: t("workspace_settings.settings.exports.title")}
</Button>
</div>
</div>
</ModalCore>
); );
}); });

View file

@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse } from "@plane/types"; import type { ISearchIssueResponse } from "@plane/types";
import { Loader } from "@plane/ui"; import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// assets // assets
import darkIssuesAsset from "@/app/assets/empty-state/search/issues-dark.webp?url"; import darkIssuesAsset from "@/app/assets/empty-state/search/issues-dark.webp?url";
import lightIssuesAsset from "@/app/assets/empty-state/search/issues-light.webp?url"; import lightIssuesAsset from "@/app/assets/empty-state/search/issues-light.webp?url";
@ -65,6 +65,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
setQuery("");
}; };
const handleSubmit = (selectedItem: string) => { const handleSubmit = (selectedItem: string) => {
@ -124,66 +125,34 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
); );
return ( return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<div className="flex flex-wrap items-start"> <Combobox value={value} onChange={handleSubmit}>
<div className="space-y-1 sm:basis-1/2"> <div className="relative m-1">
<Dialog as="div" className="relative z-30" onClose={handleClose}> <Search
<Transition.Child className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40"
as={React.Fragment} aria-hidden="true"
enter="ease-out duration-300" />
enterFrom="opacity-0" <input
enterTo="opacity-100" type="text"
leave="ease-in duration-200" className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-primary outline-none focus:ring-0 sm:text-13"
leaveFrom="opacity-100" placeholder="Search..."
leaveTo="opacity-0" onChange={(e) => setQuery(e.target.value)}
> />
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-surface-1 shadow-raised-200 transition-all">
<Combobox value={value} onChange={handleSubmit}>
<div className="relative m-1">
<Search
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40"
aria-hidden="true"
/>
<input
type="text"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-primary outline-none focus:ring-0 sm:text-13"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<Combobox.Options static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto">
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
) : (
<>{issueList}</>
)}
</Combobox.Options>
</Combobox>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</div> </div>
</div>
</Transition.Root> <Combobox.Options static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto">
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
) : (
<>{issueList}</>
)}
</Combobox.Options>
</Combobox>
</ModalCore>
); );
} }

View file

@ -1,10 +1,9 @@
import type { FC } from "react"; import { useState } from "react";
import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { Calendar } from "@plane/propel/calendar"; import { Calendar } from "@plane/propel/calendar";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
export type InboxIssueSnoozeModalProps = { export type InboxIssueSnoozeModalProps = {
isOpen: boolean; isOpen: boolean;
@ -21,63 +20,34 @@ export function InboxIssueSnoozeModal(props: InboxIssueSnoozeModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <div className="flex h-full w-full flex-col gap-y-1 px-5 py-8 sm:p-6">
<Transition.Child <Calendar
as={Fragment} className="rounded-md border border-subtle p-3"
enter="ease-out duration-300" captionLayout="dropdown"
enterFrom="opacity-0" selected={date ? new Date(date) : undefined}
enterTo="opacity-100" defaultMonth={date ? new Date(date) : undefined}
leave="ease-in duration-200" onSelect={(date: Date | undefined) => {
leaveFrom="opacity-100" if (!date) return;
leaveTo="opacity-0" setDate(date);
}}
mode="single"
disabled={[
{
before: new Date(),
},
]}
/>
<Button
variant="primary"
onClick={() => {
handleClose();
onConfirm(date);
}}
> >
<div className="fixed inset-0 bg-backdrop transition-opacity" /> {t("inbox_issue.actions.snooze")}
</Transition.Child> </Button>
<div className="fixed inset-0 z-20 flex w-full justify-center overflow-y-auto"> </div>
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> </ModalCore>
<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 flex transform rounded-lg bg-surface-1 px-5 py-8 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<div className="flex h-full w-full flex-col gap-y-1">
<Calendar
className="rounded-md border border-subtle p-3"
captionLayout="dropdown"
selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined}
onSelect={(date: Date | undefined) => {
if (!date) return;
setDate(date);
}}
mode="single"
disabled={[
{
before: new Date(),
},
]}
/>
<Button
variant="primary"
onClick={() => {
close();
onConfirm(date);
}}
>
{t("inbox_issue.actions.snooze")}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
} }

View file

@ -1,23 +1,15 @@
import React, { useState } from "react"; import { useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { mutate } from "swr"; import { mutate } from "swr";
// icons
// headless ui
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// services // services
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IImporterService } from "@plane/types"; import type { IUser, IImporterService } from "@plane/types";
import { Input } from "@plane/ui"; import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys"; import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys";
import { IntegrationService } from "@/services/integrations/integration.service"; import { IntegrationService } from "@/services/integrations/integration.service";
// ui
// icons
// types
// fetch-keys
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -64,85 +56,55 @@ export function DeleteImportModal({ isOpen, handleClose, data }: Props) {
if (!data) return <></>; if (!data) return <></>;
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <div className="flex flex-col gap-6 p-6">
<Transition.Child <div className="flex w-full items-center justify-start gap-6">
as={React.Fragment} <span className="place-items-center rounded-full bg-red-500/20 p-4">
enter="ease-out duration-300" <AlertTriangle className="h-6 w-6 text-red-500" aria-hidden="true" />
enterFrom="opacity-0" </span>
enterTo="opacity-100" <span className="flex items-center justify-start">
leave="ease-in duration-200" <h3 className="text-18 font-medium 2xl:text-20">Delete project</h3>
leaveFrom="opacity-100" </span>
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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-surface-1 text-left shadow-raised-200 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-500" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-18 font-medium 2xl:text-20">Delete project</h3>
</span>
</div>
<span>
<p className="text-13 leading-7 text-secondary">
Are you sure you want to delete import from{" "}
<span className="break-words font-semibold capitalize text-primary">{data?.service}</span>? All of
the data related to the import will be permanently removed. This action cannot be undone.
</p>
</span>
<div>
<p className="text-13 text-secondary">
To confirm, type <span className="font-medium text-primary">delete import</span> below:
</p>
<Input
id="typeDelete"
type="text"
name="typeDelete"
onChange={(e) => {
if (e.target.value === "delete import") setConfirmDeleteImport(true);
else setConfirmDeleteImport(false);
}}
placeholder="Enter 'delete import'"
className="mt-2 w-full"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button
variant="error-fill"
size="lg"
tabIndex={1}
onClick={handleDeletion}
disabled={!confirmDeleteImport}
loading={deleteLoading}
>
{deleteLoading ? "Deleting..." : "Delete Project"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> <span>
</Transition.Root> <p className="text-13 leading-7 text-secondary">
Are you sure you want to delete import from{" "}
<span className="break-words font-semibold capitalize text-primary">{data?.service}</span>? All of the data
related to the import will be permanently removed. This action cannot be undone.
</p>
</span>
<div>
<p className="text-13 text-secondary">
To confirm, type <span className="font-medium text-primary">delete import</span> below:
</p>
<Input
id="typeDelete"
type="text"
name="typeDelete"
onChange={(e) => {
if (e.target.value === "delete import") setConfirmDeleteImport(true);
else setConfirmDeleteImport(false);
}}
placeholder="Enter 'delete import'"
className="mt-2 w-full"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button
variant="error-fill"
size="lg"
tabIndex={1}
onClick={handleDeletion}
disabled={!confirmDeleteImport}
loading={deleteLoading}
>
{deleteLoading ? "Deleting..." : "Delete Project"}
</Button>
</div>
</div>
</ModalCore>
); );
} }

View file

@ -1,11 +1,11 @@
import { useState, Fragment } from "react"; import { useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// i18n // i18n
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TDeDupeIssue, TIssue } from "@plane/types"; import type { TDeDupeIssue, TIssue } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { useIssues } from "@/hooks/store/use-issues"; import { useIssues } from "@/hooks/store/use-issues";
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
@ -49,6 +49,7 @@ export function ArchiveIssueModal(props: Props) {
message: t("issue.archive.success.message"), message: t("issue.archive.success.message"),
}); });
onClose(); onClose();
return;
}) })
.catch(() => .catch(() =>
setToast({ setToast({
@ -61,51 +62,21 @@ export function ArchiveIssueModal(props: Props) {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.LG}>
<Dialog as="div" className="relative z-30" onClose={onClose}> <div className="px-5 py-4">
<Transition.Child <h3 className="text-18 font-medium 2xl:text-20">
as={Fragment} {t("issue.archive.label")} {projectDetails?.identifier} {issue.sequence_id}
enter="ease-out duration-300" </h3>
enterFrom="opacity-0" <p className="mt-3 text-13 text-secondary">{t("issue.archive.confirm_message")}</p>
enterTo="opacity-100" <div className="mt-3 flex justify-end gap-2">
leave="ease-in duration-200" <Button variant="secondary" size="lg" onClick={onClose}>
leaveFrom="opacity-100" {t("common.cancel")}
leaveTo="opacity-0" </Button>
> <Button variant="primary" size="lg" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
<div className="fixed inset-0 bg-backdrop transition-opacity" /> {isArchiving ? t("common.archiving") : t("common.archive")}
</Transition.Child> </Button>
<div className="fixed inset-0 z-30 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-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="px-5 py-4">
<h3 className="text-18 font-medium 2xl:text-20">
{t("issue.archive.label")} {projectDetails?.identifier} {issue.sequence_id}
</h3>
<p className="mt-3 text-13 text-secondary">{t("issue.archive.confirm_message")}</p>
<div className="mt-3 flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={onClose}>
{t("common.cancel")}
</Button>
<Button variant="primary" size="lg" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
{isArchiving ? t("common.archiving") : t("common.archive")}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> </div>
</Transition.Root> </ModalCore>
); );
} }

View file

@ -1,9 +1,7 @@
import React, { useState } from "react"; import { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -29,66 +27,34 @@ export function ConfirmIssueDiscard(props: 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-30" onClose={handleClose}> <div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<Transition.Child <div className="sm:flex sm:items-start">
as={React.Fragment} <div className="mt-3 text-center sm:mt-0 sm:text-left">
enter="ease-out duration-300" <h3 className="text-16 font-medium leading-6 text-primary">Save this draft?</h3>
enterFrom="opacity-0" <div className="mt-2">
enterTo="opacity-100" <p className="text-13 text-secondary">
leave="ease-in duration-200" You can save this work item to Drafts so you can come back to it later.{" "}
leaveFrom="opacity-100" </p>
leaveTo="opacity-0" </div>
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
<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-surface-1 text-left shadow-raised-200 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="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary">
Save this draft?
</Dialog.Title>
<div className="mt-2">
<p className="text-13 text-secondary">
You can save this work item to Drafts so you can come back to it later.{" "}
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-between gap-2 p-4 sm:px-6">
<div>
<Button variant="secondary" onClick={onDiscard}>
Discard
</Button>
</div>
<div className="flex items-center gap-2">
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" onClick={handleDeletion} loading={isLoading}>
{isLoading ? "Saving" : "Save to Drafts"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</div> </div>
</Dialog> </div>
</Transition.Root> <div className="flex justify-between gap-2 p-4 sm:px-6">
<div>
<Button variant="secondary" onClick={onDiscard}>
Discard
</Button>
</div>
<div className="flex items-center gap-2">
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" onClick={handleDeletion} loading={isLoading}>
{isLoading ? "Saving" : "Save to Drafts"}
</Button>
</div>
</div>
</ModalCore>
); );
} }

View file

@ -3,13 +3,13 @@ import { useParams } from "next/navigation";
// icons // icons
import { Rocket, Search } from "lucide-react"; import { Rocket, Search } from "lucide-react";
// headless ui // headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// i18n // i18n
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import type { ISearchIssueResponse } from "@plane/types"; import type { ISearchIssueResponse } from "@plane/types";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { generateWorkItemLink, getTabIndex } from "@plane/utils"; import { generateWorkItemLink, getTabIndex } from "@plane/utils";
// components // components
import { IssueSearchModalEmptyState } from "@/components/core/modals/issue-search-modal-empty-state"; import { IssueSearchModalEmptyState } from "@/components/core/modals/issue-search-modal-empty-state";
@ -85,144 +85,111 @@ export function ParentIssuesListModal({
}, [debouncedSearchTerm, isOpen, issueId, projectId, workspaceSlug]); }, [debouncedSearchTerm, isOpen, issueId, projectId, workspaceSlug]);
return ( return (
<> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear> <Combobox
<Dialog as="div" className="relative z-30" onClose={handleClose}> value={value}
<Transition.Child onChange={(val) => {
as={React.Fragment} onChange(val);
enter="ease-out duration-300" handleClose();
enterFrom="opacity-0" }}
enterTo="opacity-100" >
leave="ease-in duration-200" <div className="relative m-1">
leaveFrom="opacity-100" <Search
leaveTo="opacity-0" className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40"
> aria-hidden="true"
<div className="fixed inset-0 bg-backdrop transition-opacity" /> />
</Transition.Child> <Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-primary outline-none placeholder:text-placeholder focus:ring-0 sm:text-13"
placeholder={t("common.search.placeholder")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
displayValue={() => ""}
tabIndex={baseTabIndex}
/>
</div>
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto vertical-scrollbar scrollbar-md">
{searchTerm !== "" && (
<h5 className="mx-2 text-13 text-secondary">
Search results for{" "}
<span className="text-primary">
{'"'}
{searchTerm}
{'"'}
</span>{" "}
in project:
</h5>
)}
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20"> {isSearching || isLoading ? (
<Transition.Child <Loader className="space-y-3 p-3">
as={React.Fragment} <Loader.Item height="40px" />
enter="ease-out duration-300" <Loader.Item height="40px" />
enterFrom="opacity-0 scale-95" <Loader.Item height="40px" />
enterTo="opacity-100 scale-100" <Loader.Item height="40px" />
leave="ease-in duration-200" </Loader>
leaveFrom="opacity-100 scale-100" ) : (
leaveTo="opacity-0 scale-95" <>
> {issues.length === 0 ? (
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-surface-1 shadow-raised-200 transition-all"> <IssueSearchModalEmptyState
<Combobox debouncedSearchTerm={debouncedSearchTerm}
value={value} isSearching={isSearching}
onChange={(val) => { issues={issues}
onChange(val); searchTerm={searchTerm}
handleClose(); />
}} ) : (
> <ul className={`text-13 ${issues.length > 0 ? "p-2" : ""}`}>
<div className="relative m-1"> {issues.map((issue) => (
<Search <Combobox.Option
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40" key={issue.id}
aria-hidden="true" value={issue}
/> className={({ active, selected }) =>
<Combobox.Input `group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-secondary ${
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-primary outline-none placeholder:text-placeholder focus:ring-0 sm:text-13" active ? "bg-layer-1 text-primary" : ""
placeholder={t("common.search.placeholder")} } ${selected ? "text-primary" : ""}`
value={searchTerm} }
onChange={(e) => setSearchTerm(e.target.value)} >
displayValue={() => ""} <div className="flex flex-grow items-center gap-2 truncate">
tabIndex={baseTabIndex} <span
/> className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
</div> style={{
<Combobox.Options backgroundColor: issue.state__color,
static }}
className="max-h-80 scroll-py-2 overflow-y-auto vertical-scrollbar scrollbar-md" />
> <span className="flex-shrink-0">
{searchTerm !== "" && ( <IssueIdentifier
<h5 className="mx-2 text-13 text-secondary"> projectId={issue.project_id}
Search results for{" "} issueTypeId={issue.type_id}
<span className="text-primary"> projectIdentifier={issue.project__identifier}
{'"'} issueSequenceId={issue.sequence_id}
{searchTerm} size="xs"
{'"'} variant="secondary"
</span>{" "}
in project:
</h5>
)}
{isSearching || isLoading ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
) : (
<>
{issues.length === 0 ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={issues}
searchTerm={searchTerm}
/> />
) : ( </span>{" "}
<ul className={`text-13 ${issues.length > 0 ? "p-2" : ""}`}> <span className="truncate">{issue.name}</span>
{issues.map((issue) => ( </div>
<Combobox.Option <a
key={issue.id} href={generateWorkItemLink({
value={issue} workspaceSlug: workspaceSlug.toString(),
className={({ active, selected }) => projectId: issue?.project_id,
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-secondary ${ issueId: issue?.id,
active ? "bg-layer-1 text-primary" : "" projectIdentifier: issue.project__identifier,
} ${selected ? "text-primary" : ""}` sequenceId: issue?.sequence_id,
} })}
> target="_blank"
<div className="flex flex-grow items-center gap-2 truncate"> className="z-1 relative hidden flex-shrink-0 text-secondary hover:text-primary group-hover:block"
<span rel="noopener noreferrer"
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full" onClick={(e) => e.stopPropagation()}
style={{ >
backgroundColor: issue.state__color, <Rocket className="h-4 w-4" />
}} </a>
/> </Combobox.Option>
<span className="flex-shrink-0"> ))}
<IssueIdentifier </ul>
projectId={issue.project_id} )}
issueTypeId={issue.type_id} </>
projectIdentifier={issue.project__identifier} )}
issueSequenceId={issue.sequence_id} </Combobox.Options>
size="xs" </Combobox>
variant="secondary" </ModalCore>
/>
</span>{" "}
<span className="truncate">{issue.name}</span>
</div>
<a
href={generateWorkItemLink({
workspaceSlug: workspaceSlug.toString(),
projectId: issue?.project_id,
issueId: issue?.id,
projectIdentifier: issue.project__identifier,
sequenceId: issue?.sequence_id,
})}
target="_blank"
className="z-1 relative hidden flex-shrink-0 text-secondary hover:text-primary group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
))}
</ul>
)}
</>
)}
</Combobox.Options>
</Combobox>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</>
); );
} }

View file

@ -1,8 +1,8 @@
import { useState, Fragment } from "react"; import { useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { useModule } from "@/hooks/store/use-module"; import { useModule } from "@/hooks/store/use-module";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
@ -43,6 +43,7 @@ export function ArchiveModuleModal(props: Props) {
}); });
onClose(); onClose();
router.push(`/${workspaceSlug}/projects/${projectId}/modules`); router.push(`/${workspaceSlug}/projects/${projectId}/modules`);
return;
}) })
.catch(() => .catch(() =>
setToast({ setToast({
@ -55,57 +56,21 @@ export function ArchiveModuleModal(props: Props) {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.LG}>
<Dialog as="div" className="relative z-20" onClose={onClose}> <div className="px-5 py-4">
<Transition.Child <h3 className="text-18 font-medium 2xl:text-20">Archive module {moduleName}</h3>
as={Fragment} <p className="mt-3 text-13 text-secondary">
enter="ease-out duration-300" Are you sure you want to archive the module? All your archives can be restored later.
enterFrom="opacity-0" </p>
enterTo="opacity-100" <div className="mt-3 flex justify-end gap-2">
leave="ease-in duration-200" <Button variant="secondary" size="lg" onClick={onClose}>
leaveFrom="opacity-100" Cancel
leaveTo="opacity-0" </Button>
> <Button variant="primary" size="lg" tabIndex={1} onClick={handleArchiveModule} loading={isArchiving}>
<div className="fixed inset-0 bg-backdrop transition-opacity" /> {isArchiving ? "Archiving" : "Archive"}
</Transition.Child> </Button>
<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-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="px-5 py-4">
<h3 className="text-18 font-medium 2xl:text-20">Archive module {moduleName}</h3>
<p className="mt-3 text-13 text-secondary">
Are you sure you want to archive the module? All your archives can be restored later.
</p>
<div className="mt-3 flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={onClose}>
Cancel
</Button>
<Button
variant="primary"
size="lg"
tabIndex={1}
onClick={handleArchiveModule}
loading={isArchiving}
>
{isArchiving ? "Archiving" : "Archive"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> </div>
</Transition.Root> </ModalCore>
); );
} }

View file

@ -1,13 +1,12 @@
import React, { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// types // types
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import type { IUserLite } from "@plane/types"; import type { IUserLite } from "@plane/types";
// ui // ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
@ -48,86 +47,42 @@ export const ConfirmProjectMemberRemove = observer(function ConfirmProjectMember
const currentProjectDetails = getProjectById(projectId.toString()); const currentProjectDetails = getProjectById(projectId.toString());
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <div className="bg-surface-1 px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<Transition.Child <div className="sm:flex sm:items-start">
as={React.Fragment} <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">
enter="ease-out duration-300" <AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
enterFrom="opacity-0" </div>
enterTo="opacity-100" <div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
leave="ease-in duration-200" <h3 className="text-16 font-medium leading-6 text-primary">
leaveFrom="opacity-100" {isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}
leaveTo="opacity-0" </h3>
> <div className="mt-2">
<div className="fixed inset-0 bg-backdrop transition-opacity" /> <p className="text-13 text-secondary">
</Transition.Child> {isCurrentUser ? (
<>
<div className="fixed inset-0 z-20 overflow-y-auto"> Are you sure you want to leave the <span className="font-bold">{currentProjectDetails?.name}</span>{" "}
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> project? You will be able to join the project if invited again or if it{"'"}s public.
<Transition.Child </>
as={React.Fragment} ) : (
enter="ease-out duration-300" <>
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" Are you sure you want to remove member- <span className="font-bold">{data?.display_name}</span>?
enterTo="opacity-100 translate-y-0 sm:scale-100" They will no longer have access to this project. This action cannot be undone.
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" </p>
> </div>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-surface-1 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-16 font-medium leading-6 text-primary">
{isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}
</Dialog.Title>
<div className="mt-2">
<p className="text-13 text-secondary">
{isCurrentUser ? (
<>
Are you sure you want to leave the{" "}
<span className="font-bold">{currentProjectDetails?.name}</span> project? You will be able
to join the project if invited again or if it{"'"}s public.
</>
) : (
<>
Are you sure you want to remove member-{" "}
<span className="font-bold">{data?.display_name}</span>? They will no longer have access
to this project. This action cannot be undone.
</>
)}
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button
variant="error-fill"
size="lg"
tabIndex={1}
onClick={handleDeletion}
loading={isDeleteLoading}
>
{isCurrentUser
? isDeleteLoading
? "Leaving..."
: "Leave"
: isDeleteLoading
? "Removing..."
: "Remove"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</div> </div>
</Dialog> </div>
</Transition.Root> <div className="flex justify-end gap-2 p-4 sm:px-6">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="error-fill" size="lg" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
{isCurrentUser ? (isDeleteLoading ? "Leaving..." : "Leave") : isDeleteLoading ? "Removing..." : "Remove"}
</Button>
</div>
</ModalCore>
); );
}); });

View file

@ -1,15 +1,13 @@
import React from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// types // types
import { PROJECT_TRACKER_EVENTS } from "@plane/constants"; import { PROJECT_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IProject } from "@plane/types"; import type { IProject } from "@plane/types";
// ui // ui
import { Input } from "@plane/ui"; import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// constants // constants
// hooks // hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
@ -90,109 +88,77 @@ export function DeleteProjectModal(props: DeleteProjectModal) {
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<Transition.Child <div className="flex w-full items-center justify-start gap-6">
as={React.Fragment} <span className="place-items-center rounded-full bg-red-500/20 p-4">
enter="ease-out duration-300" <AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
enterFrom="opacity-0" </span>
enterTo="opacity-100" <span className="flex items-center justify-start">
leave="ease-in duration-200" <h3 className="text-18 font-medium 2xl:text-20">Delete project</h3>
leaveFrom="opacity-100" </span>
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)} 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-18 font-medium 2xl:text-20">Delete project</h3>
</span>
</div>
<span>
<p className="text-13 leading-7 text-secondary">
Are you sure you want to delete project{" "}
<span className="break-words font-semibold">{project?.name}</span>? All of the data related to the
project will be permanently removed. This action cannot be undone
</p>
</span>
<div className="text-secondary">
<p className="break-words text-13 ">
Enter the project name <span className="font-medium text-primary">{project?.name}</span> to
continue:
</p>
<Controller
control={control}
name="projectName"
render={({ field: { value, onChange, ref } }) => (
<Input
id="projectName"
name="projectName"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.projectName)}
placeholder="Project name"
className="mt-2 w-full"
autoComplete="off"
/>
)}
/>
</div>
<div className="text-secondary">
<p className="text-13">
To confirm, type <span className="font-medium text-primary">delete my project</span> below:
</p>
<Controller
control={control}
name="confirmDelete"
render={({ field: { value, onChange, ref } }) => (
<Input
id="confirmDelete"
name="confirmDelete"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.confirmDelete)}
placeholder="Enter 'delete my project'"
className="mt-2 w-full"
autoComplete="off"
/>
)}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="error-fill" size="lg" type="submit" disabled={!canDelete} loading={isSubmitting}>
{isSubmitting ? "Deleting" : "Delete project"}
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> <span>
</Transition.Root> <p className="text-13 leading-7 text-secondary">
Are you sure you want to delete project <span className="break-words font-semibold">{project?.name}</span>?
All of the data related to the project will be permanently removed. This action cannot be undone
</p>
</span>
<div className="text-secondary">
<p className="break-words text-13 ">
Enter the project name <span className="font-medium text-primary">{project?.name}</span> to continue:
</p>
<Controller
control={control}
name="projectName"
render={({ field: { value, onChange, ref } }) => (
<Input
id="projectName"
name="projectName"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.projectName)}
placeholder="Project name"
className="mt-2 w-full"
autoComplete="off"
/>
)}
/>
</div>
<div className="text-secondary">
<p className="text-13">
To confirm, type <span className="font-medium text-primary">delete my project</span> below:
</p>
<Controller
control={control}
name="confirmDelete"
render={({ field: { value, onChange, ref } }) => (
<Input
id="confirmDelete"
name="confirmDelete"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.confirmDelete)}
placeholder="Enter 'delete my project'"
className="mt-2 w-full"
autoComplete="off"
/>
)}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="error-fill" size="lg" type="submit" disabled={!canDelete} loading={isSubmitting}>
{isSubmitting ? "Deleting" : "Delete project"}
</Button>
</div>
</form>
</ModalCore>
); );
} }

View file

@ -1,10 +1,9 @@
import { useState, Fragment } from "react"; import { useState } from "react";
import { Transition, Dialog } from "@headlessui/react";
// types // types
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import type { IProject } from "@plane/types"; import type { IProject } from "@plane/types";
// ui // ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
@ -26,13 +25,17 @@ export function JoinProjectModal(props: TJoinProjectModalProps) {
// router // router
const router = useAppRouter(); const router = useAppRouter();
const handleJoin = () => { const handleJoin = async () => {
setIsJoiningLoading(true); setIsJoiningLoading(true);
joinProject(workspaceSlug, project.id) await joinProject(workspaceSlug, project.id)
.then(() => { .then(() => {
router.push(`/${workspaceSlug}/projects/${project.id}/issues`); router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
handleClose(); handleClose();
return;
})
.catch(() => {
console.error("Error joining project");
}) })
.finally(() => { .finally(() => {
setIsJoiningLoading(false); setIsJoiningLoading(false);
@ -40,63 +43,23 @@ export function JoinProjectModal(props: TJoinProjectModalProps) {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <div className="space-y-5 px-5 py-8 sm:p-6">
<Transition.Child <h3 className="text-16 font-medium leading-6 text-primary">Join Project?</h3>
as={Fragment} <p>
enter="ease-out duration-300" Are you sure you want to join the project <span className="break-words font-semibold">{project?.name}</span>?
enterFrom="opacity-0" Please click the &apos;Join Project&apos; button below to continue.
enterTo="opacity-100" </p>
leave="ease-in duration-200" <div className="space-y-3" />
leaveFrom="opacity-100" </div>
leaveTo="opacity-0" <div className="mt-5 flex justify-end gap-2 px-5 pb-8 sm:px-6 sm:pb-6">
> <Button variant="secondary" size="lg" onClick={handleClose}>
<div className="fixed inset-0 bg-backdrop transition-opacity" /> Cancel
</Transition.Child> </Button>
<Button variant="primary" size="lg" tabIndex={1} type="submit" onClick={handleJoin} loading={isJoiningLoading}>
<div className="fixed inset-0 z-20 overflow-y-auto"> {isJoiningLoading ? "Joining..." : "Join Project"}
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0"> </Button>
<Transition.Child </div>
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 overflow-hidden rounded-lg bg-surface-1 px-5 py-8 text-left shadow-raised-200 transition-all sm:w-full sm:max-w-xl sm:p-6">
<div className="space-y-5">
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary">
Join Project?
</Dialog.Title>
<p>
Are you sure you want to join the project{" "}
<span className="break-words font-semibold">{project?.name}</span>? Please click the &apos;Join
Project&apos; button below to continue.
</p>
<div className="space-y-3" />
</div>
<div className="mt-5 flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button
variant="primary"
size="lg"
tabIndex={1}
type="submit"
onClick={handleJoin}
loading={isJoiningLoading}
>
{isJoiningLoading ? "Joining..." : "Join Project"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
); );
} }

View file

@ -1,17 +1,15 @@
import { Fragment } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// headless ui // headless ui
import { AlertTriangleIcon } from "lucide-react"; import { AlertTriangleIcon } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// types // types
import { MEMBER_TRACKER_EVENTS } from "@plane/constants"; import { MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IProject } from "@plane/types"; import type { IProject } from "@plane/types";
// ui // ui
import { Input } from "@plane/ui"; import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// constants // constants
// hooks // hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
@ -109,113 +107,82 @@ export const LeaveProjectModal = observer(function LeaveProjectModal(props: ILea
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<Transition.Child <div className="flex w-full items-center justify-start gap-6">
as={Fragment} <span className="place-items-center rounded-full bg-red-500/20 p-4">
enter="ease-out duration-300" <AlertTriangleIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
enterFrom="opacity-0" </span>
enterTo="opacity-100" <span className="flex items-center justify-start">
leave="ease-in duration-200" <h3 className="text-18 font-medium 2xl:text-20">Leave Project</h3>
leaveFrom="opacity-100" </span>
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)} 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">
<AlertTriangleIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-18 font-medium 2xl:text-20">Leave Project</h3>
</span>
</div>
<span>
<p className="text-13 leading-7 text-secondary">
Are you sure you want to leave the project -
<span className="font-medium text-primary">{` "${project?.name}" `}</span>? All of the work items
associated with you will become inaccessible.
</p>
</span>
<div className="text-secondary">
<p className="break-words text-13 ">
Enter the project name <span className="font-medium text-primary">{project?.name}</span> to
continue:
</p>
<Controller
control={control}
name="projectName"
rules={{
required: "Label title is required",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="projectName"
name="projectName"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.projectName)}
placeholder="Enter project name"
className="mt-2 w-full"
/>
)}
/>
</div>
<div className="text-secondary">
<p className="text-13">
To confirm, type <span className="font-medium text-primary">Leave Project</span> below:
</p>
<Controller
control={control}
name="confirmLeave"
render={({ field: { value, onChange, ref } }) => (
<Input
id="confirmLeave"
name="confirmLeave"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.confirmLeave)}
placeholder="Enter 'leave project'"
className="mt-2 w-full"
/>
)}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="error-fill" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting ? "Leaving..." : "Leave Project"}
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog>
</Transition.Root> <span>
<p className="text-13 leading-7 text-secondary">
Are you sure you want to leave the project -
<span className="font-medium text-primary">{` "${project?.name}" `}</span>? All of the work items associated
with you will become inaccessible.
</p>
</span>
<div className="text-secondary">
<p className="break-words text-13 ">
Enter the project name <span className="font-medium text-primary">{project?.name}</span> to continue:
</p>
<Controller
control={control}
name="projectName"
rules={{
required: "Label title is required",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="projectName"
name="projectName"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.projectName)}
placeholder="Enter project name"
className="mt-2 w-full"
/>
)}
/>
</div>
<div className="text-secondary">
<p className="text-13">
To confirm, type <span className="font-medium text-primary">Leave Project</span> below:
</p>
<Controller
control={control}
name="confirmLeave"
render={({ field: { value, onChange, ref } }) => (
<Input
id="confirmLeave"
name="confirmLeave"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.confirmLeave)}
placeholder="Enter 'leave project'"
className="mt-2 w-full"
/>
)}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="error-fill" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting ? "Leaving..." : "Leave Project"}
</Button>
</div>
</form>
</ModalCore>
); );
}); });

View file

@ -2,14 +2,13 @@ import React, { useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useForm, Controller, useFieldArray } from "react-hook-form"; import { useForm, Controller, useFieldArray } from "react-hook-form";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports // plane imports
import { ROLE, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants"; import { ROLE, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { CloseIcon, ChevronDownIcon } from "@plane/propel/icons"; import { CloseIcon, ChevronDownIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Avatar, CustomSelect, CustomSearchSelect } from "@plane/ui"; import { Avatar, CustomSelect, CustomSearchSelect, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// helpers // helpers
import { getFileURL } from "@plane/utils"; import { getFileURL } from "@plane/utils";
// hooks // hooks
@ -183,182 +182,140 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio
}; };
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <form onSubmit={handleSubmit(onSubmit)} className="p-5">
<Transition.Child <div className="space-y-5">
as={React.Fragment} <h3 className="text-16 font-medium leading-6 text-primary">
enter="ease-out duration-300" {t("project_settings.members.invite_members.title")}
enterFrom="opacity-0" </h3>
enterTo="opacity-100" <div className="mt-2">
leave="ease-in duration-200" <p className="text-13 text-secondary">{t("project_settings.members.invite_members.sub_heading")}</p>
leaveFrom="opacity-100" </div>
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto"> <div className="mb-3 space-y-4">
<div className="flex min-h-full items-center justify-center p-4 text-center"> {fields.map((field, index) => (
<Transition.Child <div key={field.id} className="group mb-1 flex items-start justify-between gap-x-4 text-13 w-full">
as={React.Fragment} <div className="flex flex-col gap-1 flex-grow w-full">
enter="ease-out duration-300" <Controller
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" control={control}
enterTo="opacity-100 translate-y-0 sm:scale-100" name={`members.${index}.member_id`}
leave="ease-in duration-200" rules={{ required: "Please select a member" }}
leaveFrom="opacity-100 translate-y-0 sm:scale-100" render={({ field: { value, onChange } }) => {
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" const selectedMember = getWorkspaceMemberDetails(value);
> return (
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 p-5 text-left shadow-raised-200 transition-all sm:w-full sm:max-w-2xl"> <CustomSearchSelect
<form onSubmit={handleSubmit(onSubmit)}> value={value}
<div className="space-y-5"> customButton={
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary"> <button className="flex w-full items-center justify-between gap-1 rounded-md border border-subtle px-3 py-2 text-left text-13 text-secondary shadow-sm duration-300 hover:bg-layer-1 hover:text-primary focus:outline-none">
{t("project_settings.members.invite_members.title")} {value && value !== "" ? (
</Dialog.Title> <div className="flex items-center gap-2">
<div className="mt-2"> <Avatar
<p className="text-13 text-secondary"> name={selectedMember?.member.display_name}
{t("project_settings.members.invite_members.sub_heading")} src={getFileURL(selectedMember?.member.avatar_url ?? "")}
</p>
</div>
<div className="mb-3 space-y-4">
{fields.map((field, index) => (
<div
key={field.id}
className="group mb-1 flex items-start justify-between gap-x-4 text-13 w-full"
>
<div className="flex flex-col gap-1 flex-grow w-full">
<Controller
control={control}
name={`members.${index}.member_id`}
rules={{ required: "Please select a member" }}
render={({ field: { value, onChange } }) => {
const selectedMember = getWorkspaceMemberDetails(value);
return (
<CustomSearchSelect
value={value}
customButton={
<button className="flex w-full items-center justify-between gap-1 rounded-md border border-subtle px-3 py-2 text-left text-13 text-secondary shadow-sm duration-300 hover:bg-layer-1 hover:text-primary focus:outline-none">
{value && value !== "" ? (
<div className="flex items-center gap-2">
<Avatar
name={selectedMember?.member.display_name}
src={getFileURL(selectedMember?.member.avatar_url ?? "")}
/>
{selectedMember?.member.display_name}
</div>
) : (
<div className="flex items-center gap-2 py-0.5">Select co-worker</div>
)}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</button>
}
onChange={(val: string) => {
onChange(val);
// Update the role to the workspace role when member ID changes
const workspaceMemberDetails = getWorkspaceMemberDetails(val);
const workspaceRole = workspaceMemberDetails?.role ?? 5;
const newValue = ROLE[workspaceRole].toUpperCase();
setValue(
`members.${index}.role`,
EUserPermissions[newValue as keyof typeof EUserPermissions]
);
}}
options={options}
optionsClassName="w-48"
/> />
); {selectedMember?.member.display_name}
}} </div>
/> ) : (
{errors.members && errors.members[index]?.member_id && ( <div className="flex items-center gap-2 py-0.5">Select co-worker</div>
<span className="px-1 text-13 text-red-500">
{errors.members[index]?.member_id?.message}
</span>
)}
</div>
<div className="flex items-center justify-between gap-2 flex-shrink-0 ">
<div className="flex flex-col gap-1">
<Controller
name={`members.${index}.role`}
control={control}
rules={{ required: "Select Role" }}
render={({ field }) => (
<CustomSelect
{...field}
customButton={
<div className="flex w-24 items-center justify-between gap-1 rounded-md border border-subtle px-3 py-2.5 text-left text-13 text-secondary shadow-sm duration-300 hover:bg-layer-1 hover:text-primary focus:outline-none">
<span className="capitalize">
{field.value ? ROLE[field.value] : "Select role"}
</span>
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div>
}
input
>
{Object.entries(
checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))
).map(([key, label]) => {
if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null;
return (
<CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
);
})}
</CustomSelect>
)}
/>
{errors.members && errors.members[index]?.role && (
<span className="px-1 text-13 text-red-500">
{errors.members[index]?.role?.message}
</span>
)} )}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</button>
}
onChange={(val: string) => {
onChange(val);
// Update the role to the workspace role when member ID changes
const workspaceMemberDetails = getWorkspaceMemberDetails(val);
const workspaceRole = workspaceMemberDetails?.role ?? 5;
const newValue = ROLE[workspaceRole].toUpperCase();
setValue(
`members.${index}.role`,
EUserPermissions[newValue as keyof typeof EUserPermissions]
);
}}
options={options}
optionsClassName="w-48"
/>
);
}}
/>
{errors.members && errors.members[index]?.member_id && (
<span className="px-1 text-13 text-red-500">{errors.members[index]?.member_id?.message}</span>
)}
</div>
<div className="flex items-center justify-between gap-2 flex-shrink-0 ">
<div className="flex flex-col gap-1">
<Controller
name={`members.${index}.role`}
control={control}
rules={{ required: "Select Role" }}
render={({ field }) => (
<CustomSelect
{...field}
customButton={
<div className="flex w-24 items-center justify-between gap-1 rounded-md border border-subtle px-3 py-2.5 text-left text-13 text-secondary shadow-sm duration-300 hover:bg-layer-1 hover:text-primary focus:outline-none">
<span className="capitalize">{field.value ? ROLE[field.value] : "Select role"}</span>
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</div> </div>
}
input
>
{Object.entries(checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))).map(
([key, label]) => {
if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null;
{fields.length > 1 && ( return (
<div className="flex-item flex w-6"> <CustomSelect.Option key={key} value={key}>
<button {label}
type="button" </CustomSelect.Option>
className="place-items-center self-center rounded-sm" );
onClick={() => remove(index)} }
> )}
<CloseIcon className="h-4 w-4 text-secondary" /> </CustomSelect>
</button> )}
</div> />
)} {errors.members && errors.members[index]?.role && (
</div> <span className="px-1 text-13 text-red-500">{errors.members[index]?.role?.message}</span>
</div> )}
))}
</div>
</div> </div>
<div className="mt-5 flex items-center justify-between gap-2"> {fields.length > 1 && (
<button <div className="flex-item flex w-6">
type="button" <button
className="flex items-center gap-2 bg-transparent py-2 pr-3 text-13 font-medium text-accent-primary outline-accent-strong" type="button"
onClick={appendField} className="place-items-center self-center rounded-sm"
> onClick={() => remove(index)}
<Plus className="h-4 w-4" /> >
{t("common.add_more")} <CloseIcon className="h-4 w-4 text-secondary" />
</button> </button>
<div className="flex items-center gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
{t("cancel")}
</Button>
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting
? `${fields && fields.length > 1 ? `${t("add_members")}...` : `${t("add_member")}...`}`
: `${fields && fields.length > 1 ? t("add_members") : t("add_member")}`}
</Button>
</div> </div>
</div> )}
</form> </div>
</Dialog.Panel> </div>
</Transition.Child> ))}
</div> </div>
</div> </div>
</Dialog> <div className="mt-5 flex items-center justify-between gap-2">
</Transition.Root> <button
type="button"
className="flex items-center gap-2 bg-transparent py-2 pr-3 text-13 font-medium text-accent-primary outline-accent-strong"
onClick={appendField}
>
<Plus className="h-4 w-4" />
{t("common.add_more")}
</button>
<div className="flex items-center gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
{t("cancel")}
</Button>
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting
? `${fields && fields.length > 1 ? `${t("add_members")}...` : `${t("add_member")}...`}`
: `${fields && fields.length > 1 ? t("add_members") : t("add_member")}`}
</Button>
</div>
</div>
</form>
</ModalCore>
); );
}); });

View file

@ -1,8 +1,8 @@
import { useState, Fragment } from "react"; import { useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
@ -44,6 +44,7 @@ export function ArchiveRestoreProjectModal(props: Props) {
}); });
onClose(); onClose();
router.push(`/${workspaceSlug}/projects/`); router.push(`/${workspaceSlug}/projects/`);
return;
}) })
.catch(() => .catch(() =>
setToast({ setToast({
@ -66,6 +67,7 @@ export function ArchiveRestoreProjectModal(props: Props) {
}); });
onClose(); onClose();
router.push(`/${workspaceSlug}/projects/`); router.push(`/${workspaceSlug}/projects/`);
return;
}) })
.catch(() => .catch(() =>
setToast({ setToast({
@ -78,61 +80,31 @@ export function ArchiveRestoreProjectModal(props: Props) {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.LG}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <div className="px-5 py-4">
<Transition.Child <h3 className="text-18 font-medium 2xl:text-20">
as={Fragment} {archive ? "Archive" : "Restore"} {projectDetails.name}
enter="ease-out duration-300" </h3>
enterFrom="opacity-0" <p className="mt-3 text-13 text-secondary">
enterTo="opacity-100" {archive
leave="ease-in duration-200" ? "This project and its work items, cycles, modules, and pages will be archived. Its work items won't appear in search. Only project admins can restore the project."
leaveFrom="opacity-100" : "Restoring a project will activate it and make it visible to all members of the project. Are you sure you want to continue?"}
leaveTo="opacity-0" </p>
> <div className="mt-3 flex justify-end gap-2">
<div className="fixed inset-0 bg-backdrop transition-opacity" /> <Button variant="secondary" size="lg" onClick={onClose}>
</Transition.Child> Cancel
</Button>
<div className="fixed inset-0 z-10 overflow-y-auto"> <Button
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> variant="primary"
<Transition.Child size="lg"
as={Fragment} tabIndex={1}
enter="ease-out duration-300" onClick={archive ? handleArchiveProject : handleRestoreProject}
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" loading={isLoading}
enterTo="opacity-100 translate-y-0 sm:scale-100" >
leave="ease-in duration-200" {archive ? (isLoading ? "Archiving" : "Archive") : isLoading ? "Restoring" : "Restore"}
leaveFrom="opacity-100 translate-y-0 sm:scale-100" </Button>
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="px-5 py-4">
<h3 className="text-18 font-medium 2xl:text-20">
{archive ? "Archive" : "Restore"} {projectDetails.name}
</h3>
<p className="mt-3 text-13 text-secondary">
{archive
? "This project and its work items, cycles, modules, and pages will be archived. Its work items wont appear in search. Only project admins can restore the project."
: "Restoring a project will activate it and make it visible to all members of the project. Are you sure you want to continue?"}
</p>
<div className="mt-3 flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={onClose}>
Cancel
</Button>
<Button
variant="primary"
size="lg"
tabIndex={1}
onClick={archive ? handleArchiveProject : handleRestoreProject}
loading={isLoading}
>
{archive ? (isLoading ? "Archiving" : "Archive") : isLoading ? "Restoring" : "Restore"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div> </div>
</Dialog> </div>
</Transition.Root> </ModalCore>
); );
} }

View file

@ -1,16 +1,13 @@
import { Fragment } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { Transition, Dialog } from "@headlessui/react";
// plane imports // plane imports
import { allTimeIn30MinutesInterval12HoursFormat } from "@plane/constants"; import { allTimeIn30MinutesInterval12HoursFormat } from "@plane/constants";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { CloseIcon } from "@plane/propel/icons"; import { CloseIcon } from "@plane/propel/icons";
import { CustomSelect } from "@plane/ui"; import { CustomSelect, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components // components
import { getDate, cn } from "@plane/utils"; import { getDate, cn } from "@plane/utils";
import { DateDropdown } from "@/components/dropdowns/date"; import { DateDropdown } from "@/components/dropdowns/date";
// helpers
type TNotificationSnoozeModal = { type TNotificationSnoozeModal = {
isOpen: boolean; isOpen: boolean;
@ -110,155 +107,117 @@ export function NotificationSnoozeModal(props: TNotificationSnoozeModal) {
}; };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <form onSubmit={handleSubmit(onSubmit)} className="p-5">
<Transition.Child <div className="flex items-center justify-between">
as={Fragment} <h3 className="text-h5-medium leading-6 text-primary">Customize Snooze Time</h3>
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-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto"> <div>
<div className="flex min-h-full items-center justify-center p-4 text-center"> <button type="button" onClick={handleClose}>
<Transition.Child <CloseIcon className="h-5 w-5 text-primary" />
as={Fragment} </button>
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 w-full transform rounded-lg bg-surface-1 p-5 text-left shadow-raised-200 transition-all sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center justify-between">
<Dialog.Title as="h3" className="text-h5-medium leading-6 text-primary">
Customize Snooze Time
</Dialog.Title>
<div>
<button type="button" onClick={handleClose}>
<CloseIcon className="h-5 w-5 text-primary" />
</button>
</div>
</div>
<div className="mt-5 flex flex-col gap-3 md:!flex-row md:items-center">
<div className="flex-1 pb-3 md:pb-0">
<h6 className="mb-2 block text-body-xs-medium text-placeholder">Pick a date</h6>
<Controller
name="date"
control={control}
rules={{ required: "Please select a date" }}
render={({ field: { value, onChange } }) => (
<DateDropdown
value={value || null}
placeholder="Select date"
onChange={(val) => {
setValue("time", undefined);
onChange(val);
}}
minDate={new Date()}
buttonVariant="border-with-text"
buttonContainerClassName="w-full text-left"
buttonClassName="border-strong px-3 py-2.5"
hideIcon
/>
)}
/>
</div>
<div className="flex-1">
<h6 className="mb-2 block text-body-xs-medium text-placeholder">Pick a time</h6>
<Controller
control={control}
name="time"
rules={{ required: "Please select a time" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
<div className="truncate">
{value ? (
<span>
{value} {watch("period").toLowerCase()}
</span>
) : (
<span className="text-body-xs-medium text-placeholder">Select a time</span>
)}
</div>
}
input
>
<div className="mb-2 flex h-9 w-full overflow-hidden rounded-xs">
<div
onClick={() => {
setValue("period", "AM");
}}
className={cn(
"flex h-full w-1/2 cursor-pointer items-center justify-center text-center",
{
"bg-accent-primary/90 text-on-color": watch("period") === "AM",
"bg-layer-1": watch("period") !== "AM",
}
)}
>
AM
</div>
<div
onClick={() => {
setValue("period", "PM");
}}
className={cn(
"flex h-full w-1/2 cursor-pointer items-center justify-center text-center",
{
"bg-accent-primary/90 text-on-color": watch("period") === "PM",
"bg-layer-1": watch("period") !== "PM",
}
)}
>
PM
</div>
</div>
{getTimeStamp().length > 0 ? (
getTimeStamp().map((time, index) => (
<CustomSelect.Option key={`${time}-${index}`} value={time.value}>
<div className="flex items-center">
<span className="ml-3 block truncate">{time.label}</span>
</div>
</CustomSelect.Option>
))
) : (
<p className="p-3 text-center text-secondary">No available time for this date.</p>
)}
</CustomSelect>
)}
/>
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-2">
<div className="flex w-full items-center justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</Button>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</div> </div>
</Dialog>
</Transition.Root> <div className="mt-5 flex flex-col gap-3 md:!flex-row md:items-center">
<div className="flex-1 pb-3 md:pb-0">
<h6 className="mb-2 block text-body-xs-medium text-placeholder">Pick a date</h6>
<Controller
name="date"
control={control}
rules={{ required: "Please select a date" }}
render={({ field: { value, onChange } }) => (
<DateDropdown
value={value || null}
placeholder="Select date"
onChange={(val) => {
setValue("time", undefined);
onChange(val);
}}
minDate={new Date()}
buttonVariant="border-with-text"
buttonContainerClassName="w-full text-left"
buttonClassName="border-strong px-3 py-2.5"
hideIcon
/>
)}
/>
</div>
<div className="flex-1">
<h6 className="mb-2 block text-body-xs-medium text-placeholder">Pick a time</h6>
<Controller
control={control}
name="time"
rules={{ required: "Please select a time" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
<div className="truncate">
{value ? (
<span>
{value} {watch("period").toLowerCase()}
</span>
) : (
<span className="text-body-xs-medium text-placeholder">Select a time</span>
)}
</div>
}
input
>
<div className="mb-2 flex h-9 w-full overflow-hidden rounded-xs">
<div
onClick={() => {
setValue("period", "AM");
}}
className={cn("flex h-full w-1/2 cursor-pointer items-center justify-center text-center", {
"bg-accent-primary/90 text-on-color": watch("period") === "AM",
"bg-layer-1": watch("period") !== "AM",
})}
>
AM
</div>
<div
onClick={() => {
setValue("period", "PM");
}}
className={cn("flex h-full w-1/2 cursor-pointer items-center justify-center text-center", {
"bg-accent-primary/90 text-on-color": watch("period") === "PM",
"bg-layer-1": watch("period") !== "PM",
})}
>
PM
</div>
</div>
{getTimeStamp().length > 0 ? (
getTimeStamp().map((time, index) => (
<CustomSelect.Option key={`${time}-${index}`} value={time.value}>
<div className="flex items-center">
<span className="ml-3 block truncate">{time.label}</span>
</div>
</CustomSelect.Option>
))
) : (
<p className="p-3 text-center text-secondary">No available time for this date.</p>
)}
</CustomSelect>
)}
/>
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-2">
<div className="flex w-full items-center justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</Button>
</div>
</div>
</form>
</ModalCore>
); );
} }