Compare commits

...
Author SHA1 Message Date
Aaryan Khandelwal 5888f68b7b feat: embed component 2024-11-25 18:43:42 +05:30
sriram veeraghanta 1cb16bf176 fix: email error handling on magic auth 2024-11-25 15:02:50 +05:30
Bavisetti NarayanandGitHub ca88675dbf chore: added dates in issue export (#6088)
* chore: added dates in issue export

* chore: added date converter
2024-11-22 19:59:08 +05:30
NikhilandGitHub 86f8743ade chore: remove exists checks (#6086) 2024-11-22 17:00:20 +05:30
NikhilandGitHub 1a6ec7034a chore: management command to add user to a project (#6084) 2024-11-22 16:05:58 +05:30
42d6078f60 [WEB-2776] fix: restrict notifications (#6081)
* chore: restrict notifications

* chore: handled the issue filter duplicates

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-11-22 16:02:11 +05:30
6ef62820fa [WEB-2778] chore: private project join restriction (#6082)
* chore: private project join restriction

* chore: update project not found container layout

* chore: restrict other users to join private project

* chore: add check condition using enum

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-11-22 16:00:19 +05:30
28 changed files with 391 additions and 38 deletions
@@ -176,6 +176,10 @@ class ProjectViewSet(BaseViewSet):
def retrieve(self, request, slug, pk):
project = (
self.get_queryset()
.filter(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
.filter(archived_at__isnull=True)
.filter(pk=pk)
.annotate(
@@ -136,6 +136,12 @@ class UserProjectInvitationsViewset(BaseViewSet):
member=request.user, workspace__slug=slug, is_active=True
)
if workspace_member.role != ROLE.ADMIN:
return Response(
{"error": "You do not have permission to join the project"},
status=status.HTTP_403_FORBIDDEN,
)
workspace_role = workspace_member.role
workspace = workspace_member.workspace
@@ -44,10 +44,8 @@ class MagicGenerateEndpoint(APIView):
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
origin = request.META.get("HTTP_ORIGIN", "/")
email = request.data.get("email", False)
email = request.data.get("email", "").strip().lower()
try:
# Clean up the email
email = email.strip().lower()
validate_email(email)
adapter = MagicCodeProvider(request=request, key=email)
key, token = adapter.initiate()
@@ -39,10 +39,8 @@ class MagicGenerateSpaceEndpoint(APIView):
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
origin = base_host(request=request, is_space=True)
email = request.data.get("email", False)
email = request.data.get("email", "").strip().lower()
try:
# Clean up the email
email = email.strip().lower()
validate_email(email)
adapter = MagicCodeProvider(request=request, key=email)
key, token = adapter.initiate()
+17 -7
View File
@@ -157,12 +157,13 @@ def generate_table_row(issue):
issue["name"],
issue["description_stripped"],
issue["state__name"],
dateTimeConverter(issue["start_date"]),
dateTimeConverter(issue["target_date"]),
dateConverter(issue["start_date"]),
dateConverter(issue["target_date"]),
issue["priority"],
(
f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
if issue["created_by__first_name"] and issue["created_by__last_name"]
if issue["created_by__first_name"]
and issue["created_by__last_name"]
else ""
),
(
@@ -191,10 +192,13 @@ def generate_json_row(issue):
"Name": issue["name"],
"Description": issue["description_stripped"],
"State": issue["state__name"],
"Start Date": dateConverter(issue["start_date"]),
"Target Date": dateConverter(issue["target_date"]),
"Priority": issue["priority"],
"Created By": (
f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
if issue["created_by__first_name"] and issue["created_by__last_name"]
if issue["created_by__first_name"]
and issue["created_by__last_name"]
else ""
),
"Assignee": (
@@ -204,11 +208,17 @@ def generate_json_row(issue):
),
"Labels": issue["labels__name"] if issue["labels__name"] else "",
"Cycle Name": issue["issue_cycle__cycle__name"],
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
"Cycle Start Date": dateConverter(
issue["issue_cycle__cycle__start_date"]
),
"Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]),
"Module Name": issue["issue_module__module__name"],
"Module Start Date": dateConverter(issue["issue_module__module__start_date"]),
"Module Target Date": dateConverter(issue["issue_module__module__target_date"]),
"Module Start Date": dateConverter(
issue["issue_module__module__start_date"]
),
"Module Target Date": dateConverter(
issue["issue_module__module__target_date"]
),
"Created At": dateTimeConverter(issue["created_at"]),
"Updated At": dateTimeConverter(issue["updated_at"]),
"Completed At": dateTimeConverter(issue["completed_at"]),
+8 -1
View File
@@ -16,6 +16,7 @@ from plane.db.models import (
IssueComment,
IssueActivity,
UserNotificationPreference,
ProjectMember
)
# Third Party imports
@@ -94,6 +95,8 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions):
).exists()
and not Issue.objects.filter(
project_id=project_id, pk=issue_id, created_by_id=mention_id
).exists() and ProjectMember.objects.filter(
project_id=project_id, member_id=mention_id, is_active=True
).exists()
):
project = Project.objects.get(pk=project_id)
@@ -243,6 +246,10 @@ def notifications(
new_mentions = get_new_mentions(
requested_instance=requested_data, current_instance=current_instance
)
new_mentions = list(ProjectMember.objects.filter(
project_id=project_id, member_id__in=new_mentions, is_active=True
).values_list("member_id", flat=True))
new_mentions = [str(member_id) for member_id in new_mentions]
removed_mention = get_removed_mentions(
requested_instance=requested_data, current_instance=current_instance
)
@@ -286,7 +293,7 @@ def notifications(
# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
issue_subscribers = list(
IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id)
IssueSubscriber.objects.filter(project_id=project_id, issue_id=issue_id, project__project_projectmember__is_active=True,)
.exclude(
subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])
)
@@ -0,0 +1,112 @@
# Django imports
from typing import Any
from django.core.management import BaseCommand, CommandError
# Module imports
from plane.db.models import (
User,
WorkspaceMember,
ProjectMember,
Project,
IssueUserProperty,
)
class Command(BaseCommand):
help = "Add a member to a project. If present in the workspace"
def add_arguments(self, parser):
# Positional argument
parser.add_argument(
"--project_id",
type=str,
nargs="?",
help="Project ID",
)
parser.add_argument(
"--user_email",
type=str,
nargs="?",
help="User Email",
)
parser.add_argument(
"--role",
type=int,
nargs="?",
help="Role of the user in the project",
)
def handle(self, *args: Any, **options: Any):
try:
if not options["project_id"]:
raise CommandError("Project ID is required")
if not options["user_email"]:
raise CommandError("User Email is required")
project_id = options["project_id"]
user_email = options["user_email"]
role = options.get("role", 20)
print(f"Role: {role}")
user = User.objects.filter(email=user_email).first()
if not user:
raise CommandError("User not found")
# Check if the project exists
project = Project.objects.filter(pk=project_id).first()
if not project:
raise CommandError("Project not found")
# Check if the user exists in the workspace
if not WorkspaceMember.objects.filter(
workspace=project.workspace, member=user, is_active=True
).exists():
raise CommandError("User not member in workspace")
# Get the smallest sort order
smallest_sort_order = (
ProjectMember.objects.filter(
workspace_id=project.workspace_id,
)
.order_by("sort_order")
.first()
)
if smallest_sort_order:
sort_order = smallest_sort_order.sort_order - 1000
else:
sort_order = 65535
if ProjectMember.objects.filter(
project=project,
member=user,
).exists():
# Update the project member
ProjectMember.objects.filter(
project=project,
member=user,
).update(is_active=True, sort_order=sort_order, role=role)
else:
# Create the project member
ProjectMember.objects.create(
project=project,
member=user,
role=role,
sort_order=sort_order,
)
# Issue Property
IssueUserProperty.objects.get_or_create(user=user, project=project)
# Success message
self.stdout.write(
self.style.SUCCESS(
f"User {user_email} added to project {project_id}"
)
)
return
except CommandError as e:
self.stdout.write(self.style.ERROR(e))
return
+1
View File
@@ -185,6 +185,7 @@ def filter_labels(params, issue_filter, method, prefix=""):
and params.get("labels") != "null"
):
issue_filter[f"{prefix}labels__in"] = params.get("labels")
issue_filter[f"{prefix}label_issue__deleted_at__isnull"] = True
return issue_filter
@@ -6,7 +6,7 @@ import { COLORS_LIST } from "@/constants/common";
import { CalloutBlockColorSelector } from "./color-selector";
import { CalloutBlockLogoSelector } from "./logo-selector";
// types
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
import { ECalloutAttributeNames, TCalloutBlockAttributes } from "./types";
// utils
import { updateStoredBackgroundColor } from "./utils";
@@ -45,7 +45,7 @@ export const CustomCalloutBlock: React.FC<Props> = (props) => {
toggleDropdown={() => setIsColorPickerOpen((prev) => !prev)}
onSelect={(val) => {
updateAttributes({
[EAttributeNames.BACKGROUND]: val,
[ECalloutAttributeNames.BACKGROUND]: val,
});
updateStoredBackgroundColor(val);
}}
@@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core";
import { Node as NodeType } from "@tiptap/pm/model";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
// types
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
import { ECalloutAttributeNames, TCalloutBlockAttributes } from "./types";
// utils
import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils";
@@ -23,7 +23,7 @@ export const CustomCalloutExtensionConfig = Node.create({
addAttributes() {
const attributes = {
// Reduce instead of map to accumulate the attributes directly into an object
...Object.values(EAttributeNames).reduce((acc, value) => {
...Object.values(ECalloutAttributeNames).reduce((acc, value) => {
acc[value] = {
default: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[value],
};
@@ -60,7 +60,7 @@ export const CustomCalloutExtensionConfig = Node.create({
parseHTML() {
return [
{
tag: `div[${EAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[EAttributeNames.BLOCK_TYPE]}"]`,
tag: `div[${ECalloutAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.BLOCK_TYPE]}"]`,
},
];
},
@@ -1,10 +1,12 @@
import { findParentNodeClosestToPos, Predicate, ReactNodeViewRenderer } from "@tiptap/react";
// extensions
import { CustomCalloutBlock } from "@/extensions";
// helpers
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
// block
import { CustomCalloutBlock } from "./block";
// config
import { CustomCalloutExtensionConfig } from "./extension-config";
// types
import { ECalloutAttributeNames } from "./types";
// utils
import { getStoredBackgroundColor, getStoredLogo } from "./utils";
@@ -30,7 +32,7 @@ export const CustomCalloutExtension = CustomCalloutExtensionConfig.extend({
],
attrs: {
...storedLogoValues,
"data-background": storedBackgroundValue,
[ECalloutAttributeNames.BACKGROUND]: storedBackgroundValue,
},
});
},
@@ -1,3 +1,2 @@
export * from "./block";
export * from "./extension";
export * from "./read-only-extension";
@@ -1,6 +1,6 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
// extensions
import { CustomCalloutBlock } from "@/extensions";
// block
import { CustomCalloutBlock } from "./block";
// config
import { CustomCalloutExtensionConfig } from "./extension-config";
@@ -1,4 +1,4 @@
export enum EAttributeNames {
export enum ECalloutAttributeNames {
ICON_COLOR = "data-icon-color",
ICON_NAME = "data-icon-name",
EMOJI_UNICODE = "data-emoji-unicode",
@@ -9,18 +9,18 @@ export enum EAttributeNames {
}
export type TCalloutBlockIconAttributes = {
[EAttributeNames.ICON_COLOR]: string | undefined;
[EAttributeNames.ICON_NAME]: string | undefined;
[ECalloutAttributeNames.ICON_COLOR]: string | undefined;
[ECalloutAttributeNames.ICON_NAME]: string | undefined;
};
export type TCalloutBlockEmojiAttributes = {
[EAttributeNames.EMOJI_UNICODE]: string | undefined;
[EAttributeNames.EMOJI_URL]: string | undefined;
[ECalloutAttributeNames.EMOJI_UNICODE]: string | undefined;
[ECalloutAttributeNames.EMOJI_URL]: string | undefined;
};
export type TCalloutBlockAttributes = {
[EAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
[EAttributeNames.BACKGROUND]: string;
[EAttributeNames.BLOCK_TYPE]: "callout-component";
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
[ECalloutAttributeNames.BACKGROUND]: string;
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component";
} & TCalloutBlockIconAttributes &
TCalloutBlockEmojiAttributes;
@@ -4,7 +4,7 @@ import { sanitizeHTML } from "@plane/helpers";
import { TEmojiLogoProps } from "@plane/ui";
// types
import {
EAttributeNames,
ECalloutAttributeNames,
TCalloutBlockAttributes,
TCalloutBlockEmojiAttributes,
TCalloutBlockIconAttributes,
@@ -20,7 +20,7 @@ export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = {
"data-block-type": "callout-component",
};
type TStoredLogoValue = Pick<TCalloutBlockAttributes, EAttributeNames.LOGO_IN_USE> &
type TStoredLogoValue = Pick<TCalloutBlockAttributes, ECalloutAttributeNames.LOGO_IN_USE> &
(TCalloutBlockEmojiAttributes | TCalloutBlockIconAttributes);
// function to get the stored logo from local storage
@@ -19,6 +19,7 @@ import { TableHeader, TableCell, TableRow, Table } from "./table";
import { CustomTextAlignExtension } from "./text-align";
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
import { CustomColorExtension } from "./custom-color";
import { CustomEmbedExtensionConfig } from "./embed/extension-config";
export const CoreEditorExtensionsWithoutProps = [
StarterKit.configure({
@@ -89,6 +90,7 @@ export const CoreEditorExtensionsWithoutProps = [
CustomTextAlignExtension,
CustomCalloutExtensionConfig,
CustomColorExtension,
CustomEmbedExtensionConfig,
];
export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()];
@@ -0,0 +1,81 @@
import React, { useCallback, useEffect, useState } from "react";
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
// helpers
import { cn } from "@/helpers/common";
// types
import { EEmbedAttributeNames, TEmbedBlockAttributes } from "./types";
type Props = NodeViewProps & {
node: NodeViewProps["node"] & {
attrs: TEmbedBlockAttributes;
};
updateAttributes: (attrs: Partial<TEmbedBlockAttributes>) => void;
};
export const CustomEmbedBlock: React.FC<Props> = (props) => {
const { editor, node } = props;
// states
const [isResizing, setIsResizing] = useState(false);
// derived values
const embedSource = node.attrs[EEmbedAttributeNames.SOURCE];
const embedWidth = node.attrs[EEmbedAttributeNames.WIDTH];
const handleResizeStart = () => {
setIsResizing(true);
};
const handleResize = useCallback((e: MouseEvent | TouchEvent) => {}, []);
const handleResizeEnd = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
if (isResizing) {
window.addEventListener("mousemove", handleResize);
window.addEventListener("mouseup", handleResizeEnd);
window.addEventListener("mouseleave", handleResizeEnd);
window.addEventListener("touchmove", handleResize);
window.addEventListener("touchend", handleResizeEnd);
return () => {
window.removeEventListener("mousemove", handleResize);
window.removeEventListener("mouseup", handleResizeEnd);
window.removeEventListener("mouseleave", handleResizeEnd);
window.removeEventListener("touchmove", handleResize);
window.removeEventListener("touchend", handleResizeEnd);
};
}
}, [isResizing, handleResize, handleResizeEnd]);
return (
<NodeViewWrapper as="div" className="editor-embed-component group/embed-component relative my-2">
<iframe className="rounded-md" src={embedSource} width={embedWidth} />
{editor.isEditable && (
<>
<div
className={cn(
"absolute inset-0 border-2 border-custom-primary-100 pointer-events-none rounded-md transition-opacity duration-100 ease-in-out",
{
"opacity-100": isResizing,
"opacity-0 group-hover/embed-component:opacity-100": !isResizing,
}
)}
/>
<div
className={cn(
"absolute bottom-0 right-0 translate-y-1/2 translate-x-1/2 size-4 rounded-full bg-custom-primary-100 border-2 border-white cursor-nwse-resize transition-opacity duration-100 ease-in-out",
{
"opacity-100 pointer-events-auto": isResizing,
"opacity-0 pointer-events-none group-hover/embed-component:opacity-100 group-hover/embed-component:pointer-events-auto":
!isResizing,
}
)}
onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
/>
</>
)}
</NodeViewWrapper>
);
};
@@ -0,0 +1,57 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { Node as NodeType } from "@tiptap/pm/model";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
// types
import { EEmbedAttributeNames } from "./types";
// utils
import { DEFAULT_EMBED_BLOCK_ATTRIBUTES } from "./utils";
// Extend Tiptap's Commands interface
declare module "@tiptap/core" {
interface Commands<ReturnType> {
embedComponent: {
insertEmbed: () => ReturnType;
};
}
}
export const CustomEmbedExtensionConfig = Node.create({
name: "embedComponent",
group: "block",
atom: true,
inline: false,
addAttributes() {
const attributes = {
// Reduce instead of map to accumulate the attributes directly into an object
...Object.values(EEmbedAttributeNames).reduce((acc, value) => {
acc[value] = {
default: DEFAULT_EMBED_BLOCK_ATTRIBUTES[value],
};
return acc;
}, {}),
};
return attributes;
},
addStorage() {
return {
markdown: {
serialize(state: MarkdownSerializerState, node: NodeType) {},
},
};
},
parseHTML() {
return [
{
tag: `div[${EEmbedAttributeNames.BLOCK_TYPE}="${DEFAULT_EMBED_BLOCK_ATTRIBUTES[EEmbedAttributeNames.BLOCK_TYPE]}"]`,
},
];
},
// Render HTML for the embed node
renderHTML({ HTMLAttributes }) {
return ["div", mergeAttributes(HTMLAttributes), 0];
},
});
@@ -0,0 +1,35 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
// block
import { CustomEmbedBlock } from "./block";
// config
import { CustomEmbedExtensionConfig } from "./extension-config";
// types
import { EEmbedAttributeNames } from "./types";
export const CustomEmbedExtension = CustomEmbedExtensionConfig.extend({
selectable: true,
draggable: true,
addCommands() {
return {
insertEmbed:
() =>
({ commands }) =>
commands.insertContent({
type: this.name,
attrs: {
[EEmbedAttributeNames.SOURCE]: "https://www.youtube.com/embed/z5rd-ZE2f3I?si=H9bkPwKyJtCXdRGW",
[EEmbedAttributeNames.WIDTH]: "100%",
},
}),
};
},
addKeyboardShortcuts() {
return {};
},
addNodeView() {
return ReactNodeViewRenderer(CustomEmbedBlock);
},
});
@@ -0,0 +1 @@
export * from "./extension";
@@ -0,0 +1,11 @@
export enum EEmbedAttributeNames {
SOURCE = "src",
WIDTH = "width",
BLOCK_TYPE = "data-block-type",
}
export type TEmbedBlockAttributes = {
[EEmbedAttributeNames.SOURCE]: string | undefined;
[EEmbedAttributeNames.WIDTH]: string | number | undefined;
[EEmbedAttributeNames.BLOCK_TYPE]: "embed-component";
};
@@ -0,0 +1,8 @@
// types
import { TEmbedBlockAttributes } from "./types";
export const DEFAULT_EMBED_BLOCK_ATTRIBUTES: TEmbedBlockAttributes = {
src: undefined,
width: undefined,
"data-block-type": "embed-component",
};
@@ -13,6 +13,7 @@ import {
CustomCodeInlineExtension,
CustomCodeMarkPlugin,
CustomColorExtension,
CustomEmbedExtension,
CustomHorizontalRule,
CustomImageExtension,
CustomKeymap,
@@ -162,5 +163,6 @@ export const CoreEditorExtensions = (args: TArguments) => {
CustomTextAlignExtension,
CustomCalloutExtension,
CustomColorExtension,
CustomEmbedExtension,
];
};
@@ -4,6 +4,7 @@ export * from "./code-inline";
export * from "./custom-image";
export * from "./custom-link";
export * from "./custom-list-keymap";
export * from "./embed";
export * from "./image";
export * from "./issue-embed";
export * from "./mentions";
@@ -14,6 +14,7 @@ import {
ListTodo,
MessageSquareText,
MinusSquare,
ScreenShare,
Table,
TextQuote,
} from "lucide-react";
@@ -37,6 +38,7 @@ import {
insertImage,
insertCallout,
setText,
insertEmbed,
} from "@/helpers/editor-commands";
// types
import { CommandProps, ISlashCommandItem } from "@/types";
@@ -189,6 +191,15 @@ export const getSlashCommandFilteredSections =
searchTerms: ["callout", "comment", "message", "info", "alert"],
command: ({ editor, range }: CommandProps) => insertCallout(editor, range),
},
{
commandKey: "embed",
key: "embed",
title: "Embed",
icon: <ScreenShare className="size-3.5" />,
description: "Insert embed",
searchTerms: ["embed", "hyperlink", "video", "pdf"],
command: ({ editor, range }) => insertEmbed(editor, range),
},
{
commandKey: "divider",
key: "divider",
@@ -189,7 +189,13 @@ export const insertHorizontalRule = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run();
else editor.chain().focus().setHorizontalRule().run();
};
export const insertCallout = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
else editor.chain().focus().insertCallout().run();
};
export const insertEmbed = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).insertEmbed().run();
else editor.chain().focus().insertEmbed().run();
};
+3 -2
View File
@@ -39,7 +39,8 @@ export type TEditorCommands =
| "text-color"
| "background-color"
| "text-align"
| "callout";
| "callout"
| "embed";
export type TCommandExtraProps = {
image: {
@@ -121,7 +122,7 @@ export interface IEditorProps {
onEnterKeyPress?: (e?: any) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
value?: string | null;
value?: string | null;
}
export interface ILiteTextEditor extends IEditorProps {
extensions?: any[];
@@ -162,7 +162,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// check if the project info is not found.
if (!loader && !projectExists && projectId && !!hasPermissionToCurrentProject === false)
return (
<div className="container grid h-screen place-items-center bg-custom-background-100">
<div className="grid h-screen place-items-center bg-custom-background-100">
<EmptyState
title="No such project exists"
description="Try creating a new project"