Skip to content

Commit

Permalink
Merge pull request #166 from Expensify/tgolen-issue-owner
Browse files Browse the repository at this point in the history
Provide a way to manage GH issue owner
  • Loading branch information
tgolen authored Oct 17, 2023
2 parents 41f4340 + b758313 commit be4b709
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 6 deletions.
16 changes: 16 additions & 0 deletions src/css/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ $color-light-blue: #12a4d5;
$color-darker-green: #2cb528;
$color-dark-yellow: #DAA520;

.owner {
color: $color-dark-yellow !important;
}

.k2dashboard,
.passwordform {
max-width: 1680px;
Expand Down Expand Up @@ -540,3 +544,15 @@ $color-dark-yellow: #DAA520;
position: sticky;
top: 60px;
}

.js-issue-assignees .k2-button {
position: absolute;
right: -12px;
margin-top: -5px !important;
}

.alert {
color: $color-alert;
font-weight: bold;
margin: 8px 16px;
}
6 changes: 6 additions & 0 deletions src/js/component/list-item/ListItemIssue.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,18 @@ class ListItemIssue extends React.Component {
this.isHelpWanted = _.some(this.props.issue.labels, {name: 'Help Wanted'}) ? ' help-wanted' : '';
this.isContributorAssigned = this.isExternal && !this.isHelpWanted ? ' contributor-assigned' : '';
this.isUnderReview = _.find(this.props.issue.labels, label => label.name.toLowerCase() === 'reviewing');
this.isCurrentUserOwner = this.props.issue.currentUserIsOwner;
}

render() {
this.parseIssue();
return (
<div className="panel-item">
{this.isCurrentUserOwner && (
<span className="owner">
{'★ '}
</span>
)}
<a
href={this.props.issue.url}
className={this.getClassName()}
Expand Down
5 changes: 4 additions & 1 deletion src/js/component/panel/PanelIssues.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const PanelIssues = (props) => {
return null;
}

// Put the issues owned by the current user at the top of the list
const sortedData = _.sortBy(filteredData, 'currentUserIsOwner');

return (
<div className={`panel ${props.extraClass}`}>
<Title
Expand All @@ -77,7 +80,7 @@ const PanelIssues = (props) => {
</div>
) : (
<div>
{_.map(filteredData, issue => <ListItemIssue key={`issue_raw_${issue.id}`} issue={issue} />)}
{_.map(sortedData, issue => <ListItemIssue key={`issue_raw_${issue.id}`} issue={issue} />)}
</div>
)}
</div>
Expand Down
22 changes: 21 additions & 1 deletion src/js/lib/actions/Issues.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,29 @@ function getDailyImprovements() {
function getAllAssigned() {
API.getIssuesAssigned()
.then((issues) => {
const currentUser = API.getCurrentUser();
const issuesMarkedWithOwner = _.reduce(issues, (finalObject, issue) => {
const regexResult = issue.body.match(/Current Issue Owner:\s@(?<owner>\S+)/i);
const currentOwner = regexResult && regexResult.groups && regexResult.groups.owner;
if (!currentOwner || currentOwner !== currentUser) {
return {
...finalObject,
[issue.id]: issue,
};
}

return {
...finalObject,
[issue.id]: {
...issue,
currentUserIsOwner: true,
},
};
}, {});

// Always use set() here because there is no way to remove issues from Onyx
// that get closed and are no longer assigned
ReactNativeOnyx.set(ONYXKEYS.ISSUES.ASSIGNED, issues);
ReactNativeOnyx.set(ONYXKEYS.ISSUES.ASSIGNED, issuesMarkedWithOwner);
});
}

Expand Down
25 changes: 24 additions & 1 deletion src/js/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ let octokit;
*/
function getOctokit() {
if (!octokit) {
octokit = new Octokit({auth: Preferences.getGitHubToken()});
/* eslint-disable-next-line no-console */
console.log('authenticate with auth token', Preferences.getGitHubToken());
octokit = new Octokit({
auth: Preferences.getGitHubToken(),
userAgent: 'expensify-k2-extension',
});
}
return octokit;
}
Expand Down Expand Up @@ -320,6 +325,7 @@ function getIssues(assignee = 'none', labels) {
url
createdAt
updatedAt
body
assignees(first: 100) {
nodes {
avatarUrl
Expand Down Expand Up @@ -395,6 +401,21 @@ function addComment(comment) {
return getOctokit().rest.issues.createComment({...getRequestParams(), body: comment});
}

/**
* @returns {Promise}
*/
function getCurrentIssueDescription() {
return getOctokit().rest.issues.get({...getRequestParams()});
}

/**
* @param {String} body
* @returns {Promise}
*/
function setCurrentIssueBody(body) {
return getOctokit().rest.issues.update({...getRequestParams(), body});
}

export {
addComment,
getCheckRuns,
Expand All @@ -407,4 +428,6 @@ export {
getMilestones,
getCurrentUser,
getPullsByType,
getCurrentIssueDescription,
setCurrentIssueBody,
};
140 changes: 137 additions & 3 deletions src/js/lib/pages/github/issue.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable rulesdir/prefer-underscore-method */
import $ from 'jquery';
import ReactNativeOnyx from 'react-native-onyx';
import Base from './_base';
Expand All @@ -8,6 +9,120 @@ import K2pickerType from '../../../module/K2pickertype/K2pickertype';
import ToggleReview from '../../../module/ToggleReview/ToggleReview';
import K2comments from '../../../module/K2comments/K2comments';
import ONYXKEYS from '../../../ONYXKEYS';
import * as API from '../../api';

let clearErrorTimeoutID;
function catchError(e) {
$('.gh-header-actions .k2-element').remove();
$('.gh-header-actions').append('<span class="alert k2-element">OOPS!</span>');
console.error(e);
clearTimeout(clearErrorTimeoutID);
clearErrorTimeoutID = setTimeout(() => {
$('.gh-header-actions .k2-element').remove();
}, 30000);
}

/**
* Sets the owner of an issue when it doesn't have an owner yet
* @param {String} owner to set
*/
function setOwner(owner) {
API.getCurrentIssueDescription()
.then((response) => {
const ghDescription = response.data.body;
const newDescription = `${ghDescription}
<details><summary>Issue Owner</summary>Current Issue Owner: @${owner}</details>`;
API.setCurrentIssueBody(newDescription);
})
.catch(catchError);
}

/**
* Removes the existing owner of an issue
* @param {String} owner to remove
*/
function removeOwner(owner) {
API.getCurrentIssueDescription()
.then((response) => {
const ghDescription = response.data.body;
const newDescription = ghDescription.replace(`<details><summary>Issue Owner</summary>Current Issue Owner: @${owner}</details>`, '');
API.setCurrentIssueBody(newDescription);
})
.catch(catchError);
}

/**
* Replaces the existing issue owner with a different owner
* @param {String} oldOwner
* @param {String} newOwner
*/
function replaceOwner(oldOwner, newOwner) {
API.getCurrentIssueDescription()
.then((response) => {
const ghDescription = response.data.body;
const newDescription = ghDescription.replace(`Current Issue Owner: @${oldOwner}`, `Current Issue Owner: @${newOwner}`);
API.setCurrentIssueBody(newDescription);
})
.catch(catchError);
}

/**
* This method is all about adding the "issue owner" functionality which melvin will use to see who should be providing ksv2 updates to an issue.
*/
const refreshAssignees = () => {
// Always start by erasing whatever was drawn before (so it always starts from a clean slate)
$('.js-issue-assignees .k2-element').remove();

// Do nothing if there is only one person assigned. Owners can only be set when there are
// multiple assignees
if ($('.js-issue-assignees > p > span').length <= 1) {
return;
}

// Check if there is an owner for the issue
const ghDescription = $('.comment-body').text();
const regexResult = ghDescription.match(/Current Issue Owner:\s@(?<owner>\S+)/i);
const currentOwner = regexResult && regexResult.groups && regexResult.groups.owner;

// Add buttons to each assignee
$('.js-issue-assignees > p > span').each((i, el) => {
const assignee = $(el).find('.assignee span').text();
if (assignee === currentOwner) {
$(el).append(`
<button type="button" class="Button flex-md-order-2 m-0 owner k2-element k2-button k2-button-remove-owner" data-owner="${currentOwner}">
</button>
`);
} else {
$(el).append(`
<button type="button" class="Button flex-md-order-2 m-0 k2-element k2-button k2-button-make-owner" data-owner="${assignee}">
</button>
`);
}
});

// Remove the owner with this button is clicked
$('.k2-button-remove-owner').off('click').on('click', (e) => {
e.preventDefault();
const owner = $(e.target).data('owner');
removeOwner(owner);
return false;
});

// Make a new owner when this button is clicked
$('.k2-button-make-owner').off('click').on('click', (e) => {
e.preventDefault();
const newOwner = $(e.target).data('owner');
if (currentOwner) {
replaceOwner(currentOwner, newOwner);
} else {
setOwner(newOwner);
}
return false;
});
};

const refreshPicker = function () {
// Add our wrappers to the DOM which all the React components will be rendered into
Expand All @@ -28,6 +143,7 @@ const refreshPicker = function () {
* @returns {Object}
*/
export default function () {
let allreadySetup = false;
ReactNativeOnyx.init({
keys: ONYXKEYS,
});
Expand All @@ -37,14 +153,32 @@ export default function () {
IssuePage.urlPath = '^(/[\\w-]+/[\\w-.]+/issues/\\d+)$';

IssuePage.setup = function () {
// Prevent this function from running twice (it sometimes does that because of how chrome triggers the extension)
if (allreadySetup) {
return;
}
allreadySetup = true;

let refreshPickerTimeoutID;
let refreshAssigneesTimeoutID;
setTimeout(refreshPicker, 500);
setTimeout(refreshAssignees, 500);

// Listen for when the sidebar is redrawn, then redraw our pickers
$(document).bind('DOMNodeRemoved', (e) => {
if (!$(e.target).is('#partial-discussion-sidebar')) {
return;
if ($(e.target).hasClass('sidebar-assignee')) {
// Make sure that only one setTimeout runs at a time
clearTimeout(refreshAssigneesTimeoutID);
refreshAssigneesTimeoutID = setTimeout(refreshAssignees, 500);
}

if ($(e.target).is('#partial-discussion-sidebar')) {
// Make sure that only one setTimeout runs at a time
clearTimeout(refreshPickerTimeoutID);
refreshPickerTimeoutID = setTimeout(refreshPicker, 500);
clearTimeout(refreshAssigneesTimeoutID);
refreshAssigneesTimeoutID = setTimeout(refreshAssignees, 500);
}
setTimeout(refreshPicker, 500);
});
};

Expand Down
5 changes: 5 additions & 0 deletions src/js/module/dashboard/Legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ const Legend = () => {
{' '}
External
</div>
<div>
<span className="owner"></span>
{' '}
Issue owner
</div>
<div className="issue">
<sup>I</sup>
{' '}
Expand Down

0 comments on commit be4b709

Please sign in to comment.