[WEB-1792] chore: integrated inbox issue in notification peek view and handled increment/decrement of unread notifications (#5008)
* chore: added a boolean field in notification list * chore: notification filters changed * chore: handled inbox notification and typo on the card items * chore: handled notification count increment and decrement * chore: typos and ui updates --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
0fd36257d7
commit
26040144fc
16 changed files with 202 additions and 97 deletions
|
|
@ -3,11 +3,15 @@ from .base import BaseSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from plane.db.models import Notification, UserNotificationPreference
|
from plane.db.models import Notification, UserNotificationPreference
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class NotificationSerializer(BaseSerializer):
|
class NotificationSerializer(BaseSerializer):
|
||||||
triggered_by_details = UserLiteSerializer(
|
triggered_by_details = UserLiteSerializer(
|
||||||
read_only=True, source="triggered_by"
|
read_only=True, source="triggered_by"
|
||||||
)
|
)
|
||||||
|
is_inbox_issue = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Notification
|
model = Notification
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,18 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
type = request.GET.get("type", "all")
|
type = request.GET.get("type", "all")
|
||||||
q_filters = Q()
|
q_filters = Q()
|
||||||
|
|
||||||
|
inbox_issue = Issue.objects.filter(
|
||||||
|
pk=OuterRef("entity_identifier"),
|
||||||
|
issue_inbox__status__in=[0, 2, -2],
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
)
|
||||||
|
|
||||||
notifications = (
|
notifications = (
|
||||||
Notification.objects.filter(
|
Notification.objects.filter(
|
||||||
workspace__slug=slug, receiver_id=request.user.id
|
workspace__slug=slug, receiver_id=request.user.id
|
||||||
)
|
)
|
||||||
|
.filter(entity_name="issue")
|
||||||
|
.annotate(is_inbox_issue=Exists(inbox_issue))
|
||||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||||
.order_by("snoozed_till", "-created_at")
|
.order_by("snoozed_till", "-created_at")
|
||||||
)
|
)
|
||||||
|
|
@ -202,46 +210,19 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
class UnreadNotificationEndpoint(BaseAPIView):
|
class UnreadNotificationEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
# Watching Issues Count
|
# Watching Issues Count
|
||||||
subscribed_issues_count = Notification.objects.filter(
|
unread_notifications_count = Notification.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
receiver_id=request.user.id,
|
receiver_id=request.user.id,
|
||||||
read_at__isnull=True,
|
read_at__isnull=True,
|
||||||
archived_at__isnull=True,
|
archived_at__isnull=True,
|
||||||
snoozed_till__isnull=True,
|
snoozed_till__isnull=True,
|
||||||
entity_identifier__in=IssueSubscriber.objects.filter(
|
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
|
||||||
).values_list("issue_id", flat=True),
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# My Issues Count
|
|
||||||
my_issues_count = Notification.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
receiver_id=request.user.id,
|
|
||||||
read_at__isnull=True,
|
|
||||||
archived_at__isnull=True,
|
|
||||||
snoozed_till__isnull=True,
|
|
||||||
entity_identifier__in=IssueAssignee.objects.filter(
|
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
|
||||||
).values_list("issue_id", flat=True),
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Created Issues Count
|
|
||||||
created_issues_count = Notification.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
receiver_id=request.user.id,
|
|
||||||
read_at__isnull=True,
|
|
||||||
archived_at__isnull=True,
|
|
||||||
snoozed_till__isnull=True,
|
|
||||||
entity_identifier__in=Issue.objects.filter(
|
|
||||||
workspace__slug=slug, created_by=request.user
|
|
||||||
).values_list("pk", flat=True),
|
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"subscribed_issues": subscribed_issues_count,
|
"total_unread_notifications_count": int(
|
||||||
"my_issues": my_issues_count,
|
unread_notifications_count
|
||||||
"created_issues": created_issues_count,
|
)
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
13
packages/types/src/workspace-notifications.d.ts
vendored
13
packages/types/src/workspace-notifications.d.ts
vendored
|
|
@ -50,6 +50,7 @@ export type TNotification = {
|
||||||
read_at: string | undefined;
|
read_at: string | undefined;
|
||||||
archived_at: string | undefined;
|
archived_at: string | undefined;
|
||||||
snoozed_till: string | undefined;
|
snoozed_till: string | undefined;
|
||||||
|
is_inbox_issue: boolean | undefined;
|
||||||
workspace: string | undefined;
|
workspace: string | undefined;
|
||||||
project: string | undefined;
|
project: string | undefined;
|
||||||
created_at: string | undefined;
|
created_at: string | undefined;
|
||||||
|
|
@ -84,7 +85,13 @@ export type TNotificationPaginatedInfo = {
|
||||||
|
|
||||||
// notification count
|
// notification count
|
||||||
export type TUnreadNotificationsCount = {
|
export type TUnreadNotificationsCount = {
|
||||||
created_issues: number | undefined;
|
total_unread_notifications_count: number;
|
||||||
my_issues: number | undefined;
|
};
|
||||||
subscribed_issues: number | undefined;
|
|
||||||
|
export type TCurrentSelectedNotification = {
|
||||||
|
workspace_slug: string | undefined;
|
||||||
|
project_id: string | undefined;
|
||||||
|
notification_id: string | undefined;
|
||||||
|
issue_id: string | undefined;
|
||||||
|
is_inbox_issue: boolean | undefined;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,25 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
|
import { LogoSpinner } from "@/components/common";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
|
import { InboxContentRoot } from "@/components/inbox";
|
||||||
import { IssuePeekOverview } from "@/components/issues";
|
import { IssuePeekOverview } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
||||||
// hooks
|
// hooks
|
||||||
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
import { useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||||
|
|
||||||
const WorkspaceDashboardPage = observer(() => {
|
const WorkspaceDashboardPage = observer(() => {
|
||||||
// hooks
|
// hooks
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
|
const { currentSelectedNotification, notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
|
||||||
|
const {
|
||||||
|
membership: { fetchUserProjectInfo },
|
||||||
|
} = useUser();
|
||||||
// derived values
|
// derived values
|
||||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
|
||||||
|
const { workspace_slug, project_id, issue_id, is_inbox_issue } = currentSelectedNotification;
|
||||||
|
|
||||||
// fetch workspace notifications
|
// fetch workspace notifications
|
||||||
const notificationMutation =
|
const notificationMutation =
|
||||||
|
|
@ -29,15 +35,42 @@ const WorkspaceDashboardPage = observer(() => {
|
||||||
useSWR(
|
useSWR(
|
||||||
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
|
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
|
||||||
currentWorkspace?.slug
|
currentWorkspace?.slug
|
||||||
? async () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// fetching user project member info
|
||||||
|
const { isLoading: projectMemberInfoLoader } = useSWR(
|
||||||
|
workspace_slug && project_id && is_inbox_issue
|
||||||
|
? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}`
|
||||||
|
: null,
|
||||||
|
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||||
|
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||||
|
<>
|
||||||
|
{projectMemberInfoLoader ? (
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<LogoSpinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<InboxContentRoot
|
||||||
|
setIsMobileSidebar={() => {}}
|
||||||
|
isMobileSidebar={false}
|
||||||
|
workspaceSlug={workspace_slug}
|
||||||
|
projectId={project_id}
|
||||||
|
inboxIssueId={issue_id}
|
||||||
|
isNotificationEmbed
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<IssuePeekOverview embedIssue />
|
<IssuePeekOverview embedIssue />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,19 @@ type TInboxIssueActionsHeader = {
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
isMobileSidebar: boolean;
|
isMobileSidebar: boolean;
|
||||||
setIsMobileSidebar: (value: boolean) => void;
|
setIsMobileSidebar: (value: boolean) => void;
|
||||||
|
isNotificationEmbed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((props) => {
|
export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, inboxIssue, isSubmitting, isMobileSidebar, setIsMobileSidebar } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
inboxIssue,
|
||||||
|
isSubmitting,
|
||||||
|
isMobileSidebar,
|
||||||
|
setIsMobileSidebar,
|
||||||
|
isNotificationEmbed = false,
|
||||||
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isSnoozeDateModalOpen, setIsSnoozeDateModalOpen] = useState(false);
|
const [isSnoozeDateModalOpen, setIsSnoozeDateModalOpen] = useState(false);
|
||||||
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
||||||
|
|
@ -58,7 +67,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||||
const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox();
|
const { currentTab, deleteInboxIssue, filteredInboxIssueIds } = useProjectInbox();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRoleByProjectId },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
|
|
@ -66,6 +75,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||||
|
|
||||||
const issue = inboxIssue?.issue;
|
const issue = inboxIssue?.issue;
|
||||||
// derived values
|
// derived values
|
||||||
|
const currentProjectRole = currentProjectRoleByProjectId(projectId) || undefined;
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
|
const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
|
||||||
const canMarkAsAccepted = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
|
const canMarkAsAccepted = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
|
||||||
|
|
@ -240,6 +250,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{!isNotificationEmbed && (
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -256,6 +267,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||||
<ChevronDown size={14} strokeWidth={2} />
|
<ChevronDown size={14} strokeWidth={2} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{canMarkAsAccepted && (
|
{canMarkAsAccepted && (
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,18 @@ type TInboxContentRoot = {
|
||||||
inboxIssueId: string;
|
inboxIssueId: string;
|
||||||
isMobileSidebar: boolean;
|
isMobileSidebar: boolean;
|
||||||
setIsMobileSidebar: (value: boolean) => void;
|
setIsMobileSidebar: (value: boolean) => void;
|
||||||
|
isNotificationEmbed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, inboxIssueId, isMobileSidebar, setIsMobileSidebar } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
inboxIssueId,
|
||||||
|
isMobileSidebar,
|
||||||
|
setIsMobileSidebar,
|
||||||
|
isNotificationEmbed = false,
|
||||||
|
} = props;
|
||||||
/// router
|
/// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
// states
|
// states
|
||||||
|
|
@ -33,11 +41,11 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||||
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
|
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isIssueAvailable && inboxIssueId) {
|
if (!isIssueAvailable && inboxIssueId && !isNotificationEmbed) {
|
||||||
router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
|
router.replace(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}`);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isIssueAvailable]);
|
}, [isIssueAvailable, isNotificationEmbed]);
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && inboxIssueId
|
workspaceSlug && projectId && inboxIssueId
|
||||||
|
|
@ -69,6 +77,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
inboxIssue={inboxIssue}
|
inboxIssue={inboxIssue}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
|
isNotificationEmbed={isNotificationEmbed || false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5 vertical-scrollbar scrollbar-md">
|
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5 vertical-scrollbar scrollbar-md">
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const IssuePeekOverviewLoader: FC<TIssuePeekOverviewLoader> = (props) =>
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Loader className="w-full h-full overflow-hidden p-5 space-y-6">
|
<Loader className="w-full h-screen overflow-hidden p-5 space-y-6">
|
||||||
<div className="flex justify-between items-center gap-2">
|
<div className="flex justify-between items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip tooltipContent="Close the peek view" isMobile={isMobile}>
|
<Tooltip tooltipContent="Close the peek view" isMobile={isMobile}>
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,21 @@ type TNotificationAppSidebarOption = {
|
||||||
export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = observer((props) => {
|
export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = observer((props) => {
|
||||||
const { workspaceSlug, isSidebarCollapsed } = props;
|
const { workspaceSlug, isSidebarCollapsed } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { totalUnreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
|
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
|
||||||
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null
|
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
if (totalUnreadNotificationsCount <= 0) return <></>;
|
if (unreadNotificationsCount.total_unread_notifications_count <= 0) return <></>;
|
||||||
|
|
||||||
if (isSidebarCollapsed)
|
if (isSidebarCollapsed)
|
||||||
return <div className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-custom-primary-300" />;
|
return <div className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-custom-primary-300" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-[8px] ml-auto bg-custom-primary-100 text-white p-1 py-0.5 rounded-full">
|
<div className="text-[8px] ml-auto bg-custom-primary-100 text-white p-1 py-0.5 rounded-full">
|
||||||
{getNumberCount(totalUnreadNotificationsCount)}
|
{getNumberCount(unreadNotificationsCount.total_unread_notifications_count)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,7 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="font-medium">Notifications</div>
|
<div className="font-medium">Notifications</div>
|
||||||
<div className="rounded-full text-xs px-1.5 py-0.5 bg-custom-primary-100/20">
|
<div className="rounded-full text-xs px-1.5 py-0.5 bg-custom-primary-100">{notificationsCount}</div>
|
||||||
{notificationsCount}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
icon={<Bell className="h-4 w-4 text-custom-text-300" />}
|
icon={<Bell className="h-4 w-4 text-custom-text-300" />}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export const NotificationsLoader = () => (
|
export const NotificationsLoader = () => (
|
||||||
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden">
|
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden">
|
||||||
{[...Array(3)].map((i) => (
|
{[...Array(8)].map((i) => (
|
||||||
<div key={i} className="flex w-full items-center gap-4 p-3">
|
<div key={i} className="flex w-full items-center gap-4 p-3">
|
||||||
<span className="min-h-12 min-w-12 bg-custom-background-80 rounded-full" />
|
<span className="min-h-12 min-w-12 bg-custom-background-80 rounded-full" />
|
||||||
<div className="flex flex-col gap-2.5 w-full">
|
<div className="flex flex-col gap-2.5 w-full">
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Clock } from "lucide-react";
|
import { Clock } from "lucide-react";
|
||||||
|
import { TCurrentSelectedNotification } from "@plane/types";
|
||||||
import { Avatar } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { NotificationOption } from "@/components/workspace-notifications";
|
import { NotificationOption } from "@/components/workspace-notifications";
|
||||||
|
|
@ -12,7 +13,7 @@ import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/he
|
||||||
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
|
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
|
||||||
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useNotification } from "@/hooks/store";
|
import { useIssueDetail, useNotification, useWorkspaceNotifications } from "@/hooks/store";
|
||||||
|
|
||||||
type TNotificationItem = {
|
type TNotificationItem = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|
@ -22,6 +23,7 @@ type TNotificationItem = {
|
||||||
export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||||
const { workspaceSlug, notificationId } = props;
|
const { workspaceSlug, notificationId } = props;
|
||||||
// hooks
|
// hooks
|
||||||
|
const { currentSelectedNotification, setCurrentSelectedNotification } = useWorkspaceNotifications();
|
||||||
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
|
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
|
||||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||||
// states
|
// states
|
||||||
|
|
@ -44,15 +46,29 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||||
!isSnoozeStateModalOpen &&
|
!isSnoozeStateModalOpen &&
|
||||||
!customSnoozeModal
|
!customSnoozeModal
|
||||||
) {
|
) {
|
||||||
setPeekIssue({ workspaceSlug, projectId, issueId });
|
const currentSelectedNotificationPayload: TCurrentSelectedNotification = {
|
||||||
|
workspace_slug: workspaceSlug,
|
||||||
|
project_id: projectId,
|
||||||
|
issue_id: issueId,
|
||||||
|
notification_id: notification?.id,
|
||||||
|
is_inbox_issue: notification?.is_inbox_issue || false,
|
||||||
|
};
|
||||||
|
setCurrentSelectedNotification(currentSelectedNotificationPayload);
|
||||||
|
|
||||||
// make the notification as read
|
// make the notification as read
|
||||||
if (notification.read_at === null)
|
if (notification.read_at === null) {
|
||||||
try {
|
try {
|
||||||
await markNotificationAsRead(workspaceSlug);
|
await markNotificationAsRead(workspaceSlug);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notification?.is_inbox_issue === false) {
|
||||||
|
setPeekIssue({ workspaceSlug, projectId, issueId });
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField) return <></>;
|
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField) return <></>;
|
||||||
|
|
@ -61,8 +77,10 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative p-3 py-4 flex items-center gap-2 border-b border-custom-border-200 cursor-pointer transition-all group",
|
"relative p-3 py-4 flex items-center gap-2 border-b border-custom-border-200 cursor-pointer transition-all group",
|
||||||
|
currentSelectedNotification && currentSelectedNotification?.notification_id === notification?.id
|
||||||
|
? "bg-custom-background-80/30"
|
||||||
|
: "",
|
||||||
notification.read_at === null ? "bg-custom-primary-100/5" : ""
|
notification.read_at === null ? "bg-custom-primary-100/5" : ""
|
||||||
// peekIssue && peekIssue?.issueId === issueId ? "bg-custom-background-80" : "
|
|
||||||
)}
|
)}
|
||||||
onClick={handleNotificationIssuePeekOverview}
|
onClick={handleNotificationIssuePeekOverview}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export const NotificationItemArchiveOption: FC<TNotificationItemArchiveOption> =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationItemOptionButton
|
<NotificationItemOptionButton
|
||||||
tooltipContent={data.read_at ? "Mark as unread" : "Mark as read"}
|
tooltipContent={data.archived_at ? "Un archive" : "Archive"}
|
||||||
callBack={handleNotificationUpdate}
|
callBack={handleNotificationUpdate}
|
||||||
>
|
>
|
||||||
{data.archived_at ? (
|
{data.archived_at ? (
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export const NotificationsSidebar: FC = observer(() => {
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
// hooks
|
// hooks
|
||||||
const { getWorkspaceBySlug } = useWorkspace();
|
const { getWorkspaceBySlug } = useWorkspace();
|
||||||
const { paginationInfo, loader, notificationIdsByWorkspaceId } = useWorkspaceNotifications();
|
const { unreadNotificationsCount, loader, notificationIdsByWorkspaceId } = useWorkspaceNotifications();
|
||||||
// derived values
|
// derived values
|
||||||
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
|
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
|
||||||
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined;
|
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined;
|
||||||
|
|
@ -30,7 +30,7 @@ export const NotificationsSidebar: FC = observer(() => {
|
||||||
const currentTabEmptyState = ENotificationTab.ALL
|
const currentTabEmptyState = ENotificationTab.ALL
|
||||||
? EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE
|
? EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE
|
||||||
: EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE;
|
: EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE;
|
||||||
const totalNotificationCount = paginationInfo?.total_count || 0;
|
const totalNotificationCount = unreadNotificationsCount.total_unread_notifications_count;
|
||||||
|
|
||||||
if (!workspaceSlug || !workspace) return <></>;
|
if (!workspaceSlug || !workspace) return <></>;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export class Notification implements INotification {
|
||||||
read_at: string | undefined = undefined;
|
read_at: string | undefined = undefined;
|
||||||
archived_at: string | undefined = undefined;
|
archived_at: string | undefined = undefined;
|
||||||
snoozed_till: string | undefined = undefined;
|
snoozed_till: string | undefined = undefined;
|
||||||
|
is_inbox_issue: boolean | undefined = undefined;
|
||||||
workspace: string | undefined = undefined;
|
workspace: string | undefined = undefined;
|
||||||
project: string | undefined = undefined;
|
project: string | undefined = undefined;
|
||||||
created_at: string | undefined = undefined;
|
created_at: string | undefined = undefined;
|
||||||
|
|
@ -102,6 +103,7 @@ export class Notification implements INotification {
|
||||||
this.archived_at = this.notification.archived_at;
|
this.archived_at = this.notification.archived_at;
|
||||||
this.snoozed_till = this.notification.snoozed_till;
|
this.snoozed_till = this.notification.snoozed_till;
|
||||||
this.workspace = this.notification.workspace;
|
this.workspace = this.notification.workspace;
|
||||||
|
this.is_inbox_issue = this.notification.is_inbox_issue;
|
||||||
this.project = this.notification.project;
|
this.project = this.notification.project;
|
||||||
this.created_at = this.notification.created_at;
|
this.created_at = this.notification.created_at;
|
||||||
this.updated_at = this.notification.updated_at;
|
this.updated_at = this.notification.updated_at;
|
||||||
|
|
@ -131,6 +133,7 @@ export class Notification implements INotification {
|
||||||
archived_at: this.archived_at,
|
archived_at: this.archived_at,
|
||||||
snoozed_till: this.snoozed_till,
|
snoozed_till: this.snoozed_till,
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
|
is_inbox_issue: this.is_inbox_issue,
|
||||||
project: this.project,
|
project: this.project,
|
||||||
created_at: this.created_at,
|
created_at: this.created_at,
|
||||||
updated_at: this.updated_at,
|
updated_at: this.updated_at,
|
||||||
|
|
@ -187,6 +190,7 @@ export class Notification implements INotification {
|
||||||
const payload: Partial<TNotification> = {
|
const payload: Partial<TNotification> = {
|
||||||
read_at: new Date().toISOString(),
|
read_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
this.store.workspaceNotification.setUnreadNotificationsCount("decrement");
|
||||||
runInAction(() => this.mutateNotification(payload));
|
runInAction(() => this.mutateNotification(payload));
|
||||||
const notification = await workspaceNotificationService.markNotificationAsRead(workspaceSlug, this.id);
|
const notification = await workspaceNotificationService.markNotificationAsRead(workspaceSlug, this.id);
|
||||||
if (notification) {
|
if (notification) {
|
||||||
|
|
@ -195,6 +199,7 @@ export class Notification implements INotification {
|
||||||
return notification;
|
return notification;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
|
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
|
||||||
|
this.store.workspaceNotification.setUnreadNotificationsCount("increment");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -212,6 +217,7 @@ export class Notification implements INotification {
|
||||||
const payload: Partial<TNotification> = {
|
const payload: Partial<TNotification> = {
|
||||||
read_at: undefined,
|
read_at: undefined,
|
||||||
};
|
};
|
||||||
|
this.store.workspaceNotification.setUnreadNotificationsCount("increment");
|
||||||
runInAction(() => this.mutateNotification(payload));
|
runInAction(() => this.mutateNotification(payload));
|
||||||
const notification = await workspaceNotificationService.markNotificationAsUnread(workspaceSlug, this.id);
|
const notification = await workspaceNotificationService.markNotificationAsUnread(workspaceSlug, this.id);
|
||||||
if (notification) {
|
if (notification) {
|
||||||
|
|
@ -219,6 +225,7 @@ export class Notification implements INotification {
|
||||||
}
|
}
|
||||||
return notification;
|
return notification;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.store.workspaceNotification.setUnreadNotificationsCount("decrement");
|
||||||
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
|
runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import update from "lodash/update";
|
||||||
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
import {
|
import {
|
||||||
|
TCurrentSelectedNotification,
|
||||||
TNotification,
|
TNotification,
|
||||||
TNotificationFilter,
|
TNotificationFilter,
|
||||||
TNotificationPaginatedInfo,
|
TNotificationPaginatedInfo,
|
||||||
|
|
@ -28,13 +30,13 @@ type TNotificationQueryParamType = ENotificationQueryParamType;
|
||||||
export interface IWorkspaceNotificationStore {
|
export interface IWorkspaceNotificationStore {
|
||||||
// observables
|
// observables
|
||||||
loader: TNotificationLoader;
|
loader: TNotificationLoader;
|
||||||
unreadNotificationsCount: TUnreadNotificationsCount | undefined;
|
unreadNotificationsCount: TUnreadNotificationsCount;
|
||||||
notifications: Record<string, INotification>; // notification_id -> notification
|
notifications: Record<string, INotification>; // notification_id -> notification
|
||||||
currentNotificationTab: TNotificationTab;
|
currentNotificationTab: TNotificationTab;
|
||||||
|
currentSelectedNotification: TCurrentSelectedNotification;
|
||||||
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined;
|
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined;
|
||||||
filters: TNotificationFilter;
|
filters: TNotificationFilter;
|
||||||
// computed
|
// computed
|
||||||
totalUnreadNotificationsCount: number;
|
|
||||||
// computed functions
|
// computed functions
|
||||||
notificationIdsByWorkspaceId: (workspaceId: string) => string[] | undefined;
|
notificationIdsByWorkspaceId: (workspaceId: string) => string[] | undefined;
|
||||||
// helper actions
|
// helper actions
|
||||||
|
|
@ -43,6 +45,8 @@ export interface IWorkspaceNotificationStore {
|
||||||
updateBulkFilters: (filters: Partial<TNotificationFilter>) => void;
|
updateBulkFilters: (filters: Partial<TNotificationFilter>) => void;
|
||||||
// actions
|
// actions
|
||||||
setCurrentNotificationTab: (tab: TNotificationTab) => void;
|
setCurrentNotificationTab: (tab: TNotificationTab) => void;
|
||||||
|
setCurrentSelectedNotification: (notification: TCurrentSelectedNotification) => void;
|
||||||
|
setUnreadNotificationsCount: (type: "increment" | "decrement") => void;
|
||||||
getUnreadNotificationsCount: (workspaceSlug: string) => Promise<TUnreadNotificationsCount | undefined>;
|
getUnreadNotificationsCount: (workspaceSlug: string) => Promise<TUnreadNotificationsCount | undefined>;
|
||||||
getNotifications: (
|
getNotifications: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
|
|
@ -57,9 +61,18 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||||
paginatedCount = 30;
|
paginatedCount = 30;
|
||||||
// observables
|
// observables
|
||||||
loader: TNotificationLoader = undefined;
|
loader: TNotificationLoader = undefined;
|
||||||
unreadNotificationsCount: TUnreadNotificationsCount | undefined = undefined;
|
unreadNotificationsCount: TUnreadNotificationsCount = {
|
||||||
|
total_unread_notifications_count: 0,
|
||||||
|
};
|
||||||
notifications: Record<string, INotification> = {};
|
notifications: Record<string, INotification> = {};
|
||||||
currentNotificationTab: TNotificationTab = ENotificationTab.ALL;
|
currentNotificationTab: TNotificationTab = ENotificationTab.ALL;
|
||||||
|
currentSelectedNotification: TCurrentSelectedNotification = {
|
||||||
|
workspace_slug: undefined,
|
||||||
|
project_id: undefined,
|
||||||
|
notification_id: undefined,
|
||||||
|
issue_id: undefined,
|
||||||
|
is_inbox_issue: false,
|
||||||
|
};
|
||||||
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined = undefined;
|
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined = undefined;
|
||||||
filters: TNotificationFilter = {
|
filters: TNotificationFilter = {
|
||||||
type: {
|
type: {
|
||||||
|
|
@ -76,15 +89,17 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
unreadNotificationsCount: observable.ref,
|
unreadNotificationsCount: observable,
|
||||||
notifications: observable,
|
notifications: observable,
|
||||||
currentNotificationTab: observable.ref,
|
currentNotificationTab: observable.ref,
|
||||||
|
currentSelectedNotification: observable,
|
||||||
paginationInfo: observable,
|
paginationInfo: observable,
|
||||||
filters: observable,
|
filters: observable,
|
||||||
// computed
|
// computed
|
||||||
totalUnreadNotificationsCount: computed,
|
|
||||||
// helper actions
|
// helper actions
|
||||||
setCurrentNotificationTab: action,
|
setCurrentNotificationTab: action,
|
||||||
|
setCurrentSelectedNotification: action,
|
||||||
|
setUnreadNotificationsCount: action,
|
||||||
mutateNotifications: action,
|
mutateNotifications: action,
|
||||||
updateFilters: action,
|
updateFilters: action,
|
||||||
updateBulkFilters: action,
|
updateBulkFilters: action,
|
||||||
|
|
@ -96,15 +111,6 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
get totalUnreadNotificationsCount() {
|
|
||||||
let count: number = 0;
|
|
||||||
if (!this.unreadNotificationsCount) return count;
|
|
||||||
|
|
||||||
Object.values(this.unreadNotificationsCount).forEach((value) => {
|
|
||||||
count += value || 0;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// computed functions
|
// computed functions
|
||||||
/**
|
/**
|
||||||
|
|
@ -151,16 +157,14 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
.join(",") || undefined;
|
.join(",") || undefined;
|
||||||
|
|
||||||
const currentPage = this.paginationInfo ? Number(this.paginationInfo?.prev_cursor?.split(":")[1] || 0) + 1 : 0;
|
|
||||||
|
|
||||||
const queryCursorNext =
|
const queryCursorNext =
|
||||||
paramType === ENotificationQueryParamType.INIT
|
paramType === ENotificationQueryParamType.INIT
|
||||||
? `${this.paginatedCount}:0:0`
|
? `${this.paginatedCount}:0:0`
|
||||||
: paramType === ENotificationQueryParamType.CURRENT
|
: paramType === ENotificationQueryParamType.CURRENT
|
||||||
? `${this.paginatedCount}:${currentPage}:0`
|
? `${this.paginatedCount}:${0}:0`
|
||||||
: paramType === ENotificationQueryParamType.NEXT && this.paginationInfo
|
: paramType === ENotificationQueryParamType.NEXT && this.paginationInfo
|
||||||
? this.paginationInfo?.next_cursor
|
? this.paginationInfo?.next_cursor
|
||||||
: `${this.paginatedCount}:${currentPage}:0`;
|
: `${this.paginatedCount}:${0}:0`;
|
||||||
|
|
||||||
const queryParams: TNotificationPaginatedInfoQueryParams = {
|
const queryParams: TNotificationPaginatedInfoQueryParams = {
|
||||||
type: queryParamsType,
|
type: queryParamsType,
|
||||||
|
|
@ -232,6 +236,27 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||||
set(this, "currentNotificationTab", tab);
|
set(this, "currentNotificationTab", tab);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description set current selected notification
|
||||||
|
* @param { TCurrentSelectedNotification } notification
|
||||||
|
* @returns { void }
|
||||||
|
*/
|
||||||
|
setCurrentSelectedNotification = (notification: TCurrentSelectedNotification): void => {
|
||||||
|
set(this, "currentSelectedNotification", notification);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description set unread notifications count
|
||||||
|
* @param { "increment" | "decrement" } type
|
||||||
|
* @returns { void }
|
||||||
|
*/
|
||||||
|
setUnreadNotificationsCount = (type: "increment" | "decrement"): void =>
|
||||||
|
runInAction(() => {
|
||||||
|
update(this.unreadNotificationsCount, "total_unread_notifications_count", (count: 0) =>
|
||||||
|
type === "increment" ? count + 1 : count - 1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get unread notifications count
|
* @description get unread notifications count
|
||||||
* @param { string } workspaceSlug,
|
* @param { string } workspaceSlug,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ export interface IUserMembershipStore {
|
||||||
currentWorkspaceRole: EUserWorkspaceRoles | undefined;
|
currentWorkspaceRole: EUserWorkspaceRoles | undefined;
|
||||||
currentWorkspaceAllProjectsRole: IUserProjectsRole | undefined;
|
currentWorkspaceAllProjectsRole: IUserProjectsRole | undefined;
|
||||||
|
|
||||||
|
// computed functions
|
||||||
|
currentProjectRoleByProjectId: (projectId: string) => EUserProjectRoles | undefined;
|
||||||
|
|
||||||
hasPermissionToCurrentWorkspace: boolean | undefined;
|
hasPermissionToCurrentWorkspace: boolean | undefined;
|
||||||
hasPermissionToCurrentProject: boolean | undefined;
|
hasPermissionToCurrentProject: boolean | undefined;
|
||||||
// fetch actions
|
// fetch actions
|
||||||
|
|
@ -156,6 +159,14 @@ export class UserMembershipStore implements IUserMembershipStore {
|
||||||
return this.hasPermissionToProject[this.router.projectId];
|
return this.hasPermissionToProject[this.router.projectId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computed functions
|
||||||
|
/**
|
||||||
|
* Returns the current project role by project id
|
||||||
|
* @param projectId
|
||||||
|
* @returns EUserProjectRoles
|
||||||
|
*/
|
||||||
|
currentProjectRoleByProjectId = (projectId: string) => this.projectMemberInfo[projectId]?.role || undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the current user workspace info
|
* Fetches the current user workspace info
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue