cozy-ui 60.4.0 → 60.7.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/assets/icons/ui/folder-moveto.svg +1 -0
  3. package/package.json +5 -4
  4. package/react/AppLinker/Readme.md +35 -1
  5. package/react/AppLinker/__snapshots__/index.deprecated.spec.jsx.snap +24 -0
  6. package/react/AppLinker/index.deprecated.spec.jsx +157 -0
  7. package/react/AppLinker/index.jsx +41 -10
  8. package/react/AppLinker/index.spec.jsx +2 -6
  9. package/react/FilePicker/FilePickerBody.jsx +112 -0
  10. package/react/FilePicker/FilePickerBodyItem.jsx +129 -0
  11. package/react/FilePicker/FilePickerBodyItem.spec.jsx +131 -0
  12. package/react/FilePicker/FilePickerBreadcrumb.jsx +54 -0
  13. package/react/FilePicker/FilePickerFooter.jsx +40 -0
  14. package/react/FilePicker/FilePickerFooter.spec.jsx +41 -0
  15. package/react/FilePicker/FilePickerHeader.jsx +85 -0
  16. package/react/FilePicker/Readme.md +53 -0
  17. package/react/FilePicker/index.jsx +108 -0
  18. package/react/FilePicker/locales/en.json +8 -0
  19. package/react/FilePicker/locales/fr.json +8 -0
  20. package/react/FilePicker/queries.js +36 -0
  21. package/react/FilePicker/styles.styl +18 -0
  22. package/react/Icon/Readme.md +3 -1
  23. package/react/Icon/icons-sprite.js +1 -1
  24. package/react/Icons/FolderMoveto.jsx +15 -0
  25. package/react/__snapshots__/examples.spec.jsx.snap +128 -112
  26. package/react/index.js +1 -0
  27. package/transpiled/react/AppLinker/index.js +40 -13
  28. package/transpiled/react/FilePicker/FilePickerBody.js +89 -0
  29. package/transpiled/react/FilePicker/FilePickerBodyItem.js +111 -0
  30. package/transpiled/react/FilePicker/FilePickerBreadcrumb.js +52 -0
  31. package/transpiled/react/FilePicker/FilePickerFooter.js +53 -0
  32. package/transpiled/react/FilePicker/FilePickerHeader.js +78 -0
  33. package/transpiled/react/FilePicker/index.js +111 -0
  34. package/transpiled/react/FilePicker/queries.js +44 -0
  35. package/transpiled/react/Icon/icons-sprite.js +1 -1
  36. package/transpiled/react/Icons/FolderMoveto.js +14 -0
  37. package/transpiled/react/index.js +2 -1
  38. package/transpiled/react/stylesheet.css +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,40 @@
1
+ # [60.7.0](https://github.com/cozy/cozy-ui/compare/v60.6.1...v60.7.0) (2022-01-25)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add FolderMoveto icon ([7d68e72](https://github.com/cozy/cozy-ui/commit/7d68e72))
7
+
8
+ ## [60.6.1](https://github.com/cozy/cozy-ui/compare/v60.6.0...v60.6.1) (2022-01-24)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Bump cozy-device-helper ([04463f7](https://github.com/cozy/cozy-ui/commit/04463f7))
14
+ * Show breadcrumb on FilePicker documentation ([8f92f09](https://github.com/cozy/cozy-ui/commit/8f92f09))
15
+
16
+ # [60.6.0](https://github.com/cozy/cozy-ui/compare/v60.5.0...v60.6.0) (2022-01-20)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * Remove `isRequired` on AppLinker's `href` prop ([5c8b799](https://github.com/cozy/cozy-ui/commit/5c8b799))
22
+
23
+
24
+ ### Features
25
+
26
+ * Open apps using their native mobile version when available ([d4bd421](https://github.com/cozy/cozy-ui/commit/d4bd421))
27
+ * Replace `slug` prop by `app.slug` in `AppLinker` ([fc9cd87](https://github.com/cozy/cozy-ui/commit/fc9cd87))
28
+ * Upgrade cozy-intent to 1.3.0 ([e65af63](https://github.com/cozy/cozy-ui/commit/e65af63))
29
+
30
+ # [60.5.0](https://github.com/cozy/cozy-ui/compare/v60.4.0...v60.5.0) (2022-01-20)
31
+
32
+
33
+ ### Features
34
+
35
+ * Add FilePicker component ([288d047](https://github.com/cozy/cozy-ui/commit/288d047))
36
+ * Add filesize package ([9286dcd](https://github.com/cozy/cozy-ui/commit/9286dcd))
37
+
1
38
  # [60.4.0](https://github.com/cozy/cozy-ui/compare/v60.3.0...v60.4.0) (2022-01-20)
2
39
 
3
40
 
@@ -0,0 +1 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="M5.509 1c.271 0 .648.157.84.348l1.303 1.304c.192.192.579.348.848.348h6.503c.55 0 .997.456.997 1.002v8.99A2.003 2.003 0 0 1 14.006 15H1.994A2 2 0 0 1 0 12.994V2.007C0 1.45.451 1 .99 1h4.52ZM11.5 6a1 1 0 0 0-1.497 1.32l.083.094.5.586H6a1 1 0 0 0-.117 1.993L6 10h4.586l-.5.586a1 1 0 0 0 1.32 1.497L11.5 12l2.207-2.293a1 1 0 0 0 .083-1.32l-.083-.094L11.5 6Z"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui",
3
- "version": "60.4.0",
3
+ "version": "60.7.0",
4
4
  "description": "Cozy apps UI SDK",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -92,7 +92,7 @@
92
92
  "cozy-device-helper": "^1.16.0",
93
93
  "cozy-doctypes": "^1.69.0",
94
94
  "cozy-harvest-lib": "^6.7.3",
95
- "cozy-intent": "^1.1.2",
95
+ "cozy-intent": "^1.3.0",
96
96
  "cozy-sharing": "^3.10.0",
97
97
  "css-loader": "0.28.11",
98
98
  "cssnano": "4.1.11",
@@ -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",
@@ -168,10 +169,10 @@
168
169
  "peerDependencies": {
169
170
  "@material-ui/core": "4",
170
171
  "cozy-client": ">=27.5.1",
171
- "cozy-device-helper": "^1.10.0",
172
+ "cozy-device-helper": "^1.16.0",
172
173
  "cozy-doctypes": "^1.69.0",
173
174
  "cozy-harvest-lib": "^6.7.3",
174
- "cozy-intent": ">=1.1.2",
175
+ "cozy-intent": ">=1.3.0",
175
176
  "cozy-sharing": "^3.10.0",
176
177
  "piwik-react-router": "0.12.1",
177
178
  "puppeteer": "^1.20.0",
@@ -5,6 +5,12 @@ If the app is known to Cozy (for example Drive or Banks), and
5
5
  the user has installed it on its device, the native app will
6
6
  be opened.
7
7
 
8
+ The app's manifest can be set in the `app` prop. Then, in a
9
+ ReactNative environment, the AppLinker will be able to send
10
+ `openApp` message to the native environment with this `app`
11
+ data. Ideally the `mobile` member should be set with all data
12
+ needed to open the native app ([more info](https://github.com/cozy/cozy-stack/blob/master/docs/apps.md#mobile))
13
+
8
14
  Handles several cases:
9
15
 
10
16
  * On mobile app and other mobile app available
@@ -16,7 +22,35 @@ anchor.
16
22
 
17
23
  ```jsx
18
24
  import AppLinker from 'cozy-ui/transpiled/react/AppLinker';
19
- <AppLinker slug='banks' href='http://dalailama-banks.mycozy.cloud'>{
25
+
26
+ const app = {
27
+ slug: 'banks'
28
+ };
29
+
30
+ <AppLinker app={app} href='http://dalailama-banks.mycozy.cloud'>{
31
+ ({ onClick, href, name }) => (
32
+ <a href={href} onClick={onClick}>
33
+ Open { name }
34
+ </a>
35
+ )
36
+ }</AppLinker>
37
+ ```
38
+
39
+ ### Exemple with mobile data
40
+
41
+ ```jsx
42
+ import AppLinker from 'cozy-ui/transpiled/react/AppLinker';
43
+
44
+ const app = {
45
+ slug: 'passwords',
46
+ mobile: {
47
+ schema: 'cozypass://',
48
+ id_playstore: 'io.cozy.pass',
49
+ id_appstore: 'cozy-pass/id1502262449'
50
+ }
51
+ };
52
+
53
+ <AppLinker app={app} href='http://dalailama-passwords.mycozy.cloud'>{
20
54
  ({ onClick, href, name }) => (
21
55
  <a href={href} onClick={onClick}>
22
56
  Open { name }
@@ -0,0 +1,24 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`app icon should not crash if no href 1`] = `
4
+ <div>
5
+ <a
6
+ onClick={[Function]}
7
+ >
8
+ Open
9
+ Cozy Drive
10
+ </a>
11
+ </div>
12
+ `;
13
+
14
+ exports[`app icon should render correctly 1`] = `
15
+ <div>
16
+ <a
17
+ href="https://fake.link"
18
+ onClick={null}
19
+ >
20
+ Open
21
+ Cozy Drive
22
+ </a>
23
+ </div>
24
+ `;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Those tests are here to test previous AppLinker implementation base on
3
+ * `slug` prop.
4
+ * Now that AppLinkers implements `app` prop and uses `app.slug`, the old
5
+ * `slug` prop is deprecated
6
+ * Those tests should be kept until `slug` prop is completely removed
7
+ */
8
+
9
+ import React from 'react'
10
+ import { shallow } from 'enzyme'
11
+ import {
12
+ isMobileApp,
13
+ isMobile,
14
+ openDeeplinkOrRedirect,
15
+ startApp,
16
+ isAndroid
17
+ } from 'cozy-device-helper'
18
+
19
+ import AppLinker from './index'
20
+ import { generateUniversalLink } from './native'
21
+ jest.useFakeTimers()
22
+
23
+ const tMock = x => x
24
+
25
+ class DeprecatedAppItem extends React.Component {
26
+ render() {
27
+ const { app, onAppSwitch } = this.props
28
+ return (
29
+ <AppLinker
30
+ onAppSwitch={onAppSwitch}
31
+ slug={app.slug}
32
+ href={'https://fake.link'}
33
+ >
34
+ {({ onClick, href, name }) => (
35
+ <div>
36
+ <a href={href} onClick={onClick}>
37
+ Open {name}
38
+ </a>
39
+ </div>
40
+ )}
41
+ </AppLinker>
42
+ )
43
+ }
44
+ }
45
+ jest.mock('./native', () => ({
46
+ ...jest.requireActual('./native'),
47
+ generateUniversalLink: jest.fn()
48
+ }))
49
+
50
+ jest.mock('cozy-device-helper', () => ({
51
+ ...jest.requireActual('cozy-device-helper'),
52
+ isMobileApp: jest.fn(),
53
+ isMobile: jest.fn(),
54
+ openDeeplinkOrRedirect: jest.fn(),
55
+ startApp: jest.fn().mockResolvedValue(),
56
+ isAndroid: jest.fn()
57
+ }))
58
+
59
+ const app = {
60
+ slug: 'drive',
61
+ name: 'Drive'
62
+ }
63
+
64
+ describe('app icon', () => {
65
+ let spyConsoleError, spyConsoleWarn, openNativeFromNativeSpy, appSwitchMock
66
+
67
+ beforeEach(() => {
68
+ isMobileApp.mockReturnValue(false)
69
+ spyConsoleError = jest.spyOn(console, 'error')
70
+ spyConsoleError.mockImplementation(() => {})
71
+ spyConsoleWarn = jest.spyOn(console, 'warn')
72
+ spyConsoleWarn.mockImplementation(() => {})
73
+ openNativeFromNativeSpy = jest.spyOn(AppLinker, 'openNativeFromNative')
74
+ isMobileApp.mockReturnValue(false)
75
+ isMobile.mockReturnValue(false)
76
+ isAndroid.mockReturnValue(false)
77
+ appSwitchMock = jest.fn()
78
+ })
79
+
80
+ afterEach(() => {
81
+ spyConsoleError.mockRestore()
82
+ spyConsoleWarn.mockRestore()
83
+ jest.restoreAllMocks()
84
+ })
85
+
86
+ it('should render correctly', () => {
87
+ const root = shallow(<DeprecatedAppItem t={tMock} app={app} />).dive()
88
+ expect(root.getElement()).toMatchSnapshot()
89
+ })
90
+
91
+ it('should work for native -> native', () => {
92
+ const root = shallow(
93
+ <DeprecatedAppItem t={tMock} app={app} onAppSwitch={appSwitchMock} />
94
+ ).dive()
95
+ root.find('a').simulate('click')
96
+ expect(appSwitchMock).not.toHaveBeenCalled()
97
+ isMobileApp.mockReturnValue(true)
98
+ root.setState({ nativeAppIsAvailable: true })
99
+ root.find('a').simulate('click')
100
+ expect(openNativeFromNativeSpy).toHaveBeenCalled()
101
+ expect(startApp).toHaveBeenCalledWith({
102
+ appId: 'io.cozy.drive.mobile',
103
+ name: 'Cozy Drive',
104
+ uri: 'cozydrive://'
105
+ })
106
+ expect(appSwitchMock).toHaveBeenCalled()
107
+ })
108
+
109
+ it('should work for web -> native for Android (custom schema)', () => {
110
+ isMobile.mockReturnValue(true)
111
+ isAndroid.mockResolvedValue(true)
112
+ const root = shallow(
113
+ <DeprecatedAppItem t={tMock} app={app} onAppSwitch={appSwitchMock} />
114
+ ).dive()
115
+ root.find('a').simulate('click', { preventDefault: () => {} })
116
+ expect(openDeeplinkOrRedirect).toHaveBeenCalledWith(
117
+ 'cozydrive://',
118
+ expect.any(Function)
119
+ )
120
+ expect(appSwitchMock).toHaveBeenCalled()
121
+ })
122
+
123
+ it('should work for web -> native for iOS (universal link)', () => {
124
+ isMobile.mockReturnValue(true)
125
+ const root = shallow(
126
+ <DeprecatedAppItem t={tMock} app={app} onAppSwitch={appSwitchMock} />
127
+ ).dive()
128
+ root.find('a').simulate('click', { preventDefault: () => {} })
129
+
130
+ expect(generateUniversalLink).toHaveBeenCalled()
131
+ })
132
+
133
+ it('should work for native -> web', () => {
134
+ isMobileApp.mockReturnValue(true)
135
+ const root = shallow(
136
+ <DeprecatedAppItem t={tMock} app={app} onAppSwitch={appSwitchMock} />
137
+ ).dive()
138
+ root.find('a').simulate('click')
139
+ expect(appSwitchMock).toHaveBeenCalled()
140
+ })
141
+
142
+ it('should not crash if no href', () => {
143
+ isMobileApp.mockReturnValue(true)
144
+ const root = shallow(
145
+ <AppLinker onAppSwitch={appSwitchMock} slug={app.slug}>
146
+ {({ onClick, href, name }) => (
147
+ <div>
148
+ <a href={href} onClick={onClick}>
149
+ Open {name}
150
+ </a>
151
+ </div>
152
+ )}
153
+ </AppLinker>
154
+ )
155
+ expect(root.getElement()).toMatchSnapshot()
156
+ })
157
+ })
@@ -45,7 +45,7 @@ export class AppLinker extends React.Component {
45
45
  }
46
46
 
47
47
  async checkAppAvailability() {
48
- const { slug } = this.props
48
+ const slug = AppLinker.getSlug(this.props)
49
49
  const appInfo = NATIVE_APP_INFOS[slug]
50
50
  if (appInfo) {
51
51
  const nativeAppIsAvailable = Boolean(await memoizedCheckApp(appInfo))
@@ -53,8 +53,25 @@ export class AppLinker extends React.Component {
53
53
  }
54
54
  }
55
55
 
56
+ static getSlug(props) {
57
+ if (props.app && props.app.slug) {
58
+ return props.app.slug
59
+ }
60
+
61
+ return props.slug
62
+ }
63
+
64
+ static deprecateSlug(props) {
65
+ if (props.slug) {
66
+ console.warn(
67
+ `AppLinker's 'slug' prop is deprecated, please use 'app.slug' instead`
68
+ )
69
+ }
70
+ }
71
+
56
72
  static getOnClickHref(props, nativeAppIsAvailable, context) {
57
- const { slug, nativePath } = props
73
+ const { app, nativePath } = props
74
+ const slug = AppLinker.getSlug(props)
58
75
  let href = props.href
59
76
  let onClick = null
60
77
  const usingNativeApp = isMobileApp()
@@ -68,7 +85,7 @@ export class AppLinker extends React.Component {
68
85
 
69
86
  if (context) {
70
87
  return {
71
- onClick: () => context.call('openApp', href),
88
+ onClick: () => context.call('openApp', href, app),
72
89
  href: '#'
73
90
  }
74
91
  }
@@ -111,7 +128,8 @@ export class AppLinker extends React.Component {
111
128
  }
112
129
  }
113
130
  static openNativeFromWeb(props, ev) {
114
- const { href, slug, nativePath, onAppSwitch } = props
131
+ const { href, nativePath, onAppSwitch } = props
132
+ const slug = AppLinker.getSlug(props)
115
133
  const appInfo = NATIVE_APP_INFOS[slug]
116
134
 
117
135
  if (ev) {
@@ -134,7 +152,8 @@ export class AppLinker extends React.Component {
134
152
  }
135
153
 
136
154
  static openNativeFromNative(props, ev) {
137
- const { slug, onAppSwitch } = props
155
+ const { onAppSwitch } = props
156
+ const slug = AppLinker.getSlug(props)
138
157
  if (ev) {
139
158
  ev.preventDefault()
140
159
  }
@@ -150,7 +169,9 @@ export class AppLinker extends React.Component {
150
169
  }
151
170
 
152
171
  render() {
153
- const { children, slug } = this.props
172
+ const { children } = this.props
173
+ AppLinker.deprecateSlug(this.props)
174
+ const slug = AppLinker.getSlug(this.props)
154
175
  const { nativeAppIsAvailable } = this.state
155
176
  const appInfo = NATIVE_APP_INFOS[slug]
156
177
  const { href, onClick } = AppLinker.getOnClickHref(
@@ -166,18 +187,28 @@ AppLinker.defaultProps = {
166
187
  nativePath: '/'
167
188
  }
168
189
  AppLinker.propTypes = {
169
- // Slug of the app : drive / banks ...
170
- slug: PropTypes.string.isRequired,
190
+ /** DEPRECATED: please use app.slug prop */
191
+ slug: PropTypes.string,
171
192
  /*
172
193
  Full web url : Used by default on desktop browser
173
194
  Used as a fallback_uri on mobile web
174
195
  */
175
- href: PropTypes.string.isRequired,
196
+ href: PropTypes.string,
176
197
  /*
177
198
  Path used for "native link"
178
199
  */
179
200
  nativePath: PropTypes.string,
180
- onAppSwitch: PropTypes.func
201
+ onAppSwitch: PropTypes.func,
202
+ app: PropTypes.shape({
203
+ // Slug of the app : drive / banks ...
204
+ slug: PropTypes.string.isRequired,
205
+ // Information about mobile native app
206
+ mobile: PropTypes.shape({
207
+ schema: PropTypes.string,
208
+ id_playstore: PropTypes.string,
209
+ id_appstore: PropTypes.string
210
+ })
211
+ }).isRequired
181
212
  }
182
213
 
183
214
  export default AppLinker
@@ -18,11 +18,7 @@ class AppItem extends React.Component {
18
18
  render() {
19
19
  const { app, onAppSwitch } = this.props
20
20
  return (
21
- <AppLinker
22
- onAppSwitch={onAppSwitch}
23
- slug={app.slug}
24
- href={'https://fake.link'}
25
- >
21
+ <AppLinker onAppSwitch={onAppSwitch} href={'https://fake.link'} app={app}>
26
22
  {({ onClick, href, name }) => (
27
23
  <div>
28
24
  <a href={href} onClick={onClick}>
@@ -136,7 +132,7 @@ describe('app icon', () => {
136
132
  isMobileApp.mockReturnValue(true)
137
133
  spyConsoleError.mockImplementation(() => {})
138
134
  const root = shallow(
139
- <AppLinker onAppSwitch={appSwitchMock} slug={app.slug}>
135
+ <AppLinker onAppSwitch={appSwitchMock} app={app}>
140
136
  {({ onClick, href, name }) => (
141
137
  <div>
142
138
  <a href={href} onClick={onClick}>
@@ -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)