Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { getNumberCount } from "@plane/utils";
|
||||
// components
|
||||
import { CountChip } from "@/components/common/count-chip";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
|
||||
type TNotificationAppSidebarOption = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
// hooks
|
||||
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
|
||||
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null
|
||||
);
|
||||
|
||||
// derived values
|
||||
const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0 ? true : false;
|
||||
const totalNotifications = isMentionsEnabled
|
||||
? unreadNotificationsCount.mention_unread_notifications_count
|
||||
: unreadNotificationsCount.total_unread_notifications_count;
|
||||
|
||||
if (totalNotifications <= 0) return <></>;
|
||||
|
||||
return (
|
||||
<div className="ml-auto">
|
||||
<CountChip count={`${isMentionsEnabled ? `@ ` : ``}${getNumberCount(totalNotifications)}`} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
118
apps/web/core/components/workspace-notifications/root.tsx
Normal file
118
apps/web/core/components/workspace-notifications/root.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
// plane web imports
|
||||
import { useNotificationPreview } from "@/plane-web/hooks/use-notification-preview";
|
||||
// local imports
|
||||
import { InboxContentRoot } from "../inbox/content";
|
||||
|
||||
type NotificationsRootProps = {
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
export const NotificationsRoot = observer(({ workspaceSlug }: NotificationsRootProps) => {
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
currentSelectedNotificationId,
|
||||
setCurrentSelectedNotificationId,
|
||||
notificationLiteByNotificationId,
|
||||
notificationIdsByWorkspaceId,
|
||||
getNotifications,
|
||||
} = useWorkspaceNotifications();
|
||||
const { fetchUserProjectInfo } = useUserPermissions();
|
||||
const { isWorkItem, PeekOverviewComponent, setPeekWorkItem } = useNotificationPreview();
|
||||
// derived values
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||
notificationLiteByNotificationId(currentSelectedNotificationId);
|
||||
|
||||
// fetching workspace work item properties
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
|
||||
// fetch workspace notifications
|
||||
const notificationMutation =
|
||||
currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
|
||||
? ENotificationLoader.MUTATION_LOADER
|
||||
: ENotificationLoader.INIT_LOADER;
|
||||
const notificationLoader =
|
||||
currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
|
||||
? ENotificationQueryParamType.CURRENT
|
||||
: ENotificationQueryParamType.INIT;
|
||||
useSWR(
|
||||
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION_${currentWorkspace?.slug}` : null,
|
||||
currentWorkspace?.slug
|
||||
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
|
||||
: 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
|
||||
);
|
||||
|
||||
const embedRemoveCurrentNotification = useCallback(
|
||||
() => setCurrentSelectedNotificationId(undefined),
|
||||
[setCurrentSelectedNotificationId]
|
||||
);
|
||||
|
||||
// clearing up the selected notifications when unmounting the page
|
||||
useEffect(
|
||||
() => () => {
|
||||
setPeekWorkItem(undefined);
|
||||
},
|
||||
[setCurrentSelectedNotificationId, setPeekWorkItem]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("w-full h-full overflow-hidden ", isWorkItem && "overflow-y-auto")}>
|
||||
{!currentSelectedNotificationId ? (
|
||||
<div className="flex justify-center items-center size-full">
|
||||
<EmptyStateCompact assetKey="unknown" assetClassName="size-20" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{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
|
||||
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<PeekOverviewComponent embedIssue embedRemoveCurrentNotification={embedRemoveCurrentNotification} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { ENotificationTab } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
|
||||
type TNotificationEmptyStateProps = {
|
||||
currentNotificationTab: ENotificationTab;
|
||||
};
|
||||
|
||||
export const NotificationEmptyState: FC<TNotificationEmptyStateProps> = observer(({ currentNotificationTab }) => {
|
||||
// plane imports
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<EmptyStateCompact
|
||||
assetKey="inbox"
|
||||
assetClassName="size-24"
|
||||
title={
|
||||
currentNotificationTab === ENotificationTab.ALL
|
||||
? t("workspace_empty_state.inbox_sidebar_all.title")
|
||||
: t("workspace_empty_state.inbox_sidebar_mentions.title")
|
||||
}
|
||||
className="max-w-56"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import { Header, EHeaderVariant, Tag } from "@plane/ui";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
|
||||
type TAppliedFilters = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const AppliedFilters: FC<TAppliedFilters> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
// hooks
|
||||
const { filters, updateFilters } = useWorkspaceNotifications();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const isFiltersEnabled = Object.entries(filters.type || {}).some(([, value]) => value);
|
||||
|
||||
const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
|
||||
updateFilters("type", {
|
||||
...filters.type,
|
||||
[filterType]: filterValue,
|
||||
});
|
||||
|
||||
const handleClearFilters = () => {
|
||||
updateFilters("type", {
|
||||
[ENotificationFilterType.ASSIGNED]: false,
|
||||
[ENotificationFilterType.CREATED]: false,
|
||||
[ENotificationFilterType.SUBSCRIBED]: false,
|
||||
});
|
||||
};
|
||||
|
||||
if (!isFiltersEnabled || !workspaceSlug) return <></>;
|
||||
return (
|
||||
<Header variant={EHeaderVariant.TERNARY}>
|
||||
<Header.LeftItem className="w-full">
|
||||
{FILTER_TYPE_OPTIONS.map((filter) => {
|
||||
const isSelected = filters?.type?.[filter?.value] || false;
|
||||
if (!isSelected) return <></>;
|
||||
return (
|
||||
<Tag
|
||||
key={filter.value}
|
||||
className="flex flex-wrap flex-start"
|
||||
onClick={() => handleFilterTypeChange(filter?.value, !isSelected)}
|
||||
>
|
||||
<div className="whitespace-nowrap text-custom-text-200">{t(filter.i18n_label)}</div>
|
||||
<div className="w-4 h-4 flex justify-center items-center transition-all rounded-sm text-custom-text-200 hover:text-custom-text-100">
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</div>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
<button type="button" onClick={handleClearFilters}>
|
||||
<Tag>
|
||||
{t("common.clear_all")}
|
||||
<CloseIcon height={12} width={12} strokeWidth={2} />
|
||||
</Tag>
|
||||
</button>
|
||||
</Header.LeftItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// plane imports
|
||||
import type { ENotificationFilterType } from "@plane/constants";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
|
||||
export const NotificationFilterOptionItem: FC<{ label: string; value: ENotificationFilterType }> = observer((props) => {
|
||||
const { value, label } = props;
|
||||
// hooks
|
||||
const { filters, updateFilters } = useWorkspaceNotifications();
|
||||
|
||||
const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
|
||||
updateFilters("type", {
|
||||
...filters.type,
|
||||
[filterType]: filterValue,
|
||||
});
|
||||
|
||||
// derived values
|
||||
const isSelected = filters?.type?.[value] || false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value}
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() => handleFilterTypeChange(value, !isSelected)}
|
||||
>
|
||||
<div
|
||||
className={cn("flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all", {
|
||||
"bg-custom-primary text-white": isSelected,
|
||||
"bg-custom-background-90": !isSelected,
|
||||
})}
|
||||
>
|
||||
{isSelected && <Check className="h-2.5 w-2.5" />}
|
||||
</div>
|
||||
<div className={cn("whitespace-nowrap text-sm", isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
// plane imports
|
||||
import type { ENotificationFilterType } from "@plane/constants";
|
||||
import { FILTER_TYPE_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { PopoverMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local imports
|
||||
import { NotificationFilterOptionItem } from "./menu-option-item";
|
||||
|
||||
export const NotificationFilter: FC = observer(() => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const translatedFilterTypeOptions = FILTER_TYPE_OPTIONS.map((filter) => ({
|
||||
...filter,
|
||||
label: t(filter.i18n_label),
|
||||
}));
|
||||
|
||||
return (
|
||||
<PopoverMenu
|
||||
data={translatedFilterTypeOptions}
|
||||
button={
|
||||
<Tooltip tooltipContent={t("notification.options.filters")} isMobile={isMobile} position="bottom">
|
||||
<div className="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none">
|
||||
<ListFilter className="h-3 w-3" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
keyExtractor={(item: { label: string; value: ENotificationFilterType }) => item.value}
|
||||
render={(item) => <NotificationFilterOptionItem {...item} />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
// local imports
|
||||
import type { TPopoverMenuOptions } from "./root";
|
||||
|
||||
export const NotificationMenuOptionItem: FC<TPopoverMenuOptions> = observer((props) => {
|
||||
const { type, label = "", isActive, prependIcon, appendIcon, onClick } = props;
|
||||
|
||||
if (type === "menu-item")
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer mx-2 px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() => onClick && onClick()}
|
||||
>
|
||||
{prependIcon && prependIcon}
|
||||
<div className={cn("whitespace-nowrap text-sm", isActive ? "text-custom-text-100" : "text-custom-text-200")}>
|
||||
{label}
|
||||
</div>
|
||||
{appendIcon && <div className="ml-auto">{appendIcon}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div className="border-b border-custom-border-200" />;
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, CheckCircle, Clock } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ArchiveIcon } from "@plane/propel/icons";
|
||||
import type { TNotificationFilter } from "@plane/types";
|
||||
import { PopoverMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
// local imports
|
||||
import { NotificationMenuOptionItem } from "./menu-item";
|
||||
|
||||
export type TPopoverMenuOptions = {
|
||||
key: string;
|
||||
type: string;
|
||||
label?: string | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
prependIcon?: ReactNode | undefined;
|
||||
appendIcon?: ReactNode | undefined;
|
||||
onClick?: (() => void) | undefined;
|
||||
};
|
||||
|
||||
export const NotificationHeaderMenuOption = observer(() => {
|
||||
// hooks
|
||||
const { filters, updateFilters, updateBulkFilters } = useWorkspaceNotifications();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFilterChange = (filterType: keyof TNotificationFilter, filterValue: boolean) =>
|
||||
updateFilters(filterType, filterValue);
|
||||
|
||||
const handleBulkFilterChange = (filter: Partial<TNotificationFilter>) => updateBulkFilters(filter);
|
||||
|
||||
const popoverMenuOptions: TPopoverMenuOptions[] = [
|
||||
{
|
||||
key: "menu-unread",
|
||||
type: "menu-item",
|
||||
label: t("notification.options.show_unread"),
|
||||
isActive: filters?.read,
|
||||
prependIcon: <CheckCircle className="flex-shrink-0 h-3 w-3" />,
|
||||
appendIcon: filters?.read ? <Check className="w-3 h-3" /> : undefined,
|
||||
onClick: () => handleFilterChange("read", !filters?.read),
|
||||
},
|
||||
{
|
||||
key: "menu-archived",
|
||||
type: "menu-item",
|
||||
label: t("notification.options.show_archived"),
|
||||
isActive: filters?.archived,
|
||||
prependIcon: <ArchiveIcon className="flex-shrink-0 h-3 w-3" />,
|
||||
appendIcon: filters?.archived ? <Check className="w-3 h-3" /> : undefined,
|
||||
onClick: () =>
|
||||
handleBulkFilterChange({
|
||||
archived: !filters?.archived,
|
||||
snoozed: false,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "menu-snoozed",
|
||||
type: "menu-item",
|
||||
label: t("notification.options.show_snoozed"),
|
||||
isActive: filters?.snoozed,
|
||||
prependIcon: <Clock className="flex-shrink-0 h-3 w-3" />,
|
||||
appendIcon: filters?.snoozed ? <Check className="w-3 h-3" /> : undefined,
|
||||
onClick: () =>
|
||||
handleBulkFilterChange({
|
||||
snoozed: !filters?.snoozed,
|
||||
archived: false,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PopoverMenu
|
||||
data={popoverMenuOptions}
|
||||
buttonClassName="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 bg-custom-background-100 rounded-sm outline-none"
|
||||
keyExtractor={(item: TPopoverMenuOptions) => item.key}
|
||||
panelClassName="p-0 py-2 rounded-md border border-custom-border-200 bg-custom-background-100 space-y-1"
|
||||
render={(item: TPopoverMenuOptions) => <NotificationMenuOptionItem {...item} />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CheckCheck, RefreshCw } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
ENotificationLoader,
|
||||
ENotificationQueryParamType,
|
||||
NOTIFICATION_TRACKER_ELEMENTS,
|
||||
NOTIFICATION_TRACKER_EVENTS,
|
||||
} from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// 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";
|
||||
|
||||
type TNotificationSidebarHeaderOptions = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const NotificationSidebarHeaderOptions: FC<TNotificationSidebarHeaderOptions> = observer((props) => {
|
||||
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-sm">
|
||||
{/* mark all notifications as read*/}
|
||||
<Tooltip tooltipContent={t("notification.options.mark_all_as_read")} isMobile={isMobile} position="bottom">
|
||||
<div
|
||||
className="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
data-ph-element={NOTIFICATION_TRACKER_ELEMENTS.MARK_ALL_AS_READ_BUTTON}
|
||||
onClick={() => {
|
||||
captureSuccess({
|
||||
eventName: NOTIFICATION_TRACKER_EVENTS.all_marked_read,
|
||||
});
|
||||
handleMarkAllNotificationsAsRead();
|
||||
}}
|
||||
>
|
||||
{loader === ENotificationLoader.MARK_ALL_AS_READY ? (
|
||||
<Spinner height="14px" width="14px" />
|
||||
) : (
|
||||
<CheckCheck className="h-3 w-3" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{/* refetch current notifications */}
|
||||
<Tooltip tooltipContent={t("notification.options.refresh")} isMobile={isMobile} position="bottom">
|
||||
<div
|
||||
className="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={refreshNotifications}
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 ${loader === ENotificationLoader.MUTATION_LOADER ? "animate-spin" : ""}`} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{/* notification filters */}
|
||||
<NotificationFilter />
|
||||
|
||||
{/* notification menu options */}
|
||||
<NotificationHeaderMenuOption />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { InboxIcon } from "@plane/propel/icons";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { SidebarHamburgerToggle } from "@/components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
// local imports
|
||||
import { NotificationSidebarHeaderOptions } from "./options";
|
||||
|
||||
type TNotificationSidebarHeader = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
|
||||
if (!workspaceSlug) return <></>;
|
||||
return (
|
||||
<Header className="my-auto bg-custom-background-100">
|
||||
<Header.LeftItem>
|
||||
{sidebarCollapsed && <SidebarHamburgerToggle />}
|
||||
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={t("notification.label")}
|
||||
icon={<InboxIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
disableTooltip
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug} />
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,18 @@
|
||||
import { range } from "lodash-es";
|
||||
|
||||
export const NotificationsLoader = () => (
|
||||
<div className="divide-y divide-custom-border-100 animate-pulse overflow-hidden">
|
||||
{range(8).map((i) => (
|
||||
<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" />
|
||||
<div className="flex flex-col gap-2.5 w-full">
|
||||
<span className="h-5 w-36 bg-custom-background-80 rounded" />
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<span className="h-5 w-28 bg-custom-background-80 rounded" />
|
||||
<span className="h-5 w-16 bg-custom-background-80 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { FC } from "react";
|
||||
// plane imports
|
||||
import type { TNotification } from "@plane/types";
|
||||
import {
|
||||
convertMinutesToHoursMinutesString,
|
||||
renderFormattedDate,
|
||||
sanitizeCommentForNotification,
|
||||
replaceUnderscoreIfSnakeCase,
|
||||
stripAndTruncateHTML,
|
||||
} from "@plane/utils";
|
||||
// components
|
||||
import { LiteTextEditor } from "@/components/editor/lite-text";
|
||||
|
||||
export const NotificationContent: FC<{
|
||||
notification: TNotification;
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
renderCommentBox?: boolean;
|
||||
}> = ({ notification, workspaceId, workspaceSlug, projectId, renderCommentBox = false }) => {
|
||||
const { data, triggered_by_details: triggeredBy } = notification;
|
||||
const notificationField = data?.issue_activity.field;
|
||||
const newValue = data?.issue_activity.new_value;
|
||||
const oldValue = data?.issue_activity.old_value;
|
||||
const verb = data?.issue_activity.verb;
|
||||
|
||||
const renderTriggerName = () => (
|
||||
<span className="text-custom-text-100 font-medium">
|
||||
{triggeredBy?.is_bot ? triggeredBy.first_name : triggeredBy?.display_name}{" "}
|
||||
</span>
|
||||
);
|
||||
|
||||
const renderAction = () => {
|
||||
if (!notificationField) return "";
|
||||
if (notificationField === "duplicate")
|
||||
return verb === "created"
|
||||
? "marked that this work item is a duplicate of"
|
||||
: "marked that this work item is not a duplicate";
|
||||
if (notificationField === "assignees") {
|
||||
return newValue !== "" ? "added assignee" : "removed assignee";
|
||||
}
|
||||
if (notificationField === "start_date") {
|
||||
return newValue !== "" ? "set start date" : "removed the start date";
|
||||
}
|
||||
if (notificationField === "target_date") {
|
||||
return newValue !== "" ? "set due date" : "removed the due date";
|
||||
}
|
||||
if (notificationField === "labels") {
|
||||
return newValue !== "" ? "added label" : "removed label";
|
||||
}
|
||||
if (notificationField === "parent") {
|
||||
return newValue !== "" ? "added parent" : "removed parent";
|
||||
}
|
||||
if (notificationField === "relates_to") return "marked that this work item is related to";
|
||||
if (notificationField === "comment") return "commented";
|
||||
if (notificationField === "archived_at") {
|
||||
return newValue === "restore" ? "restored the work item" : "archived the work item";
|
||||
}
|
||||
if (notificationField === "None") return null;
|
||||
|
||||
const baseAction = !["comment", "archived_at"].includes(notificationField) ? verb : "";
|
||||
return `${baseAction} ${replaceUnderscoreIfSnakeCase(notificationField)}`;
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
if (notificationField === "None") return "the work item and assigned it to you.";
|
||||
if (notificationField === "comment") return renderCommentBox ? null : sanitizeCommentForNotification(newValue);
|
||||
if (notificationField === "target_date" || notificationField === "start_date") return renderFormattedDate(newValue);
|
||||
if (notificationField === "attachment") return "the work item";
|
||||
if (notificationField === "description") return stripAndTruncateHTML(newValue || "", 55);
|
||||
if (notificationField === "archived_at") return null;
|
||||
if (notificationField === "assignees") return newValue !== "" ? newValue : oldValue;
|
||||
if (notificationField === "labels") return newValue !== "" ? newValue : oldValue;
|
||||
if (notificationField === "parent") return newValue !== "" ? newValue : oldValue;
|
||||
if (notificationField === "estimate_time")
|
||||
return newValue !== ""
|
||||
? convertMinutesToHoursMinutesString(Number(newValue))
|
||||
: convertMinutesToHoursMinutesString(Number(oldValue));
|
||||
return newValue;
|
||||
};
|
||||
|
||||
const shouldShowConnector = ![
|
||||
"comment",
|
||||
"archived_at",
|
||||
"None",
|
||||
"assignees",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"parent",
|
||||
].includes(notificationField || "");
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderTriggerName()}
|
||||
<span className="text-custom-text-300">{renderAction()} </span>
|
||||
{verb !== "deleted" && (
|
||||
<>
|
||||
{shouldShowConnector && <span className="text-custom-text-300">to </span>}
|
||||
<span className="text-custom-text-100 font-medium">{renderValue()}</span>
|
||||
{notificationField === "comment" && renderCommentBox && (
|
||||
<div className="scale-75 origin-left">
|
||||
<LiteTextEditor
|
||||
editable={false}
|
||||
id=""
|
||||
initialValue={newValue ?? ""}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
displayConfig={{
|
||||
fontSize: "small-font",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{"."}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Clock } from "lucide-react";
|
||||
// plane imports
|
||||
import { Avatar, Row } from "@plane/ui";
|
||||
import { cn, calculateTimeAgo, renderFormattedDate, renderFormattedTime, getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
import { useNotification } from "@/hooks/store/notifications/use-notification";
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
// local imports
|
||||
import { NotificationContent } from "./content";
|
||||
import { NotificationOption } from "./options";
|
||||
|
||||
type TNotificationItem = {
|
||||
workspaceSlug: string;
|
||||
notificationId: string;
|
||||
};
|
||||
|
||||
export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
||||
const { workspaceSlug, notificationId } = props;
|
||||
// hooks
|
||||
const { currentSelectedNotificationId, setCurrentSelectedNotificationId } = useWorkspaceNotifications();
|
||||
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
|
||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// states
|
||||
const [isSnoozeStateModalOpen, setIsSnoozeStateModalOpen] = useState(false);
|
||||
const [customSnoozeModal, setCustomSnoozeModal] = useState(false);
|
||||
|
||||
// derived values
|
||||
const projectId = notification?.project || undefined;
|
||||
const issueId = notification?.data?.issue?.id || undefined;
|
||||
const workspace = getWorkspaceBySlug(workspaceSlug);
|
||||
|
||||
const notificationField = notification?.data?.issue_activity.field || undefined;
|
||||
const notificationTriggeredBy = notification.triggered_by_details || undefined;
|
||||
|
||||
const handleNotificationIssuePeekOverview = async () => {
|
||||
if (workspaceSlug && projectId && issueId && !isSnoozeStateModalOpen && !customSnoozeModal) {
|
||||
setPeekIssue(undefined);
|
||||
setCurrentSelectedNotificationId(notificationId);
|
||||
|
||||
// make the notification as read
|
||||
if (notification.read_at === null) {
|
||||
try {
|
||||
await markNotificationAsRead(workspaceSlug);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (notification?.is_inbox_issue === false) {
|
||||
!getIsIssuePeeked(issueId) && setPeekIssue({ workspaceSlug, projectId, issueId });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField || !workspace?.id || !projectId)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<Row
|
||||
className={cn(
|
||||
"relative py-4 flex items-center gap-2 border-b border-custom-border-200 cursor-pointer transition-all group",
|
||||
currentSelectedNotificationId === notification?.id ? "bg-custom-background-80/30" : "",
|
||||
notification.read_at === null ? "bg-custom-primary-100/5" : ""
|
||||
)}
|
||||
onClick={handleNotificationIssuePeekOverview}
|
||||
>
|
||||
{notification.read_at === null && (
|
||||
<div className="flex-shrink-0 w-1.5 h-1.5 rounded-full bg-custom-primary-100 absolute top-[50%] left-2" />
|
||||
)}
|
||||
|
||||
<div className="relative w-full flex gap-2">
|
||||
<div className="flex-shrink-0 relative flex justify-center items-center w-12 h-12 bg-custom-background-80 rounded-full">
|
||||
{notificationTriggeredBy && (
|
||||
<Avatar
|
||||
name={notificationTriggeredBy.display_name || notificationTriggeredBy?.first_name}
|
||||
src={getFileURL(notificationTriggeredBy.avatar_url)}
|
||||
size={42}
|
||||
shape="circle"
|
||||
className="!text-base !bg-custom-background-80"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1 -mt-2">
|
||||
<div className="relative flex items-center gap-3 h-8">
|
||||
<div className="w-full overflow-hidden whitespace-normal break-all truncate line-clamp-1 text-sm text-custom-text-100">
|
||||
<NotificationContent
|
||||
notification={notification}
|
||||
workspaceId={workspace.id}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
<NotificationOption
|
||||
workspaceSlug={workspaceSlug}
|
||||
notificationId={notification?.id}
|
||||
isSnoozeStateModalOpen={isSnoozeStateModalOpen}
|
||||
setIsSnoozeStateModalOpen={setIsSnoozeStateModalOpen}
|
||||
customSnoozeModal={customSnoozeModal}
|
||||
setCustomSnoozeModal={setCustomSnoozeModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center gap-3 text-xs text-custom-text-200">
|
||||
<div className="w-full overflow-hidden whitespace-normal break-words truncate line-clamp-1">
|
||||
{notification?.data?.issue?.identifier}-{notification?.data?.issue?.sequence_id}
|
||||
{notification?.data?.issue?.name}
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
{notification?.snoozed_till ? (
|
||||
<p className="flex flex-shrink-0 items-center justify-end gap-x-1 text-custom-text-300">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>
|
||||
Till {renderFormattedDate(notification.snoozed_till)},
|
||||
{renderFormattedTime(notification.snoozed_till, "12-hour")}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-auto flex-shrink-0 text-custom-text-300">
|
||||
{notification.created_at && calculateTimeAgo(notification.created_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ArchiveRestore } from "lucide-react";
|
||||
// plane imports
|
||||
import { NOTIFICATION_TRACKER_ELEMENTS, NOTIFICATION_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ArchiveIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
// helpers
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
// store
|
||||
import type { INotification } from "@/store/notifications/notification";
|
||||
// local imports
|
||||
import { NotificationItemOptionButton } from "./button";
|
||||
|
||||
type TNotificationItemArchiveOption = {
|
||||
workspaceSlug: string;
|
||||
notification: INotification;
|
||||
};
|
||||
|
||||
export const NotificationItemArchiveOption: FC<TNotificationItemArchiveOption> = observer((props) => {
|
||||
const { workspaceSlug, notification } = props;
|
||||
// hooks
|
||||
const { currentNotificationTab } = useWorkspaceNotifications();
|
||||
const { asJson: data, archiveNotification, unArchiveNotification } = notification;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleNotificationUpdate = async () => {
|
||||
try {
|
||||
const request = data.archived_at ? unArchiveNotification : archiveNotification;
|
||||
await request(workspaceSlug);
|
||||
captureSuccess({
|
||||
eventName: data.archived_at ? NOTIFICATION_TRACKER_EVENTS.unarchive : NOTIFICATION_TRACKER_EVENTS.archive,
|
||||
payload: {
|
||||
id: data?.data?.issue?.id,
|
||||
tab: currentNotificationTab,
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
title: data.archived_at ? t("notification.toasts.unarchived") : t("notification.toasts.archived"),
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
captureError({
|
||||
eventName: data.archived_at ? NOTIFICATION_TRACKER_EVENTS.unarchive : NOTIFICATION_TRACKER_EVENTS.archive,
|
||||
payload: {
|
||||
id: data?.data?.issue?.id,
|
||||
tab: currentNotificationTab,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationItemOptionButton
|
||||
data-ph-element={NOTIFICATION_TRACKER_ELEMENTS.ARCHIVE_UNARCHIVE_BUTTON}
|
||||
tooltipContent={
|
||||
data.archived_at ? t("notification.options.mark_unarchive") : t("notification.options.mark_archive")
|
||||
}
|
||||
callBack={handleNotificationUpdate}
|
||||
>
|
||||
{data.archived_at ? (
|
||||
<ArchiveRestore className="h-3 w-3 text-custom-text-300" />
|
||||
) : (
|
||||
<ArchiveIcon className="h-3 w-3 text-custom-text-300" />
|
||||
)}
|
||||
</NotificationItemOptionButton>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type TNotificationItemOptionButton = {
|
||||
tooltipContent?: string;
|
||||
buttonClassName?: string;
|
||||
callBack: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const NotificationItemOptionButton: FC<TNotificationItemOptionButton> = (props) => {
|
||||
const { tooltipContent = "", buttonClassName = "", children, callBack } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
return (
|
||||
<Tooltip tooltipContent={tooltipContent} isMobile={isMobile}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"relative flex-shrink-0 w-5 h-5 rounded-sm flex justify-center items-center outline-none bg-custom-background-80 hover:bg-custom-background-90",
|
||||
buttonClassName
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
callBack && callBack();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from "./root";
|
||||
|
||||
export * from "./read";
|
||||
export * from "./archive";
|
||||
export * from "./snooze";
|
||||
|
||||
export * from "./button";
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
// plane imports
|
||||
import { NOTIFICATION_TRACKER_ELEMENTS, NOTIFICATION_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
// helpers
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
// store
|
||||
import type { INotification } from "@/store/notifications/notification";
|
||||
// local imports
|
||||
import { NotificationItemOptionButton } from "./button";
|
||||
|
||||
type TNotificationItemReadOption = {
|
||||
workspaceSlug: string;
|
||||
notification: INotification;
|
||||
};
|
||||
|
||||
export const NotificationItemReadOption: FC<TNotificationItemReadOption> = observer((props) => {
|
||||
const { workspaceSlug, notification } = props;
|
||||
// hooks
|
||||
const { currentNotificationTab } = useWorkspaceNotifications();
|
||||
const { asJson: data, markNotificationAsRead, markNotificationAsUnRead } = notification;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleNotificationUpdate = async () => {
|
||||
try {
|
||||
const request = data.read_at ? markNotificationAsUnRead : markNotificationAsRead;
|
||||
await request(workspaceSlug);
|
||||
captureSuccess({
|
||||
eventName: data.read_at ? NOTIFICATION_TRACKER_EVENTS.mark_unread : NOTIFICATION_TRACKER_EVENTS.mark_read,
|
||||
payload: {
|
||||
id: data?.data?.issue?.id,
|
||||
tab: currentNotificationTab,
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
title: data.read_at ? t("notification.toasts.unread") : t("notification.toasts.read"),
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
captureError({
|
||||
eventName: data.read_at ? NOTIFICATION_TRACKER_EVENTS.mark_unread : NOTIFICATION_TRACKER_EVENTS.mark_read,
|
||||
payload: {
|
||||
id: data?.data?.issue?.id,
|
||||
tab: currentNotificationTab,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationItemOptionButton
|
||||
data-ph-element={NOTIFICATION_TRACKER_ELEMENTS.MARK_READ_UNREAD_BUTTON}
|
||||
tooltipContent={data.read_at ? t("notification.options.mark_unread") : t("notification.options.mark_read")}
|
||||
callBack={handleNotificationUpdate}
|
||||
>
|
||||
<MessageSquare className="h-3 w-3 text-custom-text-300" />
|
||||
</NotificationItemOptionButton>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, Dispatch, SetStateAction } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useNotification } from "@/hooks/store/notifications/use-notification";
|
||||
// local imports
|
||||
import { NotificationItemArchiveOption } from "./archive";
|
||||
import { NotificationItemReadOption } from "./read";
|
||||
import { NotificationItemSnoozeOption } from "./snooze";
|
||||
|
||||
type TNotificationOption = {
|
||||
workspaceSlug: string;
|
||||
notificationId: string;
|
||||
isSnoozeStateModalOpen: boolean;
|
||||
setIsSnoozeStateModalOpen: Dispatch<SetStateAction<boolean>>;
|
||||
customSnoozeModal: boolean;
|
||||
setCustomSnoozeModal: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const NotificationOption: FC<TNotificationOption> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
notificationId,
|
||||
isSnoozeStateModalOpen,
|
||||
setIsSnoozeStateModalOpen,
|
||||
customSnoozeModal,
|
||||
setCustomSnoozeModal,
|
||||
} = props;
|
||||
// hooks
|
||||
const notification = useNotification(notificationId);
|
||||
|
||||
return (
|
||||
<div className={cn("flex-shrink-0 hidden group-hover:block text-sm", isSnoozeStateModalOpen ? `!block` : ``)}>
|
||||
<div className="relative flex justify-center items-center gap-2">
|
||||
{/* read */}
|
||||
<NotificationItemReadOption workspaceSlug={workspaceSlug} notification={notification} />
|
||||
|
||||
{/* archive */}
|
||||
<NotificationItemArchiveOption workspaceSlug={workspaceSlug} notification={notification} />
|
||||
|
||||
{/* snooze notification */}
|
||||
<NotificationItemSnoozeOption
|
||||
workspaceSlug={workspaceSlug}
|
||||
notification={notification}
|
||||
setIsSnoozeStateModalOpen={setIsSnoozeStateModalOpen}
|
||||
customSnoozeModal={customSnoozeModal}
|
||||
setCustomSnoozeModal={setCustomSnoozeModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./modal";
|
||||
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { allTimeIn30MinutesInterval12HoursFormat } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
// components
|
||||
import { getDate } from "@plane/utils";
|
||||
import { DateDropdown } from "@/components/dropdowns/date";
|
||||
// helpers
|
||||
|
||||
type TNotificationSnoozeModal = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (dateTime?: Date | undefined) => Promise<void>;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
time: string | undefined;
|
||||
date: Date | undefined;
|
||||
period: "AM" | "PM";
|
||||
};
|
||||
|
||||
const defaultValues: FormValues = {
|
||||
time: undefined,
|
||||
date: undefined,
|
||||
period: "AM",
|
||||
};
|
||||
|
||||
const timeStamps = allTimeIn30MinutesInterval12HoursFormat;
|
||||
|
||||
export const NotificationSnoozeModal: FC<TNotificationSnoozeModal> = (props) => {
|
||||
const { isOpen, onClose, onSubmit: handleSubmitSnooze } = props;
|
||||
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
// This is a workaround to fix the issue of the Notification popover modal close on closing this modal
|
||||
const closeTimeout = setTimeout(() => {
|
||||
onClose();
|
||||
clearTimeout(closeTimeout);
|
||||
}, 50);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reset({ ...defaultValues });
|
||||
clearTimeout(timeout);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const getTimeStamp = () => {
|
||||
const today = new Date();
|
||||
const formDataDate = watch("date");
|
||||
|
||||
if (!formDataDate) return timeStamps;
|
||||
|
||||
const isToday = today.toDateString() === getDate(formDataDate)?.toDateString();
|
||||
|
||||
if (!isToday) return timeStamps;
|
||||
|
||||
const hours = today.getHours();
|
||||
const minutes = today.getMinutes();
|
||||
|
||||
return timeStamps.filter((optionTime) => {
|
||||
let optionHours = parseInt(optionTime.value.split(":")[0]);
|
||||
const optionMinutes = parseInt(optionTime.value.split(":")[1]);
|
||||
|
||||
const period = watch("period");
|
||||
|
||||
if (period === "PM" && optionHours !== 12) optionHours += 12;
|
||||
|
||||
if (optionHours < hours) return false;
|
||||
if (optionHours === hours && optionMinutes < minutes) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: FormValues) => {
|
||||
if (!workspaceSlug || !formData.date || !formData.time) return;
|
||||
|
||||
const period = formData.period;
|
||||
|
||||
const time = formData.time.split(":");
|
||||
const hours = parseInt(
|
||||
`${period === "AM" ? time[0] : parseInt(time[0]) + 12 === 24 ? "00" : parseInt(time[0]) + 12}`
|
||||
);
|
||||
const minutes = parseInt(time[1]);
|
||||
|
||||
const dateTime: Date | undefined = getDate(formData?.date);
|
||||
dateTime?.setHours(hours);
|
||||
dateTime?.setMinutes(minutes);
|
||||
|
||||
await handleSubmitSnooze(dateTime).then(() => {
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative w-full transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:!max-w-2xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Customize Snooze Time
|
||||
</Dialog.Title>
|
||||
|
||||
<div>
|
||||
<button type="button" onClick={handleClose}>
|
||||
<CloseIcon className="h-5 w-5 text-custom-text-100" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-col gap-3 md:!flex-row md:items-center">
|
||||
<div className="flex-1 pb-3 md:pb-0">
|
||||
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">Pick a date</h6>
|
||||
<Controller
|
||||
name="date"
|
||||
control={control}
|
||||
rules={{ required: "Please select a date" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DateDropdown
|
||||
value={value || null}
|
||||
placeholder="Select date"
|
||||
onChange={(val) => {
|
||||
setValue("time", undefined);
|
||||
onChange(val);
|
||||
}}
|
||||
minDate={new Date()}
|
||||
buttonVariant="border-with-text"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="border-custom-border-300 px-3 py-2.5"
|
||||
hideIcon
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h6 className="mb-2 block text-sm font-medium text-custom-text-400">Pick a time</h6>
|
||||
<Controller
|
||||
control={control}
|
||||
name="time"
|
||||
rules={{ required: "Please select a time" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<div className="truncate">
|
||||
{value ? (
|
||||
<span>
|
||||
{value} {watch("period").toLowerCase()}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-custom-text-400">Select a time</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
input
|
||||
>
|
||||
<div className="mb-2 flex h-9 w-full overflow-hidden rounded">
|
||||
<div
|
||||
onClick={() => {
|
||||
setValue("period", "AM");
|
||||
}}
|
||||
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
|
||||
watch("period") === "AM"
|
||||
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
AM
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setValue("period", "PM");
|
||||
}}
|
||||
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
|
||||
watch("period") === "PM"
|
||||
? "bg-custom-primary-100/90 text-custom-primary-0"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
PM
|
||||
</div>
|
||||
</div>
|
||||
{getTimeStamp().length > 0 ? (
|
||||
getTimeStamp().map((time, index) => (
|
||||
<CustomSelect.Option key={`${time}-${index}`} value={time.value}>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-3 block truncate">{time.label}</span>
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))
|
||||
) : (
|
||||
<p className="p-3 text-center text-custom-text-200">No available time for this date.</p>
|
||||
)}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between gap-2">
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import type { Dispatch, FC, SetStateAction } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Clock } from "lucide-react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { NOTIFICATION_SNOOZE_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// store
|
||||
import type { INotification } from "@/store/notifications/notification";
|
||||
// local imports
|
||||
import { NotificationSnoozeModal } from "./modal";
|
||||
|
||||
type TNotificationItemSnoozeOption = {
|
||||
workspaceSlug: string;
|
||||
notification: INotification;
|
||||
setIsSnoozeStateModalOpen: Dispatch<SetStateAction<boolean>>;
|
||||
customSnoozeModal: boolean;
|
||||
setCustomSnoozeModal: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const NotificationItemSnoozeOption: FC<TNotificationItemSnoozeOption> = observer((props) => {
|
||||
const { workspaceSlug, notification, setIsSnoozeStateModalOpen, customSnoozeModal, setCustomSnoozeModal } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const {} = useWorkspaceNotifications();
|
||||
const { t } = useTranslation();
|
||||
const { asJson: data, snoozeNotification, unSnoozeNotification } = notification;
|
||||
|
||||
const handleNotificationSnoozeDate = async (snoozeTill: Date | undefined) => {
|
||||
if (snoozeTill) {
|
||||
try {
|
||||
await snoozeNotification(workspaceSlug, snoozeTill);
|
||||
setToast({
|
||||
title: `${t("common.success")}!`,
|
||||
message: t("notification.toasts.snoozed"),
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await unSnoozeNotification(workspaceSlug);
|
||||
setToast({
|
||||
title: `${t("common.success")}!`,
|
||||
message: t("notification.toasts.un_snoozed"),
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
setCustomSnoozeModal(false);
|
||||
setIsSnoozeStateModalOpen(false);
|
||||
};
|
||||
|
||||
const handleDropdownSelect = (snoozeDate: Date | "un-snooze" | undefined) => {
|
||||
if (snoozeDate === "un-snooze") {
|
||||
handleNotificationSnoozeDate(undefined);
|
||||
return;
|
||||
}
|
||||
if (snoozeDate) {
|
||||
handleNotificationSnoozeDate(snoozeDate);
|
||||
} else {
|
||||
setCustomSnoozeModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NotificationSnoozeModal
|
||||
isOpen={customSnoozeModal}
|
||||
onClose={() => setCustomSnoozeModal(false)}
|
||||
onSubmit={handleNotificationSnoozeDate}
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open }) => {
|
||||
if (open) setIsSnoozeStateModalOpen(true);
|
||||
else setIsSnoozeStateModalOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
data.snoozed_till ? t("notification.options.mark_unsnooze") : t("notification.options.mark_snooze")
|
||||
}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Popover.Button
|
||||
className={cn(
|
||||
"relative flex-shrink-0 w-5 h-5 rounded-sm flex justify-center items-center outline-none bg-custom-background-80 hover:bg-custom-background-90",
|
||||
open ? "bg-custom-background-80" : ""
|
||||
)}
|
||||
>
|
||||
<Clock className="h-3 w-3 text-custom-text-300" />
|
||||
</Popover.Button>
|
||||
</Tooltip>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute mt-2 right-0 z-10 min-w-44 select-none">
|
||||
<div className="p-2 rounded-md border border-custom-border-200 bg-custom-background-100 space-y-1">
|
||||
{data.snoozed_till && (
|
||||
<button
|
||||
className="w-full text-left cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm text-custom-text-200 text-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDropdownSelect("un-snooze");
|
||||
}}
|
||||
>
|
||||
<div>{t("notification.options.mark_unsnooze")}</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{NOTIFICATION_SNOOZE_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
className="w-full text-left cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm text-custom-text-200 text-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDropdownSelect(option.value != undefined ? option.value() : option.value);
|
||||
}}
|
||||
>
|
||||
<div>{t(option?.i18n_label)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
import type { FC } from "react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import type { TNotificationTab } from "@plane/constants";
|
||||
import { NOTIFICATION_TABS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui";
|
||||
import { cn, getNumberCount } from "@plane/utils";
|
||||
// components
|
||||
import { CountChip } from "@/components/common/count-chip";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
|
||||
import { NotificationListRoot } from "@/plane-web/components/workspace-notifications/list-root";
|
||||
// local imports
|
||||
import { NotificationEmptyState } from "./empty-state";
|
||||
import { AppliedFilters } from "./filters/applied-filter";
|
||||
import { NotificationSidebarHeader } from "./header";
|
||||
import { NotificationsLoader } from "./loader";
|
||||
|
||||
export const NotificationsSidebarRoot: FC = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
// hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const {
|
||||
currentSelectedNotificationId,
|
||||
unreadNotificationsCount,
|
||||
loader,
|
||||
notificationIdsByWorkspaceId,
|
||||
currentNotificationTab,
|
||||
setCurrentNotificationTab,
|
||||
} = useWorkspaceNotifications();
|
||||
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
|
||||
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined;
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tabValue: TNotificationTab) => {
|
||||
if (currentNotificationTab !== tabValue) {
|
||||
setCurrentNotificationTab(tabValue);
|
||||
}
|
||||
},
|
||||
[currentNotificationTab, setCurrentNotificationTab]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !workspace) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-0 md:border-r border-custom-border-200 z-[10] flex-shrink-0 bg-custom-background-100 h-full transition-all max-md:overflow-hidden",
|
||||
currentSelectedNotificationId ? "w-0 md:w-2/6" : "w-full md:w-2/6"
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-full flex flex-col">
|
||||
<Row className="h-header border-b border-custom-border-200 flex flex-shrink-0">
|
||||
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
||||
</Row>
|
||||
|
||||
<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-sm transition-all`,
|
||||
currentNotificationTab === tab.value
|
||||
? "text-custom-primary-100"
|
||||
: "text-custom-text-100 hover:text-custom-text-200"
|
||||
)}
|
||||
>
|
||||
<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-custom-primary-100" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Header>
|
||||
|
||||
{/* applied filters */}
|
||||
<AppliedFilters workspaceSlug={workspaceSlug.toString()} />
|
||||
|
||||
{/* rendering notifications */}
|
||||
{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} />
|
||||
</ContentWrapper>
|
||||
) : (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<NotificationEmptyState currentNotificationTab={currentNotificationTab} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user