Skip to content

Commit 04b7458

Browse files
shikharmohanmergify[bot]
authored andcommitted
feat(tasks-new): add assignee list (#1287)
* feat(tasks-new): add assignee list * feat(tasks-new): add assignee list, update tests * feat(tasks_new): use test.each * feat(tasks_new): update snapshot
1 parent fb84aee commit 04b7458

File tree

8 files changed

+439
-5
lines changed

8 files changed

+439
-5
lines changed

i18n/en-US.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ be.taskEditMenuItem = Modify task
390390
be.taskReject = Decline
391391
# Error message when rejecting a task fails
392392
be.taskRejectErrorMessage = An error has occurred while rejecting this task. Please refresh the page and try again.
393+
# Button name to hide task assignee list
394+
be.taskShowLessAssignees = Show Less
395+
# Button name to expand task assignee list, additionalAssigneeCount is the number of additional task assignees that can be shown.
396+
be.taskShowMoreAssignees = Show {additionalAssigneeCount} More
393397
# label for button that opens task popup
394398
be.tasks.addTask = Add Task
395399
# label for menu item that opens approval task popup

src/api/APIFactory.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ class APIFactory {
479479
* API for taskCollaborators
480480
*
481481
* @param {boolean} shouldDestroy - true if the factory should destroy before returning the call
482-
* @return {TasksAPI} TaskCollaboratorsAPI instance
482+
* @return {TaskCollaboratorsAPI} TaskCollaboratorsAPI instance
483483
*/
484484
getTaskCollaboratorsAPI(shouldDestroy: boolean): TaskCollaboratorsAPI {
485485
if (shouldDestroy) {

src/elements/common/messages.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,17 @@ const messages = defineMessages({
823823
description: 'Error message when we failed to load the collaborators when user tries to edit a task',
824824
defaultMessage: 'An error has occurred while loading collaborators for this task. Please try again.',
825825
},
826+
taskShowMoreAssignees: {
827+
id: 'be.taskShowMoreAssignees',
828+
description:
829+
'Button name to expand task assignee list, additionalAssigneeCount is the number of additional task assignees that can be shown.',
830+
defaultMessage: 'Show {additionalAssigneeCount} More',
831+
},
832+
taskShowLessAssignees: {
833+
id: 'be.taskShowLessAssignees',
834+
description: 'Button name to hide task assignee list',
835+
defaultMessage: 'Show Less',
836+
},
826837
completedAssignment: {
827838
id: 'be.completedAssignment',
828839
defaultMessage: 'Completed',
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// @flow strict
2+
import * as React from 'react';
3+
import { injectIntl, FormattedMessage } from 'react-intl';
4+
import type { InjectIntlProvidedProps } from 'react-intl';
5+
import uniqueId from 'lodash/uniqueId';
6+
import PlainButton from '../../../../components/plain-button';
7+
import ReadableTime from '../../../../components/time/ReadableTime';
8+
import messages from '../../../common/messages';
9+
import AvatarGroupAvatar from './AvatarGroupAvatar';
10+
import { TASK_NEW_APPROVED, TASK_NEW_REJECTED, TASK_NEW_COMPLETED, TASK_NEW_NOT_STARTED } from '../../../../constants';
11+
12+
import './AssigneeList.scss';
13+
14+
const DEFAULT_ASSIGNEES_SHOWN = 3;
15+
const TASKS_PAGE_SIZE = 20; // service does not return the page size to the client at the moment
16+
17+
type Props = {|
18+
getAvatarUrl: GetAvatarUrlCallback,
19+
initialAssigneeCount: number,
20+
onExpand: Function,
21+
users: TaskAssigneeCollection,
22+
|} & InjectIntlProvidedProps;
23+
24+
type State = {
25+
isCollapsed: boolean,
26+
};
27+
28+
const statusMessages = {
29+
[TASK_NEW_APPROVED]: messages.tasksFeedStatusApproved,
30+
[TASK_NEW_REJECTED]: messages.tasksFeedStatusRejected,
31+
[TASK_NEW_COMPLETED]: messages.tasksFeedStatusCompleted,
32+
[TASK_NEW_NOT_STARTED]: null,
33+
};
34+
35+
const Datestamp = ({ date }: { date: ISODate | Date }) => {
36+
return <ReadableTime timestamp={new Date(date).getTime()} alwaysShowTime relativeThreshold={0} />;
37+
};
38+
39+
const AvatarDetails = React.memo(({ user, status, completedAt, className }) => {
40+
const statusMessage = statusMessages[status] || null;
41+
return (
42+
<div className={className}>
43+
<div className="bcs-AssigneeList-detailsName">{user.name}</div>
44+
{statusMessage && completedAt && (
45+
<div className="bcs-AssigneeList-detailsStatus">
46+
<FormattedMessage {...statusMessage} values={{ dateTime: <Datestamp date={completedAt} /> }} />
47+
</div>
48+
)}
49+
</div>
50+
);
51+
});
52+
53+
class AssigneeList extends React.Component<Props, State> {
54+
state = {
55+
isCollapsed: true,
56+
};
57+
58+
static defaultProps = {
59+
initialAssigneeCount: DEFAULT_ASSIGNEES_SHOWN,
60+
users: {},
61+
};
62+
63+
listTitleId = uniqueId('assignee-list-title-');
64+
65+
showLessAssignees = () => {
66+
this.setState({
67+
isCollapsed: true,
68+
});
69+
};
70+
71+
showMoreAssignees = async () => {
72+
const { users, onExpand } = this.props;
73+
74+
if (users.next_marker) {
75+
try {
76+
await onExpand();
77+
} catch (err) {
78+
// do nothing
79+
}
80+
}
81+
82+
this.setState({
83+
isCollapsed: false,
84+
});
85+
};
86+
87+
render() {
88+
const { initialAssigneeCount, users, getAvatarUrl } = this.props;
89+
const { isCollapsed } = this.state;
90+
const { entries = [], next_marker } = users;
91+
const entryCount = entries.length;
92+
const hiddenAssigneeCount = Math.max(0, entryCount - initialAssigneeCount);
93+
const numVisibleAssignees = isCollapsed ? initialAssigneeCount : entryCount;
94+
const visibleUsers = entries
95+
.slice(0, numVisibleAssignees)
96+
.map(({ id, target, status, completed_at: completedAt }) => {
97+
return (
98+
<li key={id} className="bcs-AssigneeList-listItem" data-testid="assignee-list-item">
99+
<AvatarGroupAvatar
100+
status={status}
101+
className="bcs-AssigneeList-listItemAvatar"
102+
user={target}
103+
getAvatarUrl={getAvatarUrl}
104+
/>
105+
<AvatarDetails
106+
className="bcs-AssigneeList-listItemDetails"
107+
user={target}
108+
status={status}
109+
completedAt={completedAt}
110+
/>
111+
</li>
112+
);
113+
});
114+
115+
const maxAdditionalAssignees = TASKS_PAGE_SIZE - initialAssigneeCount;
116+
const hasMoreAssigneesThanPageSize = hiddenAssigneeCount > maxAdditionalAssignees || next_marker;
117+
const additionalAssigneeCount = hasMoreAssigneesThanPageSize
118+
? `${maxAdditionalAssignees}+`
119+
: `${hiddenAssigneeCount}`;
120+
121+
return (
122+
<div>
123+
<ul
124+
className="bcs-AssigneeList-list"
125+
data-testid="task-assignee-list"
126+
arial-labelledby={this.listTitleId}
127+
>
128+
{visibleUsers}
129+
</ul>
130+
{isCollapsed && hiddenAssigneeCount > 0 && (
131+
<span>
132+
<PlainButton
133+
data-resin-target="showmorebtn"
134+
data-testid="show-more-assignees"
135+
onClick={this.showMoreAssignees}
136+
className="lnk bcs-AssigneeList-expandBtn"
137+
>
138+
<FormattedMessage
139+
{...messages.taskShowMoreAssignees}
140+
values={{ additionalAssigneeCount }}
141+
/>
142+
</PlainButton>
143+
</span>
144+
)}
145+
{!isCollapsed && (
146+
<span>
147+
<PlainButton
148+
data-resin-target="showlessbtn"
149+
data-testid="show-less-assignees"
150+
onClick={this.showLessAssignees}
151+
className="lnk bcs-AssigneeList-expandBtn"
152+
>
153+
<FormattedMessage {...messages.taskShowLessAssignees} />
154+
</PlainButton>
155+
</span>
156+
)}
157+
</div>
158+
);
159+
}
160+
}
161+
162+
export default injectIntl(AssigneeList);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
@import '../../../common/variables';
2+
@import './mixins';
3+
4+
$avatar-size: 28px;
5+
6+
.bcs-AssigneeList-list {
7+
margin: 0;
8+
padding: 0;
9+
}
10+
11+
.bcs-AssigneeList-listItem {
12+
align-items: center;
13+
display: flex;
14+
margin: 6px 0 0 0;
15+
padding: 3px 0;
16+
17+
.bcs-AssigneeList-detailsStatus {
18+
color: $bdl-neutral-02;
19+
font-size: 12px;
20+
}
21+
}
22+
23+
.bcs-AssigneeList-listItemAvatar {
24+
margin-right: 10px;
25+
26+
.bcs-AvatarGroup-avatar {
27+
height: $avatar-size;
28+
width: $avatar-size;
29+
}
30+
}
31+
32+
.bcs-AssigneeList-listItemDetails {
33+
line-height: 16px;
34+
}
35+
36+
.bcs-AssigneeList-listItemStatus {
37+
color: $bdl-neutral-02;
38+
}
39+
40+
.lnk.bcs-AssigneeList-expandBtn {
41+
margin-left: $avatar-size + 10px;
42+
margin-top: 5px;
43+
}
44+
45+
.bcs-AssigneeList-statusIcon {
46+
@include avatar-badge-icon;
47+
}
48+
49+
.bcs-AvatarGroup-avatarContainer {
50+
display: inline-block;
51+
height: $avatar-size;
52+
position: relative;
53+
width: $avatar-size;
54+
}

src/elements/content-sidebar/activity-feed/task-new/Task.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Comment from '../comment';
2525
import TaskActions from './TaskActions';
2626
import TaskDueDate from './TaskDueDate';
2727
import TaskStatus from './TaskStatus';
28-
import AvatarGroup from './AvatarGroup';
28+
import AssigneeList from './AssigneeList';
2929
import TaskModal from '../../TaskModal';
3030
import { withFeatureConsumer, getFeatureConfig } from '../../../common/feature-checking';
3131

@@ -257,7 +257,12 @@ class Task extends React.Component<Props, State> {
257257
<TaskStatus status={status} />
258258
</div>
259259
<div className="bcs-task-content">
260-
<AvatarGroup getAvatarUrl={getAvatarUrl} maxAvatars={3} users={assigned_to} />
260+
<AssigneeList
261+
onExpand={this.fetchTaskCollaborators}
262+
getAvatarUrl={getAvatarUrl}
263+
initialAssigneeCount={3}
264+
users={assigned_to}
265+
/>
261266
</div>
262267
<div className="bcs-task-content">
263268
{currentUserAssignment && shouldShowActions && (

0 commit comments

Comments
 (0)