diff --git a/dev.Dockerfile b/dev.Dockerfile index cf14c4c37..6f2e59663 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -7,7 +7,9 @@ RUN npm run build WORKDIR /usr/src/app COPY package.json ./ +COPY package-lock.json ./ COPY .npmrc ./ +RUN npm uninstall --save-dev lerna RUN npm install FROM node:19.6.1-alpine diff --git a/packages/design-system/src/lib/components/dropdown-button/dropdown-button-item/DropdownButtonItem.tsx b/packages/design-system/src/lib/components/dropdown-button/dropdown-button-item/DropdownButtonItem.tsx index a7a3218f8..4c60d9340 100644 --- a/packages/design-system/src/lib/components/dropdown-button/dropdown-button-item/DropdownButtonItem.tsx +++ b/packages/design-system/src/lib/components/dropdown-button/dropdown-button-item/DropdownButtonItem.tsx @@ -1,15 +1,15 @@ import { Dropdown as DropdownBS } from 'react-bootstrap' -import { ReactNode } from 'react' +import React, { ReactNode } from 'react' -interface DropdownItemProps { +interface DropdownItemProps extends React.HTMLAttributes { href?: string eventKey?: string children: ReactNode } -export function DropdownButtonItem({ href, eventKey, children }: DropdownItemProps) { +export function DropdownButtonItem({ href, eventKey, children, ...props }: DropdownItemProps) { return ( - + {children} ) diff --git a/packages/design-system/src/lib/components/dropdown-button/dropdown-separator/DropdownSeparator.tsx b/packages/design-system/src/lib/components/dropdown-button/dropdown-separator/DropdownSeparator.tsx new file mode 100644 index 000000000..a0cad787a --- /dev/null +++ b/packages/design-system/src/lib/components/dropdown-button/dropdown-separator/DropdownSeparator.tsx @@ -0,0 +1,5 @@ +import { Dropdown } from 'react-bootstrap' + +export function DropdownSeparator() { + return +} diff --git a/packages/design-system/src/lib/index.ts b/packages/design-system/src/lib/index.ts index 806a2a202..36d59114b 100644 --- a/packages/design-system/src/lib/index.ts +++ b/packages/design-system/src/lib/index.ts @@ -2,6 +2,7 @@ export { Badge } from './components/badge/Badge' export { Button } from './components/button/Button' export { DropdownButton } from './components/dropdown-button/DropdownButton' export { DropdownButtonItem } from './components/dropdown-button/dropdown-button-item/DropdownButtonItem' +export { DropdownSeparator } from './components/dropdown-button/dropdown-separator/DropdownSeparator' export { Col } from './components/grid/Col' export { Container } from './components/grid/Container' export { Row } from './components/grid/Row' diff --git a/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx b/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx index af3eec95b..8d79e937a 100644 --- a/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx +++ b/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx @@ -3,6 +3,7 @@ import { DropdownButtonItem } from '../../components/dropdown-button/dropdown-bu import { DropdownButton } from '../../components/dropdown-button/DropdownButton' import { IconName } from '../../components/icon/IconName' import { CanvasFixedHeight } from '../CanvasFixedHeight' +import { DropdownSeparator } from '../../components/dropdown-button/dropdown-separator/DropdownSeparator' /** * ## Description @@ -117,6 +118,19 @@ export const WithIcon: Story = { ) } +export const WithSeparatorBetweenOptions: Story = { + render: () => ( + + + Item 1 + Item 2 + + Item 3 + + + ) +} + /** * This is an example use case for a navigation dropdown button. */ diff --git a/packages/design-system/tests/component/dropdown-button/DropdownButton.spec.tsx b/packages/design-system/tests/component/dropdown-button/DropdownButton.spec.tsx index b457ae4f2..0cc593167 100644 --- a/packages/design-system/tests/component/dropdown-button/DropdownButton.spec.tsx +++ b/packages/design-system/tests/component/dropdown-button/DropdownButton.spec.tsx @@ -3,6 +3,7 @@ import { IconName } from '../../../src/lib/components/icon/IconName' import styles from '../../../src/lib/components/dropdown-button/DropdownButton.module.scss' import { ArrowClockwise } from 'react-bootstrap-icons' import DropdownItem from 'react-bootstrap/DropdownItem' +import { DropdownSeparator } from '../../../src/lib' const titleText = 'My Dropdown Button' @@ -96,4 +97,18 @@ describe('DropdownButton', () => { cy.wrap(onSelect).should('be.calledWith', '1') }) + + it('renders with separator', () => { + cy.mount( + + Item 1 + Item 2 + + Item 3 + + ) + cy.findByText(titleText).click() + + cy.findByRole('separator').should('exist') + }) }) diff --git a/public/locales/en/files.json b/public/locales/en/files.json index 30442ed68..f16ad7693 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -49,6 +49,25 @@ "size": "Size", "type": "Type" } + }, + "filters": { + "title": "Filter by" + }, + "filterByType": { + "title": "Filter Type" + }, + "filterByTag": { + "title": "Filter Tag" + }, + "filterByAccess": { + "title": "Access", + "options": { + "all": "All", + "public": "Public", + "restricted": "Restricted", + "embargoed_public": "Embargoed then Public", + "embargoed_restricted": "Embargoed then Restricted" + } } } } diff --git a/src/files/domain/models/File.ts b/src/files/domain/models/File.ts index caba31701..34f18281d 100644 --- a/src/files/domain/models/File.ts +++ b/src/files/domain/models/File.ts @@ -73,13 +73,26 @@ export interface FileLabel { value: string } +export class FileType { + constructor(readonly value: string) {} + + toDisplayFormat(): string { + const words = this.value.split(' ') + return words + .map((word) => { + return word[0].toUpperCase() + word.substring(1) + }) + .join(' ') + } +} + export class File { constructor( readonly id: string, readonly version: FileVersion, readonly name: string, readonly access: FileAccess, - readonly type: string, + readonly type: FileType, readonly size: FileSize, readonly date: FileDate, readonly downloads: number, diff --git a/src/files/domain/models/FileCriteria.ts b/src/files/domain/models/FileCriteria.ts index 1b96fbb63..97f6f8c69 100644 --- a/src/files/domain/models/FileCriteria.ts +++ b/src/files/domain/models/FileCriteria.ts @@ -1,5 +1,32 @@ -export interface FileCriteria { - sortBy?: FileSortByOption +import { FileType } from './File' + +export class FileCriteria { + constructor( + public readonly sortBy: FileSortByOption = FileSortByOption.NAME_AZ, + public readonly filterByType?: FileType, + public readonly filterByAccess?: FileAccessOption, + public readonly filterByTag?: FileTag + ) {} + + withSortBy(sortBy: FileSortByOption): FileCriteria { + return new FileCriteria(sortBy, this.filterByType, this.filterByAccess, this.filterByTag) + } + + withFilterByType(filterByType: string | undefined): FileCriteria { + const newFilterByType = filterByType === undefined ? undefined : new FileType(filterByType) + + return new FileCriteria(this.sortBy, newFilterByType, this.filterByAccess, this.filterByTag) + } + + withFilterByAccess(filterByAccess: FileAccessOption | undefined): FileCriteria { + return new FileCriteria(this.sortBy, this.filterByType, filterByAccess, this.filterByTag) + } + + withFilterByTag(filterByTag: string | undefined): FileCriteria { + const newFilterByTag = filterByTag === undefined ? undefined : new FileTag(filterByTag) + + return new FileCriteria(this.sortBy, this.filterByType, this.filterByAccess, newFilterByTag) + } } export enum FileSortByOption { @@ -10,3 +37,18 @@ export enum FileSortByOption { SIZE = 'size', TYPE = 'type' } + +export enum FileAccessOption { + PUBLIC = 'public', + RESTRICTED = 'restricted', + EMBARGOED = 'embargoed_public', + EMBARGOED_RESTRICTED = 'embargoed_restricted' +} + +export class FileTag { + constructor(readonly value: string) {} + + toDisplayFormat(): string { + return this.value[0].toUpperCase() + this.value.substring(1) + } +} diff --git a/src/files/domain/models/FilesCountInfo.ts b/src/files/domain/models/FilesCountInfo.ts new file mode 100644 index 000000000..d8a12ee99 --- /dev/null +++ b/src/files/domain/models/FilesCountInfo.ts @@ -0,0 +1,24 @@ +import { FileType } from './File' +import { FileAccessOption, FileTag } from './FileCriteria' + +export interface FilesCountInfo { + total: number + perFileType: FileTypeCount[] + perAccess: FileAccessCount[] + perFileTag: FileTagCount[] +} + +export interface FileTypeCount { + type: FileType + count: number +} + +export interface FileAccessCount { + access: FileAccessOption + count: number +} + +export interface FileTagCount { + tag: FileTag + count: number +} diff --git a/src/files/domain/repositories/FileRepository.ts b/src/files/domain/repositories/FileRepository.ts index 9d84f2988..f17f29259 100644 --- a/src/files/domain/repositories/FileRepository.ts +++ b/src/files/domain/repositories/FileRepository.ts @@ -1,5 +1,6 @@ import { File } from '../models/File' import { FileCriteria } from '../models/FileCriteria' +import { FilesCountInfo } from '../models/FilesCountInfo' export interface FileRepository { getAllByDatasetPersistentId: ( @@ -7,4 +8,8 @@ export interface FileRepository { version?: string, criteria?: FileCriteria ) => Promise + getCountInfoByDatasetPersistentId: ( + datasetPersistentId: string, + version?: string + ) => Promise } diff --git a/src/files/domain/useCases/getFilesCountInfoByDatasetPersistentId.ts b/src/files/domain/useCases/getFilesCountInfoByDatasetPersistentId.ts new file mode 100644 index 000000000..efae7bf26 --- /dev/null +++ b/src/files/domain/useCases/getFilesCountInfoByDatasetPersistentId.ts @@ -0,0 +1,15 @@ +import { FileRepository } from '../repositories/FileRepository' +import { FileVersionNotNumber } from '../models/File' +import { FilesCountInfo } from '../models/FilesCountInfo' + +export async function getFilesCountInfoByDatasetPersistentId( + fileRepository: FileRepository, + persistentId: string, + version: string = FileVersionNotNumber.LATEST +): Promise { + return fileRepository + .getCountInfoByDatasetPersistentId(persistentId, version) + .catch((error: Error) => { + throw new Error(error.message) + }) +} diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index d4203d263..ce8b035cc 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -1,6 +1,8 @@ import { FileRepository } from '../domain/repositories/FileRepository' 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' export class FileJSDataverseRepository implements FileRepository { // eslint-disable-next-line unused-imports/no-unused-vars @@ -12,4 +14,16 @@ export class FileJSDataverseRepository implements FileRepository { }, 1000) }) } + // eslint-disable-next-line unused-imports/no-unused-vars + getCountInfoByDatasetPersistentId( + persistentId: string, + version?: string + ): Promise { + // TODO - implement using js-dataverse + return new Promise((resolve) => { + setTimeout(() => { + resolve(FilesCountInfoMother.create()) + }, 1000) + }) + } } diff --git a/src/sections/dataset/dataset-files/DatasetFiles.tsx b/src/sections/dataset/dataset-files/DatasetFiles.tsx index bc0e9448c..f4ae991ba 100644 --- a/src/sections/dataset/dataset-files/DatasetFiles.tsx +++ b/src/sections/dataset/dataset-files/DatasetFiles.tsx @@ -1,11 +1,9 @@ -import { useFilesTable } from './files-table/useFilesTable' import { FileRepository } from '../../../files/domain/repositories/FileRepository' -import { useFiles } from './useFiles' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { FilesTable } from './files-table/FilesTable' -import { SpinnerSymbol } from './files-table/spinner-symbol/SpinnerSymbol' -import { FileCriteriaInputs } from './file-criteria-inputs/FileCriteriaInputs' +import { FileCriteriaControls } from './file-criteria-controls/FileCriteriaControls' import { FileCriteria } from '../../../files/domain/models/FileCriteria' +import { useFiles } from './useFiles' interface DatasetFilesProps { filesRepository: FileRepository @@ -18,30 +16,25 @@ export function DatasetFiles({ datasetPersistentId, datasetVersion }: DatasetFilesProps) { - const [criteria, setCriteria] = useState() - const { files, isLoading } = useFiles( + const [criteria, setCriteria] = useState(new FileCriteria()) + const { files, isLoading, filesCountInfo } = useFiles( filesRepository, datasetPersistentId, datasetVersion, criteria ) - const { table, setFilesTableData } = useFilesTable() const handleCriteriaChange = (newCriteria: FileCriteria) => { - setCriteria((criteria) => ({ ...criteria, ...newCriteria })) - } - - useEffect(() => { - setFilesTableData(files) - }, [files]) - - if (isLoading) { - return + setCriteria(newCriteria) } return ( <> - {files.length !== 0 && } - + + ) } diff --git a/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.module.scss b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.module.scss new file mode 100644 index 000000000..f31547962 --- /dev/null +++ b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.module.scss @@ -0,0 +1,32 @@ +@import "node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module"; +@import "node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/typography.module"; + +.criteria-section { + margin-bottom: 1em; +} + +.sort-container { + display: flex; + align-items: end; + justify-content: end; +} + +.icon { + margin-right: 0.3em; + margin-bottom: 0.2em; +} + +.text-filter-by { + margin-left: 7px; + color: $dv-subtext-color; + font-size: $dv-font-size-sm; +} + +.selected-option { + font-weight: $dv-font-weight-bold; +} + +.filters-container { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.tsx b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.tsx new file mode 100644 index 000000000..26620fe17 --- /dev/null +++ b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.tsx @@ -0,0 +1,39 @@ +import { FileCriteria } from '../../../../files/domain/models/FileCriteria' +import { Col, Row } from '@iqss/dataverse-design-system' +import styles from './FileCriteriaControls.module.scss' +import { FileCriteriaSortBy } from './FileCriteriaSortBy' +import { FileCriteriaFilters } from './FileCriteriaFilters' +import { FilesCountInfo } from '../../../../files/domain/models/FilesCountInfo' + +interface FileCriteriaInputsProps { + criteria: FileCriteria + onCriteriaChange: (criteria: FileCriteria) => void + filesCountInfo: FilesCountInfo +} + +const MINIMUM_FILES_TO_SHOW_CRITERIA_INPUTS = 2 + +export function FileCriteriaControls({ + criteria, + onCriteriaChange, + filesCountInfo +}: FileCriteriaInputsProps) { + if (filesCountInfo.total < MINIMUM_FILES_TO_SHOW_CRITERIA_INPUTS) { + return <> + } + + return ( + + + + + + + + + ) +} diff --git a/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByAccess.tsx b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByAccess.tsx new file mode 100644 index 000000000..7ff1b4351 --- /dev/null +++ b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByAccess.tsx @@ -0,0 +1,63 @@ +import { FileAccessOption, FileCriteria } from '../../../../files/domain/models/FileCriteria' +import { + DropdownButton, + DropdownButtonItem, + DropdownSeparator +} from '@iqss/dataverse-design-system' +import { FilesCountInfo } from '../../../../files/domain/models/FilesCountInfo' +import styles from './FileCriteriaControls.module.scss' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +interface FileCriteriaFilterByAccessProps { + criteria: FileCriteria + onCriteriaChange: (criteria: FileCriteria) => void + filesCountInfo: FilesCountInfo +} + +export function FileCriteriaFilterByAccess({ + criteria, + onCriteriaChange, + filesCountInfo +}: FileCriteriaFilterByAccessProps) { + const { t } = useTranslation('files') + const [selectedAccess, setSelectedAccess] = useState(criteria.filterByAccess ?? 'all') + const handleAccessChange = (eventKey: string | null) => { + if (selectedAccess !== eventKey) { + setSelectedAccess(eventKey as string) + onCriteriaChange( + criteria.withFilterByAccess(eventKey === 'all' ? undefined : (eventKey as FileAccessOption)) + ) + } + } + + if (filesCountInfo.perAccess.length === 0) { + return <> + } + + return ( + + + {t('criteria.filterByAccess.options.all')} + + + {filesCountInfo.perAccess.map(({ access, count }) => ( + + {`${t(`criteria.filterByAccess.options.${access}`)} (${count})`} + + ))} + + ) +} diff --git a/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByTag.tsx b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByTag.tsx new file mode 100644 index 000000000..638511e9e --- /dev/null +++ b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByTag.tsx @@ -0,0 +1,64 @@ +import { FileCriteria, FileTag } from '../../../../files/domain/models/FileCriteria' +import { + DropdownButton, + DropdownButtonItem, + DropdownSeparator +} from '@iqss/dataverse-design-system' +import { FilesCountInfo } from '../../../../files/domain/models/FilesCountInfo' +import styles from './FileCriteriaControls.module.scss' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +interface FileCriteriaFilterByTagProps { + criteria: FileCriteria + onCriteriaChange: (criteria: FileCriteria) => void + filesCountInfo: FilesCountInfo +} + +export function FileCriteriaFilterByTag({ + criteria, + onCriteriaChange, + filesCountInfo +}: FileCriteriaFilterByTagProps) { + const { t } = useTranslation('files') + const [selectedTag, setSelectedTag] = useState( + criteria.filterByTag ?? new FileTag('all') + ) + const handleTagChange = (eventKey: string | null) => { + if (selectedTag.value !== eventKey) { + setSelectedTag(new FileTag(eventKey as string)) + + onCriteriaChange( + criteria.withFilterByTag(eventKey === 'all' ? undefined : (eventKey as string)) + ) + } + } + + if (filesCountInfo.perFileTag.length === 0) { + return <> + } + + return ( + + + All + + + {filesCountInfo.perFileTag.map(({ tag, count }) => ( + + {`${tag.toDisplayFormat()} (${count})`} + + ))} + + ) +} diff --git a/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByType.tsx b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByType.tsx new file mode 100644 index 000000000..92f6b0e99 --- /dev/null +++ b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByType.tsx @@ -0,0 +1,64 @@ +import { FileCriteria } from '../../../../files/domain/models/FileCriteria' +import { + DropdownButton, + DropdownButtonItem, + DropdownSeparator +} from '@iqss/dataverse-design-system' +import { FilesCountInfo } from '../../../../files/domain/models/FilesCountInfo' +import styles from './FileCriteriaControls.module.scss' +import { useState } from 'react' +import { FileType } from '../../../../files/domain/models/File' +import { useTranslation } from 'react-i18next' + +interface FileCriteriaFilterByTypeProps { + criteria: FileCriteria + onCriteriaChange: (criteria: FileCriteria) => void + filesCountInfo: FilesCountInfo +} + +export function FileCriteriaFilterByType({ + criteria, + onCriteriaChange, + filesCountInfo +}: FileCriteriaFilterByTypeProps) { + const { t } = useTranslation('files') + const [selectedType, setSelectedType] = useState( + criteria.filterByType ?? new FileType('all') + ) + const handleTypeChange = (eventKey: string | null) => { + if (selectedType.value !== eventKey) { + setSelectedType(new FileType(eventKey as string)) + onCriteriaChange( + criteria.withFilterByType(eventKey === 'all' ? undefined : (eventKey as string)) + ) + } + } + + if (filesCountInfo.perFileType.length === 0) { + return <> + } + + return ( + + + All + + + {filesCountInfo.perFileType.map(({ type, count }) => ( + + {`${type.toDisplayFormat()} (${count})`} + + ))} + + ) +} diff --git a/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilters.tsx b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilters.tsx new file mode 100644 index 000000000..36fe1bd29 --- /dev/null +++ b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilters.tsx @@ -0,0 +1,51 @@ +import { FileCriteria } from '../../../../files/domain/models/FileCriteria' +import { FilesCountInfo } from '../../../../files/domain/models/FilesCountInfo' +import styles from './FileCriteriaControls.module.scss' +import { FileCriteriaFilterByType } from './FileCriteriaFilterByType' +import { FileCriteriaFilterByAccess } from './FileCriteriaFilterByAccess' +import { FileCriteriaFilterByTag } from './FileCriteriaFilterByTag' +import { useTranslation } from 'react-i18next' + +interface FileCriteriaFiltersProps { + criteria: FileCriteria + onCriteriaChange: (criteria: FileCriteria) => void + filesCountInfo: FilesCountInfo +} +export function FileCriteriaFilters({ + criteria, + onCriteriaChange, + filesCountInfo +}: FileCriteriaFiltersProps) { + const { t } = useTranslation('files') + const noFiltersCanBeApplied = + filesCountInfo.perAccess.length === 0 && + filesCountInfo.perFileType.length === 0 && + filesCountInfo.perFileTag.length === 0 + + if (noFiltersCanBeApplied) { + return <> + } + + return ( + <> + {t('criteria.filters.title')} +
+ + + +
+ + ) +} diff --git a/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaSortBy.tsx b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaSortBy.tsx new file mode 100644 index 000000000..34bc8ff58 --- /dev/null +++ b/src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaSortBy.tsx @@ -0,0 +1,36 @@ +import { ArrowDownUp } from 'react-bootstrap-icons' +import styles from './FileCriteriaControls.module.scss' +import { FileCriteria, FileSortByOption } from '../../../../files/domain/models/FileCriteria' +import { DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' +import { useTranslation } from 'react-i18next' + +export function FileCriteriaSortBy({ + criteria, + onCriteriaChange +}: { + criteria: FileCriteria + onCriteriaChange: (criteria: FileCriteria) => void +}) { + const { t } = useTranslation('files') + const handleSortChange = (eventKey: string | null) => { + onCriteriaChange(criteria.withSortBy(eventKey as FileSortByOption)) + } + + return ( + + } + title={t('criteria.sortBy.title')} + id="files-table-sort-by" + variant="secondary" + withSpacing + onSelect={handleSortChange}> + {Object.values(FileSortByOption).map((sortByOption) => ( + + {t(`criteria.sortBy.options.${sortByOption}`)} + + ))} + + ) +} diff --git a/src/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.module.scss b/src/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.module.scss deleted file mode 100644 index d5bc4d9b2..000000000 --- a/src/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -.criteria-section { - margin-bottom: 1em; -} - -.sort-container { - display: flex; - justify-content: end; -} - -.icon { - margin-right: 0.3em; - margin-bottom: 0.2em; -} diff --git a/src/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.tsx b/src/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.tsx deleted file mode 100644 index f1d3337c0..000000000 --- a/src/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FileCriteria, FileSortByOption } from '../../../../files/domain/models/FileCriteria' -import { Col, DropdownButton, DropdownButtonItem, Row } from '@iqss/dataverse-design-system' -import styles from './FileCriteriaInputs.module.scss' -import { ArrowDownUp } from 'react-bootstrap-icons' -import { useTranslation } from 'react-i18next' - -export function FileCriteriaInputs({ - onCriteriaChange -}: { - onCriteriaChange: (criteria: FileCriteria) => void -}) { - const { t } = useTranslation('files') - const handleSortChange = (eventKey: string | null) => { - onCriteriaChange({ sortBy: eventKey as FileSortByOption }) - } - - return ( - - - - } - title={t('criteria.sortBy.title')} - id="files-table-sort-by" - variant="secondary" - onSelect={handleSortChange}> - {Object.values(FileSortByOption).map((sortByOption) => ( - - {t(`criteria.sortBy.options.${sortByOption}`)} - - ))} - - - - ) -} diff --git a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx index caa4e9871..2f0320ea0 100644 --- a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx @@ -1,15 +1,24 @@ import { Col, Row, Table } from '@iqss/dataverse-design-system' -import { Table as TableModel } from '@tanstack/react-table' import { FilesTableHeader } from './FilesTableHeader' import { FilesTableBody } from './FilesTableBody' import { TablePagination } from './table-pagination/TablePagination' -import { File } from '../../../../files/domain/models/File' import styles from './FilesTable.module.scss' +import { useFilesTable } from './useFilesTable' +import { SpinnerSymbol } from './spinner-symbol/SpinnerSymbol' +import { File } from '../../../../files/domain/models/File' interface FilesTableProps { - table: TableModel + files: File[] + isLoading: boolean } -export function FilesTable({ table }: FilesTableProps) { + +export function FilesTable({ files, isLoading }: FilesTableProps) { + const { table } = useFilesTable(files) + + if (isLoading) { + return + } + return (
diff --git a/src/sections/dataset/dataset-files/files-table/file-info-cell/FileType.tsx b/src/sections/dataset/dataset-files/files-table/file-info-cell/FileType.tsx index 70b196279..576534780 100644 --- a/src/sections/dataset/dataset-files/files-table/file-info-cell/FileType.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-info-cell/FileType.tsx @@ -1,7 +1,7 @@ -import { FileSize } from '../../../../../files/domain/models/File' +import { FileSize, FileType as FileTypeModel } from '../../../../../files/domain/models/File' interface FileTypeProps { - type: string + type: FileTypeModel size: FileSize } @@ -9,15 +9,8 @@ export function FileType({ type, size }: FileTypeProps) { return (
- {capitalizeFirstLetter(type)} - {size.toString()} + {type.toDisplayFormat()} - {size.toString()}
) } - -function capitalizeFirstLetter(str: string): string { - if (str.length === 0) { - return str - } - return str.charAt(0).toUpperCase() + str.slice(1) -} diff --git a/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.tsx b/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.tsx index b551a95e1..e9cde4591 100644 --- a/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.tsx @@ -1,13 +1,13 @@ import { FileThumbnailIcon } from './FileThumbnailIcon' import { FileThumbnailPreviewImage } from './FileThumbnailPreviewImage' -import { FileAccess } from '../../../../../../files/domain/models/File' +import { FileAccess, FileType } from '../../../../../../files/domain/models/File' import { FileThumbnailRestrictedIcon } from './FileThumbnailRestrictedIcon' import styles from './FileThumbnail.module.scss' interface FileThumbnailProps { thumbnail?: string | undefined name: string - type: string + type: FileType access: FileAccess } diff --git a/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnailIcon.tsx b/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnailIcon.tsx index d59220b74..abb3f4cc8 100644 --- a/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnailIcon.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnailIcon.tsx @@ -1,5 +1,6 @@ import styles from './FileThumbnail.module.scss' import { IconName } from '@iqss/dataverse-design-system' +import { FileType } from '../../../../../../files/domain/models/File' const TYPE_TO_ICON: Record = { archive: IconName.PACKAGE, @@ -19,8 +20,8 @@ const TYPE_TO_ICON: Record = { other: IconName.OTHER } -export function FileThumbnailIcon({ type }: { type: string }) { - const icon = TYPE_TO_ICON[type] || TYPE_TO_ICON.default +export function FileThumbnailIcon({ type }: { type: FileType }) { + const icon = TYPE_TO_ICON[type.value] || TYPE_TO_ICON.default return ( diff --git a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx index ad741f29f..faf246b67 100644 --- a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx @@ -8,12 +8,11 @@ import { } from '@tanstack/react-table' import { columns } from './FilesTableColumnsDefinition' -export function useFilesTable() { - const [data, setFilesTableData] = useState(() => []) +export function useFilesTable(files: File[]) { const [rowSelection, setRowSelection] = useState({}) const table = useReactTable({ - data, + data: files, columns, state: { rowSelection @@ -26,5 +25,5 @@ export function useFilesTable() { debugTable: true }) - return { table, setFilesTableData } + return { table } } diff --git a/src/sections/dataset/dataset-files/useFiles.tsx b/src/sections/dataset/dataset-files/useFiles.tsx index e47ef01ce..292e585b0 100644 --- a/src/sections/dataset/dataset-files/useFiles.tsx +++ b/src/sections/dataset/dataset-files/useFiles.tsx @@ -3,6 +3,8 @@ import { FileRepository } from '../../../files/domain/repositories/FileRepositor import { File } from '../../../files/domain/models/File' import { getFilesByDatasetPersistentId } from '../../../files/domain/useCases/getFilesByDatasetPersistentId' import { FileCriteria } from '../../../files/domain/models/FileCriteria' +import { FilesCountInfo } from '../../../files/domain/models/FilesCountInfo' +import { getFilesCountInfoByDatasetPersistentId } from '../../../files/domain/useCases/getFilesCountInfoByDatasetPersistentId' export function useFiles( filesRepository: FileRepository, @@ -12,6 +14,12 @@ export function useFiles( ) { const [files, setFiles] = useState([]) const [isLoading, setIsLoading] = useState(true) + const [filesCountInfo, setFilesCountInfo] = useState({ + total: 0, + perFileType: [], + perAccess: [], + perFileTag: [] + }) useEffect(() => { setIsLoading(true) @@ -24,10 +32,21 @@ export function useFiles( console.error('There was an error getting the files', error) setIsLoading(false) }) - }, [filesRepository, datasetPersistentId, criteria]) + }, [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]) return { files, - isLoading + isLoading, + filesCountInfo } } diff --git a/src/stories/dataset/dataset-files/DatasetFiles.stories.tsx b/src/stories/dataset/dataset-files/DatasetFiles.stories.tsx index f1a49aa5a..a401a8e4a 100644 --- a/src/stories/dataset/dataset-files/DatasetFiles.stories.tsx +++ b/src/stories/dataset/dataset-files/DatasetFiles.stories.tsx @@ -5,6 +5,7 @@ import { DatasetMockData } from '../DatasetMockData' import { FileMockRepository } from '../../files/FileMockRepository' import { FileMockLoadingRepository } from '../../files/FileMockLoadingRepository' import { FileMockNoDataRepository } from '../../files/FileMockNoDataRepository' +import { FileMockNoFiltersRepository } from '../../files/FileMockNoFiltersRepository' const meta: Meta = { title: 'Sections/Dataset Page/DatasetFiles', @@ -46,3 +47,13 @@ export const NoFiles: Story = { /> ) } + +export const NoFilters: Story = { + render: () => ( + + ) +} diff --git a/src/stories/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.stories.tsx b/src/stories/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.stories.tsx index 06da5cdc1..b02b93a70 100644 --- a/src/stories/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.stories.tsx +++ b/src/stories/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail.stories.tsx @@ -3,6 +3,7 @@ import { faker } from '@faker-js/faker' import { FileThumbnail } from '../../../../../../sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail' import { WithI18next } from '../../../../../WithI18next' import { FileMother } from '../../../../../../../tests/component/files/domain/models/FileMother' +import { FileType } from '../../../../../../files/domain/models/File' const meta: Meta = { title: 'Sections/Dataset Page/DatasetFiles/FilesTable/FileInfoCell/FileThumbnail', @@ -16,7 +17,7 @@ type Story = StoryObj export const WithIcon: Story = { render: () => { const file = FileMother.create({ - type: 'some-type', + type: new FileType('some-type'), access: { restricted: false, canDownload: true }, thumbnail: undefined }) diff --git a/src/stories/files/FileMockLoadingRepository.ts b/src/stories/files/FileMockLoadingRepository.ts index 5d6b9a991..65a5fa7d1 100644 --- a/src/stories/files/FileMockLoadingRepository.ts +++ b/src/stories/files/FileMockLoadingRepository.ts @@ -1,5 +1,6 @@ import { FileRepository } from '../../files/domain/repositories/FileRepository' import { File } from '../../files/domain/models/File' +import { FilesCountInfo } from '../../files/domain/models/FilesCountInfo' export class FileMockLoadingRepository implements FileRepository { // eslint-disable-next-line unused-imports/no-unused-vars @@ -10,4 +11,16 @@ export class FileMockLoadingRepository implements FileRepository { }, 0) }) } + // eslint-disable-next-line unused-imports/no-unused-vars + getCountInfoByDatasetPersistentId( + persistentId: string, + version?: string + ): Promise { + // TODO - implement using js-dataverse + return new Promise((resolve) => { + setTimeout(() => { + // Do nothing + }, 1000) + }) + } } diff --git a/src/stories/files/FileMockNoDataRepository.ts b/src/stories/files/FileMockNoDataRepository.ts index 9409b01c8..bdc5c0fa1 100644 --- a/src/stories/files/FileMockNoDataRepository.ts +++ b/src/stories/files/FileMockNoDataRepository.ts @@ -1,5 +1,7 @@ import { FileRepository } from '../../files/domain/repositories/FileRepository' import { File } from '../../files/domain/models/File' +import { FilesCountInfo } from '../../files/domain/models/FilesCountInfo' +import { FilesCountInfoMother } from '../../../tests/component/files/domain/models/FilesCountInfoMother' export class FileMockNoDataRepository implements FileRepository { // eslint-disable-next-line unused-imports/no-unused-vars @@ -10,4 +12,16 @@ export class FileMockNoDataRepository implements FileRepository { }, 1000) }) } + // eslint-disable-next-line unused-imports/no-unused-vars + getCountInfoByDatasetPersistentId( + persistentId: string, + version?: string + ): Promise { + // TODO - implement using js-dataverse + return new Promise((resolve) => { + setTimeout(() => { + resolve(FilesCountInfoMother.createEmpty()) + }, 1000) + }) + } } diff --git a/src/stories/files/FileMockNoFiltersRepository.ts b/src/stories/files/FileMockNoFiltersRepository.ts new file mode 100644 index 000000000..7ab2aebe1 --- /dev/null +++ b/src/stories/files/FileMockNoFiltersRepository.ts @@ -0,0 +1,28 @@ +import { FileRepository } from '../../files/domain/repositories/FileRepository' +import { File } from '../../files/domain/models/File' +import { FilesCountInfo } from '../../files/domain/models/FilesCountInfo' +import { FilesCountInfoMother } from '../../../tests/component/files/domain/models/FilesCountInfoMother' +import { FilesMockData } from './FileMockData' + +export class FileMockNoFiltersRepository implements FileRepository { + // eslint-disable-next-line unused-imports/no-unused-vars + getAllByDatasetPersistentId(persistentId: string, version?: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(FilesMockData()) + }, 1000) + }) + } + // eslint-disable-next-line unused-imports/no-unused-vars + getCountInfoByDatasetPersistentId( + persistentId: string, + version?: string + ): Promise { + // TODO - implement using js-dataverse + return new Promise((resolve) => { + setTimeout(() => { + resolve(FilesCountInfoMother.createOnlyTotal()) + }, 1000) + }) + } +} diff --git a/src/stories/files/FileMockRepository.ts b/src/stories/files/FileMockRepository.ts index 826dea01a..4c4e8904b 100644 --- a/src/stories/files/FileMockRepository.ts +++ b/src/stories/files/FileMockRepository.ts @@ -1,6 +1,8 @@ import { FileRepository } from '../../files/domain/repositories/FileRepository' 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' export class FileMockRepository implements FileRepository { // eslint-disable-next-line unused-imports/no-unused-vars @@ -11,4 +13,16 @@ export class FileMockRepository implements FileRepository { }, 1000) }) } + // eslint-disable-next-line unused-imports/no-unused-vars + getCountInfoByDatasetPersistentId( + persistentId: string, + version?: string + ): Promise { + // TODO - implement using js-dataverse + return new Promise((resolve) => { + setTimeout(() => { + resolve(FilesCountInfoMother.create()) + }, 1000) + }) + } } diff --git a/tests/component/files/domain/models/FileMother.ts b/tests/component/files/domain/models/FileMother.ts index 18ade6f69..8587181e6 100644 --- a/tests/component/files/domain/models/FileMother.ts +++ b/tests/component/files/domain/models/FileMother.ts @@ -8,6 +8,7 @@ import { FileSize, FileSizeUnit, FileStatus, + FileType, FileVersion } from '../../../../../src/files/domain/models/File' @@ -35,7 +36,7 @@ export class FileMother { minorNumber: faker.datatype.number(), status: faker.helpers.arrayElement(Object.values(FileStatus)) }, - type: thumbnail ? 'image' : fileType, + type: new FileType(thumbnail ? 'image' : fileType), size: { value: faker.datatype.number({ max: 1024, precision: 2 }), unit: faker.helpers.arrayElement(Object.values(FileSizeUnit)) @@ -101,7 +102,7 @@ export class FileMother { static createDefault(props?: Partial): File { const defaultFile = { - type: 'file', + type: new FileType('file'), version: { majorNumber: 1, minorNumber: 0, @@ -146,7 +147,7 @@ export class FileMother { static createWithTabularData(): File { return this.createDefault({ - type: 'tabular data', + type: new FileType('tabular data'), tabularData: { variablesCount: faker.datatype.number(100), observationsCount: faker.datatype.number(100), diff --git a/tests/component/files/domain/models/FilesCountInfoMother.tsx b/tests/component/files/domain/models/FilesCountInfoMother.tsx new file mode 100644 index 000000000..bee4ec9be --- /dev/null +++ b/tests/component/files/domain/models/FilesCountInfoMother.tsx @@ -0,0 +1,61 @@ +import { FileType } from '../../../../../src/files/domain/models/File' +import { faker } from '@faker-js/faker' +import { FilesCountInfo } from '../../../../../src/files/domain/models/FilesCountInfo' +import { FileAccessOption, FileTag } from '../../../../../src/files/domain/models/FileCriteria' + +export class FilesCountInfoMother { + static create(props?: Partial): FilesCountInfo { + return { + total: faker.datatype.number(), + perFileType: [ + { + type: new FileType(faker.system.fileType()), + count: faker.datatype.number() + }, + { + type: new FileType(faker.system.fileType()), + count: faker.datatype.number() + } + ], + perAccess: [ + { + access: faker.helpers.arrayElement(Object.values(FileAccessOption)), + count: faker.datatype.number() + }, + { + access: faker.helpers.arrayElement(Object.values(FileAccessOption)), + count: faker.datatype.number() + } + ], + perFileTag: [ + { + tag: new FileTag(faker.lorem.word()), + count: faker.datatype.number() + }, + { + tag: new FileTag(faker.lorem.word()), + count: faker.datatype.number() + } + ], + ...props + } + } + + static createEmpty(): FilesCountInfo { + return { + total: 0, + perFileType: [], + perAccess: [], + perFileTag: [] + } + } + + static createOnlyTotal(): FilesCountInfo { + return { + total: faker.datatype.number(), + perFileType: [], + perAccess: [], + perFileTag: [] + } + } +} diff --git a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx index 100513dcb..3d7af822d 100644 --- a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx @@ -1,14 +1,18 @@ 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 { FilesCountInfoMother } from '../../../files/domain/models/FilesCountInfoMother' const testFiles = FileMother.createMany(200) const datasetPersistentId = 'test-dataset-persistent-id' const datasetVersion = 'test-dataset-version' const fileRepository: FileRepository = {} as FileRepository +const testFilesCountInfo = FilesCountInfoMother.create({ total: 200 }) describe('DatasetFiles', () => { beforeEach(() => { fileRepository.getAllByDatasetPersistentId = cy.stub().resolves(testFiles) + fileRepository.getCountInfoByDatasetPersistentId = cy.stub().resolves(testFilesCountInfo) }) it('renders the files table', () => { @@ -60,6 +64,9 @@ describe('DatasetFiles', () => { it('renders the no files message when there are no files', () => { fileRepository.getAllByDatasetPersistentId = cy.stub().resolves([]) + fileRepository.getCountInfoByDatasetPersistentId = cy + .stub() + .resolves(FilesCountInfoMother.createEmpty()) cy.customMount( { /> ) + cy.findByRole('button', { name: /Sort/ }).should('not.exist') + cy.findByRole('button', { name: 'Filter Type: All' }).should('not.exist') + cy.findByRole('button', { name: 'Access: All' }).should('not.exist') + cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') cy.findByText('There are no files in this dataset.').should('exist') }) @@ -103,21 +114,9 @@ describe('DatasetFiles', () => { 'be.calledWith', datasetPersistentId, datasetVersion, - { sortBy: 'name_az' } + new FileCriteria().withSortBy(FileSortByOption.NAME_AZ) ) - }) - - it('does not render the files criteria inputs when there are no files', () => { - fileRepository.getAllByDatasetPersistentId = cy.stub().resolves([]) - cy.customMount( - - ) - - cy.findByRole('button', { name: /Sort/ }).should('not.exist') + cy.findByRole('button', { name: 'Filter Type: All' }).should('exist') }) }) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.spec.tsx new file mode 100644 index 000000000..32050ecb3 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.spec.tsx @@ -0,0 +1,182 @@ +import { FileCriteriaControls } from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls' +import { + FileAccessOption, + FileCriteria, + FileTag +} from '../../../../../../src/files/domain/models/FileCriteria' +import { FilesCountInfoMother } from '../../../../files/domain/models/FilesCountInfoMother' +import { FileType } from '../../../../../../src/files/domain/models/File' + +let onCriteriaChange = () => {} +const filesCountInfo = FilesCountInfoMother.create({ + perFileType: [ + { + type: new FileType('image'), + count: 5 + }, + { + type: new FileType('text'), + count: 10 + } + ], + perAccess: [ + { + access: FileAccessOption.PUBLIC, + count: 5 + }, + { + access: FileAccessOption.RESTRICTED, + count: 10 + } + ], + perFileTag: [ + { + tag: new FileTag('document'), + count: 5 + }, + { + tag: new FileType('data'), + count: 10 + } + ] +}) +describe('FileCriteriaControls', () => { + beforeEach(() => { + onCriteriaChange = cy.stub().as('onCriteriaChange') + }) + + it('renders the SortBy input', () => { + const filesCountInfo = FilesCountInfoMother.createOnlyTotal() + cy.customMount( + + ) + + cy.findByRole('button', { name: /Sort/ }).should('exist') + cy.findByRole('button', { name: 'Filter Type: All' }).should('not.exist') + cy.findByRole('button', { name: 'Access: All' }).should('not.exist') + cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') + }) + + it('renders the Filters input', () => { + cy.customMount( + + ) + + cy.findByRole('button', { name: /Sort/ }).should('exist') + cy.findByRole('button', { name: 'Filter Type: All' }).should('exist') + cy.findByText('Filter by').should('exist') + }) + + it('saves global criteria when the sort by option changes', () => { + const criteria = new FileCriteria() + .withFilterByTag('document') + .withFilterByAccess(FileAccessOption.PUBLIC) + .withFilterByType('image') + + cy.customMount( + + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Oldest').click() + + cy.findByRole('button', { name: 'Filter Type: Image' }).should('exist') + cy.findByRole('button', { name: 'Access: Public' }).should('exist') + cy.findByRole('button', { name: 'Filter Tag: Document' }).should('exist') + }) + + it('saves global criteria when the filter by type option changes', () => { + const criteria = new FileCriteria() + .withFilterByTag('document') + .withFilterByAccess(FileAccessOption.PUBLIC) + .withFilterByType('image') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByText('Text (10)').click() + + cy.findByRole('button', { name: 'Filter Type: Text' }).should('exist') + cy.findByRole('button', { name: 'Access: Public' }).should('exist') + cy.findByRole('button', { name: 'Filter Tag: Document' }).should('exist') + }) + + it('saves global criteria when the filter by access option changes', () => { + const criteria = new FileCriteria() + .withFilterByTag('document') + .withFilterByAccess(FileAccessOption.PUBLIC) + .withFilterByType('image') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Access: Public' }).click() + cy.findByText('Restricted (10)').click() + + cy.findByRole('button', { name: 'Filter Type: Image' }).should('exist') + cy.findByRole('button', { name: 'Access: Restricted' }).should('exist') + cy.findByRole('button', { name: 'Filter Tag: Document' }).should('exist') + }) + + it('saves global criteria when the filter by tag option changes', () => { + const criteria = new FileCriteria() + .withFilterByTag('document') + .withFilterByAccess(FileAccessOption.PUBLIC) + .withFilterByType('image') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByText('Data (10)').click() + + cy.findByRole('button', { name: 'Filter Type: Image' }).should('exist') + cy.findByRole('button', { name: 'Access: Public' }).should('exist') + cy.findByRole('button', { name: 'Filter Tag: Data' }).should('exist') + }) + + it('does not render the files criteria inputs when there are less than 2 files', () => { + const filesCountInfo = FilesCountInfoMother.create({ total: 1 }) + const criteria = new FileCriteria() + cy.customMount( + + ) + + cy.findByRole('button', { name: /Sort/ }).should('not.exist') + cy.findByRole('button', { name: 'Filter Type: All' }).should('not.exist') + cy.findByRole('button', { name: 'Access: All' }).should('not.exist') + cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') + }) +}) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByAccess.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByAccess.spec.tsx new file mode 100644 index 000000000..dacdf92c2 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByAccess.spec.tsx @@ -0,0 +1,123 @@ +import { + FileAccessOption, + FileCriteria +} from '../../../../../../src/files/domain/models/FileCriteria' +import { FilesCountInfoMother } from '../../../../files/domain/models/FilesCountInfoMother' +import styles from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.module.scss' +import { FileCriteriaFilterByAccess } from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByAccess' + +const defaultCriteria = new FileCriteria() +const filesCountInfo = FilesCountInfoMother.create({ + perAccess: [ + { + access: FileAccessOption.PUBLIC, + count: 5 + }, + { + access: FileAccessOption.RESTRICTED, + count: 10 + } + ] +}) + +describe('FilesCriteriaFilterByAccess', () => { + it('renders filter by access options', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Access: All' }).click() + + cy.findByText('All').should('exist') + cy.findByText('Public (5)').should('exist') + cy.findByText('Restricted (10)').should('exist') + }) + + it('calls onCriteriaChange with the selected filter by access value', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Access: All' }).click() + cy.findByText('Public (5)').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withFilterByAccess(FileAccessOption.PUBLIC) + ) + + cy.findByRole('button', { name: 'Access: Public' }).click() + cy.findByText('Restricted (10)').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withFilterByAccess(FileAccessOption.RESTRICTED) + ) + }) + + it('shows the selected filter in the dropdown title', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + const criteria = defaultCriteria.withFilterByAccess(FileAccessOption.PUBLIC) + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Access: Public' }).click() + cy.findByText('All').should('exist').click() + cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByAccess(undefined)) + }) + + it('changes the filter option text to bold when selected', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Access: All' }).click() + cy.findByText('All').should('have.class', styles['selected-option']) + + cy.findByRole('button', { name: 'Public (5)' }).click() + cy.findByRole('button', { name: 'Access: Public' }).click() + cy.findByText('Public (5)').should('have.class', styles['selected-option']) + + cy.findByRole('button', { name: 'Restricted (10)' }).click() + cy.findByRole('button', { name: 'Access: Restricted' }).click() + cy.findByText('Restricted (10)').should('have.class', styles['selected-option']) + }) + + it('does not show the filter by access options when there are no filter options', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + const filesCountInfo = FilesCountInfoMother.create({ + perAccess: [] + }) + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Access: All' }).should('not.exist') + }) +}) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByTag.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByTag.spec.tsx new file mode 100644 index 000000000..49d36d2d2 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByTag.spec.tsx @@ -0,0 +1,116 @@ +import { FileCriteria, FileTag } from '../../../../../../src/files/domain/models/FileCriteria' +import { FilesCountInfoMother } from '../../../../files/domain/models/FilesCountInfoMother' +import { FileType } from '../../../../../../src/files/domain/models/File' +import styles from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.module.scss' +import { FileCriteriaFilterByTag } from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByTag' + +const defaultCriteria = new FileCriteria() +const filesCountInfo = FilesCountInfoMother.create({ + perFileTag: [ + { + tag: new FileTag('document'), + count: 5 + }, + { + tag: new FileType('data'), + count: 10 + } + ] +}) + +describe('FilesCriteriaFilterByTag', () => { + it('renders filter by tag options', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Tag: All' }).click() + + cy.findByText('All').should('exist') + cy.findByText('Document (5)').should('exist') + cy.findByText('Data (10)').should('exist') + }) + + it('calls onCriteriaChange with the selected filter by tag value', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Tag: All' }).click() + cy.findByText('Document (5)').click() + cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByTag('document')) + + cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByText('Data (10)').click() + cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByTag('data')) + }) + + it('shows the selected filter in the dropdown title', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + const criteria = defaultCriteria.withFilterByTag('document') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByText('All').should('exist').click() + cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType(undefined)) + }) + + it('changes the filter option text to bold when selected', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Tag: All' }).click() + cy.findByText('All').should('have.class', styles['selected-option']) + + cy.findByRole('button', { name: 'Document (5)' }).click() + cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByText('Document (5)').should('have.class', styles['selected-option']) + + cy.findByRole('button', { name: 'Data (10)' }).click() + cy.findByRole('button', { name: 'Filter Tag: Data' }).click() + cy.findByText('Data (10)').should('have.class', styles['selected-option']) + }) + + it('does not show the filter by tag dropdown when there are no filter options', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + const filesCountInfo = FilesCountInfoMother.create({ + perFileTag: [] + }) + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') + }) +}) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByType.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByType.spec.tsx new file mode 100644 index 000000000..2a28d5164 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByType.spec.tsx @@ -0,0 +1,114 @@ +import { FileCriteria } from '../../../../../../src/files/domain/models/FileCriteria' +import { FilesCountInfoMother } from '../../../../files/domain/models/FilesCountInfoMother' +import { FileType } from '../../../../../../src/files/domain/models/File' +import styles from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaControls.module.scss' +import { FileCriteriaFilterByType } from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilterByType' + +const defaultCriteria = new FileCriteria() +const filesCountInfo = FilesCountInfoMother.create({ + perFileType: [ + { + type: new FileType('image'), + count: 5 + }, + { + type: new FileType('text'), + count: 10 + } + ] +}) + +describe('FilesCriteriaFilterByType', () => { + it('renders filter by type options', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Type: All' }).click() + + cy.findByText('All').should('exist') + cy.findByText('Image (5)').should('exist') + cy.findByText('Text (10)').should('exist') + }) + + it('calls onCriteriaChange with the selected filter by type value', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Type: All' }).click() + cy.findByText('Image (5)').click() + cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType('image')) + + cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByText('Text (10)').click() + cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType('text')) + }) + + it('shows the selected filter in the dropdown title', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + const criteria = defaultCriteria.withFilterByType('image') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByText('All').should('exist').click() + cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType(undefined)) + }) + + it('changes the filter option text to bold when selected', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Type: All' }).click() + cy.findByText('All').should('have.class', styles['selected-option']) + + cy.findByRole('button', { name: 'Image (5)' }).click() + cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByText('Image (5)').should('have.class', styles['selected-option']) + + cy.findByRole('button', { name: 'Text (10)' }).click() + cy.findByRole('button', { name: 'Filter Type: Text' }).click() + cy.findByText('Text (10)').should('have.class', styles['selected-option']) + }) + + it('does not render the filter by type dropdown if there are no filter options', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + const criteria = defaultCriteria.withFilterByType('image') + + cy.customMount( + + ) + + cy.findByRole('button', { name: 'Filter Type: Image' }).should('not.exist') + }) +}) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilters.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilters.spec.tsx new file mode 100644 index 000000000..009701d05 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilters.spec.tsx @@ -0,0 +1,57 @@ +import { FileCriteria } from '../../../../../../src/files/domain/models/FileCriteria' +import { FileCriteriaFilters } from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaFilters' +import { FilesCountInfoMother } from '../../../../files/domain/models/FilesCountInfoMother' +import { FileType } from '../../../../../../src/files/domain/models/File' + +const defaultCriteria = new FileCriteria() +const filesCountInfo = FilesCountInfoMother.create({ + perFileType: [ + { + type: new FileType('image'), + count: 5 + }, + { + type: new FileType('text'), + count: 10 + } + ] +}) + +describe('FilesCriteriaFilters', () => { + it('renders filters by type options', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByText('Filter by').should('exist') + + cy.findByRole('button', { name: 'Filter Type: All' }).should('exist') + cy.findByRole('button', { name: 'Access: All' }).should('exist') + cy.findByRole('button', { name: 'Filter Tag: All' }).should('exist') + }) + + it('does not render filters by type options when there are no filters to be applied', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + const filesCountInfo = FilesCountInfoMother.createOnlyTotal() + + cy.customMount( + + ) + + cy.findByText('Filter by').should('not.exist') + + cy.findByRole('button', { name: 'Filter Type: All' }).should('not.exist') + cy.findByRole('button', { name: 'Access: All' }).should('not.exist') + cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') + }) +}) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaSortBy.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaSortBy.spec.tsx new file mode 100644 index 000000000..9d522af56 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaSortBy.spec.tsx @@ -0,0 +1,58 @@ +import { FileCriteriaSortBy } from '../../../../../../src/sections/dataset/dataset-files/file-criteria-controls/FileCriteriaSortBy' +import { + FileCriteria, + FileSortByOption +} from '../../../../../../src/files/domain/models/FileCriteria' + +const defaultCriteria = new FileCriteria() +describe('FilesCriteriaSortBy', () => { + it('calls onCriteriaChange with the selected orderBy value', () => { + const onCriteriaChange = cy.stub().as('onCriteriaChange') + + cy.customMount( + + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Name (A-Z)').should('exist').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withSortBy(FileSortByOption.NAME_AZ) + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Name (Z-A)').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withSortBy(FileSortByOption.NAME_ZA) + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Newest').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withSortBy(FileSortByOption.NEWEST) + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Oldest').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withSortBy(FileSortByOption.OLDEST) + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Size').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withSortBy(FileSortByOption.SIZE) + ) + + cy.findByRole('button', { name: /Sort/ }).click() + cy.findByText('Type').click() + cy.wrap(onCriteriaChange).should( + 'be.calledWith', + defaultCriteria.withSortBy(FileSortByOption.TYPE) + ) + }) +}) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.spec.tsx deleted file mode 100644 index 66337fd3e..000000000 --- a/tests/component/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs.spec.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FileCriteriaInputs } from '../../../../../../src/sections/dataset/dataset-files/file-criteria-inputs/FileCriteriaInputs' - -describe('FilesCriteriaInputs', () => { - it('calls onCriteriaChange with the selected orderBy value', () => { - const onCriteriaChange = cy.stub().as('onCriteriaChange') - - cy.customMount() - - cy.findByRole('button', { name: /Sort/ }).click() - cy.findByText('Name (A-Z)').should('exist').click() - cy.wrap(onCriteriaChange).should('be.calledWith', { sortBy: 'name_az' }) - - cy.findByRole('button', { name: /Sort/ }).click() - cy.findByText('Name (Z-A)').click() - cy.wrap(onCriteriaChange).should('be.calledWith', { sortBy: 'name_za' }) - - cy.findByRole('button', { name: /Sort/ }).click() - cy.findByText('Newest').click() - cy.wrap(onCriteriaChange).should('be.calledWith', { sortBy: 'newest' }) - - cy.findByRole('button', { name: /Sort/ }).click() - cy.findByText('Oldest').click() - cy.wrap(onCriteriaChange).should('be.calledWith', { sortBy: 'oldest' }) - - cy.findByRole('button', { name: /Sort/ }).click() - cy.findByText('Size').click() - cy.wrap(onCriteriaChange).should('be.calledWith', { sortBy: 'size' }) - - cy.findByRole('button', { name: /Sort/ }).click() - cy.findByText('Type').click() - cy.wrap(onCriteriaChange).should('be.calledWith', { sortBy: 'type' }) - }) -}) diff --git a/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/FileType.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/FileType.spec.tsx index 13f99017c..78f2aa631 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/FileType.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/FileType.spec.tsx @@ -6,13 +6,6 @@ describe('FileType', () => { const file = FileMother.create() cy.customMount() - cy.findByText(`${capitalizeFirstLetter(file.type)} - ${file.size.toString()}`).should('exist') + cy.findByText(`${file.type.toDisplayFormat()} - ${file.size.toString()}`).should('exist') }) }) - -function capitalizeFirstLetter(str: string): string { - if (str.length === 0) { - return str - } - return str.charAt(0).toUpperCase() + str.slice(1) -} diff --git a/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/file-thumbnail/FileThumbnail.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/file-thumbnail/FileThumbnail.spec.tsx index 7c59685d0..a1d7cffcf 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/file-thumbnail/FileThumbnail.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/files-info-cell/file-thumbnail/FileThumbnail.spec.tsx @@ -1,11 +1,12 @@ import { FileThumbnail } from '../../../../../../../../src/sections/dataset/dataset-files/files-table/file-info-cell/file-thumbnail/FileThumbnail' import { FileMother } from '../../../../../../files/domain/models/FileMother' +import { FileType } from '../../../../../../../../src/files/domain/models/File' describe('FileThumbnail', () => { it('renders FileThumbnailPreviewImage when thumbnail is provided', () => { const file = FileMother.create({ access: { restricted: false, canDownload: true }, - thumbnail: 'thumbnail' + thumbnail: 'thumbnail?' }) cy.customMount( @@ -53,7 +54,7 @@ describe('FileThumbnail', () => { const file = FileMother.create({ access: { restricted: true, canDownload: false }, thumbnail: 'thumbnail', - type: 'image' + type: new FileType('image') }) cy.customMount( @@ -75,7 +76,7 @@ describe('FileThumbnail', () => { it('renders FileThumbnailIcon when thumbnail is not provided', () => { const file = FileMother.create({ - type: 'some-type', + type: new FileType('some-type'), access: { restricted: false, canDownload: true } }) @@ -89,7 +90,7 @@ describe('FileThumbnail', () => { it('renders FileThumbnailIcon when thumbnail is not provided with lock icon when restricted with no access', () => { const file = FileMother.create({ - type: 'some-type', + type: new FileType('some-type'), access: { restricted: true, canDownload: false } }) @@ -105,7 +106,7 @@ describe('FileThumbnail', () => { it('renders FileThumbnailIcon when thumbnail is not provided with unlock icon when restricted with access', () => { const file = FileMother.create({ - type: 'some-type', + type: new FileType('some-type'), access: { restricted: true, canDownload: true } })