const DataItemsListTableBody: React.FC = ({ listRef, disallowDatasetRoot, isInRoot, selectedDataset, dataItemsReadyToDrag, dataItems, breadcrumbs, isSelectedFolderBeingEdited, selectedDataItems, onGoBackClick, onDataItemClick, onDataItemDoubleClick, getContextMenuHandler, isNewFolderRowVisible, onCreateNewFolder, onClickViewFile, continuationToken, onLoadMoreItems, isFetchingMoreItems, isCopyPopoverVisible, onCloseCopyPopover, allowMultipleSelection, refreshCurrentDataItems, handleCopyLink, handleDuplicate, handleMove, setDataItemsReadyToDrag, moveDataItemsAndUpdateState }) => { const lastClickedDataItemUuid = useRef(null) const lastMouseDownTime = useRef(null) const onClick = ( event: React.MouseEvent | null, dataItem: AnyDataItem, isCheckboxClickEvent: boolean ): void => { if (!isCheckboxClickEvent && !allowMultipleSelection && (event?.metaKey || event?.ctrlKey)) return if (isCheckboxClickEvent && allowMultipleSelection === false) return lastClickedDataItemUuid.current = dataItem.uuid setTimeout(() => { lastClickedDataItemUuid.current = null }, 250) handleRowClickEvent(event, dataItem, isCheckboxClickEvent, onDataItemClick) } const onDoubleClick = (event: React.MouseEvent | null, dataItem: AnyDataItem): void => { // If the second click is on a different data item, consider it as a single click. if (lastClickedDataItemUuid.current === dataItem.uuid) { handleRowClickEvent(event, dataItem, false, onDataItemDoubleClick) return } handleRowClickEvent(event, dataItem, false, onDataItemClick) } const onSelectAll = (selectAll: boolean): void => { if (dataItems && dataItems.length) { dataItems.forEach((dataItem: AnyDataItem) => handleRowClickEvent(null, dataItem, true, onDataItemClick)) } } const [isTextSelectDisabled, setIsTextSelectDisabled] = useState(false) const [isScrolledToTop, setIsScrolledToTop] = useState(false) const [dragOverStartTime, setDragOverStartTime] = useState(0) // IMPORTANT! // Since the data item list is rendered in a virtualised list, double click in a data item row // does not work since on every rerender the element is different. This means that DataItemRow // only fires single clicks. Since the only option then is to have the debounce logic in the parent // we only have 1 debouncedOnClick function. This is a problem because if we have multiple data // items, a click on any of them followed by a click on another also fires the same handler for a // double click, even though actually these are 2 single clicks. What this means is that it's // essential to check on double click whether the clicked item is the same one as the one clicked // first, otherwise this will lead to unexpected behaviour. // This is a very edge case, as usually such hook should be added to the actual row, which is // impossible here due to the virtualised list as mentioned above. const [debouncedOnClick] = useDebounceClick(onClick, onDoubleClick) const isAtTopLevelDirectory = disallowDatasetRoot ? breadcrumbs.length <= 1 : isInRoot const handleRowClickEvent = ( event: React.MouseEvent | null, dataItem: AnyDataItem, isCheckboxClickEvent: boolean = false, onClickEvent: ( event: React.MouseEvent | null, dataItem: AnyDataItem, isCheckboxClickEvent: boolean ) => void ) => { if (!isSelectedFolderBeingEdited) { onClickEvent(event, dataItem, isCheckboxClickEvent) // TODO (damian): Refactor to use the useDebounce hook. // If double click - prevent text select. if (lastMouseDownTime.current !== null && Date.now() - lastMouseDownTime.current < 300) { setIsTextSelectDisabled(true) } lastMouseDownTime.current = Date.now() } } const isDataItemSelected = (dataItem: AnyDataItem): boolean => { return !!selectedDataItems.find((selectedDataItem: AnyDataItem) => _.isEqual(selectedDataItem.getCompareProps(), dataItem.getCompareProps()) ) } const DataItemRowAsPartOfFixedSizeList: React.FC<{ index: number; style: CSSProperties }> = ({ index, style }) => { const dataItem = dataItems[index] if (!dataItem) return null return ( | null, dataItem: AnyDataItem, isCheckboxClickEvent: boolean ) => debouncedOnClick(event, dataItem, isCheckboxClickEvent)} getContextMenuHandler={getContextMenuHandler} isCopyPopoverVisible={isCopyPopoverVisible} onCloseCopyPopover={onCloseCopyPopover} refreshCurrentDataItems={refreshCurrentDataItems} handleCopyLink={handleCopyLink} handleDuplicate={handleDuplicate} handleMove={handleMove} selectedDataItemsCount={selectedDataItems ? selectedDataItems.length : 0} onDataItemDoubleClick={onDataItemDoubleClick} /> ) } const getVirtualizedListHeightFromParentHeight = (parentHeight: number | null, rowHeight: number) => { if (typeof parentHeight !== 'number') { // Return some arbitrary value. // This should not be 0, because otherwise the infinite loader thinks that we've reached the end of the list. return 200 } // Since we add up to two additional rows fixed on top, our list starts after that. const previousDirectoryTableRowOffset = isAtTopLevelDirectory ? 0 : rowHeight const newFolderRowOffset = isNewFolderRowVisible ? rowHeight : 0 return Math.max(parentHeight - previousDirectoryTableRowOffset - newFolderRowOffset, 0) } const onScroll = ({ scrollOffset }: ListOnScrollProps) => { if (scrollOffset === 0 && !isScrolledToTop) { setIsScrolledToTop(true) } else if (scrollOffset > 0 && isScrolledToTop) { setIsScrolledToTop(false) } } const handleOnDragDropMove = async ( destinationDataItem: VirtualFolderDataItem | Dataset, dataItems: AnyDataItem[] ) => { try { await moveDataItemsAndUpdateState(dataItems as DataItem[], destinationDataItem) openBasicMessage('success', dataItems.length > 1 ? 'Items moved successfully.' : 'Item moved successfully.') } catch (err) { const message = extractErrorMessage(err) || (dataItems.length > 1 ? 'Error Moving items.' : 'Error Moving item.') openBasicMessage('error', message) } finally { setDataItemsReadyToDrag([]) refreshCurrentDataItems && refreshCurrentDataItems() } } const handleDragandDropDuplicate = async ( destinationDataItem: VirtualFolderDataItem | Dataset, dataItems: AnyDataItem[] ) => { try { await copyDataItems(dataItems as DataItem[], destinationDataItem) openBasicMessage( 'success', dataItems.length > 1 ? 'Items duplicated successfully.' : 'Item duplicated successfully.' ) } catch (err) { const message = extractErrorMessage(err) || (dataItems.length > 1 ? 'Error Duplicating items.' : 'Error Duplicating item.') openBasicMessage('error', message) } finally { setDataItemsReadyToDrag([]) refreshCurrentDataItems && refreshCurrentDataItems() } } const onDragOverActionTriggerTimeMs = 1500 const handleOnDragOver = (e: React.DragEvent): void => { e.preventDefault() const onDragOverTimeElapsed = Date.now() - dragOverStartTime if (!dragOverStartTime) { setDragOverStartTime(Date.now()) } else if (onDragOverTimeElapsed > onDragOverActionTriggerTimeMs) { onGoBackClick() setDragOverStartTime(0) } } return ( {({ height }: { height: number }) => ( // The height of this should be controlled from outside.
) => e.preventDefault()} onDrop={(e: React.DragEvent) => { const destinationDataItem: VirtualFolderDataItem | Dataset = breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1] : selectedDataset e.ctrlKey ? handleDragandDropDuplicate(destinationDataItem, dataItemsReadyToDrag) : handleOnDragDropMove(destinationDataItem, dataItemsReadyToDrag) }} > {!isAtTopLevelDirectory && (
) => { setDragOverStartTime(0) }} >
)} {isNewFolderRowVisible && } {!isInRoot && dataItems.length === 0 && } !continuationToken || idx < dataItems.length} itemCount={continuationToken ? dataItems.length + 1 : dataItems.length} loadMoreItems={onLoadMoreItems ? onLoadMoreItems : () => null} threshold={5} > {({ onItemsRendered, ref }) => ( dataItems[index].uuid} itemSize={DATA_ROW_HEIGHT_IN_PX} height={ dataItems.length === 0 ? 0 : getVirtualizedListHeightFromParentHeight(height, DATA_ROW_HEIGHT_IN_PX) } itemCount={dataItems.length} width="100%" onItemsRendered={onItemsRendered} > {DataItemRowAsPartOfFixedSizeList} )} {isFetchingMoreItems ? ( // TODO (damian): Refactor this into a component.
) : null}
)}
) } export const DataItemsList: React.FC = props => { const [sortedDataItems, setSortedDataItems] = useState(props.dataItems) useEffect(() => { if (props.dataItems.length !== sortedDataItems.length) { setSortedDataItems(props.dataItems) } }, [props.dataItems, sortedDataItems]) const selectDataItems = (selectAll: boolean): void => { if (props.dataItems && props.dataItems.length) { props.selectDataItems(selectAll ? props.dataItems : []) } } return (
) }