import React, { memo, useEffect, useMemo, useRef } from "react"; import { useDroppable } from "@dnd-kit/core"; import { useSortable, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { CircularProgress } from "../../components"; import { cls } from "../../util"; import { BoardItem, BoardItemViewProps } from "./board_types"; interface BoardSortableListProps { columnId: string; items: BoardItem[]; ItemComponent: React.ComponentType>; isDragging: boolean; isDragOverColumn: boolean; loading?: boolean; hasMore?: boolean; error?: Error; onLoadMore?: () => void; } export function BoardSortableList({ columnId, items, ItemComponent, isDragging, isDragOverColumn, loading = false, hasMore = false, error, onLoadMore }: BoardSortableListProps) { const { setNodeRef } = useDroppable({ id: columnId, data: { type: "ITEM-LIST" } }); const sentinelRef = useRef(null); const isLoadingRef = useRef(loading); isLoadingRef.current = loading; const lastLoadTimeRef = useRef(0); useEffect(() => { if (!sentinelRef.current || !hasMore || !onLoadMore) return; const sentinel = sentinelRef.current; const observer = new IntersectionObserver( (entries) => { const now = Date.now(); if ( entries[0].isIntersecting && hasMore && !isLoadingRef.current && now - lastLoadTimeRef.current > 500 ) { lastLoadTimeRef.current = now; onLoadMore(); } }, { threshold: 0.1 } ); observer.observe(sentinel); const rect = sentinel.getBoundingClientRect(); const containerRect = sentinel.parentElement?.getBoundingClientRect(); if (containerRect && rect.top < containerRect.bottom && rect.bottom > containerRect.top) { const now = Date.now(); if (hasMore && !isLoadingRef.current && now - lastLoadTimeRef.current > 500) { lastLoadTimeRef.current = now; onLoadMore(); } } return () => observer.disconnect(); }, [hasMore, onLoadMore]); const containerClassName = useMemo(() => cls( // The scrollbar stays: this is the only thing on the page that scrolls // vertically, and hiding it left a column of cards with no sign that // there were more below the fold. "flex flex-col p-2 transition-opacity duration-100 transition-bg ease-linear w-full overflow-y-auto flex-1 min-h-0 rounded-md", isDragging && isDragOverColumn ? "bg-surface-accent-200 dark:bg-surface-900" : isDragging ? "bg-surface-50 dark:bg-surface-900 hover:bg-surface-accent-100 dark:hover:bg-surface-800" : "bg-surface-50 dark:bg-surface-900" ), [isDragging, isDragOverColumn]); return (
i.id)} strategy={verticalListSortingStrategy} > {items.length === 0 && !loading ? (
{/* "No items" over a header counting eleven of them is the wrong thing to say when the read failed. */} {error ? "Could not load this column" : "No items"}
) : ( <> {items.map((item, index) => ( ))} {(loading || hasMore) && (
{loading && }
)} )}
); } interface SortableItemProps { item: BoardItem; index: number; columnId: string; ItemComponent: React.ComponentType>; } const SortableItem = memo(function SortableItem({ item, index, columnId, ItemComponent }: SortableItemProps) { const { setNodeRef, attributes, listeners, isDragging: isItemBeingDragged, transform, transition } = useSortable({ id: item.id, data: { type: "ITEM", columnId } }); const sortableStyle = useMemo(() => ({ transform: CSS.Transform.toString(transform), transition, zIndex: isItemBeingDragged ? 2 : 1, opacity: isItemBeingDragged ? 0 : 1 }), [transform, transition, isItemBeingDragged]); return (
); }) as (props: SortableItemProps) => React.ReactElement;