Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add FilePicker component #2000

Merged
merged 2 commits into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ module.exports = {
'../react/AppSections/index.jsx',
'../react/CipherIcon/index.jsx',
'../react/CozyTheme/index.jsx',
'../react/FilePicker/index.jsx',
'../react/Overlay/index.jsx',
'../react/PasswordExample/index.jsx',
'../react/Popup/index.jsx',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"classnames": "^2.2.5",
"cozy-interapp": "^0.5.4",
"date-fns": "^1.28.5",
"filesize": "8.0.7",
"hammerjs": "^2.0.8",
"intersection-observer": "0.11.0",
"mui-bottom-sheet": "https://github.com/cozy/mui-bottom-sheet.git#v1.0.6",
Expand Down
112 changes: 112 additions & 0 deletions react/FilePicker/FilePickerBody.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useCallback, memo } from 'react'
import PropTypes from 'prop-types'

import { models, useQuery } from 'cozy-client'
import List from '../MuiCozyTheme/List'
import LoadMore from '../LoadMore'

import { buildContentFolderQuery } from './queries'
import FilePickerBodyItem from './FilePickerBodyItem'

const {
file: { isDirectory, isFile }
} = models

const FilePickerBody = ({
navigateTo,
folderId,
onSelectFileId,
filesIdsSelected,
fileTypesAccepted,
multiple
}) => {
const contentFolderQuery = buildContentFolderQuery(folderId)
const { data: contentFolder, hasMore, fetchMore } = useQuery(
contentFolderQuery.definition,
contentFolderQuery.options
)

const onCheck = useCallback(
fileId => {
const isChecked = filesIdsSelected.some(
fileIdSelected => fileIdSelected === fileId
)
if (isChecked) {
onSelectFileId(
filesIdsSelected.filter(fileIdSelected => fileIdSelected !== fileId)
)
} else onSelectFileId(prev => [...prev, fileId])
},
[filesIdsSelected, onSelectFileId]
)

// When click on checkbox/radio area...
const handleChoiceClick = useCallback(
file => () => {
if (multiple) onCheck(file._id)
else onSelectFileId(file._id)
},
[multiple, onCheck, onSelectFileId]
)

// ...when click anywhere on the rest of the line
const handleListItemClick = useCallback(
file => () => {
if (isDirectory(file)) {
navigateTo(contentFolder.find(f => f._id === file._id))
}

if (isFile(file) && fileTypesAccepted.file) {
if (multiple) onCheck(file._id)
else onSelectFileId(file._id)
}
},
[
contentFolder,
fileTypesAccepted.file,
multiple,
navigateTo,
onCheck,
onSelectFileId
]
)

return (
<List>
{contentFolder &&
contentFolder.map((file, idx) => {
const hasDivider = contentFolder
? idx !== contentFolder.length - 1
: false

return (
<FilePickerBodyItem
key={file._id}
file={file}
fileTypesAccepted={fileTypesAccepted}
multiple={multiple}
handleChoiceClick={handleChoiceClick}
handleListItemClick={handleListItemClick}
onCheck={onCheck}
filesIdsSelected={filesIdsSelected}
hasDivider={hasDivider}
/>
)
})}
{hasMore && <LoadMore label={'loadMore'} fetchMore={fetchMore} />}
</List>
)
}

FilePickerBody.propTypes = {
onSelectFileId: PropTypes.func.isRequired,
filesIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
folderId: PropTypes.string.isRequired,
navigateTo: PropTypes.func.isRequired,
fileTypesAccepted: PropTypes.exact({
file: PropTypes.bool,
folder: PropTypes.bool
})
}

export default memo(FilePickerBody)
129 changes: 129 additions & 0 deletions react/FilePicker/FilePickerBodyItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { memo } from 'react'
import PropTypes from 'prop-types'
import cx from 'classnames'
import filesize from 'filesize'
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
import { makeStyles } from '@material-ui/core/styles'

import { models } from 'cozy-client'

import ListItem from '../MuiCozyTheme/ListItem'
import ListItemIcon from '../MuiCozyTheme/ListItemIcon'
import ListItemText from '../ListItemText'
import Divider from '../MuiCozyTheme/Divider'
import Icon from '../Icon'
import FileTypeText from '../Icons/FileTypeText'
import FileTypeFolder from '../Icons/FileTypeFolder'
import Checkbox from '../Checkbox'
import Radio from '../Radio'
import { useI18n } from '../I18n'

import styles from './styles.styl'

const {
file: { isDirectory, isFile }
} = models

const useStyles = makeStyles(() => ({
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
verticalDivider: {
height: '2rem',
display: 'flex',
alignSelf: 'auto',
alignItems: 'center',
marginLeft: '0.5rem'
},
listItemIcon: {
marginLeft: '1rem'
}
}))

const FilePickerBodyItem = ({
file,
fileTypesAccepted,
multiple,
handleChoiceClick,
handleListItemClick,
filesIdsSelected,
hasDivider
}) => {
const classes = useStyles()
const { f } = useI18n()
const hasChoice =
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
(fileTypesAccepted.file && isFile(file)) ||
(fileTypesAccepted.folder && isDirectory(file))

const Input = multiple ? Checkbox : Radio

const listItemSecondaryContent = isFile(file)
? `${f(file.attributes.updated_at, 'DD MMM YYYY')} - ${filesize(
file.attributes.size,
{ base: 10 }
)}`
: null

return (
<>
<ListItem button className="u-p-0">
<div
data-testid="listitem-onclick"
className={styles['filePickerBreadcrumb-wrapper']}
onClick={handleListItemClick(file)}
>
<ListItemIcon className={classes.listItemIcon}>
<Icon
icon={isDirectory(file) ? FileTypeFolder : FileTypeText}
width="32"
height="32"
/>
</ListItemIcon>
<ListItemText
primary={file.name}
secondary={listItemSecondaryContent}
/>
</div>
{isDirectory(file) && hasChoice && (
<Divider
orientation="vertical"
flexItem
className={classes.verticalDivider}
/>
)}
<div
data-testid="choice-onclick"
className="u-ph-1 u-pv-half u-h-2 u-flex u-flex-items-center"
onClick={hasChoice ? handleChoiceClick(file) : undefined}
>
<Input
data-testid={multiple ? 'checkbox-btn' : 'radio-btn'}
gutter={false}
onChange={() => {
// handled by onClick on the container
}}
Merkur39 marked this conversation as resolved.
Show resolved Hide resolved
checked={filesIdsSelected.includes(file._id)}
value={file._id}
className={cx('u-p-0', {
'u-o-100': hasChoice,
'u-o-0': !hasChoice
})}
disabled={!hasChoice}
/>
</div>
</ListItem>
{hasDivider && <Divider component="li" />}
</>
)
}

FilePickerBodyItem.propTypes = {
file: PropTypes.object.isRequired,
fileTypesAccepted: PropTypes.exact({
file: PropTypes.bool,
folder: PropTypes.bool
}),
multiple: PropTypes.bool,
handleChoiceClick: PropTypes.func.isRequired,
handleListItemClick: PropTypes.func.isRequired,
filesIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
hasDivider: PropTypes.bool.isRequired
}

export default memo(FilePickerBodyItem)
131 changes: 131 additions & 0 deletions react/FilePicker/FilePickerBodyItem.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import filesize from 'filesize'

import DemoProvider from './docs/DemoProvider'
import FilePickerBodyItem from './FilePickerBodyItem'

const mockFile01 = {
_id: '001',
type: 'file',
name: 'Filename',
attributes: { updated_at: '2021-01-01T12:00:00.000000+01:00' }
}
const mockFolder01 = {
_id: '002',
type: 'directory',
name: 'Foldername',
attributes: { updated_at: '2021-01-01T12:00:00.000000+01:00' }
}

jest.mock('filesize', () => jest.fn())

describe('FilePickerBodyItem components:', () => {
const mockHandleChoiceClick = jest.fn()
const mockHandleListItemClick = jest.fn()
filesize.mockReturnValue('111Ko')

const setup = ({
file = mockFile01,
multiple = false,
fileTypesAccepted = { file: true, folder: false }
}) => {
return render(
<DemoProvider>
<FilePickerBodyItem
file={file}
fileTypesAccepted={fileTypesAccepted}
multiple={multiple}
handleChoiceClick={mockHandleChoiceClick}
handleListItemClick={mockHandleListItemClick}
filesIdsSelected={[]}
hasDivider={false}
/>
</DemoProvider>
)
}

afterEach(() => {
jest.clearAllMocks()
})

it('should be rendered correctly', () => {
const { container } = setup({})

expect(container).toBeDefined()
})

it('should display filename', () => {
const { getByText } = setup({})

expect(getByText('Filename'))
})

it('should display foldername', () => {
const { getByText } = setup({ file: mockFolder01 })

expect(getByText('Foldername'))
})

describe('Functions called', () => {
it('should call "handleChoiceClick" function when click on checkbox/radio area', () => {
const { getByTestId } = setup({})
fireEvent.click(getByTestId('choice-onclick'))

expect(mockHandleChoiceClick).toHaveBeenCalled()
})

it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is Folder & not accepted', () => {
const { getByTestId } = setup({ file: mockFolder01 })
fireEvent.click(getByTestId('choice-onclick'))

expect(mockHandleChoiceClick).not.toHaveBeenCalled()
})
it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is File & not accepted', () => {
const { getByTestId } = setup({
fileTypesAccepted: { file: false, folder: true }
})
fireEvent.click(getByTestId('choice-onclick'))

expect(mockHandleChoiceClick).not.toHaveBeenCalled()
})

it('should call "handleListItemClick" function when click on ListItem node', () => {
const { getByTestId } = setup({})
fireEvent.click(getByTestId('listitem-onclick'))

expect(mockHandleListItemClick).toHaveBeenCalled()
})
})

describe('Attribute "multiple"', () => {
it('should radio button exists if "multiple" atribute is False', () => {
const { getByTestId } = setup({})
const radioBtn = getByTestId('radio-btn')
expect(radioBtn).not.toBeNull()
})

it('should checkbox button exists if "multiple" atribute is True', () => {
const { getByTestId } = setup({ multiple: true })
const checkboxBtn = getByTestId('checkbox-btn')
expect(checkboxBtn).not.toBeNull()
})
})

describe('Radio/Checkbox button', () => {
it('should disable and not display the Radio button if it is a File and is not accepted', () => {
const { getByTestId } = setup({
fileTypesAccepted: { file: false }
})
const radioBtn = getByTestId('radio-btn')

expect(radioBtn.getAttribute('disabled')).toBe('')
})
it('should disable and not display the Radio button if it is a Folder and is not accepted', () => {
const { getByTestId } = setup({ file: mockFolder01 })
const radioBtn = getByTestId('radio-btn')

expect(radioBtn.getAttribute('disabled')).toBe('')
})
})
})
Loading