cozy-ui 60.4.0 → 60.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +2 -1
- package/react/FilePicker/FilePickerBody.jsx +112 -0
- package/react/FilePicker/FilePickerBodyItem.jsx +129 -0
- package/react/FilePicker/FilePickerBodyItem.spec.jsx +131 -0
- package/react/FilePicker/FilePickerBreadcrumb.jsx +54 -0
- package/react/FilePicker/FilePickerFooter.jsx +40 -0
- package/react/FilePicker/FilePickerFooter.spec.jsx +41 -0
- package/react/FilePicker/FilePickerHeader.jsx +85 -0
- package/react/FilePicker/Readme.md +53 -0
- package/react/FilePicker/index.jsx +108 -0
- package/react/FilePicker/locales/en.json +8 -0
- package/react/FilePicker/locales/fr.json +8 -0
- package/react/FilePicker/queries.js +36 -0
- package/react/FilePicker/styles.styl +18 -0
- package/react/index.js +1 -0
- package/transpiled/react/FilePicker/FilePickerBody.js +89 -0
- package/transpiled/react/FilePicker/FilePickerBodyItem.js +111 -0
- package/transpiled/react/FilePicker/FilePickerBreadcrumb.js +52 -0
- package/transpiled/react/FilePicker/FilePickerFooter.js +53 -0
- package/transpiled/react/FilePicker/FilePickerHeader.js +78 -0
- package/transpiled/react/FilePicker/index.js +111 -0
- package/transpiled/react/FilePicker/queries.js +44 -0
- package/transpiled/react/index.js +2 -1
- package/transpiled/react/stylesheet.css +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# [60.5.0](https://github.com/cozy/cozy-ui/compare/v60.4.0...v60.5.0) (2022-01-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Add FilePicker component ([288d047](https://github.com/cozy/cozy-ui/commit/288d047))
|
|
7
|
+
* Add filesize package ([9286dcd](https://github.com/cozy/cozy-ui/commit/9286dcd))
|
|
8
|
+
|
|
1
9
|
# [60.4.0](https://github.com/cozy/cozy-ui/compare/v60.3.0...v60.4.0) (2022-01-20)
|
|
2
10
|
|
|
3
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-ui",
|
|
3
|
-
"version": "60.
|
|
3
|
+
"version": "60.5.0",
|
|
4
4
|
"description": "Cozy apps UI SDK",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -153,6 +153,7 @@
|
|
|
153
153
|
"classnames": "^2.2.5",
|
|
154
154
|
"cozy-interapp": "^0.5.4",
|
|
155
155
|
"date-fns": "^1.28.5",
|
|
156
|
+
"filesize": "8.0.7",
|
|
156
157
|
"hammerjs": "^2.0.8",
|
|
157
158
|
"intersection-observer": "0.11.0",
|
|
158
159
|
"mui-bottom-sheet": "https://github.com/cozy/mui-bottom-sheet.git#v1.0.6",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useCallback, memo } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import { models, useQuery } from 'cozy-client'
|
|
5
|
+
import List from '../MuiCozyTheme/List'
|
|
6
|
+
import LoadMore from '../LoadMore'
|
|
7
|
+
|
|
8
|
+
import { buildContentFolderQuery } from './queries'
|
|
9
|
+
import FilePickerBodyItem from './FilePickerBodyItem'
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
file: { isDirectory, isFile }
|
|
13
|
+
} = models
|
|
14
|
+
|
|
15
|
+
const FilePickerBody = ({
|
|
16
|
+
navigateTo,
|
|
17
|
+
folderId,
|
|
18
|
+
onSelectFileId,
|
|
19
|
+
filesIdsSelected,
|
|
20
|
+
fileTypesAccepted,
|
|
21
|
+
multiple
|
|
22
|
+
}) => {
|
|
23
|
+
const contentFolderQuery = buildContentFolderQuery(folderId)
|
|
24
|
+
const { data: contentFolder, hasMore, fetchMore } = useQuery(
|
|
25
|
+
contentFolderQuery.definition,
|
|
26
|
+
contentFolderQuery.options
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const onCheck = useCallback(
|
|
30
|
+
fileId => {
|
|
31
|
+
const isChecked = filesIdsSelected.some(
|
|
32
|
+
fileIdSelected => fileIdSelected === fileId
|
|
33
|
+
)
|
|
34
|
+
if (isChecked) {
|
|
35
|
+
onSelectFileId(
|
|
36
|
+
filesIdsSelected.filter(fileIdSelected => fileIdSelected !== fileId)
|
|
37
|
+
)
|
|
38
|
+
} else onSelectFileId(prev => [...prev, fileId])
|
|
39
|
+
},
|
|
40
|
+
[filesIdsSelected, onSelectFileId]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// When click on checkbox/radio area...
|
|
44
|
+
const handleChoiceClick = useCallback(
|
|
45
|
+
file => () => {
|
|
46
|
+
if (multiple) onCheck(file._id)
|
|
47
|
+
else onSelectFileId(file._id)
|
|
48
|
+
},
|
|
49
|
+
[multiple, onCheck, onSelectFileId]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// ...when click anywhere on the rest of the line
|
|
53
|
+
const handleListItemClick = useCallback(
|
|
54
|
+
file => () => {
|
|
55
|
+
if (isDirectory(file)) {
|
|
56
|
+
navigateTo(contentFolder.find(f => f._id === file._id))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isFile(file) && fileTypesAccepted.file) {
|
|
60
|
+
if (multiple) onCheck(file._id)
|
|
61
|
+
else onSelectFileId(file._id)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[
|
|
65
|
+
contentFolder,
|
|
66
|
+
fileTypesAccepted.file,
|
|
67
|
+
multiple,
|
|
68
|
+
navigateTo,
|
|
69
|
+
onCheck,
|
|
70
|
+
onSelectFileId
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<List>
|
|
76
|
+
{contentFolder &&
|
|
77
|
+
contentFolder.map((file, idx) => {
|
|
78
|
+
const hasDivider = contentFolder
|
|
79
|
+
? idx !== contentFolder.length - 1
|
|
80
|
+
: false
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<FilePickerBodyItem
|
|
84
|
+
key={file._id}
|
|
85
|
+
file={file}
|
|
86
|
+
fileTypesAccepted={fileTypesAccepted}
|
|
87
|
+
multiple={multiple}
|
|
88
|
+
handleChoiceClick={handleChoiceClick}
|
|
89
|
+
handleListItemClick={handleListItemClick}
|
|
90
|
+
onCheck={onCheck}
|
|
91
|
+
filesIdsSelected={filesIdsSelected}
|
|
92
|
+
hasDivider={hasDivider}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
})}
|
|
96
|
+
{hasMore && <LoadMore label={'loadMore'} fetchMore={fetchMore} />}
|
|
97
|
+
</List>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
FilePickerBody.propTypes = {
|
|
102
|
+
onSelectFileId: PropTypes.func.isRequired,
|
|
103
|
+
filesIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
104
|
+
folderId: PropTypes.string.isRequired,
|
|
105
|
+
navigateTo: PropTypes.func.isRequired,
|
|
106
|
+
fileTypesAccepted: PropTypes.exact({
|
|
107
|
+
file: PropTypes.bool,
|
|
108
|
+
folder: PropTypes.bool
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default memo(FilePickerBody)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { memo } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import cx from 'classnames'
|
|
4
|
+
import filesize from 'filesize'
|
|
5
|
+
import { makeStyles } from '@material-ui/core/styles'
|
|
6
|
+
|
|
7
|
+
import { models } from 'cozy-client'
|
|
8
|
+
|
|
9
|
+
import ListItem from '../MuiCozyTheme/ListItem'
|
|
10
|
+
import ListItemIcon from '../MuiCozyTheme/ListItemIcon'
|
|
11
|
+
import ListItemText from '../ListItemText'
|
|
12
|
+
import Divider from '../MuiCozyTheme/Divider'
|
|
13
|
+
import Icon from '../Icon'
|
|
14
|
+
import FileTypeText from '../Icons/FileTypeText'
|
|
15
|
+
import FileTypeFolder from '../Icons/FileTypeFolder'
|
|
16
|
+
import Checkbox from '../Checkbox'
|
|
17
|
+
import Radio from '../Radio'
|
|
18
|
+
import { useI18n } from '../I18n'
|
|
19
|
+
|
|
20
|
+
import styles from './styles.styl'
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
file: { isDirectory, isFile }
|
|
24
|
+
} = models
|
|
25
|
+
|
|
26
|
+
const useStyles = makeStyles(() => ({
|
|
27
|
+
verticalDivider: {
|
|
28
|
+
height: '2rem',
|
|
29
|
+
display: 'flex',
|
|
30
|
+
alignSelf: 'auto',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
marginLeft: '0.5rem'
|
|
33
|
+
},
|
|
34
|
+
listItemIcon: {
|
|
35
|
+
marginLeft: '1rem'
|
|
36
|
+
}
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
const FilePickerBodyItem = ({
|
|
40
|
+
file,
|
|
41
|
+
fileTypesAccepted,
|
|
42
|
+
multiple,
|
|
43
|
+
handleChoiceClick,
|
|
44
|
+
handleListItemClick,
|
|
45
|
+
filesIdsSelected,
|
|
46
|
+
hasDivider
|
|
47
|
+
}) => {
|
|
48
|
+
const classes = useStyles()
|
|
49
|
+
const { f } = useI18n()
|
|
50
|
+
const hasChoice =
|
|
51
|
+
(fileTypesAccepted.file && isFile(file)) ||
|
|
52
|
+
(fileTypesAccepted.folder && isDirectory(file))
|
|
53
|
+
|
|
54
|
+
const Input = multiple ? Checkbox : Radio
|
|
55
|
+
|
|
56
|
+
const listItemSecondaryContent = isFile(file)
|
|
57
|
+
? `${f(file.attributes.updated_at, 'DD MMM YYYY')} - ${filesize(
|
|
58
|
+
file.attributes.size,
|
|
59
|
+
{ base: 10 }
|
|
60
|
+
)}`
|
|
61
|
+
: null
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<ListItem button className="u-p-0">
|
|
66
|
+
<div
|
|
67
|
+
data-testid="listitem-onclick"
|
|
68
|
+
className={styles['filePickerBreadcrumb-wrapper']}
|
|
69
|
+
onClick={handleListItemClick(file)}
|
|
70
|
+
>
|
|
71
|
+
<ListItemIcon className={classes.listItemIcon}>
|
|
72
|
+
<Icon
|
|
73
|
+
icon={isDirectory(file) ? FileTypeFolder : FileTypeText}
|
|
74
|
+
width="32"
|
|
75
|
+
height="32"
|
|
76
|
+
/>
|
|
77
|
+
</ListItemIcon>
|
|
78
|
+
<ListItemText
|
|
79
|
+
primary={file.name}
|
|
80
|
+
secondary={listItemSecondaryContent}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
{isDirectory(file) && hasChoice && (
|
|
84
|
+
<Divider
|
|
85
|
+
orientation="vertical"
|
|
86
|
+
flexItem
|
|
87
|
+
className={classes.verticalDivider}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
<div
|
|
91
|
+
data-testid="choice-onclick"
|
|
92
|
+
className="u-ph-1 u-pv-half u-h-2 u-flex u-flex-items-center"
|
|
93
|
+
onClick={hasChoice ? handleChoiceClick(file) : undefined}
|
|
94
|
+
>
|
|
95
|
+
<Input
|
|
96
|
+
data-testid={multiple ? 'checkbox-btn' : 'radio-btn'}
|
|
97
|
+
gutter={false}
|
|
98
|
+
onChange={() => {
|
|
99
|
+
// handled by onClick on the container
|
|
100
|
+
}}
|
|
101
|
+
checked={filesIdsSelected.includes(file._id)}
|
|
102
|
+
value={file._id}
|
|
103
|
+
className={cx('u-p-0', {
|
|
104
|
+
'u-o-100': hasChoice,
|
|
105
|
+
'u-o-0': !hasChoice
|
|
106
|
+
})}
|
|
107
|
+
disabled={!hasChoice}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</ListItem>
|
|
111
|
+
{hasDivider && <Divider component="li" />}
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
FilePickerBodyItem.propTypes = {
|
|
117
|
+
file: PropTypes.object.isRequired,
|
|
118
|
+
fileTypesAccepted: PropTypes.exact({
|
|
119
|
+
file: PropTypes.bool,
|
|
120
|
+
folder: PropTypes.bool
|
|
121
|
+
}),
|
|
122
|
+
multiple: PropTypes.bool,
|
|
123
|
+
handleChoiceClick: PropTypes.func.isRequired,
|
|
124
|
+
handleListItemClick: PropTypes.func.isRequired,
|
|
125
|
+
filesIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
126
|
+
hasDivider: PropTypes.bool.isRequired
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default memo(FilePickerBodyItem)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react'
|
|
3
|
+
import filesize from 'filesize'
|
|
4
|
+
|
|
5
|
+
import DemoProvider from './docs/DemoProvider'
|
|
6
|
+
import FilePickerBodyItem from './FilePickerBodyItem'
|
|
7
|
+
|
|
8
|
+
const mockFile01 = {
|
|
9
|
+
_id: '001',
|
|
10
|
+
type: 'file',
|
|
11
|
+
name: 'Filename',
|
|
12
|
+
attributes: { updated_at: '2021-01-01T12:00:00.000000+01:00' }
|
|
13
|
+
}
|
|
14
|
+
const mockFolder01 = {
|
|
15
|
+
_id: '002',
|
|
16
|
+
type: 'directory',
|
|
17
|
+
name: 'Foldername',
|
|
18
|
+
attributes: { updated_at: '2021-01-01T12:00:00.000000+01:00' }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
jest.mock('filesize', () => jest.fn())
|
|
22
|
+
|
|
23
|
+
describe('FilePickerBodyItem components:', () => {
|
|
24
|
+
const mockHandleChoiceClick = jest.fn()
|
|
25
|
+
const mockHandleListItemClick = jest.fn()
|
|
26
|
+
filesize.mockReturnValue('111Ko')
|
|
27
|
+
|
|
28
|
+
const setup = ({
|
|
29
|
+
file = mockFile01,
|
|
30
|
+
multiple = false,
|
|
31
|
+
fileTypesAccepted = { file: true, folder: false }
|
|
32
|
+
}) => {
|
|
33
|
+
return render(
|
|
34
|
+
<DemoProvider>
|
|
35
|
+
<FilePickerBodyItem
|
|
36
|
+
file={file}
|
|
37
|
+
fileTypesAccepted={fileTypesAccepted}
|
|
38
|
+
multiple={multiple}
|
|
39
|
+
handleChoiceClick={mockHandleChoiceClick}
|
|
40
|
+
handleListItemClick={mockHandleListItemClick}
|
|
41
|
+
filesIdsSelected={[]}
|
|
42
|
+
hasDivider={false}
|
|
43
|
+
/>
|
|
44
|
+
</DemoProvider>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
jest.clearAllMocks()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should be rendered correctly', () => {
|
|
53
|
+
const { container } = setup({})
|
|
54
|
+
|
|
55
|
+
expect(container).toBeDefined()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should display filename', () => {
|
|
59
|
+
const { getByText } = setup({})
|
|
60
|
+
|
|
61
|
+
expect(getByText('Filename'))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should display foldername', () => {
|
|
65
|
+
const { getByText } = setup({ file: mockFolder01 })
|
|
66
|
+
|
|
67
|
+
expect(getByText('Foldername'))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('Functions called', () => {
|
|
71
|
+
it('should call "handleChoiceClick" function when click on checkbox/radio area', () => {
|
|
72
|
+
const { getByTestId } = setup({})
|
|
73
|
+
fireEvent.click(getByTestId('choice-onclick'))
|
|
74
|
+
|
|
75
|
+
expect(mockHandleChoiceClick).toHaveBeenCalled()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is Folder & not accepted', () => {
|
|
79
|
+
const { getByTestId } = setup({ file: mockFolder01 })
|
|
80
|
+
fireEvent.click(getByTestId('choice-onclick'))
|
|
81
|
+
|
|
82
|
+
expect(mockHandleChoiceClick).not.toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is File & not accepted', () => {
|
|
85
|
+
const { getByTestId } = setup({
|
|
86
|
+
fileTypesAccepted: { file: false, folder: true }
|
|
87
|
+
})
|
|
88
|
+
fireEvent.click(getByTestId('choice-onclick'))
|
|
89
|
+
|
|
90
|
+
expect(mockHandleChoiceClick).not.toHaveBeenCalled()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should call "handleListItemClick" function when click on ListItem node', () => {
|
|
94
|
+
const { getByTestId } = setup({})
|
|
95
|
+
fireEvent.click(getByTestId('listitem-onclick'))
|
|
96
|
+
|
|
97
|
+
expect(mockHandleListItemClick).toHaveBeenCalled()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('Attribute "multiple"', () => {
|
|
102
|
+
it('should radio button exists if "multiple" atribute is False', () => {
|
|
103
|
+
const { getByTestId } = setup({})
|
|
104
|
+
const radioBtn = getByTestId('radio-btn')
|
|
105
|
+
expect(radioBtn).not.toBeNull()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should checkbox button exists if "multiple" atribute is True', () => {
|
|
109
|
+
const { getByTestId } = setup({ multiple: true })
|
|
110
|
+
const checkboxBtn = getByTestId('checkbox-btn')
|
|
111
|
+
expect(checkboxBtn).not.toBeNull()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('Radio/Checkbox button', () => {
|
|
116
|
+
it('should disable and not display the Radio button if it is a File and is not accepted', () => {
|
|
117
|
+
const { getByTestId } = setup({
|
|
118
|
+
fileTypesAccepted: { file: false }
|
|
119
|
+
})
|
|
120
|
+
const radioBtn = getByTestId('radio-btn')
|
|
121
|
+
|
|
122
|
+
expect(radioBtn.getAttribute('disabled')).toBe('')
|
|
123
|
+
})
|
|
124
|
+
it('should disable and not display the Radio button if it is a Folder and is not accepted', () => {
|
|
125
|
+
const { getByTestId } = setup({ file: mockFolder01 })
|
|
126
|
+
const radioBtn = getByTestId('radio-btn')
|
|
127
|
+
|
|
128
|
+
expect(radioBtn.getAttribute('disabled')).toBe('')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { Fragment, useCallback, memo } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import Typography from '../Typography'
|
|
5
|
+
import Icon from '../Icon'
|
|
6
|
+
import RightIcon from '../Icons/Right'
|
|
7
|
+
import useBreakpoints from '../hooks/useBreakpoints'
|
|
8
|
+
|
|
9
|
+
import styles from './styles.styl'
|
|
10
|
+
|
|
11
|
+
const FilePickerBreadcrumb = ({ path, onBreadcrumbClick }) => {
|
|
12
|
+
const { isMobile } = useBreakpoints()
|
|
13
|
+
const hasPath = path && path.length > 0
|
|
14
|
+
|
|
15
|
+
const navigateTo = useCallback(folder => () => onBreadcrumbClick(folder), [
|
|
16
|
+
onBreadcrumbClick
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Typography variant="h4" className="u-flex u-flex-items-center">
|
|
21
|
+
{hasPath
|
|
22
|
+
? isMobile
|
|
23
|
+
? path[path.length - 1].name
|
|
24
|
+
: path.map((folder, idx) => {
|
|
25
|
+
if (idx < path.length - 1) {
|
|
26
|
+
return (
|
|
27
|
+
<Fragment key={idx}>
|
|
28
|
+
<span
|
|
29
|
+
className={styles['filePickerBreadcrumb-previousPath']}
|
|
30
|
+
onClick={navigateTo(folder)}
|
|
31
|
+
>
|
|
32
|
+
{folder.name}
|
|
33
|
+
</span>
|
|
34
|
+
<Icon
|
|
35
|
+
icon={RightIcon}
|
|
36
|
+
className={styles['filePickerBreadcrumb-icon']}
|
|
37
|
+
/>
|
|
38
|
+
</Fragment>
|
|
39
|
+
)
|
|
40
|
+
} else {
|
|
41
|
+
return <span key={idx}>{folder.name}</span>
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
: null}
|
|
45
|
+
</Typography>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
FilePickerBreadcrumb.propTypes = {
|
|
50
|
+
path: PropTypes.array,
|
|
51
|
+
onBreadcrumbClick: PropTypes.func
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default memo(FilePickerBreadcrumb)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { memo } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import Button from '../Button'
|
|
5
|
+
import { createUseI18n } from '../I18n'
|
|
6
|
+
|
|
7
|
+
import en from './locales/en.json'
|
|
8
|
+
import fr from './locales/fr.json'
|
|
9
|
+
|
|
10
|
+
const locales = { en, fr }
|
|
11
|
+
const useI18n = createUseI18n(locales)
|
|
12
|
+
|
|
13
|
+
const FilePickerFooter = ({ onConfirm, onClose, disabledConfirm }) => {
|
|
14
|
+
const { t } = useI18n()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<Button
|
|
19
|
+
data-testid="close-btn"
|
|
20
|
+
label={t('footer.buttons.cancel')}
|
|
21
|
+
theme="secondary"
|
|
22
|
+
onClick={onClose}
|
|
23
|
+
/>
|
|
24
|
+
<Button
|
|
25
|
+
data-testid="confirm-btn"
|
|
26
|
+
label={t('footer.buttons.confirm')}
|
|
27
|
+
onClick={onConfirm}
|
|
28
|
+
disabled={disabledConfirm}
|
|
29
|
+
/>
|
|
30
|
+
</>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
FilePickerFooter.propTypes = {
|
|
35
|
+
onConfirm: PropTypes.func.isRequired,
|
|
36
|
+
onClose: PropTypes.func.isRequired,
|
|
37
|
+
disabledConfirm: PropTypes.bool.isRequired
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default memo(FilePickerFooter)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react'
|
|
3
|
+
|
|
4
|
+
import DemoProvider from './docs/DemoProvider'
|
|
5
|
+
import FilePickerFooter from './FilePickerFooter'
|
|
6
|
+
|
|
7
|
+
describe('FilePickerFooter components:', () => {
|
|
8
|
+
const mockOnConfirm = jest.fn()
|
|
9
|
+
const mockOnClose = jest.fn()
|
|
10
|
+
|
|
11
|
+
const setup = (disabledConfirm = false) => {
|
|
12
|
+
return render(
|
|
13
|
+
<DemoProvider>
|
|
14
|
+
<FilePickerFooter
|
|
15
|
+
onConfirm={mockOnConfirm}
|
|
16
|
+
onClose={mockOnClose}
|
|
17
|
+
disabledConfirm={disabledConfirm}
|
|
18
|
+
/>
|
|
19
|
+
</DemoProvider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
it('should be rendered correctly', () => {
|
|
23
|
+
const { container } = setup()
|
|
24
|
+
|
|
25
|
+
expect(container).toBeDefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should confirm button have been called', () => {
|
|
29
|
+
const { getByTestId } = setup()
|
|
30
|
+
fireEvent.click(getByTestId('confirm-btn'))
|
|
31
|
+
|
|
32
|
+
expect(mockOnConfirm).toHaveBeenCalled()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should close button have been called', () => {
|
|
36
|
+
const { getByTestId } = setup()
|
|
37
|
+
fireEvent.click(getByTestId('close-btn'))
|
|
38
|
+
|
|
39
|
+
expect(mockOnClose).toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useCallback, memo } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import uniqBy from 'lodash/uniqBy'
|
|
4
|
+
import get from 'lodash/get'
|
|
5
|
+
|
|
6
|
+
import { useQuery, hasQueryBeenLoaded } from 'cozy-client'
|
|
7
|
+
|
|
8
|
+
import useBreakpoints from '../hooks/useBreakpoints'
|
|
9
|
+
import IconButton from '../IconButton'
|
|
10
|
+
import Icon from '../Icon'
|
|
11
|
+
import Previous from '../Icons/Previous'
|
|
12
|
+
|
|
13
|
+
import FilePickerBreadcrumb from './FilePickerBreadcrumb'
|
|
14
|
+
import { buildCurrentFolderQuery } from './queries'
|
|
15
|
+
import { ROOT_DIR_ID } from './index'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {IOCozyFolder} displayedFolder - An io.cozy.files folder
|
|
19
|
+
* @returns {{id: string, name: string}[]}
|
|
20
|
+
*/
|
|
21
|
+
const getBreadcrumbPath = displayedFolder => {
|
|
22
|
+
return uniqBy(
|
|
23
|
+
[
|
|
24
|
+
{
|
|
25
|
+
id: ROOT_DIR_ID
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: get(displayedFolder, 'dir_id')
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: displayedFolder.id,
|
|
32
|
+
name: displayedFolder.name
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
'id'
|
|
36
|
+
)
|
|
37
|
+
.filter(({ id }) => Boolean(id))
|
|
38
|
+
.map(breadcrumb => ({
|
|
39
|
+
id: breadcrumb.id,
|
|
40
|
+
name: breadcrumb.name || (breadcrumb.id === ROOT_DIR_ID ? 'Drive' : '…')
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FilePickerHeader = ({ navigateTo, folderId, onClose }) => {
|
|
45
|
+
const { isMobile } = useBreakpoints()
|
|
46
|
+
|
|
47
|
+
const currentFolderQuery = buildCurrentFolderQuery(folderId)
|
|
48
|
+
const { data: currentFolder, ...restCurrentFolder } = useQuery(
|
|
49
|
+
currentFolderQuery.definition,
|
|
50
|
+
currentFolderQuery.options
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const path = hasQueryBeenLoaded(restCurrentFolder)
|
|
54
|
+
? getBreadcrumbPath(currentFolder[0])
|
|
55
|
+
: []
|
|
56
|
+
|
|
57
|
+
const onBack = useCallback(path => navigateTo(path), [navigateTo])
|
|
58
|
+
|
|
59
|
+
const handleClick = useCallback(() => {
|
|
60
|
+
path.length > 1 && isMobile ? onBack(path[path.length - 2]) : onClose()
|
|
61
|
+
}, [isMobile, path, onBack, onClose])
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="u-flex u-flex-items-center">
|
|
65
|
+
{isMobile && (
|
|
66
|
+
<IconButton onClick={handleClick} className="u-p-0 u-pr-1">
|
|
67
|
+
<Icon icon={Previous} />
|
|
68
|
+
</IconButton>
|
|
69
|
+
)}
|
|
70
|
+
<FilePickerBreadcrumb
|
|
71
|
+
path={path}
|
|
72
|
+
onBreadcrumbClick={navigateTo}
|
|
73
|
+
opening={false}
|
|
74
|
+
inlined
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
FilePickerHeader.propTypes = {
|
|
81
|
+
folderId: PropTypes.string.isRequired,
|
|
82
|
+
navigateTo: PropTypes.func.isRequired
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default memo(FilePickerHeader)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
FilePicker allows you to select a file or folder in Drive
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
import CozyIcon from 'cozy-ui/transpiled/react/Icons/Cozy'
|
|
5
|
+
import Button from 'cozy-ui/transpiled/react/Button'
|
|
6
|
+
import FilePicker from 'cozy-ui/transpiled/react/FilePicker'
|
|
7
|
+
import Variants from 'cozy-ui/docs/components/Variants'
|
|
8
|
+
import DemoProvider from './docs/DemoProvider';
|
|
9
|
+
|
|
10
|
+
initialState = {
|
|
11
|
+
filePickerOpened: isTesting()
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const initialVariants = [
|
|
15
|
+
{ acceptFile: true, acceptFolder: false, multiple: false }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const toggleFilePicker = () => setState({ filePickerOpened: !state.filePickerOpened });
|
|
19
|
+
const onChange = (fileId) => alert(`ID of file selected : [${fileId}]`);
|
|
20
|
+
<DemoProvider>
|
|
21
|
+
<Variants initialVariants={initialVariants} screenshotAllVariants>
|
|
22
|
+
{variant => {
|
|
23
|
+
let acceptRule = ''
|
|
24
|
+
if (variant.acceptFile) {
|
|
25
|
+
acceptRule = 'file'
|
|
26
|
+
if (variant.acceptFolder) {
|
|
27
|
+
acceptRule = 'file,folder'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (variant.acceptFolder) {
|
|
31
|
+
acceptRule = 'folder'
|
|
32
|
+
if (variant.acceptFile) {
|
|
33
|
+
acceptRule = 'folder,file'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<button onClick={toggleFilePicker}>Open FilePicker</button>
|
|
40
|
+
{state.filePickerOpened && (
|
|
41
|
+
<FilePicker
|
|
42
|
+
onClose={toggleFilePicker}
|
|
43
|
+
onChange={onChange}
|
|
44
|
+
accept={acceptRule}
|
|
45
|
+
multiple={variant.multiple}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
</>
|
|
49
|
+
)
|
|
50
|
+
}}
|
|
51
|
+
</Variants>
|
|
52
|
+
</DemoProvider>
|
|
53
|
+
```
|