Merge pull request #210 from makeplane/preview

release: v0.19.7
This commit is contained in:
sriram veeraghanta
2024-05-07 19:38:39 +05:30
committed by GitHub
119 changed files with 2196 additions and 2695 deletions

View File

@@ -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 | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](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

View File

@@ -636,6 +636,7 @@ class IssueInboxSerializer(DynamicBaseSerializer):
"project_id",
"created_at",
"label_ids",
"created_by",
]
read_only_fields = fields

View File

@@ -447,7 +447,7 @@ class IssueViewSet(BaseViewSet):
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issue_queryset, datetime_fields, request.user.user_timezone
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)

View File

@@ -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
)

View File

@@ -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

View File

@@ -8,14 +8,14 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone):
if isinstance(queryset, dict):
queryset_values = [queryset]
else:
queryset_values = list(queryset.values())
queryset_values = list(queryset)
# Iterate over the dictionaries in the list
for item in queryset_values:
# Iterate over the datetime fields
for field in datetime_fields:
# Convert the datetime field to the user's timezone
if item[field]:
if field in item and item[field]:
item[field] = item[field].astimezone(user_tz)
# If queryset was a single item, return a single item

View File

@@ -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);

View File

@@ -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" />}

View File

@@ -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" />}

View File

@@ -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.
</>
}
/>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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">

View 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>
);
};

View File

@@ -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";

View 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>
);
};

View File

@@ -7,107 +7,111 @@ import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { calculateTimeAgo } from "@/helpers/date-time.helper";
// hooks
import { useMember } from "@/hooks/store";
import { useMember, useModule } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
links: ILinkDetails[];
moduleId: string;
handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: ILinkDetails) => void;
userAuth: UserAuth;
disabled?: boolean;
};
export const LinksList: React.FC<Props> = observer(
({ links, handleDeleteLink, handleEditLink, userAuth, disabled }) => {
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
export const LinksList: React.FC<Props> = observer((props) => {
const { moduleId, handleDeleteLink, handleEditLink, userAuth, disabled } = props;
// hooks
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const { getModuleById } = useModule();
// derived values
const currentModule = getModuleById(moduleId);
const moduleLinks = currentModule?.link_module || undefined;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Copied to clipboard",
message: "The URL has been successfully copied to your clipboard",
});
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Copied to clipboard",
message: "The URL has been successfully copied to your clipboard",
});
};
return (
<>
{links.map((link) => {
const createdByDetails = getUserDetails(link.created_by);
return (
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex w-full items-start justify-between gap-2">
<div className="flex items-start gap-2 truncate">
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
if (!moduleLinks) return <></>;
return (
<>
{moduleLinks.map((link) => {
const createdByDetails = getUserDetails(link.created_by);
return (
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex w-full items-start justify-between gap-2">
<div className="flex items-start gap-2 truncate">
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
</span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<span
className="cursor-pointer truncate text-xs"
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
>
{link.title && link.title !== "" ? link.title : link.url}
</span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<span
className="cursor-pointer truncate text-xs"
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
>
{link.title && link.title !== "" ? link.title : link.url}
</span>
</Tooltip>
</div>
</Tooltip>
</div>
{!isNotAllowed && (
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEditLink(link);
}}
>
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</button>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
>
<ExternalLink className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</a>
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteLink(link.id);
}}
>
<Trash2 className="h-3 w-3" />
</button>
</div>
)}
</div>
<div className="px-5">
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
Added {calculateTimeAgo(link.created_at)}
<br />
{createdByDetails && (
<>
by{" "}
{createdByDetails?.is_bot
? createdByDetails?.first_name + " Bot"
: createdByDetails?.display_name}
</>
)}
</p>
</div>
{!isNotAllowed && (
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEditLink(link);
}}
>
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</button>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
>
<ExternalLink className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</a>
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteLink(link.id);
}}
>
<Trash2 className="h-3 w-3" />
</button>
</div>
)}
</div>
);
})}
</>
);
}
);
<div className="px-5">
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
Added {calculateTimeAgo(link.created_at)}
<br />
{createdByDetails && (
<>
by{" "}
{createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}
</>
)}
</p>
</div>
</div>
);
})}
</>
);
});

View File

@@ -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}

View File

@@ -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 ?? {}}

View File

@@ -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">

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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",
});
})

View File

@@ -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>
);
});

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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}

View File

@@ -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 ?? {}}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 ?? {}}

View File

@@ -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>

View File

@@ -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>
</>
);
});

View File

@@ -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}
/>
);
});

View File

@@ -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 && (

View File

@@ -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>
);
});
};

View File

@@ -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",
}}
/>
);
};

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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>

View File

@@ -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",
})

View File

@@ -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",
})

View File

@@ -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",
})

View File

@@ -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",
})

View File

@@ -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",
})

View File

@@ -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",
})

View File

@@ -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}

View File

@@ -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.
</>
}
/>
);
};

View File

@@ -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>

View File

@@ -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.
</>
}
/>
);
};

View File

@@ -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);
};

View File

@@ -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.",
});

View File

@@ -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.",
});

View File

@@ -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",
});

View File

@@ -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",
});
}

View File

@@ -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",
});

View File

@@ -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",
});

View File

@@ -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,
]
);

View File

@@ -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.",
});
}

View File

@@ -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",
});

View File

@@ -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>

View File

@@ -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",
});

View File

@@ -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>

View File

@@ -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>
);
});

View File

@@ -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 ?? {}}

View File

@@ -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;

View File

@@ -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",
});
}

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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 ?? {}}

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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>

View File

@@ -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>
);
});

View File

@@ -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 ?? {}}

View File

@@ -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 ?? {}}

View File

@@ -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.",
});
})
@@ -673,18 +673,20 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
</div>
)}
<LinksList
links={moduleDetails.link_module}
handleEditLink={handleEditLink}
handleDeleteLink={handleDeleteLink}
userAuth={{
isGuest: currentProjectRole === EUserProjectRoles.GUEST,
isViewer: currentProjectRole === EUserProjectRoles.VIEWER,
isMember: currentProjectRole === EUserProjectRoles.MEMBER,
isOwner: currentProjectRole === EUserProjectRoles.ADMIN,
}}
disabled={isArchived}
/>
{moduleId && (
<LinksList
moduleId={moduleId}
handleEditLink={handleEditLink}
handleDeleteLink={handleDeleteLink}
userAuth={{
isGuest: currentProjectRole === EUserProjectRoles.GUEST,
isViewer: currentProjectRole === EUserProjectRoles.VIEWER,
isMember: currentProjectRole === EUserProjectRoles.MEMBER,
isOwner: currentProjectRole === EUserProjectRoles.ADMIN,
}}
disabled={isArchived}
/>
)}
</>
) : (
<div className="flex items-center justify-between gap-2">

View File

@@ -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>
);

View File

@@ -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,
});

View File

@@ -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.",
})
);

View File

@@ -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}

View File

@@ -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>
);
};

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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>

View File

@@ -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",
})

View File

@@ -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

View File

@@ -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={

View File

@@ -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.",
})
);

View File

@@ -72,7 +72,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
})
.then(() => {
setToast({
title: "Success",
title: "Success!",
type: TOAST_TYPE.SUCCESS,
message: "Project updated successfully",
});

View File

@@ -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 ?? {}}

View File

@@ -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.",
});

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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.
</>
}
/>
);
});

View File

@@ -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>

View File

@@ -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>
);
});

View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -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.
</>
}
/>
);
};

View File

@@ -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>
);
});

View File

@@ -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>
</>
);
};

View File

@@ -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.",
})
);

Some files were not shown because too many files have changed in this diff Show More