This issue has been archived.
From d69f025b9aa5dedb8901605b923bd8205310b340 Mon Sep 17 00:00:00 2001
From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Date: Wed, 1 May 2024 18:01:53 +0530
Subject: [PATCH 004/188] [WEB-1132] fix: display datetime fields in user time
zone format (#4323)
* fix: user timezone response
* chore: removed unused variables
---
apiserver/plane/api/views/issue.py | 1 -
apiserver/plane/app/views/cycle/issue.py | 7 +++++-
apiserver/plane/app/views/issue/archive.py | 7 +++++-
apiserver/plane/app/views/issue/base.py | 13 ++++++++++
apiserver/plane/app/views/issue/draft.py | 5 ++++
apiserver/plane/app/views/issue/sub_issue.py | 5 ++++
apiserver/plane/app/views/module/archive.py | 6 +++++
apiserver/plane/app/views/module/base.py | 14 +++++++++++
apiserver/plane/app/views/module/issue.py | 7 +++++-
apiserver/plane/app/views/view/base.py | 6 ++++-
.../plane/utils/user_timezone_converter.py | 25 +++++++++++++++++++
11 files changed, 91 insertions(+), 5 deletions(-)
create mode 100644 apiserver/plane/utils/user_timezone_converter.py
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index 46a6b6937..8d72ac5db 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -32,7 +32,6 @@ from plane.api.serializers import (
LabelSerializer,
)
from plane.app.permissions import (
- WorkspaceEntityPermission,
ProjectEntityPermission,
ProjectLitePermission,
ProjectMemberPermission,
diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py
index 2a5505dd0..9a029eb25 100644
--- a/apiserver/plane/app/views/cycle/issue.py
+++ b/apiserver/plane/app/views/cycle/issue.py
@@ -38,7 +38,7 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
-
+from plane.utils.user_timezone_converter import user_timezone_converter
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleIssueSerializer
@@ -191,6 +191,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ issues = user_timezone_converter(
+ issues, datetime_fields, request.user.user_timezone
+ )
+
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, cycle_id):
diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py
index d9274ae4f..af019a7ec 100644
--- a/apiserver/plane/app/views/issue/archive.py
+++ b/apiserver/plane/app/views/issue/archive.py
@@ -47,7 +47,7 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
-
+from plane.utils.user_timezone_converter import user_timezone_converter
class IssueArchiveViewSet(BaseViewSet):
permission_classes = [
@@ -239,6 +239,11 @@ class IssueArchiveViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ issues = user_timezone_converter(
+ issue_queryset, datetime_fields, request.user.user_timezone
+ )
+
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None):
diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py
index 23df58540..7a0e5d9b1 100644
--- a/apiserver/plane/app/views/issue/base.py
+++ b/apiserver/plane/app/views/issue/base.py
@@ -50,6 +50,7 @@ from plane.db.models import (
Project,
)
from plane.utils.issue_filters import issue_filters
+from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
@@ -241,6 +242,10 @@ class IssueListEndpoint(BaseAPIView):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ issues = user_timezone_converter(
+ issues, datetime_fields, request.user.user_timezone
+ )
return Response(issues, status=status.HTTP_200_OK)
@@ -440,6 +445,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ issues = user_timezone_converter(
+ issue_queryset, datetime_fields, request.user.user_timezone
+ )
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
@@ -503,6 +512,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
)
.first()
)
+ datetime_fields = ["created_at", "updated_at"]
+ issue = user_timezone_converter(
+ issue, datetime_fields, request.user.user_timezone
+ )
return Response(issue, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py
index 077d7dcaf..fe75c61f1 100644
--- a/apiserver/plane/app/views/issue/draft.py
+++ b/apiserver/plane/app/views/issue/draft.py
@@ -45,6 +45,7 @@ from plane.db.models import (
Project,
)
from plane.utils.issue_filters import issue_filters
+from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseViewSet
@@ -229,6 +230,10 @@ class IssueDraftViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ issues = user_timezone_converter(
+ issue_queryset, datetime_fields, request.user.user_timezone
+ )
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
diff --git a/apiserver/plane/app/views/issue/sub_issue.py b/apiserver/plane/app/views/issue/sub_issue.py
index da479e0e9..2ee4574eb 100644
--- a/apiserver/plane/app/views/issue/sub_issue.py
+++ b/apiserver/plane/app/views/issue/sub_issue.py
@@ -31,6 +31,7 @@ from plane.db.models import (
IssueAttachment,
)
from plane.bgtasks.issue_activites_task import issue_activity
+from plane.utils.user_timezone_converter import user_timezone_converter
from collections import defaultdict
@@ -132,6 +133,10 @@ class SubIssuesEndpoint(BaseAPIView):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ sub_issues = user_timezone_converter(
+ sub_issues, datetime_fields, request.user.user_timezone
+ )
return Response(
{
"sub_issues": sub_issues,
diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py
index 9c0b6cca3..8a5345ff4 100644
--- a/apiserver/plane/app/views/module/archive.py
+++ b/apiserver/plane/app/views/module/archive.py
@@ -32,6 +32,8 @@ from plane.db.models import (
ModuleLink,
)
from plane.utils.analytics_plot import burndown_plot
+from plane.utils.user_timezone_converter import user_timezone_converter
+
# Module imports
from .. import BaseAPIView
@@ -199,6 +201,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
"updated_at",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ modules = user_timezone_converter(
+ modules, datetime_fields, request.user.user_timezone
+ )
return Response(modules, status=status.HTTP_200_OK)
else:
queryset = (
diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py
index 4cd52b3b1..59f26a036 100644
--- a/apiserver/plane/app/views/module/base.py
+++ b/apiserver/plane/app/views/module/base.py
@@ -48,6 +48,8 @@ from plane.db.models import (
Project,
)
from plane.utils.analytics_plot import burndown_plot
+from plane.utils.user_timezone_converter import user_timezone_converter
+
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
@@ -236,6 +238,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"updated_at",
)
).first()
+ datetime_fields = ["created_at", "updated_at"]
+ module = user_timezone_converter(
+ module, datetime_fields, request.user.user_timezone
+ )
return Response(module, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -277,6 +283,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"created_at",
"updated_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ modules = user_timezone_converter(
+ modules, datetime_fields, request.user.user_timezone
+ )
return Response(modules, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk):
@@ -454,6 +464,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"created_at",
"updated_at",
).first()
+ datetime_fields = ["created_at", "updated_at"]
+ module = user_timezone_converter(
+ module, datetime_fields, request.user.user_timezone
+ )
return Response(module, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py
index d26433340..e0fcb2d3c 100644
--- a/apiserver/plane/app/views/module/issue.py
+++ b/apiserver/plane/app/views/module/issue.py
@@ -31,7 +31,7 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
-
+from plane.utils.user_timezone_converter import user_timezone_converter
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = ModuleIssueSerializer
@@ -150,6 +150,11 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ issues = user_timezone_converter(
+ issues, datetime_fields, request.user.user_timezone
+ )
+
return Response(issues, status=status.HTTP_200_OK)
# create multiple issues inside a module
diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py
index 35772ccf3..7736e465c 100644
--- a/apiserver/plane/app/views/view/base.py
+++ b/apiserver/plane/app/views/view/base.py
@@ -42,7 +42,7 @@ from plane.db.models import (
IssueAttachment,
)
from plane.utils.issue_filters import issue_filters
-
+from plane.utils.user_timezone_converter import user_timezone_converter
class GlobalViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
@@ -255,6 +255,10 @@ class GlobalViewIssuesViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
+ datetime_fields = ["created_at", "updated_at"]
+ issues = user_timezone_converter(
+ issues, datetime_fields, request.user.user_timezone
+ )
return Response(issues, status=status.HTTP_200_OK)
diff --git a/apiserver/plane/utils/user_timezone_converter.py b/apiserver/plane/utils/user_timezone_converter.py
new file mode 100644
index 000000000..579b96c26
--- /dev/null
+++ b/apiserver/plane/utils/user_timezone_converter.py
@@ -0,0 +1,25 @@
+import pytz
+
+def user_timezone_converter(queryset, datetime_fields, user_timezone):
+ # Create a timezone object for the user's timezone
+ user_tz = pytz.timezone(user_timezone)
+
+ # Check if queryset is a dictionary (single item) or a list of dictionaries
+ if isinstance(queryset, dict):
+ queryset_values = [queryset]
+ else:
+ queryset_values = list(queryset.values())
+
+ # 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]:
+ item[field] = item[field].astimezone(user_tz)
+
+ # If queryset was a single item, return a single item
+ if isinstance(queryset, dict):
+ return queryset_values[0]
+ else:
+ return queryset_values
From ecc277c5712f67dfb7f0fc0c6426abdfcd5b7baa Mon Sep 17 00:00:00 2001
From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Date: Wed, 1 May 2024 18:03:13 +0530
Subject: [PATCH 005/188] [WEB-1101] chore: workspace view quick action
enhancement (#4324)
* chore: workspace view quick action enhancement
* fix: issue quick action height
---
.../quick-action-dropdowns/all-issue.tsx | 1 +
.../quick-action-dropdowns/archived-issue.tsx | 1 +
.../quick-action-dropdowns/cycle-issue.tsx | 1 +
.../quick-action-dropdowns/draft-issue.tsx | 1 +
.../quick-action-dropdowns/module-issue.tsx | 1 +
.../quick-action-dropdowns/project-issue.tsx | 1 +
.../views/default-view-quick-action.tsx | 127 ++++++++++++++
web/components/workspace/views/header.tsx | 79 +++++----
web/components/workspace/views/index.ts | 2 +
.../workspace/views/quick-action.tsx | 155 ++++++++++++++++++
10 files changed, 339 insertions(+), 30 deletions(-)
create mode 100644 web/components/workspace/views/default-view-quick-action.tsx
create mode 100644 web/components/workspace/views/quick-action.tsx
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
index 7a73a25f9..b7825fc57 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx
@@ -161,6 +161,7 @@ export const AllIssueQuickActions: React.FC
= observer((props
portalElement={portalElement}
placement={placements}
menuItemsClassName="z-[14]"
+ maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx
index 0327755a9..62b808b3f 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx
@@ -123,6 +123,7 @@ export const ArchivedIssueQuickActions: React.FC = observer((
portalElement={portalElement}
placement={placements}
menuItemsClassName="z-[14]"
+ maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
index 503d8258e..a35de2735 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx
@@ -181,6 +181,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro
customButton={customActionButton}
portalElement={portalElement}
menuItemsClassName="z-[14]"
+ maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx
index 18c259107..bbeda85ce 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx
@@ -107,6 +107,7 @@ export const DraftIssueQuickActions: React.FC = observer((pro
portalElement={portalElement}
placement={placements}
menuItemsClassName="z-[14]"
+ maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
index 3cc3343b6..6061c0bee 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx
@@ -178,6 +178,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr
customButton={customActionButton}
portalElement={portalElement}
menuItemsClassName="z-[14]"
+ maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
index 0fbe10da9..b74c9c57d 100644
--- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
+++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx
@@ -171,6 +171,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p
customButton={customActionButton}
portalElement={portalElement}
menuItemsClassName="z-[14]"
+ maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
diff --git a/web/components/workspace/views/default-view-quick-action.tsx b/web/components/workspace/views/default-view-quick-action.tsx
new file mode 100644
index 000000000..5a58e1737
--- /dev/null
+++ b/web/components/workspace/views/default-view-quick-action.tsx
@@ -0,0 +1,127 @@
+import { observer } from "mobx-react";
+import Link from "next/link";
+import { ExternalLink, LinkIcon } from "lucide-react";
+// ui
+import { TStaticViewTypes } from "@plane/types";
+import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
+// helpers
+import { cn } from "@/helpers/common.helper";
+import { copyUrlToClipboard } from "@/helpers/string.helper";
+
+type Props = {
+ parentRef: React.RefObject;
+ workspaceSlug: string;
+ globalViewId: string | undefined;
+ view: {
+ key: TStaticViewTypes;
+ label: string;
+ };
+};
+
+export const DefaultWorkspaceViewQuickActions: React.FC = observer((props) => {
+ const { parentRef, globalViewId, view, workspaceSlug } = props;
+
+ const viewLink = `${workspaceSlug}/workspace-views/${view.key}`;
+ const handleCopyText = () =>
+ copyUrlToClipboard(viewLink).then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Link Copied!",
+ message: "View link copied to clipboard.",
+ });
+ });
+ const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank");
+
+ const MENU_ITEMS: TContextMenuItem[] = [
+ {
+ key: "open-new-tab",
+ action: handleOpenInNewTab,
+ title: "Open in new tab",
+ icon: ExternalLink,
+ },
+ {
+ key: "copy-link",
+ action: handleCopyText,
+ title: "Copy link",
+ icon: LinkIcon,
+ },
+ ];
+
+ return (
+ <>
+
+
+
+ {view.key === globalViewId ? (
+
+ {view.label}
+
+ ) : (
+
+
+ {view.label}
+
+
+ )}
+ >
+ }
+ placement="bottom-end"
+ menuItemsClassName="z-20"
+ closeOnSelect
+ >
+ {MENU_ITEMS.map((item) => {
+ if (item.shouldRender === false) return null;
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ item.action();
+ }}
+ className={cn(
+ "flex items-center gap-2",
+ {
+ "text-custom-text-400": item.disabled,
+ },
+ item.className
+ )}
+ >
+ {item.icon && }
+
+
{item.title}
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+ );
+ })}
+
+ >
+ );
+});
diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx
index 35c01481b..2e381052a 100644
--- a/web/components/workspace/views/header.tsx
+++ b/web/components/workspace/views/header.tsx
@@ -1,11 +1,16 @@
import React, { useEffect, useRef, useState } 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 { TStaticViewTypes } from "@plane/types";
// components
-import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
+import {
+ CreateUpdateWorkspaceViewModal,
+ DefaultWorkspaceViewQuickActions,
+ WorkspaceViewQuickActions,
+} from "@/components/workspace";
// constants
import { GLOBAL_VIEW_OPENED } from "@/constants/event-tracker";
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "@/constants/workspace";
@@ -14,6 +19,8 @@ import { useEventTracker, useGlobalView, useUser } from "@/hooks/store";
const ViewTab = observer((props: { viewId: string }) => {
const { viewId } = props;
+ // refs
+ const parentRef = useRef(null);
// router
const router = useRouter();
const { workspaceSlug, globalViewId } = router.query;
@@ -22,30 +29,54 @@ const ViewTab = observer((props: { viewId: string }) => {
const view = getViewDetailsById(viewId);
- if (!view) return null;
+ if (!view || !workspaceSlug || !globalViewId) return null;
return (
-
-
- {view.name}
-
-
+
+
+
);
});
+const DefaultViewTab = (props: {
+ tab: {
+ key: TStaticViewTypes;
+ label: string;
+ };
+}) => {
+ const { tab } = props;
+ // refs
+ const parentRef = useRef(null);
+ // router
+ const router = useRouter();
+ const { workspaceSlug, globalViewId } = router.query;
+
+ if (!workspaceSlug || !globalViewId) return null;
+ return (
+
+
+
+ );
+};
+
export const GlobalViewsHeader: React.FC = observer(() => {
// states
const [createViewModal, setCreateViewModal] = useState(false);
const containerRef = useRef(null);
// router
const router = useRouter();
- const { workspaceSlug, globalViewId } = router.query;
+ const { globalViewId } = router.query;
// store hooks
const { currentWorkspaceViews } = useGlobalView();
const {
@@ -82,23 +113,11 @@ export const GlobalViewsHeader: React.FC = observer(() => {
ref={containerRef}
className="flex w-full items-center overflow-x-auto px-4 horizontal-scrollbar scrollbar-sm"
>
- {DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => (
-
-
- {tab.label}
-
-
+ {DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => (
+
))}
- {currentWorkspaceViews?.map((viewId) => (
-
- ))}
+ {currentWorkspaceViews?.map((viewId) => )}
{isAuthorizedUser && (
diff --git a/web/components/workspace/views/index.ts b/web/components/workspace/views/index.ts
index 7d0547f64..c41d75238 100644
--- a/web/components/workspace/views/index.ts
+++ b/web/components/workspace/views/index.ts
@@ -5,3 +5,5 @@ export * from "./header";
export * from "./modal";
export * from "./view-list-item";
export * from "./views-list";
+export * from "./quick-action";
+export * from "./default-view-quick-action";
diff --git a/web/components/workspace/views/quick-action.tsx b/web/components/workspace/views/quick-action.tsx
new file mode 100644
index 000000000..3a67a95b9
--- /dev/null
+++ b/web/components/workspace/views/quick-action.tsx
@@ -0,0 +1,155 @@
+import { useState } from "react";
+import { observer } from "mobx-react";
+import Link from "next/link";
+import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
+// types
+import { IWorkspaceView } from "@plane/types";
+// ui
+import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
+// components
+import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace";
+// constants
+import { EUserProjectRoles } from "@/constants/project";
+// helpers
+import { cn } from "@/helpers/common.helper";
+import { copyUrlToClipboard } from "@/helpers/string.helper";
+// hooks
+import { useUser } from "@/hooks/store";
+
+type Props = {
+ parentRef: React.RefObject