Compare commits

...
Author SHA1 Message Date
vamsikrishnamathala 0917ef6f5f draft: toast migration 2025-08-19 12:11:08 +05:30
Anmol Singh BhatiaandGitHub 6bf36ad85c [WEB-4684] feat: migrate headless-ui dialog to base-ui dialog (#7579)
* chore: replaced headless-ui dialog component with base-ui dialog in ui package and web app

* chore: code refactor
2025-08-16 14:19:31 +05:30
Jayash Tripathy 3272abac09 refactor: style cleanup 2025-08-12 01:22:18 +05:30
Jayash Tripathy 650df35840 Merge remote-tracking branch 'origin/preview' into feat-base-ui-migration 2025-08-12 01:03:42 +05:30
Jayash TripathyandGitHub 5e0d3c9108 Feat base UI tabs (#7569)
* chore: added base-ui tabs

* refactor: cleanup
2025-08-11 18:47:15 +05:30
sriramveeraghanta 70cdd49ea9 fix: update avatar component 2025-08-06 20:01:47 +05:30
42 changed files with 2102 additions and 2895 deletions
@@ -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 dont 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 dont 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 &apos;Join
Project&apos; button below to continue.
</p>
<div className="space-y-3" />
</div>
<div className="mt-5 flex justify-end gap-2">
<Button variant="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 &apos;Join Project&apos; 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>
);
});
@@ -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 wont appear in search. Only project admins can restore the project."
: "Restoring a project will activate it and make it visible to all members of the project. Are you sure you want to continue?"}
</p>
<div className="mt-3 flex justify-end gap-2">
<Button variant="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 wont appear in search. Only project admins can restore the project."
: "Restoring a project will activate it and make it visible to all members of the project. Are you sure you want to continue?"}
</p>
<div className="mt-3 flex justify-end gap-2">
<Button variant="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>
);
};
@@ -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>
+2 -1
View File
@@ -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",
+27 -75
View File
@@ -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>
);
};
+1
View File
@@ -0,0 +1 @@
export * from "./root";
+77
View File
@@ -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 };
+1
View File
@@ -37,3 +37,4 @@ export * from "./typography";
export * from "./utils";
export * from "./billing";
export * from "./oauth";
export * from "./dialog";
+9 -51
View File
@@ -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>
);
};
+9 -22
View File
@@ -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>
);
+38 -35
View File
@@ -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
View File
@@ -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
View File
@@ -1,2 +1,3 @@
export * from "./classname";
export * from "./icons";
export * from "./classname";
+5 -1
View File
@@ -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"]
+70 -34
View File
@@ -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==