cozy-ui 145.0.0 → 146.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 +23 -0
- package/package.json +4 -2
- package/react/Markdown/Readme.md +15 -0
- package/react/Markdown/helpers.js +31 -0
- package/react/Markdown/helpers.spec.js +68 -0
- package/react/Markdown/index.jsx +2 -2
- package/react/MemberList/EmptyMembers.jsx +45 -0
- package/react/MemberList/MemberList.spec.jsx +48 -0
- package/react/MemberList/Readme.md +72 -0
- package/react/MemberList/index.jsx +70 -0
- package/react/MemberList/locales/de.json +8 -0
- package/react/MemberList/locales/en.json +8 -0
- package/react/MemberList/locales/es.json +8 -0
- package/react/MemberList/locales/fr.json +8 -0
- package/react/MemberList/locales/index.js +17 -0
- package/react/MemberList/locales/it.json +8 -0
- package/react/MemberList/locales/ru.json +8 -0
- package/react/MemberList/locales/vi.json +8 -0
- package/react/MemberRow/MemberAvatar.jsx +31 -0
- package/react/MemberRow/MemberAvatar.spec.jsx +26 -0
- package/react/MemberRow/MemberGroupAvatar.jsx +17 -0
- package/react/MemberRow/MemberRoleMenu.jsx +71 -0
- package/react/MemberRow/MemberRoleMenu.spec.jsx +49 -0
- package/react/MemberRow/MemberRoleMenuHeader.jsx +40 -0
- package/react/MemberRow/MemberRow.spec.jsx +39 -0
- package/react/MemberRow/MemberRowActions.jsx +56 -0
- package/react/MemberRow/Readme.md +57 -0
- package/react/MemberRow/index.jsx +112 -0
- package/react/MemberRow/locales/de.json +8 -0
- package/react/MemberRow/locales/en.json +8 -0
- package/react/MemberRow/locales/es.json +8 -0
- package/react/MemberRow/locales/fr.json +8 -0
- package/react/MemberRow/locales/index.js +17 -0
- package/react/MemberRow/locales/it.json +8 -0
- package/react/MemberRow/locales/ru.json +8 -0
- package/react/MemberRow/locales/vi.json +8 -0
- package/react/MemberRow/propTypes.js +33 -0
- package/react/MuiCozyTheme/overrides/makeDarkOverrides.js +6 -0
- package/react/MuiCozyTheme/overrides/makeLightOverrides.js +5 -0
- package/skills/cozy-ui-reference/references/components.md +9 -0
- package/transpiled/react/Markdown/helpers.d.ts +3 -0
- package/transpiled/react/Markdown/helpers.js +28 -0
- package/transpiled/react/Markdown/helpers.spec.d.ts +1 -0
- package/transpiled/react/Markdown/index.js +2 -2
- package/transpiled/react/MemberList/EmptyMembers.d.ts +2 -0
- package/transpiled/react/MemberList/EmptyMembers.js +42 -0
- package/transpiled/react/MemberList/MemberList.spec.d.ts +1 -0
- package/transpiled/react/MemberList/index.d.ts +61 -0
- package/transpiled/react/MemberList/index.js +63 -0
- package/transpiled/react/MemberList/locales/index.d.ts +16 -0
- package/transpiled/react/MemberList/locales/index.js +65 -0
- package/transpiled/react/MemberRow/MemberAvatar.d.ts +6 -0
- package/transpiled/react/MemberRow/MemberAvatar.js +34 -0
- package/transpiled/react/MemberRow/MemberAvatar.spec.d.ts +1 -0
- package/transpiled/react/MemberRow/MemberGroupAvatar.d.ts +6 -0
- package/transpiled/react/MemberRow/MemberGroupAvatar.js +21 -0
- package/transpiled/react/MemberRow/MemberRoleMenu.d.ts +45 -0
- package/transpiled/react/MemberRow/MemberRoleMenu.js +87 -0
- package/transpiled/react/MemberRow/MemberRoleMenu.spec.d.ts +1 -0
- package/transpiled/react/MemberRow/MemberRoleMenuHeader.d.ts +35 -0
- package/transpiled/react/MemberRow/MemberRoleMenuHeader.js +40 -0
- package/transpiled/react/MemberRow/MemberRow.spec.d.ts +1 -0
- package/transpiled/react/MemberRow/MemberRowActions.d.ts +45 -0
- package/transpiled/react/MemberRow/MemberRowActions.js +61 -0
- package/transpiled/react/MemberRow/index.d.ts +68 -0
- package/transpiled/react/MemberRow/index.js +102 -0
- package/transpiled/react/MemberRow/locales/index.d.ts +16 -0
- package/transpiled/react/MemberRow/locales/index.js +65 -0
- package/transpiled/react/MemberRow/propTypes.d.ts +32 -0
- package/transpiled/react/MemberRow/propTypes.js +30 -0
- package/transpiled/react/MuiCozyTheme/overrides/makeDarkOverrides.d.ts +10 -0
- package/transpiled/react/MuiCozyTheme/overrides/makeDarkOverrides.js +6 -0
- package/transpiled/react/MuiCozyTheme/overrides/makeLightOverrides.d.ts +5 -0
- package/transpiled/react/MuiCozyTheme/overrides/makeLightOverrides.js +5 -0
- package/transpiled/react/stylesheet.css +1 -1
- package/react/Tile/index.jsx +0 -98
- package/react/Tile/styles.styl +0 -122
- package/transpiled/react/Tile/index.d.ts +0 -68
- package/transpiled/react/Tile/index.js +0 -94
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
# [146.1.0](https://github.com/cozy/cozy-ui/compare/v146.0.0...v146.1.0) (2026-10-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **MemberList:** Add the members list, row, role menu and avatars ([f96cadf](https://github.com/cozy/cozy-ui/commit/f96cadf))
|
|
7
|
+
|
|
8
|
+
# [146.0.0](https://github.com/cozy/cozy-ui/compare/v145.0.0...v146.0.0) (2026-10-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **Markdown:** Add getPlainTextFromMarkdown helper ([012ddff](https://github.com/cozy/cozy-ui/commit/012ddff))
|
|
14
|
+
* **MuiCozyTheme:** Lighten the Backdrop to 12% black in light theme ([f5945a5](https://github.com/cozy/cozy-ui/commit/f5945a5))
|
|
15
|
+
* Remove Tile component ([4641542](https://github.com/cozy/cozy-ui/commit/4641542))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* Tile has been moved to cozy-ui-plus. Replace
|
|
21
|
+
`import Tile from 'cozy-ui/transpiled/react/Tile'` with
|
|
22
|
+
`import Tile from 'cozy-ui-plus/dist/Tile'`.
|
|
23
|
+
|
|
1
24
|
# [145.0.0](https://github.com/cozy/cozy-ui/compare/v144.0.0...v145.0.0) (2026-10-22)
|
|
2
25
|
|
|
3
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "146.1.0",
|
|
4
4
|
"description": "Cozy apps UI SDK",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "24.x"
|
|
@@ -181,7 +181,9 @@
|
|
|
181
181
|
"react-select": "^4.3.0",
|
|
182
182
|
"react-virtuoso": "^4.13.0",
|
|
183
183
|
"remark-gfm": "^1.0.0",
|
|
184
|
-
"
|
|
184
|
+
"remark-parse": "^9.0.0",
|
|
185
|
+
"rooks": "8.4.0",
|
|
186
|
+
"unified": "^9.0.0"
|
|
185
187
|
},
|
|
186
188
|
"peerDependencies": {
|
|
187
189
|
"@linagora/twake-icons": ">=2.6.2",
|
package/react/Markdown/Readme.md
CHANGED
|
@@ -69,3 +69,18 @@ Block code:
|
|
|
69
69
|
<Markdown content={textInMarkdown} />
|
|
70
70
|
|
|
71
71
|
```
|
|
72
|
+
|
|
73
|
+
### Plain text
|
|
74
|
+
|
|
75
|
+
`getPlainTextFromMarkdown` gives the words of a Markdown content on one line, without its syntax, as `Markdown` shows them: for a preview, in a list for example.
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
import { getPlainTextFromMarkdown } from 'cozy-ui/transpiled/react/Markdown/helpers'
|
|
79
|
+
import Typography from 'cozy-ui/transpiled/react/Typography'
|
|
80
|
+
|
|
81
|
+
const answer = '# Demo\n\nSome **bold** text, a [link](https://cozy.io) and a list:\n\n- first\n- second'
|
|
82
|
+
|
|
83
|
+
;
|
|
84
|
+
|
|
85
|
+
<Typography noWrap>{getPlainTextFromMarkdown(answer)}</Typography>
|
|
86
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import remarkGfm from 'remark-gfm'
|
|
2
|
+
import remarkParse from 'remark-parse'
|
|
3
|
+
import unified from 'unified'
|
|
4
|
+
|
|
5
|
+
export const remarkPlugins = [remarkGfm]
|
|
6
|
+
|
|
7
|
+
const markdown = unified().use(remarkParse).use(remarkPlugins)
|
|
8
|
+
|
|
9
|
+
// Nodes whose children are blocks or cells: a space keeps their words apart
|
|
10
|
+
const BLOCKS = ['root', 'blockquote', 'list', 'listItem', 'table', 'tableRow']
|
|
11
|
+
|
|
12
|
+
const getWords = node => {
|
|
13
|
+
if (node.type === 'image' || node.type === 'imageReference')
|
|
14
|
+
return node.alt ?? ''
|
|
15
|
+
if (node.type === 'break') return ' '
|
|
16
|
+
if (typeof node.value === 'string') return node.value
|
|
17
|
+
|
|
18
|
+
return (node.children ?? [])
|
|
19
|
+
.map(getWords)
|
|
20
|
+
.join(BLOCKS.includes(node.type) ? ' ' : '')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gives the words of a Markdown content on one line, without its syntax,
|
|
25
|
+
* as the Markdown component shows them: for a preview
|
|
26
|
+
*
|
|
27
|
+
* @param {string} content - Markdown content
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export const getPlainTextFromMarkdown = content =>
|
|
31
|
+
getWords(markdown.parse(content)).replace(/\s+/g, ' ').trim()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getPlainTextFromMarkdown } from './helpers'
|
|
2
|
+
|
|
3
|
+
describe('getPlainTextFromMarkdown', () => {
|
|
4
|
+
it('gives the words of a Markdown content on one line', () => {
|
|
5
|
+
const content = [
|
|
6
|
+
'# Title',
|
|
7
|
+
'',
|
|
8
|
+
'Some **bold** text, with _italic_',
|
|
9
|
+
'',
|
|
10
|
+
'- a first item',
|
|
11
|
+
'1. a second item, with `inline code`',
|
|
12
|
+
'',
|
|
13
|
+
'| Name | Role |',
|
|
14
|
+
'|-------|:---------:|',
|
|
15
|
+
'| Alice | Developer |',
|
|
16
|
+
'',
|
|
17
|
+
'> A quote, a link: [cozy](https://cozy.io) and .',
|
|
18
|
+
'',
|
|
19
|
+
'```js',
|
|
20
|
+
"console.log('hello')",
|
|
21
|
+
'```'
|
|
22
|
+
].join('\n')
|
|
23
|
+
|
|
24
|
+
expect(getPlainTextFromMarkdown(content)).toBe(
|
|
25
|
+
"Title Some bold text, with italic a first item a second item, with inline code Name Role Alice Developer A quote, a link: cozy and a logo. console.log('hello')"
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('keeps the characters that are no Markdown', () => {
|
|
30
|
+
expect(getPlainTextFromMarkdown('my_file_name is 2 * 3 = 6')).toBe(
|
|
31
|
+
'my_file_name is 2 * 3 = 6'
|
|
32
|
+
)
|
|
33
|
+
expect(getPlainTextFromMarkdown('2 ** 10 bytes in my__file')).toBe(
|
|
34
|
+
'2 ** 10 bytes in my__file'
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('removes bold, struck and italic by pairs', () => {
|
|
39
|
+
expect(
|
|
40
|
+
getPlainTextFromMarkdown('**a** and **b**, ~~c~~, *d* and _e_')
|
|
41
|
+
).toBe('a and b, c, d and e')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('gives the text of a link with parentheses in its URL', () => {
|
|
45
|
+
expect(
|
|
46
|
+
getPlainTextFromMarkdown(
|
|
47
|
+
'[Foo](https://en.wikipedia.org/wiki/Foo_(bar)) and .png)'
|
|
48
|
+
)
|
|
49
|
+
).toBe('Foo and a chart')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('gives the alt of an image by reference', () => {
|
|
53
|
+
expect(
|
|
54
|
+
getPlainTextFromMarkdown('![a logo][logo]\n\n[logo]: logo.png')
|
|
55
|
+
).toBe('a logo')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('puts the lines of a hard break apart', () => {
|
|
59
|
+
expect(getPlainTextFromMarkdown('first line \nsecond line')).toBe(
|
|
60
|
+
'first line second line'
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('gives nothing for a content of Markdown only', () => {
|
|
65
|
+
expect(getPlainTextFromMarkdown('')).toBe('')
|
|
66
|
+
expect(getPlainTextFromMarkdown('---')).toBe('')
|
|
67
|
+
})
|
|
68
|
+
})
|
package/react/Markdown/index.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import ReactMarkdown from 'react-markdown'
|
|
3
|
-
import remarkGfm from 'remark-gfm'
|
|
4
3
|
|
|
4
|
+
import { remarkPlugins } from './helpers'
|
|
5
5
|
import Link from '../Link'
|
|
6
6
|
import Typography from '../Typography'
|
|
7
7
|
|
|
@@ -32,7 +32,7 @@ const components = {
|
|
|
32
32
|
|
|
33
33
|
const Markdown = ({ content }) => {
|
|
34
34
|
return (
|
|
35
|
-
<ReactMarkdown remarkPlugins={
|
|
35
|
+
<ReactMarkdown remarkPlugins={remarkPlugins} components={components}>
|
|
36
36
|
{content}
|
|
37
37
|
</ReactMarkdown>
|
|
38
38
|
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Icon, Team } from '@linagora/twake-icons'
|
|
2
|
+
import cx from 'classnames'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { useI18n, useExtendI18n } from 'twake-i18n'
|
|
5
|
+
|
|
6
|
+
import { locales } from './locales'
|
|
7
|
+
import Typography from '../Typography'
|
|
8
|
+
import useBreakpoints from '../providers/Breakpoints'
|
|
9
|
+
import { alpha, makeStyles } from '../styles'
|
|
10
|
+
|
|
11
|
+
const useStyles = makeStyles(theme => ({
|
|
12
|
+
root: {
|
|
13
|
+
backgroundColor: alpha(
|
|
14
|
+
theme.palette.secondary.main,
|
|
15
|
+
theme.palette.action.hoverOpacity
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const EmptyMembers = () => {
|
|
21
|
+
useExtendI18n(locales)
|
|
22
|
+
const { t } = useI18n()
|
|
23
|
+
const { isMobile } = useBreakpoints()
|
|
24
|
+
const classes = useStyles()
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cx(
|
|
29
|
+
'u-flex u-flex-column u-flex-items-center u-pv-2 u-ph-1 u-bdrs-6',
|
|
30
|
+
isMobile ? 'u-mh-1' : 'u-mh-2',
|
|
31
|
+
classes.root
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<Icon icon={Team} size={48} color="var(--primaryColorLight)" />
|
|
35
|
+
<Typography variant="body2" className="u-mt-half">
|
|
36
|
+
{t('MemberList.empty.title')}
|
|
37
|
+
</Typography>
|
|
38
|
+
<Typography variant="caption" color="textSecondary">
|
|
39
|
+
{t('MemberList.empty.subtitle')}
|
|
40
|
+
</Typography>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default EmptyMembers
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import MemberList from '.'
|
|
5
|
+
import DemoProvider from '../providers/DemoProvider'
|
|
6
|
+
|
|
7
|
+
const roles = [
|
|
8
|
+
{ value: 'viewer', label: 'Viewer' },
|
|
9
|
+
{ value: 'editor', label: 'Editor' }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const owner = { id: 'owner', name: 'Alice', isOwner: true }
|
|
13
|
+
const bob = { id: 'bob', name: 'Bob', role: 'editor' }
|
|
14
|
+
|
|
15
|
+
describe('MemberList', () => {
|
|
16
|
+
const setup = (members, props) =>
|
|
17
|
+
render(
|
|
18
|
+
<DemoProvider>
|
|
19
|
+
<MemberList
|
|
20
|
+
{...props}
|
|
21
|
+
members={members}
|
|
22
|
+
roles={roles}
|
|
23
|
+
onRoleChange={jest.fn()}
|
|
24
|
+
onRemove={jest.fn()}
|
|
25
|
+
/>
|
|
26
|
+
</DemoProvider>
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
it('shows the owner first', () => {
|
|
30
|
+
setup([bob, owner])
|
|
31
|
+
|
|
32
|
+
const rows = screen.getAllByRole('listitem')
|
|
33
|
+
expect(rows[0]).toHaveTextContent('Alice')
|
|
34
|
+
expect(rows[1]).toHaveTextContent('Bob')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('shows the empty state when there is no member but the owner', () => {
|
|
38
|
+
setup([owner])
|
|
39
|
+
|
|
40
|
+
expect(screen.queryByText('No one has been added yet')).toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('does not show the empty state when there are members', () => {
|
|
44
|
+
setup([owner, bob])
|
|
45
|
+
|
|
46
|
+
expect(screen.queryByText('No one has been added yet')).toBe(null)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Lists the members of a sharing with `MemberRow`, the owner first.
|
|
2
|
+
|
|
3
|
+
The list only gets display data: the app turns its own members (sharing recipients, address book users, space members…) into the shape described in `MemberRow` and handles `onRoleChange` and `onRemove`. New members fade in.
|
|
4
|
+
|
|
5
|
+
### Demo
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
|
|
9
|
+
import Buttons from 'cozy-ui/transpiled/react/Buttons'
|
|
10
|
+
import MemberList from 'cozy-ui/transpiled/react/MemberList'
|
|
11
|
+
|
|
12
|
+
const roles = [
|
|
13
|
+
{ value: 'viewer', label: 'Viewer' },
|
|
14
|
+
{ value: 'editor', label: 'Editor' }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
initialState = {
|
|
18
|
+
members: [
|
|
19
|
+
{ id: 'alice', name: 'Alice', subtitle: 'alice@cozy.cc', initials: 'A', isOwner: true, isCurrentUser: true },
|
|
20
|
+
{ id: 'bob', name: 'Bob', subtitle: 'bob@cozy.cc', initials: 'B', role: 'editor', canChangeRole: true, canRemove: true },
|
|
21
|
+
{ id: 'carol', name: 'Carol', subtitle: 'Invitation sent', initials: 'C', role: 'viewer', canChangeRole: true, canRemove: true }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const changeRole = (member, role) =>
|
|
26
|
+
setState(({ members }) => ({
|
|
27
|
+
members: members.map(m => (m.id === member.id ? { ...m, role } : m))
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
const remove = member =>
|
|
31
|
+
setState(({ members }) => ({ members: members.filter(m => m.id !== member.id) }))
|
|
32
|
+
|
|
33
|
+
const add = () =>
|
|
34
|
+
setState(({ members }) => ({
|
|
35
|
+
members: [
|
|
36
|
+
...members,
|
|
37
|
+
{ id: `dave-${members.length}`, name: 'Dave', subtitle: 'dave@cozy.cc', initials: 'D', role: 'viewer', canChangeRole: true, canRemove: true }
|
|
38
|
+
]
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
;
|
|
42
|
+
|
|
43
|
+
<DemoProvider>
|
|
44
|
+
<Buttons variant="secondary" label="Add a member" onClick={add} />
|
|
45
|
+
<MemberList
|
|
46
|
+
members={state.members}
|
|
47
|
+
roles={roles}
|
|
48
|
+
onRoleChange={changeRole}
|
|
49
|
+
onRemove={remove}
|
|
50
|
+
/>
|
|
51
|
+
</DemoProvider>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Empty state
|
|
55
|
+
|
|
56
|
+
A placeholder is shown when there is no member but the owner, unless `showEmptyState` is `false`. An app replaces its texts by defining the `MemberList.empty.title` and `MemberList.empty.subtitle` keys in its own locales.
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
|
|
60
|
+
import MemberList from 'cozy-ui/transpiled/react/MemberList'
|
|
61
|
+
|
|
62
|
+
;
|
|
63
|
+
|
|
64
|
+
<DemoProvider>
|
|
65
|
+
<MemberList
|
|
66
|
+
members={[{ id: 'alice', name: 'Alice', subtitle: 'alice@cozy.cc', initials: 'A', isOwner: true, isCurrentUser: true }]}
|
|
67
|
+
roles={[]}
|
|
68
|
+
onRoleChange={() => {}}
|
|
69
|
+
onRemove={() => {}}
|
|
70
|
+
/>
|
|
71
|
+
</DemoProvider>
|
|
72
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { useGetIsMounted } from 'rooks'
|
|
4
|
+
|
|
5
|
+
import EmptyMembers from './EmptyMembers'
|
|
6
|
+
import List from '../List'
|
|
7
|
+
import MemberRow from '../MemberRow'
|
|
8
|
+
import { memberPropType, rolesPropType } from '../MemberRow/propTypes'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lists the members of a sharing, the owner first
|
|
12
|
+
*
|
|
13
|
+
* @param {object} props
|
|
14
|
+
* @param {object[]} props.members - Members to display, see `MemberRow`
|
|
15
|
+
* @param {object[]} props.roles - Roles a member can get, as `{ value, label }`
|
|
16
|
+
* @param {Function} props.onRoleChange - Called with the member and its new role
|
|
17
|
+
* @param {Function} props.onRemove - Called with the member to remove
|
|
18
|
+
* @param {boolean} [props.showEmptyState] - Whether to show a placeholder
|
|
19
|
+
* when there is no member but the owner
|
|
20
|
+
* @param {string} [props.className] - Class name of the list
|
|
21
|
+
*/
|
|
22
|
+
const MemberList = ({
|
|
23
|
+
members,
|
|
24
|
+
roles,
|
|
25
|
+
onRoleChange,
|
|
26
|
+
onRemove,
|
|
27
|
+
showEmptyState = true,
|
|
28
|
+
className
|
|
29
|
+
}) => {
|
|
30
|
+
const getIsMounted = useGetIsMounted()
|
|
31
|
+
const owner = members.find(member => member.isOwner)
|
|
32
|
+
const otherMembers = members.filter(member => !member.isOwner)
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<List className={className}>
|
|
37
|
+
{owner && (
|
|
38
|
+
<MemberRow
|
|
39
|
+
member={owner}
|
|
40
|
+
roles={roles}
|
|
41
|
+
onRoleChange={onRoleChange}
|
|
42
|
+
onRemove={onRemove}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
{otherMembers.map(member => (
|
|
46
|
+
<MemberRow
|
|
47
|
+
key={member.id}
|
|
48
|
+
member={member}
|
|
49
|
+
roles={roles}
|
|
50
|
+
isNew={getIsMounted()}
|
|
51
|
+
onRoleChange={onRoleChange}
|
|
52
|
+
onRemove={onRemove}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</List>
|
|
56
|
+
{showEmptyState && otherMembers.length === 0 && <EmptyMembers />}
|
|
57
|
+
</>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
MemberList.propTypes = {
|
|
62
|
+
members: PropTypes.arrayOf(memberPropType).isRequired,
|
|
63
|
+
roles: rolesPropType.isRequired,
|
|
64
|
+
onRoleChange: PropTypes.func.isRequired,
|
|
65
|
+
onRemove: PropTypes.func.isRequired,
|
|
66
|
+
showEmptyState: PropTypes.bool,
|
|
67
|
+
className: PropTypes.string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default MemberList
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import de from './de.json'
|
|
2
|
+
import en from './en.json'
|
|
3
|
+
import es from './es.json'
|
|
4
|
+
import fr from './fr.json'
|
|
5
|
+
import it from './it.json'
|
|
6
|
+
import ru from './ru.json'
|
|
7
|
+
import vi from './vi.json'
|
|
8
|
+
|
|
9
|
+
export const locales = {
|
|
10
|
+
de,
|
|
11
|
+
en,
|
|
12
|
+
es,
|
|
13
|
+
fr,
|
|
14
|
+
it,
|
|
15
|
+
ru,
|
|
16
|
+
vi
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import Avatar from '../Avatar'
|
|
4
|
+
|
|
5
|
+
const MemberAvatar = ({ initials, avatarUrl, ...rest }) => {
|
|
6
|
+
// Remember which image url failed to load so we can fall back to the
|
|
7
|
+
// initials. Storing the url (instead of a boolean) means a new url is
|
|
8
|
+
// retried automatically, without an effect to reset the error state.
|
|
9
|
+
const [erroredSrc, setErroredSrc] = useState(null)
|
|
10
|
+
|
|
11
|
+
// The avatar can become unreachable (eg. the related sharing is revoked
|
|
12
|
+
// while its members are still rendered), in which case the image request
|
|
13
|
+
// fails. Without a fallback the browser shows a broken-image icon, so we
|
|
14
|
+
// render the initials instead.
|
|
15
|
+
return (
|
|
16
|
+
<Avatar {...rest}>
|
|
17
|
+
{avatarUrl && erroredSrc !== avatarUrl ? (
|
|
18
|
+
<img
|
|
19
|
+
width="100%"
|
|
20
|
+
height="100%"
|
|
21
|
+
src={avatarUrl}
|
|
22
|
+
onError={() => setErroredSrc(avatarUrl)}
|
|
23
|
+
/>
|
|
24
|
+
) : (
|
|
25
|
+
initials
|
|
26
|
+
)}
|
|
27
|
+
</Avatar>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default MemberAvatar
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import MemberAvatar from './MemberAvatar'
|
|
5
|
+
import DemoProvider from '../providers/DemoProvider'
|
|
6
|
+
|
|
7
|
+
describe('MemberAvatar', () => {
|
|
8
|
+
const setup = (initials, avatarUrl) =>
|
|
9
|
+
render(
|
|
10
|
+
<DemoProvider>
|
|
11
|
+
<MemberAvatar initials={initials} avatarUrl={avatarUrl} />
|
|
12
|
+
</DemoProvider>
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
it('falls back to the initials when the avatar image fails to load', () => {
|
|
16
|
+
const { container, getByText } = setup(
|
|
17
|
+
'B',
|
|
18
|
+
'http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
fireEvent.error(container.querySelector('img'))
|
|
22
|
+
|
|
23
|
+
expect(container.querySelector('img')).toBeNull()
|
|
24
|
+
expect(getByText('B')).toBeTruthy()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Icon, Peoples } from '@linagora/twake-icons'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import Avatar, { colorToGradient } from '../Avatar'
|
|
5
|
+
import { COLORS } from '../ColorList/helpers'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_COLOR = COLORS[4]
|
|
8
|
+
|
|
9
|
+
const MemberGroupAvatar = ({ size, color = DEFAULT_COLOR, className }) => {
|
|
10
|
+
return (
|
|
11
|
+
<Avatar size={size} color={colorToGradient(color)} className={className}>
|
|
12
|
+
<Icon icon={Peoples} />
|
|
13
|
+
</Avatar>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default MemberGroupAvatar
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React, { useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import MemberRoleMenuHeader from './MemberRoleMenuHeader'
|
|
5
|
+
import { memberPropType, rolesPropType } from './propTypes'
|
|
6
|
+
import ActionsMenuItem from '../ActionsMenu/ActionsMenuItem'
|
|
7
|
+
import ActionsMenuWrapper from '../ActionsMenu/ActionsMenuWrapper'
|
|
8
|
+
import DropdownButton from '../DropdownButton'
|
|
9
|
+
import ListItemIcon from '../ListItemIcon'
|
|
10
|
+
import ListItemText from '../ListItemText'
|
|
11
|
+
import Radios from '../Radios'
|
|
12
|
+
import useBreakpoints from '../providers/Breakpoints'
|
|
13
|
+
|
|
14
|
+
const MemberRoleMenu = ({ member, value, roles, onChange }) => {
|
|
15
|
+
const { isMobile } = useBreakpoints()
|
|
16
|
+
const buttonRef = useRef()
|
|
17
|
+
const [isMenuDisplayed, setMenuDisplayed] = useState(false)
|
|
18
|
+
|
|
19
|
+
const label = roles.find(role => role.value === value)?.label
|
|
20
|
+
|
|
21
|
+
const handleRoleClick = role => {
|
|
22
|
+
if (role.value !== value) onChange(role.value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<DropdownButton
|
|
28
|
+
ref={buttonRef}
|
|
29
|
+
aria-controls="simple-menu"
|
|
30
|
+
aria-haspopup="true"
|
|
31
|
+
onClick={() => setMenuDisplayed(true)}
|
|
32
|
+
textVariant="body2"
|
|
33
|
+
>
|
|
34
|
+
{label}
|
|
35
|
+
</DropdownButton>
|
|
36
|
+
<ActionsMenuWrapper
|
|
37
|
+
open={isMenuDisplayed}
|
|
38
|
+
anchorEl={buttonRef.current}
|
|
39
|
+
getContentAnchorEl={null}
|
|
40
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
41
|
+
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
|
42
|
+
keepMounted
|
|
43
|
+
autoClose
|
|
44
|
+
onClose={() => setMenuDisplayed(false)}
|
|
45
|
+
>
|
|
46
|
+
{/* The header renders nothing on desktop but would still take the initial focus of the menu */}
|
|
47
|
+
{isMobile && <MemberRoleMenuHeader member={member} />}
|
|
48
|
+
{roles.map(role => (
|
|
49
|
+
<ActionsMenuItem
|
|
50
|
+
key={role.value}
|
|
51
|
+
onClick={() => handleRoleClick(role)}
|
|
52
|
+
>
|
|
53
|
+
<ListItemIcon>
|
|
54
|
+
<Radios checked={role.value === value} />
|
|
55
|
+
</ListItemIcon>
|
|
56
|
+
<ListItemText primary={role.label} />
|
|
57
|
+
</ActionsMenuItem>
|
|
58
|
+
))}
|
|
59
|
+
</ActionsMenuWrapper>
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
MemberRoleMenu.propTypes = {
|
|
65
|
+
member: memberPropType.isRequired,
|
|
66
|
+
value: PropTypes.string.isRequired,
|
|
67
|
+
roles: rolesPropType.isRequired,
|
|
68
|
+
onChange: PropTypes.func.isRequired
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default MemberRoleMenu
|