Skip to content

Commit 4645733

Browse files
authored
feat(unified-share-modal): add remove collaborators (#4070)
* feat(unified-share-modal): add remove collaborators * feat(unified-share-modal): back to fragment to not cause unexpected * feat(unified-share-modal): fix flow errors * feat(unified-share-modal): add an example of UnifiedShareModal * feat(unified-share-modal): address comments from PR review * feat(unified-share-modal): replace queryBy with getBy
1 parent 1555ede commit 4645733

21 files changed

+1370
-33
lines changed

i18n/en-US.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,6 +1952,10 @@ boxui.unifiedShare.previewerUploaderLevelDescription = Upload and preview
19521952
boxui.unifiedShare.previewerUploaderLevelText = Previewer Uploader
19531953
# Tooltip description to explain recommendation for sharing tooltip
19541954
boxui.unifiedShare.recommendedSharingTooltipCalloutText = Based on your usage, we think {fullName} would be interested in this file.
1955+
# Description for confirmation modal to remove a collaborator
1956+
boxui.unifiedShare.removeCollaboratorConfirmationDescription = Are you sure you want to remove {name} as a collaborator?
1957+
# Label for confirmation modal to remove a collaborator (title-case)
1958+
boxui.unifiedShare.removeCollaboratorConfirmationTitle = Remove Collaborator
19551959
# Description for confirmation modal to remove a shared link
19561960
boxui.unifiedShare.removeLinkConfirmationDescription = This will permanently remove the shared link. If this item is embedded on other sites it will also become inaccessible. Any custom properties, settings and expirations will be removed as well. Do you want to continue?
19571961
# Label for confirmation modal to remove a shared link (title-case)

src/features/collaborator-avatars/CollaboratorList.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import './CollaboratorList.scss';
2121
const MAX_COLLABORATOR_LIST_SIZE = 90;
2222

2323
type Props = {
24+
canRemoveCollaborators?: boolean,
2425
collaborators: Array<collaboratorType>,
2526
doneButtonProps?: Object,
2627
item: ItemType,
2728
maxCollaboratorListSize: number,
2829
onDoneClick: Function,
30+
onRemoveCollaboratorClick?: (collaborator: collaboratorType) => void,
2931
trackingProps: collaboratorListTrackingType,
3032
};
3133

@@ -47,7 +49,14 @@ class CollaboratorList extends React.Component<Props> {
4749
}
4850

4951
render() {
50-
const { collaborators, onDoneClick, maxCollaboratorListSize, trackingProps } = this.props;
52+
const {
53+
canRemoveCollaborators,
54+
collaborators,
55+
onDoneClick,
56+
maxCollaboratorListSize,
57+
onRemoveCollaboratorClick,
58+
trackingProps,
59+
} = this.props;
5160
const { usernameProps, emailProps, manageLinkProps, viewAdditionalProps, doneButtonProps } = trackingProps;
5261
const manageAllBtn = (
5362
<span className="manage-all-btn">
@@ -73,6 +82,8 @@ class CollaboratorList extends React.Component<Props> {
7382
usernameProps,
7483
emailProps,
7584
}}
85+
canRemoveCollaborators={canRemoveCollaborators}
86+
onRemoveCollaborator={onRemoveCollaboratorClick}
7687
/>
7788
);
7889
})}

src/features/collaborator-avatars/CollaboratorList.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
@import '../../styles/variables';
22

33
.usm-collaborator-list {
4+
display: flex;
5+
flex-direction: column;
6+
47
.collaborator-list {
58
height: 210px;
69
margin: 0;

src/features/collaborator-avatars/CollaboratorListItem.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,26 @@ import classnames from 'classnames';
44
import { FormattedMessage } from 'react-intl';
55

66
import { Link } from '../../components/link';
7-
7+
import PlainButton from '../../components/plain-button';
8+
import Tooltip from '../../components/tooltip';
9+
import IconClose from '../../icon/fill/X16';
810
import { COLLAB_GROUP_TYPE, COLLAB_PENDING_TYPE } from './constants';
911
import messages from './messages';
12+
import commonMessages from '../../elements/common/messages';
1013
import CollaboratorAvatarItem from './CollaboratorAvatarItem';
14+
import type { collaboratorType } from '../unified-share-modal/flowTypes';
1115
import './CollaboratorListItem.scss';
1216

1317
type Props = {
18+
canRemoveCollaborators?: boolean,
1419
collaborator: Object,
1520
index: number,
21+
onRemoveCollaborator?: (collaborator: collaboratorType) => void,
1622
trackingProps: { emailProps: ?Object, usernameProps: ?Object },
1723
};
1824

1925
const CollaboratorListItem = (props: Props) => {
20-
const { index, trackingProps } = props;
26+
const { index, trackingProps, canRemoveCollaborators = false, onRemoveCollaborator } = props;
2127
const { usernameProps, emailProps } = trackingProps;
2228
const {
2329
email,
@@ -30,6 +36,7 @@ const CollaboratorListItem = (props: Props) => {
3036
profileURL,
3137
translatedRole,
3238
userID,
39+
isRemovable = false,
3340
} = props.collaborator;
3441

3542
const userOrGroupNameContent =
@@ -52,6 +59,12 @@ const CollaboratorListItem = (props: Props) => {
5259
</div>
5360
) : null;
5461

62+
const roleNodeContent = (
63+
<div className="role">
64+
{type === COLLAB_PENDING_TYPE ? <FormattedMessage {...messages.pendingCollabText} /> : translatedRole}
65+
</div>
66+
);
67+
5568
return (
5669
<li>
5770
<div className="collaborator-list-item">
@@ -71,13 +84,24 @@ const CollaboratorListItem = (props: Props) => {
7184
name={name}
7285
/>
7386
</div>
74-
<div className="role">
75-
{type === COLLAB_PENDING_TYPE ? (
76-
<FormattedMessage {...messages.pendingCollabText} />
77-
) : (
78-
translatedRole
79-
)}
80-
</div>
87+
{canRemoveCollaborators ? (
88+
<div className="user-actions">
89+
{roleNodeContent}
90+
{isRemovable && (
91+
<Tooltip isTabbable={false} text={<FormattedMessage {...commonMessages.remove} />}>
92+
<PlainButton
93+
className="remove-button"
94+
onClick={() => onRemoveCollaborator?.(props.collaborator)}
95+
type="button"
96+
>
97+
<IconClose color="##6f6f6f" height={16} width={16} />
98+
</PlainButton>
99+
</Tooltip>
100+
)}
101+
</div>
102+
) : (
103+
roleNodeContent
104+
)}
81105
</div>
82106
</li>
83107
);

src/features/collaborator-avatars/CollaboratorListItem.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,33 @@
5454
text-overflow: ellipsis;
5555
}
5656

57+
.user-actions {
58+
display: flex;
59+
gap: 8px;
60+
align-items: center;
61+
margin-left: auto;
62+
}
63+
5764
.role {
5865
color: $bdl-gray-65;
5966
line-height: 32px;
6067
}
68+
69+
.remove-button {
70+
display: flex;
71+
align-items: center;
72+
justify-content: center;
73+
width: $bdl-btn-height;
74+
height: $bdl-btn-height;
75+
background: none;
76+
border: 0;
77+
cursor: pointer;
78+
79+
&:hover {
80+
background: $bdl-gray-05;
81+
border-radius: $bdl-border-radius-size;
82+
}
83+
}
6184
}
6285

6386
.collaborator-list-item.more {

src/features/collaborator-avatars/__tests__/CollaboratorList.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,16 @@ describe('features/collaborator-avatars/CollaboratorList', () => {
6767
});
6868
expect(wrapper).toMatchSnapshot();
6969
});
70+
71+
test('should render component when canRemoveCollaborators prop is true', () => {
72+
const onRemoveCollaboratorClickMock = jest.fn();
73+
const wrapper = getWrapper({
74+
canRemoveCollaborators: true,
75+
onRemoveCollaboratorClick: onRemoveCollaboratorClickMock,
76+
});
77+
78+
expect(wrapper.find('CollaboratorListItem')).toHaveLength(collaborators.length);
79+
expect(wrapper).toMatchSnapshot();
80+
});
7081
});
7182
});

src/features/collaborator-avatars/__tests__/CollaboratorListItem.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,56 @@ describe('features/collaborator-avatars/CollaboratorListItem', () => {
6969

7070
expect(wrapper).toMatchSnapshot();
7171
});
72+
73+
test('should render component when canRemoveCollaborators prop is true and collaborator is not removable', () => {
74+
const wrapper = getWrapper({ canRemoveCollaborators: true, collaborator });
75+
76+
expect(wrapper.find('.role').exists()).toBe(true);
77+
expect(wrapper.find('PlainButton')).toHaveLength(0);
78+
expect(wrapper).toMatchSnapshot();
79+
});
80+
81+
test('should render component when canRemoveCollaborators prop is true and collaborator is removable', () => {
82+
const wrapper = getWrapper({
83+
canRemoveCollaborators: true,
84+
collaborator: { ...collaborator, isRemovable: true },
85+
});
86+
87+
expect(wrapper.find('.role').exists()).toBe(true);
88+
expect(wrapper.find('PlainButton').exists()).toBe(true);
89+
expect(wrapper.find('Tooltip').exists()).toBe(true);
90+
expect(wrapper).toMatchSnapshot();
91+
});
92+
93+
test('should call onRemoveCollaborator when onRemoveCollaborator prop is passed', () => {
94+
const onRemoveCollaboratorMock = jest.fn();
95+
const wrapper = getWrapper({
96+
canRemoveCollaborators: true,
97+
onRemoveCollaborator: onRemoveCollaboratorMock,
98+
collaborator: { ...collaborator, isRemovable: true },
99+
});
100+
101+
const removeButton = wrapper.find('PlainButton');
102+
removeButton.simulate('click');
103+
104+
expect(onRemoveCollaboratorMock).toHaveBeenCalledWith({ ...collaborator, isRemovable: true });
105+
expect(wrapper).toMatchSnapshot();
106+
});
107+
108+
test('should not call onRemoveCollaborator when onRemoveCollaborator prop is undefined', () => {
109+
const onRemoveCollaboratorMock = jest.fn();
110+
111+
const wrapper = getWrapper({
112+
canRemoveCollaborators: true,
113+
onRemoveCollaborator: undefined,
114+
collaborator: { ...collaborator, isRemovable: true },
115+
});
116+
117+
const removeButton = wrapper.find('PlainButton');
118+
removeButton.simulate('click');
119+
120+
expect(onRemoveCollaboratorMock).not.toHaveBeenCalled();
121+
expect(wrapper).toMatchSnapshot();
122+
});
72123
});
73124
});

src/features/collaborator-avatars/__tests__/__snapshots__/CollaboratorList.test.js.snap

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,115 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`features/collaborator-avatars/CollaboratorList render() should render component when canRemoveCollaborators prop is true 1`] = `
4+
<div
5+
className="usm-collaborator-list"
6+
>
7+
<div
8+
className="manage-all-btn-container"
9+
>
10+
<Link
11+
className=""
12+
href="/file/111/collaborators/"
13+
rel="noopener"
14+
target="_blank"
15+
>
16+
<span
17+
className="manage-all-btn"
18+
>
19+
<FormattedMessage
20+
defaultMessage="Manage All"
21+
id="boxui.collaboratorAvatars.manageAllLinkText"
22+
/>
23+
</span>
24+
</Link>
25+
</div>
26+
<ul
27+
className="be collaborator-list"
28+
>
29+
<CollaboratorListItem
30+
canRemoveCollaborators={true}
31+
collaborator={
32+
{
33+
"collabID": 1,
34+
"email": "[email protected]",
35+
"hasCustomAvatar": false,
36+
"name": "test a",
37+
"type": "user",
38+
"userID": 1,
39+
}
40+
}
41+
index={0}
42+
key="1-user"
43+
onRemoveCollaborator={[MockFunction]}
44+
trackingProps={
45+
{
46+
"emailProps": undefined,
47+
"usernameProps": undefined,
48+
}
49+
}
50+
/>
51+
<CollaboratorListItem
52+
canRemoveCollaborators={true}
53+
collaborator={
54+
{
55+
"collabID": 2,
56+
"email": "[email protected]",
57+
"name": "test b",
58+
"type": "user",
59+
"userID": 2,
60+
}
61+
}
62+
index={1}
63+
key="2-user"
64+
onRemoveCollaborator={[MockFunction]}
65+
trackingProps={
66+
{
67+
"emailProps": undefined,
68+
"usernameProps": undefined,
69+
}
70+
}
71+
/>
72+
<CollaboratorListItem
73+
canRemoveCollaborators={true}
74+
collaborator={
75+
{
76+
"collabID": 3,
77+
"email": "[email protected]",
78+
"hasCustomAvatar": true,
79+
"imageUrl": "https://foo.bar",
80+
"name": "test c",
81+
"profileUrl": "http://foo.bar.profile",
82+
"type": "user",
83+
"userID": 3,
84+
}
85+
}
86+
index={2}
87+
key="3-user"
88+
onRemoveCollaborator={[MockFunction]}
89+
trackingProps={
90+
{
91+
"emailProps": undefined,
92+
"usernameProps": undefined,
93+
}
94+
}
95+
/>
96+
</ul>
97+
<ModalActions>
98+
<Button
99+
className="btn-done"
100+
isLoading={false}
101+
showRadar={false}
102+
type="button"
103+
>
104+
<FormattedMessage
105+
defaultMessage="Done"
106+
id="boxui.core.done"
107+
/>
108+
</Button>
109+
</ModalActions>
110+
</div>
111+
`;
112+
3113
exports[`features/collaborator-avatars/CollaboratorList render() should render default component 1`] = `
4114
<div
5115
className="usm-collaborator-list"

0 commit comments

Comments
 (0)