"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}
);
}