Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to manage GH issue owner #166

Merged
merged 7 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was running into a lot of issues when testing this where I kept hitting a secondary rate limit. I added this log to hopefully help trouble-shoot if it happens in production.

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
Loading