diff --git a/src/files/domain/models/FilePaginationInfo.ts b/src/files/domain/models/FilePaginationInfo.ts new file mode 100644 index 000000000..5f0e1ddcc --- /dev/null +++ b/src/files/domain/models/FilePaginationInfo.ts @@ -0,0 +1,52 @@ +export class FilePaginationInfo { + constructor( + public readonly page: number = 1, + public readonly pageSize: number = 10, + public readonly totalFiles: number = 0 + ) {} + + withTotal(total: number): FilePaginationInfo { + return new FilePaginationInfo(this.page, this.pageSize, total) + } + goToPage(page: number): FilePaginationInfo { + return new FilePaginationInfo(page, this.pageSize, this.totalFiles) + } + + goToPreviousPage(): FilePaginationInfo { + if (!this.previousPage) throw new Error('No previous page') + return this.goToPage(this.previousPage) + } + + goToNextPage(): FilePaginationInfo { + if (!this.nextPage) throw new Error('No next page') + return this.goToPage(this.nextPage) + } + + withPageSize(pageSize: number): FilePaginationInfo { + const getNewPage = (oldPageSize: number, newPageSize: number) => { + const newPage = Math.ceil((this.page * oldPageSize) / newPageSize) + return newPage > 0 ? newPage : 1 + } + return new FilePaginationInfo(getNewPage(this.pageSize, pageSize), pageSize, this.totalFiles) + } + + get totalPages(): number { + return Math.ceil(this.totalFiles / this.pageSize) + } + + get hasPreviousPage(): boolean { + return this.page > 1 + } + + get hasNextPage(): boolean { + return this.page < this.totalPages + } + + get previousPage(): number | undefined { + return this.hasPreviousPage ? this.page - 1 : undefined + } + + get nextPage(): number | undefined { + return this.hasNextPage ? this.page + 1 : undefined + } +} diff --git a/src/files/domain/repositories/FileRepository.ts b/src/files/domain/repositories/FileRepository.ts index f17f29259..eda64b94c 100644 --- a/src/files/domain/repositories/FileRepository.ts +++ b/src/files/domain/repositories/FileRepository.ts @@ -1,11 +1,13 @@ import { File } from '../models/File' import { FileCriteria } from '../models/FileCriteria' import { FilesCountInfo } from '../models/FilesCountInfo' +import { FilePaginationInfo } from '../models/FilePaginationInfo' export interface FileRepository { getAllByDatasetPersistentId: ( datasetPersistentId: string, version?: string, + paginationInfo?: FilePaginationInfo, criteria?: FileCriteria ) => Promise getCountInfoByDatasetPersistentId: ( diff --git a/src/files/domain/useCases/getFilesByDatasetPersistentId.ts b/src/files/domain/useCases/getFilesByDatasetPersistentId.ts index 0c68ffafe..fd4baa093 100644 --- a/src/files/domain/useCases/getFilesByDatasetPersistentId.ts +++ b/src/files/domain/useCases/getFilesByDatasetPersistentId.ts @@ -1,15 +1,17 @@ import { FileRepository } from '../repositories/FileRepository' import { File, FileVersionNotNumber } from '../models/File' import { FileCriteria } from '../models/FileCriteria' +import { FilePaginationInfo } from '../models/FilePaginationInfo' export async function getFilesByDatasetPersistentId( fileRepository: FileRepository, persistentId: string, version: string = FileVersionNotNumber.LATEST, + paginationInfo?: FilePaginationInfo, criteria?: FileCriteria ): Promise { return fileRepository - .getAllByDatasetPersistentId(persistentId, version, criteria) + .getAllByDatasetPersistentId(persistentId, version, paginationInfo, criteria) .catch((error: Error) => { throw new Error(error.message) }) diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index c890c16c3..fbbb5511d 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -3,10 +3,17 @@ import { File } from '../domain/models/File' import { FilesMockData } from '../../stories/files/FileMockData' import { FilesCountInfo } from '../domain/models/FilesCountInfo' import { FilesCountInfoMother } from '../../../tests/component/files/domain/models/FilesCountInfoMother' +import { FilePaginationInfo } from '../domain/models/FilePaginationInfo' export class FileJSDataverseRepository implements FileRepository { - // eslint-disable-next-line unused-imports/no-unused-vars - getAllByDatasetPersistentId(persistentId: string, version?: string): Promise { + getAllByDatasetPersistentId( + // eslint-disable-next-line unused-imports/no-unused-vars + persistentId: string, + // eslint-disable-next-line unused-imports/no-unused-vars + version?: string, + // eslint-disable-next-line unused-imports/no-unused-vars + paginationInfo?: FilePaginationInfo + ): Promise { // TODO - implement using js-dataverse return new Promise((resolve) => { setTimeout(() => { diff --git a/src/sections/dataset/dataset-files/DatasetFiles.tsx b/src/sections/dataset/dataset-files/DatasetFiles.tsx index 572a16876..df133b93c 100644 --- a/src/sections/dataset/dataset-files/DatasetFiles.tsx +++ b/src/sections/dataset/dataset-files/DatasetFiles.tsx @@ -4,6 +4,8 @@ import { FilesTable } from './files-table/FilesTable' import { FileCriteriaForm } from './file-criteria-form/FileCriteriaForm' import { FileCriteria } from '../../../files/domain/models/FileCriteria' import { useFiles } from './useFiles' +import { FilePaginationInfo } from '../../../files/domain/models/FilePaginationInfo' +import { FilesPagination } from './files-pagination/FilesPagination' interface DatasetFilesProps { filesRepository: FileRepository @@ -16,25 +18,30 @@ export function DatasetFiles({ datasetPersistentId, datasetVersion }: DatasetFilesProps) { + const [paginationInfo, setPaginationInfo] = useState(new FilePaginationInfo()) const [criteria, setCriteria] = useState(new FileCriteria()) const { files, isLoading, filesCountInfo } = useFiles( filesRepository, datasetPersistentId, datasetVersion, + paginationInfo, criteria ) - const handleCriteriaChange = (newCriteria: FileCriteria) => { - setCriteria(newCriteria) - } return ( <> - + + ) } diff --git a/src/sections/dataset/dataset-files/files-table/table-pagination/TablePagination.module.scss b/src/sections/dataset/dataset-files/files-pagination/FilesPagination.module.scss similarity index 83% rename from src/sections/dataset/dataset-files/files-table/table-pagination/TablePagination.module.scss rename to src/sections/dataset/dataset-files/files-pagination/FilesPagination.module.scss index db6710af1..f75206577 100644 --- a/src/sections/dataset/dataset-files/files-table/table-pagination/TablePagination.module.scss +++ b/src/sections/dataset/dataset-files/files-pagination/FilesPagination.module.scss @@ -1,3 +1,7 @@ +.row { + justify-content: center; +} + .container { display: flex; align-items: center; diff --git a/src/sections/dataset/dataset-files/files-pagination/FilesPagination.tsx b/src/sections/dataset/dataset-files/files-pagination/FilesPagination.tsx new file mode 100644 index 000000000..13035b5a1 --- /dev/null +++ b/src/sections/dataset/dataset-files/files-pagination/FilesPagination.tsx @@ -0,0 +1,80 @@ +import { Col, Pagination, Row } from '@iqss/dataverse-design-system' +import { PageNumbersButtonsWithEllipsis } from './PageNumbersButtonsWithEllipsis' +import { PageSizeSelector } from './PageSizeSelector' +import styles from './FilesPagination.module.scss' +import { FilePaginationInfo } from '../../../../files/domain/models/FilePaginationInfo' +import { useEffect, useState } from 'react' + +interface FilesPaginationProps { + onPaginationInfoChange: (paginationInfo: FilePaginationInfo) => void + page: number + pageSize: number + total: number +} +const NO_PAGES = 0 +export function FilesPagination({ + onPaginationInfoChange, + page, + pageSize, + total +}: FilesPaginationProps) { + const [paginationInfo, setPaginationInfo] = useState( + new FilePaginationInfo(page, pageSize, total) + ) + const goToPage = (newPage: number) => { + setPaginationInfo(paginationInfo.goToPage(newPage)) + } + const goToPreviousPage = () => { + setPaginationInfo(paginationInfo.goToPreviousPage()) + } + const goToNextPage = () => { + setPaginationInfo(paginationInfo.goToNextPage()) + } + const setPageSize = (newPageSize: number) => { + setPaginationInfo(paginationInfo.withPageSize(newPageSize)) + } + + useEffect(() => { + onPaginationInfoChange(paginationInfo) + }, [paginationInfo]) + + useEffect(() => { + setPaginationInfo(paginationInfo.withTotal(total)) + }, [total]) + + if (paginationInfo.totalPages === NO_PAGES) { + return <> + } + return ( + + +
+ + goToPage(1)} + disabled={!paginationInfo.hasPreviousPage} + /> + goToPreviousPage()} + disabled={!paginationInfo.hasPreviousPage} + /> + + goToNextPage()} + disabled={!paginationInfo.hasNextPage} + /> + goToPage(paginationInfo.totalPages)} + disabled={!paginationInfo.hasNextPage} + /> + + +
+ +
+ ) +} diff --git a/src/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtons.tsx b/src/sections/dataset/dataset-files/files-pagination/PageNumbersButtons.tsx similarity index 93% rename from src/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtons.tsx rename to src/sections/dataset/dataset-files/files-pagination/PageNumbersButtons.tsx index 0a1d5b666..af829c8ab 100644 --- a/src/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtons.tsx +++ b/src/sections/dataset/dataset-files/files-pagination/PageNumbersButtons.tsx @@ -23,7 +23,7 @@ export function PageNumbersButtons({ goToPage(pageIndex)} + onClick={() => goToPage(pageIndex + 1)} active={selectedPageIndex === pageIndex} /> ) diff --git a/src/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtonsWithEllipsis.tsx b/src/sections/dataset/dataset-files/files-pagination/PageNumbersButtonsWithEllipsis.tsx similarity index 90% rename from src/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtonsWithEllipsis.tsx rename to src/sections/dataset/dataset-files/files-pagination/PageNumbersButtonsWithEllipsis.tsx index fa4386690..3e56a6ed0 100644 --- a/src/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtonsWithEllipsis.tsx +++ b/src/sections/dataset/dataset-files/files-pagination/PageNumbersButtonsWithEllipsis.tsx @@ -1,7 +1,7 @@ import { Pagination } from '@iqss/dataverse-design-system' import { PageNumbersButtons } from './PageNumbersButtons' -interface TablePaginationNumbersWithEllipsisProps { +interface PageNumbersButtonsWithEllipsisProps { selectedPageIndex: number pageCount: number goToPage: (pageIndex: number) => void @@ -11,7 +11,7 @@ export function PageNumbersButtonsWithEllipsis({ selectedPageIndex, pageCount, goToPage -}: TablePaginationNumbersWithEllipsisProps) { +}: PageNumbersButtonsWithEllipsisProps) { const firstPageNumber = 1 const firstPageIndex = 0 const lastPageIndex = pageCount - 1 @@ -28,7 +28,7 @@ export function PageNumbersButtonsWithEllipsis({ goToPage(firstPageIndex)} + onClick={() => goToPage(firstPageIndex + 1)} /> @@ -45,7 +45,7 @@ export function PageNumbersButtonsWithEllipsis({ goToPage(lastPageIndex)} + onClick={() => goToPage(lastPageIndex + 1)} /> )} diff --git a/src/sections/dataset/dataset-files/files-table/table-pagination/PageSizeSelector.tsx b/src/sections/dataset/dataset-files/files-pagination/PageSizeSelector.tsx similarity index 94% rename from src/sections/dataset/dataset-files/files-table/table-pagination/PageSizeSelector.tsx rename to src/sections/dataset/dataset-files/files-pagination/PageSizeSelector.tsx index b8980ac93..a5acbf945 100644 --- a/src/sections/dataset/dataset-files/files-table/table-pagination/PageSizeSelector.tsx +++ b/src/sections/dataset/dataset-files/files-pagination/PageSizeSelector.tsx @@ -1,4 +1,4 @@ -import styles from './TablePagination.module.scss' +import styles from './FilesPagination.module.scss' import { useTranslation } from 'react-i18next' export function PageSizeSelector({ diff --git a/src/sections/dataset/dataset-files/files-table/FilesTable.module.scss b/src/sections/dataset/dataset-files/files-table/FilesTable.module.scss index 34a353ff4..65e61e784 100644 --- a/src/sections/dataset/dataset-files/files-table/FilesTable.module.scss +++ b/src/sections/dataset/dataset-files/files-table/FilesTable.module.scss @@ -1,7 +1,3 @@ -.pagination-container { - justify-content: center; -} - .file-info-header { display: flex; align-items: flex-start; diff --git a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx index 83f49fc8e..908d983ec 100644 --- a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx @@ -1,55 +1,41 @@ -import { Col, Row, Table } from '@iqss/dataverse-design-system' +import { Table } from '@iqss/dataverse-design-system' import { FilesTableHeader } from './FilesTableHeader' import { FilesTableBody } from './FilesTableBody' -import { TablePagination } from './table-pagination/TablePagination' -import styles from './FilesTable.module.scss' import { useFilesTable } from './useFilesTable' import { File } from '../../../../files/domain/models/File' import { RowSelectionMessage } from './row-selection/RowSelectionMessage' import { ZipDownloadLimitMessage } from './zip-download-limit-message/ZipDownloadLimitMessage' import { SpinnerSymbol } from './spinner-symbol/SpinnerSymbol' +import { FilePaginationInfo } from '../../../../files/domain/models/FilePaginationInfo' interface FilesTableProps { files: File[] isLoading: boolean - filesCountTotal: number + paginationInfo: FilePaginationInfo } -export function FilesTable({ files, isLoading, filesCountTotal }: FilesTableProps) { - const { table, rowSelection, setRowSelection } = useFilesTable(files) +export function FilesTable({ files, isLoading, paginationInfo }: FilesTableProps) { + const { table, fileSelection, selectAllFiles, clearFileSelection } = useFilesTable( + files, + paginationInfo + ) if (isLoading) { return } return ( -
+ <> - row.original)} + fileSelection={fileSelection} + selectAllRows={selectAllFiles} + totalFilesCount={paginationInfo.totalFiles} + 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 51bc69772..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,34 +1,29 @@ -import { createRowSelection, RowSelection } from '../useFilesTable' +import { FileSelection } from './useFileSelection' import { Button } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' import styles from './RowSelectionMessage.module.scss' interface RowSelectionMessageProps { - selectedFilesCount: number + fileSelection: FileSelection totalFilesCount: number - setRowSelection: (rowSelection: RowSelection) => void + selectAllRows: () => void + clearRowSelection: () => void } const MINIMUM_SELECTED_FILES_TO_SHOW_MESSAGE = 0 const MINIMUM_FILES_TO_SHOW_MESSAGE = 10 export function RowSelectionMessage({ - selectedFilesCount, + fileSelection, totalFilesCount, - setRowSelection + selectAllRows, + clearRowSelection }: RowSelectionMessageProps) { const { t } = useTranslation('files') + const selectedFilesCount = Object.keys(fileSelection).length const showMessage = totalFilesCount > MINIMUM_FILES_TO_SHOW_MESSAGE && selectedFilesCount > MINIMUM_SELECTED_FILES_TO_SHOW_MESSAGE - const selectAllRowsHandler = () => { - const rowSelectionAll = createRowSelection(totalFilesCount) - setRowSelection(rowSelectionAll) - } - const clearSelectionHandler = () => { - const rowSelectionNone = createRowSelection(0) - setRowSelection(rowSelectionNone) - } if (!showMessage) { return <> @@ -39,11 +34,11 @@ export function RowSelectionMessage({ {t('table.rowSelection.filesSelected', { count: selectedFilesCount })} {selectedFilesCount < totalFilesCount && ( - )} - 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..61bc82b40 --- /dev/null +++ b/src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection.ts @@ -0,0 +1,103 @@ +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({}) + } + + useEffect(() => { + setFileSelection(updateFileSelection()) + }, [currentPageSelectedRowModel]) + + useEffect(() => { + setCurrentPageRowSelection(computeCurrentPageRowSelection()) + }, [paginationInfo]) + + return { + fileSelection, + selectAllFiles, + clearFileSelection + } +} + +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/table-pagination/TablePagination.tsx b/src/sections/dataset/dataset-files/files-table/table-pagination/TablePagination.tsx deleted file mode 100644 index 38f4d9083..000000000 --- a/src/sections/dataset/dataset-files/files-table/table-pagination/TablePagination.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Pagination } from '@iqss/dataverse-design-system' -import { PageNumbersButtonsWithEllipsis } from './PageNumbersButtonsWithEllipsis' -import { PageSizeSelector } from './PageSizeSelector' -import styles from './TablePagination.module.scss' - -interface TablePaginationProps { - pageIndex: number - pageCount: number - pageSize: number - setPageSize: (pageSize: number) => void - goToPage: (pageIndex: number) => void - goToPreviousPage: () => void - goToNextPage: () => void - canGoToPreviousPage: boolean - canGoToNextPage: boolean -} - -export function TablePagination({ - pageIndex, - pageCount, - pageSize, - setPageSize, - goToPage, - goToPreviousPage, - goToNextPage, - canGoToPreviousPage, - canGoToNextPage -}: TablePaginationProps) { - if (pageCount === 0) { - return <> - } - return ( -
- - goToPage(0)} disabled={!canGoToPreviousPage} /> - goToPreviousPage()} disabled={!canGoToPreviousPage} /> - - goToNextPage()} disabled={!canGoToNextPage} /> - goToPage(pageCount - 1)} disabled={!canGoToNextPage} /> - - -
- ) -} diff --git a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx index 40e7e11c3..52cd528fb 100644 --- a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx @@ -1,43 +1,50 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { File } from '../../../../files/domain/models/File' -import { - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - useReactTable -} from '@tanstack/react-table' +import { getCoreRowModel, Row, useReactTable } from '@tanstack/react-table' import { columns } from './FilesTableColumnsDefinition' +import { FilePaginationInfo } from '../../../../files/domain/models/FilePaginationInfo' +import { useFileSelection } from './row-selection/useFileSelection' -export interface RowSelection { +export type RowSelection = { [key: string]: boolean } -export function useFilesTable(files: File[]) { - const [rowSelection, setRowSelection] = useState({}) - +export function useFilesTable(files: File[], paginationInfo: FilePaginationInfo) { + const [currentPageRowSelection, setCurrentPageRowSelection] = useState({}) + const [currentPageSelectedRowModel, setCurrentPageSelectedRowModel] = useState< + Record> + >({}) + const { fileSelection, selectAllFiles, clearFileSelection } = useFileSelection( + currentPageSelectedRowModel, + setCurrentPageRowSelection, + paginationInfo + ) const table = useReactTable({ data: files, - columns, + columns: columns, state: { - rowSelection + rowSelection: currentPageRowSelection }, enableRowSelection: true, - onRowSelectionChange: setRowSelection, + onRowSelectionChange: setCurrentPageRowSelection, getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - debugTable: true + manualPagination: true, + pageCount: paginationInfo.totalPages }) - return { table, rowSelection, setRowSelection } -} + useEffect(() => { + table.setPageSize(paginationInfo.pageSize) + table.setPageIndex(paginationInfo.page - 1) + }, [paginationInfo]) -export function createRowSelection(numberOfRows: number) { - const rowSelection: Record = {} + useEffect(() => { + setCurrentPageSelectedRowModel(table.getSelectedRowModel().rowsById) + }, [table.getSelectedRowModel().rowsById]) - for (let i = 0; i < numberOfRows; i++) { - rowSelection[i as unknown as string] = true + return { + table, + fileSelection, + selectAllFiles, + clearFileSelection } - - return rowSelection } 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/src/sections/dataset/dataset-files/useFiles.tsx b/src/sections/dataset/dataset-files/useFiles.tsx index 292e585b0..a4def71b4 100644 --- a/src/sections/dataset/dataset-files/useFiles.tsx +++ b/src/sections/dataset/dataset-files/useFiles.tsx @@ -5,11 +5,13 @@ import { getFilesByDatasetPersistentId } from '../../../files/domain/useCases/ge import { FileCriteria } from '../../../files/domain/models/FileCriteria' import { FilesCountInfo } from '../../../files/domain/models/FilesCountInfo' import { getFilesCountInfoByDatasetPersistentId } from '../../../files/domain/useCases/getFilesCountInfoByDatasetPersistentId' +import { FilePaginationInfo } from '../../../files/domain/models/FilePaginationInfo' export function useFiles( filesRepository: FileRepository, datasetPersistentId: string, datasetVersion?: string, + paginationInfo?: FilePaginationInfo, criteria?: FileCriteria ) { const [files, setFiles] = useState([]) @@ -21,9 +23,25 @@ export function useFiles( perFileTag: [] }) + useEffect(() => { + getFilesCountInfoByDatasetPersistentId(filesRepository, datasetPersistentId, datasetVersion) + .then((filesCountInfo: FilesCountInfo) => { + setFilesCountInfo(filesCountInfo) + }) + .catch((error) => { + console.error('There was an error getting the files count info', error) + }) + }, [filesRepository, datasetPersistentId, datasetVersion]) + useEffect(() => { setIsLoading(true) - getFilesByDatasetPersistentId(filesRepository, datasetPersistentId, datasetVersion, criteria) + getFilesByDatasetPersistentId( + filesRepository, + datasetPersistentId, + datasetVersion, + paginationInfo, + criteria + ) .then((files: File[]) => { setFiles(files) setIsLoading(false) @@ -32,17 +50,7 @@ export function useFiles( console.error('There was an error getting the files', error) setIsLoading(false) }) - }, [filesRepository, datasetPersistentId, datasetVersion, criteria]) - - useEffect(() => { - getFilesCountInfoByDatasetPersistentId(filesRepository, datasetPersistentId, datasetVersion) - .then((filesCountInfo: FilesCountInfo) => { - setFilesCountInfo(filesCountInfo) - }) - .catch((error) => { - console.error('There was an error getting the files count info', error) - }) - }, [filesRepository, datasetPersistentId, datasetVersion]) + }, [filesRepository, datasetPersistentId, datasetVersion, paginationInfo, criteria]) return { files, diff --git a/src/stories/dataset/dataset-files/files-pagination/FilesPagination.stories.tsx b/src/stories/dataset/dataset-files/files-pagination/FilesPagination.stories.tsx new file mode 100644 index 000000000..b090683ab --- /dev/null +++ b/src/stories/dataset/dataset-files/files-pagination/FilesPagination.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from '@storybook/react' +import { WithI18next } from '../../../WithI18next' +import { FilesPagination } from '../../../../sections/dataset/dataset-files/files-pagination/FilesPagination' + +const meta: Meta = { + title: 'Sections/Dataset Page/DatasetFiles/FilesPagination', + component: FilesPagination, + decorators: [WithI18next] +} + +export default meta +type Story = StoryObj + +const emptyFunction = () => {} +export const Default: Story = { + render: () => ( + + ) +} + +export const NoEllipsis: Story = { + render: () => ( + + ) +} diff --git a/src/stories/dataset/dataset-files/files-table/table-pagination/TablePagination.stories.tsx b/src/stories/dataset/dataset-files/files-table/table-pagination/TablePagination.stories.tsx deleted file mode 100644 index fe3788326..000000000 --- a/src/stories/dataset/dataset-files/files-table/table-pagination/TablePagination.stories.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react' -import { WithI18next } from '../../../../WithI18next' -import { TablePagination } from '../../../../../sections/dataset/dataset-files/files-table/table-pagination/TablePagination' - -const meta: Meta = { - title: 'Sections/Dataset Page/DatasetFiles/FilesTable/TablePagination', - component: TablePagination, - decorators: [WithI18next] -} - -export default meta -type Story = StoryObj - -const emptyFunction = () => {} -export const Default: Story = { - render: () => ( - - ) -} - -export const NoEllipsis: Story = { - render: () => ( - - ) -} diff --git a/src/stories/files/FileMockData.ts b/src/stories/files/FileMockData.ts index 0d7e70fed..3854a8b36 100644 --- a/src/stories/files/FileMockData.ts +++ b/src/stories/files/FileMockData.ts @@ -15,4 +15,4 @@ export function makeFiles(len: number): File[] { }) } -export const FilesMockData = (): File[] => makeFiles(200) +export const FilesMockData = (amount = 200): File[] => makeFiles(amount) diff --git a/src/stories/files/FileMockRepository.ts b/src/stories/files/FileMockRepository.ts index df1224a35..90bb32613 100644 --- a/src/stories/files/FileMockRepository.ts +++ b/src/stories/files/FileMockRepository.ts @@ -3,13 +3,18 @@ import { FilesMockData } from './FileMockData' import { File } from '../../files/domain/models/File' import { FilesCountInfo } from '../../files/domain/models/FilesCountInfo' import { FilesCountInfoMother } from '../../../tests/component/files/domain/models/FilesCountInfoMother' +import { FilePaginationInfo } from '../../files/domain/models/FilePaginationInfo' export class FileMockRepository implements FileRepository { // eslint-disable-next-line unused-imports/no-unused-vars - getAllByDatasetPersistentId(persistentId: string, version?: string): Promise { + getAllByDatasetPersistentId( + persistentId: string, + version?: string, + paginationInfo?: FilePaginationInfo + ): Promise { return new Promise((resolve) => { setTimeout(() => { - resolve(FilesMockData()) + resolve(FilesMockData(paginationInfo?.pageSize || 10)) }, 1000) }) } diff --git a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx index 701030599..2049c0c7d 100644 --- a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx @@ -1,19 +1,46 @@ import { FileMother } from '../../../files/domain/models/FileMother' import { DatasetFiles } from '../../../../../src/sections/dataset/dataset-files/DatasetFiles' import { FileRepository } from '../../../../../src/files/domain/repositories/FileRepository' -import { FileCriteria, FileSortByOption } from '../../../../../src/files/domain/models/FileCriteria' +import { + FileAccessOption, + FileCriteria, + FileSortByOption, + FileTag +} from '../../../../../src/files/domain/models/FileCriteria' import { FilesCountInfoMother } from '../../../files/domain/models/FilesCountInfoMother' -import { FileSize, FileSizeUnit } from '../../../../../src/files/domain/models/File' -import { ZipDownloadLimit } from '../../../../../src/settings/domain/models/ZipDownloadLimit' +import { FilePaginationInfo } from '../../../../../src/files/domain/models/FilePaginationInfo' +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' -import styles from '../../../../../src/sections/dataset/dataset-files/files-table/FilesTable.module.scss' -const testFiles = FileMother.createMany(200) +const testFiles = FileMother.createMany(10) const datasetPersistentId = 'test-dataset-persistent-id' const datasetVersion = 'test-dataset-version' const fileRepository: FileRepository = {} as FileRepository -const testFilesCountInfo = FilesCountInfoMother.create({ total: 200 }) +const testFilesCountInfo = FilesCountInfoMother.create({ + total: 200, + perFileType: [ + { + type: new FileType('text'), + count: 5 + }, + { + type: new FileType('image'), + count: 485 + } + ], + perAccess: [ + { access: FileAccessOption.PUBLIC, count: 222 }, + { access: FileAccessOption.RESTRICTED, count: 10 } + ], + perFileTag: [ + { tag: new FileTag('document'), count: 5 }, + { tag: new FileTag('code'), count: 10 } + ] +}) +const filePaginationInfo = new FilePaginationInfo(1, 10, 200) describe('DatasetFiles', () => { beforeEach(() => { fileRepository.getAllByDatasetPersistentId = cy.stub().resolves(testFiles) @@ -31,197 +58,282 @@ describe('DatasetFiles', () => { cy.findByRole('table').should('exist') cy.findByRole('columnheader', { name: /Files/ }).should('exist') - cy.get('table > thead > tr > th:nth-child(1) > input[type="checkbox"]').should('exist') - - testFiles.slice(0, 10).forEach((file) => { - cy.findByText(file.name).should('exist') - }) }) - it('renders the files table with the correct header on a page different than the first one ', () => { - cy.customMount( - - ) + describe('Pagination navigation', () => { + it('renders the files table with the correct header on a page different than the first one ', () => { + cy.customMount( + + ) - cy.findByRole('button', { name: '6' }).click() + cy.findByRole('button', { name: '6' }).click() - cy.findByRole('columnheader', { name: '51 to 60 of 200 Files' }).should('exist') - }) + cy.findByRole('columnheader', { name: '51 to 60 of 200 Files' }).should('exist') + }) - it('renders the files table with the correct header with a different page size ', () => { - cy.customMount( - - ) + it('renders the files table with the correct page selected after updating the pageSize', () => { + cy.customMount( + + ) - cy.findByLabelText('Files per page').select('50') - cy.findByRole('button', { name: '3' }).click() + cy.findByRole('button', { name: '3' }).click() - cy.findByRole('columnheader', { name: '101 to 150 of 200 Files' }).should('exist') - }) + cy.findByRole('columnheader', { name: '21 to 30 of 200 Files' }).should('exist') - it('renders the no files message when there are no files', () => { - fileRepository.getAllByDatasetPersistentId = cy.stub().resolves([]) - fileRepository.getCountInfoByDatasetPersistentId = cy - .stub() - .resolves(FilesCountInfoMother.createEmpty()) + cy.findByLabelText('Files per page').select('25') - cy.customMount( - - ) + cy.findByRole('columnheader', { name: '26 to 50 of 200 Files' }).should('exist') + }) - cy.findByRole('button', { name: /Sort/ }).should('not.exist') - cy.findByRole('button', { name: 'File Type: All' }).should('not.exist') - cy.findByRole('button', { name: 'Access: All' }).should('not.exist') - cy.findByRole('button', { name: 'File Tags: All' }).should('not.exist') - cy.findByText('There are no files in this dataset.').should('exist') - }) + it('renders the files table with the correct header with a different page size ', () => { + cy.customMount( + + ) - it('calls the useFiles hook with the correct parameters', () => { - cy.customMount( - - ) + cy.findByLabelText('Files per page').select('50') + cy.findByRole('button', { name: '3' }).click() - cy.wrap(fileRepository.getAllByDatasetPersistentId).should( - 'be.calledWith', - datasetPersistentId, - datasetVersion - ) - }) + cy.findByRole('columnheader', { name: '101 to 150 of 200 Files' }).should('exist') + }) - it('calls the useFiles hook with the correct parameters when criteria changes', () => { - cy.customMount( - - ) + it('maintains the selection when the page changes', () => { + cy.customMount( + + ) + cy.findByRole('columnheader', { name: '1 to 10 of 200 Files' }).should('exist') - cy.findByRole('button', { name: /Sort/ }).click() - cy.findByText('Name (A-Z)').should('exist').click() - cy.wrap(fileRepository.getAllByDatasetPersistentId).should( - 'be.calledWith', - datasetPersistentId, - datasetVersion, - new FileCriteria().withSortBy(FileSortByOption.NAME_AZ) - ) + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + cy.get('table > tbody > tr:nth-child(2)').should('have.class', styles['selected-row']) - cy.findByRole('button', { name: 'File Type: All' }).should('exist') - }) + cy.findByText('1 file is currently selected.').should('exist') - it('calls the useFiles hook with the correct parameters when searchText criteria changes', () => { - cy.customMount( - - ) + cy.findByRole('button', { name: 'Next' }).click() + cy.findByRole('columnheader', { name: '11 to 20 of 200 Files' }).should('exist') - cy.findByLabelText('Search').type('test{enter}') - cy.wrap(fileRepository.getAllByDatasetPersistentId).should( - 'be.calledWith', - datasetPersistentId, - datasetVersion, - new FileCriteria().withSearchText('test') - ) - }) + cy.get('table > tbody > tr:nth-child(2)').should('not.have.class', styles['selected-row']) - it("selects all rows when the 'Select all' button is clicked", () => { - cy.customMount( - - ) + cy.findByText('1 file is currently selected.').should('exist') - cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + cy.get('table > tbody > tr:nth-child(3) > td:nth-child(1) > input[type=checkbox]').click() + cy.get('table > tbody > tr:nth-child(3)').should('have.class', styles['selected-row']) - cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).click() + cy.findByText('2 files are currently selected.').should('exist') - cy.findByText('200 files are currently selected.').should('exist') + cy.findByRole('button', { name: 'Previous' }).click() + cy.findByRole('columnheader', { name: '1 to 10 of 200 Files' }).should('exist') - cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).should('not.exist') - }) + cy.get('table > tbody > tr:nth-child(2)').should('have.class', styles['selected-row']) + }) - it('clears the selection when the clear selection button is clicked', () => { - cy.customMount( - - ) + it('maintains the selection when the page size changes', () => { + cy.customMount( + + ) + cy.findByRole('columnheader', { name: '1 to 10 of 200 Files' }).should('exist') - cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + cy.get('table > tbody > tr:nth-child(2)').should('have.class', styles['selected-row']) - cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).click() + cy.findByText('1 file is currently selected.').should('exist') - cy.findByText('200 files are currently selected.').should('exist') + cy.findByLabelText('Files per page').select('25') - cy.findByRole('button', { name: 'Clear selection.' }).click() + cy.findByRole('columnheader', { name: '1 to 25 of 200 Files' }).should('exist') - cy.findByText(/files are currently selected./).should('not.exist') - }) + cy.get('table > tbody > tr:nth-child(2)').should('have.class', styles['selected-row']) - it('highlights the selected rows', () => { - cy.customMount( - - ) - cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + cy.findByText('1 file is currently selected.').should('exist') + }) - cy.get('table > tbody > tr:nth-child(2)').should('have.class', styles['selected-row']) + 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') + }) }) - it('renders the zip download limit message when the zip download limit is reached', () => { - const testFiles = [ - FileMother.create({ size: new FileSize(1024, FileSizeUnit.BYTES) }), - FileMother.create({ size: new FileSize(2048, FileSizeUnit.BYTES) }) - ] - fileRepository.getAllByDatasetPersistentId = cy.stub().resolves(testFiles) - const getSettingByName = cy - .stub() - .resolves(SettingMother.createZipDownloadLimit(new ZipDownloadLimit(500, FileSizeUnit.BYTES))) + describe('Calling use cases', () => { + it('calls the useFiles hook with the correct parameters', () => { + cy.customMount( + + ) + + cy.wrap(fileRepository.getAllByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion + ) + + cy.wrap(fileRepository.getCountInfoByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion + ) + }) - cy.customMount( - + it('calls the useFiles hook with the correct parameters when sortBy criteria changes', () => { + cy.customMount( - - ) + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Name (A-Z)').should('exist').click() + cy.wrap(fileRepository.getAllByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion, + filePaginationInfo, + new FileCriteria().withSortBy(FileSortByOption.NAME_AZ) + ) + }) + + it('calls the useFiles hook with the correct parameters when filterByType criteria changes', () => { + cy.customMount( + + ) + + cy.findByRole('button', { name: 'File Type: All' }).click() + cy.findByText('Image (485)').should('exist').click() + cy.wrap(fileRepository.getAllByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion, + filePaginationInfo, + new FileCriteria().withFilterByType('image') + ) + }) + + it('calls the useFiles hook with the correct parameters when filterByAccess criteria changes', () => { + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Access: All' }).click() + cy.findByText('Public (222)').should('exist').click() + cy.wrap(fileRepository.getAllByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion, + filePaginationInfo, + new FileCriteria().withFilterByAccess(FileAccessOption.PUBLIC) + ) + }) - cy.findByText( - 'The overall size of the files selected (3.0 KB) for download exceeds the zip limit of 500.0 B. Please unselect some files to continue.' - ).should('not.exist') - cy.get('table > tbody > tr:nth-child(1) > td:nth-child(1) > input[type=checkbox]').click() - cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + it('calls the useFiles hook with the correct parameters when filterByTag criteria changes', () => { + cy.customMount( + + ) + + cy.findByRole('button', { name: 'File Tags: All' }).click() + cy.findByText('Document (5)').should('exist').click() + cy.wrap(fileRepository.getAllByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion, + filePaginationInfo, + new FileCriteria().withFilterByTag('document') + ) + }) - cy.findByText( - 'The overall size of the files selected (3.0 KB) for download exceeds the zip limit of 500.0 B. Please unselect some files to continue.' - ).should('exist') + it('calls the useFiles hook with the correct parameters when searchText criteria changes', () => { + cy.customMount( + + ) + + cy.findByLabelText('Search').type('test{enter}') + cy.wrap(fileRepository.getAllByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion, + filePaginationInfo, + new FileCriteria().withSearchText('test') + ) + }) + + it('calls the useFiles hook with the correct parameters when paginationInfo changes', () => { + cy.customMount( + + ) + + cy.findByRole('button', { name: '5' }).click() + cy.wrap(fileRepository.getAllByDatasetPersistentId).should( + 'be.calledWith', + datasetPersistentId, + datasetVersion, + new FilePaginationInfo(5, 10, 200) + ) + }) }) }) diff --git a/tests/component/sections/dataset/dataset-files/files-pagination/FilesPagination.spec.tsx b/tests/component/sections/dataset/dataset-files/files-pagination/FilesPagination.spec.tsx new file mode 100644 index 000000000..df2a50ce6 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/files-pagination/FilesPagination.spec.tsx @@ -0,0 +1,112 @@ +import { FilesPagination } from '../../../../../../src/sections/dataset/dataset-files/files-pagination/FilesPagination' +import { FilePaginationInfo } from '../../../../../../src/files/domain/models/FilePaginationInfo' + +let paginationInfo: FilePaginationInfo +const page = 3 +const pageSize = 10 +const total = 200 +describe('FilesPagination', () => { + beforeEach(() => { + cy.viewport(1000, 1000) + paginationInfo = new FilePaginationInfo(page, pageSize, total) + }) + + it('clicking on the first page button calls goToPage 1', () => { + const onPaginationInfoChange = cy.stub().as('onPaginationInfoChange') + cy.customMount( + + ) + + cy.findByRole('button', { name: 'First' }).click() + cy.wrap(onPaginationInfoChange).should('have.been.calledWith', paginationInfo.goToPage(1)) + }) + + it('clicking on the previous page button calls goToPreviousPage', () => { + const onPaginationInfoChange = cy.stub().as('onPaginationInfoChange') + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Previous' }).click() + cy.wrap(onPaginationInfoChange).should( + 'have.been.calledWith', + paginationInfo.goToPreviousPage() + ) + }) + + it('clicking on a page button calls goToPage with the correct number', () => { + const onPaginationInfoChange = cy.stub().as('onPaginationInfoChange') + cy.customMount( + + ) + + cy.findByRole('button', { name: '5' }).click() + cy.wrap(onPaginationInfoChange).should('have.been.calledWith', paginationInfo.goToPage(5)) + }) + + it('clicking on the next page button calls goToNextPage', () => { + const onPaginationInfoChange = cy.stub().as('onPaginationInfoChange') + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Next' }).click() + cy.wrap(onPaginationInfoChange).should('have.been.calledWith', paginationInfo.goToNextPage()) + }) + + it('clicking on the last page button calls setPageIndex with the last index', () => { + const onPaginationInfoChange = cy.stub().as('onPaginationInfoChange') + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Last' }).click() + cy.wrap(onPaginationInfoChange).should('have.been.calledWith', paginationInfo.goToPage(20)) + }) + + it('selecting a page size calls setPageSize with the selected value', () => { + const onPaginationInfoChange = cy.stub().as('onPaginationInfoChange') + cy.customMount( + + ) + + cy.findByLabelText('Files per page').select('50') + cy.wrap(onPaginationInfoChange).should('have.been.calledWith', paginationInfo.withPageSize(50)) + + cy.findByRole('button', { name: 'Last' }).click() + cy.wrap(onPaginationInfoChange).should( + 'have.been.calledWith', + paginationInfo.withPageSize(50).goToPage(4) + ) + }) +}) diff --git a/tests/component/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtonsWithEllipsis.spec.tsx b/tests/component/sections/dataset/dataset-files/files-pagination/PageNumbersButtonsWithEllipsis.spec.tsx similarity index 90% rename from tests/component/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtonsWithEllipsis.spec.tsx rename to tests/component/sections/dataset/dataset-files/files-pagination/PageNumbersButtonsWithEllipsis.spec.tsx index 128584d9b..0b4fffc39 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtonsWithEllipsis.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-pagination/PageNumbersButtonsWithEllipsis.spec.tsx @@ -1,4 +1,4 @@ -import { PageNumbersButtonsWithEllipsis } from '../../../../../../../src/sections/dataset/dataset-files/files-table/table-pagination/PageNumbersButtonsWithEllipsis' +import { PageNumbersButtonsWithEllipsis } from '../../../../../../src/sections/dataset/dataset-files/files-pagination/PageNumbersButtonsWithEllipsis' let goToPage: (pageIndex: number) => void const selectedPageIndex = 3 @@ -46,10 +46,10 @@ describe('PageNumbersButtonsWithEllipsis', () => { ) cy.findByText('1').click() - cy.wrap(goToPage).should('have.been.calledWith', 0) + cy.wrap(goToPage).should('have.been.calledWith', 1) cy.findByText('7').click() - cy.wrap(goToPage).should('have.been.calledWith', 6) + cy.wrap(goToPage).should('have.been.calledWith', 7) }) it('renders both ellipsis correctly', () => { @@ -96,9 +96,9 @@ describe('PageNumbersButtonsWithEllipsis', () => { ) cy.findByText('1').click() - cy.wrap(goToPage).should('have.been.calledWith', 0) + cy.wrap(goToPage).should('have.been.calledWith', 1) cy.findByText('100').click() - cy.wrap(goToPage).should('have.been.calledWith', 99) + cy.wrap(goToPage).should('have.been.calledWith', 100) }) }) diff --git a/tests/component/sections/dataset/dataset-files/files-table/FilesTable.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/FilesTable.spec.tsx new file mode 100644 index 000000000..495f2b00b --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/files-table/FilesTable.spec.tsx @@ -0,0 +1,139 @@ +import { FileMother } from '../../../../files/domain/models/FileMother' +import { FilesTable } from '../../../../../../src/sections/dataset/dataset-files/files-table/FilesTable' +import { FileSize, FileSizeUnit } from '../../../../../../src/files/domain/models/File' +import { SettingMother } from '../../../../settings/domain/models/SettingMother' +import { ZipDownloadLimit } from '../../../../../../src/settings/domain/models/ZipDownloadLimit' +import { SettingsContext } from '../../../../../../src/sections/settings/SettingsContext' +import styles from '../../../../../../src/sections/dataset/dataset-files/files-table/FilesTable.module.scss' +import { FilePaginationInfo } from '../../../../../../src/files/domain/models/FilePaginationInfo' + +const testFiles = FileMother.createMany(10) +const paginationInfo = new FilePaginationInfo(1, 10, 200) +describe('FilesTable', () => { + it('renders the files table', () => { + cy.customMount( + + ) + + cy.findByRole('table').should('exist') + cy.findByRole('columnheader', { name: '1 to 10 of 200 Files' }).should('exist') + cy.get('table > thead > tr > th:nth-child(1) > input[type="checkbox"]').should('exist') + + testFiles.slice(0, 10).forEach((file) => { + cy.findByText(file.name).should('exist') + }) + }) + + it('renders the spinner when the data isLoading', () => { + cy.customMount( + + ) + + cy.findByLabelText('Files loading spinner symbol').should('exist') + }) + + it('renders the no files message when there are no files', () => { + cy.customMount() + + cy.findByText('There are no files in this dataset.').should('exist') + }) + + describe('Row selection', () => { + it('selects all rows in the current page when the header checkbox is clicked', () => { + cy.customMount( + + ) + + cy.wait(1000) // wait for the table to load + + cy.get('table > thead > tr > th > input[type=checkbox]').click() + + cy.findByText('10 files are currently selected.').should('exist') + + cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).should('exist') + }) + + it.only('clears row selection for the current page when the header checkbox is clicked', () => { + cy.customMount( + + ) + + cy.wait(1000) // wait for the table to load + + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).click() + + cy.findByText('200 files are currently selected.').should('exist') + + cy.get('table > thead > tr > th > input[type=checkbox]').click() + + cy.findByText('190 files are currently selected.').should('exist') + }) + + it("selects all rows when the 'Select all' button is clicked", () => { + cy.customMount( + + ) + + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).click() + + cy.findByText('200 files are currently selected.').should('exist') + + cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).should('not.exist') + }) + + it('clears the selection when the clear selection button is clicked', () => { + cy.customMount( + + ) + + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).click() + + cy.findByText('200 files are currently selected.').should('exist') + + cy.findByRole('button', { name: 'Clear selection.' }).click() + + cy.findByText(/files are currently selected./).should('not.exist') + }) + + it('highlights the selected rows', () => { + cy.customMount( + + ) + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + + cy.get('table > tbody > tr:nth-child(2)').should('have.class', styles['selected-row']) + }) + }) + + it('renders the zip download limit message when the zip download limit is reached', () => { + const testFiles = [ + FileMother.create({ size: new FileSize(1024, FileSizeUnit.BYTES) }), + FileMother.create({ size: new FileSize(2048, FileSizeUnit.BYTES) }) + ] + const getSettingByName = cy + .stub() + .resolves(SettingMother.createZipDownloadLimit(new ZipDownloadLimit(500, FileSizeUnit.BYTES))) + + cy.customMount( + + + + ) + + cy.findByText( + 'The overall size of the files selected (3.0 KB) for download exceeds the zip limit of 500.0 B. Please unselect some files to continue.' + ).should('not.exist') + cy.get('table > tbody > tr:nth-child(1) > td:nth-child(1) > input[type=checkbox]').click() + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByText( + 'The overall size of the files selected (3.0 KB) for download exceeds the zip limit of 500.0 B. Please unselect some files to continue.' + ).should('exist') + }) +}) 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 b028b50bb..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,10 +1,22 @@ import { RowSelectionMessage } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage' -import { createRowSelection } from '../../../../../../../src/sections/dataset/dataset-files/files-table/useFilesTable' +import { createRowSelection } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection' +let selectAllRows = () => {} +let clearRowSelection = () => {} describe('RowSelectionMessage', () => { + beforeEach(() => { + selectAllRows = cy.stub().as('selectAllRows') + clearRowSelection = cy.stub().as('clearRowSelection') + }) + it('renders the message when there are more than 10 files and some row is selected', () => { cy.customMount( - {}} /> + ) cy.findByText('1 file is currently selected.') @@ -14,7 +26,12 @@ describe('RowSelectionMessage', () => { it('does not render the message when there are less than 10 files', () => { cy.customMount( - {}} /> + ) cy.findByText('1 file is currently selected.').should('not.exist') @@ -24,7 +41,12 @@ describe('RowSelectionMessage', () => { it('does not render the message when there are more than 10 files but no row is selected', () => { cy.customMount( - {}} /> + ) cy.findByText('1 file is currently selected.').should('not.exist') @@ -34,41 +56,44 @@ describe('RowSelectionMessage', () => { it('renders the plural form of the message when there is more than 1 row selected', () => { cy.customMount( - {}} /> + ) cy.findByText('2 files are currently selected.') }) - it("calls setRowSelection when the 'Select all' button is clicked", () => { - const setRowSelection = cy.stub().as('setRowSelection') - + it("calls selectAllRows when the 'Select all' button is clicked", () => { cy.customMount( ) cy.findByRole('button', { name: 'Select all 11 files in this dataset.' }).click() - cy.wrap(setRowSelection).should('be.calledWith', createRowSelection(11)) + cy.wrap(selectAllRows).should('be.called') }) - it("calls setRowSelection when the 'Clear selection.' button is clicked", () => { - const setRowSelection = cy.stub().as('setRowSelection') - + it("calls clearRowSelection when the 'Clear selection.' button is clicked", () => { cy.customMount( ) cy.findByRole('button', { name: 'Clear selection.' }).click() - cy.wrap(setRowSelection).should('be.calledWith', createRowSelection(0)) + cy.wrap(clearRowSelection).should('be.called') }) }) diff --git a/tests/component/sections/dataset/dataset-files/files-table/table-pagination/TablePaginatioin.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/table-pagination/TablePaginatioin.spec.tsx deleted file mode 100644 index f36f2408d..000000000 --- a/tests/component/sections/dataset/dataset-files/files-table/table-pagination/TablePaginatioin.spec.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { TablePagination } from '../../../../../../../src/sections/dataset/dataset-files/files-table/table-pagination/TablePagination' - -let pageIndex: number -let pageCount: number -let pageSize: number -let setPageSize: (pageSize: number) => void -let goToPage: (pageIndex: number) => void -let goToPreviousPage: () => void -let goToNextPage: () => void -let canGoToPreviousPage: boolean -let canGoToNextPage: boolean - -describe('TalePagination', () => { - beforeEach(() => { - pageIndex = 2 - pageCount = 200 - pageSize = 10 - setPageSize = cy.stub().resolves() - goToPage = cy.stub().resolves() - goToPreviousPage = cy.stub().resolves() - goToNextPage = cy.stub().resolves() - canGoToPreviousPage = true - canGoToNextPage = true - }) - - it('clicking on the first page button calls goToPage 0', () => { - cy.customMount( - - ) - - cy.findByRole('button', { name: 'First' }).click() - cy.wrap(goToPage).should('have.been.calledWith', 0) - }) - - it('clicking on the previous page button calls previousPage', () => { - cy.customMount( - - ) - - cy.findByRole('button', { name: 'Previous' }).click() - cy.wrap(goToPreviousPage).should('have.been.called') - }) - - it('clicking on a page button calls setPageIndex with the correct index', () => { - cy.customMount( - - ) - - cy.findByRole('button', { name: '5' }).click() - cy.wrap(goToPage).should('have.been.calledWith', 4) - }) - - it('clicking on the next page button calls nextPage', () => { - cy.customMount( - - ) - - cy.findByRole('button', { name: 'Next' }).click() - cy.wrap(goToNextPage).should('have.been.called') - }) - - it('clicking on the last page button calls setPageIndex with the last index', () => { - cy.customMount( - - ) - - cy.findByRole('button', { name: 'Last' }).click() - cy.wrap(goToPage).should('have.been.calledWith', 199) - }) - - it('selecting a page size calls setPageSize with the selected value', () => { - cy.customMount( - - ) - - cy.findByLabelText('Files per page').select('50') - cy.wrap(setPageSize).should('have.been.calledWith', 50) - }) -}) diff --git a/tests/component/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.spec.tsx index cfff711bf..bc4fe4f08 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.spec.tsx @@ -5,13 +5,13 @@ import { SettingMother } from '../../../../../settings/domain/models/SettingMoth import { ZipDownloadLimit } from '../../../../../../../src/settings/domain/models/ZipDownloadLimit' import { SettingsContext } from '../../../../../../../src/sections/settings/SettingsContext' -const files = [ - FileMother.create({ size: new FileSize(1024, FileSizeUnit.BYTES) }), - FileMother.create({ size: new FileSize(2048, FileSizeUnit.BYTES) }) -] +const fileSelection = { + '1': FileMother.create({ size: new FileSize(1024, FileSizeUnit.BYTES) }), + '2': FileMother.create({ size: new FileSize(2048, FileSizeUnit.BYTES) }) +} const zipDownloadLimit = new ZipDownloadLimit(500, FileSizeUnit.BYTES) describe('ZipDownloadLimitMessage', () => { - 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( - + )