import React from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import type { VirtualItem } from '@tanstack/virtual-core'; import { EntityCard } from './entity-card'; import type { EntityAssociation } from '../../../main/db/schema'; /** * Props for EntityList component */ export interface EntityListProps { entities: EntityAssociation[]; selectedEntities: Set; onEntitySelect: (entityId: string, selected: boolean) => void; onEntityDelete: (entityId: string) => void; onEntityRename: (entityId: string, newName: string) => void; onBulkSelect: (entityIds: string[]) => void; loading?: boolean; } /** * Virtualized list component for displaying entities */ export const EntityList: React.FC = ({ entities, selectedEntities, onEntitySelect, onEntityDelete, onEntityRename, onBulkSelect, loading = false, }) => { const scrollElementRef = React.useRef(null); const [sortBy, setSortBy] = React.useState<'name' | 'date' | 'type'>('date'); const [sortOrder, setSortOrder] = React.useState<'asc' | 'desc'>('desc'); const sortedEntities = React.useMemo(() => { const sorted = [...entities].sort((a, b) => { let comparison = 0; switch (sortBy) { case 'name': comparison = a.entityName.localeCompare(b.entityName); break; case 'date': comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); break; case 'type': comparison = a.entityType.localeCompare(b.entityType); break; default: comparison = 0; } return sortOrder === 'asc' ? comparison : -comparison; }); return sorted; }, [entities, sortBy, sortOrder]); const virtualizer = useVirtualizer({ count: sortedEntities.length, getScrollElement: () => scrollElementRef.current, estimateSize: () => 120, overscan: 5, }); const handleSelectAll = React.useCallback( (selected: boolean) => { if (selected) { onBulkSelect(sortedEntities.map((entity) => entity.entityId)); } else { onBulkSelect([]); } }, [sortedEntities, onBulkSelect] ); const handleSort = React.useCallback( (newSortBy: 'name' | 'date' | 'type') => { if (sortBy === newSortBy) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortBy(newSortBy); setSortOrder('asc'); } }, [sortBy, sortOrder] ); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent, index: number) => { const entity = sortedEntities[index]; switch (event.key) { case ' ': event.preventDefault(); onEntitySelect( entity.entityId, !selectedEntities.has(entity.entityId) ); break; case 'ArrowDown': event.preventDefault(); if (index < sortedEntities.length - 1) { const nextElement = document.getElementById( `entity-card-${index + 1}` ); nextElement?.focus(); } break; case 'ArrowUp': event.preventDefault(); if (index > 0) { const prevElement = document.getElementById( `entity-card-${index - 1}` ); prevElement?.focus(); } break; case 'Delete': event.preventDefault(); onEntityDelete(entity.entityId); break; } }, [sortedEntities, selectedEntities, onEntitySelect, onEntityDelete] ); if (loading) { return (
Loading entities...
); } if (sortedEntities.length === 0) { return (
No entities found
Create entities using the available tools to see them here.
); } const allSelected = sortedEntities.length > 0 && sortedEntities.every((entity) => selectedEntities.has(entity.entityId)); const someSelected = sortedEntities.some((entity) => selectedEntities.has(entity.entityId) ); return (
{/* Header with sorting and select all */}
{selectedEntities.size > 0 && ( {selectedEntities.size} entities selected )}
Sort by:
{/* Virtualized list */}
{virtualizer.getVirtualItems().map((virtualRow: VirtualItem) => { const entity = sortedEntities[virtualRow.index]; return (
onEntitySelect(entity.entityId, selected) } onDelete={() => onEntityDelete(entity.entityId)} onRename={(newName) => onEntityRename(entity.entityId, newName) } onCopyToClipboard={() => { navigator.clipboard.writeText(entity.entityId); }} onKeyDown={(event) => handleKeyDown(event, virtualRow.index)} tabIndex={0} style={{ marginBottom: '8px' }} />
); })}
); };