Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a47f709c5d |
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.24 on 2026-01-10 18:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0115_auto_20260105_1406'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='notification_view_mode',
|
||||
field=models.CharField(choices=[('full', 'Full'), ('compact', 'Compact')], default='full', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -192,6 +192,10 @@ class Profile(TimeAuditModel):
|
||||
FRIDAY = 5
|
||||
SATURDAY = 6
|
||||
|
||||
class NotificationViewMode(models.TextChoices):
|
||||
FULL = "full", "Full"
|
||||
COMPACT = "compact", "Compact"
|
||||
|
||||
START_OF_THE_WEEK_CHOICES = (
|
||||
(SUNDAY, "Sunday"),
|
||||
(MONDAY, "Monday"),
|
||||
@@ -221,7 +225,9 @@ class Profile(TimeAuditModel):
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
company_name = models.CharField(max_length=255, blank=True)
|
||||
|
||||
notification_view_mode = models.CharField(
|
||||
max_length=255, choices=NotificationViewMode.choices, default=NotificationViewMode.FULL
|
||||
)
|
||||
is_smooth_cursor_enabled = models.BooleanField(default=False)
|
||||
# mobile
|
||||
is_mobile_onboarded = models.BooleanField(default=False)
|
||||
|
||||
@@ -20,7 +20,7 @@ type Props = {
|
||||
|
||||
const PEEK_MODES: {
|
||||
key: IPeekMode;
|
||||
icon: any;
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ key: "side", icon: SidePanelIcon, label: "Side Peek" },
|
||||
|
||||
@@ -2,8 +2,8 @@ import { set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import { UserService } from "@plane/services";
|
||||
import { NOTIFICATION_VIEW_MODES, EStartOfTheWeek } from "@plane/types";
|
||||
import type { TUserProfile } from "@plane/types";
|
||||
import { EStartOfTheWeek } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
@@ -44,6 +44,7 @@ export class ProfileStore implements IProfileStore {
|
||||
},
|
||||
is_onboarded: false,
|
||||
is_tour_completed: false,
|
||||
notification_view_mode: NOTIFICATION_VIEW_MODES[0].key,
|
||||
use_case: undefined,
|
||||
billing_address_country: undefined,
|
||||
billing_address: undefined,
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
// components
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { cn } from "@plane/utils";
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { HelpMenuRoot } from "@/components/workspace/sidebar/help-section/root";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||
import { useAppRailPreferences } from "@/hooks/use-navigation-preferences";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
||||
import { InboxIcon } from "@plane/propel/icons";
|
||||
import useSWR from "swr";
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
import { cn } from "@plane/utils";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// local imports
|
||||
import { StarUsOnGitHubLink } from "@/app/(all)/[workspaceSlug]/(projects)/star-us-link";
|
||||
import { NotificationsPopoverRoot } from "@/components/notifications/popover/root";
|
||||
|
||||
export const TopNavigationRoot = observer(function TopNavigationRoot() {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
// store hooks
|
||||
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||
const { preferences } = useAppRailPreferences();
|
||||
|
||||
const showLabel = preferences.displayMode === "icon_with_label";
|
||||
|
||||
// Fetch notification count
|
||||
useSWR(
|
||||
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
|
||||
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
// Calculate notification count
|
||||
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0;
|
||||
const totalNotifications = isMentionsEnabled
|
||||
? unreadNotificationsCount.mention_unread_notifications_count
|
||||
: unreadNotificationsCount.total_unread_notifications_count;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex items-center min-h-10 w-full px-3.5 bg-canvas z-[27] transition-all duration-300", {
|
||||
@@ -54,23 +35,7 @@ export const TopNavigationRoot = observer(function TopNavigationRoot() {
|
||||
</div>
|
||||
{/* Additional Actions */}
|
||||
<div className="shrink-0 flex-1 flex gap-1 items-center justify-end">
|
||||
<Tooltip tooltipContent="Inbox" position="bottom">
|
||||
<AppSidebarItem
|
||||
variant="link"
|
||||
item={{
|
||||
href: `/${workspaceSlug?.toString()}/notifications/`,
|
||||
icon: (
|
||||
<div className="relative">
|
||||
<InboxIcon className="size-5" />
|
||||
{totalNotifications > 0 && (
|
||||
<span className="absolute top-0 right-0 size-2 rounded-full bg-danger-primary" />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
isActive: pathname?.includes("/notifications/"),
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<NotificationsPopoverRoot workspaceSlug={workspaceSlug?.toString()} />
|
||||
<HelpMenuRoot />
|
||||
<StarUsOnGitHubLink />
|
||||
<div className="flex items-center justify-center size-8 hover:bg-layer-1-hover rounded-md">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NotificationCardListRoot } from "./notification-card/root";
|
||||
export type TNotificationListRoot = {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
onNotificationClick?: () => void;
|
||||
};
|
||||
|
||||
export function NotificationListRoot(props: TNotificationListRoot) {
|
||||
|
||||
@@ -11,10 +11,11 @@ import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
type TNotificationCardListRoot = {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
onNotificationClick?: () => void;
|
||||
};
|
||||
|
||||
export const NotificationCardListRoot = observer(function NotificationCardListRoot(props: TNotificationCardListRoot) {
|
||||
const { workspaceSlug, workspaceId } = props;
|
||||
const { workspaceSlug, workspaceId, onNotificationClick } = props;
|
||||
// hooks
|
||||
const { loader, paginationInfo, getNotifications, notificationIdsByWorkspaceId } = useWorkspaceNotifications();
|
||||
const notificationIds = notificationIdsByWorkspaceId(workspaceId);
|
||||
@@ -32,7 +33,12 @@ export const NotificationCardListRoot = observer(function NotificationCardListRo
|
||||
return (
|
||||
<div>
|
||||
{notificationIds.map((notificationId: string) => (
|
||||
<NotificationItem key={notificationId} workspaceSlug={workspaceSlug} notificationId={notificationId} />
|
||||
<NotificationItem
|
||||
key={notificationId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
notificationId={notificationId}
|
||||
onNotificationClick={onNotificationClick}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* fetch next page notifications */}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
* SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
*
|
||||
* Licensed under the Plane Commercial License (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* https://plane.so/legals/eula
|
||||
*
|
||||
* DO NOT remove or modify this notice.
|
||||
* NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
*/
|
||||
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
||||
import { NotificationsSidebarRoot } from "@/components/workspace-notifications/sidebar";
|
||||
import { Popover } from "@plane/propel/popover";
|
||||
|
||||
type NotificationsPopoverRootProps = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export function NotificationsPopoverRoot({ workspaceSlug }: NotificationsPopoverRootProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { unreadNotificationsCount, viewMode } = useWorkspaceNotifications();
|
||||
|
||||
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0;
|
||||
const totalNotifications = isMentionsEnabled
|
||||
? unreadNotificationsCount.mention_unread_notifications_count
|
||||
: unreadNotificationsCount.total_unread_notifications_count;
|
||||
const isNotificationsPath = pathname.includes(`/${workspaceSlug}/notifications/`);
|
||||
const shouldPopoverBeOpen = viewMode === "compact" && !isNotificationsPath && isOpen;
|
||||
|
||||
const handleSidebarClick = () => {
|
||||
if (viewMode === "full") {
|
||||
setIsOpen(false);
|
||||
router.push(`/${workspaceSlug}/notifications/`);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopoverChange = (open: boolean) => {
|
||||
if (!isNotificationsPath) {
|
||||
setIsOpen(open);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={shouldPopoverBeOpen} onOpenChange={handlePopoverChange}>
|
||||
<Popover.Button>
|
||||
<AppSidebarItem
|
||||
variant={"button"}
|
||||
item={{
|
||||
icon: (
|
||||
<div className="relative">
|
||||
<InboxIcon className="size-5" />
|
||||
{totalNotifications > 0 && (
|
||||
<span className="absolute top-0 right-0 size-2 rounded-full bg-danger-primary" />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
isActive: isOpen,
|
||||
onClick: handleSidebarClick,
|
||||
}}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel side="bottom" align="start" positionerClassName={"z-30"} className={"h-[477px] w-[530px]"}>
|
||||
<NotificationsSidebarRoot
|
||||
viewMode="compact"
|
||||
onFullViewMode={() => setIsOpen(false)}
|
||||
onNotificationClick={() => setIsOpen(false)}
|
||||
onModeChange={(mode) => {
|
||||
if (mode === "full" && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export const NotificationFilter = observer(function NotificationFilter() {
|
||||
data={translatedFilterTypeOptions}
|
||||
button={
|
||||
<Tooltip tooltipContent={t("notification.options.filters")} isMobile={isMobile} position="bottom">
|
||||
<IconButton size="base" variant="ghost" icon={ListFilter} />
|
||||
<IconButton size="base" variant="secondary" icon={ListFilter} />
|
||||
</Tooltip>
|
||||
}
|
||||
keyExtractor={(item: { label: string; value: ENotificationFilterType }) => item.value}
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export const NotificationHeaderMenuOption = observer(function NotificationHeader
|
||||
return (
|
||||
<PopoverMenu
|
||||
data={popoverMenuOptions}
|
||||
button={<IconButton size="base" variant="ghost" icon={MoreVertical} />}
|
||||
button={<IconButton size="base" variant="secondary" icon={MoreVertical} />}
|
||||
keyExtractor={(item: TPopoverMenuOptions) => item.key}
|
||||
panelClassName="p-0 py-2 rounded-md border border-subtle bg-surface-1 space-y-1"
|
||||
render={(item: TPopoverMenuOptions) => <NotificationMenuOptionItem {...item} />}
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { CheckCheck, RefreshCw } from "lucide-react";
|
||||
// plane imports
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local imports
|
||||
import { NotificationFilter } from "../../filters/menu";
|
||||
import { NotificationHeaderMenuOption } from "./menu-option";
|
||||
import { IconButton } from "@plane/propel/icon-button";
|
||||
|
||||
type TNotificationSidebarHeaderOptions = {
|
||||
workspaceSlug: string;
|
||||
@@ -20,56 +10,8 @@ type TNotificationSidebarHeaderOptions = {
|
||||
export const NotificationSidebarHeaderOptions = observer(function NotificationSidebarHeaderOptions(
|
||||
props: TNotificationSidebarHeaderOptions
|
||||
) {
|
||||
const { workspaceSlug } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { loader, getNotifications, markAllNotificationsAsRead } = useWorkspaceNotifications();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const refreshNotifications = async () => {
|
||||
if (loader) return;
|
||||
try {
|
||||
await getNotifications(workspaceSlug, ENotificationLoader.MUTATION_LOADER, ENotificationQueryParamType.CURRENT);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkAllNotificationsAsRead = async () => {
|
||||
// NOTE: We are using loader to prevent continues request when we are making all the notification to read
|
||||
if (loader) return;
|
||||
try {
|
||||
await markAllNotificationsAsRead(workspaceSlug);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex justify-center items-center gap-2 text-body-xs-medium">
|
||||
{/* mark all notifications as read*/}
|
||||
<Tooltip tooltipContent={t("notification.options.mark_all_as_read")} isMobile={isMobile} position="bottom">
|
||||
<IconButton
|
||||
size="base"
|
||||
variant="ghost"
|
||||
icon={loader === ENotificationLoader.MARK_ALL_AS_READY ? Spinner : CheckCheck}
|
||||
onClick={() => {
|
||||
handleMarkAllNotificationsAsRead();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* refetch current notifications */}
|
||||
<Tooltip tooltipContent={t("notification.options.refresh")} isMobile={isMobile} position="bottom">
|
||||
<IconButton
|
||||
size="base"
|
||||
variant="ghost"
|
||||
icon={RefreshCw}
|
||||
className={loader === ENotificationLoader.MUTATION_LOADER ? "animate-spin" : ""}
|
||||
onClick={refreshNotifications}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* notification filters */}
|
||||
<NotificationFilter />
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ import { NotificationOption } from "./options";
|
||||
type TNotificationItem = {
|
||||
workspaceSlug: string;
|
||||
notificationId: string;
|
||||
onNotificationClick?: () => void;
|
||||
};
|
||||
|
||||
export const NotificationItem = observer(function NotificationItem(props: TNotificationItem) {
|
||||
const { workspaceSlug, notificationId } = props;
|
||||
const { workspaceSlug, notificationId, onNotificationClick } = props;
|
||||
// hooks
|
||||
const { currentSelectedNotificationId, setCurrentSelectedNotificationId } = useWorkspaceNotifications();
|
||||
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
|
||||
@@ -39,6 +40,8 @@ export const NotificationItem = observer(function NotificationItem(props: TNotif
|
||||
|
||||
const handleNotificationIssuePeekOverview = async () => {
|
||||
if (workspaceSlug && projectId && issueId && !isSnoozeStateModalOpen && !customSnoozeModal) {
|
||||
onNotificationClick?.();
|
||||
|
||||
setPeekIssue(undefined);
|
||||
setCurrentSelectedNotificationId(notificationId);
|
||||
|
||||
|
||||
@@ -1,115 +1,198 @@
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
// plane imports
|
||||
import type { TNotificationTab } from "@plane/constants";
|
||||
import { NOTIFICATION_TABS } from "@plane/constants";
|
||||
import { ENotificationLoader, ENotificationQueryParamType, NOTIFICATION_TABS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui";
|
||||
import { ContentWrapper, ERowVariant, Spinner, Tooltip } from "@plane/ui";
|
||||
import { cn, getNumberCount } from "@plane/utils";
|
||||
import type { TNotificationsViewMode } from "@plane/types";
|
||||
// components
|
||||
import { CountChip } from "@/components/common/count-chip";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
// plane web components
|
||||
import { NotificationListRoot } from "@/plane-web/components/workspace-notifications/list-root";
|
||||
// local imports
|
||||
|
||||
import { getIconButtonStyling, IconButton } from "@plane/propel/icon-button";
|
||||
import { Tabs } from "@plane/propel/tabs";
|
||||
import { CheckCheck, MoveDiagonal, RefreshCw } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { NotificationEmptyState } from "./empty-state";
|
||||
import { AppliedFilters } from "./filters/applied-filter";
|
||||
import { NotificationSidebarHeader } from "./header";
|
||||
import { NotificationSidebarHeaderOptions } from "./header/options";
|
||||
import { NotificationsLoader } from "./loader";
|
||||
import { ViewModeSelector } from "./view-mode-selector";
|
||||
import { usePlatformOS } from "@plane/hooks";
|
||||
|
||||
export const NotificationsSidebarRoot = observer(function NotificationsSidebarRoot() {
|
||||
type NotificationsSidebarRootProps = {
|
||||
viewMode?: TNotificationsViewMode;
|
||||
onFullViewMode?: () => void;
|
||||
onNotificationClick?: () => void;
|
||||
onModeChange?: (mode: TNotificationsViewMode) => void;
|
||||
};
|
||||
|
||||
const getSidebarWidthClass = (viewMode: TNotificationsViewMode, hasSelection: boolean) => {
|
||||
if (viewMode === "compact") {
|
||||
return "w-full md:w-full rounded-xl border border-subtle shadow-raised-200";
|
||||
}
|
||||
|
||||
return hasSelection ? "w-0 md:w-3/12" : "w-full md:w-3/12";
|
||||
};
|
||||
|
||||
export const NotificationsSidebarRoot = observer(function NotificationsSidebarRoot({
|
||||
viewMode = "full",
|
||||
onFullViewMode,
|
||||
onNotificationClick,
|
||||
onModeChange,
|
||||
}: NotificationsSidebarRootProps) {
|
||||
const { workspaceSlug } = useParams();
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const notifications = useWorkspaceNotifications();
|
||||
const router = useRouter();
|
||||
if (!workspaceSlug) return null;
|
||||
|
||||
const workspace = getWorkspaceBySlug(workspaceSlug.toString());
|
||||
if (!workspace) return null;
|
||||
|
||||
const {
|
||||
currentSelectedNotificationId,
|
||||
unreadNotificationsCount,
|
||||
loader,
|
||||
notificationIdsByWorkspaceId,
|
||||
currentNotificationTab,
|
||||
setCurrentNotificationTab,
|
||||
} = useWorkspaceNotifications();
|
||||
setViewMode,
|
||||
viewMode: currentViewMode,
|
||||
loader,
|
||||
getNotifications,
|
||||
markAllNotificationsAsRead,
|
||||
} = notifications;
|
||||
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
|
||||
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined;
|
||||
const notificationIds = notificationIdsByWorkspaceId(workspace.id);
|
||||
const sidebarWidthClass = getSidebarWidthClass(viewMode, Boolean(currentSelectedNotificationId));
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tabValue: TNotificationTab) => {
|
||||
if (currentNotificationTab !== tabValue) {
|
||||
setCurrentNotificationTab(tabValue);
|
||||
}
|
||||
},
|
||||
[currentNotificationTab, setCurrentNotificationTab]
|
||||
);
|
||||
const refreshNotifications = async () => {
|
||||
if (loader) return;
|
||||
try {
|
||||
await getNotifications(workspaceSlug, ENotificationLoader.MUTATION_LOADER, ENotificationQueryParamType.CURRENT);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !workspace) return <></>;
|
||||
const handleMarkAllNotificationsAsRead = async () => {
|
||||
// NOTE: We are using loader to prevent continues request when we are making all the notification to read
|
||||
if (loader) return;
|
||||
try {
|
||||
await markAllNotificationsAsRead(workspaceSlug);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-0 md:border-r border-subtle z-[10] flex-shrink-0 bg-surface-1 h-full transition-all max-md:overflow-hidden",
|
||||
currentSelectedNotificationId ? "w-0 md:w-3/12" : "w-full md:w-3/12"
|
||||
"relative flex-shrink-0 bg-surface-1 h-full transition-all md:border-r border-subtle max-md:overflow-hidden",
|
||||
sidebarWidthClass
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-full flex flex-col">
|
||||
<Row className="h-header border-b border-subtle flex flex-shrink-0">
|
||||
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
||||
</Row>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="px-3 py-2 border-b border-subtle flex items-center justify-between mb-2">
|
||||
<h4 className="text-18 font-medium">{t("notification.label")}</h4>
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip tooltipContent={t("notification.options.mark_all_as_read")} isMobile={isMobile} position="bottom">
|
||||
<IconButton
|
||||
size="base"
|
||||
variant="ghost"
|
||||
icon={loader === ENotificationLoader.MARK_ALL_AS_READY ? Spinner : CheckCheck}
|
||||
onClick={() => {
|
||||
handleMarkAllNotificationsAsRead();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Header variant={EHeaderVariant.SECONDARY} className="justify-start">
|
||||
{NOTIFICATION_TABS.map((tab) => (
|
||||
<div
|
||||
key={tab.value}
|
||||
className="h-full px-3 relative cursor-pointer"
|
||||
onClick={() => handleTabClick(tab.value)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full flex justify-center items-center gap-1 text-body-xs-medium transition-all",
|
||||
{
|
||||
"text-accent-primary": currentNotificationTab === tab.value,
|
||||
"text-primary hover:text-secondary": currentNotificationTab !== tab.value,
|
||||
}
|
||||
)}
|
||||
{/* refetch current notifications */}
|
||||
<Tooltip tooltipContent={t("notification.options.refresh")} isMobile={isMobile} position="bottom">
|
||||
<IconButton
|
||||
size="base"
|
||||
variant="ghost"
|
||||
icon={RefreshCw}
|
||||
className={loader === ENotificationLoader.MUTATION_LOADER ? "animate-spin" : ""}
|
||||
onClick={refreshNotifications}
|
||||
/>
|
||||
</Tooltip>
|
||||
{viewMode === "compact" && (
|
||||
<Link
|
||||
to={`/${workspaceSlug}/notifications/`}
|
||||
className={getIconButtonStyling("ghost", "base")}
|
||||
onClick={onFullViewMode}
|
||||
>
|
||||
<div className="font-medium">{t(tab.i18n_label)}</div>
|
||||
{tab.count(unreadNotificationsCount) > 0 && (
|
||||
<CountChip count={getNumberCount(tab.count(unreadNotificationsCount))} />
|
||||
)}
|
||||
</div>
|
||||
{currentNotificationTab === tab.value && (
|
||||
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-accent-strong" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Header>
|
||||
<MoveDiagonal size={16} />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* applied filters */}
|
||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||
|
||||
{/* rendering notifications */}
|
||||
{loader === "init-loader" ? (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<NotificationsLoader />
|
||||
<ViewModeSelector
|
||||
value={currentViewMode}
|
||||
onChange={(mode) => {
|
||||
setViewMode(mode);
|
||||
if (mode === "full") {
|
||||
router.push(`/${workspaceSlug}/notifications/`);
|
||||
}
|
||||
onModeChange?.(mode);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{notificationIds && notificationIds.length > 0 ? (
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultValue={currentNotificationTab}
|
||||
onValueChange={setCurrentNotificationTab}
|
||||
className={"overflow-y-hidden flex-1"}
|
||||
>
|
||||
<div className="flex items-center justify-between mx-3">
|
||||
<Tabs.List className="w-fit">
|
||||
{NOTIFICATION_TABS.map((tab) => (
|
||||
<Tabs.Trigger key={tab.value} value={tab.value} size="sm">
|
||||
<div className="flex items-center gap-1.5 px-1">
|
||||
{t(tab.i18n_label)}
|
||||
{tab.count(unreadNotificationsCount) > 0 && (
|
||||
<span>{getNumberCount(tab.count(unreadNotificationsCount))}</span>
|
||||
)}
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
|
||||
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug.toString()} />
|
||||
</div>
|
||||
|
||||
<Tabs.Content value={currentNotificationTab} className="py-2 overflow-y-auto flex-1">
|
||||
{loader === "init-loader" ? (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<NotificationsLoader />
|
||||
</div>
|
||||
) : notificationIds && notificationIds.length > 0 ? (
|
||||
<ContentWrapper variant={ERowVariant.HUGGING}>
|
||||
<NotificationListRoot workspaceSlug={workspaceSlug.toString()} workspaceId={workspace?.id} />
|
||||
<NotificationListRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
workspaceId={workspace.id}
|
||||
onNotificationClick={onNotificationClick}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
) : (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
<NotificationEmptyState currentNotificationTab={currentNotificationTab} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
|
||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { CenterPanelIcon, FullScreenPanelIcon } from "@plane/propel/icons";
|
||||
import type { TNotificationsViewMode } from "@/store/notifications/workspace-notifications.store";
|
||||
import { Menu } from "@plane/propel/menu";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
||||
const VIEW_MODES = [
|
||||
{ key: "compact", icon: CenterPanelIcon, i18n_label: "notifications.compact" },
|
||||
{ key: "full", icon: FullScreenPanelIcon, i18n_label: "notifications.full" },
|
||||
] as const;
|
||||
|
||||
type ViewModeSelectorProps = {
|
||||
value: TNotificationsViewMode;
|
||||
onChange: (mode: TNotificationsViewMode) => void;
|
||||
};
|
||||
|
||||
export function ViewModeSelector({ value, onChange }: ViewModeSelectorProps) {
|
||||
const CurrentIcon = VIEW_MODES.find((m) => m.key === value)?.icon;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu
|
||||
ariaLabel={t("notifications.select_default_view")}
|
||||
customButton={
|
||||
<span className="flex items-center justify-center">
|
||||
{CurrentIcon && <CurrentIcon className="h-4 w-4 text-tertiary hover:text-secondary" />}
|
||||
</span>
|
||||
}
|
||||
optionsClassName="p-1"
|
||||
>
|
||||
<div className="text-tertiary text-12 px-2 py-1">{t("notifications.select_default_view")}</div>
|
||||
{VIEW_MODES.map(({ key, icon: Icon, i18n_label }) => {
|
||||
const selected = key === value;
|
||||
return (
|
||||
<Menu.MenuItem key={key} onClick={() => onChange(key)}>
|
||||
<div className="flex items-center justify-between w-full px-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon className="h-4 w-4" />
|
||||
{t(i18n_label)}
|
||||
</div>
|
||||
{selected && <CheckIcon className="h-4 w-4" />}
|
||||
</div>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { orderBy, isEmpty, update, set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import type { TNotificationTab } from "@plane/constants";
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
TNotificationLite,
|
||||
TNotificationPaginatedInfo,
|
||||
TNotificationPaginatedInfoQueryParams,
|
||||
TNotificationsViewMode,
|
||||
TUnreadNotificationsCount,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
@@ -24,6 +25,8 @@ import type { CoreRootStore } from "@/store/root.store";
|
||||
type TNotificationLoader = ENotificationLoader | undefined;
|
||||
type TNotificationQueryParamType = ENotificationQueryParamType;
|
||||
|
||||
export type TGroupedNotifications = Record<string, TNotification[]>;
|
||||
|
||||
export interface IWorkspaceNotificationStore {
|
||||
// observables
|
||||
loader: TNotificationLoader;
|
||||
@@ -34,6 +37,7 @@ export interface IWorkspaceNotificationStore {
|
||||
paginationInfo: Omit<TNotificationPaginatedInfo, "results"> | undefined;
|
||||
filters: TNotificationFilter;
|
||||
// computed
|
||||
viewMode: TNotificationsViewMode;
|
||||
// computed functions
|
||||
notificationIdsByWorkspaceId: (workspaceId: string) => string[] | undefined;
|
||||
notificationLiteByNotificationId: (notificationId: string | undefined) => TNotificationLite;
|
||||
@@ -52,6 +56,7 @@ export interface IWorkspaceNotificationStore {
|
||||
queryCursorType?: TNotificationQueryParamType
|
||||
) => Promise<TNotificationPaginatedInfo | undefined>;
|
||||
markAllNotificationsAsRead: (workspaceId: string) => Promise<void>;
|
||||
setViewMode: (viewMode: TNotificationsViewMode) => void;
|
||||
}
|
||||
|
||||
export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||
@@ -89,6 +94,7 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||
paginationInfo: observable,
|
||||
filters: observable,
|
||||
// computed
|
||||
viewMode: computed,
|
||||
// helper actions
|
||||
setCurrentNotificationTab: action,
|
||||
setCurrentSelectedNotificationId: action,
|
||||
@@ -100,10 +106,21 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||
getUnreadNotificationsCount: action,
|
||||
getNotifications: action,
|
||||
markAllNotificationsAsRead: action,
|
||||
setViewMode: action,
|
||||
});
|
||||
}
|
||||
|
||||
setViewMode = (viewMode: TNotificationsViewMode): void => {
|
||||
if (this.store.user.userProfile.data) {
|
||||
this.store.user.userProfile.data.notification_view_mode = viewMode;
|
||||
}
|
||||
this.store.user.userProfile.updateUserProfile({ notification_view_mode: viewMode });
|
||||
};
|
||||
|
||||
// computed
|
||||
get viewMode(): TNotificationsViewMode {
|
||||
return this.store.user.userProfile.data?.notification_view_mode || "full";
|
||||
}
|
||||
|
||||
// computed functions
|
||||
/**
|
||||
|
||||
@@ -53,6 +53,13 @@ export interface IUserAccount {
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export const NOTIFICATION_VIEW_MODES = [
|
||||
{ key: "full", label: "Full" },
|
||||
{ key: "compact", label: "Compact" },
|
||||
] as const;
|
||||
|
||||
export type TNotificationsViewMode = (typeof NOTIFICATION_VIEW_MODES)[number]["key"];
|
||||
|
||||
export type TUserProfile = {
|
||||
id: string | undefined;
|
||||
user: string | undefined;
|
||||
@@ -76,6 +83,7 @@ export type TUserProfile = {
|
||||
created_at: Date | string;
|
||||
updated_at: Date | string;
|
||||
start_of_the_week: EStartOfTheWeek;
|
||||
notification_view_mode: TNotificationsViewMode;
|
||||
};
|
||||
|
||||
export interface IInstanceAdminStatus {
|
||||
|
||||
Reference in New Issue
Block a user