cozy-sharing 40.1.9 → 40.2.1
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 +14 -0
- package/README.md +1 -0
- package/dist/SharingOwnerAvatar.js +2 -2
- package/dist/SharingProvider.js +3 -1
- package/dist/components/Avatar/AvatarList.js +2 -2
- package/dist/components/Avatar/MemberAvatar.js +8 -33
- package/dist/components/Avatar/MemberAvatar.spec.js +17 -77
- package/dist/components/Avatar/MemberAvatarWrapper.js +20 -0
- package/dist/components/ConfirmTrustedRecipientsDialog.js +5 -1
- package/dist/components/ContactSuggestion.js +5 -2
- package/dist/components/Recipient/MemberRecipient.js +6 -3
- package/dist/components/Recipient/MemberRecipient.spec.js +1 -9
- package/dist/components/Recipient/MemberRecipientLite.js +2 -2
- package/dist/components/Recipient/PermissionTypeMenuWrapper.spec.js +3 -1
- package/dist/components/ShareAutosuggest.js +93 -16
- package/dist/components/ShareAutosuggest.spec.js +126 -3
- package/dist/components/ShareAutosuggest.stories.js +3 -0
- package/dist/components/ShareAutosuggestWrapper.js +65 -5
- package/dist/components/ShareAutosuggestWrapper.spec.js +113 -11
- package/dist/components/WhoHasAccessWrapper.js +6 -2
- package/dist/helpers/contacts.js +75 -16
- package/dist/helpers/contacts.spec.js +150 -1
- package/dist/helpers/recipients.js +38 -0
- package/dist/helpers/recipients.spec.js +53 -1
- package/dist/hooks/useResolveRecipients.js +6 -2
- package/dist/hooks/useResolveRecipients.spec.js +11 -2
- package/dist/index.js +1 -1
- package/dist/queries/queries.js +16 -3
- package/package.json +3 -3
- package/src/SharingOwnerAvatar.jsx +2 -2
- package/src/SharingProvider.jsx +6 -1
- package/src/components/Avatar/AvatarList.jsx +2 -2
- package/src/components/Avatar/MemberAvatar.jsx +4 -34
- package/src/components/Avatar/MemberAvatar.spec.jsx +13 -73
- package/src/components/Avatar/MemberAvatarWrapper.jsx +22 -0
- package/src/components/ConfirmTrustedRecipientsDialog.jsx +7 -0
- package/src/components/ContactSuggestion.jsx +8 -3
- package/src/components/Recipient/MemberRecipient.jsx +4 -2
- package/src/components/Recipient/MemberRecipient.spec.jsx +1 -8
- package/src/components/Recipient/MemberRecipientLite.jsx +2 -2
- package/src/components/Recipient/PermissionTypeMenuWrapper.spec.jsx +3 -1
- package/src/components/ShareAutosuggest.jsx +56 -9
- package/src/components/ShareAutosuggest.spec.jsx +60 -2
- package/src/components/ShareAutosuggest.stories.jsx +1 -0
- package/src/components/ShareAutosuggestWrapper.jsx +36 -4
- package/src/components/ShareAutosuggestWrapper.spec.jsx +78 -5
- package/src/components/WhoHasAccessWrapper.jsx +14 -2
- package/src/helpers/contacts.js +31 -1
- package/src/helpers/contacts.spec.js +102 -1
- package/src/helpers/recipients.js +40 -0
- package/src/helpers/recipients.spec.js +83 -1
- package/src/hooks/useResolveRecipients.js +5 -2
- package/src/hooks/useResolveRecipients.spec.js +8 -2
- package/src/index.jsx +1 -1
- package/src/queries/queries.js +6 -3
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { useClient } from 'cozy-client'
|
|
4
|
+
|
|
5
|
+
import { MemberAvatar } from './MemberAvatar'
|
|
6
|
+
import { makeAvatarUrl } from '../../helpers/recipients'
|
|
7
|
+
|
|
8
|
+
const MemberAvatarWrapper = ({ recipient, ...avatarProps }) => {
|
|
9
|
+
const client = useClient()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<MemberAvatar
|
|
13
|
+
recipient={recipient}
|
|
14
|
+
avatarUrl={
|
|
15
|
+
recipient ? makeAvatarUrl(recipient, client.options.uri) : null
|
|
16
|
+
}
|
|
17
|
+
{...avatarProps}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { MemberAvatarWrapper }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import cx from 'classnames'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
+
import { useClient } from 'cozy-client'
|
|
4
5
|
import List from 'cozy-ui/transpiled/react/List'
|
|
5
6
|
import Typography from 'cozy-ui/transpiled/react/Typography'
|
|
6
7
|
import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'
|
|
@@ -8,6 +9,7 @@ import { useI18n } from 'twake-i18n'
|
|
|
8
9
|
|
|
9
10
|
import MemberRecipient from './Recipient/MemberRecipient'
|
|
10
11
|
import ShareDialogTwoStepsConfirmationContainer from './ShareDialogTwoStepsConfirmationContainer'
|
|
12
|
+
import { makeAvatarUrl } from '../helpers/recipients'
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Displays the sharing interface that can be used when ConfirmTrustedRecipientsDialog is in sharing state
|
|
@@ -15,6 +17,7 @@ import ShareDialogTwoStepsConfirmationContainer from './ShareDialogTwoStepsConfi
|
|
|
15
17
|
const SharingContent = ({ recipientsToBeConfirmed, verifyRecipient }) => {
|
|
16
18
|
const { t } = useI18n()
|
|
17
19
|
const { isMobile } = useBreakpoints()
|
|
20
|
+
const client = useClient()
|
|
18
21
|
|
|
19
22
|
return (
|
|
20
23
|
<>
|
|
@@ -36,6 +39,10 @@ const SharingContent = ({ recipientsToBeConfirmed, verifyRecipient }) => {
|
|
|
36
39
|
isOwner={false}
|
|
37
40
|
recipientConfirmationData={recipientConfirmationData}
|
|
38
41
|
verifyRecipient={verifyRecipient}
|
|
42
|
+
avatarUrl={makeAvatarUrl(
|
|
43
|
+
recipientConfirmationData,
|
|
44
|
+
client.options.uri
|
|
45
|
+
)}
|
|
39
46
|
/>
|
|
40
47
|
)
|
|
41
48
|
})}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { GroupAvatar } from './Avatar/GroupAvatar'
|
|
17
17
|
import { MemberAvatar } from './Avatar/MemberAvatar'
|
|
18
18
|
|
|
19
|
-
export const ContactSuggestion = ({ contactOrGroup }) => {
|
|
19
|
+
export const ContactSuggestion = ({ contactOrGroup, avatarUrl }) => {
|
|
20
20
|
const { t } = useI18n()
|
|
21
21
|
|
|
22
22
|
if (contactOrGroup._type === DOCTYPE_GROUPS) {
|
|
@@ -41,7 +41,11 @@ export const ContactSuggestion = ({ contactOrGroup }) => {
|
|
|
41
41
|
return (
|
|
42
42
|
<ListItem button>
|
|
43
43
|
<ListItemIcon>
|
|
44
|
-
<MemberAvatar
|
|
44
|
+
<MemberAvatar
|
|
45
|
+
recipient={contactOrGroup}
|
|
46
|
+
avatarUrl={avatarUrl}
|
|
47
|
+
size="m"
|
|
48
|
+
/>
|
|
45
49
|
</ListItemIcon>
|
|
46
50
|
<ListItemText primary={name} secondary={cozyUrl || '-'} />
|
|
47
51
|
</ListItem>
|
|
@@ -59,7 +63,8 @@ ContactSuggestion.propTypes = {
|
|
|
59
63
|
groupPropType,
|
|
60
64
|
newUnknownContactProptypes
|
|
61
65
|
]).isRequired,
|
|
62
|
-
contacts: PropTypes.arrayOf(contactPropType)
|
|
66
|
+
contacts: PropTypes.arrayOf(contactPropType),
|
|
67
|
+
avatarUrl: PropTypes.string
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
export default ContactSuggestion
|
|
@@ -32,6 +32,7 @@ const MemberRecipient = props => {
|
|
|
32
32
|
verifyRecipient,
|
|
33
33
|
fadeIn,
|
|
34
34
|
isOrgSharedDrive,
|
|
35
|
+
avatarUrl,
|
|
35
36
|
...rest
|
|
36
37
|
} = props
|
|
37
38
|
|
|
@@ -58,7 +59,7 @@ const MemberRecipient = props => {
|
|
|
58
59
|
<Fade in timeout={fadeIn ? FADE_IN_DURATION : 0}>
|
|
59
60
|
<ListItem gutters={isMobile ? 'default' : 'double'} size="small">
|
|
60
61
|
<ListItemIcon>
|
|
61
|
-
<MemberAvatar size="m" recipient={props} />
|
|
62
|
+
<MemberAvatar size="m" recipient={props} avatarUrl={avatarUrl} />
|
|
62
63
|
</ListItemIcon>
|
|
63
64
|
<ListItemText
|
|
64
65
|
primary={
|
|
@@ -90,7 +91,8 @@ MemberRecipient.propTypes = {
|
|
|
90
91
|
isCurrentUser: PropTypes.bool,
|
|
91
92
|
recipientConfirmationData: PropTypes.object,
|
|
92
93
|
verifyRecipient: PropTypes.func,
|
|
93
|
-
isOrgSharedDrive: PropTypes.bool
|
|
94
|
+
isOrgSharedDrive: PropTypes.bool,
|
|
95
|
+
avatarUrl: PropTypes.string
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export default MemberRecipient
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { render, fireEvent } from '@testing-library/react'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
-
import { createMockClient } from 'cozy-client'
|
|
5
|
-
|
|
6
4
|
import MemberRecipient from './MemberRecipient'
|
|
7
5
|
import AppLike from '../../../test/AppLike'
|
|
8
6
|
|
|
9
7
|
describe('MemberRecipient component', () => {
|
|
10
|
-
const client = createMockClient({})
|
|
11
|
-
client.options = {
|
|
12
|
-
uri: 'foo.mycozy.cloud'
|
|
13
|
-
}
|
|
14
|
-
|
|
15
8
|
const setup = props => {
|
|
16
9
|
return render(
|
|
17
|
-
<AppLike
|
|
10
|
+
<AppLike>
|
|
18
11
|
<MemberRecipient {...props} />
|
|
19
12
|
</AppLike>
|
|
20
13
|
)
|
|
@@ -12,7 +12,7 @@ import MemberRecipientStatus from './MemberRecipientStatus'
|
|
|
12
12
|
import { DEFAULT_DISPLAY_NAME } from '../../helpers/recipients'
|
|
13
13
|
import withLocales from '../../hoc/withLocales'
|
|
14
14
|
import { getDisplayName } from '../../models'
|
|
15
|
-
import {
|
|
15
|
+
import { MemberAvatarWrapper } from '../Avatar/MemberAvatarWrapper'
|
|
16
16
|
|
|
17
17
|
const MemberRecipientLite = ({ recipient, isOwner, ...props }) => {
|
|
18
18
|
const { t } = useI18n()
|
|
@@ -25,7 +25,7 @@ const MemberRecipientLite = ({ recipient, isOwner, ...props }) => {
|
|
|
25
25
|
return (
|
|
26
26
|
<ListItem size="small" ellipsis>
|
|
27
27
|
<ListItemIcon>
|
|
28
|
-
<
|
|
28
|
+
<MemberAvatarWrapper size="m" recipient={recipient} {...props} />
|
|
29
29
|
</ListItemIcon>
|
|
30
30
|
<ListItemText
|
|
31
31
|
primary={
|
|
@@ -160,7 +160,9 @@ describe('PermissionTypeMenu component', () => {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
beforeEach(() => {
|
|
163
|
-
flag.
|
|
163
|
+
flag.mockImplementation(
|
|
164
|
+
name => name === 'drive.federated-shared-folder.enabled'
|
|
165
|
+
)
|
|
164
166
|
mockHasSharedParent.mockReturnValue(true)
|
|
165
167
|
mockGetSharingById.mockReturnValue({
|
|
166
168
|
id: 'sharing-123',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PropTypes from 'prop-types'
|
|
2
|
-
import React, { useState, useRef } from 'react'
|
|
2
|
+
import React, { useEffect, useState, useRef } from 'react'
|
|
3
3
|
import Autosuggest from 'react-autosuggest'
|
|
4
4
|
|
|
5
5
|
import Chip from 'cozy-ui/transpiled/react/Chips'
|
|
@@ -10,10 +10,14 @@ import { MemberAvatar } from './Avatar/MemberAvatar'
|
|
|
10
10
|
import ContactSuggestion from './ContactSuggestion'
|
|
11
11
|
import { isContactToBeCreated } from '../helpers/contacts'
|
|
12
12
|
import { extractEmails, validateEmail } from '../helpers/email'
|
|
13
|
+
import logger from '../logger'
|
|
13
14
|
import { getDisplayName, DOCTYPE_CONTACTS, DOCTYPE_GROUPS } from '../models'
|
|
14
15
|
import styles from '../styles/autosuggest.styl'
|
|
15
16
|
import { contactOrGroupMatch } from '../suggestionMatchers'
|
|
16
17
|
|
|
18
|
+
const FETCH_CONTACTS_DEBOUNCE = 300
|
|
19
|
+
const FETCH_CONTACTS_MIN_LENGTH = 3
|
|
20
|
+
|
|
17
21
|
const ShareAutosuggest = ({
|
|
18
22
|
loading,
|
|
19
23
|
disabled,
|
|
@@ -24,15 +28,22 @@ const ShareAutosuggest = ({
|
|
|
24
28
|
enableCreateContact,
|
|
25
29
|
placeholder,
|
|
26
30
|
autoFocus,
|
|
27
|
-
endAdornment
|
|
31
|
+
endAdornment,
|
|
32
|
+
fetchContacts,
|
|
33
|
+
getAvatarUrl
|
|
28
34
|
}) => {
|
|
29
35
|
const [inputValue, setInputValue] = useState('')
|
|
30
36
|
const [suggestions, setSuggestions] = useState([])
|
|
31
37
|
const [isPasted, setIsPasted] = useState(false)
|
|
32
38
|
const [isLoadingDisplayed, setLoadingDisplayed] = useState(false)
|
|
39
|
+
const [isFetching, setIsFetching] = useState(false)
|
|
33
40
|
|
|
34
41
|
const autosuggestRef = useRef(null)
|
|
35
42
|
const containerRef = useRef(null)
|
|
43
|
+
const fetchTimeoutRef = useRef(null)
|
|
44
|
+
const fetchedValueRef = useRef(null)
|
|
45
|
+
|
|
46
|
+
useEffect(() => () => clearTimeout(fetchTimeoutRef.current), [])
|
|
36
47
|
|
|
37
48
|
const computeSuggestions = value => {
|
|
38
49
|
const inputValue = value.trim().toLowerCase()
|
|
@@ -44,9 +55,7 @@ const ShareAutosuggest = ({
|
|
|
44
55
|
)
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
const
|
|
48
|
-
const computedSuggestions = computeSuggestions(value)
|
|
49
|
-
|
|
58
|
+
const showSuggestions = (computedSuggestions, value) => {
|
|
50
59
|
if (computedSuggestions.length > 0) {
|
|
51
60
|
setSuggestions(computedSuggestions)
|
|
52
61
|
} else if (enableCreateContact && validateEmail(value)) {
|
|
@@ -61,7 +70,36 @@ const ShareAutosuggest = ({
|
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
const onSuggestionsFetchRequested = ({ value }) => {
|
|
74
|
+
const computedSuggestions = computeSuggestions(value)
|
|
75
|
+
clearTimeout(fetchTimeoutRef.current)
|
|
76
|
+
fetchedValueRef.current = value
|
|
77
|
+
|
|
78
|
+
if (!fetchContacts || value.trim().length < FETCH_CONTACTS_MIN_LENGTH) {
|
|
79
|
+
setIsFetching(false)
|
|
80
|
+
showSuggestions(computedSuggestions, value)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setIsFetching(true)
|
|
85
|
+
fetchTimeoutRef.current = setTimeout(async () => {
|
|
86
|
+
let contacts = []
|
|
87
|
+
try {
|
|
88
|
+
contacts = await fetchContacts(value.trim())
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logger.error('Failed to fetch contact suggestions', err)
|
|
91
|
+
}
|
|
92
|
+
// The user kept typing or left the input while the stack was answering
|
|
93
|
+
if (fetchedValueRef.current !== value) return
|
|
94
|
+
setIsFetching(false)
|
|
95
|
+
showSuggestions([...contacts, ...computedSuggestions], value)
|
|
96
|
+
}, FETCH_CONTACTS_DEBOUNCE)
|
|
97
|
+
}
|
|
98
|
+
|
|
64
99
|
const onSuggestionsClearRequested = () => {
|
|
100
|
+
clearTimeout(fetchTimeoutRef.current)
|
|
101
|
+
fetchedValueRef.current = null
|
|
102
|
+
setIsFetching(false)
|
|
65
103
|
setSuggestions([])
|
|
66
104
|
}
|
|
67
105
|
|
|
@@ -162,7 +200,11 @@ const ShareAutosuggest = ({
|
|
|
162
200
|
isContactGroup ? (
|
|
163
201
|
<GroupAvatar size="xs" color={recipient.color} />
|
|
164
202
|
) : (
|
|
165
|
-
<MemberAvatar
|
|
203
|
+
<MemberAvatar
|
|
204
|
+
size="xs"
|
|
205
|
+
recipient={recipient}
|
|
206
|
+
avatarUrl={getAvatarUrl(recipient)}
|
|
207
|
+
/>
|
|
166
208
|
)
|
|
167
209
|
}
|
|
168
210
|
label={name}
|
|
@@ -174,7 +216,7 @@ const ShareAutosuggest = ({
|
|
|
174
216
|
)
|
|
175
217
|
})}
|
|
176
218
|
<input {...inputProps} onKeyPress={onKeyPress} onKeyUp={onKeyUp} />
|
|
177
|
-
{loading && isLoadingDisplayed ? (
|
|
219
|
+
{(loading || isFetching) && isLoadingDisplayed ? (
|
|
178
220
|
<Spinner color="var(--primaryColor)" />
|
|
179
221
|
) : null}
|
|
180
222
|
{endAdornment ? (
|
|
@@ -184,7 +226,10 @@ const ShareAutosuggest = ({
|
|
|
184
226
|
)
|
|
185
227
|
|
|
186
228
|
const renderSuggestion = contactOrGroup => (
|
|
187
|
-
<ContactSuggestion
|
|
229
|
+
<ContactSuggestion
|
|
230
|
+
contactOrGroup={contactOrGroup}
|
|
231
|
+
avatarUrl={getAvatarUrl(contactOrGroup)}
|
|
232
|
+
/>
|
|
188
233
|
)
|
|
189
234
|
|
|
190
235
|
return (
|
|
@@ -231,7 +276,9 @@ ShareAutosuggest.propTypes = {
|
|
|
231
276
|
enableCreateContact: PropTypes.bool,
|
|
232
277
|
placeholder: PropTypes.string,
|
|
233
278
|
autoFocus: PropTypes.bool,
|
|
234
|
-
endAdornment: PropTypes.node
|
|
279
|
+
endAdornment: PropTypes.node,
|
|
280
|
+
fetchContacts: PropTypes.func,
|
|
281
|
+
getAvatarUrl: PropTypes.func.isRequired
|
|
235
282
|
}
|
|
236
283
|
|
|
237
284
|
export default ShareAutosuggest
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, fireEvent, screen } from '@testing-library/react'
|
|
1
|
+
import { act, render, fireEvent, screen } from '@testing-library/react'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
4
|
import ShareAutosuggest from './ShareAutosuggest'
|
|
@@ -13,7 +13,8 @@ describe('ShareAutosuggest', () => {
|
|
|
13
13
|
onPick,
|
|
14
14
|
onRemove,
|
|
15
15
|
loading = false,
|
|
16
|
-
enableCreateContact = true
|
|
16
|
+
enableCreateContact = true,
|
|
17
|
+
fetchContacts
|
|
17
18
|
}) => {
|
|
18
19
|
render(
|
|
19
20
|
<AppLike>
|
|
@@ -25,6 +26,8 @@ describe('ShareAutosuggest', () => {
|
|
|
25
26
|
recipients={[]}
|
|
26
27
|
onRemove={onRemove}
|
|
27
28
|
enableCreateContact={enableCreateContact}
|
|
29
|
+
fetchContacts={fetchContacts}
|
|
30
|
+
getAvatarUrl={() => null}
|
|
28
31
|
/>
|
|
29
32
|
</AppLike>
|
|
30
33
|
)
|
|
@@ -88,4 +91,59 @@ describe('ShareAutosuggest', () => {
|
|
|
88
91
|
|
|
89
92
|
expect(screen.queryByText('loading')).toBeNull()
|
|
90
93
|
})
|
|
94
|
+
|
|
95
|
+
describe('with fetchContacts', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
jest.useFakeTimers()
|
|
98
|
+
// Not implemented by jsdom, called when a suggestion is highlighted
|
|
99
|
+
Element.prototype.scrollIntoView = jest.fn()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
jest.useRealTimers()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should suggest the fetched contacts, debounced and from 3 characters', async () => {
|
|
107
|
+
const fetchContacts = jest.fn().mockResolvedValue([
|
|
108
|
+
{
|
|
109
|
+
_id: 'org-member-1',
|
|
110
|
+
_type: 'io.cozy.contacts',
|
|
111
|
+
displayName: 'Jean Dupont',
|
|
112
|
+
email: [{ address: 'jean@org.example', primary: true }]
|
|
113
|
+
}
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
setup({ onPick: jest.fn(), onRemove: jest.fn(), fetchContacts })
|
|
117
|
+
|
|
118
|
+
const inputNode = screen.getByPlaceholderText('myPlaceHolder')
|
|
119
|
+
fireEvent.change(inputNode, { target: { value: 'du' } })
|
|
120
|
+
act(() => jest.runAllTimers())
|
|
121
|
+
expect(fetchContacts).not.toHaveBeenCalled()
|
|
122
|
+
|
|
123
|
+
fireEvent.change(inputNode, { target: { value: 'dup' } })
|
|
124
|
+
fireEvent.change(inputNode, { target: { value: 'dupo' } })
|
|
125
|
+
act(() => jest.runAllTimers())
|
|
126
|
+
expect(fetchContacts).toHaveBeenCalledTimes(1)
|
|
127
|
+
expect(fetchContacts).toHaveBeenCalledWith('dupo')
|
|
128
|
+
|
|
129
|
+
expect(await screen.findByText('Jean Dupont')).toBeInTheDocument()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should show the loading indicator until the contacts are fetched', async () => {
|
|
133
|
+
let resolveFetch
|
|
134
|
+
const fetchContacts = jest.fn(
|
|
135
|
+
() => new Promise(resolve => (resolveFetch = resolve))
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
setup({ onPick: jest.fn(), onRemove: jest.fn(), fetchContacts })
|
|
139
|
+
|
|
140
|
+
const inputNode = screen.getByPlaceholderText('myPlaceHolder')
|
|
141
|
+
fireEvent.change(inputNode, { target: { value: 'dup' } })
|
|
142
|
+
expect(screen.getByText('loading')).toBeInTheDocument()
|
|
143
|
+
|
|
144
|
+
act(() => jest.runAllTimers())
|
|
145
|
+
await act(async () => resolveFetch([]))
|
|
146
|
+
expect(screen.queryByText('loading')).toBeNull()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
91
149
|
})
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import PropTypes from 'prop-types'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
-
import { useQueryAll, isQueryLoading } from 'cozy-client'
|
|
4
|
+
import { useClient, useQueryAll, isQueryLoading } from 'cozy-client'
|
|
5
|
+
import { models } from 'cozy-client'
|
|
5
6
|
import flag from 'cozy-flags'
|
|
6
7
|
|
|
7
8
|
import ShareAutosuggest from './ShareAutosuggest'
|
|
8
|
-
import { computeContactsAndGroups } from '../helpers/contacts'
|
|
9
|
+
import { computeContactsAndGroups, suggestContacts } from '../helpers/contacts'
|
|
10
|
+
import { makeAvatarUrl } from '../helpers/recipients'
|
|
11
|
+
import { useSharingContext } from '../hooks/useSharingContext'
|
|
12
|
+
import { DOCTYPE_GROUPS } from '../models'
|
|
9
13
|
import {
|
|
10
14
|
buildReachableContactsQuery,
|
|
11
15
|
buildContactGroupsQuery,
|
|
12
16
|
buildUnreachableContactsWithGroupsQuery
|
|
13
17
|
} from '../queries/queries'
|
|
14
18
|
|
|
19
|
+
const ContactModel = models.contact
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* ShareAutosuggestWrapper is responsible for fetching contacts and groups.
|
|
17
23
|
* We retrieve all the contacts that are reachable and the groups they belong to.
|
|
18
24
|
* In order to display the total number of members in each group, we also retrieve the contacts that are not reachable.
|
|
25
|
+
* With contactsSuggestEnabled, the stack suggests the contacts and we only retrieve the group members.
|
|
19
26
|
*/
|
|
20
27
|
const ShareAutosuggestWrapper = ({
|
|
21
28
|
currentRecipients,
|
|
@@ -28,7 +35,12 @@ const ShareAutosuggestWrapper = ({
|
|
|
28
35
|
autoFocus,
|
|
29
36
|
endAdornment
|
|
30
37
|
}) => {
|
|
31
|
-
const
|
|
38
|
+
const client = useClient()
|
|
39
|
+
const contactsSuggestEnabled = useSharingContext()?.contactsSuggestEnabled
|
|
40
|
+
|
|
41
|
+
const reachableContactsQuery = buildReachableContactsQuery({
|
|
42
|
+
inGroups: contactsSuggestEnabled
|
|
43
|
+
})
|
|
32
44
|
const reachableContactsResult = useQueryAll(
|
|
33
45
|
reachableContactsQuery.definition,
|
|
34
46
|
reachableContactsQuery.options
|
|
@@ -59,7 +71,7 @@ const ShareAutosuggestWrapper = ({
|
|
|
59
71
|
(isQueryLoading(unreachableContactsWithGroupsResult) ||
|
|
60
72
|
unreachableContactsWithGroupsResult.hasMore))
|
|
61
73
|
|
|
62
|
-
const
|
|
74
|
+
const suggestedContactsAndGroups = computeContactsAndGroups({
|
|
63
75
|
reachableContacts: reachableContactsResult.data || [],
|
|
64
76
|
unreachableContactsWithGroups:
|
|
65
77
|
unreachableContactsWithGroupsResult.data || [],
|
|
@@ -68,6 +80,24 @@ const ShareAutosuggestWrapper = ({
|
|
|
68
80
|
pendingRecipients: recipients
|
|
69
81
|
})
|
|
70
82
|
|
|
83
|
+
const contactsAndGroups = contactsSuggestEnabled
|
|
84
|
+
? suggestedContactsAndGroups.filter(({ _type }) => _type === DOCTYPE_GROUPS)
|
|
85
|
+
: suggestedContactsAndGroups
|
|
86
|
+
|
|
87
|
+
// The stack matches the emails whatever their case
|
|
88
|
+
const normalizeEmail = email => email?.trim().toLowerCase()
|
|
89
|
+
const isRecipient = email =>
|
|
90
|
+
currentRecipients.some(r => normalizeEmail(r.email) === email) ||
|
|
91
|
+
recipients.some(
|
|
92
|
+
r => normalizeEmail(ContactModel.getPrimaryEmail(r)) === email
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const fetchContacts = async q =>
|
|
96
|
+
(await suggestContacts(client, q)).filter(
|
|
97
|
+
contact =>
|
|
98
|
+
!isRecipient(normalizeEmail(ContactModel.getPrimaryEmail(contact)))
|
|
99
|
+
)
|
|
100
|
+
|
|
71
101
|
return (
|
|
72
102
|
<ShareAutosuggest
|
|
73
103
|
loading={isLoading}
|
|
@@ -80,6 +110,8 @@ const ShareAutosuggestWrapper = ({
|
|
|
80
110
|
placeholder={placeholder}
|
|
81
111
|
autoFocus={autoFocus}
|
|
82
112
|
endAdornment={endAdornment}
|
|
113
|
+
fetchContacts={contactsSuggestEnabled ? fetchContacts : undefined}
|
|
114
|
+
getAvatarUrl={recipient => makeAvatarUrl(recipient, client.options.uri)}
|
|
83
115
|
/>
|
|
84
116
|
)
|
|
85
117
|
}
|
|
@@ -12,9 +12,12 @@ jest.mock('cozy-flags', () => ({
|
|
|
12
12
|
default: jest.fn(() => null)
|
|
13
13
|
}))
|
|
14
14
|
|
|
15
|
+
let mockAutosuggestProps
|
|
15
16
|
jest.mock('./ShareAutosuggest', () => ({
|
|
16
17
|
__esModule: true,
|
|
17
|
-
default:
|
|
18
|
+
default: props => {
|
|
19
|
+
mockAutosuggestProps = props
|
|
20
|
+
const { contactsAndGroups } = props
|
|
18
21
|
return (
|
|
19
22
|
<ul>
|
|
20
23
|
{contactsAndGroups.map(contactOrGroup => {
|
|
@@ -39,8 +42,8 @@ jest.mock('./ShareAutosuggest', () => ({
|
|
|
39
42
|
|
|
40
43
|
describe('ShareAutosuggestWrapper component', () => {
|
|
41
44
|
const setup = (
|
|
42
|
-
{ contacts, contactGroups, unreachableContact },
|
|
43
|
-
{ currentRecipients, recipients } = {}
|
|
45
|
+
{ contacts, contactGroups, unreachableContact, groupMembers },
|
|
46
|
+
{ currentRecipients, recipients, contactsSuggestEnabled } = {}
|
|
44
47
|
) => {
|
|
45
48
|
const mockClient = createMockClient({
|
|
46
49
|
queries: {
|
|
@@ -55,11 +58,18 @@ describe('ShareAutosuggestWrapper component', () => {
|
|
|
55
58
|
'io.cozy.contacts/unreachable-with-groups': {
|
|
56
59
|
doctype: 'io.cozy.contacts',
|
|
57
60
|
data: unreachableContact
|
|
61
|
+
},
|
|
62
|
+
'io.cozy.contacts/reachable-with-groups': {
|
|
63
|
+
doctype: 'io.cozy.contacts',
|
|
64
|
+
data: groupMembers
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
67
|
})
|
|
61
68
|
render(
|
|
62
|
-
<AppLike
|
|
69
|
+
<AppLike
|
|
70
|
+
client={mockClient}
|
|
71
|
+
sharingContextValue={{ contactsSuggestEnabled }}
|
|
72
|
+
>
|
|
63
73
|
<ShareAutosuggestWrapper
|
|
64
74
|
onPick={() => {}}
|
|
65
75
|
onRemove={() => {}}
|
|
@@ -69,6 +79,7 @@ describe('ShareAutosuggestWrapper component', () => {
|
|
|
69
79
|
/>
|
|
70
80
|
</AppLike>
|
|
71
81
|
)
|
|
82
|
+
return mockClient
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
it('should include groups and contacts when are loaded', async () => {
|
|
@@ -219,7 +230,7 @@ describe('ShareAutosuggestWrapper component', () => {
|
|
|
219
230
|
})
|
|
220
231
|
|
|
221
232
|
it('should include unreachable members when flag sharing.show-recipient-groups is activated', async () => {
|
|
222
|
-
flag.
|
|
233
|
+
flag.mockImplementation(name => name === 'sharing.show-recipient-groups')
|
|
223
234
|
|
|
224
235
|
const contacts = [
|
|
225
236
|
{
|
|
@@ -294,4 +305,66 @@ describe('ShareAutosuggestWrapper component', () => {
|
|
|
294
305
|
screen.getByText("The Night's Watch - 3 members")
|
|
295
306
|
).toBeInTheDocument()
|
|
296
307
|
})
|
|
308
|
+
|
|
309
|
+
describe('with contactsSuggestEnabled', () => {
|
|
310
|
+
beforeEach(() => {
|
|
311
|
+
flag.mockReset()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should suggest only groups, with the count of their members', () => {
|
|
315
|
+
const groupMembers = [
|
|
316
|
+
{
|
|
317
|
+
id: '5a3b4ccf-c257',
|
|
318
|
+
_id: '5a3b4ccf-c257',
|
|
319
|
+
_type: 'io.cozy.contacts',
|
|
320
|
+
name: { givenName: 'Teagan', familyName: 'Wolf' },
|
|
321
|
+
relationships: {
|
|
322
|
+
groups: {
|
|
323
|
+
data: [{ _id: 'fe86af20-c6c5', _type: 'io.cozy.contacts.groups' }]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
const contactGroups = [
|
|
329
|
+
{
|
|
330
|
+
id: 'fe86af20-c6c5',
|
|
331
|
+
_id: 'fe86af20-c6c5',
|
|
332
|
+
_type: 'io.cozy.contacts.groups',
|
|
333
|
+
name: 'Friends'
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
setup({ contactGroups, groupMembers }, { contactsSuggestEnabled: true })
|
|
338
|
+
|
|
339
|
+
expect(screen.getByText('Friends - 1 members')).toBeInTheDocument()
|
|
340
|
+
expect(screen.queryByText('Teagan Wolf')).not.toBeInTheDocument()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should fetch suggestions without the current and pending recipients, whatever the case of their email', async () => {
|
|
344
|
+
const suggestion = email => ({
|
|
345
|
+
type: 'io.cozy.contacts',
|
|
346
|
+
attributes: { fullname: email, email: [{ address: email }] }
|
|
347
|
+
})
|
|
348
|
+
const mockClient = setup(
|
|
349
|
+
{ contactGroups: [], groupMembers: [] },
|
|
350
|
+
{
|
|
351
|
+
currentRecipients: [{ email: 'Current@Cozy.cc' }],
|
|
352
|
+
recipients: [{ email: 'pending@cozy.cc' }],
|
|
353
|
+
contactsSuggestEnabled: true
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
const fetchJSON = jest.fn().mockResolvedValue({
|
|
357
|
+
data: [
|
|
358
|
+
suggestion('current@cozy.cc'),
|
|
359
|
+
suggestion('PENDING@cozy.cc'),
|
|
360
|
+
suggestion('new@cozy.cc')
|
|
361
|
+
]
|
|
362
|
+
})
|
|
363
|
+
jest.spyOn(mockClient, 'getStackClient').mockReturnValue({ fetchJSON })
|
|
364
|
+
|
|
365
|
+
const contacts = await mockAutosuggestProps.fetchContacts('coz')
|
|
366
|
+
|
|
367
|
+
expect(contacts.map(c => c.fullname)).toEqual(['new@cozy.cc'])
|
|
368
|
+
})
|
|
369
|
+
})
|
|
297
370
|
})
|
|
@@ -3,7 +3,12 @@ import React from 'react'
|
|
|
3
3
|
import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client'
|
|
4
4
|
|
|
5
5
|
import WhoHasAccess from './WhoHasAccess'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
addAvatarUrls,
|
|
8
|
+
hasMemberActions,
|
|
9
|
+
makeAvatarUrl,
|
|
10
|
+
markCurrentUser
|
|
11
|
+
} from '../helpers/recipients'
|
|
7
12
|
import { useMemberPermissionType } from '../hooks/useMemberPermissionType'
|
|
8
13
|
import { useSharingContext } from '../hooks/useSharingContext'
|
|
9
14
|
import { buildInstanceSettingsQuery } from '../queries/queries'
|
|
@@ -44,7 +49,10 @@ const WhoHasAccessWrapper = ({
|
|
|
44
49
|
} = useSharingContext()
|
|
45
50
|
|
|
46
51
|
const instanceUri = client.options.uri
|
|
47
|
-
const markedRecipients =
|
|
52
|
+
const markedRecipients = addAvatarUrls(
|
|
53
|
+
markCurrentUser(recipients, instanceUri),
|
|
54
|
+
instanceUri
|
|
55
|
+
)
|
|
48
56
|
const hasMemberMenus = markedRecipients.some(
|
|
49
57
|
recipient =>
|
|
50
58
|
!recipient.members &&
|
|
@@ -75,6 +83,10 @@ const WhoHasAccessWrapper = ({
|
|
|
75
83
|
status: 'owner',
|
|
76
84
|
instance: instanceUri,
|
|
77
85
|
avatarPath: OWNER_PUBLIC_AVATAR_PATH,
|
|
86
|
+
avatarUrl: makeAvatarUrl(
|
|
87
|
+
{ status: 'owner', avatarPath: OWNER_PUBLIC_AVATAR_PATH },
|
|
88
|
+
instanceUri
|
|
89
|
+
),
|
|
78
90
|
public_name: instanceSettingsResult.data?.attributes?.public_name
|
|
79
91
|
}
|
|
80
92
|
: null
|