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.
- package/CHANGELOG.md +37 -0
- package/assets/icons/ui/folder-moveto.svg +1 -0
- package/package.json +5 -4
- package/react/AppLinker/Readme.md +35 -1
- package/react/AppLinker/__snapshots__/index.deprecated.spec.jsx.snap +24 -0
- package/react/AppLinker/index.deprecated.spec.jsx +157 -0
- package/react/AppLinker/index.jsx +41 -10
- package/react/AppLinker/index.spec.jsx +2 -6
- 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/Icon/Readme.md +3 -1
- package/react/Icon/icons-sprite.js +1 -1
- package/react/Icons/FolderMoveto.jsx +15 -0
- package/react/__snapshots__/examples.spec.jsx.snap +128 -112
- package/react/index.js +1 -0
- package/transpiled/react/AppLinker/index.js +40 -13
- 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/Icon/icons-sprite.js +1 -1
- package/transpiled/react/Icons/FolderMoveto.js +14 -0
- package/transpiled/react/index.js +2 -1
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
170
|
-
slug: PropTypes.string
|
|
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
|
|
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}
|
|
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)
|