[WEB-5782]chore: migrated modals to @plane/ui (#8420)
* chore: migrated modal to @plane/ui * chore: fixed spacings
This commit is contained in:
parent
39728d4cc4
commit
5b28327551
25 changed files with 1530 additions and 2412 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 don’t 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 don’t 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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 'Join
|
||||
Project' 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 'Join Project' 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue