mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
added pop over
This commit is contained in:
@@ -5,8 +5,8 @@ import { NotificationsSidebarRoot } from "@/components/workspace-notifications";
|
||||
|
||||
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden flex items-center">
|
||||
<NotificationsSidebarRoot />
|
||||
<div className="relative w-full h-full flex items-center">
|
||||
<NotificationsSidebar />
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './root';
|
||||
export * from './item'
|
||||
export * from "./root";
|
||||
export * from "./item";
|
||||
export * from "./preview";
|
||||
|
||||
@@ -1,25 +1,90 @@
|
||||
import { FC } from 'react'
|
||||
import { FC, useMemo, useState } from "react";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { observer } from "mobx-react";
|
||||
import { Row } from '@plane/ui';
|
||||
import { usePopper } from "react-popper";
|
||||
import { Popover } from "@headlessui/react";
|
||||
import { Row } from "@plane/ui";
|
||||
import { MemberDropdown } from "@/components/dropdowns";
|
||||
import { calculateTimeAgo, convertToEpoch } from "@/helpers/date-time.helper";
|
||||
//store
|
||||
import { useIssueDetail } from "@/hooks/store"
|
||||
import { useWorkspaceNotifications } from "@/hooks/store";
|
||||
//components
|
||||
import { IssueIdentifier } from '@/plane-web/components/issues';
|
||||
import { NotificationCardPreview } from "@/plane-web/components/workspace-notifications";
|
||||
export interface INotificationItem {
|
||||
issueId: string;
|
||||
notificationsCount: number;
|
||||
issueId: string;
|
||||
}
|
||||
export const NotificationItem: FC<INotificationItem> = observer((props)=>{
|
||||
const { issueId, notificationsCount } = props;
|
||||
const { issue: { getIssueById }} = useIssueDetail();
|
||||
const issueDetail = getIssueById(issueId);
|
||||
export const NotificationItem: FC<INotificationItem> = observer((props) => {
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
if(!issueDetail) return null
|
||||
return (
|
||||
<div className="border-b">
|
||||
<Row>
|
||||
<IssueIdentifier issueId={issueDetail.id} projectId={issueDetail.project_id || ""} textContainerClassName="text-xs" />
|
||||
</Row>
|
||||
const { issueId } = props;
|
||||
const { groupedNotifications } = useWorkspaceNotifications();
|
||||
const notificationGroup = groupedNotifications[issueId];
|
||||
const issue = notificationGroup[0].data?.issue;
|
||||
const unreadCount = notificationGroup.filter((e) => !e.read_at).length;
|
||||
const authorIds = notificationGroup
|
||||
.map((e) => e.triggered_by)
|
||||
.filter((id) => id != undefined && id != null) as string[];
|
||||
|
||||
if (!notificationGroup || !issue || !authorIds) return <></>;
|
||||
|
||||
const latestNotificationTime = useMemo(() => {
|
||||
const latestNotification = orderBy(notificationGroup, (n) => convertToEpoch(n.created_at), "desc")[0];
|
||||
if (latestNotification.created_at) return calculateTimeAgo(latestNotification.created_at);
|
||||
}, [notificationGroup]);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "right-end",
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover as="div" className={""}>
|
||||
<Popover.Button className="w-full">
|
||||
<div
|
||||
className="border-b transition-all py-4 border-custom-border-200 cursor-pointer group w-full"
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
{/* Issue card header */}
|
||||
<Row className="flex items-center gap-1">
|
||||
<span className="text-sm font-semibold break-words">
|
||||
{issue.sequence_id}-{issue.identifier}
|
||||
</span>
|
||||
<div className="flex-1 flex gap-2 justify-between items-center">
|
||||
<span className="overflow-hidden whitespace-normal break-all truncate line-clamp-1 text-sm text-custom-text-100">
|
||||
{issue.name}
|
||||
</span>
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-xs px-2 font-medium py-1 text-white bg-custom-primary-300 rounded-lg">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
<Row className="flex items-center justify-between">
|
||||
{/* Assignesss avatars / author avatars */}
|
||||
<MemberDropdown
|
||||
// projectId={issue?.project}
|
||||
value={authorIds}
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
multiple
|
||||
buttonVariant={authorIds?.length > 0 ? "transparent-without-text" : "border-without-text"}
|
||||
buttonClassName={authorIds?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
showTooltip={authorIds?.length === 0}
|
||||
placeholder="Assignees"
|
||||
optionsClassName="z-10"
|
||||
tooltipContent=""
|
||||
/>
|
||||
<div />
|
||||
<span className="text-xs text-custom-text-100">{latestNotificationTime}</span>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
</Popover.Button>
|
||||
<Popover.Panel {...attributes.popper} className={""}>
|
||||
<div ref={setPopperElement} className={"absolute z-10 right-0 translate-x-[100%]"}>
|
||||
<NotificationCardPreview notificationGroup={notificationGroup} />
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Dispatch, SetStateAction } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import {
|
||||
NotificationItemReadOption,
|
||||
NotificationItemArchiveOption,
|
||||
NotificationItemSnoozeOption,
|
||||
} from "@/components/workspace-notifications";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useNotification } from "@/hooks/store";
|
||||
|
||||
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 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FC } from "react";
|
||||
import { TNotification } from "@plane/types";
|
||||
import { Row } from "@plane/ui";
|
||||
|
||||
export type TNotificationCardPreview = {
|
||||
notificationGroup: TNotification[];
|
||||
};
|
||||
export const NotificationCardPreview: FC<TNotificationCardPreview> = (props) => {
|
||||
const { notificationGroup } = props;
|
||||
const issue = notificationGroup[0].data?.issue;
|
||||
const unreadCount = notificationGroup.filter((e) => !e.read_at).length;
|
||||
|
||||
if (!issue) return;
|
||||
return (
|
||||
<div className="p-3 border rounded-md shadow-md border-custom-border-100">
|
||||
<div className="flex justify-between gap-4">
|
||||
<p className="text-sm after:-100">
|
||||
{issue.identifier}-{issue.sequence_id}
|
||||
</p>
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-xs first-letter:font-medium py-1 px-2 text-white bg-custom-primary-300 rounded-lg">
|
||||
{unreadCount} new updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-medium">{issue.name}</p>
|
||||
<div>{/* Lastest notifications */}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ type TNotificationCardListRoot = {
|
||||
export const NotificationCardListRoot: FC<TNotificationCardListRoot> = observer((props) => {
|
||||
const { workspaceSlug, workspaceId } = props;
|
||||
// hooks
|
||||
const { loader, paginationInfo, getNotifications, notificationIssueIdsByWorkspaceId, groupedNotifications } = useWorkspaceNotifications();
|
||||
const { loader, paginationInfo, getNotifications, notificationIssueIdsByWorkspaceId } = useWorkspaceNotifications();
|
||||
const notificationIssueIds = notificationIssueIdsByWorkspaceId(workspaceId);
|
||||
|
||||
const getNextNotifications = async () => {
|
||||
@@ -30,10 +30,10 @@ export const NotificationCardListRoot: FC<TNotificationCardListRoot> = observer(
|
||||
|
||||
if (!workspaceSlug || !workspaceId || !notificationIssueIds) return <></>;
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{notificationIssueIds.map((issueId: string) => (
|
||||
// <NotificationItem key={notificationId} workspaceSlug={workspaceSlug} notificationId={notificationId} />
|
||||
<NotificationItem issueId={issueId} key={issueId} notificationsCount={groupedNotifications[issueId].length}/>
|
||||
<NotificationItem issueId={issueId} key={issueId} />
|
||||
))}
|
||||
|
||||
{/* fetch next page notifications */}
|
||||
@@ -52,6 +52,6 @@ export const NotificationCardListRoot: FC<TNotificationCardListRoot> = observer(
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import { getNumberCount } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { NotificationCardListRoot } from '@/plane-web/components/workspace-notifications'
|
||||
import { NotificationCardListRoot } from "@/plane-web/components/workspace-notifications";
|
||||
|
||||
export const NotificationsSidebar: FC = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
@@ -43,11 +43,11 @@ export const NotificationsSidebar: FC = observer(() => {
|
||||
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 overflow-hidden",
|
||||
"relative border-0 md:border-r border-custom-border-200 z-[10] flex-shrink-0 bg-custom-background-100 h-full transition-all",
|
||||
currentSelectedNotificationId ? "w-0 md:w-2/6" : "w-full md:w-2/6"
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-full overflow-hidden flex flex-col">
|
||||
<div className="relative w-full h-full flex flex-col">
|
||||
<Row className="h-[3.75rem] border-b border-custom-border-200 flex">
|
||||
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
|
||||
</Row>
|
||||
|
||||
@@ -5,11 +5,16 @@ import { computedFn } from "mobx-utils";
|
||||
import { TNotification, TNotificationPaginatedInfo } from "@plane/types";
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
||||
import { convertToEpoch } from "@/helpers/date-time.helper";
|
||||
import {RootStore} from '@/plane-web/store/root.store'
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
import workspaceNotificationService from "@/services/workspace-notification.service";
|
||||
import { IWorkspaceNotificationStore as IWorkspaceNotificationStoreCore , TNotificationLoader, TNotificationQueryParamType, WorkspaceNotificationStore as WorkspaceNotificationStoreCore } from "@/store/notifications/workspace-notifications.store";
|
||||
import {
|
||||
IWorkspaceNotificationStore as IWorkspaceNotificationStoreCore,
|
||||
TNotificationLoader,
|
||||
TNotificationQueryParamType,
|
||||
WorkspaceNotificationStore as WorkspaceNotificationStoreCore,
|
||||
} from "@/store/notifications/workspace-notifications.store";
|
||||
|
||||
export type TGroupedNotifications = Record<string,TNotification[]>
|
||||
export type TGroupedNotifications = Record<string, TNotification[]>;
|
||||
|
||||
export interface IWorkspaceNotificationStore extends IWorkspaceNotificationStoreCore {
|
||||
// observales
|
||||
@@ -37,25 +42,24 @@ export class WorkspaceNotificationStore extends WorkspaceNotificationStoreCore i
|
||||
* @param { INotification[] } notifications
|
||||
*/
|
||||
groupNotificationsById = action((notifications: TNotification[]) => {
|
||||
this.groupedNotifications = groupBy(notifications, (n) => n.data?.issue?.id);
|
||||
this.groupedNotifications = groupBy(notifications, (n) => n.entity_identifier);
|
||||
});
|
||||
|
||||
notificationIssueIdsByWorkspaceId = computedFn((workspaceId: string)=>{
|
||||
if(!workspaceId || Object.keys(this.groupedNotifications).length === 0) return undefined;
|
||||
notificationIssueIdsByWorkspaceId = computedFn((workspaceId: string) => {
|
||||
if (!workspaceId || Object.keys(this.groupedNotifications).length === 0) return undefined;
|
||||
|
||||
const groupedNotificationIssueIds = orderBy(
|
||||
Object.keys(this.groupedNotifications),
|
||||
(issueId) => {
|
||||
const notifications = this.groupedNotifications[issueId];
|
||||
const latestNotification = orderBy(notifications,(n)=>convertToEpoch(n.created_at),'desc')[0];
|
||||
return convertToEpoch(latestNotification.created_at)
|
||||
const latestNotification = orderBy(notifications, (n) => convertToEpoch(n.created_at), "desc")[0];
|
||||
return convertToEpoch(latestNotification.created_at);
|
||||
},
|
||||
'desc'
|
||||
)
|
||||
"desc"
|
||||
);
|
||||
|
||||
return groupedNotificationIssueIds;
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get all workspace notification
|
||||
@@ -77,7 +81,7 @@ export class WorkspaceNotificationStore extends WorkspaceNotificationStoreCore i
|
||||
const { results, ...paginationInfo } = notificationResponse;
|
||||
runInAction(() => {
|
||||
if (results) {
|
||||
this.groupNotificationsById(results)
|
||||
this.groupNotificationsById(results);
|
||||
}
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
});
|
||||
@@ -91,5 +95,3 @@ export class WorkspaceNotificationStore extends WorkspaceNotificationStoreCore i
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user