cozy-sharing 37.0.2 → 37.2.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 (65) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/OpenSharingLinkButton.js +1 -1
  3. package/dist/SharingProvider.js +315 -138
  4. package/dist/SharingProvider.spec.js +454 -240
  5. package/dist/actions/addToCozySharingLink.js +1 -1
  6. package/dist/actions/createCozySharingLink.js +1 -1
  7. package/dist/actions/shareNative.js +1 -1
  8. package/dist/actions/syncToCozySharingLink.js +1 -1
  9. package/dist/components/Avatar/AvatarList.js +1 -1
  10. package/dist/components/Recipient/GroupRecipient.js +0 -2
  11. package/dist/components/Recipient/GroupRecipientDetail.js +2 -25
  12. package/dist/components/Recipient/GroupRecipientDetailWithoutAccess.js +1 -1
  13. package/dist/components/Recipient/LinkRecipient.js +1 -1
  14. package/dist/components/Recipient/LinkRecipientLite.js +1 -1
  15. package/dist/components/Recipient/actions/revokeLink.js +1 -1
  16. package/dist/components/ShareAutosuggest.js +6 -6
  17. package/dist/components/ShareButton.js +1 -1
  18. package/dist/components/ShareByLink.js +1 -1
  19. package/dist/components/ShareLinkAccessModal/ShareLinkAccessModal.js +316 -0
  20. package/dist/components/ShareLinkAccessModal/ShareLinkAccessModal.spec.js +372 -0
  21. package/dist/components/ShareLinkAccessModal/index.js +1 -0
  22. package/dist/components/ShareRestrictionModal/BoxDate.js +1 -1
  23. package/dist/components/ShareRestrictionModal/BoxEditingRights.js +1 -1
  24. package/dist/components/ShareRestrictionModal/BoxPassword.js +1 -1
  25. package/dist/components/ShareRestrictionModal/ShareRestrictionModal.js +1 -1
  26. package/dist/components/ShareRestrictionModal/helpers.js +36 -18
  27. package/dist/components/ShareRestrictionModal/helpers.spec.js +3 -3
  28. package/dist/components/SharedBadge.js +1 -1
  29. package/dist/components/SharedDrive/SharedDriveHeader.js +1 -1
  30. package/dist/index.js +1 -0
  31. package/locales/en.json +21 -3
  32. package/locales/fr.json +22 -4
  33. package/locales/ru.json +21 -3
  34. package/locales/vi.json +21 -3
  35. package/package.json +3 -3
  36. package/src/OpenSharingLinkButton.jsx +1 -1
  37. package/src/SharingProvider.jsx +91 -27
  38. package/src/SharingProvider.spec.jsx +212 -60
  39. package/src/actions/addToCozySharingLink.js +1 -1
  40. package/src/actions/createCozySharingLink.js +1 -1
  41. package/src/actions/shareNative.js +1 -1
  42. package/src/actions/syncToCozySharingLink.js +1 -1
  43. package/src/components/Avatar/AvatarList.jsx +1 -1
  44. package/src/components/Recipient/GroupRecipient.jsx +1 -2
  45. package/src/components/Recipient/GroupRecipientDetail.jsx +3 -33
  46. package/src/components/Recipient/GroupRecipientDetailWithoutAccess.jsx +1 -1
  47. package/src/components/Recipient/LinkRecipient.jsx +1 -1
  48. package/src/components/Recipient/LinkRecipientLite.jsx +1 -1
  49. package/src/components/Recipient/actions/revokeLink.js +1 -1
  50. package/src/components/ShareAutosuggest.jsx +3 -4
  51. package/src/components/ShareButton.jsx +1 -1
  52. package/src/components/ShareByLink.jsx +1 -1
  53. package/src/components/ShareLinkAccessModal/ShareLinkAccessModal.jsx +293 -0
  54. package/src/components/ShareLinkAccessModal/ShareLinkAccessModal.spec.jsx +291 -0
  55. package/src/components/ShareLinkAccessModal/index.js +1 -0
  56. package/src/components/ShareRestrictionModal/BoxDate.jsx +1 -1
  57. package/src/components/ShareRestrictionModal/BoxEditingRights.jsx +1 -1
  58. package/src/components/ShareRestrictionModal/BoxPassword.jsx +1 -1
  59. package/src/components/ShareRestrictionModal/ShareRestrictionModal.jsx +1 -1
  60. package/src/components/ShareRestrictionModal/helpers.js +30 -6
  61. package/src/components/ShareRestrictionModal/helpers.spec.js +3 -3
  62. package/src/components/SharedBadge.jsx +2 -1
  63. package/src/components/SharedDrive/SharedDriveHeader.jsx +1 -1
  64. package/src/helpers/sharings.js +0 -1
  65. package/src/index.jsx +1 -0
@@ -0,0 +1,291 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
2
+ import React from 'react'
3
+
4
+ import { ShareLinkAccessModal } from './ShareLinkAccessModal'
5
+ import { useSharingContext } from '../../hooks/useSharingContext'
6
+
7
+ jest.mock('../../hooks/useSharingContext', () => ({
8
+ useSharingContext: jest.fn()
9
+ }))
10
+
11
+ jest.mock('../../hoc/withLocales', () => Component => props => (
12
+ <Component {...props} />
13
+ ))
14
+
15
+ jest.mock('cozy-ui/transpiled/react/CozyDialogs', () => ({
16
+ ConfirmDialog: ({ title, content, actions, onBack, onClose, size }) => (
17
+ <div role="dialog" data-size={size}>
18
+ {onBack && <button onClick={onBack}>Back</button>}
19
+ {onClose && <button onClick={onClose}>Close</button>}
20
+ <div>{title}</div>
21
+ <div>{content}</div>
22
+ <div>{actions}</div>
23
+ </div>
24
+ )
25
+ }))
26
+
27
+ jest.mock('cozy-ui/transpiled/react/DropdownButton', () => {
28
+ const mockReact = require('react')
29
+
30
+ return {
31
+ __esModule: true,
32
+ default: mockReact.forwardRef(
33
+ ({ children, onClick, disabled, 'aria-label': ariaLabel }, ref) => (
34
+ <button
35
+ ref={ref}
36
+ aria-label={ariaLabel}
37
+ onClick={onClick}
38
+ disabled={disabled}
39
+ >
40
+ {children}
41
+ </button>
42
+ )
43
+ )
44
+ }
45
+ })
46
+
47
+ jest.mock('cozy-ui/transpiled/react/Menu', () => ({
48
+ __esModule: true,
49
+ default: ({ open, children }) =>
50
+ open ? <div role="menu">{children}</div> : null
51
+ }))
52
+
53
+ jest.mock('cozy-ui/transpiled/react/MenuItem', () => ({
54
+ __esModule: true,
55
+ default: ({ children, onClick }) => (
56
+ <button onClick={onClick}>{children}</button>
57
+ )
58
+ }))
59
+
60
+ jest.mock('cozy-ui/transpiled/react/DatePicker', () => ({
61
+ __esModule: true,
62
+ default: ({ label, value, onChange, disabled }) => (
63
+ <input
64
+ aria-label={label}
65
+ type="date"
66
+ value={value.toISOString().slice(0, 10)}
67
+ onChange={event => onChange(new Date(event.target.value))}
68
+ disabled={disabled}
69
+ />
70
+ )
71
+ }))
72
+
73
+ jest.mock('cozy-ui/transpiled/react/TextField', () => ({
74
+ __esModule: true,
75
+ default: ({ label, value, onChange, disabled }) => (
76
+ <input
77
+ aria-label={label}
78
+ value={value}
79
+ onChange={onChange}
80
+ disabled={disabled}
81
+ />
82
+ )
83
+ }))
84
+
85
+ jest.mock('twake-i18n', () => ({
86
+ useI18n: () => ({
87
+ t: key =>
88
+ ({
89
+ 'ShareLinkAccessModal.title': 'Set link access',
90
+ 'ShareLinkAccessModal.introText':
91
+ 'Choose what recipients can do with these files.',
92
+ 'ShareLinkAccessModal.anyoneWithLink':
93
+ 'Allow anyone with the link to access',
94
+ 'ShareLinkAccessModal.settings': 'Sharing settings',
95
+ 'ShareLinkAccessModal.accessLevel': 'Access level',
96
+ 'ShareLinkAccessModal.viewer': 'Viewer',
97
+ 'ShareLinkAccessModal.editor': 'Editor',
98
+ 'ShareLinkAccessModal.cancel': 'Cancel',
99
+ 'ShareLinkAccessModal.addLinks': 'Add links',
100
+ 'ShareLinkAccessModal.settingsTitle': 'Sharing settings',
101
+ 'ShareLinkAccessModal.expiry': 'Set an expiration date',
102
+ 'ShareLinkAccessModal.expiryDate': 'Expiration date',
103
+ 'ShareLinkAccessModal.password': 'Require a password',
104
+ 'ShareLinkAccessModal.passwordLabel': 'Password',
105
+ 'ShareLinkAccessModal.passwordTooShort':
106
+ 'Password must contain at least 4 characters.',
107
+ 'ShareLinkAccessModal.error.persistence':
108
+ 'Could not save link permissions. Please try again.'
109
+ })[key] || key
110
+ })
111
+ }))
112
+
113
+ describe('ShareLinkAccessModal', () => {
114
+ const documents = [
115
+ { _id: 'file-id', name: 'invoice.pdf' },
116
+ { _id: 'folder-id', name: 'Projects' }
117
+ ]
118
+ const onCancel = jest.fn()
119
+ const onSuccess = jest.fn()
120
+ const ensureSharingLink = jest.fn()
121
+
122
+ beforeEach(() => {
123
+ jest.clearAllMocks()
124
+ useSharingContext.mockReturnValue({ ensureSharingLink })
125
+ })
126
+
127
+ const renderModal = (overrides = {}) =>
128
+ render(
129
+ <ShareLinkAccessModal
130
+ documents={documents}
131
+ onCancel={onCancel}
132
+ onSuccess={onSuccess}
133
+ {...overrides}
134
+ />
135
+ )
136
+
137
+ it('shows the selected documents with viewer access by default', () => {
138
+ renderModal()
139
+
140
+ expect(screen.getByRole('dialog')).toHaveTextContent('invoice.pdf')
141
+ expect(screen.getByRole('dialog')).toHaveTextContent('Projects')
142
+ expect(screen.getByText('invoice.pdf').parentElement).toHaveClass(
143
+ 'u-maw-100'
144
+ )
145
+ expect(screen.getByLabelText('Access level')).toHaveTextContent('Viewer')
146
+ expect(screen.getByRole('dialog')).toHaveAttribute('data-size', 'small')
147
+ })
148
+
149
+ it('keeps document icons inside their chips', () => {
150
+ const renderDocumentIcon = jest.fn(document => (
151
+ <span data-testid={`icon-${document._id}`}>icon</span>
152
+ ))
153
+
154
+ renderModal({ renderDocumentIcon })
155
+
156
+ expect(renderDocumentIcon).toHaveBeenNthCalledWith(1, documents[0], 18)
157
+ expect(renderDocumentIcon).toHaveBeenNthCalledWith(2, documents[1], 18)
158
+ expect(screen.getByTestId('icon-file-id').parentElement).toHaveClass(
159
+ 'u-flex',
160
+ 'u-ov-hidden'
161
+ )
162
+ })
163
+
164
+ it('calls onCancel from the back button', () => {
165
+ renderModal()
166
+
167
+ fireEvent.click(screen.getByRole('button', { name: 'Back' }))
168
+
169
+ expect(onCancel).toHaveBeenCalledTimes(1)
170
+ })
171
+
172
+ it('preserves the access settings after returning from the settings screen', () => {
173
+ renderModal()
174
+
175
+ fireEvent.click(screen.getByLabelText('Access level'))
176
+ fireEvent.click(screen.getByRole('button', { name: 'Editor' }))
177
+ fireEvent.click(screen.getByRole('button', { name: 'Sharing settings' }))
178
+ fireEvent.click(screen.getByLabelText('Set an expiration date'))
179
+ fireEvent.click(screen.getByLabelText('Require a password'))
180
+ fireEvent.change(screen.getByLabelText('Password'), {
181
+ target: { value: 'secret' }
182
+ })
183
+ fireEvent.click(screen.getByRole('button', { name: 'Back' }))
184
+
185
+ expect(screen.getByLabelText('Access level')).toHaveTextContent('Editor')
186
+
187
+ fireEvent.click(screen.getByRole('button', { name: 'Sharing settings' }))
188
+
189
+ expect(screen.getByLabelText('Set an expiration date')).toBeChecked()
190
+ expect(screen.getByLabelText('Require a password')).toBeChecked()
191
+ expect(screen.getByLabelText('Password')).toHaveValue('secret')
192
+ })
193
+
194
+ it('requires a four-character password before adding links', () => {
195
+ renderModal()
196
+
197
+ fireEvent.click(screen.getByRole('button', { name: 'Sharing settings' }))
198
+ fireEvent.click(screen.getByLabelText('Require a password'))
199
+ fireEvent.change(screen.getByLabelText('Password'), {
200
+ target: { value: 'abc' }
201
+ })
202
+ fireEvent.click(screen.getByRole('button', { name: 'Back' }))
203
+
204
+ expect(screen.getByRole('button', { name: 'Add links' })).toBeDisabled()
205
+ })
206
+
207
+ it('disables every main action while links are being saved', async () => {
208
+ let resolveSharingLink
209
+ ensureSharingLink.mockReturnValue(
210
+ new Promise(resolve => {
211
+ resolveSharingLink = resolve
212
+ })
213
+ )
214
+
215
+ renderModal()
216
+
217
+ fireEvent.click(screen.getByRole('button', { name: 'Add links' }))
218
+
219
+ await waitFor(() => {
220
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeDisabled()
221
+ expect(
222
+ screen.getByRole('button', { name: 'Sharing settings' })
223
+ ).toBeDisabled()
224
+ expect(screen.getByLabelText('Access level')).toBeDisabled()
225
+ })
226
+
227
+ resolveSharingLink({ documentId: 'file-id', url: 'https://first' })
228
+
229
+ await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1))
230
+ })
231
+
232
+ it('starts every link concurrently and returns ordered results', async () => {
233
+ let resolveFirst
234
+ let resolveSecond
235
+ ensureSharingLink
236
+ .mockReturnValueOnce(
237
+ new Promise(resolve => {
238
+ resolveFirst = resolve
239
+ })
240
+ )
241
+ .mockReturnValueOnce(
242
+ new Promise(resolve => {
243
+ resolveSecond = resolve
244
+ })
245
+ )
246
+
247
+ renderModal()
248
+
249
+ fireEvent.click(screen.getByRole('button', { name: 'Add links' }))
250
+
251
+ expect(ensureSharingLink).toHaveBeenCalledTimes(2)
252
+ resolveSecond({ documentId: 'folder-id', url: 'https://second' })
253
+ resolveFirst({ documentId: 'file-id', url: 'https://first' })
254
+
255
+ await waitFor(() =>
256
+ expect(onSuccess).toHaveBeenCalledWith([
257
+ { documentId: 'file-id', url: 'https://first' },
258
+ { documentId: 'folder-id', url: 'https://second' }
259
+ ])
260
+ )
261
+ expect(ensureSharingLink).toHaveBeenNthCalledWith(1, documents[0], {
262
+ editingRights: 'readOnly',
263
+ dateEnabled: false,
264
+ selectedDate: null,
265
+ passwordEnabled: false,
266
+ password: ''
267
+ })
268
+ })
269
+
270
+ it('keeps the draft open after a failed save so the user can retry', async () => {
271
+ ensureSharingLink
272
+ .mockRejectedValueOnce(new Error('offline'))
273
+ .mockResolvedValueOnce({ documentId: 'file-id', url: 'https://first' })
274
+ .mockResolvedValueOnce({ documentId: 'folder-id', url: 'https://second' })
275
+
276
+ renderModal()
277
+
278
+ fireEvent.click(screen.getByRole('button', { name: 'Add links' }))
279
+
280
+ await waitFor(() =>
281
+ expect(screen.getByRole('dialog')).toHaveTextContent(
282
+ 'Could not save link permissions. Please try again.'
283
+ )
284
+ )
285
+ expect(onSuccess).not.toHaveBeenCalled()
286
+
287
+ fireEvent.click(screen.getByRole('button', { name: 'Add links' }))
288
+
289
+ await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1))
290
+ })
291
+ })
@@ -0,0 +1 @@
1
+ export { default as ShareLinkAccessModal } from './ShareLinkAccessModal'
@@ -1,8 +1,8 @@
1
- import { Icon, Calendar } from '@linagora/twake-icons'
2
1
  import { format } from 'date-fns'
3
2
  import PropTypes from 'prop-types'
4
3
  import React from 'react'
5
4
 
5
+ import { Icon, Calendar } from '@linagora/twake-icons'
6
6
  import Box from 'cozy-ui/transpiled/react/Box'
7
7
  import DatePicker from 'cozy-ui/transpiled/react/DatePicker'
8
8
  import List from 'cozy-ui/transpiled/react/List'
@@ -1,7 +1,7 @@
1
- import { Icon, Bottom, People } from '@linagora/twake-icons'
2
1
  import PropTypes from 'prop-types'
3
2
  import React, { useReducer, useRef } from 'react'
4
3
 
4
+ import { Icon, Bottom, People } from '@linagora/twake-icons'
5
5
  import { useClient } from 'cozy-client'
6
6
  import { isDirectory as isDir } from 'cozy-client/dist/models/file'
7
7
  import flag from 'cozy-flags'
@@ -1,7 +1,7 @@
1
- import { Icon, Copy, Password } from '@linagora/twake-icons'
2
1
  import PropTypes from 'prop-types'
3
2
  import React, { useState } from 'react'
4
3
 
4
+ import { Icon, Copy, Password } from '@linagora/twake-icons'
5
5
  import Box from 'cozy-ui/transpiled/react/Box'
6
6
  import IconButton from 'cozy-ui/transpiled/react/IconButton'
7
7
  import List from 'cozy-ui/transpiled/react/List'
@@ -1,8 +1,8 @@
1
- import { Icon, Trash } from '@linagora/twake-icons'
2
1
  import { addDays } from 'date-fns'
3
2
  import PropTypes from 'prop-types'
4
3
  import React, { useState } from 'react'
5
4
 
5
+ import { Icon, Trash } from '@linagora/twake-icons'
6
6
  import { generateWebLink, useClient } from 'cozy-client'
7
7
  import flag from 'cozy-flags'
8
8
  import Button from 'cozy-ui/transpiled/react/Buttons'
@@ -192,6 +192,24 @@ export const makeTTL = selectedDate => {
192
192
  }
193
193
  }
194
194
 
195
+ export const getLinkAccessOptions = ({
196
+ editingRights,
197
+ dateEnabled,
198
+ selectedDate,
199
+ passwordEnabled,
200
+ password
201
+ }) => {
202
+ const expirationDate =
203
+ dateEnabled && selectedDate ? toExpirationDate(selectedDate) : null
204
+
205
+ return {
206
+ verbs: editingRights === 'write' ? WRITE_PERMS : READ_ONLY_PERMS,
207
+ expiresAt: expirationDate?.toISOString() || '',
208
+ ttl: expirationDate ? makeTTL(expirationDate) : undefined,
209
+ password: passwordEnabled ? password : ''
210
+ }
211
+ }
212
+
195
213
  /**
196
214
  * createPermissions - Create the permissions of a file
197
215
  * @param {object} options
@@ -257,15 +275,21 @@ export const updatePermissions = async ({
257
275
  showAlert
258
276
  }) => {
259
277
  try {
260
- const expiresAt = dateToggle
261
- ? toExpirationDate(selectedDate)?.toISOString() || undefined
262
- : ''
263
- const ensurePassword = passwordToggle ? password || undefined : ''
264
- const verbs = editingRights === 'readOnly' ? READ_ONLY_PERMS : WRITE_PERMS
278
+ const {
279
+ verbs,
280
+ expiresAt,
281
+ password: ensuredPassword
282
+ } = getLinkAccessOptions({
283
+ editingRights,
284
+ dateEnabled: dateToggle,
285
+ selectedDate,
286
+ passwordEnabled: passwordToggle,
287
+ password
288
+ })
265
289
  return updateDocumentPermissions(file, {
266
290
  verbs,
267
291
  expiresAt,
268
- password: ensurePassword
292
+ password: ensuredPassword
269
293
  })
270
294
  } catch (err) {
271
295
  showAlert({
@@ -124,7 +124,7 @@ describe('ShareRestrictionModal/helpers', () => {
124
124
  })
125
125
  })
126
126
 
127
- it('should call updateDocumentPermissions with "undefined" date & password if their switches are true but the permission doesn\'t yet have these values', async () => {
127
+ it('should call updateDocumentPermissions with empty date & password if enabled without values', async () => {
128
128
  const updateDocumentPermissions = jest.fn()
129
129
  const file = { _id: '123' }
130
130
 
@@ -142,8 +142,8 @@ describe('ShareRestrictionModal/helpers', () => {
142
142
  })
143
143
 
144
144
  expect(updateDocumentPermissions).toHaveBeenCalledWith(file, {
145
- expiresAt: undefined,
146
- password: undefined,
145
+ expiresAt: '',
146
+ password: '',
147
147
  verbs: ['GET']
148
148
  })
149
149
  })
@@ -1,7 +1,8 @@
1
- import { Icon, Share } from '@linagora/twake-icons'
2
1
  import classNames from 'classnames'
3
2
  import React from 'react'
4
3
 
4
+ import { Icon, Share } from '@linagora/twake-icons'
5
+
5
6
  import styles from '../styles/badge.styl'
6
7
 
7
8
  const SharedBadge = ({ byMe, className, small, xsmall }) => (
@@ -1,6 +1,6 @@
1
- import { Icon } from '@linagora/twake-icons'
2
1
  import React from 'react'
3
2
 
3
+ import { Icon } from '@linagora/twake-icons'
4
4
  import Typography from 'cozy-ui/transpiled/react/Typography'
5
5
 
6
6
  import SharedDriveIcon from '../../assets/icons/shared-drive.svg'
@@ -1,5 +1,4 @@
1
1
  import { CloudPlusOutlined, Sync, ToTheCloud } from '@linagora/twake-icons'
2
-
3
2
  import { Q } from 'cozy-client'
4
3
 
5
4
  import { fetchFilesPaths } from './files'
package/src/index.jsx CHANGED
@@ -25,6 +25,7 @@ export { SharedDriveModal } from './components/SharedDrive/SharedDriveModal'
25
25
  export { SharedDriveForm } from './components/SharedDrive/SharedDriveForm'
26
26
  export { FederatedFolderModal } from './components/FederatedFolder/FederatedFolderModal'
27
27
  export { ShareModal } from './components/ShareModal/ShareModal'
28
+ export { ShareLinkAccessModal } from './components/ShareLinkAccessModal'
28
29
  export { RefreshableSharings } from './RefreshableSharings'
29
30
  export { CozyPassFingerprintDialogContent } from './components/CozyPassFingerprintDialogContent'
30
31
  export { SharingBannerPlugin } from './components/SharingBanner'