Set up infinite loading for notebooks (#49321)

## Context

Sets up infinite loading for notebooks with the `InfiniteListDefault`
component

Also adds the notebook and chats count on the explorer home nav
<img width="275" height="137" alt="image"
src="https://github.com/user-attachments/assets/c3f16e8f-f520-4107-a188-43b1abcca043"
/>


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

* **New Features**
  * Explorer navigation now displays accurate notebook and chat counts.
* Notebook lists support infinite scrolling, loading indicators, and
improved active-state styling.
  * Notebook navigation remains available as additional items load.

* **Bug Fixes**
* Corrected default markdown cell formatting by removing unintended
leading spaces from headings and notes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
This commit is contained in:
Joshen LimandCharis authored and GitHub committed 2026-08-21 11:15:26 +08:00
1 parent 6779bf52e1
commit 9a3500aad2
3 files changed
+73 -20

No files matched your search

@@ -40,8 +40,9 @@ export const createLogCellSkeleton = ({
}
const DEFAULT_MARKDOWN_CONTENT = `
# New section
Add notes about your queries and results
# New section
Add notes about your queries and results
`.trim()
export const createMarkdownCellSkeleton = ({
@@ -12,6 +12,7 @@ import {
} 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'
@@ -27,6 +28,10 @@ export const ExplorerNavHome = ({
const notebooks = notebooksData?.pages.flatMap((page) => page.content) ?? []
const chats = useAiAssistantChatList()
const { data: notebookCountData } = 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 (
@@ -52,7 +57,9 @@ export const ExplorerNavHome = ({
>
<Icon size={14} className="shrink-0" />
<span className="flex-1 text-left">{label}</span>
<span className="text-xs text-foreground-lighter">{/* Length will be here */}</span>
<span className="text-xs text-foreground-lighter">
{type === 'notebook' ? notebookCount : chats.length}
</span>
<ChevronRight size={14} className="shrink-0 text-foreground-muted" />
</button>
)
@@ -7,23 +7,69 @@ import { cn } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ExplorerNavResourceWrapper, rowClassName } from './ExplorerLayout.constants'
import { useNotebooksInfiniteQuery } from '@/data/content/notebooks/notebooks-infinite-query'
import {
InfiniteListDefault,
LoaderForIconMenuItems,
type RowComponentBaseProps,
} from '@/components/ui/InfiniteList'
import {
NotebookRow,
useNotebooksInfiniteQuery,
} from '@/data/content/notebooks/notebooks-infinite-query'
const NOTEBOOK_ROW_HEIGHT = 28
type NotebookListItemProps = RowComponentBaseProps<NotebookRow> & {
projectRef: string | undefined
activeNotebookId: string | undefined
}
const NotebookListItem = ({
item: notebook,
style,
projectRef,
activeNotebookId,
}: NotebookListItemProps) => {
const isActive = activeNotebookId === notebook.id
return (
<Link
href={`/project/${projectRef}/explorer/notebook/${notebook.id}`}
className={rowClassName(isActive)}
style={style}
>
<NotebookText size={14} className={cn('shrink-0', isActive && 'text-foreground')} />
<span className="truncate text-left">{notebook.name}</span>
</Link>
)
}
export const ExplorerNavNotebooks = ({ onBack }: { onBack: () => void }) => {
const router = useRouter()
const { ref, id } = useParams()
const [search, setSearch] = useState('')
const { data: notebooksData, isPending } = useNotebooksInfiniteQuery({
const {
data: notebooksData,
isPending,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useNotebooksInfiniteQuery({
projectRef: ref,
limit: 100,
name: search,
})
const notebooks = useMemo(() => {
const items = notebooksData?.pages.flatMap((page) => page.content) ?? []
return items
}, [notebooksData?.pages])
const activeNotebookId = router.pathname.includes('/explorer/notebook/') ? id : undefined
const itemProps = useMemo(() => ({ projectRef: ref, activeNotebookId }), [ref, activeNotebookId])
return (
<ExplorerNavResourceWrapper
type="notebook"
@@ -31,7 +77,7 @@ export const ExplorerNavNotebooks = ({ onBack }: { onBack: () => void }) => {
setSearch={setSearch}
onBack={onBack}
>
<div className="flex flex-1 flex-col gap-px overflow-y-auto px-3 pb-3">
<div className="flex flex-1 min-h-0 flex-col px-3 pb-3">
{isPending ? (
<GenericSkeletonLoader />
) : notebooks.length === 0 ? (
@@ -39,20 +85,19 @@ export const ExplorerNavNotebooks = ({ onBack }: { onBack: () => void }) => {
{search ? 'No notebooks found' : 'No notebooks created yet'}
</p>
) : (
notebooks.map((notebook) => {
const isActive = router.pathname.includes('/explorer/notebook/') && id === notebook.id
return (
<Link
key={notebook.id}
href={`/project/${ref}/explorer/notebook/${notebook.id}`}
className={rowClassName(isActive)}
>
<NotebookText size={14} className={cn('shrink-0', isActive && 'text-foreground')} />
<span className="truncate text-left">{notebook.name}</span>
</Link>
)
})
<InfiniteListDefault
className="h-full w-full"
items={notebooks}
itemProps={itemProps}
ItemComponent={NotebookListItem}
LoaderComponent={LoaderForIconMenuItems}
getItemKey={(index) => notebooks[index]?.id ?? `notebook-${index}`}
getItemSize={() => NOTEBOOK_ROW_HEIGHT}
gap={1}
hasNextPage={hasNextPage}
isLoadingNextPage={isFetchingNextPage}
onLoadNextPage={fetchNextPage}
/>
)}
</div>
</ExplorerNavResourceWrapper>