cozy-ui 116.0.0 → 117.1.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 CHANGED
@@ -1,3 +1,24 @@
1
+ # [117.1.0](https://github.com/cozy/cozy-ui/compare/v117.0.0...v117.1.0) (2025-01-21)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add Download and Add/Remove favorites actions ([6e4bca4](https://github.com/cozy/cozy-ui/commit/6e4bca4))
7
+
8
+ # [117.0.0](https://github.com/cozy/cozy-ui/compare/v116.0.0...v117.0.0) (2025-01-13)
9
+
10
+
11
+ ### Features
12
+
13
+ * Remove cordova stuff from AppLinker ([add9fbb](https://github.com/cozy/cozy-ui/commit/add9fbb))
14
+ * Remove deprecated slug prop from AppLinker ([a3d47e0](https://github.com/cozy/cozy-ui/commit/a3d47e0))
15
+
16
+
17
+ ### BREAKING CHANGES
18
+
19
+ * Use app={{ slug: 'drive' }} instead of slug='drive' as
20
+ AppLinker prop. Also, `name` is not passed anymore as render prop.
21
+
1
22
  # [116.0.0](https://github.com/cozy/cozy-ui/compare/v115.1.0...v116.0.0) (2025-01-07)
2
23
 
3
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui",
3
- "version": "116.0.0",
3
+ "version": "117.1.0",
4
4
  "description": "Cozy apps UI SDK",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -0,0 +1,66 @@
1
+ import React, { forwardRef } from 'react'
2
+
3
+ import { splitFilename } from 'cozy-client/dist/models/file'
4
+
5
+ import { getActionsI18n } from './locales/withActionsLocales'
6
+ import Icon from '../../Icon'
7
+ import StarOutlineIcon from '../../Icons/StarOutline'
8
+ import ListItemIcon from '../../ListItemIcon'
9
+ import ListItemText from '../../ListItemText'
10
+ import ActionsMenuItem from '../ActionsMenuItem'
11
+
12
+ const makeComponent = (label, icon) => {
13
+ const Component = forwardRef((props, ref) => {
14
+ return (
15
+ <ActionsMenuItem {...props} ref={ref}>
16
+ <ListItemIcon>
17
+ <Icon icon={icon} />
18
+ </ListItemIcon>
19
+ <ListItemText primary={label} />
20
+ </ActionsMenuItem>
21
+ )
22
+ })
23
+
24
+ Component.displayName = 'addToFavorites'
25
+
26
+ return Component
27
+ }
28
+
29
+ export const addToFavorites = ({ showAlert }) => {
30
+ const { t } = getActionsI18n()
31
+ const icon = StarOutlineIcon
32
+ const label = t('favorites.add.label')
33
+
34
+ return {
35
+ name: 'addToFavorites',
36
+ icon,
37
+ label,
38
+ displayCondition: docs =>
39
+ docs.length > 0 && docs.every(doc => !doc.cozyMetadata?.favorite),
40
+ Component: makeComponent(label, icon),
41
+ action: async (docs, { client }) => {
42
+ try {
43
+ for (const doc of docs) {
44
+ await client.save({
45
+ ...doc,
46
+ cozyMetadata: {
47
+ ...doc.cozyMetadata,
48
+ favorite: true
49
+ }
50
+ })
51
+ }
52
+
53
+ const { filename } = splitFilename(docs[0])
54
+ showAlert({
55
+ message: t('favorites.add.success', {
56
+ filename,
57
+ smart_count: docs.length
58
+ }),
59
+ severity: 'success'
60
+ })
61
+ } catch (error) {
62
+ showAlert({ message: t('favorites.error'), severity: 'error' })
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,42 @@
1
+ import React, { forwardRef } from 'react'
2
+
3
+ import { downloadFile } from 'cozy-client/dist/models/file'
4
+
5
+ import { getActionsI18n } from './locales/withActionsLocales'
6
+ import Icon from '../../Icon'
7
+ import DownloadIcon from '../../Icons/Download'
8
+ import ListItemIcon from '../../ListItemIcon'
9
+ import ListItemText from '../../ListItemText'
10
+ import ActionsMenuItem from '../ActionsMenuItem'
11
+
12
+ const makeComponent = (label, icon) => {
13
+ const Component = forwardRef((props, ref) => {
14
+ return (
15
+ <ActionsMenuItem {...props} ref={ref}>
16
+ <ListItemIcon>
17
+ <Icon icon={icon} />
18
+ </ListItemIcon>
19
+ <ListItemText primary={label} />
20
+ </ActionsMenuItem>
21
+ )
22
+ })
23
+
24
+ Component.displayName = 'download'
25
+
26
+ return Component
27
+ }
28
+
29
+ export const download = ({ encryptedUrl }) => {
30
+ const { t } = getActionsI18n()
31
+ const icon = DownloadIcon
32
+ const label = t('download')
33
+
34
+ return {
35
+ name: 'download',
36
+ icon,
37
+ label,
38
+ Component: makeComponent(label, icon),
39
+ action: (docs, { client, webviewIntent }) =>
40
+ downloadFile({ client, file: docs[0], url: encryptedUrl, webviewIntent })
41
+ }
42
+ }
@@ -5,6 +5,9 @@ export { smsTo } from './smsTo'
5
5
  export { call } from './call'
6
6
  export { emailTo } from './emailTo'
7
7
  export { print } from './print'
8
+ export { download } from './download'
9
+ export { addToFavorites } from './addToFavorites'
10
+ export { removeFromFavorites } from './removeFromFavorites'
8
11
  export { viewInContacts } from './viewInContacts'
9
12
  export { viewInDrive } from './viewInDrive'
10
13
  export { copyToClipboard } from './copyToClipboard'
@@ -5,6 +5,18 @@
5
5
  "emailTo": "Send an email",
6
6
  "smsTo": "Send a message",
7
7
  "print": "Print",
8
+ "download": "Download",
9
+ "favorites": {
10
+ "add": {
11
+ "label": "Add to favorites",
12
+ "success": "%{filename} has been added to favorites |||| These items have been added to favorites"
13
+ },
14
+ "remove": {
15
+ "label": "Remove from favorites",
16
+ "success": "%{filename} has been removed from favorites |||| These items have been removed from favorites"
17
+ },
18
+ "error": "An error occurred, please try again."
19
+ },
8
20
  "others": "Others",
9
21
  "editAttribute": "Edit attribute",
10
22
  "copyToClipboard": {
@@ -5,6 +5,18 @@
5
5
  "emailTo": "Envoyer un e-mail",
6
6
  "smsTo": "Envoyer un message",
7
7
  "print": "Imprimer",
8
+ "download": "Télécharger",
9
+ "favorites": {
10
+ "add": {
11
+ "label": "Ajouter aux favoris",
12
+ "success": "%{filename} a été ajouté aux favoris |||| Ces éléments ont été ajoutés aux favoris"
13
+ },
14
+ "remove": {
15
+ "label": "Retirer des favoris",
16
+ "success": "%{filename} a été retiré des favoris |||| Ces éléments ont été retirés des favoris"
17
+ },
18
+ "error": "Une erreur est survenue, merci de réessayer."
19
+ },
8
20
  "others": "Autres",
9
21
  "editAttribute": "Editer l'attribut",
10
22
  "copyToClipboard": {
@@ -0,0 +1,66 @@
1
+ import React, { forwardRef } from 'react'
2
+
3
+ import { splitFilename } from 'cozy-client/dist/models/file'
4
+
5
+ import { getActionsI18n } from './locales/withActionsLocales'
6
+ import Icon from '../../Icon'
7
+ import StarIcon from '../../Icons/Star'
8
+ import ListItemIcon from '../../ListItemIcon'
9
+ import ListItemText from '../../ListItemText'
10
+ import ActionsMenuItem from '../ActionsMenuItem'
11
+
12
+ const makeComponent = (label, icon) => {
13
+ const Component = forwardRef((props, ref) => {
14
+ return (
15
+ <ActionsMenuItem {...props} ref={ref}>
16
+ <ListItemIcon>
17
+ <Icon icon={icon} />
18
+ </ListItemIcon>
19
+ <ListItemText primary={label} />
20
+ </ActionsMenuItem>
21
+ )
22
+ })
23
+
24
+ Component.displayName = 'removeFromFavorites'
25
+
26
+ return Component
27
+ }
28
+
29
+ export const removeFromFavorites = ({ showAlert }) => {
30
+ const { t } = getActionsI18n()
31
+ const icon = StarIcon
32
+ const label = t('favorites.remove.label')
33
+
34
+ return {
35
+ name: 'removeFromFavorites',
36
+ icon,
37
+ label,
38
+ displayCondition: docs =>
39
+ docs.length > 0 && docs.every(doc => doc.cozyMetadata?.favorite),
40
+ Component: makeComponent(label, icon),
41
+ action: async (docs, { client }) => {
42
+ try {
43
+ for (const doc of docs) {
44
+ await client.save({
45
+ ...doc,
46
+ cozyMetadata: {
47
+ ...doc.cozyMetadata,
48
+ favorite: false
49
+ }
50
+ })
51
+ }
52
+
53
+ const { filename } = splitFilename(docs[0])
54
+ showAlert({
55
+ message: t('favorites.success.remove', {
56
+ filename,
57
+ smart_count: docs.length
58
+ }),
59
+ severity: 'success'
60
+ })
61
+ } catch (error) {
62
+ showAlert({ message: t('favorites.error'), severity: 'error' })
63
+ }
64
+ }
65
+ }
66
+ }
@@ -1,15 +1,10 @@
1
1
  Render-props component that provides onClick/href handler to
2
2
  apply to an anchor that needs to open an app.
3
3
 
4
- If the app is known to Cozy (for example Drive or Banks), and
5
- the user has installed it on its device, the native app will
6
- be opened.
7
-
8
4
  The app's manifest can be set in the `app` prop. Then, in a
9
5
  ReactNative environment, the AppLinker will be able to send
10
6
  `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))
7
+ data.
13
8
 
14
9
  Handles several cases:
15
10
 
@@ -28,32 +23,9 @@ const app = {
28
23
  };
29
24
 
30
25
  <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'>{
54
- ({ onClick, href, name }) => (
26
+ ({ onClick, href }) => (
55
27
  <a href={href} onClick={onClick}>
56
- Open { name }
28
+ Open
57
29
  </a>
58
30
  )
59
31
  }</AppLinker>
@@ -1,16 +1,5 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`app icon should not crash if no href 1`] = `
4
- <div>
5
- <div>
6
- <a>
7
- Open
8
- Cozy Drive
9
- </a>
10
- </div>
11
- </div>
12
- `;
13
-
14
3
  exports[`app icon should render correctly 1`] = `
15
4
  <div>
16
5
  <div>
@@ -18,7 +7,6 @@ exports[`app icon should render correctly 1`] = `
18
7
  href="https://fake.link"
19
8
  >
20
9
  Open
21
- Cozy Drive
22
10
  </a>
23
11
  </div>
24
12
  </div>
@@ -2,45 +2,25 @@ import PropTypes from 'prop-types'
2
2
  import React from 'react'
3
3
 
4
4
  import { withClient } from 'cozy-client'
5
- import {
6
- checkApp,
7
- startApp,
8
- isMobileApp,
9
- isMobile,
10
- openDeeplinkOrRedirect,
11
- isAndroid,
12
- isFlagshipApp
13
- } from 'cozy-device-helper'
5
+ import { isFlagshipApp } from 'cozy-device-helper'
14
6
  import { WebviewContext } from 'cozy-intent'
15
7
  import logger from 'cozy-logger'
16
8
 
17
- import expiringMemoize from './expiringMemoize'
18
9
  import {
19
10
  generateUniversalLink,
20
11
  generateWebLink,
21
12
  getUniversalLinkDomain
22
13
  } from './native'
23
- import { NATIVE_APP_INFOS } from './native.config'
24
-
25
- const expirationDelay = 10 * 1000
26
- const memoizedCheckApp = expiringMemoize(
27
- appInfo => checkApp(appInfo).catch(() => false),
28
- expirationDelay,
29
- appInfo => appInfo.appId
30
- )
31
14
 
32
15
  export class AppLinker extends React.Component {
33
16
  static contextType = WebviewContext
34
17
 
35
18
  state = {
36
- nativeAppIsAvailable: null,
37
- isFetchingAppInfo: false
19
+ imgRef: null
38
20
  }
39
21
 
40
22
  constructor(props) {
41
23
  super(props)
42
-
43
- this.imgRef = null
44
24
  }
45
25
 
46
26
  setImgRef = img => {
@@ -48,44 +28,14 @@ export class AppLinker extends React.Component {
48
28
  this.setState({ imgRef: this.imgRef })
49
29
  }
50
30
 
51
- componentDidMount() {
52
- if (isMobileApp()) {
53
- this.checkAppAvailability()
54
- }
55
- }
56
-
57
- async checkAppAvailability() {
58
- const slug = AppLinker.getSlug(this.props)
59
- const appInfo = NATIVE_APP_INFOS[slug]
60
- if (appInfo) {
61
- const nativeAppIsAvailable = Boolean(await memoizedCheckApp(appInfo))
62
- this.setState({ nativeAppIsAvailable })
63
- }
64
- }
65
-
66
31
  static getSlug(props) {
67
- if (props.app && props.app.slug) {
68
- return props.app.slug
69
- }
70
-
71
- return props.slug
72
- }
73
-
74
- static deprecateSlug(props) {
75
- if (props.slug) {
76
- console.warn(
77
- `AppLinker's 'slug' prop is deprecated, please use 'app.slug' instead`
78
- )
79
- }
32
+ return props.app.slug
80
33
  }
81
34
 
82
- static getOnClickHref(props, nativeAppIsAvailable, context, imgRef) {
83
- const { app, client, nativePath } = props
84
- const slug = AppLinker.getSlug(props)
35
+ static getOnClickHref(props, context, imgRef) {
36
+ const { app, client } = props
85
37
  let href = props.href
86
38
  let onClick = null
87
- const usingNativeApp = isMobileApp()
88
- const appInfo = NATIVE_APP_INFOS[slug]
89
39
 
90
40
  if (isFlagshipApp()) {
91
41
  const { app: currentApp } = client
@@ -114,59 +64,11 @@ export class AppLinker extends React.Component {
114
64
  }
115
65
  }
116
66
 
117
- if (usingNativeApp) {
118
- if (nativeAppIsAvailable) {
119
- // If we are on the native app and the other native app is available,
120
- // we open the native app
121
- onClick = AppLinker.openNativeFromNative.bind(this, props)
122
- href = '#'
123
- } else {
124
- // If we are on a native app, but the other native app is not available
125
- // we open the web link, this is done by the href prop. We still
126
- // have to call the prop callback
127
- onClick = AppLinker.openWeb.bind(this, props)
128
- }
129
- } else if (isMobile() && appInfo) {
130
- // If we are on the "mobile web version", we try to open the native app
131
- // if it exists with an universal links. If it fails, we redirect to the web
132
- // version of the requested app
133
- // Only on iOS ATM
134
- if (isAndroid()) {
135
- onClick = AppLinker.openNativeFromWeb.bind(this, props)
136
- } else {
137
- // Since generateUniversalLink can rise an error, let's catch it to not crash
138
- // all the page.
139
- try {
140
- href = generateUniversalLink({ slug, nativePath, fallbackUrl: href })
141
- } catch (err) {
142
- console.error(err)
143
- href = '#'
144
- }
145
- }
146
- }
147
-
148
67
  return {
149
68
  href,
150
69
  onClick
151
70
  }
152
71
  }
153
- static openNativeFromWeb(props, ev) {
154
- const { href, nativePath, onAppSwitch } = props
155
- const slug = AppLinker.getSlug(props)
156
- const appInfo = NATIVE_APP_INFOS[slug]
157
-
158
- if (ev) {
159
- ev.preventDefault()
160
- }
161
-
162
- AppLinker.onAppSwitch(onAppSwitch)
163
- openDeeplinkOrRedirect(
164
- appInfo.uri + (nativePath === '/' ? '' : nativePath),
165
- function () {
166
- window.location.href = href
167
- }
168
- )
169
- }
170
72
 
171
73
  static onAppSwitch(onAppSwitchFn) {
172
74
  if (typeof onAppSwitchFn === 'function') {
@@ -174,38 +76,20 @@ export class AppLinker extends React.Component {
174
76
  }
175
77
  }
176
78
 
177
- static openNativeFromNative(props, ev) {
178
- const { onAppSwitch } = props
179
- const slug = AppLinker.getSlug(props)
180
- if (ev) {
181
- ev.preventDefault()
182
- }
183
- const appInfo = NATIVE_APP_INFOS[slug]
184
- AppLinker.onAppSwitch(onAppSwitch)
185
- startApp(appInfo).catch(err => {
186
- console.error('AppLinker: Could not open native app', err)
187
- })
188
- }
189
-
190
79
  static openWeb(props) {
191
80
  AppLinker.onAppSwitch(props.onAppSwitch)
192
81
  }
193
82
 
194
83
  render() {
195
84
  const { children } = this.props
196
- AppLinker.deprecateSlug(this.props)
197
- const slug = AppLinker.getSlug(this.props)
198
- const { nativeAppIsAvailable } = this.state
199
- const appInfo = NATIVE_APP_INFOS[slug]
85
+
200
86
  const { href, onClick } = AppLinker.getOnClickHref(
201
87
  this.props,
202
- nativeAppIsAvailable,
203
88
  this.context,
204
89
  this.state.imgRef
205
90
  )
206
91
 
207
92
  return children({
208
- ...appInfo,
209
93
  iconRef: this.setImgRef,
210
94
  onClick: onClick,
211
95
  href
@@ -213,38 +97,18 @@ export class AppLinker extends React.Component {
213
97
  }
214
98
  }
215
99
 
216
- AppLinker.defaultProps = {
217
- nativePath: '/'
218
- }
219
100
  AppLinker.propTypes = {
220
- /** DEPRECATED: please use app.slug prop */
221
- slug: PropTypes.string,
222
101
  /*
223
102
  Full web url : Used by default on desktop browser
224
103
  Used as a fallback_uri on mobile web
225
104
  */
226
105
  href: PropTypes.string,
227
- /*
228
- Path used for "native link"
229
- */
230
- nativePath: PropTypes.string,
231
106
  onAppSwitch: PropTypes.func,
232
107
  app: PropTypes.shape({
233
108
  // Slug of the app : drive / banks ...
234
- slug: PropTypes.string.isRequired,
235
- // Information about mobile native app
236
- mobile: PropTypes.shape({
237
- schema: PropTypes.string,
238
- id_playstore: PropTypes.string,
239
- id_appstore: PropTypes.string
240
- })
109
+ slug: PropTypes.string.isRequired
241
110
  }).isRequired
242
111
  }
243
112
 
244
113
  export default withClient(AppLinker)
245
- export {
246
- NATIVE_APP_INFOS,
247
- getUniversalLinkDomain,
248
- generateWebLink,
249
- generateUniversalLink
250
- }
114
+ export { getUniversalLinkDomain, generateWebLink, generateUniversalLink }