Compare commits

...

1 Commits

16 changed files with 364 additions and 177 deletions
@@ -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),
),
]
+7 -1
View File
@@ -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 -1
View File
@@ -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}
@@ -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
/**
+8
View File
@@ -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 {