"use client"; import type { PoWorkbenchListRow } from "@/components/PoWorkbench/types"; import PoWorkbenchSearchResultsListSkeleton from "@/components/PoWorkbench/PoWorkbenchSearchResultsListSkeleton"; import WorkbenchResultSummary from "@/components/PoWorkbench/WorkbenchResultSummary"; import SearchOffOutlinedIcon from "@mui/icons-material/SearchOffOutlined"; import Box from "@mui/material/Box"; import List from "@mui/material/List"; import ListItemButton from "@mui/material/ListItemButton"; import Skeleton from "@mui/material/Skeleton"; import Typography from "@mui/material/Typography"; import { alpha } from "@mui/material/styles"; import type { Theme } from "@mui/material/styles"; import { useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; const LIST_CONTAINER_SX = { position: "absolute", inset: 0, display: "flex", flexDirection: "column", overflow: "hidden", bgcolor: "background.paper", } as const; const LIST_HEADER_SX = { flexShrink: 0, px: 2, pt: 0.5, pb: 0.5, borderBottom: 1, borderColor: "divider", bgcolor: "background.paper", } as const; const EMPTY_STATE_SX = { flex: 1, minHeight: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", px: 3, py: 4, } as const; const RESULTS_BODY_SX = { flex: 1, minHeight: 0, display: "flex", flexDirection: "column", bgcolor: "background.paper", } as const; const SCROLL_NEAR_BOTTOM_PX = 120; /** * Scroll host: the list; load the next page when the user scrolls near the bottom. * Use `overflow-y: scroll` (not `auto`) so a scrollbar track stays visible when content is short; * the list still does not scroll until content overflows. */ const SCROLL_HOST_SX = { flex: 1, minHeight: 0, height: "100%", overflowY: "scroll", overflowX: "hidden", scrollbarGutter: "stable", py: 0, } as const; interface ResultListItemProps { row: PoWorkbenchListRow; selected: boolean; onSelect: (id: string) => void; theme: Theme; } function ResultListItem({ row, selected, onSelect, theme, }: ResultListItemProps) { const rowSx = { m: 0, mx: 0, borderRadius: 0, pt: 1.25, pb: 1, px: 2, borderBottom: 1, borderColor: "divider", borderLeftStyle: "solid", borderLeftWidth: 10, borderLeftColor: selected ? "primary.main" : "transparent", borderTopLeftRadius: 0, borderBottomLeftRadius: 0, borderTopRightRadius: 0, borderBottomRightRadius: 0, bgcolor: selected ? alpha(theme.palette.primary.main, 0.08) : "transparent", "&:hover": { borderTopRightRadius: 10, borderBottomRightRadius: 10, bgcolor: selected ? alpha(theme.palette.primary.main, 0.12) : "action.hover", }, "&.Mui-selected": { borderTopRightRadius: 10, borderBottomRightRadius: 10, bgcolor: alpha(theme.palette.primary.main, 0.08), "&:hover": { borderTopRightRadius: 10, borderBottomRightRadius: 10, bgcolor: alpha(theme.palette.primary.main, 0.12), }, }, } as const; return ( onSelect(row.id)} alignItems="flex-start" sx={rowSx} > ); } export interface PoWorkbenchSearchResultsListProps { results: readonly PoWorkbenchListRow[]; totalMatches: number; isLoading: boolean; isLoadingMore: boolean; loadError: string | null; hasMore: boolean; onLoadMore: () => void; selectedId: string | null; onSelect: (id: string) => void; theme: Theme; } export default function PoWorkbenchSearchResultsList({ results, totalMatches, isLoading, isLoadingMore, loadError, hasMore, onLoadMore, selectedId, onSelect, theme, }: PoWorkbenchSearchResultsListProps) { const { t } = useTranslation("poWorkbench"); const scrollRootRef = useRef(null); /** * Load the next page when the user scrolls near the bottom. * No auto-chaining on first paint (avoids firing many /po/list calls before any scroll). */ const handleScroll = useCallback( (e: React.UIEvent) => { if (!hasMore || isLoading || isLoadingMore) { return; } const el = e.currentTarget; if (el.scrollTop <= 0) { return; } if ( el.scrollTop + el.clientHeight >= el.scrollHeight - SCROLL_NEAR_BOTTOM_PX ) { onLoadMore(); } }, [hasMore, isLoading, isLoadingMore, onLoadMore], ); const showLoadingHeaderOnly = isLoading && results.length === 0; return ( {showLoadingHeaderOnly ? ( ) : ( <> {t("results.totalMatches", { total: totalMatches })} )} {loadError && results.length > 0 ? ( {loadError} ) : null} {isLoading && results.length === 0 ? ( ) : null} {!isLoading && results.length === 0 ? ( p.palette.grey[400], }} aria-hidden /> {loadError ?? t("results.emptyState")} ) : null} {results.length > 0 ? ( {results.map((row) => ( ))} {isLoadingMore ? ( {t("results.loading")} ) : null} ) : null} ); }