diff --git a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx index 129792f8f..908d983ec 100644 --- a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx @@ -15,7 +15,7 @@ interface FilesTableProps { } export function FilesTable({ files, isLoading, paginationInfo }: FilesTableProps) { - const { table, rowSelection, selectAllRows, clearRowSelection } = useFilesTable( + const { table, fileSelection, selectAllFiles, clearFileSelection } = useFilesTable( files, paginationInfo ) @@ -26,14 +26,12 @@ export function FilesTable({ files, isLoading, paginationInfo }: FilesTableProps return ( <> - row.original)} + clearRowSelection={clearFileSelection} /> + diff --git a/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx b/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx index 26d028ec4..82d0c8791 100644 --- a/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx +++ b/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx @@ -1,10 +1,10 @@ -import { RowSelection } from './useRowSelection' +import { FileSelection } from './useFileSelection' import { Button } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' import styles from './RowSelectionMessage.module.scss' interface RowSelectionMessageProps { - rowSelection: RowSelection + fileSelection: FileSelection totalFilesCount: number selectAllRows: () => void clearRowSelection: () => void @@ -14,13 +14,13 @@ const MINIMUM_SELECTED_FILES_TO_SHOW_MESSAGE = 0 const MINIMUM_FILES_TO_SHOW_MESSAGE = 10 export function RowSelectionMessage({ - rowSelection, + fileSelection, totalFilesCount, selectAllRows, clearRowSelection }: RowSelectionMessageProps) { const { t } = useTranslation('files') - const selectedFilesCount = Object.keys(rowSelection).length + const selectedFilesCount = Object.keys(fileSelection).length const showMessage = totalFilesCount > MINIMUM_FILES_TO_SHOW_MESSAGE && selectedFilesCount > MINIMUM_SELECTED_FILES_TO_SHOW_MESSAGE diff --git a/src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection.ts b/src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection.ts new file mode 100644 index 000000000..e8e9fec2c --- /dev/null +++ b/src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection.ts @@ -0,0 +1,114 @@ +import { useEffect, useState } from 'react' +import { FilePaginationInfo } from '../../../../../files/domain/models/FilePaginationInfo' +import { File } from '../../../../../files/domain/models/File' +import { Row } from '@tanstack/react-table' +import { RowSelection } from '../useFilesTable' + +export type FileSelection = { + [key: string]: File | undefined +} + +export function useFileSelection( + currentPageSelectedRowModel: Record>, + setCurrentPageRowSelection: (rowSelection: RowSelection) => void, + paginationInfo: FilePaginationInfo +) { + const [fileSelection, setFileSelection] = useState({}) + const updateFileSelection = () => { + const currentPageFileSelection = getCurrentPageFileSelection() + const currentPageIndexes = getCurrentPageIndexes() + + Object.keys(fileSelection).forEach((key) => { + const rowIndex = parseInt(key) + if (currentPageIndexes.includes(rowIndex)) { + if (!currentPageFileSelection[key]) { + delete fileSelection[key] + } + } + }) + + return { ...fileSelection, ...currentPageFileSelection } + } + const getCurrentPageIndexes = () => { + return Array.from( + { length: paginationInfo.pageSize }, + (_, i) => i + (paginationInfo.page - 1) * paginationInfo.pageSize + ) + } + const getCurrentPageFileSelection = () => { + const rowSelectionFixed: FileSelection = {} + const currentPageIndexes = getCurrentPageIndexes() + + Object.entries(currentPageSelectedRowModel).forEach(([string, Row]) => { + const rowIndex = parseInt(string) + rowSelectionFixed[currentPageIndexes[rowIndex]] = Row.original + }) + return rowSelectionFixed + } + const computeCurrentPageRowSelection = () => { + const rowSelectionOfCurrentPage: RowSelection = {} + const currentPageIndexes = getCurrentPageIndexes() + + Object.keys(fileSelection).forEach((key) => { + const rowIndex = parseInt(key) + if (currentPageIndexes.includes(rowIndex)) { + rowSelectionOfCurrentPage[currentPageIndexes.indexOf(rowIndex)] = true + } + }) + + return rowSelectionOfCurrentPage + } + const selectAllFiles = () => { + setCurrentPageRowSelection(createRowSelection(paginationInfo.pageSize)) + setFileSelection(createFileSelection(paginationInfo.totalFiles)) + } + const clearFileSelection = () => { + setCurrentPageRowSelection({}) + setFileSelection({}) + } + const toggleAllFilesSelected = () => { + if (areAllFilesSelected()) { + clearFileSelection() + } else { + selectAllFiles() + } + } + const areAllFilesSelected = () => { + return Object.keys(fileSelection).length === paginationInfo.totalFiles + } + + useEffect(() => { + setFileSelection(updateFileSelection()) + }, [currentPageSelectedRowModel]) + + useEffect(() => { + setCurrentPageRowSelection(computeCurrentPageRowSelection()) + }, [paginationInfo]) + + return { + fileSelection, + selectAllFiles, + clearFileSelection, + toggleAllFilesSelected + } +} + +export function createRowSelection(numberOfRows: number) { + const rowSelection: Record = {} + + for (let i = 0; i < numberOfRows; i++) { + rowSelection[String(i)] = true + } + + return rowSelection +} + +export function createFileSelection(numberOfRows: number) { + const fileSelection: FileSelection = {} + + for (let i = 0; i < numberOfRows; i++) { + fileSelection[String(i)] = undefined + } + + return fileSelection +} diff --git a/src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection.ts b/src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection.ts deleted file mode 100644 index d7aa68b96..000000000 --- a/src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { useEffect, useState } from 'react' -import { FilePaginationInfo } from '../../../../../files/domain/models/FilePaginationInfo' - -export type RowSelection = { - [key: string]: boolean -} - -export function useRowSelection( - currentPageRowSelection: RowSelection, - setCurrentPageRowSelection: (rowSelection: RowSelection) => void, - paginationInfo: FilePaginationInfo -) { - const [rowSelection, setRowSelection] = useState({}) - const updatedRowSelection = () => { - const currentPageRowSelectionFixed = getCurrentPageRowSelectionFixed() - const currentPageIndexes = getCurrentPageIndexes() - - Object.keys(rowSelection).forEach((key) => { - const rowIndex = parseInt(key) - if (currentPageIndexes.includes(rowIndex)) { - if (!currentPageRowSelectionFixed[key]) { - delete rowSelection[key] - } - } - }) - - return { ...rowSelection, ...currentPageRowSelectionFixed } - } - const getCurrentPageIndexes = () => { - return Array.from( - { length: paginationInfo.pageSize }, - (_, i) => i + (paginationInfo.page - 1) * paginationInfo.pageSize - ) - } - const getCurrentPageRowSelectionFixed = () => { - const rowSelectionFixed: RowSelection = {} - const currentPageIndexes = getCurrentPageIndexes() - - Object.keys(currentPageRowSelection).forEach((key) => { - const rowIndex = parseInt(key) - rowSelectionFixed[currentPageIndexes[rowIndex]] = currentPageRowSelection[key] - }) - return rowSelectionFixed - } - const getRowSelectionOfCurrentPage = () => { - const rowSelectionOfCurrentPage: RowSelection = {} - const currentPageIndexes = getCurrentPageIndexes() - - Object.keys(rowSelection).forEach((key) => { - const rowIndex = parseInt(key) - if (currentPageIndexes.includes(rowIndex)) { - rowSelectionOfCurrentPage[currentPageIndexes.indexOf(rowIndex)] = rowSelection[key] - } - }) - - return rowSelectionOfCurrentPage - } - const selectAllRows = () => { - setCurrentPageRowSelection(createRowSelection(paginationInfo.pageSize)) - setRowSelection(createRowSelection(paginationInfo.totalFiles)) - } - const clearRowSelection = () => { - setCurrentPageRowSelection({}) - setRowSelection({}) - } - const toggleAllRowsSelected = () => { - if (isAllRowsSelected()) { - clearRowSelection() - } else { - selectAllRows() - } - } - const isAllRowsSelected = () => { - return Object.keys(rowSelection).length === paginationInfo.totalFiles - } - - useEffect(() => { - setRowSelection(updatedRowSelection()) - }, [currentPageRowSelection]) - - useEffect(() => { - setCurrentPageRowSelection(getRowSelectionOfCurrentPage()) - }, [paginationInfo]) - - return { - rowSelection, - setRowSelection, - selectAllRows, - clearRowSelection, - toggleAllRowsSelected - } -} - -export function createRowSelection(numberOfRows: number) { - const rowSelection: Record = {} - - for (let i = 0; i < numberOfRows; i++) { - rowSelection[String(i)] = true - } - - return rowSelection -} diff --git a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx index bb524d4b4..467f76e1b 100644 --- a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx @@ -1,20 +1,24 @@ import { useEffect, useState } from 'react' import { File } from '../../../../files/domain/models/File' -import { getCoreRowModel, useReactTable } from '@tanstack/react-table' +import { getCoreRowModel, Row, useReactTable } from '@tanstack/react-table' import { createColumnsDefinition } from './FilesTableColumnsDefinition' import { FilePaginationInfo } from '../../../../files/domain/models/FilePaginationInfo' -import { RowSelection, useRowSelection } from './row-selection/useRowSelection' +import { useFileSelection } from './row-selection/useFileSelection' + +export type RowSelection = { + [key: string]: boolean +} export function useFilesTable(files: File[], paginationInfo: FilePaginationInfo) { const [currentPageRowSelection, setCurrentPageRowSelection] = useState({}) - const { rowSelection, selectAllRows, clearRowSelection, toggleAllRowsSelected } = useRowSelection( - currentPageRowSelection, - setCurrentPageRowSelection, - paginationInfo - ) + const [currentPageSelectedRowModel, setCurrentPageSelectedRowModel] = useState< + Record> + >({}) + const { fileSelection, selectAllFiles, clearFileSelection, toggleAllFilesSelected } = + useFileSelection(currentPageSelectedRowModel, setCurrentPageRowSelection, paginationInfo) const table = useReactTable({ data: files, - columns: createColumnsDefinition(toggleAllRowsSelected), + columns: createColumnsDefinition(toggleAllFilesSelected), state: { rowSelection: currentPageRowSelection }, @@ -30,10 +34,14 @@ export function useFilesTable(files: File[], paginationInfo: FilePaginationInfo) table.setPageIndex(paginationInfo.page - 1) }, [paginationInfo]) + useEffect(() => { + setCurrentPageSelectedRowModel(table.getSelectedRowModel().rowsById) + }, [table.getSelectedRowModel().rowsById]) + return { table, - rowSelection, - selectAllRows, - clearRowSelection + fileSelection, + selectAllFiles, + clearFileSelection } } diff --git a/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx b/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx index a066dba7a..7e1c474ee 100644 --- a/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx +++ b/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx @@ -5,19 +5,18 @@ import { useSettings } from '../../../../settings/SettingsContext' import { SettingName } from '../../../../../settings/domain/models/Setting' import { ZipDownloadLimit } from '../../../../../settings/domain/models/ZipDownloadLimit' import { useEffect, useState } from 'react' +import { FileSelection } from '../row-selection/useFileSelection' interface ZipDownloadLimitMessageProps { - selectedFiles: File[] + fileSelection: FileSelection } const MINIMUM_FILES_TO_SHOW_MESSAGE = 1 -export function ZipDownloadLimitMessage({ selectedFiles }: ZipDownloadLimitMessageProps) { +export function ZipDownloadLimitMessage({ fileSelection }: ZipDownloadLimitMessageProps) { const { t } = useTranslation('files') const { getSettingByName } = useSettings() const [zipDownloadLimitInBytes, setZipDownloadLimitInBytes] = useState() - const selectionTotalSizeInBytes = getFilesTotalSizeInBytes(selectedFiles) - useEffect(() => { getSettingByName(SettingName.ZIP_DOWNLOAD_LIMIT) .then((zipDownloadLimit) => { @@ -28,9 +27,11 @@ export function ZipDownloadLimitMessage({ selectedFiles }: ZipDownloadLimitMessa }) }, [getSettingByName]) + // TODO - When selecting all files, the size should come from a call to a use case that returns the total size of the dataset files. Check issue https://github.com/IQSS/dataverse-frontend/issues/170 + const selectionTotalSizeInBytes = getFilesTotalSizeInBytes(Object.values(fileSelection)) const showMessage = zipDownloadLimitInBytes && - selectedFiles.length > MINIMUM_FILES_TO_SHOW_MESSAGE && + Object.values(fileSelection).length > MINIMUM_FILES_TO_SHOW_MESSAGE && selectionTotalSizeInBytes > zipDownloadLimitInBytes if (!showMessage) { @@ -48,8 +49,10 @@ export function ZipDownloadLimitMessage({ selectedFiles }: ZipDownloadLimitMessa ) } -function getFilesTotalSizeInBytes(files: File[]) { - return files.map((file) => file.size).reduce((bytes, size) => bytes + size.toBytes(), 0) +function getFilesTotalSizeInBytes(files: (File | undefined)[]) { + return files + .map((file) => file?.size) + .reduce((bytes, size) => bytes + (size ? size.toBytes() : 0), 0) } function bytesToHumanReadable(bytes: number) { diff --git a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx index 25088e757..2049c0c7d 100644 --- a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx @@ -9,8 +9,11 @@ import { } from '../../../../../src/files/domain/models/FileCriteria' import { FilesCountInfoMother } from '../../../files/domain/models/FilesCountInfoMother' import { FilePaginationInfo } from '../../../../../src/files/domain/models/FilePaginationInfo' -import { FileType } from '../../../../../src/files/domain/models/File' +import { FileSizeUnit, FileType } from '../../../../../src/files/domain/models/File' import styles from '../../../../../src/sections/dataset/dataset-files/files-table/FilesTable.module.scss' +import { SettingMother } from '../../../settings/domain/models/SettingMother' +import { ZipDownloadLimit } from '../../../../../src/settings/domain/models/ZipDownloadLimit' +import { SettingsContext } from '../../../../../src/sections/settings/SettingsContext' const testFiles = FileMother.createMany(10) const datasetPersistentId = 'test-dataset-persistent-id' @@ -161,6 +164,36 @@ describe('DatasetFiles', () => { cy.findByText('1 file is currently selected.').should('exist') }) + + it('renders the zip download limit message when selecting rows from different pages', () => { + const getSettingByName = cy + .stub() + .resolves(SettingMother.createZipDownloadLimit(new ZipDownloadLimit(1, FileSizeUnit.BYTES))) + + cy.customMount( + + + + ) + + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + cy.findByRole('button', { name: 'Next' }).click() + cy.get('table > tbody > tr:nth-child(3) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByText( + /exceeds the zip limit of 1.0 B. Please unselect some files to continue./ + ).should('exist') + + cy.get('table > tbody > tr:nth-child(3) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByText( + /exceeds the zip limit of 1.0 B. Please unselect some files to continue./ + ).should('not.exist') + }) }) describe('Calling use cases', () => { diff --git a/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx index e5eb4b997..5584bdb76 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx @@ -1,5 +1,5 @@ import { RowSelectionMessage } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage' -import { createRowSelection } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection' +import { createRowSelection } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection' let selectAllRows = () => {} let clearRowSelection = () => {} @@ -12,7 +12,7 @@ describe('RowSelectionMessage', () => { it('renders the message when there are more than 10 files and some row is selected', () => { cy.customMount( { it('does not render the message when there are less than 10 files', () => { cy.customMount( { it('does not render the message when there are more than 10 files but no row is selected', () => { cy.customMount( { it('renders the plural form of the message when there is more than 1 row selected', () => { cy.customMount( { it("calls selectAllRows when the 'Select all' button is clicked", () => { cy.customMount( { it("calls clearRowSelection when the 'Clear selection.' button is clicked", () => { cy.customMount( { - it('should render the component with no message if conditions are not met', () => { + it('should not render if there is less than 1 file selected', () => { const getSettingByName = cy .stub() .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) @@ -19,7 +19,9 @@ describe('ZipDownloadLimitMessage', () => { cy.customMount( ) @@ -27,13 +29,32 @@ describe('ZipDownloadLimitMessage', () => { cy.findByText(/The overall size of the files selected/).should('not.exist') }) - it('should render the component with the appropriate message if conditions are met', () => { + it('should not render if the zipDownloadLimit is not exceeded', () => { + const getSettingByName = cy + .stub() + .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) + + cy.customMount( + + + + ) + + cy.findByText(/The overall size of the files selected/).should('not.exist') + }) + + it('should render if there is more than 1 file and they exceed the zipDownloadLimit', () => { const getSettingByName = cy .stub() .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) cy.customMount( - + ) @@ -46,13 +67,13 @@ describe('ZipDownloadLimitMessage', () => { const getSettingByName = cy .stub() .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) - const files = [ - FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }), - FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }) - ] + const fileSelection = { + '1': FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }), + '2': FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }) + } cy.customMount( - + )