cozy-sharing 28.4.4 → 28.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.
@@ -3,6 +3,7 @@ import React, { Component } from 'react'
3
3
  import { withClient } from 'cozy-client'
4
4
  import minilog from 'cozy-minilog'
5
5
 
6
+ import { generateShareLinkFromFile } from './components/ShareRestrictionModal/helpers'
6
7
  import SharingContext from './context'
7
8
  import { fetchNextPermissions } from './fetchNextPermissions'
8
9
  import { fetchFilesPaths } from './helpers/files'
@@ -11,6 +12,7 @@ import {
11
12
  createSharingInStore,
12
13
  updateSharingInStore
13
14
  } from './helpers/sharings'
15
+ import { getShortcode } from './helpers/shortcodes'
14
16
  import { SynchronousJobQueue } from './helpers/synchronousJobQueue'
15
17
  import { fetchApps } from './queries/queries'
16
18
  import reducer, {
@@ -67,6 +69,7 @@ export class SharingProvider extends Component {
67
69
  canReshare: docId => canReshare(this.state, docId, instanceUri),
68
70
  getOwner: docId => getOwner(this.state, docId),
69
71
  getSharingType: docId => getSharingType(this.state, docId, instanceUri),
72
+ getSharingById: docId => getSharingById(this.state, docId),
70
73
  getSharingForSelf: docId => getSharingForSelf(this.state, docId),
71
74
  getRecipients: docId => getRecipients(this.state, docId),
72
75
  getSharedParentPath: documentPath =>
@@ -83,6 +86,7 @@ export class SharingProvider extends Component {
83
86
  revokeGroup: this.revokeGroup,
84
87
  revokeSelf: this.revokeSelf,
85
88
  shareByLink: this.shareByLink,
89
+ getFederatedShareLink: this.getFederatedShareLink,
86
90
  updateDocumentPermissions: this.updateDocumentPermissions,
87
91
  revokeSharingLink: this.revokeSharingLink,
88
92
  hasLoadedAtLeastOnePage: false,
@@ -336,11 +340,66 @@ export class SharingProvider extends Component {
336
340
  }
337
341
 
338
342
  shareByLink = async (document, options) => {
339
- const resp = await this.permissionCol.createSharingLink(document, options)
343
+ const resp = document.driveId
344
+ ? await this.createSharedDriveSharingLink(document, options)
345
+ : await this.permissionCol.createSharingLink(document, options)
340
346
  this.dispatch(addSharingLink(resp.data))
341
347
  return resp
342
348
  }
343
349
 
350
+ getFederatedShareLink = document => {
351
+ if (!document.driveId) return null
352
+
353
+ const permissions = getDocumentPermissions(this.state, document._id)
354
+ const perm = permissions.find(p => getShortcode(p))
355
+ if (!perm) return null
356
+
357
+ const code = getShortcode(perm)
358
+ if (!code) return null
359
+
360
+ const { client } = this.props
361
+ return generateShareLinkFromFile({
362
+ client,
363
+ file: document,
364
+ sharecode: code,
365
+ getOwner: this.state.getOwner,
366
+ getSharingById: this.state.getSharingById,
367
+ documentType: this.state.documentType
368
+ })
369
+ }
370
+
371
+ createSharedDriveSharingLink = async (document, options) => {
372
+ const { client } = this.props
373
+ const drivePermissionCollection = client.collection('io.cozy.permissions', {
374
+ driveId: document.driveId
375
+ })
376
+ const resp = await drivePermissionCollection.createSharingLink(
377
+ document,
378
+ options
379
+ )
380
+ return resp
381
+ }
382
+
383
+ updateSharedDrivePermissions = async (
384
+ document,
385
+ permissionDocument,
386
+ updatedPermissions,
387
+ options
388
+ ) => {
389
+ const { client } = this.props
390
+ const { expiresAt, password } = options
391
+ const drivePermissionCollection = client.collection('io.cozy.permissions', {
392
+ driveId: document.driveId
393
+ })
394
+ const resp = await drivePermissionCollection.add(
395
+ permissionDocument,
396
+ updatedPermissions,
397
+ { expiresAt, password }
398
+ )
399
+ this.dispatch(updateSharingLink(resp.data))
400
+ return resp
401
+ }
402
+
344
403
  /**
345
404
  * updateDocumentPermissions - Description
346
405
  *
@@ -356,19 +415,29 @@ export class SharingProvider extends Component {
356
415
  const { verbs, expiresAt, password } = options
357
416
  const permissions = getDocumentPermissions(this.state, document.id)
358
417
 
418
+ if (!permissions || permissions.length === 0) {
419
+ return []
420
+ }
421
+
359
422
  const responses = await Promise.all(
360
423
  permissions.map(async permissionDocument => {
361
424
  const updatedPermissions = permissionDocument.attributes.permissions
362
- Object.keys(updatedPermissions).map(permType => {
425
+ Object.keys(updatedPermissions).forEach(permType => {
363
426
  updatedPermissions[permType].verbs = verbs
364
427
  })
365
428
 
366
- const resp = await this.permissionCol.add(
367
- permissionDocument,
368
- updatedPermissions,
369
- { expiresAt, password }
370
- )
371
- this.dispatch(updateSharingLink(resp.data))
429
+ const resp = document.driveId
430
+ ? await this.updateSharedDrivePermissions(
431
+ document,
432
+ permissionDocument,
433
+ updatedPermissions,
434
+ options
435
+ )
436
+ : await this.permissionCol.add(
437
+ permissionDocument,
438
+ updatedPermissions,
439
+ { expiresAt, password }
440
+ )
372
441
  return resp
373
442
  })
374
443
  )
@@ -5,7 +5,7 @@ import { createMockClient } from 'cozy-client'
5
5
 
6
6
  import { SharingProvider } from './SharingProvider'
7
7
  import SharingContext from './context'
8
- // import { receiveSharings } from './state'
8
+ import reducer, { addSharingLink, getDocumentPermissions } from './state'
9
9
  import AppLike from '../test/AppLike'
10
10
 
11
11
  const AppWrapper = ({ children, client, isPublic }) => {
@@ -105,6 +105,135 @@ describe('SharingProvider', () => {
105
105
  })
106
106
  })
107
107
 
108
+ describe('shareByLink', () => {
109
+ const PERM_DRIVE_FILE = {
110
+ type: 'io.cozy.permissions',
111
+ id: 'perm_drive_file',
112
+ attributes: {
113
+ type: 'share',
114
+ permissions: {
115
+ rule0: {
116
+ type: 'io.cozy.files',
117
+ verbs: ['GET'],
118
+ values: ['file_in_drive']
119
+ }
120
+ },
121
+ shortcodes: { code: 'shortcode123' }
122
+ }
123
+ }
124
+
125
+ const driveFile = {
126
+ _id: 'file_in_drive',
127
+ id: 'file_in_drive',
128
+ driveId: 'drive_123'
129
+ }
130
+
131
+ it('dispatches addSharingLink exactly once for a shared drive file', async () => {
132
+ const mockCreateSharingLink = jest
133
+ .fn()
134
+ .mockResolvedValue({ data: PERM_DRIVE_FILE })
135
+ const mockClient = createMockClient({})
136
+ mockClient.collection = jest.fn().mockReturnValue({
137
+ createSharingLink: mockCreateSharingLink
138
+ })
139
+
140
+ const provider = new SharingProvider({ client: mockClient })
141
+ provider.state = reducer()
142
+ provider.dispatch = jest.fn(action => {
143
+ provider.state = reducer(provider.state, action)
144
+ })
145
+
146
+ await provider.shareByLink(driveFile, { verbs: ['GET'] })
147
+
148
+ expect(provider.dispatch).toHaveBeenCalledTimes(1)
149
+ const permissions = getDocumentPermissions(provider.state, driveFile._id)
150
+ expect(permissions).toHaveLength(1)
151
+ expect(permissions[0].id).toBe(PERM_DRIVE_FILE.id)
152
+ })
153
+
154
+ it('dispatches addSharingLink once for a regular file (non shared drive)', async () => {
155
+ const PERM_REGULAR_FILE = {
156
+ type: 'io.cozy.permissions',
157
+ id: 'perm_regular_file',
158
+ attributes: {
159
+ type: 'share',
160
+ permissions: {
161
+ rule0: {
162
+ type: 'io.cozy.files',
163
+ verbs: ['GET'],
164
+ values: ['regular_file_id']
165
+ }
166
+ },
167
+ shortcodes: { code: 'shortcode456' }
168
+ }
169
+ }
170
+ const regularFile = { _id: 'regular_file_id', id: 'regular_file_id' }
171
+ const mockCreateSharingLink = jest
172
+ .fn()
173
+ .mockResolvedValue({ data: PERM_REGULAR_FILE })
174
+ const mockClient = createMockClient({})
175
+ mockClient.collection = jest.fn().mockReturnValue({
176
+ createSharingLink: mockCreateSharingLink
177
+ })
178
+
179
+ const provider = new SharingProvider({ client: mockClient })
180
+ provider.state = reducer()
181
+ provider.permissionCol = { createSharingLink: mockCreateSharingLink }
182
+ provider.dispatch = jest.fn(action => {
183
+ provider.state = reducer(provider.state, action)
184
+ })
185
+
186
+ await provider.shareByLink(regularFile, { verbs: ['GET'] })
187
+
188
+ expect(provider.dispatch).toHaveBeenCalledTimes(1)
189
+ const permissions = getDocumentPermissions(provider.state, regularFile._id)
190
+ expect(permissions).toHaveLength(1)
191
+ expect(permissions[0].id).toBe(PERM_REGULAR_FILE.id)
192
+ })
193
+ })
194
+
195
+ describe('updateDocumentPermissions', () => {
196
+ const PERM_DRIVE_FILE = {
197
+ type: 'io.cozy.permissions',
198
+ id: 'perm_drive_file',
199
+ attributes: {
200
+ type: 'share',
201
+ permissions: {
202
+ rule0: {
203
+ type: 'io.cozy.files',
204
+ verbs: ['GET'],
205
+ values: ['file_in_drive']
206
+ }
207
+ },
208
+ shortcodes: { code: 'shortcode123' }
209
+ }
210
+ }
211
+
212
+ it('sends only one PATCH request for a shared drive file', async () => {
213
+ const driveFile = {
214
+ _id: 'file_in_drive',
215
+ id: 'file_in_drive',
216
+ driveId: 'drive_123'
217
+ }
218
+ const mockAdd = jest.fn().mockResolvedValue({ data: PERM_DRIVE_FILE })
219
+ const mockClient = createMockClient({})
220
+ mockClient.collection = jest.fn().mockReturnValue({ add: mockAdd })
221
+
222
+ const provider = new SharingProvider({ client: mockClient })
223
+ // Seed the state with exactly one permission (as it would be after a correct shareByLink)
224
+ provider.state = reducer(undefined, addSharingLink(PERM_DRIVE_FILE))
225
+ provider.dispatch = jest.fn()
226
+
227
+ await provider.updateDocumentPermissions(driveFile, {
228
+ verbs: ['GET'],
229
+ expiresAt: '',
230
+ password: ''
231
+ })
232
+
233
+ expect(mockAdd).toHaveBeenCalledTimes(1)
234
+ })
235
+ })
236
+
108
237
  // TODO Convert with react-testing-library
109
238
  // describe('hasWriteAccess', () => {
110
239
  // it('tells if a doc is writtable', () => {
@@ -3,7 +3,7 @@ import React from 'react'
3
3
  import { useI18n } from 'twake-i18n'
4
4
 
5
5
  import withLocales from '../../hoc/withLocales'
6
- import DumbBatchSharedFolderModal from '../SharedFolder/DumbBatchSharedFolderModal'
6
+ import { DumbBatchSharedFolderModal } from '../SharedFolder/DumbBatchSharedFolderModal'
7
7
 
8
8
  export const DumbFederatedFolderModal = withLocales(
9
9
  ({
@@ -18,7 +18,8 @@ export const DumbFederatedFolderModal = withLocales(
18
18
  onSend,
19
19
  onClose,
20
20
  onShare,
21
- sharingLink
21
+ sharingLink,
22
+ showShareByEmail
22
23
  }) => {
23
24
  const { t } = useI18n()
24
25
  return (
@@ -39,6 +40,7 @@ export const DumbFederatedFolderModal = withLocales(
39
40
  addPeopleLabel={t('FederatedFolder.addPeople')}
40
41
  addButtonLabel={t('FederatedFolder.add')}
41
42
  shareLabel={t('FederatedFolder.share')}
43
+ showShareByEmail={showShareByEmail}
42
44
  />
43
45
  )
44
46
  }
@@ -56,7 +58,8 @@ DumbFederatedFolderModal.propTypes = {
56
58
  onSend: PropTypes.func.isRequired,
57
59
  onClose: PropTypes.func.isRequired,
58
60
  onShare: PropTypes.func.isRequired,
59
- sharingLink: PropTypes.string
61
+ sharingLink: PropTypes.string,
62
+ showShareByEmail: PropTypes.bool
60
63
  }
61
64
 
62
65
  export default DumbFederatedFolderModal
@@ -1,5 +1,5 @@
1
1
  import PropTypes from 'prop-types'
2
- import React, { useState } from 'react'
2
+ import React, { useState, useEffect } from 'react'
3
3
  import { useI18n } from 'twake-i18n'
4
4
 
5
5
  import { useClient } from 'cozy-client'
@@ -19,13 +19,43 @@ export const FederatedFolderModal = withLocales(
19
19
  ({ onClose, document: existingDocument }) => {
20
20
  const client = useClient()
21
21
  const { t } = useI18n()
22
- const { share, getSharingLink } = useSharingContext()
22
+ const {
23
+ share,
24
+ getSharingLink,
25
+ getFederatedShareLink,
26
+ getDocumentPermissions,
27
+ getOwner,
28
+ getSharingById
29
+ } = useSharingContext()
23
30
  const { showAlert } = useAlert()
24
31
 
25
- // Get sharing link
26
- const sharingLink = existingDocument
27
- ? getSharingLink(existingDocument._id)
28
- : null
32
+ const [sharingLink, setSharingLink] = useState(null)
33
+
34
+ const documentPermissions = existingDocument
35
+ ? getDocumentPermissions(existingDocument._id)
36
+ : []
37
+
38
+ useEffect(() => {
39
+ const fetchSharingLink = async () => {
40
+ if (!existingDocument) return
41
+
42
+ if (existingDocument.driveId) {
43
+ const link = await getFederatedShareLink(existingDocument)
44
+ setSharingLink(link)
45
+ } else {
46
+ setSharingLink(getSharingLink(existingDocument._id))
47
+ }
48
+ }
49
+
50
+ fetchSharingLink()
51
+ }, [
52
+ existingDocument,
53
+ documentPermissions,
54
+ getFederatedShareLink,
55
+ getSharingLink,
56
+ getOwner,
57
+ getSharingById
58
+ ]) // eslint-disable-line react-hooks/exhaustive-deps
29
59
 
30
60
  const [federatedRecipients, setFederatedRecipients] = useState({
31
61
  recipients: [],
@@ -92,6 +122,8 @@ export const FederatedFolderModal = withLocales(
92
122
 
93
123
  const modalTitle = t('FederatedFolder.shareTitle', { name: folderName })
94
124
 
125
+ const isSharedDrive = Boolean(existingDocument?.driveId)
126
+
95
127
  return (
96
128
  <DumbFederatedFolderModal
97
129
  title={modalTitle}
@@ -106,6 +138,7 @@ export const FederatedFolderModal = withLocales(
106
138
  onClose={onClose}
107
139
  onShare={onShare}
108
140
  sharingLink={sharingLink}
141
+ showShareByEmail={!isSharedDrive}
109
142
  />
110
143
  )
111
144
  }
@@ -23,7 +23,7 @@ const ShareByLink = ({ link, document, documentType }) => {
23
23
  const { isMobile } = useBreakpoints()
24
24
  const { showAlert } = useAlert()
25
25
  const client = useClient()
26
- const { shareByLink } = useSharingContext()
26
+ const { shareByLink, getOwner, getSharingById } = useSharingContext()
27
27
  const [isGenerating, setIsGenerating] = useState(false)
28
28
 
29
29
  const canShare = typeof navigator?.share === 'function'
@@ -44,6 +44,8 @@ const ShareByLink = ({ link, document, documentType }) => {
44
44
  file: document,
45
45
  documentType,
46
46
  shareByLink,
47
+ getOwner,
48
+ getSharingById,
47
49
  t,
48
50
  showAlert,
49
51
  password: '',
@@ -2,6 +2,7 @@ import { generateWebLink } from 'cozy-client'
2
2
  import minilog from 'cozy-minilog'
3
3
 
4
4
  import { getAppSlugFromDocumentType } from '../../helpers/link'
5
+ import { getShortcode } from '../../helpers/shortcodes'
5
6
 
6
7
  const log = minilog('ShareRestrictionModal/helpers')
7
8
 
@@ -28,6 +29,8 @@ export const createSharingLink = async ({
28
29
  file,
29
30
  documentType,
30
31
  shareByLink,
32
+ getOwner,
33
+ getSharingById,
31
34
  t,
32
35
  ttl,
33
36
  editingRights = 'readOnly',
@@ -44,13 +47,48 @@ export const createSharingLink = async ({
44
47
  shareByLink,
45
48
  showAlert
46
49
  })
47
- if (!permsResult?.data?.attributes?.shortcodes?.code) {
50
+ const code = getShortcode(permsResult?.data)
51
+ if (!code) {
48
52
  return null
49
53
  }
50
- const { data: perms } = permsResult
54
+ return generateShareLinkFromFile({
55
+ client,
56
+ file,
57
+ sharecode: code,
58
+ getOwner,
59
+ getSharingById,
60
+ documentType
61
+ })
62
+ }
63
+
64
+ /**
65
+ * Generate a sharing link from a file and sharecode
66
+ * @param {object} options
67
+ * @param {object} options.client - CozyClient instance
68
+ * @param {object} options.file - File to share
69
+ * @param {string} options.sharecode - Share code
70
+ * @param {Function} options.getOwner - Function to get the owner of a sharing
71
+ * @param {Function} options.getSharingById - Function to get a sharing by ID
72
+ * @param {string} options.documentType - Type of the document
73
+ * @returns {string} - The generated web link
74
+ */
75
+ export const generateShareLinkFromFile = ({
76
+ client,
77
+ file,
78
+ sharecode,
79
+ getOwner,
80
+ getSharingById,
81
+ documentType
82
+ }) => {
83
+ let cozyUrl = client.getStackClient().uri
84
+ if (file.driveId) {
85
+ const sharing = getSharingById(file.driveId)
86
+ const owner = getOwner(sharing?.rules?.[0]?.values?.[0])
87
+ cozyUrl = owner?.instance || client.getStackClient().uri
88
+ }
51
89
  return generateWebLink({
52
- cozyUrl: client.getStackClient().uri,
53
- searchParams: [['sharecode', perms.attributes.shortcodes.code]],
90
+ cozyUrl,
91
+ searchParams: [['sharecode', sharecode]],
54
92
  pathname: '/public',
55
93
  slug: getAppSlugFromDocumentType({ documentType }),
56
94
  subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested'
@@ -3,7 +3,7 @@ import React from 'react'
3
3
  import { useI18n } from 'twake-i18n'
4
4
 
5
5
  import withLocales from '../../hoc/withLocales'
6
- import DumbBatchSharedFolderModal from '../SharedFolder/DumbBatchSharedFolderModal'
6
+ import { DumbBatchSharedFolderModal } from '../SharedFolder/DumbBatchSharedFolderModal'
7
7
 
8
8
  export const DumbSharedDriveModal = withLocales(
9
9
  ({
@@ -37,7 +37,8 @@ export const DumbBatchSharedFolderModal = withLocales(
37
37
  createLabel,
38
38
  shareLabel,
39
39
  saveLabel,
40
- sharingDesc
40
+ sharingDesc,
41
+ showShareByEmail = true
41
42
  }) => {
42
43
  const hasRecipients = Boolean(recipients?.length)
43
44
 
@@ -52,6 +53,16 @@ export const DumbBatchSharedFolderModal = withLocales(
52
53
  )
53
54
  }
54
55
 
56
+ if (!showShareByEmail) {
57
+ return (
58
+ <ShareByLink
59
+ link={sharingLink}
60
+ document={document}
61
+ documentType="Files"
62
+ />
63
+ )
64
+ }
65
+
55
66
  if (!showNameField) {
56
67
  return (
57
68
  <>
@@ -111,19 +122,21 @@ export const DumbBatchSharedFolderModal = withLocales(
111
122
  <Typography variant="h6" className="u-mt-1-half u-mb-half">
112
123
  {addPeopleLabel}
113
124
  </Typography>
114
- <DumbShareByEmail
115
- createContact={createContact}
116
- currentRecipients={currentRecipients}
117
- document={document}
118
- documentType="Files"
119
- sharedDrive
120
- sharingDesc={sharingDesc}
121
- onShare={params => {
122
- onShare(params)
123
- }}
124
- submitLabel={addButtonLabel}
125
- showNotifications={false}
126
- />
125
+ {showShareByEmail && (
126
+ <DumbShareByEmail
127
+ createContact={createContact}
128
+ currentRecipients={currentRecipients}
129
+ document={document}
130
+ documentType="Files"
131
+ sharedDrive
132
+ sharingDesc={sharingDesc}
133
+ onShare={params => {
134
+ onShare(params)
135
+ }}
136
+ submitLabel={addButtonLabel}
137
+ showNotifications={false}
138
+ />
139
+ )}
127
140
  </div>
128
141
  <WhoHasAccess
129
142
  isOwner
@@ -168,7 +181,10 @@ DumbBatchSharedFolderModal.propTypes = {
168
181
  createLabel: PropTypes.string,
169
182
  shareLabel: PropTypes.string,
170
183
  saveLabel: PropTypes.string,
171
- sharingDesc: PropTypes.string.isRequired
184
+ sharingDesc: PropTypes.string,
185
+ showShareByEmail: PropTypes.bool
172
186
  }
173
187
 
174
- export default DumbBatchSharedFolderModal
188
+ DumbBatchSharedFolderModal.defaultProps = {
189
+ sharingDesc: ''
190
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Get the shortcode from a permission object
3
+ * The shortcode can be in different places depending on how the sharing was created:
4
+ * - attributes.shortcodes.email
5
+ * - attributes.shortcodes.code
6
+ * - attributes.codes.email
7
+ * - attributes.codes.code
8
+ *
9
+ * We used to use `email` as `codes` attribute for a sharingByLink.
10
+ * But when we use cozy-client to create a Permission, by default
11
+ * the codes attribute is set to `code`. MesPapiers app is using this
12
+ * default behavior... So the sharing by link created by mes papiers
13
+ * didn't appear correctly in cozy-sharing.
14
+ * This is a bit ugly, we should have a better way to know if this is
15
+ * a sharing by link or not.
16
+ *
17
+ * @param {object} permission - The permission object
18
+ * @returns {string|null} The shortcode or null if not found
19
+ */
20
+ export const getShortcode = permission => {
21
+ return (
22
+ permission?.attributes?.shortcodes?.email ||
23
+ permission?.attributes?.shortcodes?.code ||
24
+ permission?.attributes?.codes?.email ||
25
+ permission?.attributes?.codes?.code ||
26
+ null
27
+ )
28
+ }
package/src/state.js CHANGED
@@ -1,7 +1,7 @@
1
- import get from 'lodash/get'
2
-
3
1
  import flag from 'cozy-flags'
4
2
 
3
+ import { getShortcode } from './helpers/shortcodes'
4
+
5
5
  const RECEIVE_SHARINGS = 'RECEIVE_SHARINGS'
6
6
  const ADD_SHARING = 'ADD_SHARING'
7
7
  const UPDATE_SHARING = 'UPDATE_SHARING'
@@ -423,18 +423,7 @@ export const getSharingLink = (state, docId, documentType) => {
423
423
  const perms = getDocumentPermissions(state, docId)
424
424
  if (perms.length === 0) return null
425
425
  const perm = perms[0]
426
- // We used to use `email` as `codes` attribute for a sharingByLink.
427
- // But when we use cozy-client to create a Permission, by default
428
- // the codes attribute is set to `code`. MesPapiers app is using this
429
- // default behavior... So the sharing by link created by mes papiers
430
- // didn't appear correctly in cozy-sharing.
431
- // This is a bit ugly, we should have a better way to know if this is
432
- // a sharing by link or not.
433
- const code =
434
- get(perm, 'attributes.shortcodes.email') ||
435
- get(perm, 'attributes.shortcodes.code') ||
436
- get(perm, 'attributes.codes.email') ||
437
- get(perm, 'attributes.codes.code')
426
+ const code = getShortcode(perm)
438
427
  if (code) {
439
428
  return buildSharingLink(state, documentType, code)
440
429
  } else {