Files
supabase/apps/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopoverV2/NotificationRow.tsx
T
Charis cfa59dabc8 refactor: use @tanstack/react-virtual for virtualization (#39992)
* refactor(infinite list): use @tanstack/react-virtual for virtualization

Swapping virtualization libraries from `react-window` to
`@tanstack/react-virtual`.

Motivation: we need a completely headless library for maximum
flexibility. `react-window` injects extra DOM elements which makes it
hard to customize styling and placement on elements that are very picky
about their DOM structure, like tables.

* refactor(table editor menu): use new infinite list

* refactor(notifications): use new infinite list

* cleanup(infinite list): remove old infinite list

* refactor(storage menu): use new infinite list & remove react-window deps
2025-10-30 14:27:53 -04:00

190 lines
6.9 KiB
TypeScript

import dayjs from 'dayjs'
import { Archive, ArchiveRestoreIcon, ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'
import { useInView } from 'react-intersection-observer'
import { Button, cn } from 'ui'
import { Markdown } from 'components/interfaces/Markdown'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useVirtualizerContext } from 'components/ui/InfiniteList'
import { NotificationData, type NotificationsData } from 'data/notifications/notifications-v2-query'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import type { Organization } from 'types'
import { CriticalIcon, WarningIcon } from 'ui'
interface NotificationRowProps {
index: number
item: NotificationsData[number]
getOrganizationById: (id: number) => Organization
getOrganizationBySlug: (slug: string) => Organization
onUpdateNotificationStatus: (id: string, status: 'archived' | 'seen') => void
queueMarkRead: (id: string) => void
}
const NotificationRow = ({
index,
item: notification,
getOrganizationById,
getOrganizationBySlug,
onUpdateNotificationStatus,
queueMarkRead,
}: NotificationRowProps) => {
const { virtualizer } = useVirtualizerContext()
const { ref: viewRef, inView } = useInView()
const { status, priority } = notification
const data = notification.data as NotificationData
const { data: project } = useProjectDetailQuery({ ref: data.project_ref })
const organization =
data.org_slug !== undefined
? getOrganizationBySlug(data.org_slug)
: project !== undefined
? getOrganizationById(project.organization_id)
: undefined
const daysFromNow = dayjs().diff(dayjs(notification.inserted_at), 'day')
const formattedTimeFromNow = dayjs(notification.inserted_at).fromNow()
const formattedInsertedAt = dayjs(notification.inserted_at).format('MMM DD, YYYY')
const onButtonAction = (type?: string) => {
// [Joshen] Implement accordingly - BE team will need to give us a heads up on this
console.log('Action', type)
}
useEffect(() => {
if (inView && notification.status === 'new') {
queueMarkRead(notification.id)
}
}, [inView])
return (
<div
ref={virtualizer.measureElement}
className={cn(
`p-4 flex justify-between gap-x-3 group`,
index !== 0 ? 'border-t border-overlay' : '',
status === 'new' ? 'bg-surface-100/50' : 'bg-background'
)}
>
<div ref={viewRef} className="flex flex-col gap-y-2.5 w-full py-0.5">
{(project !== undefined || organization !== undefined) && (
<div className="flex items-center max-w-[350px]">
{organization !== undefined && (
<Link
title={organization.name}
href={`/org/${organization.slug}/general`}
className="text-xs transition text-foreground-light hover:text-foreground underline truncate"
>
{organization.name}
</Link>
)}
{organization !== undefined && project !== undefined && (
<span className="text-foreground-lighter">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
)}
{project !== undefined && (
<Link
title={project.name}
href={`/project/${project.ref}`}
className="text-xs transition text-foreground-light hover:text-foreground underline truncate"
>
{project.name}
</Link>
)}
</div>
)}
<div className="flex items-center gap-x-2">
<p className="text-sm break-word">
{data.title}{' '}
<span className="ml-1 text-xs text-foreground-light capitalize-sentence">
{daysFromNow > 1 ? formattedInsertedAt : formattedTimeFromNow}
</span>
</p>
</div>
{data.message !== undefined && (
<Markdown className="text-foreground-light text-xs" content={data.message} />
)}
{(data.actions ?? []).length > 0 && (
<div className="flex items-center gap-x-2">
{data.actions.map((action, idx) => {
const key = `${notification.id}-action-${idx}`
if (action.url !== undefined) {
const url = action.url.includes('[ref]')
? action.url.replace('[ref]', project?.ref ?? '_')
: action.url.includes('[slug]')
? action.url.replace('[slug]', organization?.slug ?? '_')
: action.url
return (
<Button
key={key}
type="default"
icon={<ExternalLink strokeWidth={1.5} />}
asChild
>
<Link href={url} target="_blank" rel="noreferrer">
{action.label}
</Link>
</Button>
)
} else if (action.action_type !== undefined) {
return (
<Button
key={key}
type="default"
onClick={() => onButtonAction(action.action_type)}
>
{action.label}
</Button>
)
} else {
return null
}
})}
</div>
)}
</div>
<div className="flex flex-col items-center gap-y-2">
{priority === 'Warning' && <WarningIcon className="w-5 h-5" />}
{priority === 'Critical' && <CriticalIcon className="w-5 h-5" />}
{notification.status === 'archived' ? (
<ButtonTooltip
type="outline"
icon={
<ArchiveRestoreIcon size={13} strokeWidth={2} className="text-foreground-light" />
}
className="p-1.5 group-hover:opacity-100 opacity-0 transition rounded-full"
onClick={() => onUpdateNotificationStatus(notification.id, 'seen')}
tooltip={{ content: { text: 'Unarchive', side: 'bottom' } }}
/>
) : (
<ButtonTooltip
type="outline"
icon={<Archive size={13} strokeWidth={2} className="text-foreground-light" />}
className="p-1.5 group-hover:opacity-100 opacity-0 transition rounded-full"
onClick={() => onUpdateNotificationStatus(notification.id, 'archived')}
tooltip={{ content: { text: 'Archive', side: 'bottom' } }}
/>
)}
</div>
</div>
)
}
export default NotificationRow