/* eslint-disable react-hooks/exhaustive-deps */ import { ColumnDef, ColumnSizingState, Row, SortingState, flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; import byteSize from 'byte-size'; import clsx from 'clsx'; import dayjs from 'dayjs'; import { CaretDown, CaretUp } from 'phosphor-react'; import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { useKey, useOnWindowResize } from 'rooks'; import { ExplorerItem, FilePath, ObjectKind, isObject, isPath } from '@sd/client'; import { getExplorerStore, useDismissibleNoticeStore, useExplorerStore, useScrolled } from '~/hooks'; import RenameTextBox from './File/RenameTextBox'; import FileThumb from './File/Thumb'; import { InfoPill } from './Inspector'; import { ViewItem } from './View'; import { useExplorerViewContext } from './ViewContext'; import { getExplorerItemData, getItemFilePath } from './util'; interface ListViewItemProps { row: Row; index: number; selected: boolean; columnSizing: ColumnSizingState; } const ListViewItem = memo((props: ListViewItemProps) => { return (
{props.row.getVisibleCells().map((cell) => { return (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
); })}
); }); export default () => { const explorerStore = useExplorerStore(); const dismissibleNoticeStore = useDismissibleNoticeStore(); const { data, scrollRef, onLoadMore, hasNextPage, isFetchingNextPage } = useExplorerViewContext(); const { isScrolled } = useScrolled(scrollRef, 5); const [sized, setSized] = useState(false); const [sorting, setSorting] = useState([]); const [columnSizing, setColumnSizing] = useState({}); const [locked, setLocked] = useState(true); const paddingX = 16; const scrollBarWidth = 8; const getObjectData = (data: ExplorerItem) => (isObject(data) ? data.item : data.item.object); const getFileName = (path: FilePath) => `${path.name}${path.extension && `.${path.extension}`}`; const columns = useMemo[]>( () => [ { header: 'Name', minSize: 200, meta: { className: '!overflow-visible !text-ink' }, accessorFn: (file) => { const filePathData = getItemFilePath(file); return filePathData && getFileName(filePathData); }, cell: (cell) => { const file = cell.row.original; const filePathData = getItemFilePath(file); const selected = explorerStore.selectedRowIndex === cell.row.index; return (
{filePathData && ( )}
); } }, { header: 'Type', accessorFn: (file) => { return isPath(file) && file.item.is_dir ? 'Folder' : ObjectKind[getObjectData(file)?.kind || 0]; }, cell: (cell) => { const file = cell.row.original; return ( {isPath(file) && file.item.is_dir ? 'Folder' : ObjectKind[getObjectData(file)?.kind || 0]} ); } }, { header: 'Size', size: 100, accessorFn: (file) => byteSize(Number(getItemFilePath(file)?.size_in_bytes || 0)) }, { header: 'Date Created', accessorFn: (file) => dayjs(file.item.date_created).format('MMM Do YYYY'), sortingFn: (a, b, name) => { const aDate = a.original.item.date_created; const bDate = b.original.item.date_created; if (aDate === bDate) { const desc = sorting.find((s) => s.id === name)?.desc; const aPathData = getItemFilePath(a.original); const bPathData = getItemFilePath(b.original); const aName = aPathData ? getFileName(aPathData) : ''; const bName = bPathData ? getFileName(bPathData) : ''; return aName === bName ? 0 : aName > bName ? desc ? 1 : -1 : desc ? -1 : 1; } return aDate > bDate ? 1 : -1; } }, { header: 'Content ID', size: 180, accessorFn: (file) => getExplorerItemData(file).casId } ], [explorerStore.selectedRowIndex, explorerStore.isRenaming, sorting] ); const table = useReactTable({ data, columns, defaultColumn: { minSize: 100 }, state: { columnSizing, sorting }, onColumnSizingChange: setColumnSizing, onSortingChange: setSorting, columnResizeMode: 'onChange', getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel() }); const tableLength = table.getTotalSize(); const { rows } = table.getRowModel(); const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => scrollRef.current, estimateSize: () => 45, paddingStart: 12, paddingEnd: 12, overscan: !dismissibleNoticeStore.listView ? 5 : 1 }); const virtualRows = rowVirtualizer.getVirtualItems(); useEffect(() => { const lastRow = virtualRows[virtualRows.length - 1]; if (lastRow?.index === rows.length - 1 && hasNextPage && !isFetchingNextPage) { onLoadMore?.(); } }, [hasNextPage, onLoadMore, isFetchingNextPage, virtualRows, rows.length]); function handleResize() { if (scrollRef.current) { if (locked && Object.keys(columnSizing).length > 0) { table.setColumnSizing((sizing) => { const scrollWidth = scrollRef.current?.offsetWidth; const nameWidth = sizing.Name; return { ...sizing, ...(scrollWidth && nameWidth ? { Name: nameWidth + scrollWidth - paddingX * 2 - scrollBarWidth - tableLength } : {}) }; }); } else { const scrollWidth = scrollRef.current.offsetWidth; const tableWidth = tableLength; if (Math.abs(scrollWidth - tableWidth) < 10) { setLocked(true); } } } } // Measure initial column widths useEffect(() => { if (scrollRef.current) { const columns = table.getAllColumns(); const sizings = columns.reduce( (sizings, column) => column.id === 'Name' ? sizings : { ...sizings, [column.id]: column.getSize() }, {} as ColumnSizingState ); const scrollWidth = scrollRef.current.offsetWidth; const sizingsSum = Object.values(sizings).reduce((a, b) => a + b, 0); const nameWidth = scrollWidth - paddingX * 2 - scrollBarWidth - sizingsSum; table.setColumnSizing({ ...sizings, Name: nameWidth }); setSized(true); } }, []); // Resize view on window resize useOnWindowResize(handleResize); const lastSelectedIndex = useRef(explorerStore.selectedRowIndex); // Resize view on item selection/deselection useEffect(() => { const { selectedRowIndex } = explorerStore; if ( explorerStore.showInspector && typeof lastSelectedIndex.current !== typeof selectedRowIndex ) handleResize(); lastSelectedIndex.current = selectedRowIndex; }, [explorerStore.selectedRowIndex]); // Resize view on inspector toggle useEffect(() => { if (explorerStore.selectedRowIndex !== null) handleResize(); }, [explorerStore.showInspector]); // Force recalculate range // https://github.com/TanStack/virtual/issues/485 useMemo(() => { // @ts-ignore rowVirtualizer.calculateRange(); }, [rows.length, rowVirtualizer]); // Select item with arrow up key useKey( 'ArrowUp', (e) => { e.preventDefault(); const { selectedRowIndex } = explorerStore; if (selectedRowIndex === null) return; if (selectedRowIndex > 0) { const currentIndex = rows.findIndex((row) => row.index === selectedRowIndex); const newIndex = rows[currentIndex - 1]?.index; if (newIndex !== undefined) getExplorerStore().selectedRowIndex = newIndex; } }, { when: !explorerStore.isRenaming } ); // Select item with arrow down key useKey( 'ArrowDown', (e) => { e.preventDefault(); const { selectedRowIndex } = explorerStore; if (selectedRowIndex === null) return; if (selectedRowIndex !== data.length - 1) { const currentIndex = rows.findIndex((row) => row.index === selectedRowIndex); const newIndex = rows[currentIndex + 1]?.index; if (newIndex !== undefined) getExplorerStore().selectedRowIndex = newIndex; } }, { when: !explorerStore.isRenaming } ); if (!sized) return null; return (
e.stopPropagation()} className={clsx( 'sticky top-0 z-20 table-header-group', isScrolled && 'top-bar-blur !bg-app/90' )} > {table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header, i) => { const size = header.column.getSize(); return (
{header.isPlaceholder ? null : (
{flexRender( header.column.columnDef.header, header.getContext() )}
{{ asc: , desc: }[header.column.getIsSorted() as string] ?? null} {(i !== headerGroup.headers.length - 1 || (i === headerGroup.headers.length - 1 && !locked)) && (
e.stopPropagation()} onMouseDown={(e) => { setLocked(false); header.getResizeHandler()(e); }} onTouchStart={header.getResizeHandler()} className="absolute right-0 h-[70%] w-2 cursor-col-resize border-r border-app-line/50" /> )}
)}
); })}
))}
{virtualRows.map((virtualRow) => { const row = rows[virtualRow.index]!; const selected = explorerStore.selectedRowIndex === row.index; return (
); })}
); };