mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
Merge branch 'develop' of github.com:makeplane/plane-ee into preview
This commit is contained in:
14
README.md
14
README.md
@@ -7,7 +7,7 @@
|
||||
</p>
|
||||
|
||||
<h3 align="center"><b>Plane</b></h3>
|
||||
<p align="center"><b>Open-source project management that unlocks customer value.</b></p>
|
||||
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.com/invite/A92xrEGCge">
|
||||
@@ -40,22 +40,22 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Meet [Plane](https://dub.sh/plane-website-readme). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘♀️
|
||||
Meet [Plane](https://dub.sh/plane-website-readme), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘♀️
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve in our upcoming releases.
|
||||
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
|
||||
|
||||
## ⚡ Installation
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users.
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
|
||||
|
||||
If you want more control over your data, prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
|
||||
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
|
||||
|
||||
| Installation Methods | Documentation Link |
|
||||
| Installation methods | Docs link |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Docker | [](https://docs.plane.so/self-hosting/methods/docker-compose) |
|
||||
| Kubernetes | [](https://docs.plane.so/kubernetes) |
|
||||
|
||||
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
|
||||
`Instance admins` can configure instance settings with [God-mode](https://docs.plane.so/instance-admin).
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
|
||||
@@ -636,6 +636,7 @@ class IssueInboxSerializer(DynamicBaseSerializer):
|
||||
"project_id",
|
||||
"created_at",
|
||||
"label_ids",
|
||||
"created_by",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -431,15 +431,15 @@ class ModuleViewSet(BaseViewSet):
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
module = self.get_queryset().filter(pk=pk)
|
||||
current_instance = json.dumps(
|
||||
ModuleSerializer(module).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
if module.first().archived_at:
|
||||
return Response(
|
||||
{"error": "Archived module cannot be updated"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
ModuleSerializer(module.first()).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
serializer = ModuleWriteSerializer(
|
||||
module.first(), data=request.data, partial=True
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
# Module imports
|
||||
from plane.api.serializers import (
|
||||
@@ -422,6 +423,9 @@ def webhook_activity(
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
# Return if a does not exist error occurs
|
||||
if isinstance(e, ObjectDoesNotExist):
|
||||
return
|
||||
if settings.DEBUG:
|
||||
print(e)
|
||||
log_exception(e)
|
||||
@@ -462,21 +466,23 @@ def model_activity(
|
||||
|
||||
# Loop through all keys in requested data and check the current value and requested value
|
||||
for key in requested_data:
|
||||
current_value = current_instance.get(key, None)
|
||||
requested_value = requested_data.get(key, None)
|
||||
if current_value != requested_value:
|
||||
webhook_activity.delay(
|
||||
event=model_name,
|
||||
verb="updated",
|
||||
field=key,
|
||||
old_value=current_value,
|
||||
new_value=requested_value,
|
||||
actor_id=actor_id,
|
||||
slug=slug,
|
||||
current_site=origin,
|
||||
event_id=model_id,
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
)
|
||||
# Check if key is present in current instance or not
|
||||
if key in current_instance:
|
||||
current_value = current_instance.get(key, None)
|
||||
requested_value = requested_data.get(key, None)
|
||||
if current_value != requested_value:
|
||||
webhook_activity.delay(
|
||||
event=model_name,
|
||||
verb="updated",
|
||||
field=key,
|
||||
old_value=current_value,
|
||||
new_value=requested_value,
|
||||
actor_id=actor_id,
|
||||
slug=slug,
|
||||
current_site=origin,
|
||||
event_id=model_id,
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -158,7 +158,7 @@ export async function startImageUpload(
|
||||
const transaction = view.state.tr.insert(pos - 1, node).setMeta(uploadKey, { remove: { id } });
|
||||
|
||||
view.dispatch(transaction);
|
||||
view.focus();
|
||||
if (view.hasFocus()) view.focus();
|
||||
editor.storage.image.uploadInProgress = false;
|
||||
} catch (error) {
|
||||
removePlaceholder(editor, view, id);
|
||||
|
||||
@@ -66,6 +66,11 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (isOpen) closeDropdown();
|
||||
else openDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -90,7 +95,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
onClick={openDropdown}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
@@ -107,7 +112,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={openDropdown}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
|
||||
@@ -47,6 +47,11 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (isOpen) closeDropdown();
|
||||
else openDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
as="div"
|
||||
@@ -67,7 +72,7 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
className={`flex items-center justify-between gap-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
onClick={openDropdown}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
@@ -82,7 +87,7 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
} ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={openDropdown}
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useState, Fragment, FC } from "react";
|
||||
import { useState, FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { IApiToken } from "@plane/types";
|
||||
// services
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
import { APITokenService } from "@/services/api_token.service";
|
||||
// ui
|
||||
// types
|
||||
import { IApiToken } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// fetch-keys
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// services
|
||||
import { APITokenService } from "@/services/api_token.service";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -32,12 +33,12 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
|
||||
setDeleteLoading(false);
|
||||
};
|
||||
|
||||
const handleDeletion = () => {
|
||||
const handleDeletion = async () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
setDeleteLoading(true);
|
||||
|
||||
apiTokenService
|
||||
await apiTokenService
|
||||
.deleteApiToken(workspaceSlug.toString(), tokenId)
|
||||
.then(() => {
|
||||
setToast({
|
||||
@@ -57,7 +58,7 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: err?.message ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
@@ -65,58 +66,17 @@ export const DeleteApiTokenModal: 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 bg-opacity-50 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 border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Are you sure you want to delete the token?
|
||||
</h3>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Any application using this token will no longer have the access to Plane data. This action cannot
|
||||
be undone.
|
||||
</p>
|
||||
</span>
|
||||
<div className="mt-2 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" onClick={handleClose} size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDeletion} loading={deleteLoading} size="sm">
|
||||
{deleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDeletion}
|
||||
isDeleting={deleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete API token"
|
||||
content={
|
||||
<>
|
||||
Any application using this token will no longer have the access to Plane data. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IApiToken } from "@plane/types";
|
||||
// services
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
||||
// components
|
||||
import { CreateApiTokenForm, GeneratedTokenDetails } from "@/components/api-token";
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// fetch-keys
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { csvDownload } from "@/helpers/download.helper";
|
||||
// services
|
||||
import { APITokenService } from "@/services/api_token.service";
|
||||
// ui
|
||||
// components
|
||||
// helpers
|
||||
// types
|
||||
// fetch-keys
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -77,7 +76,7 @@ export const CreateApiTokenModal: React.FC<Props> = (props) => {
|
||||
.catch((err) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: err.message,
|
||||
});
|
||||
|
||||
@@ -86,47 +85,17 @@ export const CreateApiTokenModal: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" 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>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="grid h-full w-full place-items-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 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-2xl">
|
||||
{generatedToken ? (
|
||||
<GeneratedTokenDetails handleClose={handleClose} tokenDetails={generatedToken} />
|
||||
) : (
|
||||
<CreateApiTokenForm
|
||||
handleClose={handleClose}
|
||||
neverExpires={neverExpires}
|
||||
toggleNeverExpires={() => setNeverExpires((prevData) => !prevData)}
|
||||
onSubmit={handleCreateToken}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ModalCore isOpen={isOpen} handleClose={() => {}} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
{generatedToken ? (
|
||||
<GeneratedTokenDetails handleClose={handleClose} tokenDetails={generatedToken} />
|
||||
) : (
|
||||
<CreateApiTokenForm
|
||||
handleClose={handleClose}
|
||||
neverExpires={neverExpires}
|
||||
toggleNeverExpires={() => setNeverExpires((prevData) => !prevData)}
|
||||
onSubmit={handleCreateToken}
|
||||
/>
|
||||
)}
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,13 +2,15 @@ import { useState } from "react";
|
||||
import { add } from "date-fns";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Calendar } from "lucide-react";
|
||||
// types
|
||||
import { IApiToken } from "@plane/types";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { DateDropdown } from "@/components/dropdowns";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
@@ -106,13 +108,14 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = add(today, { days: 1 });
|
||||
const expiredAt = watch("expired_at");
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Create token</h3>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Create token</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="label"
|
||||
@@ -130,8 +133,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.label)}
|
||||
placeholder="Token title"
|
||||
className="w-full text-sm font-medium"
|
||||
placeholder="Title"
|
||||
className="w-full text-base"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -145,8 +148,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.description)}
|
||||
placeholder="Token description"
|
||||
className="min-h-24 w-full text-sm"
|
||||
placeholder="Description"
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -162,9 +165,12 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
||||
<CustomSelect
|
||||
customButton={
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 ${
|
||||
neverExpires ? "text-custom-text-400" : ""
|
||||
}`}
|
||||
className={cn(
|
||||
"h-7 flex items-center gap-2 rounded border-[0.5px] border-custom-border-300 px-2 py-0.5",
|
||||
{
|
||||
"text-custom-text-400": neverExpires,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Calendar className="h-3 w-3" />
|
||||
{value === "custom"
|
||||
@@ -188,33 +194,35 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{watch("expired_at") === "custom" && (
|
||||
<DateDropdown
|
||||
value={customDate}
|
||||
onChange={(date) => setCustomDate(date)}
|
||||
minDate={tomorrow}
|
||||
icon={<Calendar className="h-3 w-3" />}
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Set date"
|
||||
disabled={neverExpires}
|
||||
/>
|
||||
{expiredAt === "custom" && (
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={customDate}
|
||||
onChange={(date) => setCustomDate(date)}
|
||||
minDate={tomorrow}
|
||||
icon={<Calendar className="h-3 w-3" />}
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Set date"
|
||||
disabled={neverExpires}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!neverExpires && (
|
||||
<span className="text-xs text-custom-text-400">
|
||||
{watch("expired_at") === "custom"
|
||||
{expiredAt === "custom"
|
||||
? customDate
|
||||
? `Expires ${renderFormattedDate(customDate)}`
|
||||
: null
|
||||
: watch("expired_at")
|
||||
? `Expires ${getExpiryDate(watch("expired_at") ?? "")}`
|
||||
: expiredAt
|
||||
? `Expires ${getExpiryDate(expiredAt ?? "")}`
|
||||
: null}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center justify-between gap-2">
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<div className="flex cursor-pointer items-center gap-1.5" onClick={toggleNeverExpires}>
|
||||
<div className="flex cursor-pointer items-center justify-center">
|
||||
<ToggleSwitch value={neverExpires} onChange={() => {}} size="sm" />
|
||||
@@ -223,10 +231,10 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Discard
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Generating..." : "Generate token"}
|
||||
{isSubmitting ? "Generating" : "Generate token"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full p-5">
|
||||
<div className="w-full space-y-3 text-wrap">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3>
|
||||
<p className="text-sm text-custom-text-400">
|
||||
|
||||
90
web/components/core/modals/alert-modal.tsx
Normal file
90
web/components/core/modals/alert-modal.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { AlertTriangle, LucideIcon } from "lucide-react";
|
||||
// ui
|
||||
import { Button, TButtonVariant } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export type TModalVariant = "danger";
|
||||
|
||||
type Props = {
|
||||
content: React.ReactNode | string;
|
||||
handleClose: () => void;
|
||||
handleSubmit: () => Promise<void>;
|
||||
hideIcon?: boolean;
|
||||
isDeleting: boolean;
|
||||
isOpen: boolean;
|
||||
position?: EModalPosition;
|
||||
primaryButtonText?: {
|
||||
loading: string;
|
||||
default: string;
|
||||
};
|
||||
secondaryButtonText?: string;
|
||||
title: string;
|
||||
variant?: TModalVariant;
|
||||
width?: EModalWidth;
|
||||
};
|
||||
|
||||
const VARIANT_ICONS: Record<TModalVariant, LucideIcon> = {
|
||||
danger: AlertTriangle,
|
||||
};
|
||||
|
||||
const BUTTON_VARIANTS: Record<TModalVariant, TButtonVariant> = {
|
||||
danger: "danger",
|
||||
};
|
||||
|
||||
const VARIANT_CLASSES: Record<TModalVariant, string> = {
|
||||
danger: "bg-red-500/20 text-red-500",
|
||||
};
|
||||
|
||||
export const AlertModalCore: React.FC<Props> = (props) => {
|
||||
const {
|
||||
content,
|
||||
handleClose,
|
||||
handleSubmit,
|
||||
hideIcon = false,
|
||||
isDeleting,
|
||||
isOpen,
|
||||
position = EModalPosition.CENTER,
|
||||
primaryButtonText = {
|
||||
loading: "Deleting",
|
||||
default: "Delete",
|
||||
},
|
||||
secondaryButtonText = "Cancel",
|
||||
title,
|
||||
variant = "danger",
|
||||
width = EModalWidth.XL,
|
||||
} = props;
|
||||
|
||||
const Icon = VARIANT_ICONS[variant];
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={position} width={width}>
|
||||
<div className="p-5 flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
||||
{!hideIcon && (
|
||||
<span
|
||||
className={cn(
|
||||
"flex-shrink-0 grid place-items-center rounded-full size-12 sm:size-10",
|
||||
VARIANT_CLASSES[variant]
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
<div className="text-center sm:text-left">
|
||||
<h3 className="text-lg font-medium">{title}</h3>
|
||||
<p className="mt-1 text-sm text-custom-text-200">{content}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{secondaryButtonText}
|
||||
</Button>
|
||||
<Button variant={BUTTON_VARIANTS[variant]} size="sm" tabIndex={1} onClick={handleSubmit} loading={isDeleting}>
|
||||
{isDeleting ? primaryButtonText.loading : primaryButtonText.default}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
export * from "./alert-modal";
|
||||
export * from "./bulk-delete-issues-modal";
|
||||
export * from "./existing-issues-list-modal";
|
||||
export * from "./gpt-assistant-popover";
|
||||
export * from "./link-modal";
|
||||
export * from "./modal-core";
|
||||
export * from "./user-image-upload-modal";
|
||||
export * from "./workspace-image-upload-modal";
|
||||
export * from "./issue-search-modal-empty-state";
|
||||
|
||||
68
web/components/core/modals/modal-core.tsx
Normal file
68
web/components/core/modals/modal-core.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export enum EModalPosition {
|
||||
TOP = "flex items-center justify-center text-center mx-4 my-10 md:my-20",
|
||||
CENTER = "flex items-end sm:items-center justify-center p-4 min-h-full",
|
||||
}
|
||||
|
||||
export enum EModalWidth {
|
||||
XL = "sm:max-w-xl",
|
||||
XXL = "sm:max-w-2xl",
|
||||
XXXL = "sm:max-w-3xl",
|
||||
XXXXL = "sm:max-w-4xl",
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
position?: EModalPosition;
|
||||
width?: EModalWidth;
|
||||
};
|
||||
export const ModalCore: React.FC<Props> = (props) => {
|
||||
const { children, handleClose, isOpen, position = EModalPosition.CENTER, width = EModalWidth.XXL } = 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={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
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { CycleFiltersSelection } from "@/components/cycles";
|
||||
import { FiltersDropdown } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useCycleFilter } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
@@ -61,6 +62,8 @@ export const ArchivedCyclesHeader: FC = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(currentProjectArchivedFilters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="group relative flex border-b border-custom-border-200">
|
||||
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
||||
@@ -110,7 +113,12 @@ export const ArchivedCyclesHeader: FC = observer(() => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<CycleFiltersSelection
|
||||
filters={currentProjectArchivedFilters ?? {}}
|
||||
handleFiltersUpdate={handleFilters}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import router from "next/router";
|
||||
//components
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
// constants
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
||||
|
||||
export const CycleMobileHeader = () => {
|
||||
@@ -103,6 +107,8 @@ export const CycleMobileHeader = () => {
|
||||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -142,6 +148,7 @@ export const CycleMobileHeader = () => {
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { ListFilter, Search, X } from "lucide-react";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// types
|
||||
import { TCycleFilters } from "@plane/types";
|
||||
@@ -13,6 +15,7 @@ import { FiltersDropdown } from "@/components/issues";
|
||||
import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "@/constants/cycle";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useCycleFilter } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
@@ -75,6 +78,8 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(currentProjectFilters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="h-[50px] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-custom-border-200 px-6 sm:pb-0">
|
||||
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||
@@ -135,7 +140,12 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<CycleFiltersSelection filters={currentProjectFilters ?? {}} handleFiltersUpdate={handleFilters} />
|
||||
</FiltersDropdown>
|
||||
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { ICycle } from "@plane/types";
|
||||
// hooks
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { CYCLE_DELETED } from "@/constants/event-tracker";
|
||||
import { useEventTracker, useCycle } from "@/hooks/store";
|
||||
// components
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// constants
|
||||
import { CYCLE_DELETED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle } from "@/hooks/store";
|
||||
|
||||
interface ICycleDelete {
|
||||
cycle: ICycle;
|
||||
@@ -24,12 +24,12 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
|
||||
// states
|
||||
const [loader, setLoader] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { cycleId, peekCycle } = router.query;
|
||||
// store hooks
|
||||
const { captureCycleEvent } = useEventTracker();
|
||||
const { deleteCycle } = useCycle();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { cycleId, peekCycle } = router.query;
|
||||
|
||||
const formSubmit = async () => {
|
||||
if (!cycle) return;
|
||||
@@ -70,66 +70,19 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<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.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-4">
|
||||
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20">
|
||||
<AlertTriangle width={16} strokeWidth={2} className="text-red-600" />
|
||||
</div>
|
||||
<div className="text-xl font-medium 2xl:text-2xl">Delete cycle</div>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete cycle{' "'}
|
||||
<span className="break-words font-medium text-custom-text-100">{cycle?.name}</span>
|
||||
{'"'}? All of the data related to the cycle will be permanently removed. This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
</span>
|
||||
<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={formSubmit} loading={loader}>
|
||||
{loader ? "Deleting" : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</div>
|
||||
</div>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={formSubmit}
|
||||
isDeleting={loader}
|
||||
isOpen={isOpen}
|
||||
title="Delete Cycle"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete cycle{' "'}
|
||||
<span className="break-words font-medium text-custom-text-100">{cycle?.name}</span>
|
||||
{'"'}? All of the data related to the cycle will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
|
||||
// components
|
||||
import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
|
||||
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldRenderProject } from "@/helpers/project.helper";
|
||||
|
||||
@@ -53,121 +53,119 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData, dirtyFields))}>
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="flex items-center gap-x-3">
|
||||
{!status && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="project_id"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ProjectDropdown
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
setActiveProject(val);
|
||||
}}
|
||||
buttonVariant="background-with-text"
|
||||
renderCondition={(project) => shouldRenderProject(project)}
|
||||
tabIndex={7}
|
||||
/>
|
||||
<div className="h-7">
|
||||
<ProjectDropdown
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
setActiveProject(val);
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
renderCondition={(project) => shouldRenderProject(project)}
|
||||
tabIndex={7}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-xl font-medium leading-6 text-custom-text-200">{status ? "Update" : "New"} Cycle</h3>
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{status ? "Update" : "Create"} Cycle</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="mt-2 space-y-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="cycle_name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Cycle Title"
|
||||
className="w-full resize-none placeholder:text-sm placeholder:font-medium focus:border-blue-400"
|
||||
value={value}
|
||||
inputSize="md"
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors?.name)}
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
id="cycle_description"
|
||||
name="description"
|
||||
placeholder="Description..."
|
||||
className="w-full text-sm resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
tabIndex={2}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="end_date"
|
||||
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
|
||||
<DateRangeDropdown
|
||||
buttonVariant="border-with-text"
|
||||
className="h-7"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(startDateValue),
|
||||
to: getDate(endDateValue),
|
||||
}}
|
||||
onSelect={(val) => {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
to: "End date",
|
||||
}}
|
||||
hideIcon={{
|
||||
to: true,
|
||||
}}
|
||||
tabIndex={3}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
className="w-full text-base"
|
||||
value={value}
|
||||
inputSize="md"
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors?.name)}
|
||||
tabIndex={1}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
tabIndex={2}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name="end_date"
|
||||
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
|
||||
<DateRangeDropdown
|
||||
buttonVariant="border-with-text"
|
||||
className="h-7"
|
||||
minDate={new Date()}
|
||||
value={{
|
||||
from: getDate(startDateValue),
|
||||
to: getDate(endDateValue),
|
||||
}}
|
||||
onSelect={(val) => {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
to: "End date",
|
||||
}}
|
||||
hideIcon={{
|
||||
to: true,
|
||||
}}
|
||||
tabIndex={3}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
|
||||
{data ? (isSubmitting ? "Updating" : "Update cycle") : isSubmitting ? "Creating" : "Create cycle"}
|
||||
{data ? (isSubmitting ? "Updating" : "Update Cycle") : isSubmitting ? "Creating" : "Create Cycle"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types";
|
||||
// services
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { CycleForm } from "@/components/cycles";
|
||||
// constants
|
||||
import { CYCLE_CREATED, CYCLE_UPDATED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle, useProject } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
// hooks
|
||||
// components
|
||||
// ui
|
||||
// types
|
||||
// constants
|
||||
|
||||
type CycleModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -166,45 +166,15 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
}, [activeProject, data, projectId, workspaceProjectIds, isOpen]);
|
||||
|
||||
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-10 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={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">
|
||||
<CycleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
status={data ? true : false}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
data={data}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<CycleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
status={data ? true : false}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
data={data}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const TransferIssuesModal: React.FC<Props> = observer((props) => {
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Issues transferred successfully",
|
||||
title: "Success!",
|
||||
message: "Issues have been transferred successfully",
|
||||
});
|
||||
})
|
||||
|
||||
@@ -2,15 +2,16 @@ import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
||||
// store hooks
|
||||
import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { checkDuplicates } from "@/helpers/array.helper";
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
// ui
|
||||
// helpers
|
||||
// types
|
||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// helpers
|
||||
import { checkDuplicates } from "@/helpers/array.helper";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -196,133 +197,96 @@ export const CreateUpdateEstimateModal: React.FC<Props> = observer((props) => {
|
||||
}, [data, reset]);
|
||||
|
||||
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-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-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 onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium leading-6">{data ? "Update" : "Create"} Estimate</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="name"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-xl"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
value={value}
|
||||
placeholder="Description"
|
||||
onChange={onChange}
|
||||
className="mt-3 min-h-32 resize-none text-sm"
|
||||
hasError={Boolean(errors?.description)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* list of all the points */}
|
||||
{/* since they are all the same, we can use a loop to render them */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{Array(6)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div className="flex items-center" key={i}>
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">{i + 1}</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`value${i + 1}` as keyof FormValues}
|
||||
rules={{
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: "Estimate point must at most be of 20 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
id={`value${i + 1}`}
|
||||
name={`value${i + 1}`}
|
||||
placeholder={`Point ${i + 1}`}
|
||||
className="w-full rounded-l-none"
|
||||
hasError={Boolean(errors[`value${i + 1}` as keyof FormValues])}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</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" type="submit" loading={isSubmitting}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating Estimate..."
|
||||
: "Update Estimate"
|
||||
: isSubmitting
|
||||
? "Creating Estimate..."
|
||||
: "Create Estimate"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Create"} Estimate</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="name"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full text-base"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
value={value}
|
||||
placeholder="Description"
|
||||
onChange={onChange}
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
{/* list of all the points */}
|
||||
{/* since they are all the same, we can use a loop to render them */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{Array(6)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div className="flex items-center" key={i}>
|
||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">{i + 1}</span>
|
||||
<span className="rounded-r-lg bg-custom-background-100">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`value${i + 1}` as keyof FormValues}
|
||||
rules={{
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: "Estimate point must at most be of 20 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
id={`value${i + 1}`}
|
||||
name={`value${i + 1}`}
|
||||
placeholder={`Point ${i + 1}`}
|
||||
className="w-full rounded-l-none"
|
||||
hasError={Boolean(errors[`value${i + 1}` as keyof FormValues])}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{data ? (isSubmitting ? "Updating" : "Update Estimate") : isSubmitting ? "Creating" : "Create Estimate"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { IEstimate } from "@plane/types";
|
||||
// store hooks
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
// types
|
||||
import { IEstimate } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useEstimate } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -26,15 +26,16 @@ export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
|
||||
// store hooks
|
||||
const { deleteEstimate } = useEstimate();
|
||||
|
||||
const handleEstimateDelete = () => {
|
||||
const handleEstimateDelete = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
const estimateId = data?.id!;
|
||||
|
||||
deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId)
|
||||
await deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId)
|
||||
.then(() => {
|
||||
setIsDeleteLoading(false);
|
||||
handleClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -46,7 +47,8 @@ export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
|
||||
title: "Error!",
|
||||
message: errorString ?? "Estimate could not be deleted. Please try again",
|
||||
});
|
||||
});
|
||||
})
|
||||
.finally(() => setIsDeleteLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -59,72 +61,19 @@ export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" 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>
|
||||
|
||||
<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={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">
|
||||
<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-600" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Estimate</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="break-words text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to delete estimate-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>
|
||||
{""}? All of the data related to the estiamte will be permanently removed. This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={() => {
|
||||
setIsDeleteLoading(true);
|
||||
handleEstimateDelete();
|
||||
}}
|
||||
loading={isDeleteLoading}
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Estimate"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={onClose}
|
||||
handleSubmit={handleEstimateDelete}
|
||||
isDeleting={isDeleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete Estimate"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete estimate-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>
|
||||
{""}? All of the data related to the estiamte will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,19 +2,25 @@ import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
// components
|
||||
// icons
|
||||
import { ArrowRight, Plus, PanelRight } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
useApplication,
|
||||
useEventTracker,
|
||||
@@ -27,12 +33,7 @@ import {
|
||||
useIssues,
|
||||
} from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// ui
|
||||
// icons
|
||||
// helpers
|
||||
// types
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// constants
|
||||
|
||||
const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
|
||||
// router
|
||||
@@ -152,6 +153,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
: cycleDetails.total_issues
|
||||
: undefined;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -239,7 +242,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember, useUser, useIssues } from "@/hooks/store";
|
||||
|
||||
@@ -94,6 +96,8 @@ export const GlobalIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||
@@ -110,7 +114,7 @@ export const GlobalIssuesHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
@@ -2,18 +2,25 @@ import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
// icons
|
||||
import { ArrowRight, PanelRight, Plus } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// constants
|
||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
useApplication,
|
||||
useEventTracker,
|
||||
@@ -27,13 +34,7 @@ import {
|
||||
} from "@/hooks/store";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// components
|
||||
// ui
|
||||
// icons
|
||||
// helpers
|
||||
// types
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// constants
|
||||
|
||||
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
|
||||
// router
|
||||
@@ -152,6 +153,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
: moduleDetails.total_issues
|
||||
: undefined;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -240,7 +243,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// ui
|
||||
// helper
|
||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
@@ -83,6 +85,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
: currentProjectDetails.draft_issues
|
||||
: undefined;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -131,7 +135,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// hooks
|
||||
// ui
|
||||
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import {
|
||||
useApplication,
|
||||
useEventTracker,
|
||||
@@ -21,12 +28,7 @@ import {
|
||||
useMember,
|
||||
} from "@/hooks/store";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
// components
|
||||
// ui
|
||||
// types
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// constants
|
||||
// helper
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
// states
|
||||
@@ -109,6 +111,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
: currentProjectDetails?.total_issues
|
||||
: undefined;
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -180,7 +184,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
@@ -2,21 +2,23 @@ import { useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { Plus } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
// types
|
||||
// constants
|
||||
import { ProjectLogo } from "@/components/project";
|
||||
// constants
|
||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
useApplication,
|
||||
useEventTracker,
|
||||
@@ -128,6 +130,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -200,7 +204,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" disabled={!canUserCreateIssue}>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
disabled={!canUserCreateIssue}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// icons
|
||||
import { Search, Plus, Briefcase, X, ListFilter } from "lucide-react";
|
||||
// types
|
||||
import { TProjectFilters } from "@plane/types";
|
||||
// hooks
|
||||
// components
|
||||
// ui
|
||||
import { Breadcrumbs, Button } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// helpers
|
||||
// constants
|
||||
import { FiltersDropdown } from "@/components/issues";
|
||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
|
||||
@@ -78,6 +81,8 @@ export const ProjectsHeader = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -145,7 +150,12 @@ export const ProjectsHeader = observer(() => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<ProjectFiltersSelection
|
||||
displayFilters={displayFilters ?? {}}
|
||||
filters={filters ?? {}}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { FC, FormEvent, useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// editor
|
||||
import { EditorRefApi } from "@plane/rich-text-editor";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
@@ -92,7 +94,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${TOAST_TYPE.SUCCESS}!`,
|
||||
title: `Success!`,
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
})
|
||||
@@ -109,7 +111,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: `${TOAST_TYPE.ERROR}!`,
|
||||
title: `Error!`,
|
||||
message: "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
@@ -120,31 +122,37 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
|
||||
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
|
||||
return (
|
||||
<form className="relative space-y-4" onSubmit={handleFormSubmit}>
|
||||
<InboxIssueTitle
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||
/>
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
<div className="relative flex justify-between items-center gap-3">
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Create Inbox Issue</h3>
|
||||
<div className="space-y-3">
|
||||
<InboxIssueTitle
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||
/>
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1.5"
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
role="button"
|
||||
>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">Create more</span>
|
||||
</div>
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
|
||||
Discard
|
||||
</Button>
|
||||
@@ -155,7 +163,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
loading={formSubmitting}
|
||||
disabled={isTitleLengthMoreThan255Character}
|
||||
>
|
||||
{formSubmitting ? "Adding Issue..." : "Add Issue"}
|
||||
{formSubmitting ? "Creating" : "Create Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// editor
|
||||
import { EditorRefApi } from "@plane/rich-text-editor";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
@@ -15,7 +18,7 @@ import { ISSUE_UPDATED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useInboxIssues, useWorkspace } from "@/hooks/store";
|
||||
import { useEventTracker, useInboxIssues, useProject, useWorkspace } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueEditRoot = {
|
||||
workspaceSlug: string;
|
||||
@@ -31,8 +34,9 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
const router = useRouter();
|
||||
// refs
|
||||
const descriptionEditorRef = useRef<EditorRefApi>(null);
|
||||
// hooks
|
||||
// store hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { updateProjectIssue } = useInboxIssues(issueId);
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
|
||||
@@ -95,7 +99,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${TOAST_TYPE.SUCCESS}!`,
|
||||
title: `Success!`,
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
descriptionEditorRef?.current?.clearEditor();
|
||||
@@ -114,7 +118,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: `${TOAST_TYPE.ERROR}!`,
|
||||
title: `Error!`,
|
||||
message: "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
@@ -125,23 +129,30 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
|
||||
if (!workspaceSlug || !projectId || !workspaceId || !formData) return <></>;
|
||||
return (
|
||||
<div className="relative space-y-4">
|
||||
<InboxIssueTitle
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||
/>
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
||||
<div className="relative flex justify-end items-center gap-3">
|
||||
<>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">
|
||||
Move {currentProjectDetails?.identifier}-{issue?.sequence_id} to project issues
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InboxIssueTitle
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||
/>
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" type="button" onClick={handleModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -153,9 +164,9 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
disabled={isTitleLengthMoreThan255Character}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
{formSubmitting ? "Adding..." : "Add to project"}
|
||||
{formSubmitting ? "Adding" : "Add to project"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -32,19 +32,18 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
||||
<Loader.Item width="100%" height="140px" />
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<RichTextEditor
|
||||
initialValue={!data?.description_html || data?.description_html === "" ? "<p></p>" : data?.description_html}
|
||||
ref={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
</div>
|
||||
<RichTextEditor
|
||||
initialValue={!data?.description_html || data?.description_html === "" ? "<p></p>" : data?.description_html}
|
||||
ref={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
|
||||
const { data, handleData, isTitleLengthMoreThan255Character } = props;
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-wrap gap-2 items-center">
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
@@ -21,7 +21,7 @@ export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
|
||||
value={data?.name}
|
||||
onChange={(e) => handleData("name", e.target.value)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-xl"
|
||||
className="w-full text-base"
|
||||
required
|
||||
/>
|
||||
{isTitleLengthMoreThan255Character && (
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
import { FC } from "react";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { InboxIssueCreateRoot, InboxIssueEditRoot } from "@/components/inbox/modals/create-edit-modal";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueCreateEditModalRoot = {
|
||||
workspaceSlug: string;
|
||||
@@ -16,69 +14,28 @@ type TInboxIssueCreateEditModalRoot = {
|
||||
onSubmit?: () => void;
|
||||
};
|
||||
|
||||
export const InboxIssueCreateEditModalRoot: FC<TInboxIssueCreateEditModalRoot> = observer((props) => {
|
||||
export const InboxIssueCreateEditModalRoot: FC<TInboxIssueCreateEditModalRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, modalState, handleModalClose, issue, onSubmit } = props;
|
||||
// hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition.Root show={modalState} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleModalClose}>
|
||||
<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="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 transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all w-full lg:max-w-4xl">
|
||||
{issue && issue?.id ? (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-medium text-custom-text-100">
|
||||
Move {currentProjectDetails?.identifier}-{issue?.sequence_id} to project issues
|
||||
</h3>
|
||||
<InboxIssueEditRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issue.id}
|
||||
issue={issue}
|
||||
handleModalClose={handleModalClose}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-medium text-custom-text-100">Create Inbox Issue</h3>
|
||||
<InboxIssueCreateRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
handleModalClose={handleModalClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</div>
|
||||
<ModalCore
|
||||
isOpen={modalState}
|
||||
handleClose={handleModalClose}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXXXL}
|
||||
>
|
||||
{issue && issue?.id ? (
|
||||
<InboxIssueEditRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issue.id}
|
||||
issue={issue}
|
||||
handleModalClose={handleModalClose}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : (
|
||||
<InboxIssueCreateRoot workspaceSlug={workspaceSlug} projectId={projectId} handleModalClose={handleModalClose} />
|
||||
)}
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import type { TIssue } from "@plane/types";
|
||||
// icons
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { TIssue } from "@plane/types";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
@@ -15,81 +13,46 @@ type Props = {
|
||||
onSubmit: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeclineIssueModal: React.FC<Props> = ({ isOpen, onClose, data, onSubmit }) => {
|
||||
export const DeclineIssueModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose, data, onSubmit } = props;
|
||||
// states
|
||||
const [isDeclining, setIsDeclining] = useState(false);
|
||||
// hooks
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const projectDetails = data.project_id ? getProjectById(data?.project_id) : undefined;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeclining(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
const handleDecline = async () => {
|
||||
setIsDeclining(true);
|
||||
onSubmit().finally(() => setIsDeclining(false));
|
||||
await onSubmit().finally(() => setIsDeclining(false));
|
||||
};
|
||||
|
||||
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-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={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">
|
||||
<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-600" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Decline Issue</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to decline issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{(data && data?.project_id && getProjectById(data?.project_id)?.identifier) || ""}-
|
||||
{data?.sequence_id}
|
||||
</span>
|
||||
{""}? This action cannot be undone.
|
||||
</p>
|
||||
</span>
|
||||
<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={handleDecline} loading={isDeclining}>
|
||||
{isDeclining ? "Declining..." : "Decline Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDecline}
|
||||
isDeleting={isDeclining}
|
||||
isOpen={isOpen}
|
||||
title="Decline Issue"
|
||||
content={
|
||||
<>
|
||||
{" "}
|
||||
Are you sure you want to decline issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{projectDetails?.identifier}-{data?.sequence_id}
|
||||
</span>
|
||||
{""}? This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
primaryButtonText={{
|
||||
loading: "Declining",
|
||||
default: "Decline",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
// icons
|
||||
import type { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
import { useProject } from "@/hooks/store";
|
||||
// types
|
||||
import type { TIssue } from "@plane/types";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
data: Partial<TIssue>;
|
||||
@@ -20,79 +17,37 @@ type Props = {
|
||||
export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, onSubmit, data }) => {
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const projectDetails = data.project_id ? getProjectById(data?.project_id) : undefined;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleting(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
onSubmit().finally(() => handleClose());
|
||||
await onSubmit().finally(() => 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>
|
||||
|
||||
<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={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">
|
||||
<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-600" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{(data && data?.project_id && getProjectById(data?.project_id)?.identifier) || ""}-
|
||||
{data?.sequence_id}
|
||||
</span>
|
||||
{""}? The issue will only be deleted from the inbox and this action cannot be undone.
|
||||
</p>
|
||||
</span>
|
||||
<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={handleDelete} loading={isDeleting}>
|
||||
{isDeleting ? "Deleting..." : "Delete Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDelete}
|
||||
isDeleting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
title="Delete Issue"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{projectDetails?.identifier}-{data?.sequence_id}
|
||||
</span>
|
||||
{""}? The issue will only be deleted from the inbox and this action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,12 +4,13 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Tooltip, PriorityIcon } from "@plane/ui";
|
||||
// components
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { InboxIssueStatus } from "@/components/inbox";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useLabel, useProjectInbox } from "@/hooks/store";
|
||||
import { useLabel, useMember, useProjectInbox } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// store
|
||||
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||
@@ -31,6 +32,7 @@ export const InboxIssueListItem: FC<InboxIssueListItemProps> = observer((props)
|
||||
const { currentTab } = useProjectInbox();
|
||||
const { projectLabels } = useLabel();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { getUserDetails } = useMember();
|
||||
const issue = inboxIssue.issue;
|
||||
|
||||
const handleIssueRedirection = (event: MouseEvent, currentIssueId: string | undefined) => {
|
||||
@@ -39,6 +41,9 @@ export const InboxIssueListItem: FC<InboxIssueListItemProps> = observer((props)
|
||||
};
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
const createdByDetails = issue?.created_by ? getUserDetails(issue?.created_by) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
@@ -63,50 +68,54 @@ export const InboxIssueListItem: FC<InboxIssueListItemProps> = observer((props)
|
||||
<h3 className="truncate w-full text-sm">{issue.name}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tooltip
|
||||
tooltipHeading="Created on"
|
||||
tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<div className="text-xs text-custom-text-200">{renderFormattedDate(issue.created_at ?? "")}</div>
|
||||
</Tooltip>
|
||||
|
||||
<div className="border-2 rounded-full border-custom-border-400" />
|
||||
|
||||
{issue.priority && (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
||||
<PriorityIcon priority={issue.priority} withContainer className="w-3 h-3" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tooltip
|
||||
tooltipHeading="Created on"
|
||||
tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<div className="text-xs text-custom-text-200">{renderFormattedDate(issue.created_at ?? "")}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{issue.label_ids && issue.label_ids.length > 3 ? (
|
||||
<div className="relative !h-[17.5px] flex items-center gap-1 rounded border border-custom-border-300 px-1 text-xs">
|
||||
<span className="h-2 w-2 rounded-full bg-orange-400" />
|
||||
<span className="normal-case max-w-28 truncate">{`${issue.label_ids.length} labels`}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(issue.label_ids ?? []).map((labelId) => {
|
||||
const labelDetails = projectLabels?.find((l) => l.id === labelId);
|
||||
if (!labelDetails) return null;
|
||||
return (
|
||||
<div
|
||||
key={labelId}
|
||||
className="relative !h-[17.5px] flex items-center gap-1 rounded border border-custom-border-300 px-1 text-xs"
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: labelDetails.color,
|
||||
}}
|
||||
/>
|
||||
<span className="normal-case max-w-28 truncate">{labelDetails.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<div className="border-2 rounded-full border-custom-border-400" />
|
||||
|
||||
{issue.priority && (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
||||
<PriorityIcon priority={issue.priority} withContainer className="w-3 h-3" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{issue.label_ids && issue.label_ids.length > 3 ? (
|
||||
<div className="relative !h-[17.5px] flex items-center gap-1 rounded border border-custom-border-300 px-1 text-xs">
|
||||
<span className="h-2 w-2 rounded-full bg-orange-400" />
|
||||
<span className="normal-case max-w-28 truncate">{`${issue.label_ids.length} labels`}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(issue.label_ids ?? []).map((labelId) => {
|
||||
const labelDetails = projectLabels?.find((l) => l.id === labelId);
|
||||
if (!labelDetails) return null;
|
||||
return (
|
||||
<div
|
||||
key={labelId}
|
||||
className="relative !h-[17.5px] flex items-center gap-1 rounded border border-custom-border-300 px-1 text-xs"
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: labelDetails.color,
|
||||
}}
|
||||
/>
|
||||
<span className="normal-case max-w-28 truncate">{labelDetails.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* created by */}
|
||||
{createdByDetails && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
|
||||
.updateInstanceConfigurations(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "AI Settings updated successfully",
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
.updateInstanceConfigurations(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Email Settings updated successfully",
|
||||
})
|
||||
|
||||
@@ -40,7 +40,7 @@ export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||
.updateInstanceInfo(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Settings updated successfully",
|
||||
})
|
||||
|
||||
@@ -42,7 +42,7 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
|
||||
.updateInstanceConfigurations(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Github Configuration Settings updated successfully",
|
||||
})
|
||||
|
||||
@@ -40,7 +40,7 @@ export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) =
|
||||
.updateInstanceConfigurations(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Google Configuration Settings updated successfully",
|
||||
})
|
||||
|
||||
@@ -40,7 +40,7 @@ export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (props) =>
|
||||
.updateInstanceConfigurations(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Image Configuration Settings updated successfully",
|
||||
})
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ArchiveTabsList } from "@/components/archives";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
@@ -62,6 +64,8 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property);
|
||||
};
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="group relative flex border-b border-custom-border-200">
|
||||
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
||||
@@ -69,7 +73,7 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
</div>
|
||||
{/* filter options */}
|
||||
<div className="flex items-center gap-2 px-8">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters || {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { FC, Fragment, useState } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { FC, useState } from "react";
|
||||
import type { TIssueAttachment } from "@plane/types";
|
||||
// headless ui
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// helper
|
||||
import { getFileName } from "@/helpers/attachment.helper";
|
||||
// types
|
||||
@@ -35,74 +32,19 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
data && (
|
||||
<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-[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">
|
||||
Delete attachment
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete attachment-{" "}
|
||||
<span className="font-bold">{getFileName(data.attributes.name)}</span>? This attachment will
|
||||
be permanently removed. 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(data.id);
|
||||
}}
|
||||
disabled={loader}
|
||||
>
|
||||
{loader ? "Deleting" : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={() => handleDeletion(data.id)}
|
||||
isDeleting={loader}
|
||||
isOpen={isOpen}
|
||||
title="Delete attachment"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete attachment-{" "}
|
||||
<span className="font-bold">{getFileName(data.attributes.name)}</span>? This attachment will be permanently
|
||||
removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" onClick={handleDeletion} loading={isLoading}>
|
||||
{isLoading ? "Saving..." : "Save Draft"}
|
||||
{isLoading ? "Saving" : "Save draft"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState, Fragment } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// types
|
||||
import { useIssues, useProject } from "@/hooks/store";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useIssues, useProject } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -18,12 +18,10 @@ type Props = {
|
||||
|
||||
export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
const { dataId, data, isOpen, handleClose, onSubmit } = props;
|
||||
|
||||
const { issueMap } = useIssues();
|
||||
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// hooks
|
||||
// store hooks
|
||||
const { issueMap } = useIssues();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,7 +30,9 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
|
||||
if (!dataId && !data) return null;
|
||||
|
||||
// derived values
|
||||
const issue = data ? data : issueMap[dataId!];
|
||||
const projectDetails = getProjectById(issue?.project_id);
|
||||
|
||||
const onClose = () => {
|
||||
setIsDeleting(false);
|
||||
@@ -57,65 +57,21 @@ export const DeleteIssueModal: 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-2xl">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{getProjectById(issue?.project_id)?.identifier}-{issue?.sequence_id}
|
||||
</span>
|
||||
{""}? All of the data related to the issue will be permanently removed. This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleIssueDelete} loading={isDeleting}>
|
||||
{isDeleting ? "Deleting" : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={onClose}
|
||||
handleSubmit={handleIssueDelete}
|
||||
isDeleting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
title="Delete Issue"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</span>
|
||||
{""}? All of the data related to the issue will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
|
||||
const handleIssueCycleChange = async (cycleId: string | null) => {
|
||||
if (!issue || issue.cycle_id === cycleId) return;
|
||||
setIsUpdating(true);
|
||||
if (cycleId) await issueOperations.addIssueToCycle?.(workspaceSlug, projectId, cycleId, [issueId]);
|
||||
if (cycleId) await issueOperations.addCycleToIssue?.(workspaceSlug, projectId, cycleId, issueId);
|
||||
else await issueOperations.removeIssueFromCycle?.(workspaceSlug, projectId, issue.cycle_id ?? "", issueId);
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
@@ -53,13 +53,13 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await createComment(workspaceSlug, projectId, issueId, data);
|
||||
setToast({
|
||||
title: "Comment created successfully.",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Comment created successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Comment creation failed.",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Comment creation failed. Please try again later.",
|
||||
});
|
||||
@@ -70,13 +70,13 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await updateComment(workspaceSlug, projectId, issueId, commentId, data);
|
||||
setToast({
|
||||
title: "Comment updated successfully.",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Comment updated successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Comment update failed.",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Comment update failed. Please try again later.",
|
||||
});
|
||||
@@ -87,13 +87,13 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await removeComment(workspaceSlug, projectId, issueId, commentId);
|
||||
setToast({
|
||||
title: "Comment removed successfully.",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Comment removed successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Comment remove failed.",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Comment remove failed. Please try again later.",
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ export const LabelCreate: FC<ILabelCreate> = (props) => {
|
||||
reset(defaultValues);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Label creation failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Label creation failed. Please try again sometime later.",
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||
else await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Issue update failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Issue update failed",
|
||||
});
|
||||
@@ -54,14 +54,14 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||
const labelResponse = await createLabel(workspaceSlug, projectId, data);
|
||||
if (!isInboxIssue)
|
||||
setToast({
|
||||
title: "Label created successfully",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Label created successfully",
|
||||
});
|
||||
return labelResponse;
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Label creation failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Label creation failed",
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
|
||||
if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields");
|
||||
await createCommentReaction(workspaceSlug, projectId, commentId, reaction);
|
||||
setToast({
|
||||
title: "Reaction created successfully",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Reaction created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Reaction creation failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Reaction creation failed",
|
||||
});
|
||||
@@ -58,13 +58,13 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
|
||||
if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields");
|
||||
removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id);
|
||||
setToast({
|
||||
title: "Reaction removed successfully",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Reaction removed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Reaction remove failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Reaction remove failed",
|
||||
});
|
||||
|
||||
@@ -40,13 +40,13 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||
await createReaction(workspaceSlug, projectId, issueId, reaction);
|
||||
setToast({
|
||||
title: "Reaction created successfully",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Reaction created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Reaction creation failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Reaction creation failed",
|
||||
});
|
||||
@@ -57,13 +57,13 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
||||
if (!workspaceSlug || !projectId || !issueId || !currentUser?.id) throw new Error("Missing fields");
|
||||
await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id);
|
||||
setToast({
|
||||
title: "Reaction removed successfully",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Reaction removed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Reaction remove failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Reaction remove failed",
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export type TIssueOperations = {
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addCycleToIssue?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
|
||||
@@ -62,6 +63,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
addCycleToIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addModulesToIssue,
|
||||
@@ -109,7 +111,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
path: router.asPath,
|
||||
});
|
||||
setToast({
|
||||
title: "Issue update failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Issue update failed",
|
||||
});
|
||||
@@ -120,7 +122,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId);
|
||||
else await removeIssue(workspaceSlug, projectId, issueId);
|
||||
setToast({
|
||||
title: "Issue deleted successfully",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
@@ -131,7 +133,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Issue delete failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Issue delete failed",
|
||||
});
|
||||
@@ -158,21 +160,38 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
addCycleToIssue: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||
try {
|
||||
await addCycleToIssue(workspaceSlug, projectId, cycleId, issueId);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "cycle_id",
|
||||
change_details: cycleId,
|
||||
},
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be added to the cycle. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { state: "FAILED", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "cycle_id",
|
||||
change_details: cycleId,
|
||||
},
|
||||
path: router.asPath,
|
||||
});
|
||||
}
|
||||
},
|
||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
setPromiseToast(addToCyclePromise, {
|
||||
loading: "Adding cycle to issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Cycle added to issue successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Cycle add to issue failed",
|
||||
},
|
||||
});
|
||||
await addToCyclePromise;
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" },
|
||||
@@ -183,6 +202,11 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be added to the cycle. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { state: "FAILED", element: "Issue detail page" },
|
||||
@@ -198,14 +222,14 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
try {
|
||||
const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||
setPromiseToast(removeFromCyclePromise, {
|
||||
loading: "Removing cycle from issue...",
|
||||
loading: "Removing issue from the cycle...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Cycle removed from issue successfully",
|
||||
message: () => "Issue removed from the cycle successfully.",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Cycle remove from issue failed",
|
||||
message: () => "Issue could not be removed from the cycle. Please try again.",
|
||||
},
|
||||
});
|
||||
await removeFromCyclePromise;
|
||||
@@ -232,19 +256,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
},
|
||||
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||
try {
|
||||
const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
setPromiseToast(addToModulePromise, {
|
||||
loading: "Adding module to issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Module added to issue successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Module add to issue failed",
|
||||
},
|
||||
});
|
||||
const response = await addToModulePromise;
|
||||
const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...response, state: "SUCCESS", element: "Issue detail page" },
|
||||
@@ -255,6 +267,11 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be added to the module. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
@@ -270,14 +287,14 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
try {
|
||||
const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||
setPromiseToast(removeFromModulePromise, {
|
||||
loading: "Removing module from issue...",
|
||||
loading: "Removing issue from the module...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Module removed from issue successfully",
|
||||
message: () => "Issue removed from the module successfully.",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Module remove from issue failed",
|
||||
message: () => "Issue could not be removed from the module. Please try again.",
|
||||
},
|
||||
});
|
||||
await removeFromModulePromise;
|
||||
@@ -335,6 +352,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
addModulesToIssue,
|
||||
removeIssueFromModule,
|
||||
removeModulesFromIssue,
|
||||
captureIssueEvent,
|
||||
router.asPath,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||
else await createSubscription(workspaceSlug, projectId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`,
|
||||
title: "Success!",
|
||||
message: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`,
|
||||
});
|
||||
setLoading(false);
|
||||
@@ -41,7 +41,7 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||
setLoading(false);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
updateIssue
|
||||
).catch((err) => {
|
||||
setToast({
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: err?.detail ?? "Failed to perform this action",
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { usePopper } from "react-popper";
|
||||
// icons
|
||||
import { ChevronUp } from "lucide-react";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -15,10 +16,20 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
menuButton?: React.ReactNode;
|
||||
isFiltersApplied?: boolean;
|
||||
};
|
||||
|
||||
export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
const { children, icon, title = "Dropdown", placement, disabled = false, tabIndex, menuButton } = props;
|
||||
const {
|
||||
children,
|
||||
icon,
|
||||
title = "Dropdown",
|
||||
placement,
|
||||
disabled = false,
|
||||
tabIndex,
|
||||
menuButton,
|
||||
isFiltersApplied = false,
|
||||
} = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -50,10 +61,16 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
|
||||
}
|
||||
tabIndex={tabIndex}
|
||||
className="relative"
|
||||
>
|
||||
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
<>
|
||||
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
{isFiltersApplied && (
|
||||
<span className="absolute h-2 w-2 -right-0.5 -top-0.5 bg-custom-primary-100 rounded-full" />
|
||||
)}
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
</Popover.Button>
|
||||
@@ -73,7 +90,9 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex max-h-[30rem] lg:max-h-[37.5rem] w-[18.75rem] flex-col overflow-hidden">{children}</div>
|
||||
<div className="flex max-h-[30rem] lg:max-h-[37.5rem] w-[18.75rem] flex-col overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
|
||||
@@ -163,7 +163,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
orderBy !== "sort_order"
|
||||
).catch((err) => {
|
||||
setToast({
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: err?.detail ?? "Failed to perform this action",
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useState, useRef, useEffect, Fragment } from "react";
|
||||
import React, { FC, useState, useRef, useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -296,8 +296,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
<form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="flex items-center gap-x-3">
|
||||
{/* Don't show project selection if editing an issue */}
|
||||
{!data?.id && (
|
||||
<Controller
|
||||
@@ -322,16 +322,14 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">
|
||||
{data?.id ? "Update" : "Create"} issue
|
||||
</h3>
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{data?.id ? "Update" : "Create"} Issue</h3>
|
||||
</div>
|
||||
{watch("parent_id") && selectedParentIssue && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent_id"
|
||||
render={({ field: { onChange } }) => (
|
||||
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
||||
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-90 p-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
@@ -361,7 +359,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<div className="mt-2 space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
@@ -384,376 +382,372 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
}}
|
||||
ref={issueTitleRef || ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Issue Title"
|
||||
className="w-full resize-none text-xl"
|
||||
placeholder="Title"
|
||||
className="w-full text-base"
|
||||
tabIndex={getTabIndex("name")}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||
|
||||
<div className="relative">
|
||||
{data?.description_html === undefined ? (
|
||||
<Loader className="min-h-[7rem] space-y-2 overflow-hidden rounded-md border border-custom-border-200 p-2 py-2">
|
||||
<Loader.Item width="100%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<Loader.Item width="80%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="50%" height="26px" />
|
||||
</div>
|
||||
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2">
|
||||
<Loader.Item width="100px" height="26px" />
|
||||
<Loader.Item width="50px" height="26px" />
|
||||
</div>
|
||||
</Loader>
|
||||
) : (
|
||||
<Fragment>
|
||||
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2">
|
||||
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1 rounded bg-custom-background-80 px-1.5 py-1 text-xs ${
|
||||
iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
tabIndex={getTabIndex("feeling_lucky")}
|
||||
>
|
||||
{iAmFeelingLucky ? (
|
||||
"Generating response"
|
||||
) : (
|
||||
<>
|
||||
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{envConfig?.has_openai_configured && (
|
||||
<GptAssistantPopover
|
||||
isOpen={gptAssistantModal}
|
||||
projectId={projectId}
|
||||
handleClose={() => {
|
||||
setGptAssistantModal((prevData) => !prevData);
|
||||
// this is done so that the title do not reset after gpt popover closed
|
||||
reset(getValues());
|
||||
}}
|
||||
onResponse={(response) => {
|
||||
handleAiAssistance(response);
|
||||
}}
|
||||
placement="top-end"
|
||||
button={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||
tabIndex={getTabIndex("ai_assistant")}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" />
|
||||
AI
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<RichTextEditor
|
||||
initialValue={value}
|
||||
value={data.description_html}
|
||||
workspaceSlug={workspaceSlug?.toString() as string}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
onChange={(_description: object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
handleFormChange();
|
||||
}}
|
||||
ref={editorRef}
|
||||
tabIndex={getTabIndex("description_html")}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="state_id"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<StateDropdown
|
||||
value={value}
|
||||
onChange={(stateId) => {
|
||||
onChange(stateId);
|
||||
handleFormChange();
|
||||
}}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("state_id")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="priority"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<PriorityDropdown
|
||||
value={value}
|
||||
onChange={(priority) => {
|
||||
onChange(priority);
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("priority")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="assignee_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<MemberDropdown
|
||||
projectId={projectId}
|
||||
value={value}
|
||||
onChange={(assigneeIds) => {
|
||||
onChange(assigneeIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
|
||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
tabIndex={getTabIndex("assignee_ids")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="label_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<IssueLabelSelect
|
||||
setIsOpen={setLabelModal}
|
||||
value={value}
|
||||
onChange={(labelIds) => {
|
||||
onChange(labelIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
projectId={projectId}
|
||||
tabIndex={getTabIndex("label_ids")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={value}
|
||||
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
|
||||
buttonVariant="border-with-text"
|
||||
maxDate={maxDate ?? undefined}
|
||||
placeholder="Start date"
|
||||
tabIndex={getTabIndex("start_date")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={value}
|
||||
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
|
||||
buttonVariant="border-with-text"
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Due date"
|
||||
tabIndex={getTabIndex("target_date")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{projectDetails?.cycle_view && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="cycle_id"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<CycleDropdown
|
||||
projectId={projectId}
|
||||
onChange={(cycleId) => {
|
||||
onChange(cycleId);
|
||||
handleFormChange();
|
||||
}}
|
||||
placeholder="Cycle"
|
||||
value={value}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("cycle_id")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{projectDetails?.module_view && workspaceSlug && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="module_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<ModuleDropdown
|
||||
projectId={projectId}
|
||||
value={value ?? []}
|
||||
onChange={(moduleIds) => {
|
||||
onChange(moduleIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
placeholder="Modules"
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("module_ids")}
|
||||
multiple
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{areEstimatesEnabledForProject(projectId) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<EstimateDropdown
|
||||
value={value}
|
||||
onChange={(estimatePoint) => {
|
||||
onChange(estimatePoint);
|
||||
handleFormChange();
|
||||
}}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("estimate_point")}
|
||||
placeholder="Estimate"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watch("parent_id") ? (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
</div>
|
||||
<div className="relative">
|
||||
{data?.description_html === undefined ? (
|
||||
<Loader className="min-h-[7rem] space-y-2 overflow-hidden rounded-md border border-custom-border-200 p-2 py-2">
|
||||
<Loader.Item width="100%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<Loader.Item width="80%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="50%" height="26px" />
|
||||
</div>
|
||||
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2">
|
||||
<Loader.Item width="100px" height="26px" />
|
||||
<Loader.Item width="50px" height="26px" />
|
||||
</div>
|
||||
</Loader>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex items-center gap-2">
|
||||
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
className={`flex items-center gap-1 rounded bg-custom-background-90 px-1.5 py-1 text-xs ${
|
||||
iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
tabIndex={getTabIndex("feeling_lucky")}
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{selectedParentIssue &&
|
||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
placement="bottom-start"
|
||||
tabIndex={getTabIndex("parent_id")}
|
||||
>
|
||||
<>
|
||||
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
|
||||
Change parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent_id"
|
||||
render={({ field: { onChange } }) => (
|
||||
<CustomMenu.MenuItem
|
||||
className="!p-1"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
handleFormChange();
|
||||
}}
|
||||
>
|
||||
Remove parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
{iAmFeelingLucky ? (
|
||||
"Generating response"
|
||||
) : (
|
||||
<>
|
||||
<Sparkle className="h-3.5 w-3.5" />I{"'"}m feeling lucky
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{envConfig?.has_openai_configured && (
|
||||
<GptAssistantPopover
|
||||
isOpen={gptAssistantModal}
|
||||
projectId={projectId}
|
||||
handleClose={() => {
|
||||
setGptAssistantModal((prevData) => !prevData);
|
||||
// this is done so that the title do not reset after gpt popover closed
|
||||
reset(getValues());
|
||||
}}
|
||||
onResponse={(response) => {
|
||||
handleAiAssistance(response);
|
||||
}}
|
||||
placement="top-end"
|
||||
button={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||
tabIndex={getTabIndex("ai_assistant")}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" />
|
||||
AI
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">Add parent</span>
|
||||
</button>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent_id"
|
||||
render={({ field: { onChange } }) => (
|
||||
<ParentIssuesListModal
|
||||
isOpen={parentIssueListModalOpen}
|
||||
handleClose={() => setParentIssueListModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
)}
|
||||
</div>
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<RichTextEditor
|
||||
initialValue={value}
|
||||
value={data.description_html}
|
||||
workspaceSlug={workspaceSlug?.toString() as string}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
onChange={(_description: object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
handleFormChange();
|
||||
}}
|
||||
ref={editorRef}
|
||||
tabIndex={getTabIndex("description_html")}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="state_id"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<StateDropdown
|
||||
value={value}
|
||||
onChange={(stateId) => {
|
||||
onChange(stateId);
|
||||
handleFormChange();
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId}
|
||||
issueId={isDraft ? undefined : data?.id}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("state_id")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="priority"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<PriorityDropdown
|
||||
value={value}
|
||||
onChange={(priority) => {
|
||||
onChange(priority);
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("priority")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="assignee_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<MemberDropdown
|
||||
projectId={projectId}
|
||||
value={value}
|
||||
onChange={(assigneeIds) => {
|
||||
onChange(assigneeIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
|
||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
tabIndex={getTabIndex("assignee_ids")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="label_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<IssueLabelSelect
|
||||
setIsOpen={setLabelModal}
|
||||
value={value}
|
||||
onChange={(labelIds) => {
|
||||
onChange(labelIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
projectId={projectId}
|
||||
tabIndex={getTabIndex("label_ids")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={value}
|
||||
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
|
||||
buttonVariant="border-with-text"
|
||||
maxDate={maxDate ?? undefined}
|
||||
placeholder="Start date"
|
||||
tabIndex={getTabIndex("start_date")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={value}
|
||||
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
|
||||
buttonVariant="border-with-text"
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Due date"
|
||||
tabIndex={getTabIndex("target_date")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{projectDetails?.cycle_view && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="cycle_id"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<CycleDropdown
|
||||
projectId={projectId}
|
||||
onChange={(cycleId) => {
|
||||
onChange(cycleId);
|
||||
handleFormChange();
|
||||
}}
|
||||
placeholder="Cycle"
|
||||
value={value}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("cycle_id")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{projectDetails?.module_view && workspaceSlug && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="module_ids"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<ModuleDropdown
|
||||
projectId={projectId}
|
||||
value={value ?? []}
|
||||
onChange={(moduleIds) => {
|
||||
onChange(moduleIds);
|
||||
handleFormChange();
|
||||
}}
|
||||
placeholder="Modules"
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("module_ids")}
|
||||
multiple
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{areEstimatesEnabledForProject(projectId) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-7">
|
||||
<EstimateDropdown
|
||||
value={value}
|
||||
onChange={(estimatePoint) => {
|
||||
onChange(estimatePoint);
|
||||
handleFormChange();
|
||||
}}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getTabIndex("estimate_point")}
|
||||
placeholder="Estimate"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watch("parent_id") ? (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{selectedParentIssue &&
|
||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
placement="bottom-start"
|
||||
tabIndex={getTabIndex("parent_id")}
|
||||
>
|
||||
<>
|
||||
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
|
||||
Change parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent_id"
|
||||
render={({ field: { onChange } }) => (
|
||||
<CustomMenu.MenuItem
|
||||
className="!p-1"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
handleFormChange();
|
||||
}}
|
||||
>
|
||||
Remove parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">Add parent</span>
|
||||
</button>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent_id"
|
||||
render={({ field: { onChange } }) => (
|
||||
<ParentIssuesListModal
|
||||
isOpen={parentIssueListModalOpen}
|
||||
handleClose={() => setParentIssueListModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
handleFormChange();
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId}
|
||||
issueId={isDraft ? undefined : data?.id}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-100 px-5 pt-5">
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<div>
|
||||
{!data?.id && (
|
||||
<div
|
||||
className="inline-flex cursor-default items-center gap-1.5"
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
|
||||
}}
|
||||
tabIndex={getTabIndex("create_more")}
|
||||
role="button"
|
||||
>
|
||||
<div className="flex cursor-pointer items-center justify-center">
|
||||
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
|
||||
</div>
|
||||
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">Create more</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose} tabIndex={getTabIndex("discard_button")}>
|
||||
Discard
|
||||
</Button>
|
||||
|
||||
{isDraft && (
|
||||
<Fragment>
|
||||
<>
|
||||
{data?.id ? (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
@@ -775,9 +769,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
{isSubmitting ? "Saving" : "Save as draft"}
|
||||
</Button>
|
||||
)}
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
@@ -785,7 +778,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
loading={isSubmitting}
|
||||
tabIndex={isDraft ? getTabIndex("submit_button") : getTabIndex("draft_button")}
|
||||
>
|
||||
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"}
|
||||
{data?.id ? (isSubmitting ? "Updating" : "Update Issue") : isSubmitting ? "Creating" : "Create Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import type { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// constants
|
||||
import { ISSUE_CREATED, ISSUE_UPDATED } from "@/constants/event-tracker";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// hooks
|
||||
import {
|
||||
useApplication,
|
||||
useEventTracker,
|
||||
@@ -22,9 +25,6 @@ import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// components
|
||||
import { DraftIssueLayout } from "./draft-issue-layout";
|
||||
import { IssueFormRoot } from "./form";
|
||||
// ui
|
||||
// types
|
||||
// constants
|
||||
|
||||
export interface IssuesModalProps {
|
||||
data?: Partial<TIssue>;
|
||||
@@ -241,72 +241,47 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
if (!workspaceProjectIds || workspaceProjectIds.length === 0 || !activeProjectId) return null;
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => handleClose(true)}>
|
||||
<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 bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 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={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 mx-4 transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-4xl">
|
||||
{withDraftIssueWrapper ? (
|
||||
<DraftIssueLayout
|
||||
changesMade={changesMade}
|
||||
data={{
|
||||
...data,
|
||||
description_html: description,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||
}}
|
||||
issueTitleRef={issueTitleRef}
|
||||
onChange={handleFormChange}
|
||||
onClose={handleClose}
|
||||
onSubmit={handleFormSubmit}
|
||||
projectId={activeProjectId}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
) : (
|
||||
<IssueFormRoot
|
||||
issueTitleRef={issueTitleRef}
|
||||
data={{
|
||||
...data,
|
||||
description_html: description,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||
}}
|
||||
onClose={() => handleClose(false)}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
onSubmit={handleFormSubmit}
|
||||
projectId={activeProjectId}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ModalCore
|
||||
isOpen={isOpen}
|
||||
handleClose={() => handleClose(true)}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXXXL}
|
||||
>
|
||||
{withDraftIssueWrapper ? (
|
||||
<DraftIssueLayout
|
||||
changesMade={changesMade}
|
||||
data={{
|
||||
...data,
|
||||
description_html: description,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||
}}
|
||||
issueTitleRef={issueTitleRef}
|
||||
onChange={handleFormChange}
|
||||
onClose={handleClose}
|
||||
onSubmit={handleFormSubmit}
|
||||
projectId={activeProjectId}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
) : (
|
||||
<IssueFormRoot
|
||||
issueTitleRef={issueTitleRef}
|
||||
data={{
|
||||
...data,
|
||||
description_html: description,
|
||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||
}}
|
||||
onClose={() => handleClose(false)}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
onSubmit={handleFormSubmit}
|
||||
projectId={activeProjectId}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
)}
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import router from "next/router";
|
||||
// components
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// icons
|
||||
// constants
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
// layouts
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts";
|
||||
|
||||
export const IssuesMobileHeader = observer(() => {
|
||||
const layouts = [
|
||||
@@ -83,6 +86,8 @@ export const IssuesMobileHeader = observer(() => {
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -122,6 +127,7 @@ export const IssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { FC, useEffect, useState, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
import { IssueView } from "@/components/issues";
|
||||
// ui
|
||||
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { IssueView } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "@/constants/event-tracker";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail, useIssues, useUser } from "@/hooks/store";
|
||||
// components
|
||||
// types
|
||||
// constants
|
||||
|
||||
interface IIssuePeekOverview {
|
||||
is_archived?: boolean;
|
||||
@@ -60,8 +59,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
archiveIssue,
|
||||
issue: { getIssueById, fetchIssue },
|
||||
} = useIssueDetail();
|
||||
const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } =
|
||||
useIssueDetail();
|
||||
const {
|
||||
addCycleToIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addModulesToIssue,
|
||||
removeIssueFromModule,
|
||||
removeModulesFromIssue,
|
||||
} = useIssueDetail();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
// state
|
||||
const [loader, setLoader] = useState(false);
|
||||
@@ -100,7 +105,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
path: router.asPath,
|
||||
});
|
||||
setToast({
|
||||
title: "Issue update failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Issue update failed",
|
||||
});
|
||||
@@ -110,7 +115,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
try {
|
||||
removeIssue(workspaceSlug, projectId, issueId);
|
||||
setToast({
|
||||
title: "Issue deleted successfully",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
@@ -121,7 +126,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Issue delete failed",
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Issue delete failed",
|
||||
});
|
||||
@@ -174,21 +179,39 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
addCycleToIssue: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||
try {
|
||||
console.log("Peek adding...");
|
||||
await addCycleToIssue(workspaceSlug, projectId, cycleId, issueId);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||
updates: {
|
||||
changed_property: "cycle_id",
|
||||
change_details: cycleId,
|
||||
},
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be added to the cycle. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||
updates: {
|
||||
changed_property: "cycle_id",
|
||||
change_details: cycleId,
|
||||
},
|
||||
path: router.asPath,
|
||||
});
|
||||
}
|
||||
},
|
||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
setPromiseToast(addToCyclePromise, {
|
||||
loading: "Adding cycle to issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Cycle added to issue successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Cycle add to issue failed",
|
||||
},
|
||||
});
|
||||
await addToCyclePromise;
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" },
|
||||
@@ -199,6 +222,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be added to the cycle. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { state: "FAILED", element: "Issue peek-overview" },
|
||||
@@ -214,14 +242,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
try {
|
||||
const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||
setPromiseToast(removeFromCyclePromise, {
|
||||
loading: "Removing cycle from issue...",
|
||||
loading: "Removing issue from the cycle...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Cycle removed from issue successfully",
|
||||
message: () => "Issue removed from the cycle successfully.",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Cycle remove from issue failed",
|
||||
message: () => "Issue could not be removed from the cycle. Please try again.",
|
||||
},
|
||||
});
|
||||
await removeFromCyclePromise;
|
||||
@@ -248,19 +276,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
},
|
||||
addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => {
|
||||
try {
|
||||
const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
setPromiseToast(addToModulePromise, {
|
||||
loading: "Adding module to issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Module added to issue successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Module add to issue failed",
|
||||
},
|
||||
});
|
||||
const response = await addToModulePromise;
|
||||
const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" },
|
||||
@@ -271,6 +287,11 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be added to the module. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||
@@ -286,14 +307,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
try {
|
||||
const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||
setPromiseToast(removeFromModulePromise, {
|
||||
loading: "Removing module from issue...",
|
||||
loading: "Removing issue from the module...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Module removed from issue successfully",
|
||||
message: () => "Issue removed from the module successfully.",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Module remove from issue failed",
|
||||
message: () => "Issue could not be removed from the module. Please try again.",
|
||||
},
|
||||
});
|
||||
await removeFromModulePromise;
|
||||
|
||||
@@ -143,7 +143,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error fetching sub-issues",
|
||||
title: "Error!",
|
||||
message: "Error fetching sub-issues",
|
||||
});
|
||||
}
|
||||
@@ -153,13 +153,13 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Sub-issues added successfully",
|
||||
title: "Success!",
|
||||
message: "Sub-issues added successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error adding sub-issue",
|
||||
title: "Error!",
|
||||
message: "Error adding sub-issue",
|
||||
});
|
||||
}
|
||||
@@ -187,7 +187,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Sub-issue updated successfully",
|
||||
title: "Success!",
|
||||
message: "Sub-issue updated successfully",
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
@@ -203,7 +203,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error updating sub-issue",
|
||||
title: "Error!",
|
||||
message: "Error updating sub-issue",
|
||||
});
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Sub-issue removed successfully",
|
||||
title: "Success!",
|
||||
message: "Sub-issue removed successfully",
|
||||
});
|
||||
captureIssueEvent({
|
||||
@@ -239,7 +239,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error removing sub-issue",
|
||||
title: "Error!",
|
||||
message: "Error removing sub-issue",
|
||||
});
|
||||
}
|
||||
@@ -250,7 +250,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Issue deleted successfully",
|
||||
title: "Error!",
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
captureIssueEvent({
|
||||
@@ -267,7 +267,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error deleting issue",
|
||||
title: "Error!",
|
||||
message: "Error deleting issue",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import type { IIssueLabel } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { useLabel } from "@/hooks/store";
|
||||
// icons
|
||||
// ui
|
||||
// types
|
||||
import type { IIssueLabel } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useLabel } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -54,64 +53,18 @@ export const DeleteLabelModal: 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">
|
||||
Delete Label
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you wish to delete{" "}
|
||||
<span className="font-medium text-custom-text-100">{data?.name}</span>? This will remove the
|
||||
label from all the issue and from any views where the label is being filtered upon.
|
||||
</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}>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDeletion}
|
||||
isDeleting={isDeleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete Label"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete <span className="font-medium text-custom-text-100">{data?.name}</span>? This
|
||||
will remove the label from all the issue and from any views where the label is being filtered upon.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FiltersDropdown } from "@/components/issues";
|
||||
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useMember, useModuleFilter } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
@@ -70,6 +71,8 @@ export const ArchivedModulesHeader: FC = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(currentProjectArchivedFilters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="group relative flex border-b border-custom-border-200">
|
||||
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
|
||||
@@ -128,7 +131,12 @@ export const ArchivedModulesHeader: FC = observer(() => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<ModuleFiltersSelection
|
||||
displayFilters={currentProjectDisplayFilters ?? {}}
|
||||
filters={currentProjectArchivedFilters ?? {}}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import type { IModule } from "@plane/types";
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { MODULE_DELETED } from "@/constants/event-tracker";
|
||||
import { useEventTracker, useModule } from "@/hooks/store";
|
||||
// ui
|
||||
// icons
|
||||
// types
|
||||
import type { IModule } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// constants
|
||||
import { MODULE_DELETED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, useModule } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
data: IModule;
|
||||
@@ -71,64 +70,19 @@ export const DeleteModuleModal: 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-500/20 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">
|
||||
Delete Module
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete module-{" "}
|
||||
<span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the
|
||||
data related to the module will be permanently removed. 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}>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDeletion}
|
||||
isDeleting={isDeleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete Module"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete module-{" "}
|
||||
<span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the data related to
|
||||
the module will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateModule)}>
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="flex items-center gap-x-3">
|
||||
{!status && (
|
||||
<Controller
|
||||
@@ -86,11 +86,10 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-xl font-medium leading-6 text-custom-text-200">{status ? "Update" : "New"} Module</h3>
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{status ? "Update" : "Create"} Module</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
@@ -101,18 +100,18 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
||||
message: "Title should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors?.name)}
|
||||
placeholder="Module Title"
|
||||
className="w-full resize-none placeholder:text-sm placeholder:font-medium focus:border-blue-400"
|
||||
placeholder="Title"
|
||||
className="w-full text-base"
|
||||
tabIndex={1}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -128,8 +127,8 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
||||
name="description"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Description..."
|
||||
className="w-full text-sm resize-none min-h-24"
|
||||
placeholder="Description"
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
tabIndex={2}
|
||||
/>
|
||||
@@ -211,12 +210,12 @@ export const ModuleForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200 pt-5">
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={7}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={8}>
|
||||
{status ? (isSubmitting ? "Updating" : "Update module") : isSubmitting ? "Creating" : "Create module"}
|
||||
{status ? (isSubmitting ? "Updating" : "Update Module") : isSubmitting ? "Creating" : "Create Module"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import type { IModule } from "@plane/types";
|
||||
// components
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { ModuleForm } from "@/components/modules";
|
||||
// constants
|
||||
import { MODULE_CREATED, MODULE_UPDATED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, useModule, useProject } from "@/hooks/store";
|
||||
// ui
|
||||
// components
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -140,45 +140,15 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
}, [activeProject, data, projectId, workspaceProjectIds, isOpen]);
|
||||
|
||||
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-10 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={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">
|
||||
<ModuleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
status={data ? true : false}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
data={data}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<ModuleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
status={data ? true : false}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
data={data}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,17 +3,19 @@ import { observer } from "mobx-react";
|
||||
import router from "next/router";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// hooks
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
// types
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
export const ModuleMobileHeader = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -86,6 +88,8 @@ export const ModuleMobileHeader = observer(() => {
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="block md:hidden">
|
||||
<ProjectAnalyticsModal
|
||||
@@ -125,6 +129,7 @@ export const ModuleMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { FC, useCallback, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { ListFilter, Search, X } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@plane/editor-core";
|
||||
// types
|
||||
import { TModuleFilters } from "@plane/types";
|
||||
@@ -12,6 +14,8 @@ import { FiltersDropdown } from "@/components/issues";
|
||||
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules/dropdowns";
|
||||
// constants
|
||||
import { MODULE_VIEW_LAYOUTS } from "@/constants/module";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useMember, useModuleFilter } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
@@ -79,6 +83,9 @@ export const ModuleViewHeader: FC = observer(() => {
|
||||
useOutsideClickDetector(inputRef, () => {
|
||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
});
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="hidden h-full sm:flex items-center gap-3 self-end">
|
||||
<div className="flex items-center">
|
||||
@@ -135,7 +142,12 @@ export const ModuleViewHeader: FC = observer(() => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<ModuleFiltersSelection
|
||||
displayFilters={displayFilters ?? {}}
|
||||
filters={filters ?? {}}
|
||||
|
||||
@@ -125,7 +125,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Module link created",
|
||||
title: "Success!",
|
||||
message: "Module link created successfully.",
|
||||
});
|
||||
})
|
||||
@@ -151,7 +151,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Module link updated",
|
||||
title: "Success!",
|
||||
message: "Module link updated successfully.",
|
||||
});
|
||||
})
|
||||
@@ -175,7 +175,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Module link deleted",
|
||||
title: "Success!",
|
||||
message: "Module link deleted successfully.",
|
||||
});
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Menu } from "@headlessui/react";
|
||||
// type
|
||||
import type { IUserNotification, NotificationType } from "@plane/types";
|
||||
// ui
|
||||
import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import {
|
||||
ISSUE_OPENED,
|
||||
@@ -23,7 +23,6 @@ import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
|
||||
type NotificationCardProps = {
|
||||
selectedTab: NotificationType;
|
||||
notification: IUserNotification;
|
||||
@@ -184,12 +183,12 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
{notificationField === "comment"
|
||||
? "commented"
|
||||
: notificationField === "archived_at"
|
||||
? notification.data.issue_activity.new_value === "restore"
|
||||
? "restored the issue"
|
||||
: "archived the issue"
|
||||
: notificationField === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notificationField)}{" "}
|
||||
? notification.data.issue_activity.new_value === "restore"
|
||||
? "restored the issue"
|
||||
: "archived the issue"
|
||||
: notificationField === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notificationField)}{" "}
|
||||
{!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""}
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
@@ -381,46 +380,46 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
<Tooltip tooltipContent="Snooze" isMobile={isMobile}>
|
||||
<CustomMenu
|
||||
className="flex items-center"
|
||||
customButton={
|
||||
<CustomMenu
|
||||
className="flex items-center"
|
||||
customButton={
|
||||
<Tooltip tooltipContent="Snooze" isMobile={isMobile}>
|
||||
<div className="flex w-full items-center gap-x-2 rounded bg-custom-background-80 p-0.5 text-sm hover:bg-custom-background-100">
|
||||
<Clock className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
</div>
|
||||
}
|
||||
optionsClassName="!z-20"
|
||||
>
|
||||
{snoozeOptions.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.label}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
</Tooltip>
|
||||
}
|
||||
optionsClassName="!z-20"
|
||||
>
|
||||
{snoozeOptions.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.label}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!item.value) {
|
||||
setSelectedNotificationForSnooze(notification.id);
|
||||
return;
|
||||
}
|
||||
if (!item.value) {
|
||||
setSelectedNotificationForSnooze(notification.id);
|
||||
return;
|
||||
}
|
||||
|
||||
markSnoozeNotification(notification.id, item.value).then(() => {
|
||||
captureEvent(NOTIFICATION_SNOOZED, {
|
||||
issue_id: notification.data.issue.id,
|
||||
tab: selectedTab,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
setToast({
|
||||
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
markSnoozeNotification(notification.id, item.value).then(() => {
|
||||
captureEvent(NOTIFICATION_SNOOZED, {
|
||||
issue_id: notification.data.issue.id,
|
||||
tab: selectedTab,
|
||||
state: "SUCCESS",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</Tooltip>
|
||||
setToast({
|
||||
title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
|
||||
handleClose();
|
||||
onSuccess();
|
||||
setToast({
|
||||
title: "Notification snoozed",
|
||||
title: "Success!",
|
||||
message: "Notification snoozed successfully",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
copyTextToClipboard(editorRef.getMarkDown()).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Successful!",
|
||||
title: "Success!",
|
||||
message: "Markdown copied to clipboard.",
|
||||
})
|
||||
);
|
||||
@@ -106,7 +106,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${id}`).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Successful!",
|
||||
title: "Success!",
|
||||
message: "Page link copied to clipboard.",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -45,6 +45,8 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
[filters.filters, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-shrink-0 h-[50px] w-full border-b border-custom-border-200 px-6 relative flex items-center gap-4 justify-between">
|
||||
@@ -59,7 +61,12 @@ export const PagesListHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
if (val.order) updateFilters("sortBy", val.order);
|
||||
}}
|
||||
/>
|
||||
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<PageFiltersSelection
|
||||
filters={filters}
|
||||
handleFiltersUpdate={updateFilters}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FC, Fragment, useState } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { TPage } from "@plane/types";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { PageForm } from "@/components/pages";
|
||||
// constants
|
||||
import { PAGE_CREATED } from "@/constants/event-tracker";
|
||||
@@ -67,43 +67,18 @@ export const CreatePageModal: FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isModalOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleModalClose}>
|
||||
<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="my-10 flex 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 transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-2xl">
|
||||
<PageForm
|
||||
formData={pageFormData}
|
||||
handleFormData={handlePageFormData}
|
||||
handleModalClose={handleStateClear}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ModalCore
|
||||
isOpen={isModalOpen}
|
||||
handleClose={handleModalClose}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXL}
|
||||
>
|
||||
<PageForm
|
||||
formData={pageFormData}
|
||||
handleFormData={handlePageFormData}
|
||||
handleModalClose={handleStateClear}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// constants
|
||||
import { PAGE_DELETED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
@@ -71,64 +71,19 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
|
||||
};
|
||||
|
||||
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-500/20 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">
|
||||
Delete Page
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete page-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{name}</span>? The Page will be
|
||||
deleted permanently. 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={handleDelete} loading={isDeleting}>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDelete}
|
||||
isDeleting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
title="Delete Page"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete page-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{name}</span>? The Page will be deleted
|
||||
permanently. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,28 +39,27 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<form onSubmit={handlePageFormSubmit}>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Create Page</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Create Page</h3>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleFormData("name", e.target.value)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-lg"
|
||||
className="w-full resize-none text-base"
|
||||
tabIndex={1}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{isTitleLengthMoreThan255Character && (
|
||||
<span className="text-xs text-red-500">Max length of the name should be less than 255 characters</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 relative flex items-center justify-between gap-2">
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
|
||||
{PAGE_ACCESS_SPECIFIERS.map((access, index) => (
|
||||
<Tooltip key={access.key} tooltipContent={access.label} isMobile={isMobile}>
|
||||
@@ -88,8 +87,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
{PAGE_ACCESS_SPECIFIERS.find((access) => access.key === formData.access)?.label}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center gap-2 justify-end">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleModalClose} tabIndex={4}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
.updateCurrentUserEmailNotificationSettings(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Email Notification Settings updated successfully",
|
||||
})
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "@/components/issues";
|
||||
// hooks
|
||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { useIssues, useLabel } from "@/hooks/store";
|
||||
// constants
|
||||
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel } from "@/hooks/store";
|
||||
|
||||
export const ProfileIssuesFilter = observer(() => {
|
||||
// router
|
||||
@@ -93,6 +96,8 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-end gap-2">
|
||||
<LayoutSelection
|
||||
@@ -101,7 +106,7 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
|
||||
|
||||
@@ -11,10 +11,11 @@ import { CustomMenu } from "@plane/ui";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel } from "@/hooks/store";
|
||||
|
||||
|
||||
const ProfileIssuesMobileHeader = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
@@ -99,6 +100,9 @@ const ProfileIssuesMobileHeader = observer(() => {
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
|
||||
<CustomMenu
|
||||
@@ -135,6 +139,7 @@ const ProfileIssuesMobileHeader = observer(() => {
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: err?.error || "Something went wrong. Please try again.",
|
||||
})
|
||||
);
|
||||
@@ -67,7 +67,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
||||
(err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: err?.error || "Something went wrong. Please try again.",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
||||
})
|
||||
.then(() => {
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Project updated successfully",
|
||||
});
|
||||
|
||||
@@ -5,10 +5,12 @@ import { ChevronDown, ListFilter } from "lucide-react";
|
||||
// types
|
||||
import { TProjectFilters } from "@plane/types";
|
||||
// hooks
|
||||
import { FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project/dropdowns";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useApplication, useMember, useProjectFilter } from "@/hooks/store";
|
||||
// components
|
||||
import { FiltersDropdown } from "../issues";
|
||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "./dropdowns";
|
||||
|
||||
const ProjectsMobileHeader = observer(() => {
|
||||
const {
|
||||
@@ -44,6 +46,8 @@ const ProjectsMobileHeader = observer(() => {
|
||||
[filters, updateFilters, workspaceSlug]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="flex py-2 border-b border-custom-border-200 md:hidden bg-custom-background-100 w-full">
|
||||
<ProjectOrderByDropdown
|
||||
@@ -68,6 +72,7 @@ const ProjectsMobileHeader = observer(() => {
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<ProjectFiltersSelection
|
||||
displayFilters={displayFilters ?? {}}
|
||||
|
||||
@@ -81,7 +81,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
||||
if (onSuccess) onSuccess();
|
||||
onClose();
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Members added successfully.",
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import type { IState } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// constants
|
||||
import { STATE_DELETED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, useProjectState } from "@/hooks/store";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -78,64 +78,18 @@ export const DeleteStateModal: 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 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">
|
||||
Delete State
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete state-{" "}
|
||||
<span className="font-medium text-custom-text-100">{data?.name}</span>? All of the data
|
||||
related to the state will be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 bg-custom-background-100 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}>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDeletion}
|
||||
isDeleting={isDeleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete State"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete state- <span className="font-medium text-custom-text-100">{data?.name}</span>?
|
||||
All of the data related to the state will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IProjectView } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useProjectView } from "@/hooks/store";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
data: IProjectView;
|
||||
@@ -59,64 +59,19 @@ export const DeleteProjectViewModal: 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-500/20 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">
|
||||
Delete View
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete view-{" "}
|
||||
<span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the
|
||||
data related to the view will be permanently removed. 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={handleDeleteView}>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDeleteView}
|
||||
isDeleting={isDeleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete View"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete view-{" "}
|
||||
<span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the data related to
|
||||
the view will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { IProjectView, IIssueFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
// components
|
||||
// ui
|
||||
// types
|
||||
import { IProjectView, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
// components
|
||||
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// hooks
|
||||
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
data?: IProjectView | null;
|
||||
@@ -109,10 +109,10 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{data ? "Update" : "Create"} View</h3>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Create"} View</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
@@ -132,8 +132,9 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-xl focus:border-blue-400"
|
||||
className="w-full text-base"
|
||||
tabIndex={1}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -148,7 +149,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
className="min-h-24 w-full resize-none text-sm"
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@@ -206,18 +207,12 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" tabIndex={5} disabled={isSubmitting}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating View..."
|
||||
: "Update View"
|
||||
: isSubmitting
|
||||
? "Creating View..."
|
||||
: "Create View"}
|
||||
<Button variant="primary" size="sm" type="submit" tabIndex={5} loading={isSubmitting}>
|
||||
{data ? (isSubmitting ? "Updating" : "Update View") : isSubmitting ? "Creating" : "Create View"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IProjectView } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
import { ProjectViewForm } from "@/components/views";
|
||||
// hooks
|
||||
import { useProjectView } from "@/hooks/store";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
data?: IProjectView | null;
|
||||
@@ -65,43 +65,13 @@ export const CreateUpdateProjectViewModal: FC<Props> = 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="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 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">
|
||||
<ProjectViewForm
|
||||
data={data}
|
||||
handleClose={handleClose}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
preLoadedData={preLoadedData}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<ProjectViewForm
|
||||
data={data}
|
||||
handleClose={handleClose}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
preLoadedData={preLoadedData}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
||||
|
||||
// derived values
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
// @ts-expect-error key types are not compatible
|
||||
|
||||
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
||||
|
||||
// handlers
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
||||
// helpers
|
||||
import { csvDownload } from "@/helpers/download.helper";
|
||||
// types
|
||||
// components
|
||||
import { WebhookForm } from "./form";
|
||||
import { GeneratedHookDetails } from "./generated-hook-details";
|
||||
// utils
|
||||
import { getCurrentHookAsCSV } from "./utils";
|
||||
// ui
|
||||
|
||||
interface ICreateWebhookModal {
|
||||
currentWorkspace: IWorkspace | null;
|
||||
@@ -93,48 +93,19 @@ export const CreateWebhookModal: React.FC<ICreateWebhookModal> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-20"
|
||||
onClose={() => {
|
||||
if (!generatedWebhook) 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 bg-opacity-50 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 border border-custom-border-200 bg-custom-background-100 p-6 text-left shadow-xl transition-all sm:my-8 w-full sm:max-w-2xl">
|
||||
{!generatedWebhook ? (
|
||||
<WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} />
|
||||
) : (
|
||||
<GeneratedHookDetails webhookDetails={generatedWebhook} handleClose={handleClose} />
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<ModalCore
|
||||
isOpen={isOpen}
|
||||
handleClose={() => {
|
||||
if (!generatedWebhook) handleClose();
|
||||
}}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXL}
|
||||
>
|
||||
{!generatedWebhook ? (
|
||||
<WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} />
|
||||
) : (
|
||||
<GeneratedHookDetails webhookDetails={generatedWebhook} handleClose={handleClose} />
|
||||
)}
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// hooks
|
||||
import { useWebhook } from "@/hooks/store";
|
||||
|
||||
@@ -52,59 +52,18 @@ export const DeleteWebhookModal: FC<IDeleteWebhook> = (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 bg-opacity-50 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 border border-custom-border-200 bg-custom-background-100 p-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<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 webhook</h3>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm text-custom-text-200">
|
||||
Are you sure you want to delete this webhook? Future events will not be delivered to this webhook.
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDelete} loading={isDeleting}>
|
||||
{isDeleting ? "Deleting..." : "Delete webhook"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDelete}
|
||||
isDeleting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
title="Delete webhook"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete this webhook? Future events will not be delivered to this webhook. This action
|
||||
cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,11 +58,11 @@ export const WebhookForm: FC<Props> = observer((props) => {
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-xl font-medium">{data ? "Webhook details" : "Create webhook"}</div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="text-xl font-medium text-custom-text-200">{data ? "Webhook details" : "Create webhook"}</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
@@ -76,34 +76,31 @@ export const WebhookForm: FC<Props> = observer((props) => {
|
||||
{errors.url && <div className="text-xs text-red-500">{errors.url.message}</div>}
|
||||
</div>
|
||||
{data && <WebhookToggle control={control} />}
|
||||
<div className="space-y-3">
|
||||
<WebhookOptions value={webhookEventType} onChange={(val) => setWebhookEventType(val)} />
|
||||
</div>
|
||||
<WebhookOptions value={webhookEventType} onChange={(val) => setWebhookEventType(val)} />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{webhookEventType === "individual" && <WebhookIndividualEventOptions control={control} />}
|
||||
</div>
|
||||
{data ? (
|
||||
<div className="mt-8 space-y-8">
|
||||
<WebhookSecretKey data={data} />
|
||||
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Updating..." : "Update"}
|
||||
</div>
|
||||
{data ? (
|
||||
<div className="p-5 pt-0 space-y-5">
|
||||
<WebhookSecretKey data={data} />
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Updating" : "Update"}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
{!webhookSecretKey && (
|
||||
<Button type="submit" variant="primary" size="sm" loading={isSubmitting}>
|
||||
{isSubmitting ? "Creating" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" onClick={handleClose}>
|
||||
Discard
|
||||
</Button>
|
||||
{!webhookSecretKey && (
|
||||
<Button type="submit" variant="primary" loading={isSubmitting}>
|
||||
{isSubmitting ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,20 +14,22 @@ export const GeneratedHookDetails: React.FC<Props> = (props) => {
|
||||
const { handleClose, webhookDetails } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-3 mb-3">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3>
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file
|
||||
containing the key has been downloaded.
|
||||
</p>
|
||||
<>
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Key created</h3>
|
||||
<p className="text-sm text-custom-text-400">
|
||||
Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file
|
||||
containing the key has been downloaded.
|
||||
</p>
|
||||
</div>
|
||||
<WebhookSecretKey data={webhookDetails} />
|
||||
</div>
|
||||
<WebhookSecretKey data={webhookDetails} />
|
||||
<div className="mt-6 flex justify-end">
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,14 +41,14 @@ export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
message: "Invitation removed successfully.",
|
||||
});
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: err?.error || "Something went wrong. Please try again.",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: err?.error || "Something went wrong. Please try again.",
|
||||
})
|
||||
);
|
||||
@@ -66,7 +66,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
await removeMemberFromWorkspace(workspaceSlug.toString(), memberDetails.member.id).catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
title: "Error!",
|
||||
message: err?.error || "Something went wrong. Please try again.",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -75,7 +75,7 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
title: "Success",
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Workspace updated successfully",
|
||||
});
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// ui
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
// types
|
||||
import { IWorkspaceView } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { AlertModalCore } from "@/components/core";
|
||||
// constants
|
||||
import { GLOBAL_VIEW_DELETED } from "@/constants/event-tracker";
|
||||
// store hooks
|
||||
// hooks
|
||||
import { useGlobalView, useEventTracker } from "@/hooks/store";
|
||||
// ui
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
data: IWorkspaceView;
|
||||
@@ -32,9 +29,7 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
|
||||
const { deleteGlobalView } = useGlobalView();
|
||||
const { captureEvent } = useEventTracker();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
const handleClose = () => onClose();
|
||||
|
||||
const handleDeletion = async () => {
|
||||
if (!workspaceSlug) return;
|
||||
@@ -69,64 +64,19 @@ export const DeleteGlobalViewModal: 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-500/20 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">
|
||||
Delete View
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete view-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? All of the
|
||||
data related to the view will be permanently removed. 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}>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDeletion}
|
||||
isDeleting={isDeleteLoading}
|
||||
isOpen={isOpen}
|
||||
title="Delete View"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete view-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? All of the data related to
|
||||
the view will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user