Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit 20ebd0f

Browse files
authored
Merge pull request #2117 from atom/vy/review-comments-grouping
Group resolved comments
2 parents e0b5fb4 + 200dcc2 commit 20ebd0f

File tree

5 files changed

+97
-30
lines changed

5 files changed

+97
-30
lines changed

lib/controllers/reviews-controller.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import unresolveReviewThreadMutation from '../mutations/unresolve-review-thread'
1414
import IssueishDetailItem from '../items/issueish-detail-item';
1515
import {addEvent} from '../reporter-proxy';
1616

17-
// Milliseconds to leave scrollToThreadID non-null before reverting.
17+
// Milliseconds to update highlightedThreadIDs
1818
const FLASH_DELAY = 1500;
1919

2020
export class BareReviewsController extends React.Component {
@@ -81,13 +81,14 @@ export class BareReviewsController extends React.Component {
8181
threadIDsOpen: new Set(
8282
this.props.initThreadID ? [this.props.initThreadID] : [],
8383
),
84+
highlightedThreadIDs: new Set(),
8485
};
8586
}
8687

8788
componentDidMount() {
8889
const {scrollToThreadID} = this.state;
8990
if (scrollToThreadID) {
90-
setTimeout(() => this.setState({scrollToThreadID: null}), FLASH_DELAY);
91+
this.highlightThread(scrollToThreadID);
9192
}
9293
}
9394

@@ -96,9 +97,8 @@ export class BareReviewsController extends React.Component {
9697
if (initThreadID && initThreadID !== prevProps.initThreadID) {
9798
this.setState(prev => {
9899
prev.threadIDsOpen.add(initThreadID);
100+
this.highlightThread(initThreadID);
99101
return {commentSectionOpen: true, scrollToThreadID: initThreadID};
100-
}, () => {
101-
setTimeout(() => this.setState({scrollToThreadID: null}), FLASH_DELAY);
102102
});
103103
}
104104
}
@@ -126,6 +126,7 @@ export class BareReviewsController extends React.Component {
126126
summarySectionOpen={this.state.summarySectionOpen}
127127
commentSectionOpen={this.state.commentSectionOpen}
128128
threadIDsOpen={this.state.threadIDsOpen}
129+
highlightedThreadIDs={this.state.highlightedThreadIDs}
129130
scrollToThreadID={this.state.scrollToThreadID}
130131

131132
moreContext={this.moreContext}
@@ -231,6 +232,21 @@ export class BareReviewsController extends React.Component {
231232
return {};
232233
}, resolve));
233234

235+
highlightThread = threadID => {
236+
this.setState(state => {
237+
state.highlightedThreadIDs.add(threadID);
238+
return {};
239+
}, () => {
240+
setTimeout(() => this.setState(state => {
241+
state.highlightedThreadIDs.delete(threadID);
242+
if (state.scrollToThreadID === threadID) {
243+
return {scrollToThreadID: null};
244+
}
245+
return {};
246+
}), FLASH_DELAY);
247+
});
248+
}
249+
234250
resolveThread = async thread => {
235251
if (thread.viewerCanResolve) {
236252
// optimistically hide the thread to avoid jankiness;
@@ -242,6 +258,7 @@ export class BareReviewsController extends React.Component {
242258
viewerID: this.props.viewer.id,
243259
viewerLogin: this.props.viewer.login,
244260
});
261+
this.highlightThread(thread.id);
245262
addEvent('resolve-comment-thread', {package: 'github'});
246263
} catch (err) {
247264
this.showThreadID(thread.id);
@@ -258,6 +275,7 @@ export class BareReviewsController extends React.Component {
258275
viewerID: this.props.viewer.id,
259276
viewerLogin: this.props.viewer.login,
260277
});
278+
this.highlightThread(thread.id);
261279
addEvent('unresolve-comment-thread', {package: 'github'});
262280
} catch (err) {
263281
this.props.reportMutationErrors('Unable to unresolve the comment thread', err);

lib/views/reviews-view.js

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export default class ReviewsView extends React.Component {
5454
threadIDsOpen: PropTypes.shape({
5555
has: PropTypes.func.isRequired,
5656
}),
57+
highlightedThreadIDs: PropTypes.shape({
58+
has: PropTypes.func.isRequired,
59+
}),
5760
postingToThreadID: PropTypes.string,
5861
scrollToThreadID: PropTypes.string,
5962
// Structure: Map< relativePath: String, {
@@ -110,26 +113,14 @@ export default class ReviewsView extends React.Component {
110113
componentDidMount() {
111114
const {scrollToThreadID} = this.props;
112115
if (scrollToThreadID) {
113-
const threadHolder = this.threadHolders.get(scrollToThreadID);
114-
if (threadHolder) {
115-
threadHolder.map(element => {
116-
element.scrollIntoViewIfNeeded();
117-
return null; // shh, eslint
118-
});
119-
}
116+
this.scrollToThread(scrollToThreadID);
120117
}
121118
}
122119

123120
componentDidUpdate(prevProps) {
124121
const {scrollToThreadID} = this.props;
125122
if (scrollToThreadID && scrollToThreadID !== prevProps.scrollToThreadID) {
126-
const threadHolder = this.threadHolders.get(scrollToThreadID);
127-
if (threadHolder) {
128-
threadHolder.map(element => {
129-
element.scrollIntoViewIfNeeded();
130-
return null; // shh, eslint
131-
});
132-
}
123+
this.scrollToThread(scrollToThreadID);
133124
}
134125
}
135126

@@ -309,7 +300,8 @@ export default class ReviewsView extends React.Component {
309300
return null;
310301
}
311302

312-
const resolvedThreads = commentThreads.filter(pair => pair.thread.isResolved).length;
303+
const resolvedThreads = commentThreads.filter(pair => pair.thread.isResolved);
304+
const unresolvedThreads = commentThreads.filter(pair => !pair.thread.isResolved);
313305

314306
const toggleComments = evt => {
315307
evt.preventDefault();
@@ -330,16 +322,28 @@ export default class ReviewsView extends React.Component {
330322
<span className="github-Reviews-progress">
331323
<span className="github-Reviews-count">
332324
Resolved
333-
{' '}<span className="github-Reviews-countNr">{resolvedThreads}</span>{' '}
325+
{' '}<span className="github-Reviews-countNr">{resolvedThreads.length}</span>{' '}
334326
of
335-
{' '}<span className="github-Reviews-countNr">{commentThreads.length}</span>
327+
{' '}<span className="github-Reviews-countNr">{resolvedThreads.length + unresolvedThreads.length}</span>
336328
</span>
337-
<progress className="github-Reviews-progessBar" value={resolvedThreads} max={commentThreads.length} />
329+
<progress
330+
className="github-Reviews-progessBar" value={resolvedThreads.length}
331+
max={resolvedThreads.length + unresolvedThreads.length}
332+
/>
338333
</span>
339334
</summary>
340-
<main className="github-Reviews-container">
341-
{commentThreads.map(this.renderReviewCommentThread)}
342-
</main>
335+
336+
{unresolvedThreads.length > 0 && <main className="github-Reviews-container">
337+
{unresolvedThreads.map(this.renderReviewCommentThread)}
338+
</main>}
339+
{resolvedThreads.length > 0 && <details className="github-Reviews-section resolved-comments" open>
340+
<summary className="github-Reviews-header">
341+
<span className="github-Reviews-title">Resolved</span>
342+
</summary>
343+
<main className="github-Reviews-container">
344+
{resolvedThreads.map(this.renderReviewCommentThread)}
345+
</main>
346+
</details>}
343347

344348
</details>
345349
);
@@ -372,7 +376,7 @@ export default class ReviewsView extends React.Component {
372376
const openDiffClasses = cx('icon-diff', ...navButtonClasses);
373377

374378
const isOpen = this.props.threadIDsOpen.has(thread.id);
375-
const isHighlighted = this.props.scrollToThreadID === thread.id;
379+
const isHighlighted = this.props.highlightedThreadIDs.has(thread.id);
376380
const toggle = evt => {
377381
evt.preventDefault();
378382
evt.stopPropagation();
@@ -495,7 +499,7 @@ export default class ReviewsView extends React.Component {
495499
<button
496500
className="github-Review-resolveButton btn icon icon-check"
497501
title="Unresolve conversation"
498-
onClick={() => this.props.unresolveThread(thread)}>
502+
onClick={() => this.resolveUnresolveThread(thread)}>
499503
Unresolve conversation
500504
</button>
501505
);
@@ -504,7 +508,7 @@ export default class ReviewsView extends React.Component {
504508
<button
505509
className="github-Review-resolveButton btn icon icon-check"
506510
title="Resolve conversation"
507-
onClick={() => this.props.resolveThread(thread)}>
511+
onClick={() => this.resolveUnresolveThread(thread)}>
508512
Resolve conversation
509513
</button>
510514
);
@@ -646,4 +650,23 @@ export default class ReviewsView extends React.Component {
646650

647651
return {lineNumber, positionText};
648652
}
653+
654+
/* istanbul ignore next */
655+
scrollToThread(threadID) {
656+
const threadHolder = this.threadHolders.get(threadID);
657+
if (threadHolder) {
658+
threadHolder.map(element => {
659+
element.scrollIntoViewIfNeeded();
660+
return null; // shh, eslint
661+
});
662+
}
663+
}
664+
665+
async resolveUnresolveThread(thread) {
666+
if (thread.isResolved) {
667+
await this.props.unresolveThread(thread);
668+
} else {
669+
await this.props.resolveThread(thread);
670+
}
671+
}
649672
}

styles/review.less

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@
7474

7575
&-section {
7676
border-bottom: 1px solid @base-border-color;
77+
78+
&.resolved-comments {
79+
border-bottom: 0;
80+
padding-left: @component-padding;
81+
.github-Reviews {
82+
&-title {
83+
color: @text-color-subtle;
84+
}
85+
&-header {
86+
color: @text-color-subtle;
87+
padding-top: @component-padding * 0.5;
88+
}
89+
}
90+
}
7791
}
7892

7993
&-header {

test/controllers/reviews-controller.test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,22 @@ describe('ReviewsController', function() {
9595
assert.strictEqual(opWrapper.find(ReviewsView).prop('extra'), extra);
9696
});
9797

98-
it('scrolls to a specific thread on mount', function() {
98+
it('scrolls to and highlight a specific thread on mount', function() {
9999
clock = sinon.useFakeTimers();
100100
const wrapper = shallow(buildApp({initThreadID: 'thread0'}));
101101
let opWrapper = wrapper.find(PullRequestCheckoutController).renderProp('children')(noop);
102102

103103
assert.include(opWrapper.find(ReviewsView).prop('threadIDsOpen'), 'thread0');
104+
assert.include(opWrapper.find(ReviewsView).prop('highlightedThreadIDs'), 'thread0');
104105
assert.strictEqual(opWrapper.find(ReviewsView).prop('scrollToThreadID'), 'thread0');
105106

106107
clock.tick(2000);
107108
opWrapper = wrapper.find(PullRequestCheckoutController).renderProp('children')(noop);
109+
assert.notInclude(opWrapper.find(ReviewsView).prop('highlightedThreadIDs'), 'thread0');
108110
assert.isNull(opWrapper.find(ReviewsView).prop('scrollToThreadID'));
109111
});
110112

111-
it('scrolls to a specific thread on update', function() {
113+
it('scrolls to and highlight a specific thread on update', function() {
112114
clock = sinon.useFakeTimers();
113115
const wrapper = shallow(buildApp());
114116
let opWrapper = wrapper.find(PullRequestCheckoutController).renderProp('children')(noop);
@@ -118,11 +120,13 @@ describe('ReviewsController', function() {
118120
opWrapper = wrapper.find(PullRequestCheckoutController).renderProp('children')(noop);
119121

120122
assert.include(opWrapper.find(ReviewsView).prop('threadIDsOpen'), 'hang-by-a-thread');
123+
assert.include(opWrapper.find(ReviewsView).prop('highlightedThreadIDs'), 'hang-by-a-thread');
121124
assert.isTrue(opWrapper.find(ReviewsView).prop('commentSectionOpen'));
122125
assert.strictEqual(opWrapper.find(ReviewsView).prop('scrollToThreadID'), 'hang-by-a-thread');
123126

124127
clock.tick(2000);
125128
opWrapper = wrapper.find(PullRequestCheckoutController).renderProp('children')(noop);
129+
assert.notInclude(opWrapper.find(ReviewsView).prop('highlightedThreadIDs'), 'hang-by-a-thread');
126130
assert.isNull(opWrapper.find(ReviewsView).prop('scrollToThreadID'));
127131
});
128132

test/views/reviews-view.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('ReviewsView', function() {
3636
summarySectionOpen: true,
3737
commentSectionOpen: true,
3838
threadIDsOpen: new Set(),
39+
highlightedThreadIDs: new Set(),
3940

4041
number: 100,
4142
repo: 'github',
@@ -318,6 +319,13 @@ describe('ReviewsView', function() {
318319
assert.strictEqual(comment.find('em').text(), 'This comment was hidden');
319320
});
320321

322+
it('groups comments by their resolved status', function() {
323+
const unresolvedThreads = wrapper.find('.github-Reviews-section.comments').find('.github-Review');
324+
const resolvedThreads = wrapper.find('.github-Reviews-section.resolved-comments').find('.github-Review');
325+
assert.lengthOf(resolvedThreads, 1);
326+
assert.lengthOf(unresolvedThreads, 3);
327+
});
328+
321329
describe('indication that a comment has been edited', function() {
322330
it('indicates that a review summary comment has been edited', function() {
323331
const summary = wrapper.find('.github-ReviewSummary').at(0);

0 commit comments

Comments
 (0)