Files
supabase/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx
Joshen Lim 15234190f7 Show loader for notebook counts (#49529)
## Context

Shows a loader UI for the notebook count, otherwise it'll incorrectly
show `0` initially

<img width="292" height="117" alt="image"
src="https://github.com/user-attachments/assets/17e3841d-b60b-4869-a0f0-3b44feb461c0"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Added a loading indicator for notebook counts while data is being
retrieved.
* Prevented incomplete or unavailable counts from appearing before
loading finishes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-25 23:27:30 +08:00

123 lines
4.2 KiB
TypeScript

import { useParams } from 'common'
import { motion } from 'framer-motion'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import {
EXPLORER_SECTIONS,
ExplorerResourceType,
LEVEL_OFFSET,
LEVEL_TRANSITION,
rowClassName,
} from './ExplorerLayout.constants'
import { formatRelativeTimeShort, getRecentlyUpdatedItems } from './ExplorerNavHome.utils'
import { useCreateChat } from '@/components/interfaces/Explorer/hooks'
import { useContentCountQuery } from '@/data/content/content-count-query'
import { useNotebooksInfiniteQuery } from '@/data/content/notebooks/notebooks-infinite-query'
import { useAiAssistantChatList } from '@/state/ai-assistant-state'
export const ExplorerNavHome = ({
onSelectSection,
}: {
onSelectSection: (section: ExplorerResourceType) => void
}) => {
const { ref } = useParams()
const { openChat } = useCreateChat()
const { data: notebooksData } = useNotebooksInfiniteQuery({ projectRef: ref, limit: 100 })
const notebooks = notebooksData?.pages.flatMap((page) => page.content) ?? []
const chats = useAiAssistantChatList()
const { data: notebookCountData, isPending } = useContentCountQuery({
projectRef: ref,
type: 'notebook',
})
// [Joshen] Notebooks are all shared by default, none private
const notebookCount = notebookCountData?.shared ?? 0
const recentItems = getRecentlyUpdatedItems({ notebooks, chats })
return (
<motion.div
key="root"
role="group"
aria-label="All resources"
initial={{ opacity: 0, x: -LEVEL_OFFSET }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -LEVEL_OFFSET }}
transition={LEVEL_TRANSITION}
className="absolute inset-0 flex flex-col gap-4 overflow-y-auto p-3"
>
<nav className="flex flex-col gap-px">
{EXPLORER_SECTIONS.map(({ type, label, icon: Icon }) => {
return (
<button
key={type}
type="button"
tabIndex={0}
className={rowClassName(false)}
onClick={() => onSelectSection(type)}
>
<Icon size={14} className="shrink-0" />
<span className="flex-1 text-left">{label}</span>
{type === 'notebook' ? (
isPending ? (
<ShimmeringLoader className="w-3 py-2" />
) : (
<span className="text-xs text-foreground-lighter">{notebookCount}</span>
)
) : (
<span className="text-xs text-foreground-lighter">{chats.length}</span>
)}
<ChevronRight size={14} className="shrink-0 text-foreground-muted" />
</button>
)
})}
</nav>
<section className="flex flex-col gap-px">
<h3 className="mb-2 px-3 font-mono text-sm font-normal uppercase text-foreground-lighter">
Recently updated
</h3>
{recentItems.length === 0 ? (
<p className="px-3 text-xs text-foreground-lighter">Nothing edited yet</p>
) : (
recentItems.map((item) => {
const Icon = EXPLORER_SECTIONS.find((section) => section.type === item.type)?.icon
const content = (
<>
{Icon && <Icon size={14} className="shrink-0" aria-hidden="true" />}
<span className="flex-1 truncate text-left">{item.label}</span>
<span className="shrink-0 text-xs text-foreground-lighter">
{formatRelativeTimeShort(item.updatedAt)}
</span>
</>
)
return item.type === 'chat' ? (
<button
key={item.id}
type="button"
tabIndex={0}
className={rowClassName(false)}
onClick={() => openChat(item.id)}
>
{content}
</button>
) : (
<Link
key={item.id}
href={`/project/${ref}/explorer/notebook/${item.id}`}
className={rowClassName(false)}
>
{content}
</Link>
)
})
)}
</section>
</motion.div>
)
}