Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0917ef6f5f | ||
|
|
6bf36ad85c | ||
|
|
3272abac09 | ||
|
|
650df35840 | ||
|
|
5e0d3c9108 | ||
|
|
70cdd49ea9 |
@@ -2,11 +2,10 @@
|
||||
|
||||
import React, { 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, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useUser } from "@/hooks/store";
|
||||
@@ -63,65 +62,34 @@ export const DeactivateAccountModal: React.FC<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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="">
|
||||
<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-2xl font-medium leading-6 text-custom-text-100">
|
||||
{t("deactivate_your_account")}
|
||||
</Dialog.Title>
|
||||
<p className="mt-6 list-disc pr-4 text-base font-normal text-custom-text-200">
|
||||
{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="neutral-primary" onClick={onClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDeleteAccount}>
|
||||
{isDeactivating ? t("deactivating") : t("confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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>
|
||||
<Dialog.Title className="my-4 text-2xl font-medium leading-6 text-custom-text-100">
|
||||
{t("deactivate_your_account")}
|
||||
</Dialog.Title>
|
||||
<p className="mt-6 list-disc pr-4 text-base font-normal text-custom-text-200">
|
||||
{t("deactivate_your_account_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="mb-2 flex items-center justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="neutral-primary" onClick={onClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDeleteAccount}>
|
||||
{isDeactivating ? t("deactivating") : t("confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,9 @@ 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 type { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
import { Button, Input, Dialog, EModalWidth } from "@plane/ui";
|
||||
// types
|
||||
|
||||
// types
|
||||
@@ -44,124 +42,96 @@ export const SelectMonthModal: React.FC<Props> = ({ type, initialValues, isOpen,
|
||||
};
|
||||
|
||||
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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
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-custom-border-200"
|
||||
min={1}
|
||||
max={12}
|
||||
/>
|
||||
<span className="absolute right-8 top-2.5 text-sm text-custom-text-200">Months</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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-custom-background-100 px-4 pb-4 pt-5 text-left shadow-custom-shadow-md 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-lg font-medium leading-6 text-custom-text-100">
|
||||
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-custom-border-200"
|
||||
min={1}
|
||||
max={12}
|
||||
/>
|
||||
<span className="absolute right-8 top-2.5 text-sm text-custom-text-200">Months</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{errors.close_in && (
|
||||
<span className="px-1 text-sm 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-custom-border-200"
|
||||
min={1}
|
||||
max={12}
|
||||
/>
|
||||
<span className="absolute right-8 top-2.5 text-sm text-custom-text-200">Months</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.archive_in && (
|
||||
<span className="px-1 text-sm text-red-500">Select a month between 1 and 12.</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
{errors.close_in && (
|
||||
<span className="px-1 text-sm 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-custom-border-200"
|
||||
min={1}
|
||||
max={12}
|
||||
/>
|
||||
<span className="absolute right-8 top-2.5 text-sm text-custom-text-200">Months</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.archive_in && (
|
||||
<span className="px-1 text-sm text-red-500">Select a month between 1 and 12.</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState, Fragment } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { Input } from "@plane/ui";
|
||||
import { Input, Dialog, EModalWidth } from "@plane/ui";
|
||||
import { ShortcutCommandsList } from "@/components/command-palette";
|
||||
// ui
|
||||
|
||||
@@ -24,60 +23,30 @@ export const ShortcutsModal: FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
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-custom-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-20">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex h-full items-center justify-center">
|
||||
<div className="flex h-[61vh] w-full flex-col space-y-4 overflow-hidden rounded-lg bg-custom-background-100 p-5 shadow-custom-shadow-md transition-all sm:w-[28rem]">
|
||||
<Dialog.Title as="h3" className="flex justify-between">
|
||||
<span className="text-lg font-medium">Keyboard shortcuts</span>
|
||||
<button type="button" onClick={handleClose}>
|
||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" aria-hidden="true" />
|
||||
</button>
|
||||
</Dialog.Title>
|
||||
<div className="flex w-full items-center rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<Input
|
||||
id="search"
|
||||
name="search"
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search for shortcuts"
|
||||
className="w-full border-none bg-transparent py-1 text-xs text-custom-text-200 outline-none"
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
<ShortcutCommandsList searchQuery={query} />
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL} className="flex flex-col space-y-4">
|
||||
<Dialog.Title className="flex justify-between">
|
||||
<span className="text-lg font-medium">Keyboard shortcuts</span>
|
||||
<button type="button" onClick={handleClose}>
|
||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" aria-hidden="true" />
|
||||
</button>
|
||||
</Dialog.Title>
|
||||
<div className="flex w-full items-center rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<Input
|
||||
id="search"
|
||||
name="search"
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search for shortcuts"
|
||||
className="w-full border-none bg-transparent py-1 text-xs text-custom-text-200 outline-none"
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ShortcutCommandsList searchQuery={query} />
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import { Button, Calendar } from "@plane/ui";
|
||||
import { Button, Calendar, Dialog, EModalWidth } from "@plane/ui";
|
||||
|
||||
import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@plane/utils";
|
||||
import { DateFilterSelect } from "./date-filter-select";
|
||||
@@ -49,122 +48,95 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
|
||||
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-custom-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-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<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} />
|
||||
)}
|
||||
/>
|
||||
<X 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
|
||||
classNames={{
|
||||
root: ` border border-custom-border-200 p-3 rounded-md`,
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
selected={dateValue}
|
||||
defaultMonth={dateValue}
|
||||
onSelect={(date) => {
|
||||
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
|
||||
classNames={{
|
||||
root: ` border border-custom-border-200 p-3 rounded-md`,
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
selected={dateValue}
|
||||
defaultMonth={dateValue}
|
||||
onSelect={(date) => {
|
||||
if (!date) return;
|
||||
onChange(date);
|
||||
}}
|
||||
mode="single"
|
||||
disabled={date1Value ? [{ before: date1Value }] : undefined}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{watch("filterType") === "range" && (
|
||||
<h6 className="flex items-center gap-1 text-xs">
|
||||
<span className="text-custom-text-200">After:</span>
|
||||
<span>{renderFormattedDate(watch("date1"))}</span>
|
||||
<span className="ml-1 text-custom-text-200">Before:</span>
|
||||
{!isInvalid && <span>{renderFormattedDate(watch("date2"))}</span>}
|
||||
</h6>
|
||||
)}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
disabled={isInvalid}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<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} />
|
||||
)}
|
||||
/>
|
||||
<X className="h-4 w-4 cursor-pointer" onClick={handleClose} />
|
||||
</div>
|
||||
</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
|
||||
classNames={{
|
||||
root: ` border border-custom-border-200 p-3 rounded-md`,
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
selected={dateValue}
|
||||
defaultMonth={dateValue}
|
||||
onSelect={(date) => {
|
||||
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
|
||||
classNames={{
|
||||
root: ` border border-custom-border-200 p-3 rounded-md`,
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
selected={dateValue}
|
||||
defaultMonth={dateValue}
|
||||
onSelect={(date) => {
|
||||
if (!date) return;
|
||||
onChange(date);
|
||||
}}
|
||||
mode="single"
|
||||
disabled={date1Value ? [{ before: date1Value }] : undefined}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{watch("filterType") === "range" && (
|
||||
<h6 className="flex items-center gap-1 text-xs">
|
||||
<span className="text-custom-text-200">After:</span>
|
||||
<span>{renderFormattedDate(watch("date1"))}</span>
|
||||
<span className="ml-1 text-custom-text-200">Before:</span>
|
||||
{!isInvalid && <span>{renderFormattedDate(watch("date2"))}</span>}
|
||||
</h6>
|
||||
)}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
disabled={isInvalid}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,11 +5,11 @@ import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { SubmitHandler, 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 { EIssuesStoreType, ISearchIssueResponse, IUser } from "@plane/types";
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Loader, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// components
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// hooks
|
||||
@@ -141,78 +141,59 @@ export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
|
||||
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-custom-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 open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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]);
|
||||
}}
|
||||
>
|
||||
<Dialog.Panel className="relative flex w-full items-center justify-center ">
|
||||
<div className="w-full max-w-2xl transform divide-y divide-custom-border-200 divide-opacity-10 rounded-lg bg-custom-background-100 shadow-custom-shadow-md 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-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative m-1">
|
||||
<Search
|
||||
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 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-custom-border-200 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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" 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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" onClick={handleSubmit(handleDelete)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Deleting..." : "Delete selected work items"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Rocket, Search, X } from "lucide-react";
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
import { generateWorkItemLink, getTabIndex } from "@plane/utils";
|
||||
// helpers
|
||||
// hooks
|
||||
@@ -127,230 +127,199 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
|
||||
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"
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="relative m-1">
|
||||
<Search
|
||||
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 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-custom-background-100 shadow-custom-shadow-md transition-all">
|
||||
<Combobox
|
||||
as="div"
|
||||
onChange={(val: ISearchIssueResponse) => {
|
||||
if (selectedIssues.some((i) => i.id === val.id))
|
||||
setSelectedIssues((prevData) => prevData.filter((i) => i.id !== val.id));
|
||||
else setSelectedIssues((prevData) => [...prevData, val]);
|
||||
}}
|
||||
>
|
||||
<div className="relative m-1">
|
||||
<Search
|
||||
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0"
|
||||
placeholder={t("common.search.placeholder")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
tabIndex={baseTabIndex}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-reverse gap-4 p-2 text-[0.825rem] text-custom-text-200 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-custom-border-200 bg-custom-background-80 py-1 pl-2 text-xs text-custom-text-100"
|
||||
>
|
||||
<IssueIdentifier
|
||||
projectId={issue.project_id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={issue.project__identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="group p-1"
|
||||
onClick={() => setSelectedIssues((prevData) => prevData.filter((i) => i.id !== issue.id))}
|
||||
>
|
||||
<X className="h-3 w-3 text-custom-text-200 group-hover:text-custom-text-100" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
{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-xs ${
|
||||
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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-[0.825rem] text-custom-text-200">
|
||||
Search results for{" "}
|
||||
<span className="text-custom-text-100">
|
||||
{'"'}
|
||||
{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-sm text-custom-text-100 ${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-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
>
|
||||
<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}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
</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-custom-text-200 hover:text-custom-text-100 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">
|
||||
<Button
|
||||
variant="link-primary"
|
||||
size="sm"
|
||||
onClick={handleSelectIssues}
|
||||
disabled={filteredIssues.length === 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 p-3">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={onSubmit}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || selectedIssues.length === 0}
|
||||
<div className="flex flex-col-reverse gap-4 p-2 text-[0.825rem] text-custom-text-200 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-custom-border-200 bg-custom-background-80 py-1 pl-2 text-xs text-custom-text-100"
|
||||
>
|
||||
{isSubmitting ? t("common.adding") : t("issue.select.add_selected")}
|
||||
</Button>
|
||||
</div>
|
||||
<IssueIdentifier
|
||||
projectId={issue.project_id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={issue.project__identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="group p-1"
|
||||
onClick={() => setSelectedIssues((prevData) => prevData.filter((i) => i.id !== issue.id))}
|
||||
>
|
||||
<X className="h-3 w-3 text-custom-text-200 group-hover:text-custom-text-100" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
) : (
|
||||
<div className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
{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-xs ${
|
||||
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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-[0.825rem] text-custom-text-200">
|
||||
Search results for{" "}
|
||||
<span className="text-custom-text-100">
|
||||
{'"'}
|
||||
{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-sm text-custom-text-100 ${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-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
>
|
||||
<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}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
</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-custom-text-200 hover:text-custom-text-100 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">
|
||||
<Button
|
||||
variant="link-primary"
|
||||
size="sm"
|
||||
onClick={handleSelectIssues}
|
||||
disabled={filteredIssues.length === 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 p-3">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={onSubmit}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || selectedIssues.length === 0}
|
||||
>
|
||||
{isSubmitting ? t("common.adding") : t("issue.select.add_selected")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,10 @@ import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils";
|
||||
// helpers
|
||||
// services
|
||||
@@ -90,106 +89,70 @@ export const UserImageUploadModal: React.FC<Props> = observer((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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
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-custom-primary focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{image !== null || (value && value !== "") ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
|
||||
>
|
||||
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>
|
||||
<UserCircle2 className="mx-auto h-16 w-16 text-custom-text-200" />
|
||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
</div>
|
||||
{fileRejections.length > 0 && (
|
||||
<p className="text-sm 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-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="danger" size="sm" onClick={handleImageRemove} disabled={!value}>
|
||||
{isRemoving ? "Removing" : "Remove"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={!image}
|
||||
loading={isImageUploading}
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XL}>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">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-custom-primary focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{image !== null || (value && value !== "") ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
|
||||
>
|
||||
{isImageUploading ? "Uploading" : "Upload & Save"}
|
||||
</Button>
|
||||
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>
|
||||
<UserCircle2 className="mx-auto h-16 w-16 text-custom-text-200" />
|
||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
</div>
|
||||
{fileRejections.length > 0 && (
|
||||
<p className="text-sm 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>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<p className="my-4 text-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="danger" size="sm" onClick={handleImageRemove} disabled={!value}>
|
||||
{isRemoving ? "Removing" : "Remove"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" onClick={handleSubmit} disabled={!image} loading={isImageUploading}>
|
||||
{isImageUploading ? "Uploading" : "Upload & Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,11 +4,10 @@ import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils";
|
||||
// helpers
|
||||
// hooks
|
||||
@@ -102,106 +101,70 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
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-custom-primary focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{image !== null || (value && value !== "") ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
|
||||
>
|
||||
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>
|
||||
<UserCircle2 className="mx-auto h-16 w-16 text-custom-text-200" />
|
||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
</div>
|
||||
{fileRejections.length > 0 && (
|
||||
<p className="text-sm 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-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="danger" size="sm" onClick={handleImageRemove} disabled={!value} loading={isRemoving}>
|
||||
{isRemoving ? "Removing" : "Remove"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={!image}
|
||||
loading={isImageUploading}
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">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-custom-primary focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{image !== null || (value && value !== "") ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-0 top-0 z-40 -translate-y-1/2 translate-x-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
|
||||
>
|
||||
{isImageUploading ? "Uploading" : "Upload & Save"}
|
||||
</Button>
|
||||
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>
|
||||
<UserCircle2 className="mx-auto h-16 w-16 text-custom-text-200" />
|
||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
</div>
|
||||
{fileRejections.length > 0 && (
|
||||
<p className="text-sm 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>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<p className="my-4 text-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="danger" size="sm" onClick={handleImageRemove} disabled={!value} loading={isRemoving}>
|
||||
{isRemoving ? "Removing" : "Remove"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" onClick={handleSubmit} disabled={!image} loading={isImageUploading}>
|
||||
{isImageUploading ? "Uploading" : "Upload & Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
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, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useCycle } from "@/hooks/store";
|
||||
@@ -70,51 +69,23 @@ export const ArchiveCycleModal: React.FC<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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Archive cycle {cycleName}</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">
|
||||
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="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveCycle} loading={isArchiving}>
|
||||
{isArchiving ? "Archiving" : "Archive"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog.Panel width={EModalWidth.LG}>
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Archive cycle {cycleName}</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">
|
||||
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="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveCycle} loading={isArchiving}>
|
||||
{isArchiving ? "Archiving" : "Archive"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,11 @@ import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { AlertCircle, Search, X } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// hooks
|
||||
// ui
|
||||
//icons
|
||||
import { ContrastIcon, TransferIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { ContrastIcon, TransferIcon, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
import { useCycle, useIssues } from "@/hooks/store";
|
||||
//icons
|
||||
// constants
|
||||
@@ -84,100 +83,72 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
|
||||
// }, [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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<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-custom-text-100" />
|
||||
<h4 className="text-xl font-medium text-custom-text-100">Transfer work items</h4>
|
||||
</div>
|
||||
<button onClick={handleClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-b border-custom-border-200 px-5 pb-3">
|
||||
<Search className="h-4 w-4 text-custom-text-200" />
|
||||
<input
|
||||
className="outline-none text-sm"
|
||||
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);
|
||||
|
||||
<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-custom-background-100 py-5 text-left shadow-custom-shadow-md 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-custom-text-100" />
|
||||
<h4 className="text-xl font-medium text-custom-text-100">Transfer work items</h4>
|
||||
</div>
|
||||
<button onClick={handleClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-b border-custom-border-200 px-5 pb-3">
|
||||
<Search className="h-4 w-4 text-custom-text-200" />
|
||||
<input
|
||||
className="outline-none text-sm"
|
||||
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;
|
||||
|
||||
if (!cycleDetails) return;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={optionId}
|
||||
className="flex w-full items-center gap-4 rounded px-4 py-3 text-sm text-custom-text-200 hover:bg-custom-background-90"
|
||||
onClick={() => {
|
||||
transferIssue({
|
||||
new_cycle_id: optionId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ContrastIcon 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-custom-background-80 px-2 capitalize">
|
||||
{cycleDetails.status.toLocaleLowerCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
|
||||
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className="text-center text-custom-text-200">
|
||||
You don’t have any current cycle. Please create one to transfer the work items.
|
||||
return (
|
||||
<button
|
||||
key={optionId}
|
||||
className="flex w-full items-center gap-4 rounded px-4 py-3 text-sm text-custom-text-200 hover:bg-custom-background-90"
|
||||
onClick={() => {
|
||||
transferIssue({
|
||||
new_cycle_id: optionId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ContrastIcon 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-custom-background-80 px-2 capitalize">
|
||||
{cycleDetails.status.toLocaleLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
|
||||
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<span className="text-center text-custom-text-200">
|
||||
You don’t have any current cycle. Please create one to transfer the work items.
|
||||
</span>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
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 { ISearchIssueResponse } from "@plane/types";
|
||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Loader, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// components
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// hooks
|
||||
@@ -120,69 +120,36 @@ export const SelectDuplicateInboxIssueModal: React.FC<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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</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-custom-background-100 shadow-custom-shadow-md 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-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{isSearching ? (
|
||||
<Loader className="space-y-3 p-3">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<>{issueList}</>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Root>
|
||||
<Combobox.Options static className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 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>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { FC, useState } from "react";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button, Calendar } from "@plane/ui";
|
||||
import { Button, Calendar, Dialog, EModalWidth } from "@plane/ui";
|
||||
|
||||
export type InboxIssueSnoozeModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -21,63 +20,36 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
|
||||
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"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-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-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<div className="flex h-full w-full flex-col gap-y-1">
|
||||
<Calendar
|
||||
captionLayout="dropdown"
|
||||
classNames={{ root: `rounded-md border border-custom-border-200 p-3` }}
|
||||
selected={date ? new Date(date) : undefined}
|
||||
defaultMonth={date ? new Date(date) : undefined}
|
||||
onSelect={(date) => {
|
||||
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>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<div className="flex h-full w-full flex-col gap-y-1">
|
||||
<Calendar
|
||||
captionLayout="dropdown"
|
||||
classNames={{ root: `rounded-md border border-custom-border-200 p-3` }}
|
||||
selected={date ? new Date(date) : undefined}
|
||||
defaultMonth={date ? new Date(date) : undefined}
|
||||
onSelect={(date) => {
|
||||
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>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,10 +8,9 @@ import { mutate } from "swr";
|
||||
|
||||
// headless ui
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { IUser, IImporterService } from "@plane/types";
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Dialog, EModalWidth, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys";
|
||||
import { IntegrationService } from "@/services/integrations/integration.service";
|
||||
// ui
|
||||
@@ -64,85 +63,57 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
|
||||
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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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-xl font-medium 2xl:text-2xl">Delete project</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to delete import from{" "}
|
||||
<span className="break-words font-semibold capitalize text-custom-text-100">{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-sm text-custom-text-200">
|
||||
To confirm, type <span className="font-medium text-custom-text-100">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={handleDeletion}
|
||||
disabled={!confirmDeleteImport}
|
||||
loading={deleteLoading}
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<AlertTriangle className="h-6 w-6 text-red-500" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete project</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to delete import from{" "}
|
||||
<span className="break-words font-semibold capitalize text-custom-text-100">{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-sm text-custom-text-200">
|
||||
To confirm, type <span className="font-medium text-custom-text-100">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={handleDeletion}
|
||||
disabled={!confirmDeleteImport}
|
||||
loading={deleteLoading}
|
||||
>
|
||||
{deleteLoading ? "Deleting..." : "Delete Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
{deleteLoading ? "Deleting..." : "Delete Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TDeDupeIssue, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
@@ -63,51 +62,23 @@ export const ArchiveIssueModal: React.FC<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-custom-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-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">
|
||||
{t("issue.archive.label")} {projectDetails?.identifier} {issue.sequence_id}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">{t("issue.archive.confirm_message")}</p>
|
||||
<div className="mt-3 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
|
||||
{isArchiving ? t("common.archiving") : t("common.archive")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">
|
||||
{t("issue.archive.label")} {projectDetails?.identifier} {issue.sequence_id}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">{t("issue.archive.confirm_message")}</p>
|
||||
<div className="mt-3 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
|
||||
{isArchiving ? t("common.archiving") : t("common.archive")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button, Dialog, EModalWidth } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -31,66 +28,38 @@ export const ConfirmIssueDiscard: React.FC<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-custom-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-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Save this draft?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
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="neutral-primary" size="sm" onClick={onDiscard}>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" onClick={handleDeletion} loading={isLoading}>
|
||||
{isLoading ? "Saving" : "Save to Drafts"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Save this draft?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
You can save this work item to Drafts so you can come back to it later.{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="flex justify-between gap-2 p-4 sm:px-6">
|
||||
<div>
|
||||
<Button variant="neutral-primary" size="sm" onClick={onDiscard}>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" onClick={handleDeletion} loading={isLoading}>
|
||||
{isLoading ? "Saving" : "Save to Drafts"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,13 +5,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 { ISearchIssueResponse } from "@plane/types";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
import { Loader, Dialog, EModalWidth } from "@plane/ui";
|
||||
import { generateWorkItemLink, getTabIndex } from "@plane/utils";
|
||||
// components
|
||||
import { IssueSearchModalEmptyState } from "@/components/core";
|
||||
@@ -88,142 +88,113 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
|
||||
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"
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="relative m-1">
|
||||
<Search
|
||||
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0 sm:text-sm"
|
||||
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-[0.825rem] text-custom-text-200">
|
||||
Search results for{" "}
|
||||
<span className="text-custom-text-100">
|
||||
{'"'}
|
||||
{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-custom-background-100 shadow-custom-shadow-md 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-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
{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}
|
||||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0 sm:text-sm"
|
||||
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-[0.825rem] text-custom-text-200">
|
||||
Search results for{" "}
|
||||
<span className="text-custom-text-100">
|
||||
{'"'}
|
||||
{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}
|
||||
/>
|
||||
) : (
|
||||
<ul className={`text-sm ${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-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
>
|
||||
<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}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
</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-custom-text-200 hover:text-custom-text-100 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>
|
||||
) : (
|
||||
<ul className={`text-sm ${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-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
>
|
||||
<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}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
</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-custom-text-200 hover:text-custom-text-100 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>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,13 +5,13 @@ import { observer } from "mobx-react";
|
||||
import { TwitterPicker } from "react-color";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { Dialog, Popover, Transition } from "@headlessui/react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { ETabIndices, LABEL_COLOR_OPTIONS, getRandomLabelColor } from "@plane/constants";
|
||||
// types
|
||||
import type { IIssueLabel, IState } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Input, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// helpers
|
||||
import { getTabIndex } from "@plane/utils";
|
||||
// hooks
|
||||
@@ -83,136 +83,106 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
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"
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 px-4 pb-4 pt-5 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Create Label
|
||||
</Dialog.Title>
|
||||
<div className="mt-8 flex items-center gap-2">
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group inline-flex items-center rounded-sm py-2 text-base font-medium hover:text-custom-text-100 focus:outline-none ${
|
||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{watch("color") && watch("color") !== "" && (
|
||||
<span
|
||||
className="ml-2 h-5 w-5 rounded"
|
||||
style={{
|
||||
backgroundColor: watch("color") ?? "black",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ChevronDown
|
||||
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
||||
open ? "text-gray-600" : "text-gray-400"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="fixed left-5 z-50 mt-3 w-screen max-w-xs transform px-2 sm:px-0">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TwitterPicker
|
||||
color={value}
|
||||
colors={LABEL_COLOR_OPTIONS}
|
||||
onChange={(value) => {
|
||||
onChange(value.hex);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<div className="flex w-full flex-col justify-center gap-0.5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Label title is required",
|
||||
<div>
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">Create Label</Dialog.Title>
|
||||
<div className="mt-8 flex items-center gap-2">
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group inline-flex items-center rounded-sm py-2 text-base font-medium hover:text-custom-text-100 focus:outline-none ${
|
||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{watch("color") && watch("color") !== "" && (
|
||||
<span
|
||||
className="ml-2 h-5 w-5 rounded"
|
||||
style={{
|
||||
backgroundColor: watch("color") ?? "black",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
tabIndex={getIndex("name")}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Label title"
|
||||
className="w-full resize-none text-xl"
|
||||
/>
|
||||
)}
|
||||
<ChevronDown
|
||||
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
||||
open ? "text-gray-600" : "text-gray-400"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="fixed left-5 z-50 mt-3 w-screen max-w-xs transform px-2 sm:px-0">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TwitterPicker
|
||||
color={value}
|
||||
colors={LABEL_COLOR_OPTIONS}
|
||||
onChange={(value) => {
|
||||
onChange(value.hex);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Creating Label..." : "Create Label"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
<div className="flex w-full flex-col justify-center gap-0.5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Label title is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
tabIndex={getIndex("name")}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Label title"
|
||||
className="w-full resize-none text-xl"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Creating Label..." : "Create Label"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
@@ -56,51 +55,23 @@ export const ArchiveModuleModal: React.FC<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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Archive module {moduleName}</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">
|
||||
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="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveModule} loading={isArchiving}>
|
||||
{isArchiving ? "Archiving" : "Archive"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Archive module {moduleName}</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">
|
||||
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="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveModule} loading={isArchiving}>
|
||||
{isArchiving ? "Archiving" : "Archive"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,9 +4,8 @@ import React, { useState } from "react";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { ArrowRightLeft } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
@@ -52,62 +51,34 @@ export const SwitchAccountModal: React.FC<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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="p-6 pb-1">
|
||||
<div className="flex gap-x-4">
|
||||
<div className="flex items-start">
|
||||
<div className="grid place-items-center rounded-full bg-custom-primary-100/20 p-4">
|
||||
<ArrowRightLeft className="h-5 w-5 text-custom-primary-100" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col py-3 gap-y-6">
|
||||
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-custom-text-100">
|
||||
Switch account
|
||||
</Dialog.Title>
|
||||
{userData?.email && (
|
||||
<div className="text-base font-normal text-custom-text-200">
|
||||
If you have signed up via <span className="text-custom-primary-100">{userData.email}</span>{" "}
|
||||
un-intentionally, you can switch your account to a different one from here.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<div className="p-6 pb-1">
|
||||
<div className="flex gap-x-4">
|
||||
<div className="flex items-start">
|
||||
<div className="grid place-items-center rounded-full bg-custom-primary-100/20 p-4">
|
||||
<ArrowRightLeft className="h-5 w-5 text-custom-primary-100" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col py-3 gap-y-6">
|
||||
<Dialog.Title className="text-2xl font-medium leading-6 text-custom-text-100">
|
||||
Switch account
|
||||
</Dialog.Title>
|
||||
{userData?.email && (
|
||||
<div className="text-base font-normal text-custom-text-200">
|
||||
If you have signed up via <span className="text-custom-primary-100">{userData.email}</span>{" "}
|
||||
un-intentionally, you can switch your account to a different one from here.
|
||||
</div>
|
||||
<div className="mb-2 flex items-center justify-end gap-3 p-4 sm:px-6">
|
||||
<Button variant="accent-primary" onClick={handleSwitchAccount} disabled={switchingAccount}>
|
||||
{switchingAccount ? "Switching..." : "Switch account"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="mb-2 flex items-center justify-end gap-3 p-4 sm:px-6">
|
||||
<Button variant="accent-primary" onClick={handleSwitchAccount} disabled={switchingAccount}>
|
||||
{switchingAccount ? "Switching..." : "Switch account"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,10 @@ import React, { 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 { IUserLite } from "@plane/types";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
|
||||
@@ -49,80 +47,45 @@ export const ConfirmProjectMemberRemove: React.FC<Props> = observer((props) => {
|
||||
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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="bg-custom-background-100 px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
|
||||
{isCurrentUser
|
||||
? isDeleteLoading
|
||||
? "Leaving..."
|
||||
: "Leave"
|
||||
: isDeleteLoading
|
||||
? "Removing..."
|
||||
: "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<div className="bg-custom-background-100 px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{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>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
|
||||
{isCurrentUser ? (isDeleteLoading ? "Leaving..." : "Leave") : isDeleteLoading ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,12 +4,11 @@ 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 type { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Input, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// constants
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
@@ -90,110 +89,80 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-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-xl font-medium 2xl:text-2xl">Delete project</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
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-custom-text-200">
|
||||
<p className="break-words text-sm ">
|
||||
Enter the project name <span className="font-medium text-custom-text-100">{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-custom-text-200">
|
||||
<p className="text-sm">
|
||||
To confirm, type <span className="font-medium text-custom-text-100">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" type="submit" disabled={!canDelete} loading={isSubmitting}>
|
||||
{isSubmitting ? "Deleting" : "Delete project"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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-xl font-medium 2xl:text-2xl">Delete project</h3>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
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-custom-text-200">
|
||||
<p className="break-words text-sm ">
|
||||
Enter the project name <span className="font-medium text-custom-text-100">{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-custom-text-200">
|
||||
<p className="text-sm">
|
||||
To confirm, type <span className="font-medium text-custom-text-100">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" type="submit" disabled={!canDelete} loading={isSubmitting}>
|
||||
{isSubmitting ? "Deleting" : "Delete project"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Fragment } from "react";
|
||||
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
// types
|
||||
import type { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
@@ -44,63 +42,32 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="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-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
type="submit"
|
||||
onClick={handleJoin}
|
||||
loading={isJoiningLoading}
|
||||
>
|
||||
{isJoiningLoading ? "Joining..." : "Join Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XL}>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">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>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
type="submit"
|
||||
onClick={handleJoin}
|
||||
loading={isJoiningLoading}
|
||||
>
|
||||
{isJoiningLoading ? "Joining..." : "Join Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
import { FC } 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 { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Input, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// constants
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
@@ -109,113 +107,85 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<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-xl font-medium 2xl:text-2xl">Leave Project</h3>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to leave the project -
|
||||
<span className="font-medium text-custom-text-100">{` "${project?.name}" `}</span>? All of the
|
||||
work items associated with you will become inaccessible.
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div className="text-custom-text-200">
|
||||
<p className="break-words text-sm ">
|
||||
Enter the project name <span className="font-medium text-custom-text-100">{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-custom-text-200">
|
||||
<p className="text-sm">
|
||||
To confirm, type <span className="font-medium text-custom-text-100">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Leaving..." : "Leave Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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-xl font-medium 2xl:text-2xl">Leave Project</h3>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to leave the project -
|
||||
<span className="font-medium text-custom-text-100">{` "${project?.name}" `}</span>? All of the work items
|
||||
associated with you will become inaccessible.
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div className="text-custom-text-200">
|
||||
<p className="break-words text-sm ">
|
||||
Enter the project name <span className="font-medium text-custom-text-100">{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-custom-text-200">
|
||||
<p className="text-sm">
|
||||
To confirm, type <span className="font-medium text-custom-text-100">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Leaving..." : "Leave Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,11 +4,10 @@ import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
||||
import { ChevronDown, Plus, X } 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 { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// helpers
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
@@ -181,183 +180,144 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{t("project_settings.members.invite_members.title")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">{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-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{t("project_settings.members.invite_members.title")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{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-sm 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-custom-border-200 px-3 py-2 text-left text-sm text-custom-text-200 shadow-sm duration-300 hover:bg-custom-background-80 hover:text-custom-text-100 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>
|
||||
)}
|
||||
<ChevronDown 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-sm 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-custom-border-200 px-3 py-2.5 text-left text-sm text-custom-text-200 shadow-sm duration-300 hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none">
|
||||
<span className="capitalize">
|
||||
{field.value ? ROLE[field.value] : "Select role"}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
}
|
||||
input
|
||||
optionsClassName="w-full"
|
||||
>
|
||||
{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>
|
||||
<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-sm 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-custom-border-200 px-3 py-2 text-left text-sm text-custom-text-200 shadow-sm duration-300 hover:bg-custom-background-80 hover:text-custom-text-100 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>
|
||||
)}
|
||||
/>
|
||||
{errors.members && errors.members[index]?.role && (
|
||||
<span className="px-1 text-sm text-red-500">
|
||||
{errors.members[index]?.role?.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown 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-sm text-red-500">{errors.members[index]?.member_id?.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fields.length > 1 && (
|
||||
<div className="flex-item flex w-6">
|
||||
<button
|
||||
type="button"
|
||||
className="place-items-center self-center rounded"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<X className="h-4 w-4 text-custom-text-200" />
|
||||
</button>
|
||||
<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-custom-border-200 px-3 py-2.5 text-left text-sm text-custom-text-200 shadow-sm duration-300 hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none">
|
||||
<span className="capitalize">{field.value ? ROLE[field.value] : "Select role"}</span>
|
||||
<ChevronDown className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
input
|
||||
optionsClassName="w-full"
|
||||
>
|
||||
{Object.entries(checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))).map(
|
||||
([key, label]) => {
|
||||
if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null;
|
||||
|
||||
<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-sm font-medium text-custom-primary outline-custom-primary"
|
||||
onClick={appendField}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("common.add_more")}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" 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>
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={key}>
|
||||
{label}
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.members && errors.members[index]?.role && (
|
||||
<span className="px-1 text-sm text-red-500">{errors.members[index]?.role?.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fields.length > 1 && (
|
||||
<div className="flex-item flex w-6">
|
||||
<button
|
||||
type="button"
|
||||
className="place-items-center self-center rounded"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<X className="h-4 w-4 text-custom-text-200" />
|
||||
</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-sm font-medium text-custom-primary outline-custom-primary"
|
||||
onClick={appendField}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("common.add_more")}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" 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>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
+26
-55
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, TOAST_TYPE, setToast, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
@@ -79,60 +78,32 @@ export const ArchiveRestoreProjectModal: React.FC<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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.LG}>
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">
|
||||
{archive ? "Archive" : "Restore"} {projectDetails.name}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">
|
||||
{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="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={archive ? handleArchiveProject : handleRestoreProject}
|
||||
loading={isLoading}
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">
|
||||
{archive ? "Archive" : "Restore"} {projectDetails.name}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">
|
||||
{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="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={archive ? handleArchiveProject : handleRestoreProject}
|
||||
loading={isLoading}
|
||||
>
|
||||
{archive ? (isLoading ? "Archiving" : "Archive") : isLoading ? "Restoring" : "Restore"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
{archive ? (isLoading ? "Archiving" : "Archive") : isLoading ? "Restoring" : "Restore"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
+119
-148
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, FC } from "react";
|
||||
import { FC } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { X } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { allTimeIn30MinutesInterval12HoursFormat } from "@plane/constants";
|
||||
import { Button, CustomSelect } from "@plane/ui";
|
||||
import { Button, CustomSelect, Dialog, EModalWidth } from "@plane/ui";
|
||||
// components
|
||||
import { getDate } from "@plane/utils";
|
||||
import { DateDropdown } from "@/components/dropdowns";
|
||||
@@ -111,152 +110,124 @@ export const NotificationSnoozeModal: FC<TNotificationSnoozeModal> = (props) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel width={EModalWidth.XXL}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Customize Snooze Time
|
||||
</Dialog.Title>
|
||||
|
||||
<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-custom-background-100 p-5 text-left shadow-custom-shadow-md 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-lg font-medium leading-6 text-custom-text-100">
|
||||
Customize Snooze Time
|
||||
</Dialog.Title>
|
||||
|
||||
<div>
|
||||
<button type="button" onClick={handleClose}>
|
||||
<X className="h-5 w-5 text-custom-text-100" />
|
||||
</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-sm font-medium text-custom-text-400">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-custom-border-300 px-3 py-2.5"
|
||||
hideIcon
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">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-sm text-custom-text-400">Select a time</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
<div className="mb-2 flex h-9 w-full overflow-hidden rounded">
|
||||
<div
|
||||
onClick={() => {
|
||||
setValue("period", "AM");
|
||||
}}
|
||||
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
|
||||
watch("period") === "AM"
|
||||
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
AM
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setValue("period", "PM");
|
||||
}}
|
||||
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
|
||||
watch("period") === "PM"
|
||||
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
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-custom-text-200">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<div>
|
||||
<button type="button" onClick={handleClose}>
|
||||
<X className="h-5 w-5 text-custom-text-100" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<div className="mt-5 flex flex-col gap-3 md:!flex-row md:items-center">
|
||||
<div className="flex-1 pb-3 md:pb-0">
|
||||
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">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-custom-border-300 px-3 py-2.5"
|
||||
hideIcon
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">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-sm text-custom-text-400">Select a time</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
<div className="mb-2 flex h-9 w-full overflow-hidden rounded">
|
||||
<div
|
||||
onClick={() => {
|
||||
setValue("period", "AM");
|
||||
}}
|
||||
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
|
||||
watch("period") === "AM"
|
||||
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
AM
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setValue("period", "PM");
|
||||
}}
|
||||
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
|
||||
watch("period") === "PM"
|
||||
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
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-custom-text-200">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="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button, Dialog, EModalWidth } from "@plane/ui";
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { Props } from "./confirm-workspace-member-remove";
|
||||
|
||||
@@ -29,79 +28,49 @@ export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{currentUser?.id === userDetails.id
|
||||
? "Leave workspace?"
|
||||
: `Remove ${userDetails?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
{currentUser?.id === userDetails.id ? (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to leave the workspace? You will no longer have access to this
|
||||
workspace. This action cannot be undone.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{userDetails?.display_name}</span>? They will no longer have
|
||||
access to this workspace. This action cannot be undone.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
{currentUser?.id === userDetails.id
|
||||
? isRemoving
|
||||
? "Leaving"
|
||||
: "Leave"
|
||||
: isRemoving
|
||||
? "Removing"
|
||||
: "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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="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 className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{currentUser?.id === userDetails.id ? "Leave workspace?" : `Remove ${userDetails?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
{currentUser?.id === userDetails.id ? (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to leave the workspace? You will no longer have access to this workspace. This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{userDetails?.display_name}</span>? They will no longer have access to
|
||||
this workspace. This action cannot be undone.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
{currentUser?.id === userDetails.id
|
||||
? isRemoving
|
||||
? "Leaving"
|
||||
: "Leave"
|
||||
: isRemoving
|
||||
? "Removing"
|
||||
: "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button, Dialog, EModalWidth } from "@plane/ui";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
|
||||
@@ -42,79 +41,49 @@ export const ConfirmWorkspaceMemberRemove: React.FC<Props> = observer((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-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{currentUser?.id === userDetails.id
|
||||
? "Leave workspace?"
|
||||
: `Remove ${userDetails?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
{currentUser?.id === userDetails.id ? (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{t("workspace_settings.settings.members.leave_confirmation")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{/* TODO: Add translation here */}
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{userDetails?.display_name}</span>? They will no longer have
|
||||
access to this workspace. This action cannot be undone.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
{currentUser?.id === userDetails.id
|
||||
? isRemoving
|
||||
? t("leaving")
|
||||
: t("leave")
|
||||
: isRemoving
|
||||
? t("removing")
|
||||
: t("remove")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<Dialog.Panel 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="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 className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{currentUser?.id === userDetails.id ? "Leave workspace?" : `Remove ${userDetails?.display_name}?`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
{currentUser?.id === userDetails.id ? (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{t("workspace_settings.settings.members.leave_confirmation")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200">
|
||||
{/* TODO: Add translation here */}
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{userDetails?.display_name}</span>? They will no longer have access to
|
||||
this workspace. This action cannot be undone.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
{currentUser?.id === userDetails.id
|
||||
? isRemoving
|
||||
? t("leaving")
|
||||
: t("leave")
|
||||
: isRemoving
|
||||
? t("removing")
|
||||
: t("remove")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog } from "@headlessui/react";
|
||||
import { Dialog } from "@plane/ui";
|
||||
|
||||
type TInvitationFormProps = {
|
||||
title: string;
|
||||
@@ -18,9 +18,7 @@ export const InvitationForm = observer((props: TInvitationFormProps) => {
|
||||
return (
|
||||
<form onSubmit={onSubmit} className={className}>
|
||||
<div className="space-y-4">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Title className="text-lg font-medium leading-6 text-custom-text-100">{title}</Dialog.Title>
|
||||
<div className="text-sm text-custom-text-200">{description}</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -31,13 +31,14 @@
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@base-ui-components/react": "^1.0.0-beta.2",
|
||||
"@blueprintjs/core": "^4.16.3",
|
||||
"@blueprintjs/popover2": "^1.13.3",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@plane/constants": "*",
|
||||
"@plane/hooks": "*",
|
||||
"@plane/utils": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/utils": "*",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"clsx": "^2.0.0",
|
||||
|
||||
@@ -1,52 +1,19 @@
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui-components/react/avatar";
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Tooltip } from "../tooltip";
|
||||
// helpers
|
||||
// utils
|
||||
import { cn } from "../utils";
|
||||
|
||||
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The name of the avatar which will be displayed on the tooltip
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The background color if the avatar image fails to load
|
||||
*/
|
||||
fallbackBackgroundColor?: string;
|
||||
/**
|
||||
* The text to display if the avatar image fails to load
|
||||
*/
|
||||
name?: string; //The name of the avatar which will be displayed on the tooltip
|
||||
fallbackBackgroundColor?: string; //The background color if the avatar image fails to load
|
||||
fallbackText?: string;
|
||||
/**
|
||||
* The text color if the avatar image fails to load
|
||||
*/
|
||||
fallbackTextColor?: string;
|
||||
/**
|
||||
* Whether to show the tooltip or not
|
||||
* @default true
|
||||
*/
|
||||
fallbackTextColor?: string; //The text color if the avatar image fails to load
|
||||
showTooltip?: boolean;
|
||||
/**
|
||||
* The size of the avatars
|
||||
* Possible values: "sm", "md", "base", "lg"
|
||||
* @default "md"
|
||||
*/
|
||||
size?: TAvatarSize;
|
||||
/**
|
||||
* The shape of the avatar
|
||||
* Possible values: "circle", "square"
|
||||
* @default "circle"
|
||||
*/
|
||||
size?: TAvatarSize; //The size of the avatars
|
||||
shape?: "circle" | "square";
|
||||
/**
|
||||
* The source of the avatar image
|
||||
*/
|
||||
src?: string;
|
||||
/**
|
||||
* The custom CSS class name to apply to the component
|
||||
*/
|
||||
src?: string; //The source of the avatar image
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@@ -129,41 +96,26 @@ export const Avatar: React.FC<Props> = (props) => {
|
||||
// get size details based on the size prop
|
||||
const sizeInfo = getSizeInfo(size);
|
||||
|
||||
const fallbackLetter = name?.[0]?.toUpperCase() ?? fallbackText ?? "?";
|
||||
return (
|
||||
<Tooltip tooltipContent={fallbackText ?? name ?? "?"} disabled={!showTooltip}>
|
||||
<div
|
||||
className={cn("grid place-items-center overflow-hidden", getBorderRadius(shape), {
|
||||
[sizeInfo.avatarSize]: !isAValidNumber(size),
|
||||
})}
|
||||
style={
|
||||
isAValidNumber(size)
|
||||
? {
|
||||
height: `${size}px`,
|
||||
width: `${size}px`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{src ? (
|
||||
<img src={src} className={cn("h-full w-full", getBorderRadius(shape), className)} alt={name} />
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
sizeInfo.fontSize,
|
||||
"grid h-full w-full place-items-center",
|
||||
getBorderRadius(shape),
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: fallbackBackgroundColor ?? "#028375",
|
||||
color: fallbackTextColor ?? "#ffffff",
|
||||
}}
|
||||
>
|
||||
{name?.[0]?.toUpperCase() ?? fallbackText ?? "?"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
className={cn("grid place-items-center overflow-hidden", getBorderRadius(shape), {
|
||||
[sizeInfo.avatarSize]: !isAValidNumber(size),
|
||||
})}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<AvatarPrimitive.Root className={cn("h-full w-full", getBorderRadius(shape), className)}>
|
||||
<AvatarPrimitive.Image src={src} width="48" height="48" />
|
||||
<AvatarPrimitive.Fallback
|
||||
className={cn(sizeInfo.fontSize, "grid h-full w-full place-items-center", getBorderRadius(shape), className)}
|
||||
style={{
|
||||
backgroundColor: fallbackBackgroundColor ?? "rgba(var(--color-primary-500))",
|
||||
color: fallbackTextColor ?? "#ffffff",
|
||||
}}
|
||||
>
|
||||
{fallbackLetter}
|
||||
</AvatarPrimitive.Fallback>
|
||||
</AvatarPrimitive.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog as BaseDialog } from "@base-ui-components/react";
|
||||
import * as React from "react";
|
||||
import { EModalWidth } from "../modals/constants";
|
||||
import { cn } from "../utils";
|
||||
|
||||
function DialogPortal({ ...props }: React.ComponentProps<typeof BaseDialog.Portal>) {
|
||||
return <BaseDialog.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof BaseDialog.Backdrop>) {
|
||||
return (
|
||||
<BaseDialog.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-30 bg-custom-backdrop transition-all duration-200 [&[data-ending-style]]:opacity-0 [&[data-starting-style]]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Dialog({ ...props }: React.ComponentProps<typeof BaseDialog.Root>) {
|
||||
return <BaseDialog.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: React.ComponentProps<typeof BaseDialog.Trigger>) {
|
||||
return <BaseDialog.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPanel({
|
||||
className,
|
||||
width = EModalWidth.XXL,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof BaseDialog.Popup> & { width?: EModalWidth }) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<BaseDialog.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed flex justify-center top-0 left-0 w-full z-30 px-4 sm:py-20 overflow-y-auto overflow-hidden outline-none"
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all w-full",
|
||||
width,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</BaseDialog.Popup>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: React.ComponentProps<typeof BaseDialog.Title>) {
|
||||
return (
|
||||
<BaseDialog.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// compound components
|
||||
Dialog.Trigger = DialogTrigger;
|
||||
Dialog.Panel = DialogPanel;
|
||||
Dialog.Title = DialogTitle;
|
||||
|
||||
export { Dialog, DialogTitle, DialogTrigger, DialogPanel };
|
||||
@@ -37,3 +37,4 @@ export * from "./typography";
|
||||
export * from "./utils";
|
||||
export * from "./billing";
|
||||
export * from "./oauth";
|
||||
export * from "./dialog";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// constants
|
||||
import { EModalPosition, EModalWidth } from "./constants";
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Dialog } from "../dialog";
|
||||
// helpers
|
||||
import { cn } from "../utils";
|
||||
// constants
|
||||
import { EModalPosition, EModalWidth } from "./constants";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -14,54 +15,11 @@ type Props = {
|
||||
className?: string;
|
||||
};
|
||||
export const ModalCore: React.FC<Props> = (props) => {
|
||||
const {
|
||||
children,
|
||||
handleClose,
|
||||
isOpen,
|
||||
position = EModalPosition.CENTER,
|
||||
width = EModalWidth.XXL,
|
||||
className = "",
|
||||
} = props;
|
||||
const { children, handleClose, isOpen, width = EModalWidth.XXL, className = "" } = props;
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={() => handleClose && handleClose()}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
<div className={position}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
className={cn(
|
||||
"relative transform rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all w-full",
|
||||
width,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<Dialog open={isOpen} onOpenChange={() => handleClose && handleClose()}>
|
||||
<Dialog.Panel className={cn(width, className)}>{children}</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { Tabs as BaseTabs } from "@base-ui-components/react/tabs";
|
||||
import { LucideProps } from "lucide-react";
|
||||
import React, { FC } from "react";
|
||||
// helpers
|
||||
@@ -18,26 +18,17 @@ type TTabListProps = {
|
||||
tabClassName?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
selectedTab?: string;
|
||||
onTabChange?: (key: string) => void;
|
||||
};
|
||||
|
||||
export const TabList: FC<TTabListProps> = ({
|
||||
tabs,
|
||||
tabListClassName,
|
||||
tabClassName,
|
||||
size = "md",
|
||||
selectedTab,
|
||||
onTabChange,
|
||||
}) => (
|
||||
<Tab.List
|
||||
as="div"
|
||||
export const TabList: FC<TTabListProps> = ({ tabs, tabListClassName, tabClassName, size = "md", selectedTab }) => (
|
||||
<BaseTabs.List
|
||||
className={cn(
|
||||
"flex w-full min-w-fit items-center justify-between gap-1.5 rounded-md text-sm p-0.5 bg-custom-background-80/60",
|
||||
"flex w-full min-w-fit items-center justify-between gap-1.5 rounded-md text-sm p-0.5 bg-custom-background-80/60 relative",
|
||||
tabListClassName
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
<BaseTabs.Tab
|
||||
className={({ selected }) =>
|
||||
cn(
|
||||
"flex items-center justify-center p-1 min-w-fit w-full font-medium text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all rounded",
|
||||
@@ -55,17 +46,13 @@ export const TabList: FC<TTabListProps> = ({
|
||||
)
|
||||
}
|
||||
key={tab.key}
|
||||
onClick={() => {
|
||||
if (!tab.disabled) {
|
||||
onTabChange?.(tab.key);
|
||||
tab.onClick?.();
|
||||
}
|
||||
}}
|
||||
disabled={tab.disabled}
|
||||
>
|
||||
{tab.icon && <tab.icon className="size-4" />}
|
||||
{tab.label}
|
||||
</Tab>
|
||||
</BaseTabs.Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
|
||||
<BaseTabs.Indicator className="absolute left-0 top-[50%] z-[-1] h-6 w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] -translate-y-[50%] rounded-sm bg-custom-background-100 shadow-sm transition-[width,transform] duration-200 ease-in-out" />
|
||||
</BaseTabs.List>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tab } from "@headlessui/react";
|
||||
import React, { FC, Fragment, useEffect, useState } from "react";
|
||||
import { Tabs as BaseTabs } from "@base-ui-components/react/tabs";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
// helpers
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import { cn } from "../utils";
|
||||
@@ -40,49 +40,52 @@ export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
|
||||
size = "md",
|
||||
storeInLocalStorage = true,
|
||||
} = props;
|
||||
// local storage
|
||||
|
||||
const { storedValue, setValue } = useLocalStorage(
|
||||
storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`,
|
||||
defaultTab
|
||||
);
|
||||
// state
|
||||
const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab);
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(() => {
|
||||
const initialTab = storedValue ?? defaultTab;
|
||||
return tabs.findIndex((tab) => tab.key === initialTab);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (storeInLocalStorage) {
|
||||
setValue(selectedTab);
|
||||
if (storeInLocalStorage && tabs[activeIndex]) {
|
||||
setValue(tabs[activeIndex].key);
|
||||
}
|
||||
}, [selectedTab, setValue, storeInLocalStorage, storageKey]);
|
||||
}, [activeIndex, setValue, storeInLocalStorage, tabs]);
|
||||
|
||||
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setSelectedTab(key);
|
||||
const handleTabChange = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
if (!tabs[index].disabled) {
|
||||
tabs[index].onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full">
|
||||
<Tab.Group defaultIndex={currentTabIndex(selectedTab)}>
|
||||
<div className={cn("flex flex-col w-full h-full gap-2", containerClassName)}>
|
||||
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
tabListClassName={tabListClassName}
|
||||
tabClassName={tabClassName}
|
||||
size={size}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
{actions && <div className="flex-grow">{actions}</div>}
|
||||
</div>
|
||||
<Tab.Panels as={Fragment}>
|
||||
{tabs.map((tab) => (
|
||||
<Tab.Panel key={tab.key} as="div" className={cn("relative outline-none", tabPanelClassName)}>
|
||||
{tab.content}
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</div>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
<BaseTabs.Root
|
||||
value={activeIndex}
|
||||
onValueChange={handleTabChange}
|
||||
className={cn("flex flex-col w-full h-full overflow-hidden", containerClassName)}
|
||||
>
|
||||
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
tabListClassName={tabListClassName}
|
||||
tabClassName={tabClassName}
|
||||
size={size}
|
||||
selectedTab={tabs[activeIndex]?.key}
|
||||
/>
|
||||
{actions && <div className="flex-grow">{actions}</div>}
|
||||
</div>
|
||||
|
||||
{tabs.map((tab) => (
|
||||
<BaseTabs.Panel key={tab.key} className={cn("relative h-full overflow-auto", tabPanelClassName)}>
|
||||
{tab.content}
|
||||
</BaseTabs.Panel>
|
||||
))}
|
||||
</BaseTabs.Root>
|
||||
);
|
||||
};
|
||||
|
||||
+180
-131
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
// icons
|
||||
import { Toast as BaseToast } from "@base-ui-components/react/toast";
|
||||
import { AlertTriangle, CheckCircle2, X, XCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
// icons
|
||||
// spinner
|
||||
import { CircularBarSpinner } from "../spinners";
|
||||
// helper
|
||||
@@ -43,158 +43,207 @@ type PromiseToastOptions<ToastData> = {
|
||||
error: PromiseToastData<ToastData>;
|
||||
};
|
||||
|
||||
type ToastContentProps = {
|
||||
toastId: string | number;
|
||||
icon?: React.ReactNode;
|
||||
textColorClassName: string;
|
||||
backgroundColorClassName: string;
|
||||
borderColorClassName: string;
|
||||
};
|
||||
|
||||
type ToastProps = {
|
||||
theme: "light" | "dark" | "system";
|
||||
};
|
||||
|
||||
// Global toast manager to allow triggering from anywhere
|
||||
const toastManager = BaseToast.createToastManager();
|
||||
|
||||
export const Toast = (props: ToastProps) => {
|
||||
const { theme } = props;
|
||||
return <Toaster visibleToasts={5} gap={16} theme={theme} />;
|
||||
|
||||
return (
|
||||
<BaseToast.Provider toastManager={toastManager} limit={5} timeout={4000}>
|
||||
<BaseToast.Viewport
|
||||
render={(vpProps) => (
|
||||
<div
|
||||
{...vpProps}
|
||||
className={cn(
|
||||
vpProps.className,
|
||||
"fixed isolate z-[100] bottom-4 right-4 flex w-full max-w-[420px] flex-col gap-3 px-4",
|
||||
"data-[expanded]:[&>*]:translate-y-[var(--toast-offset-y)]"
|
||||
)}
|
||||
>
|
||||
{vpProps.children}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<ToastList />
|
||||
</BaseToast.Viewport>
|
||||
<div data-theme={theme} />
|
||||
</BaseToast.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const setToast = (props: SetToastProps) => {
|
||||
const renderToastContent = ({
|
||||
toastId,
|
||||
icon,
|
||||
textColorClassName,
|
||||
backgroundColorClassName,
|
||||
borderColorClassName,
|
||||
}: ToastContentProps) =>
|
||||
props.type === TOAST_TYPE.LOADING ? (
|
||||
<div className="flex items-center h-[98px] w-[350px]" data-prevent-outside-click>
|
||||
<div
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
className={cn("w-full rounded-lg border shadow-sm p-2", backgroundColorClassName, borderColorClassName)}
|
||||
>
|
||||
<div className="w-full h-full flex items-center justify-center px-4 py-2">
|
||||
{icon && <div className="flex items-center justify-center">{icon}</div>}
|
||||
<div className={cn("w-full flex items-center gap-0.5 pr-1", icon ? "pl-4" : "pl-1")}>
|
||||
<div className={cn("grow text-sm font-semibold", textColorClassName)}>{props.title ?? "Loading..."}</div>
|
||||
<div className="flex-shrink-0">
|
||||
<X
|
||||
className="text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
|
||||
strokeWidth={1.5}
|
||||
width={14}
|
||||
height={14}
|
||||
onClick={() => toast.dismiss(toastId)}
|
||||
/>
|
||||
function ToastList() {
|
||||
const { toasts } = BaseToast.useToastManager();
|
||||
|
||||
return toasts.map((t) => {
|
||||
const type =
|
||||
((t as any).type as TOAST_TYPE) || (((t.data as any) && (t.data as any).tone) as TOAST_TYPE) || TOAST_TYPE.INFO;
|
||||
|
||||
const classesByType = (toastType: TOAST_TYPE) => {
|
||||
switch (toastType) {
|
||||
case TOAST_TYPE.SUCCESS:
|
||||
return {
|
||||
text: "text-toast-text-success",
|
||||
bg: "bg-toast-background-success",
|
||||
border: "border-toast-border-success",
|
||||
icon: <CheckCircle2 width={24} height={24} strokeWidth={1.5} className="text-toast-text-success" />,
|
||||
};
|
||||
case TOAST_TYPE.ERROR:
|
||||
return {
|
||||
text: "text-toast-text-error",
|
||||
bg: "bg-toast-background-error",
|
||||
border: "border-toast-border-error",
|
||||
icon: <XCircle width={24} height={24} strokeWidth={1.5} className="text-toast-text-error" />,
|
||||
};
|
||||
case TOAST_TYPE.WARNING:
|
||||
return {
|
||||
text: "text-toast-text-warning",
|
||||
bg: "bg-toast-background-warning",
|
||||
border: "border-toast-border-warning",
|
||||
icon: <AlertTriangle width={24} height={24} strokeWidth={1.5} className="text-toast-text-warning" />,
|
||||
};
|
||||
case TOAST_TYPE.LOADING:
|
||||
return {
|
||||
text: "text-toast-text-loading",
|
||||
bg: "bg-toast-background-loading",
|
||||
border: "border-toast-border-loading",
|
||||
icon: <CircularBarSpinner className="text-toast-text-tertiary" />,
|
||||
};
|
||||
case TOAST_TYPE.INFO:
|
||||
default:
|
||||
return {
|
||||
text: "text-toast-text-info",
|
||||
bg: "bg-toast-background-info",
|
||||
border: "border-toast-border-info",
|
||||
icon: undefined as React.ReactNode | undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const cls = classesByType(type);
|
||||
const actionItems =
|
||||
typeof (t.data as any)?.actionItems === "function" ? (t.data as any).actionItems() : (t.data as any)?.actionItems;
|
||||
|
||||
return (
|
||||
<BaseToast.Root
|
||||
key={t.id}
|
||||
toast={t}
|
||||
className={cn(
|
||||
"[--toast-index:var(--toast-index)] relative group flex flex-col w-[350px] rounded-lg border shadow-custom-shadow-md p-2",
|
||||
"transition-all duration-300",
|
||||
"data-[starting-style]:opacity-0 data-[starting-style]:-translate-y-2",
|
||||
"data-[ending-style]:opacity-0 data-[ending-style]:translate-y-2",
|
||||
cls.bg,
|
||||
cls.border
|
||||
)}
|
||||
>
|
||||
{type === TOAST_TYPE.LOADING ? (
|
||||
<div className="flex items-center h-[98px] w-[350px]" data-prevent-outside-click>
|
||||
<div className={cn("w-full rounded-lg border shadow-sm p-2", cls.bg, cls.border)}>
|
||||
<div className="w-full h-full flex items-center justify-center px-4 py-2">
|
||||
{cls.icon && <div className="flex items-center justify-center">{cls.icon}</div>}
|
||||
<div className={cn("w-full flex items-center gap-0.5 pr-1", cls.icon ? "pl-4" : "pl-1", cls.text)}>
|
||||
<div className={cn("grow text-sm font-semibold", cls.text)}>{t.title ?? "Loading..."}</div>
|
||||
<div className="flex-shrink-0">
|
||||
<BaseToast.Close
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className="text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
|
||||
>
|
||||
<X strokeWidth={1.5} width={14} height={14} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
data-prevent-outside-click
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
className={cn(
|
||||
"relative group flex flex-col w-[350px] rounded-lg border shadow-sm p-2",
|
||||
backgroundColorClassName,
|
||||
borderColorClassName
|
||||
)}
|
||||
>
|
||||
<X
|
||||
className="absolute top-2 right-2.5 text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
|
||||
strokeWidth={1.5}
|
||||
width={14}
|
||||
height={14}
|
||||
onClick={() => toast.dismiss(toastId)}
|
||||
/>
|
||||
<div className="w-full flex flex-col gap-2 p-2">
|
||||
<div className="flex items-center w-full">
|
||||
{icon && <div className="flex items-center justify-center">{icon}</div>}
|
||||
<div className={cn("flex flex-col gap-0.5 pr-1", icon ? "pl-4" : "pl-1")}>
|
||||
<div className={cn("text-sm font-semibold", textColorClassName)}>{props.title}</div>
|
||||
{props.message && <div className="text-toast-text-secondary text-xs font-medium">{props.message}</div>}
|
||||
) : (
|
||||
<div data-prevent-outside-click>
|
||||
<BaseToast.Close
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className="absolute top-2 right-2.5 text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
|
||||
>
|
||||
<X strokeWidth={1.5} width={14} height={14} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<div className="w-full flex flex-col gap-2 p-2">
|
||||
<div className="flex items-center w-full">
|
||||
{cls.icon && <div className="flex items-center justify-center">{cls.icon}</div>}
|
||||
<div className={cn("flex flex-col gap-0.5 pr-1", cls.icon ? "pl-4" : "pl-1")}>
|
||||
<div className={cn("text-sm font-semibold", cls.text)}>{t.title}</div>
|
||||
{t.description && (
|
||||
<div className={cn("text-xs font-medium", "text-toast-text-secondary")}>{t.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{actionItems && <div className="flex items-center pl-[32px]">{actionItems}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{props.actionItems && <div className="flex items-center pl-[32px]">{props.actionItems}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</BaseToast.Root>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
switch (props.type) {
|
||||
case TOAST_TYPE.SUCCESS:
|
||||
return toast.custom(
|
||||
(toastId) =>
|
||||
renderToastContent({
|
||||
toastId,
|
||||
icon: <CheckCircle2 width={24} height={24} strokeWidth={1.5} className="text-toast-text-success" />,
|
||||
textColorClassName: "text-toast-text-success",
|
||||
backgroundColorClassName: "bg-toast-background-success",
|
||||
borderColorClassName: "border-toast-border-success",
|
||||
}),
|
||||
props.id ? { id: props.id } : {}
|
||||
);
|
||||
case TOAST_TYPE.ERROR:
|
||||
return toast.custom(
|
||||
(toastId) =>
|
||||
renderToastContent({
|
||||
toastId,
|
||||
icon: <XCircle width={24} height={24} strokeWidth={1.5} className="text-toast-text-error" />,
|
||||
textColorClassName: "text-toast-text-error",
|
||||
backgroundColorClassName: "bg-toast-background-error",
|
||||
borderColorClassName: "border-toast-border-error",
|
||||
}),
|
||||
props.id ? { id: props.id } : {}
|
||||
);
|
||||
case TOAST_TYPE.WARNING:
|
||||
return toast.custom(
|
||||
(toastId) =>
|
||||
renderToastContent({
|
||||
toastId,
|
||||
icon: <AlertTriangle width={24} height={24} strokeWidth={1.5} className="text-toast-text-warning" />,
|
||||
textColorClassName: "text-toast-text-warning",
|
||||
backgroundColorClassName: "bg-toast-background-warning",
|
||||
borderColorClassName: "border-toast-border-warning",
|
||||
}),
|
||||
props.id ? { id: props.id } : {}
|
||||
);
|
||||
case TOAST_TYPE.INFO:
|
||||
return toast.custom(
|
||||
(toastId) =>
|
||||
renderToastContent({
|
||||
toastId,
|
||||
textColorClassName: "text-toast-text-info",
|
||||
backgroundColorClassName: "bg-toast-background-info",
|
||||
borderColorClassName: "border-toast-border-info",
|
||||
}),
|
||||
props.id ? { id: props.id } : {}
|
||||
);
|
||||
|
||||
case TOAST_TYPE.LOADING:
|
||||
return toast.custom((toastId) =>
|
||||
renderToastContent({
|
||||
toastId,
|
||||
icon: <CircularBarSpinner className="text-toast-text-tertiary" />,
|
||||
textColorClassName: "text-toast-text-loading",
|
||||
backgroundColorClassName: "bg-toast-background-loading",
|
||||
borderColorClassName: "border-toast-border-loading",
|
||||
})
|
||||
);
|
||||
export const setToast = (props: SetToastProps) => {
|
||||
if (props.type === TOAST_TYPE.LOADING) {
|
||||
// Add loading toast (non-dismissable by timeout)
|
||||
const id = toastManager.add({
|
||||
title: props.title ?? "Loading...",
|
||||
type: TOAST_TYPE.LOADING,
|
||||
timeout: 0,
|
||||
data: {},
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
const { id, type, title, message, actionItems } = props as Exclude<SetToastProps, { type: TOAST_TYPE.LOADING }>;
|
||||
if (id !== undefined) {
|
||||
toastManager.update(String(id), {
|
||||
title,
|
||||
description: message,
|
||||
type,
|
||||
data: { actionItems },
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
return toastManager.add({
|
||||
title,
|
||||
description: message,
|
||||
type,
|
||||
data: { actionItems },
|
||||
});
|
||||
};
|
||||
|
||||
export const setPromiseToast = <ToastData,>(
|
||||
promise: Promise<ToastData>,
|
||||
options: PromiseToastOptions<ToastData>
|
||||
): void => {
|
||||
// create a loading toast and keep its id for subsequent updates
|
||||
const tId = setToast({ type: TOAST_TYPE.LOADING, title: options.loading });
|
||||
|
||||
// also wire Base UI's promise helper to manage description lifecycle
|
||||
// using strings/functions per API for loading/success/error
|
||||
toastManager.promise(promise, {
|
||||
loading: options.loading ?? "Loading...",
|
||||
success: options.success.message ? (data: ToastData) => options.success.message!(data) : options.success.title,
|
||||
error: options.error.message ? (data: ToastData) => options.error.message!(data) : options.error.title,
|
||||
});
|
||||
|
||||
// preserve title + action items behavior exactly like the existing API
|
||||
promise
|
||||
.then((data: ToastData) => {
|
||||
setToast({
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./classname";
|
||||
export * from "./icons";
|
||||
export * from "./classname";
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
"extends": "@plane/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"lib": ["esnext", "dom"]
|
||||
"lib": ["esnext", "dom"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
|
||||
"@babel/helpers@7.26.10", "@babel/helpers@^7.27.6":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
|
||||
resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
|
||||
integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==
|
||||
dependencies:
|
||||
"@babel/template" "^7.26.9"
|
||||
@@ -165,16 +165,16 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.28.0"
|
||||
|
||||
"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
|
||||
"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.27.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
|
||||
integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.26.9", "@babel/template@^7.27.2":
|
||||
version "7.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
|
||||
resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
|
||||
integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.27.1"
|
||||
@@ -194,22 +194,37 @@
|
||||
"@babel/types" "^7.28.0"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.28.0":
|
||||
version "7.28.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.1.tgz#2aaf3c10b31ba03a77ac84f52b3912a0edef4cf9"
|
||||
integrity sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@babel/types@^7.26.10":
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.26.10", "@babel/types@^7.27.1", "@babel/types@^7.28.0":
|
||||
version "7.28.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
|
||||
resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
|
||||
integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@base-ui-components/react@^1.0.0-beta.2":
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.npmjs.org/@base-ui-components/react/-/react-1.0.0-beta.2.tgz#9a07bb6a462907b13aa35979e2b8235cc145adbc"
|
||||
integrity sha512-jfAUfSgXvsfr8mQi7r/6gLG8U1Ybr77NN8WK5IXXM0c/hBvFDBtvUfwDJACV0gXiYbSKpA+dRzZz01V1tULobA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.27.6"
|
||||
"@base-ui-components/utils" "0.1.0"
|
||||
"@floating-ui/react-dom" "^2.1.5"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
reselect "^5.1.1"
|
||||
tabbable "^6.2.0"
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
||||
"@base-ui-components/utils@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@base-ui-components/utils/-/utils-0.1.0.tgz#56d0c801eb5afda7fe3f16b94b78cb75745d1eb1"
|
||||
integrity sha512-9+uaWyF1o/PgXqHLJnC81IIG0HlV3o9eFCQ5hWZDMx5NHrFk0rrwqEFGQOB8lti/rnbxNPi+kYYw1D4e8xSn/Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.27.6"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
reselect "^5.1.1"
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
||||
"@blueprintjs/colors@^4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@blueprintjs/colors/-/colors-4.2.1.tgz#603b2512caee84feddcb3dbd536534c140b9a1f3"
|
||||
@@ -634,7 +649,14 @@
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/dom@^1.7.1", "@floating-ui/dom@^1.7.2":
|
||||
"@floating-ui/core@^1.7.3":
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7"
|
||||
integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/dom@^1.7.1":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.2.tgz#3540b051cf5ce0d4f4db5fb2507a76e8ea5b4a45"
|
||||
integrity sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==
|
||||
@@ -642,12 +664,20 @@
|
||||
"@floating-ui/core" "^1.7.2"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/react-dom@^2.1.2":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.4.tgz#a0689be8978352fff2be2dfdd718cf668c488ec3"
|
||||
integrity sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==
|
||||
"@floating-ui/dom@^1.7.3":
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.3.tgz#6174ac3409e6a064bbdf1f4bb07188ee9461f8cf"
|
||||
integrity sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.7.2"
|
||||
"@floating-ui/core" "^1.7.3"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/react-dom@^2.1.2", "@floating-ui/react-dom@^2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.5.tgz#d11e3726d2eb385d8cf3216348742907c1d49fcf"
|
||||
integrity sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.7.3"
|
||||
|
||||
"@floating-ui/react@^0.26.4":
|
||||
version "0.26.28"
|
||||
@@ -2467,7 +2497,7 @@
|
||||
|
||||
"@types/express@*", "@types/express@4.17.23", "@types/express@^4.17.21", "@types/express@^4.17.23":
|
||||
version "4.17.23"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef"
|
||||
resolved "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef"
|
||||
integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
@@ -2673,7 +2703,7 @@
|
||||
|
||||
"@types/react-dom@18.3.0":
|
||||
version "18.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
|
||||
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
@@ -2696,15 +2726,16 @@
|
||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||
|
||||
"@types/react@*":
|
||||
version "19.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.8.tgz#ff8395f2afb764597265ced15f8dddb0720ae1c3"
|
||||
integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==
|
||||
version "18.3.18"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b"
|
||||
integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@18.3.1":
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e"
|
||||
integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
@@ -3766,7 +3797,7 @@ boolbase@^1.0.0:
|
||||
|
||||
brace-expansion@2.0.2, brace-expansion@^1.1.7, brace-expansion@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
|
||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
|
||||
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
@@ -3964,7 +3995,7 @@ check-error@^2.1.1:
|
||||
|
||||
chokidar@3.6.0, chokidar@^3.3.0, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0, chokidar@^4.0.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
@@ -5077,7 +5108,7 @@ esbuild-register@^3.5.0:
|
||||
|
||||
esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", esbuild@^0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92"
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92"
|
||||
integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.25.0"
|
||||
@@ -7460,7 +7491,7 @@ mz@^2.7.0:
|
||||
|
||||
nanoid@3.3.8, nanoid@^3.3.11, nanoid@^3.3.6:
|
||||
version "3.3.8"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||
|
||||
napi-build-utils@^2.0.0:
|
||||
@@ -8475,7 +8506,7 @@ prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transfor
|
||||
|
||||
prosemirror-view@1.40.0, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.39.1:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.40.0.tgz#212e627a0c4f0198ac9823a1232e0099c9a92865"
|
||||
resolved "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.40.0.tgz#212e627a0c4f0198ac9823a1232e0099c9a92865"
|
||||
integrity sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==
|
||||
dependencies:
|
||||
prosemirror-model "^1.20.0"
|
||||
@@ -9001,6 +9032,11 @@ require-from-string@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
reselect@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
|
||||
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
@@ -9781,7 +9817,7 @@ symbol-tree@^3.2.4:
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
tabbable@^6.0.0:
|
||||
tabbable@^6.0.0, tabbable@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
|
||||
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||
@@ -9831,7 +9867,7 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
|
||||
|
||||
tar-fs@3.0.9, tar-fs@^2.0.0, tar-fs@^3.0.4:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.9.tgz#d570793c6370d7078926c41fa422891566a0b617"
|
||||
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz#d570793c6370d7078926c41fa422891566a0b617"
|
||||
integrity sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
@@ -10463,7 +10499,7 @@ use-sidecar@^1.1.3:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1, use-sync-external-store@^1.4.0:
|
||||
use-sync-external-store@^1, use-sync-external-store@^1.4.0, use-sync-external-store@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
|
||||
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
||||
|
||||
Reference in New Issue
Block a user