diff --git a/components/AppNotifications.tsx b/components/AppNotifications.tsx new file mode 100644 index 0000000..dffc985 --- /dev/null +++ b/components/AppNotifications.tsx @@ -0,0 +1,99 @@ +import { combineClassNames } from "@/submodules/javascript-functions/general"; +import { MemoIconBell } from "@/submodules/react-components/components/kern-icons/icons"; +import useOnClickOutside from "@/submodules/react-components/hooks/useHooks/useOnClickOutside"; +import { useLocalStorage } from "@/submodules/react-components/hooks/useLocalStorage"; +import Link from "next/link"; +import { useCallback, useMemo, useRef, useState } from "react"; +import tinycolor from 'tinycolor2' +import { useTranslation } from "react-i18next"; +import { UserRole } from "@/submodules/javascript-functions/enums/enums"; + +type AppNotificationsProps = { + project: { customerColorPrimary: string; } + user: { role: UserRole; languageDisplay: string; }; + forChatArea?: boolean; + notifications: any[]; +} + +const MIN_NOTIFICATIONS_SHOW = 3; +const MAX_NOTIFICATIONS_SHOW = 7; + +export default function AppNotifications(props: AppNotificationsProps) { + const { t } = useTranslation('projectOverview'); + const [lastSeenNotification, setLastSeenNotification] = useLocalStorage('lastSeen', 'releaseNotification', undefined, -1); + const [idsSeenNotifications, setIdsSeenNotifications] = useLocalStorage('idsSeenNotifications', 'releaseNotification', undefined, []); + const [showNotifications, setShowNotifications] = useState(false); + const [showMoreClicked, setShowMoreClicked] = useState(false); + const justClickedOutsideRef = useRef(false); // to prevent showing the notifications if the user just clicked outside + const refNotificationBox = useRef(null); + useOnClickOutside(refNotificationBox, () => { + setShowNotifications(false); justClickedOutsideRef.current = true; + setTimeout(() => justClickedOutsideRef.current = false, 200); + const newIdsSeen = props.notifications.map(n => n.id).filter(id => !idsSeenNotifications.includes(id)); + if (newIdsSeen.length > 0) setIdsSeenNotifications([...idsSeenNotifications, ...newIdsSeen]); + }); + + const clickBell = useCallback(() => { + if (justClickedOutsideRef.current) return; // if the user just clicked outside, don't show the notifications since the person is trying to close via bell icon + setShowNotifications(true); + if (props.notifications.length === 0) return; + setLastSeenNotification(props.notifications[0].id); + }, [props.notifications]); + + const finalNotifications = useMemo(() => { + if (props.notifications.length <= MIN_NOTIFICATIONS_SHOW) return props.notifications; + if (!showMoreClicked) return props.notifications.slice(0, MIN_NOTIFICATIONS_SHOW); + if (showMoreClicked) return props.notifications.slice(0, MAX_NOTIFICATIONS_SHOW); + }, [props.notifications, showMoreClicked]); + + const hasNewNotifications = useMemo(() => { + if (lastSeenNotification === -1) return true; // if no notification was seen + return props.notifications.some(notification => notification.id > lastSeenNotification); + }, [lastSeenNotification, props.notifications]); + + const isLightDesign = useMemo(() => tinycolor(props.project?.customerColorPrimary).isLight(), [props.project?.customerColorPrimary]); + + const buttonClasses = useMemo(() => { + if (props.forChatArea) { + const classes = "items-center justify-center w-8 h-8 border group flex -x-3 rounded-md p-1 text-sm leading-6 font-semibold" + if (isLightDesign) return 'bg-gray-100 text-gray-700 border-gray-300 ' + classes; + else return 'bg-zinc-900 text-zinc-100 border-zinc-700 ' + classes; + } + return "text-gray-400 hover:text-green-600 hover:bg-zinc-800 border-gray-700 items-center justify-center w-10 h-10 border group flex -x-3 rounded-md p-2 text-sm leading-6 font-semibold" + }, [props.forChatArea, isLightDesign]); + + return
+ + {hasNewNotifications &&
} + {showNotifications &&
+
+
{t("notificationBell.header")}
+ {finalNotifications.map((notification, idx) => ( +
+
+
+
{notification.config[props.user?.languageDisplay].headline}
+
{notification.config[props.user?.languageDisplay].description}
+ {t("notificationBell.link")} +
+
+ {!idsSeenNotifications.includes(notification.id) &&
} +
+
+
+ ))} + {finalNotifications.length === 0 &&
{t("notificationBell.noNotifications")}
} +
+ {props.notifications.length > MIN_NOTIFICATIONS_SHOW &&
+ +
} +
+ } +
+} diff --git a/components/kern-table/CellComponents.tsx b/components/kern-table/CellComponents.tsx index 7c54d4c..a2139dd 100644 --- a/components/kern-table/CellComponents.tsx +++ b/components/kern-table/CellComponents.tsx @@ -9,7 +9,7 @@ import { AdminMessageLevel } from "../../types/admin-messages"; import { FeedbackType, ModelsDownloadedStatus } from "@/submodules/javascript-functions/enums/enums"; import LoadingIcon from "@/submodules/react-components/components/LoadingIcon"; import { EvaluationRunState } from "../../types/evaluationRun"; -import { MemoIconAlertCircle, MemoIconAlertTriangleFilled, MemoIconArrowRight, MemoIconCircleCheckFilled, MemoIconExternalLink, MemoIconFileDownload, MemoIconInfoCircle, MemoIconInfoSquare, MemoIconLoader, MemoIconNotes, MemoIconTag, MemoIconThumbDownFilled, MemoIconThumbUpFilled, MemoIconTrash, MemoIconUserX } from "../kern-icons/icons"; +import { MemoIconAlertCircle, MemoIconAlertTriangleFilled, MemoIconArrowRight, MemoIconCircleCheckFilled, MemoIconEdit, MemoIconExternalLink, MemoIconFileDownload, MemoIconInfoCircle, MemoIconInfoSquare, MemoIconLoader, MemoIconNotes, MemoIconTag, MemoIconThumbDownFilled, MemoIconThumbUpFilled, MemoIconTrash, MemoIconUserX } from "../kern-icons/icons"; function OrganizationAndUsersCell({ organization }) { @@ -327,4 +327,20 @@ function ExpiredTokenCell({ value }) { } -export { OrganizationAndUsersCell, MaxRowsColsCharsCell, CommentsCell, ExportConsumptionAndDeleteCell, BadgeCell, OrganizationUserCell, DeleteCell, LevelCell, ArchiveReasonCell, ProjectNameTaskCell, CancelTaskCell, IconCell, ConfigCell, EditDeleteOrgButtonCell, ViewStackCell, AbortSessionButtonCell, FeedbackMessageCell, FeedbackMessageTextCell, JumpToConversationCell, RemoteVersionCell, ExternalLinkCell, ModelDateCell, FileSizeCell, StatusModelCell, DeleteModelCell, LabelCell, ViewCell, EvaluationRunStateCell, EvaluationRunDetailsCell, EtlApiTokenCell, EmailCell, EditIntegrationCell, ExpiredTokenCell } \ No newline at end of file +function LinkCell({ value }) { + return
+ {value} + + + +
+} + +function ConfigReleaseNotificationCell({ onClickView, onClickEdit }) { + return
+ + +
; +} + +export { OrganizationAndUsersCell, MaxRowsColsCharsCell, CommentsCell, ExportConsumptionAndDeleteCell, BadgeCell, OrganizationUserCell, DeleteCell, LevelCell, ArchiveReasonCell, ProjectNameTaskCell, CancelTaskCell, IconCell, ConfigCell, EditDeleteOrgButtonCell, ViewStackCell, AbortSessionButtonCell, FeedbackMessageCell, FeedbackMessageTextCell, JumpToConversationCell, RemoteVersionCell, ExternalLinkCell, ModelDateCell, FileSizeCell, StatusModelCell, DeleteModelCell, LabelCell, ViewCell, EvaluationRunStateCell, EvaluationRunDetailsCell, EtlApiTokenCell, EmailCell, EditIntegrationCell, ExpiredTokenCell, LinkCell, ConfigReleaseNotificationCell } \ No newline at end of file diff --git a/components/kern-table/KernTable.tsx b/components/kern-table/KernTable.tsx index b940c1d..4075aa6 100644 --- a/components/kern-table/KernTable.tsx +++ b/components/kern-table/KernTable.tsx @@ -1,6 +1,6 @@ import SortArrows from "@/submodules/react-components/components/kern-table/SortArrows"; import { KernTableProps } from "../../types/kern-table"; -import { AbortSessionButtonCell, ArchiveReasonCell, BadgeCell, CancelTaskCell, CommentsCell, ConfigCell, DeleteModelCell, DeleteCell, EditDeleteOrgButtonCell, EmailCell, EtlApiTokenCell, EvaluationRunDetailsCell, EvaluationRunStateCell, ExportConsumptionAndDeleteCell, ExternalLinkCell, FeedbackMessageCell, FeedbackMessageTextCell, FileSizeCell, IconCell, JumpToConversationCell, LabelCell, LevelCell, MaxRowsColsCharsCell, ModelDateCell, OrganizationAndUsersCell, OrganizationUserCell, ProjectNameTaskCell, RemoteVersionCell, StatusModelCell, ViewCell, ViewStackCell, EditIntegrationCell, ExpiredTokenCell } from "./CellComponents"; +import { AbortSessionButtonCell, ArchiveReasonCell, BadgeCell, CancelTaskCell, CommentsCell, ConfigCell, DeleteModelCell, DeleteCell, EditDeleteOrgButtonCell, EmailCell, EtlApiTokenCell, EvaluationRunDetailsCell, EvaluationRunStateCell, ExportConsumptionAndDeleteCell, ExternalLinkCell, FeedbackMessageCell, FeedbackMessageTextCell, FileSizeCell, IconCell, JumpToConversationCell, LabelCell, LevelCell, MaxRowsColsCharsCell, ModelDateCell, OrganizationAndUsersCell, OrganizationUserCell, ProjectNameTaskCell, RemoteVersionCell, StatusModelCell, ViewCell, ViewStackCell, EditIntegrationCell, ExpiredTokenCell, LinkCell, ConfigReleaseNotificationCell } from "./CellComponents"; import { Fragment, useMemo } from "react"; import KernDropdown from "../KernDropdown"; import { NotApplicableBadge } from "@/submodules/react-components/components/Badges"; @@ -158,6 +158,10 @@ function ComponentMapper(cell: any) { return ; case 'ExpiredTokenCell': return ; + case 'LinkCell': + return ; + case 'ConfigReleaseNotificationCell': + return ; } case 'text': return {cell.value ?? }