diff --git a/src/components/app/filter-dropdown.jsx b/src/components/app/filter-dropdown.jsx index 3d608c1d..fde064de 100644 --- a/src/components/app/filter-dropdown.jsx +++ b/src/components/app/filter-dropdown.jsx @@ -243,6 +243,21 @@ class FilterDropdown extends Component { return (); }; + renderReviews = () => { + const filters = getFilters(); + const {reviews} = filters.getState(); + + const items = ['my-reviews', 'reviews-under-my-pr', 'my-meta-reviews', 'others'].map((review) => { + return { + text: review, + isSelected: reviews.indexOf(review) >= 0, + toggleHref: filters.toggleReviews(review).url(), + }; + }); + + return (); + }; + render() { const {milestones, labels} = this.props; @@ -294,6 +309,9 @@ class FilterDropdown extends Component { {this.renderTypes()} + + {this.renderReviews()} + ); diff --git a/src/components/repo-kanban.jsx b/src/components/repo-kanban.jsx index 08162074..5783e032 100644 --- a/src/components/repo-kanban.jsx +++ b/src/components/repo-kanban.jsx @@ -6,6 +6,7 @@ import {ListUnorderedIcon} from 'react-octicons'; import {getFilters} from '../route-utils'; import {filterKanbanLabels} from '../lib/columns'; import {UNCATEGORIZED_NAME} from '../helpers'; +import Client from '../github-client'; import IssueStore from '../issue-store'; import {filterCards} from '../issue-store'; import SettingsStore from '../settings-store'; @@ -91,11 +92,28 @@ function ReviewColumn(props) { } class KanbanRepo extends Component { + state = {login: null}; + componentDidMount() { const repoTitle = titlecaps(this.props.repoInfos[0].repoName); document.title = `${repoTitle} Kanban Board`; + Client.on('changeToken', this.onChangeToken); + this.onChangeToken(); + } + + componentWillUnmount() { + Client.off('changeToken', this.onChangeToken); } + onChangeToken = () => { + CurrentUserStore.fetchUser() + .then((info) => { + this.setState({login: info.login}); + }).catch(() => { + this.setState({login: null}); + }); + }; + render() { const {columnData, cards, repoInfos} = this.props; @@ -107,6 +125,7 @@ class KanbanRepo extends Component { comment.repoOwner = card.repoOwner; comment.repoName = card.repoName; comment.number = card.number; + comment.prAuthor = card.issue.user ? card.issue.user.login : null; return comment; }); } else { @@ -115,7 +134,7 @@ class KanbanRepo extends Component { })); // filter and sort review comments - const sortedReviews = FilterStore.filterAndSortReviews(reviews); + const sortedReviews = FilterStore.filterAndSortReviews(reviews, this.state.login); // Get the primary repoOwner and repoName const [primaryRepo] = repoInfos; diff --git a/src/filter-store.js b/src/filter-store.js index d0c877a9..5f8c5576 100644 --- a/src/filter-store.js +++ b/src/filter-store.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import {EventEmitter} from 'events'; +import {getFilters, filterReviewsByFilter} from './route-utils'; import SettingsStore from './settings-store'; // import {filterCards} from './issue-store'; @@ -171,10 +172,10 @@ class Store extends EventEmitter { return sortedCards; } - filterAndSortReviews(reviews) { - // Sort the reviews by `lastEditedAt` and then - // by `createdAt` (if `lastEditedAt` does not exist) - let sortedReviews = reviews.map(review => { + filterAndSortReviews(reviews, login) { + const filter = getFilters(); + const filteredReviews = filterReviewsByFilter(reviews, filter, login); + const sortedReviews = filteredReviews.map(review => { review.parsedUpdatedAt = Date.parse(review.updatedAt); return review; }).sort((a, b) => { diff --git a/src/github-client.js b/src/github-client.js index 1dbaafd5..1e597e0c 100644 --- a/src/github-client.js +++ b/src/github-client.js @@ -169,6 +169,7 @@ class Client extends EventEmitter { constructor() { super(); this.LOW_RATE_LIMIT = 60; + this.setMaxListeners(20); } // Used for checking if we should retreive ALL Issues or just open ones canCacheLots() { return this.hasCredentials() /*&& !!cacheHandler._db*/; } diff --git a/src/route-utils.js b/src/route-utils.js index f1fb5c20..f0ce9ec7 100644 --- a/src/route-utils.js +++ b/src/route-utils.js @@ -47,7 +47,7 @@ function addParams(options, key, vals, defaults) { // Generate a URL based on various filters and whatnot // `/r/:repoStr(/m/:milestonesStr)(/t/:tagsStr)(/u/:user)(/x/:columnRegExp)/:name(/:startShas)(/:endShas) -export function buildRoute(name, {repoInfos, milestoneTitles, tagNames, columnLabels, userName, columnRegExp, routeSegmentName, states, types}={}, ...otherFields) { +export function buildRoute(name, {repoInfos, milestoneTitles, tagNames, columnLabels, userName, columnRegExp, routeSegmentName, states, types, reviews}={}, ...otherFields) { repoInfos = repoInfos || []; milestoneTitles = milestoneTitles || []; tagNames = tagNames || []; @@ -67,6 +67,7 @@ export function buildRoute(name, {repoInfos, milestoneTitles, tagNames, columnLa addParams(options, 'u', userName); addParams(options, 's', states, DEFAULTS.states); // include the defaults so the URL is cleaner addParams(options, 't', types, DEFAULTS.types); + addParams(options, 'r', reviews, DEFAULTS.reviews); if (columnRegExp) { const re = columnRegExp.toString(); // Strip off the wrapping `/` marks @@ -118,6 +119,7 @@ export function parseRoute({params, routes, location}) { let userName; let states; let types; + let reviews; let columnRegExp; // TODO: remove these fallbacks once URL's are updated. @@ -130,9 +132,10 @@ export function parseRoute({params, routes, location}) { if (query.u) { userName = query.u; } if (query.s) { states = parseArray(query.s); } if (query.t) { types = parseArray(query.t); } + if (query.r) { reviews = parseArray(query.r); } if (query.x) { columnRegExp = new RegExp(query.x); } - return {repoInfos, milestoneTitles, tagNames, columnLabels, userName, states, types, columnRegExp, routeSegmentName}; + return {repoInfos, milestoneTitles, tagNames, columnLabels, userName, states, types, reviews, columnRegExp, routeSegmentName}; } class FilterState { @@ -190,6 +193,9 @@ class FilterState { toggleType(type) { return this._toggleKey('types', type); } + toggleReviews(reviews) { + return this._toggleKey('reviews', reviews); + } // setUser(user) // clearUser() toggleUserName(name) { @@ -220,6 +226,7 @@ const DEFAULTS = { tagNames: [], states: ['open'], types: ['issue', 'pull-request'], + reviews: ['my-reviews', 'reviews-under-my-pr', 'my-meta-reviews', 'others'], columnLabels: [], columnRegExp: undefined }; @@ -350,3 +357,51 @@ export function filterCardsByFilter(cards, filter) { return true; }); } + +// Filters the list of reviews by the criteria set in the URL. +// Note this happens after `issues/prs` get filtered. A review is +// just a part of a pull request, so this would only take effect +// if its corresponding issue is not filtered out. +// Used by FilterStore.filterAndSortReviews() +export function filterReviewsByFilter(reviews, filter, user) { + filter = filter || getFilters(); + const {reviews: reviewOptions} = filter.getState(); + + let myReviews, reviewsUnderMyPr, myMetaReviews, others; + for (const reviewOption of reviewOptions) { + switch (reviewOption) { + case 'my-reviews': + myReviews = true; + break; + case 'reviews-under-my-pr': + reviewsUnderMyPr = true; + break; + case 'my-meta-reviews': + myMetaReviews = true; + break; + case 'others': + others = true; + break; + default: + throw new Error('Review filter is invalid!'); + } + } + + return reviews.filter(review => { + const isMyReview = review.author && review.author.login && review.author.login === user; + const isReviewUnderMyPr = review.prAuthor === user; + const hasMyMetaReview = review.reactions && review.reactions.some(reaction => { + return reaction.user && reaction.user.login && reaction.user.login === user; + }); + + // This is main filter that is needed. It should be default not to show the + // user their own comments, unless they explicitly request it. + if (!myReviews && isMyReview) return false; + + if (myReviews && isMyReview) return true; + if (reviewsUnderMyPr && isReviewUnderMyPr) return true; + if (myMetaReviews && hasMyMetaReview) return true; + if (others && !isMyReview && !isReviewUnderMyPr && !hasMyMetaReview) return true; + return false; + }); +}