cozy-ui 146.0.0 → 146.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 +16 -0
  2. package/dist/cozy-ui.min.css +1 -1
  3. package/dist/cozy-ui.utils.min.css +1 -1
  4. package/package.json +1 -1
  5. package/react/MemberList/EmptyMembers.jsx +45 -0
  6. package/react/MemberList/MemberList.spec.jsx +48 -0
  7. package/react/MemberList/Readme.md +72 -0
  8. package/react/MemberList/index.jsx +70 -0
  9. package/react/MemberList/locales/de.json +8 -0
  10. package/react/MemberList/locales/en.json +8 -0
  11. package/react/MemberList/locales/es.json +8 -0
  12. package/react/MemberList/locales/fr.json +8 -0
  13. package/react/MemberList/locales/index.js +17 -0
  14. package/react/MemberList/locales/it.json +8 -0
  15. package/react/MemberList/locales/ru.json +8 -0
  16. package/react/MemberList/locales/vi.json +8 -0
  17. package/react/MemberRow/MemberAvatar.jsx +31 -0
  18. package/react/MemberRow/MemberAvatar.spec.jsx +26 -0
  19. package/react/MemberRow/MemberGroupAvatar.jsx +17 -0
  20. package/react/MemberRow/MemberRoleMenu.jsx +71 -0
  21. package/react/MemberRow/MemberRoleMenu.spec.jsx +49 -0
  22. package/react/MemberRow/MemberRoleMenuHeader.jsx +40 -0
  23. package/react/MemberRow/MemberRow.spec.jsx +39 -0
  24. package/react/MemberRow/MemberRowActions.jsx +56 -0
  25. package/react/MemberRow/Readme.md +57 -0
  26. package/react/MemberRow/index.jsx +112 -0
  27. package/react/MemberRow/locales/de.json +8 -0
  28. package/react/MemberRow/locales/en.json +8 -0
  29. package/react/MemberRow/locales/es.json +8 -0
  30. package/react/MemberRow/locales/fr.json +8 -0
  31. package/react/MemberRow/locales/index.js +17 -0
  32. package/react/MemberRow/locales/it.json +8 -0
  33. package/react/MemberRow/locales/ru.json +8 -0
  34. package/react/MemberRow/locales/vi.json +8 -0
  35. package/react/MemberRow/propTypes.js +33 -0
  36. package/react/MuiCozyTheme/makePalette.js +2 -2
  37. package/stylus/settings/themes/light.styl +5 -5
  38. package/theme/palette.json +10 -10
  39. package/transpiled/react/MemberList/EmptyMembers.d.ts +2 -0
  40. package/transpiled/react/MemberList/EmptyMembers.js +42 -0
  41. package/transpiled/react/MemberList/MemberList.spec.d.ts +1 -0
  42. package/transpiled/react/MemberList/index.d.ts +61 -0
  43. package/transpiled/react/MemberList/index.js +63 -0
  44. package/transpiled/react/MemberList/locales/index.d.ts +16 -0
  45. package/transpiled/react/MemberList/locales/index.js +65 -0
  46. package/transpiled/react/MemberRow/MemberAvatar.d.ts +6 -0
  47. package/transpiled/react/MemberRow/MemberAvatar.js +34 -0
  48. package/transpiled/react/MemberRow/MemberAvatar.spec.d.ts +1 -0
  49. package/transpiled/react/MemberRow/MemberGroupAvatar.d.ts +6 -0
  50. package/transpiled/react/MemberRow/MemberGroupAvatar.js +21 -0
  51. package/transpiled/react/MemberRow/MemberRoleMenu.d.ts +45 -0
  52. package/transpiled/react/MemberRow/MemberRoleMenu.js +87 -0
  53. package/transpiled/react/MemberRow/MemberRoleMenu.spec.d.ts +1 -0
  54. package/transpiled/react/MemberRow/MemberRoleMenuHeader.d.ts +35 -0
  55. package/transpiled/react/MemberRow/MemberRoleMenuHeader.js +40 -0
  56. package/transpiled/react/MemberRow/MemberRow.spec.d.ts +1 -0
  57. package/transpiled/react/MemberRow/MemberRowActions.d.ts +45 -0
  58. package/transpiled/react/MemberRow/MemberRowActions.js +61 -0
  59. package/transpiled/react/MemberRow/index.d.ts +68 -0
  60. package/transpiled/react/MemberRow/index.js +102 -0
  61. package/transpiled/react/MemberRow/locales/index.d.ts +16 -0
  62. package/transpiled/react/MemberRow/locales/index.js +65 -0
  63. package/transpiled/react/MemberRow/propTypes.d.ts +32 -0
  64. package/transpiled/react/MemberRow/propTypes.js +30 -0
  65. package/transpiled/react/MuiCozyTheme/makePalette.js +2 -2
@@ -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,8 @@
1
+ {
2
+ "MemberList": {
3
+ "empty": {
4
+ "title": "Noch niemand hinzugefügt",
5
+ "subtitle": "Eingeladene Personen werden hier angezeigt."
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "MemberList": {
3
+ "empty": {
4
+ "title": "No one has been added yet",
5
+ "subtitle": "People invited to join will be listed here."
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "MemberList": {
3
+ "empty": {
4
+ "title": "Todavía no se ha añadido a nadie",
5
+ "subtitle": "Las personas invitadas aparecerán aquí."
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "MemberList": {
3
+ "empty": {
4
+ "title": "Personne n'a encore été ajouté",
5
+ "subtitle": "Les personnes invitées apparaîtront ici."
6
+ }
7
+ }
8
+ }
@@ -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,8 @@
1
+ {
2
+ "MemberList": {
3
+ "empty": {
4
+ "title": "Nessuno è stato ancora aggiunto",
5
+ "subtitle": "Le persone invitate verranno elencate qui."
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "MemberList": {
3
+ "empty": {
4
+ "title": "Пока никто не добавлен",
5
+ "subtitle": "Здесь появятся приглашённые пользователи."
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "MemberList": {
3
+ "empty": {
4
+ "title": "Chưa có ai được thêm",
5
+ "subtitle": "Những người được mời sẽ hiển thị tại đây."
6
+ }
7
+ }
8
+ }
@@ -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
@@ -0,0 +1,49 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import React from 'react'
3
+
4
+ import MemberRoleMenu from './MemberRoleMenu'
5
+ import DemoProvider from '../providers/DemoProvider'
6
+
7
+ const viewer = { value: 'viewer', label: 'Viewer' }
8
+ const editor = { value: 'editor', label: 'Editor' }
9
+
10
+ describe('MemberRoleMenu', () => {
11
+ let createRangeBackup
12
+
13
+ beforeAll(() => {
14
+ createRangeBackup = global.document.createRange
15
+ global.document.createRange = jest.fn(() => ({
16
+ setStart: () => {},
17
+ setEnd: () => {},
18
+ commonAncestorContainer: { nodeName: 'BODY', ownerDocument: document }
19
+ }))
20
+ })
21
+
22
+ afterAll(() => {
23
+ global.document.createRange = createRangeBackup
24
+ })
25
+
26
+ const setup = roles => {
27
+ const onChange = jest.fn()
28
+ render(
29
+ <DemoProvider>
30
+ <MemberRoleMenu
31
+ member={{ id: 'bob', name: 'Bob' }}
32
+ value="editor"
33
+ roles={roles}
34
+ onChange={onChange}
35
+ />
36
+ </DemoProvider>
37
+ )
38
+ return { onChange }
39
+ }
40
+
41
+ it('does not change the role when the current one is picked', () => {
42
+ const { onChange } = setup([viewer, editor])
43
+
44
+ fireEvent.click(screen.getByRole('button', { name: 'Editor' }))
45
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Editor' }))
46
+
47
+ expect(onChange).not.toHaveBeenCalled()
48
+ })
49
+ })
@@ -0,0 +1,40 @@
1
+ import React from 'react'
2
+ import { useI18n } from 'twake-i18n'
3
+
4
+ import MemberAvatar from './MemberAvatar'
5
+ import MemberGroupAvatar from './MemberGroupAvatar'
6
+ import { memberPropType } from './propTypes'
7
+ import ActionsMenuMobileHeader from '../ActionsMenu/ActionsMenuMobileHeader'
8
+ import ListItemIcon from '../ListItemIcon'
9
+ import ListItemText from '../ListItemText'
10
+
11
+ const MemberRoleMenuHeader = ({ member }) => {
12
+ const { t } = useI18n()
13
+ const { name, avatarUrl, initials, color, isGroup, isCurrentUser } = member
14
+
15
+ return (
16
+ <ActionsMenuMobileHeader>
17
+ <ListItemIcon>
18
+ {isGroup ? (
19
+ <MemberGroupAvatar size="m" color={color} />
20
+ ) : (
21
+ <MemberAvatar size="m" initials={initials} avatarUrl={avatarUrl} />
22
+ )}
23
+ </ListItemIcon>
24
+ <ListItemText
25
+ primary={
26
+ isCurrentUser
27
+ ? t('MemberRow.you')
28
+ : name || t('MemberRow.defaultName')
29
+ }
30
+ primaryTypographyProps={{ variant: 'h6' }}
31
+ />
32
+ </ActionsMenuMobileHeader>
33
+ )
34
+ }
35
+
36
+ MemberRoleMenuHeader.propTypes = {
37
+ member: memberPropType.isRequired
38
+ }
39
+
40
+ export default MemberRoleMenuHeader
@@ -0,0 +1,39 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+ import { I18n, useI18n } from 'twake-i18n'
4
+
5
+ import MemberRow from '.'
6
+ import List from '../List'
7
+ import DemoProvider from '../providers/DemoProvider'
8
+
9
+ describe('MemberRow', () => {
10
+ it('keeps the translations of the app in the subtitle, and its own', () => {
11
+ const AppSubtitle = () => {
12
+ const { t } = useI18n()
13
+ return t('status')
14
+ }
15
+ render(
16
+ <DemoProvider>
17
+ <I18n lang="en" dictRequire={() => ({ status: 'Invitation sent' })}>
18
+ <List>
19
+ <MemberRow
20
+ member={{
21
+ id: 'bob',
22
+ name: 'Bob',
23
+ subtitle: <AppSubtitle />,
24
+ role: 'editor',
25
+ canRemove: true
26
+ }}
27
+ roles={[{ value: 'editor', label: 'Editor' }]}
28
+ onRoleChange={jest.fn()}
29
+ onRemove={jest.fn()}
30
+ />
31
+ </List>
32
+ </I18n>
33
+ </DemoProvider>
34
+ )
35
+
36
+ expect(screen.queryByText('Invitation sent')).toBeInTheDocument()
37
+ expect(screen.queryByLabelText('Remove from sharing')).toBeInTheDocument()
38
+ })
39
+ })
@@ -0,0 +1,56 @@
1
+ import { Icon, CrossCircleOutline } from '@linagora/twake-icons'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+ import { useI18n } from 'twake-i18n'
5
+
6
+ import MemberRoleMenu from './MemberRoleMenu'
7
+ import { memberPropType, rolesPropType } from './propTypes'
8
+ import IconButton from '../IconButton'
9
+ import Spinner from '../Spinner'
10
+ import Typography from '../Typography'
11
+
12
+ const MemberRowActions = ({ member, roles, onRoleChange, onRemove }) => {
13
+ const { t } = useI18n()
14
+ const { role, canChangeRole, canRemove, removeIcon, removeLabel, isPending } =
15
+ member
16
+ const memberRoles = member.roles ?? roles
17
+
18
+ const isRoleMenuShown = canChangeRole && !isPending
19
+
20
+ return (
21
+ <div className="u-flex u-flex-items-center u-g-1">
22
+ {isPending && <Spinner />}
23
+ {isRoleMenuShown ? (
24
+ <MemberRoleMenu
25
+ member={member}
26
+ value={role}
27
+ roles={memberRoles}
28
+ onChange={newRole => onRoleChange(member, newRole)}
29
+ />
30
+ ) : (
31
+ <Typography variant="body2">
32
+ {memberRoles.find(memberRole => memberRole.value === role)?.label}
33
+ </Typography>
34
+ )}
35
+ {canRemove && !isPending && (
36
+ <IconButton
37
+ onClick={() => onRemove(member)}
38
+ size="small"
39
+ className="u-ml-half"
40
+ aria-label={removeLabel ?? t('MemberRow.remove')}
41
+ >
42
+ <Icon icon={removeIcon ?? CrossCircleOutline} />
43
+ </IconButton>
44
+ )}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ MemberRowActions.propTypes = {
50
+ member: memberPropType.isRequired,
51
+ roles: rolesPropType.isRequired,
52
+ onRoleChange: PropTypes.func.isRequired,
53
+ onRemove: PropTypes.func.isRequired
54
+ }
55
+
56
+ export default MemberRowActions
@@ -0,0 +1,57 @@
1
+ Displays a member of a sharing: avatar, name, subtitle, and on the right its role and a button to remove them. `MemberList` renders one row per member.
2
+
3
+ ### Props of a member
4
+
5
+ * **id**: unique key of the member
6
+ * **name**: displayed name, replaced by "You" when `isCurrentUser`
7
+ * **subtitle**: email, status… (any node)
8
+ * **avatarUrl** and **initials**: picture of the member, initials when there is no picture or it fails to load
9
+ * **isGroup** and **color**: shows a group avatar
10
+ * **isOwner**: shows the "Owner" label and no action
11
+ * **role** and **roles**: current role, and the roles of this member when they differ from the `roles` of the row
12
+ * **canChangeRole**: shows the role in a menu, as text otherwise
13
+ * **canRemove**, **removeIcon** and **removeLabel**: remove button, its icon and its accessible label
14
+ * **action**: `{ label, onClick }` button shown in place of the role and the remove button
15
+ * **isPending**: shows a spinner instead of the actions
16
+ * **onClick**: makes the row clickable
17
+ * **data**: anything the app needs back in its callbacks, not read by the row
18
+
19
+ ### Demo
20
+
21
+ ```jsx
22
+ import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
23
+ import List from 'cozy-ui/transpiled/react/List'
24
+ import MemberRow from 'cozy-ui/transpiled/react/MemberRow'
25
+ import { Unlink } from '@linagora/twake-icons'
26
+
27
+ const roles = [
28
+ { value: 'viewer', label: 'Viewer' },
29
+ { value: 'editor', label: 'Editor' }
30
+ ]
31
+
32
+ const rows = [
33
+ ['Owner', { id: 'alice', name: 'Alice', subtitle: 'alice@cozy.cc', initials: 'A', isOwner: true, isCurrentUser: true }],
34
+ ['Role and remove button', { id: 'bob', name: 'Bob', subtitle: 'bob@cozy.cc', initials: 'B', role: 'editor', canChangeRole: true, canRemove: true }],
35
+ ['Role as text', { id: 'carol', name: 'Carol', subtitle: 'carol@cozy.cc', initials: 'C', role: 'viewer' }],
36
+ ['Action', { id: 'dave', name: 'Dave', subtitle: 'dave@cozy.cc', initials: 'D', action: { label: 'Verify', onClick: () => {} } }],
37
+ ['Pending', { id: 'eve', name: 'Eve', subtitle: 'eve@cozy.cc', initials: 'E', role: 'editor', canChangeRole: true, canRemove: true, isPending: true }],
38
+ ['Clickable group with its only role', { id: 'family', name: 'Family', subtitle: '2 members', isGroup: true, role: 'editor', roles: [{ value: 'editor', label: 'Editor' }], canChangeRole: true, canRemove: true, onClick: () => {} }],
39
+ ['Clickable group, other remove icon', { id: 'team', name: 'Team', subtitle: '3 members', isGroup: true, color: '#FF7F1B', role: 'editor', canRemove: true, removeIcon: Unlink, removeLabel: 'Unlink Team', onClick: () => {} }]
40
+ ]
41
+
42
+ ;
43
+
44
+ <DemoProvider>
45
+ <List>
46
+ {rows.map(([label, member]) => (
47
+ <MemberRow
48
+ key={member.id}
49
+ member={{ ...member, name: `${member.name} (${label})` }}
50
+ roles={roles}
51
+ onRoleChange={() => {}}
52
+ onRemove={() => {}}
53
+ />
54
+ ))}
55
+ </List>
56
+ </DemoProvider>
57
+ ```
@@ -0,0 +1,112 @@
1
+ import PropTypes from 'prop-types'
2
+ import React, { Fragment } from 'react'
3
+ import { useI18n, useExtendI18n } from 'twake-i18n'
4
+
5
+ import MemberAvatar from './MemberAvatar'
6
+ import MemberGroupAvatar from './MemberGroupAvatar'
7
+ import MemberRowActions from './MemberRowActions'
8
+ import { locales } from './locales'
9
+ import { memberPropType, rolesPropType } from './propTypes'
10
+ import Button from '../Buttons'
11
+ import Fade from '../Fade'
12
+ import ListItem from '../ListItem'
13
+ import ListItemIcon from '../ListItemIcon'
14
+ import ListItemSecondaryAction from '../ListItemSecondaryAction'
15
+ import ListItemText from '../ListItemText'
16
+ import Typography from '../Typography'
17
+ import useBreakpoints from '../providers/Breakpoints'
18
+
19
+ export const FADE_IN_DURATION = 600
20
+
21
+ /**
22
+ * Displays a member of a sharing: avatar, name, subtitle, role and remove button
23
+ *
24
+ * @param {object} props
25
+ * @param {object} props.member - Member to display, see `memberPropType`
26
+ * @param {object[]} props.roles - Roles a member can get, as `{ value, label }`
27
+ * @param {boolean} [props.isNew] - Whether the row fades in
28
+ * @param {Function} props.onRoleChange - Called with the member and its new role
29
+ * @param {Function} props.onRemove - Called with the member to remove
30
+ */
31
+ const MemberRow = ({ member, roles, isNew, onRoleChange, onRemove }) => {
32
+ useExtendI18n(locales)
33
+ const { t } = useI18n()
34
+ const { isMobile } = useBreakpoints()
35
+ const {
36
+ name,
37
+ subtitle,
38
+ avatarUrl,
39
+ initials,
40
+ color,
41
+ isGroup,
42
+ isOwner,
43
+ isCurrentUser,
44
+ action,
45
+ onClick
46
+ } = member
47
+
48
+ const ActionsWrapper = onClick ? ListItemSecondaryAction : Fragment
49
+ const actionsWrapperProps = onClick
50
+ ? { className: isMobile ? 'u-mr-1' : 'u-mr-2' }
51
+ : undefined
52
+
53
+ return (
54
+ <Fade in timeout={isNew ? FADE_IN_DURATION : 0}>
55
+ <ListItem
56
+ button={Boolean(onClick)}
57
+ onClick={onClick}
58
+ gutters={isMobile ? 'default' : 'double'}
59
+ size="small"
60
+ >
61
+ <ListItemIcon>
62
+ {isGroup ? (
63
+ <MemberGroupAvatar size="m" color={color} />
64
+ ) : (
65
+ <MemberAvatar size="m" initials={initials} avatarUrl={avatarUrl} />
66
+ )}
67
+ </ListItemIcon>
68
+ <ListItemText
69
+ primary={
70
+ <Typography variant="body1">
71
+ {isCurrentUser
72
+ ? t('MemberRow.you')
73
+ : name || t('MemberRow.defaultName')}
74
+ </Typography>
75
+ }
76
+ secondary={subtitle}
77
+ />
78
+ <ActionsWrapper {...actionsWrapperProps}>
79
+ {isOwner && (
80
+ <Typography variant="body2">{t('MemberRow.owner')}</Typography>
81
+ )}
82
+ {action && (
83
+ <Button
84
+ style={{ position: 'initial' }} // fix z-index bug on iOS when under a BottomDrawer due to relative position
85
+ variant="text"
86
+ onClick={action.onClick}
87
+ label={action.label}
88
+ />
89
+ )}
90
+ {!isOwner && !action && (
91
+ <MemberRowActions
92
+ member={member}
93
+ roles={roles}
94
+ onRoleChange={onRoleChange}
95
+ onRemove={onRemove}
96
+ />
97
+ )}
98
+ </ActionsWrapper>
99
+ </ListItem>
100
+ </Fade>
101
+ )
102
+ }
103
+
104
+ MemberRow.propTypes = {
105
+ member: memberPropType.isRequired,
106
+ roles: rolesPropType.isRequired,
107
+ isNew: PropTypes.bool,
108
+ onRoleChange: PropTypes.func.isRequired,
109
+ onRemove: PropTypes.func.isRequired
110
+ }
111
+
112
+ export default MemberRow
@@ -0,0 +1,8 @@
1
+ {
2
+ "MemberRow": {
3
+ "owner": "Eigentümer",
4
+ "you": "Du",
5
+ "remove": "Aus der Freigabe entfernen",
6
+ "defaultName": "Anonym"
7
+ }
8
+ }