[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 { Dialog, Transition } from "@headlessui/react";
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUser } from "@/hooks/store/user";
@ -47,6 +47,7 @@ export function DeactivateAccountModal(props: Props) {
signOut();
router.push("/");
handleClose();
return;
})
.catch((err: any) => {
captureError({
@ -62,65 +63,30 @@ export function DeactivateAccountModal(props: Props) {
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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-[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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<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>
<h3 className="my-4 text-20 font-medium leading-6 text-primary">{t("deactivate_your_account")}</h3>
<p className="mt-6 list-disc pr-4 text-14 font-regular text-secondary">
{t("deactivate_your_account_description")}
</p>
</div>
</div>
</div>
</Dialog>
</Transition.Root>
</div>
<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";
// 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 type { IProject } from "@plane/types";
// ui
import { Input } from "@plane/ui";
// types
import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// types
type Props = {
@ -43,124 +39,92 @@ export function SelectMonthModal({ type, initialValues, isOpen, handleClose, han
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-30" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-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">
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary">
Customize time range
</Dialog.Title>
<div className="mt-8 flex items-center gap-2">
<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>
)}
</>
)}
<ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<h3 className="text-16 font-medium leading-6 text-primary">Customize time range</h3>
<div className="mt-8 flex items-center gap-2">
<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>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<Button variant="secondary" size="lg" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
)}
/>
{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>
</Dialog>
</Transition.Root>
<div className="mt-5 flex justify-end gap-2">
<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 { Dialog, Transition } from "@headlessui/react";
import { Button } from "@plane/propel/button";
import { Calendar } from "@plane/propel/calendar";
import { CloseIcon } from "@plane/propel/icons";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@plane/utils";
import { DateFilterSelect } from "./date-filter-select";
type Props = {
@ -49,118 +45,89 @@ export function DateFilterModal({ title, handleClose, isOpen, onSelect }: Props)
const isInvalid = watch("filterType") === "range" && date1 && date2 ? date1 > date2 : false;
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<form className="space-y-4 px-5 py-8 sm:p-6">
<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>
</Dialog>
</Transition.Root>
<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>
</ModalCore>
);
}

View file

@ -1,18 +1,18 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
import type { SubmitHandler } from "react-hook-form";
import { useForm } from "react-hook-form";
import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse, IUser } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
import { Loader } from "@plane/ui";
import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// assets
import darkIssuesAsset from "@/app/assets/empty-state/search/issues-dark.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 (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<div className="fixed inset-0 z-20 overflow-y-auto bg-backdrop p-4 transition-opacity 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 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">
<form>
<Combobox
onChange={(val: string) => {
const selectedIssues = watch("delete_issue_ids");
if (selectedIssues.includes(val))
setValue(
"delete_issue_ids",
selectedIssues.filter((i) => i !== val)
);
else setValue("delete_issue_ids", [...selectedIssues, val]);
}}
>
<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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<form>
<Combobox
onChange={(val: string) => {
const selectedIssues = watch("delete_issue_ids");
if (selectedIssues.includes(val))
setValue(
"delete_issue_ids",
selectedIssues.filter((i) => i !== val)
);
else setValue("delete_issue_ids", [...selectedIssues, val]);
}}
>
<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">
{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>
<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>
{issues.length > 0 && (
<div className="flex items-center justify-end gap-2 p-3">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button
variant="error-fill"
size="lg"
onClick={handleSubmit(handleDelete)}
loading={isSubmitting}
>
{isSubmitting ? "Deleting..." : "Delete selected work items"}
</Button>
</div>
)}
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
{issues.length > 0 && (
<div className="flex items-center justify-end gap-2 p-3">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="error-fill" size="lg" onClick={handleSubmit(handleDelete)} loading={isSubmitting}>
{isSubmitting ? "Deleting..." : "Delete selected work items"}
</Button>
</div>
)}
</form>
</ModalCore>
);
});

View file

@ -1,12 +1,11 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { Transition, Dialog } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
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";
// helpers
import { authErrorHandler } from "@/helpers/authentication.helper";
@ -127,119 +126,82 @@ export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props)
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-backdrop" />
</Transition.Child>
<div className="overflow-y-auto fixed inset-0 z-30">
<div className="flex justify-center items-center p-4 min-h-full 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-4 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
<div className="py-4 space-y-0">
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary">
{changeEmailT("title")}
</Dialog.Title>
<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">
{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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<div className="py-4 space-y-0 px-4">
<h3 className="text-16 font-medium leading-6 text-primary">{changeEmailT("title")}</h3>
<p className="my-4 text-13 text-secondary">{changeEmailT("description")}</p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 px-4" noValidate>
<div className="flex flex-col gap-1">
{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>
</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 { Combobox, Dialog, Transition } from "@headlessui/react";
import { Combobox } from "@headlessui/react";
// i18n
import { useTranslation } from "@plane/i18n";
// types
@ -10,7 +10,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
// ui
import { Loader, ToggleSwitch } from "@plane/ui";
import { Loader, ToggleSwitch, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { generateWorkItemLink, getTabIndex } from "@plane/utils";
// helpers
// hooks
@ -131,62 +131,136 @@ export function ExistingIssuesListModal(props: Props) {
const filteredIssues = issues.filter((issue) => !shouldHideIssue?.(issue));
return (
<>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<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">
<Search
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-primary text-opacity-40"
aria-hidden="true"
/>
<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">
<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
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="flex flex-col-reverse gap-4 p-2 text-13 text-secondary sm:flex-row sm:items-center sm:justify-between">
{selectedIssues.length > 0 ? (
<div className="mt-1 flex flex-wrap items-center gap-2">
{selectedIssues.map((issue) => (
<div
key={issue.id}
className="flex items-center gap-1 whitespace-nowrap rounded-md border border-subtle bg-layer-1 py-1 pl-2 text-11 text-primary"
>
<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"
/>
<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>
<IssueIdentifier
projectId={issue.project_id}
issueTypeId={issue.type_id}
projectIdentifier={issue.project__identifier}
issueSequenceId={issue.sequence_id}
size="xs"
variant="secondary"
/>
<button
type="button"
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>
</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">
{selectedIssues.length > 0 ? (
<div className="mt-1 flex flex-wrap items-center gap-2">
{selectedIssues.map((issue) => (
<div
key={issue.id}
className="flex items-center gap-1 whitespace-nowrap rounded-md border border-subtle bg-layer-1 py-1 pl-2 text-11 text-primary"
>
<Combobox.Options static className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto">
{/* TODO: Translate here */}
{searchTerm !== "" && (
<h5 className="mx-2 text-13 text-secondary">
Search results for{" "}
<span className="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
projectId={issue.project_id}
issueTypeId={issue.type_id}
@ -195,169 +269,57 @@ export function ExistingIssuesListModal(props: Props) {
size="xs"
variant="secondary"
/>
<button
type="button"
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>
</span>
<span className="truncate">{issue.name}</span>
</div>
</Tooltip>
)}
</div>
<Combobox.Options
static
className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto"
>
{/* TODO: Translate here */}
{searchTerm !== "" && (
<h5 className="mx-2 text-13 text-secondary">
Search results for{" "}
<span className="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
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>
</>
<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>
</ModalCore>
);
}

View file

@ -1,13 +1,13 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { useDropzone } from "react-dropzone";
import { Transition, Dialog } from "@headlessui/react";
// plane imports
import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { UserCirclePropertyIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils";
// services
import { FileService } from "@/services/file.service";
@ -88,106 +88,68 @@ export const UserImageUploadModal = observer(function UserImageUploadModal(props
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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"
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XL}>
<div className="space-y-5 px-5 py-8 sm:p-6">
<h3 className="text-16 font-medium leading-6 text-primary">Upload Image</h3>
<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"
: ""
}`}
>
<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">
Upload Image
</Dialog.Title>
<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>
)}
{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}>
{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>
</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}>
{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>
</Dialog>
</Transition.Root>
</div>
</ModalCore>
);
});

View file

@ -1,14 +1,14 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useDropzone } from "react-dropzone";
import { Transition, Dialog } from "@headlessui/react";
// plane imports
import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { UserCirclePropertyIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
@ -101,112 +101,68 @@ export const WorkspaceImageUploadModal = observer(function WorkspaceImageUploadM
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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"
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XL}>
<div className="space-y-5 px-5 py-8 sm:p-6">
<h3 className="text-16 font-medium leading-6 text-primary">Upload image</h3>
<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"
: ""
}`}
>
<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">
Upload image
</Dialog.Title>
<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}
{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"
>
{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>
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>
</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>
</Dialog>
</Transition.Root>
</div>
</ModalCore>
);
});

View file

@ -1,9 +1,9 @@
import { useState, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { useState } from "react";
// ui
import { CYCLE_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store/use-cycle";
@ -51,6 +51,7 @@ export function ArchiveCycleModal(props: Props) {
});
onClose();
router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
return;
})
.catch(() => {
setToast({
@ -69,51 +70,21 @@ export function ArchiveCycleModal(props: Props) {
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-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>
<ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.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>
</Dialog>
</Transition.Root>
</div>
</ModalCore>
);
}

View file

@ -1,15 +1,13 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { AlertCircle, Search } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { CycleIcon, TransferIcon, CloseIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EIssuesStoreType } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues";
//icons
// constants
type Props = {
isOpen: boolean;
@ -72,109 +70,71 @@ export const TransferIssuesModal = observer(function TransferIssuesModal(props:
return cycleDetails?.name?.toLowerCase().includes(query?.toLowerCase());
});
// useEffect(() => {
// const handleKeyDown = (e: KeyboardEvent) => {
// if (e.key === "Escape") {
// handleClose();
// }
// };
// }, [handleClose]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<div className="flex flex-col gap-4 py-5">
<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>
</Dialog>
</Transition.Root>
<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>
</ModalCore>
);
});

View file

@ -2,14 +2,13 @@ import React, { useState } from "react";
import { intersection } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Dialog, Transition } from "@headlessui/react";
// types
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IImporterService } from "@plane/types";
// ui
import { Checkbox, CustomSearchSelect } from "@plane/ui";
import { Checkbox, CustomSearchSelect, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUser } from "@/hooks/store/user";
@ -99,101 +98,64 @@ export const Exporter = observer(function Exporter(props: Props) {
}
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog
as="div"
className="relative z-20"
onClose={() => {
if (!isSelectOpen) handleClose();
}}
>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<ModalCore
isOpen={isOpen}
handleClose={() => {
if (!isSelectOpen) handleClose();
}}
position={EModalPosition.CENTER}
width={EModalWidth.XL}
>
<div className="flex flex-col gap-6 gap-y-4 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="flex items-center justify-start">
<h3 className="text-18 font-medium 2xl:text-20">
{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);
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-full sm:max-w-xl">
<div className="flex flex-col gap-6 gap-y-4 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="flex items-center justify-start">
<h3 className="text-18 font-medium 2xl:text-20">
{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>
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 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>
</Dialog>
</Transition.Root>
<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>
</ModalCore>
);
});

View file

@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { useTheme } from "next-themes";
import { Search } from "lucide-react";
import { Combobox, Dialog, Transition } from "@headlessui/react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ISearchIssueResponse } from "@plane/types";
import { Loader } from "@plane/ui";
import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// assets
import darkIssuesAsset from "@/app/assets/empty-state/search/issues-dark.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 = () => {
onClose();
setQuery("");
};
const handleSubmit = (selectedItem: string) => {
@ -124,66 +125,34 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
);
return (
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<div className="flex flex-wrap items-start">
<div className="space-y-1 sm:basis-1/2">
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<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>
</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 { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { useState } from "react";
// ui
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { Calendar } from "@plane/propel/calendar";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
export type InboxIssueSnoozeModalProps = {
isOpen: boolean;
@ -21,63 +20,34 @@ export function InboxIssueSnoozeModal(props: InboxIssueSnoozeModalProps) {
const { t } = useTranslation();
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<div className="flex h-full w-full flex-col gap-y-1 px-5 py-8 sm:p-6">
<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={() => {
handleClose();
onConfirm(date);
}}
>
<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">
<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>
{t("inbox_issue.actions.snooze")}
</Button>
</div>
</ModalCore>
);
}

View file

@ -1,23 +1,15 @@
import React, { useState } from "react";
import { useState } from "react";
import { useParams } from "next/navigation";
import { mutate } from "swr";
// headless ui
// icons
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// services
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
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 { IntegrationService } from "@/services/integrations/integration.service";
// ui
// icons
// types
// fetch-keys
type Props = {
isOpen: boolean;
@ -64,85 +56,55 @@ export function DeleteImportModal({ isOpen, handleClose, data }: Props) {
if (!data) return <></>;
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<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>
</Dialog>
</Transition.Root>
<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>
</ModalCore>
);
}

View file

@ -1,11 +1,11 @@
import { useState, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { useState } from "react";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TDeDupeIssue, TIssue } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
import { useProject } from "@/hooks/store/use-project";
@ -49,6 +49,7 @@ export function ArchiveIssueModal(props: Props) {
message: t("issue.archive.success.message"),
});
onClose();
return;
})
.catch(() =>
setToast({
@ -61,51 +62,21 @@ export function ArchiveIssueModal(props: Props) {
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-30" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<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>
<ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.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>
</Dialog>
</Transition.Root>
</div>
</ModalCore>
);
}

View file

@ -1,9 +1,7 @@
import React, { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
import { useState } from "react";
// ui
import { Button } from "@plane/propel/button";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
type Props = {
isOpen: boolean;
@ -29,66 +27,34 @@ export function ConfirmIssueDiscard(props: Props) {
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
<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">
<h3 className="text-16 font-medium leading-6 text-primary">Save this draft?</h3>
<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>
</Dialog>
</Transition.Root>
</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>
</ModalCore>
);
}

View file

@ -3,13 +3,13 @@ import { useParams } from "next/navigation";
// icons
import { Rocket, Search } from "lucide-react";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
import { Combobox } from "@headlessui/react";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import type { ISearchIssueResponse } from "@plane/types";
// ui
import { Loader } from "@plane/ui";
import { Loader, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
import { generateWorkItemLink, getTabIndex } from "@plane/utils";
// components
import { IssueSearchModalEmptyState } from "@/components/core/modals/issue-search-modal-empty-state";
@ -85,144 +85,111 @@ export function ParentIssuesListModal({
}, [debouncedSearchTerm, isOpen, issueId, projectId, workspaceSlug]);
return (
<>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Combobox
value={value}
onChange={(val) => {
onChange(val);
handleClose();
}}
>
<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"
/>
<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">
<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={(val) => {
onChange(val);
handleClose();
}}
>
<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"
/>
<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>
)}
{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}
{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}
/>
) : (
<ul className={`text-13 ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue}
className={({ active, selected }) =>
`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 flex-grow items-center gap-2 truncate">
<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"
/>
) : (
<ul className={`text-13 ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue}
className={({ active, selected }) =>
`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 flex-grow items-center gap-2 truncate">
<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: 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>
</>
</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>
</ModalCore>
);
}

View file

@ -1,8 +1,8 @@
import { useState, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { useState } from "react";
// ui
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useModule } from "@/hooks/store/use-module";
import { useAppRouter } from "@/hooks/use-app-router";
@ -43,6 +43,7 @@ export function ArchiveModuleModal(props: Props) {
});
onClose();
router.push(`/${workspaceSlug}/projects/${projectId}/modules`);
return;
})
.catch(() =>
setToast({
@ -55,57 +56,21 @@ export function ArchiveModuleModal(props: Props) {
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-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>
<ModalCore isOpen={isOpen} handleClose={onClose} position={EModalPosition.CENTER} width={EModalWidth.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>
</Dialog>
</Transition.Root>
</div>
</ModalCore>
);
}

View file

@ -1,13 +1,12 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { AlertTriangle } from "lucide-react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// types
import { Button } from "@plane/propel/button";
import type { IUserLite } from "@plane/types";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUser } from "@/hooks/store/user";
@ -48,86 +47,42 @@ export const ConfirmProjectMemberRemove = observer(function ConfirmProjectMember
const currentProjectDetails = getProjectById(projectId.toString());
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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-[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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<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">
<h3 className="text-16 font-medium leading-6 text-primary">
{isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}
</h3>
<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>
</Dialog>
</Transition.Root>
</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>
</ModalCore>
);
});

View file

@ -1,15 +1,13 @@
import React from "react";
import { useParams } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import { AlertTriangle } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// types
import { PROJECT_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IProject } from "@plane/types";
// ui
import { Input } from "@plane/ui";
import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// constants
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
@ -90,109 +88,77 @@ export function DeleteProjectModal(props: DeleteProjectModal) {
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<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>
</Dialog>
</Transition.Root>
<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>
</ModalCore>
);
}

View file

@ -1,10 +1,9 @@
import { useState, Fragment } from "react";
import { Transition, Dialog } from "@headlessui/react";
import { useState } from "react";
// types
import { Button } from "@plane/propel/button";
import type { IProject } from "@plane/types";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
@ -26,13 +25,17 @@ export function JoinProjectModal(props: TJoinProjectModalProps) {
// router
const router = useAppRouter();
const handleJoin = () => {
const handleJoin = async () => {
setIsJoiningLoading(true);
joinProject(workspaceSlug, project.id)
await joinProject(workspaceSlug, project.id)
.then(() => {
router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
handleClose();
return;
})
.catch(() => {
console.error("Error joining project");
})
.finally(() => {
setIsJoiningLoading(false);
@ -40,63 +43,23 @@ export function JoinProjectModal(props: TJoinProjectModalProps) {
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={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">
<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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XL}>
<div className="space-y-5 px-5 py-8 sm:p-6">
<h3 className="text-16 font-medium leading-6 text-primary">Join Project?</h3>
<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 px-5 pb-8 sm:px-6 sm:pb-6">
<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>
</ModalCore>
);
}

View file

@ -1,17 +1,15 @@
import { Fragment } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
// headless ui
import { AlertTriangleIcon } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// types
import { MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IProject } from "@plane/types";
// ui
import { Input } from "@plane/ui";
import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// constants
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
@ -109,113 +107,82 @@ export const LeaveProjectModal = observer(function LeaveProjectModal(props: ILea
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<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>
</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 { useForm, Controller, useFieldArray } from "react-hook-form";
import { Plus } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { ROLE, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { CloseIcon, ChevronDownIcon } from "@plane/propel/icons";
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
import { getFileURL } from "@plane/utils";
// hooks
@ -183,182 +182,140 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<form onSubmit={handleSubmit(onSubmit)} className="p-5">
<div className="space-y-5">
<h3 className="text-16 font-medium leading-6 text-primary">
{t("project_settings.members.invite_members.title")}
</h3>
<div className="mt-2">
<p className="text-13 text-secondary">{t("project_settings.members.invite_members.sub_heading")}</p>
</div>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 p-5 text-left shadow-raised-200 transition-all sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-5">
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-primary">
{t("project_settings.members.invite_members.title")}
</Dialog.Title>
<div className="mt-2">
<p className="text-13 text-secondary">
{t("project_settings.members.invite_members.sub_heading")}
</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"
<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 ?? "")}
/>
);
}}
/>
{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>
}
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>
{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"
/>
);
}}
/>
{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>
}
input
>
{Object.entries(checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))).map(
([key, label]) => {
if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null;
{fields.length > 1 && (
<div className="flex-item flex w-6">
<button
type="button"
className="place-items-center self-center rounded-sm"
onClick={() => remove(index)}
>
<CloseIcon className="h-4 w-4 text-secondary" />
</button>
</div>
)}
</div>
</div>
))}
</div>
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>
)}
</div>
<div className="mt-5 flex items-center justify-between gap-2">
<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>
{fields.length > 1 && (
<div className="flex-item flex w-6">
<button
type="button"
className="place-items-center self-center rounded-sm"
onClick={() => remove(index)}
>
<CloseIcon className="h-4 w-4 text-secondary" />
</button>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
)}
</div>
</div>
))}
</div>
</div>
</Dialog>
</Transition.Root>
<div className="mt-5 flex items-center justify-between gap-2">
<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 { Dialog, Transition } from "@headlessui/react";
import { useState } from "react";
// ui
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router";
@ -44,6 +44,7 @@ export function ArchiveRestoreProjectModal(props: Props) {
});
onClose();
router.push(`/${workspaceSlug}/projects/`);
return;
})
.catch(() =>
setToast({
@ -66,6 +67,7 @@ export function ArchiveRestoreProjectModal(props: Props) {
});
onClose();
router.push(`/${workspaceSlug}/projects/`);
return;
})
.catch(() =>
setToast({
@ -78,61 +80,31 @@ export function ArchiveRestoreProjectModal(props: Props) {
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-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>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.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 won't 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>
</Dialog>
</Transition.Root>
</div>
</ModalCore>
);
}

View file

@ -1,16 +1,13 @@
import { Fragment } from "react";
import { useParams } from "next/navigation";
import { useForm, Controller } from "react-hook-form";
import { Transition, Dialog } from "@headlessui/react";
// plane imports
import { allTimeIn30MinutesInterval12HoursFormat } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { CloseIcon } from "@plane/propel/icons";
import { CustomSelect } from "@plane/ui";
import { CustomSelect, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components
import { getDate, cn } from "@plane/utils";
import { DateDropdown } from "@/components/dropdowns/date";
// helpers
type TNotificationSnoozeModal = {
isOpen: boolean;
@ -110,155 +107,117 @@ export function NotificationSnoozeModal(props: TNotificationSnoozeModal) {
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<form onSubmit={handleSubmit(onSubmit)} className="p-5">
<div className="flex items-center justify-between">
<h3 className="text-h5-medium leading-6 text-primary">Customize Snooze Time</h3>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<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 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>
<button type="button" onClick={handleClose}>
<CloseIcon className="h-5 w-5 text-primary" />
</button>
</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>
);
}