[WEB-1900] chore: mentions mutation, ui fix on app sidebar notification badge, and back button inbox issue notification embed (#5083)
* chore: mention notification boolean field * chore: handled mentions and all notification mutation and UI fix on the app sidebar notification badge and Back redirection button on inbox issue resposiveness * chore: Moved everthing to chip * chore: cleaning up the selection when we unmount the page * chore: resolved build error --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
988201d729
commit
f617937542
14 changed files with 85 additions and 18 deletions
|
|
@ -12,6 +12,7 @@ class NotificationSerializer(BaseSerializer):
|
||||||
read_only=True, source="triggered_by"
|
read_only=True, source="triggered_by"
|
||||||
)
|
)
|
||||||
is_inbox_issue = serializers.BooleanField(read_only=True)
|
is_inbox_issue = serializers.BooleanField(read_only=True)
|
||||||
|
is_mentioned_notification = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Notification
|
model = Notification
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q, Case, When, BooleanField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
|
@ -60,6 +60,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||||
)
|
)
|
||||||
.filter(entity_name="issue")
|
.filter(entity_name="issue")
|
||||||
.annotate(is_inbox_issue=Exists(inbox_issue))
|
.annotate(is_inbox_issue=Exists(inbox_issue))
|
||||||
|
.annotate(
|
||||||
|
is_mentioned_notification=Case(
|
||||||
|
When(sender__icontains="mentioned", then=True),
|
||||||
|
default=False,
|
||||||
|
output_field=BooleanField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
.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")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export type TNotification = {
|
||||||
archived_at: string | undefined;
|
archived_at: string | undefined;
|
||||||
snoozed_till: string | undefined;
|
snoozed_till: string | undefined;
|
||||||
is_inbox_issue: boolean | undefined;
|
is_inbox_issue: boolean | undefined;
|
||||||
|
is_mentioned_notification: boolean | undefined;
|
||||||
workspace: string | undefined;
|
workspace: string | undefined;
|
||||||
project: string | undefined;
|
project: string | undefined;
|
||||||
created_at: string | undefined;
|
created_at: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
|
|
@ -10,7 +11,7 @@ import { IssuePeekOverview } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
import { useIssueDetail, useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||||
|
|
||||||
const WorkspaceDashboardPage = observer(() => {
|
const WorkspaceDashboardPage = observer(() => {
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -25,6 +26,7 @@ const WorkspaceDashboardPage = observer(() => {
|
||||||
const {
|
const {
|
||||||
membership: { fetchUserProjectInfo },
|
membership: { fetchUserProjectInfo },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const { setPeekIssue } = useIssueDetail();
|
||||||
// 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 } =
|
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||||
|
|
@ -54,6 +56,15 @@ const WorkspaceDashboardPage = observer(() => {
|
||||||
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// clearing up the selected notifications when unmounting the page
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
setCurrentSelectedNotificationId(undefined);
|
||||||
|
setPeekIssue(undefined);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||||
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, CountChip, Logo } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import {
|
import {
|
||||||
|
|
@ -161,9 +161,7 @@ export const ProjectIssuesHeader = observer(() => {
|
||||||
tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue"} in this project`}
|
tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue"} in this project`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
|
<CountChip count={issuesCount} />
|
||||||
{issuesCount}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
25
web/core/components/common/count-chip.tsx
Normal file
25
web/core/components/common/count-chip.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
//
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
||||||
|
type TCountChip = {
|
||||||
|
count: string | number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CountChip: FC<TCountChip> = (props) => {
|
||||||
|
const { count, className = "" } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex justify-center items-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -4,3 +4,4 @@ export * from "./latest-feature-block";
|
||||||
export * from "./breadcrumb-link";
|
export * from "./breadcrumb-link";
|
||||||
export * from "./logo-spinner";
|
export * from "./logo-spinner";
|
||||||
export * from "./logo";
|
export * from "./logo";
|
||||||
|
export * from "./count-chip";
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,8 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
isMobileSidebar={isMobileSidebar}
|
isMobileSidebar={isMobileSidebar}
|
||||||
setIsMobileSidebar={setIsMobileSidebar}
|
setIsMobileSidebar={setIsMobileSidebar}
|
||||||
|
isNotificationEmbed={isNotificationEmbed}
|
||||||
|
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Link,
|
Link,
|
||||||
Trash2,
|
Trash2,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
|
MoveRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
|
|
@ -44,6 +45,8 @@ type Props = {
|
||||||
handleCopyIssueLink: () => void;
|
handleCopyIssueLink: () => void;
|
||||||
isMobileSidebar: boolean;
|
isMobileSidebar: boolean;
|
||||||
setIsMobileSidebar: (value: boolean) => void;
|
setIsMobileSidebar: (value: boolean) => void;
|
||||||
|
isNotificationEmbed: boolean;
|
||||||
|
embedRemoveCurrentNotification?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) => {
|
export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) => {
|
||||||
|
|
@ -65,6 +68,8 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
||||||
handleCopyIssueLink,
|
handleCopyIssueLink,
|
||||||
isMobileSidebar,
|
isMobileSidebar,
|
||||||
setIsMobileSidebar,
|
setIsMobileSidebar,
|
||||||
|
isNotificationEmbed,
|
||||||
|
embedRemoveCurrentNotification,
|
||||||
} = props;
|
} = props;
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const issue = inboxIssue?.issue;
|
const issue = inboxIssue?.issue;
|
||||||
|
|
@ -76,6 +81,11 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-12 relative flex border-custom-border-200 w-full items-center gap-2 px-4">
|
<div className="h-12 relative flex border-custom-border-200 w-full items-center gap-2 px-4">
|
||||||
|
{isNotificationEmbed && (
|
||||||
|
<button onClick={embedRemoveCurrentNotification}>
|
||||||
|
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<PanelLeft
|
<PanelLeft
|
||||||
onClick={() => setIsMobileSidebar(!isMobileSidebar)}
|
onClick={() => setIsMobileSidebar(!isMobileSidebar)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// components
|
||||||
|
import { CountChip } from "@/components/common";
|
||||||
// helpers
|
// helpers
|
||||||
import { getNumberCount } from "@/helpers/string.helper";
|
import { getNumberCount } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -35,8 +37,8 @@ export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = o
|
||||||
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="ml-auto">
|
||||||
{`${isMentionsEnabled ? `@` : ``}${getNumberCount(totalNotifications)}`}
|
<CountChip count={`${isMentionsEnabled ? `@` : ``}${getNumberCount(totalNotifications)}`} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||||
|
|
||||||
const handleNotificationIssuePeekOverview = async () => {
|
const handleNotificationIssuePeekOverview = async () => {
|
||||||
if (workspaceSlug && projectId && issueId && !isSnoozeStateModalOpen && !customSnoozeModal) {
|
if (workspaceSlug && projectId && issueId && !isSnoozeStateModalOpen && !customSnoozeModal) {
|
||||||
|
setPeekIssue(undefined);
|
||||||
setCurrentSelectedNotificationId(notificationId);
|
setCurrentSelectedNotificationId(notificationId);
|
||||||
|
|
||||||
// make the notification as read
|
// make the notification as read
|
||||||
|
|
@ -51,7 +52,6 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||||
|
|
||||||
if (notification?.is_inbox_issue === false) {
|
if (notification?.is_inbox_issue === false) {
|
||||||
!getIsIssuePeeked(issueId) && setPeekIssue({ workspaceSlug, projectId, issueId });
|
!getIsIssuePeeked(issueId) && setPeekIssue({ workspaceSlug, projectId, issueId });
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// components
|
// components
|
||||||
|
import { CountChip } from "@/components/common";
|
||||||
import {
|
import {
|
||||||
NotificationsLoader,
|
NotificationsLoader,
|
||||||
NotificationEmptyState,
|
NotificationEmptyState,
|
||||||
|
|
@ -65,14 +66,12 @@ export const NotificationsSidebar: FC = observer(() => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="font-medium">{tab.label}</div>
|
<div className="font-medium">{tab.label}</div>
|
||||||
<div
|
<CountChip
|
||||||
className={cn(
|
count={getNumberCount(tab.count(unreadNotificationsCount))}
|
||||||
`rounded-full text-xs px-1.5 py-0.5`,
|
className={
|
||||||
currentNotificationTab === tab.value ? `bg-custom-primary-100/20` : `bg-custom-background-80/50`
|
currentNotificationTab === tab.value ? `bg-custom-primary-100/20` : `bg-custom-background-80/50`
|
||||||
)}
|
}
|
||||||
>
|
/>
|
||||||
{getNumberCount(tab.count(unreadNotificationsCount))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{currentNotificationTab === tab.value && (
|
{currentNotificationTab === tab.value && (
|
||||||
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-custom-primary-100" />
|
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-custom-primary-100" />
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export class Notification implements INotification {
|
||||||
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;
|
is_inbox_issue: boolean | undefined = undefined;
|
||||||
|
is_mentioned_notification: 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;
|
||||||
|
|
@ -70,6 +71,8 @@ export class Notification implements INotification {
|
||||||
read_at: observable.ref,
|
read_at: observable.ref,
|
||||||
archived_at: observable.ref,
|
archived_at: observable.ref,
|
||||||
snoozed_till: observable.ref,
|
snoozed_till: observable.ref,
|
||||||
|
is_inbox_issue: observable.ref,
|
||||||
|
is_mentioned_notification: observable.ref,
|
||||||
workspace: observable.ref,
|
workspace: observable.ref,
|
||||||
project: observable.ref,
|
project: observable.ref,
|
||||||
created_at: observable.ref,
|
created_at: observable.ref,
|
||||||
|
|
@ -102,8 +105,9 @@ export class Notification implements INotification {
|
||||||
this.read_at = this.notification.read_at;
|
this.read_at = this.notification.read_at;
|
||||||
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.is_inbox_issue = this.notification.is_inbox_issue;
|
this.is_inbox_issue = this.notification.is_inbox_issue;
|
||||||
|
this.is_mentioned_notification = this.notification.is_mentioned_notification;
|
||||||
|
this.workspace = this.notification.workspace;
|
||||||
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;
|
||||||
|
|
@ -132,8 +136,9 @@ export class Notification implements INotification {
|
||||||
read_at: this.read_at,
|
read_at: this.read_at,
|
||||||
archived_at: this.archived_at,
|
archived_at: this.archived_at,
|
||||||
snoozed_till: this.snoozed_till,
|
snoozed_till: this.snoozed_till,
|
||||||
workspace: this.workspace,
|
|
||||||
is_inbox_issue: this.is_inbox_issue,
|
is_inbox_issue: this.is_inbox_issue,
|
||||||
|
is_mentioned_notification: this.is_mentioned_notification,
|
||||||
|
workspace: this.workspace,
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,11 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||||
);
|
);
|
||||||
const workspaceNotificationIds = workspaceNotifications
|
const workspaceNotificationIds = workspaceNotifications
|
||||||
.filter((n) => n.workspace === workspaceId)
|
.filter((n) => n.workspace === workspaceId)
|
||||||
|
.filter((n) =>
|
||||||
|
this.currentNotificationTab === ENotificationTab.MENTIONS
|
||||||
|
? n.is_mentioned_notification
|
||||||
|
: !n.is_mentioned_notification
|
||||||
|
)
|
||||||
.filter((n) => {
|
.filter((n) => {
|
||||||
if (!this.filters.archived && !this.filters.snoozed) {
|
if (!this.filters.archived && !this.filters.snoozed) {
|
||||||
if (n.archived_at) {
|
if (n.archived_at) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue