diff --git a/src/css/content.scss b/src/css/content.scss
index ce6878ff..d557baec 100644
--- a/src/css/content.scss
+++ b/src/css/content.scss
@@ -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;
@@ -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;
+}
diff --git a/src/js/component/list-item/ListItemIssue.js b/src/js/component/list-item/ListItemIssue.js
index 962b11a8..447452e4 100644
--- a/src/js/component/list-item/ListItemIssue.js
+++ b/src/js/component/list-item/ListItemIssue.js
@@ -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 (
diff --git a/src/js/lib/actions/Issues.js b/src/js/lib/actions/Issues.js
index b9874ea5..82a82e34 100644
--- a/src/js/lib/actions/Issues.js
+++ b/src/js/lib/actions/Issues.js
@@ -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@(?\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);
});
}
diff --git a/src/js/lib/api.js b/src/js/lib/api.js
index a9bde9a9..85295040 100644
--- a/src/js/lib/api.js
+++ b/src/js/lib/api.js
@@ -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;
}
@@ -320,6 +325,7 @@ function getIssues(assignee = 'none', labels) {
url
createdAt
updatedAt
+ body
assignees(first: 100) {
nodes {
avatarUrl
@@ -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,
@@ -407,4 +428,6 @@ export {
getMilestones,
getCurrentUser,
getPullsByType,
+ getCurrentIssueDescription,
+ setCurrentIssueBody,
};
diff --git a/src/js/lib/pages/github/issue.js b/src/js/lib/pages/github/issue.js
index 38dc61ca..d73f4a3e 100644
--- a/src/js/lib/pages/github/issue.js
+++ b/src/js/lib/pages/github/issue.js
@@ -1,3 +1,4 @@
+/* eslint-disable rulesdir/prefer-underscore-method */
import $ from 'jquery';
import ReactNativeOnyx from 'react-native-onyx';
import Base from './_base';
@@ -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('OOPS!');
+ 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}
+
+Issue Owner
Current Issue Owner: @${owner} `;
+ 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(`Issue Owner
Current Issue Owner: @${owner} `, '');
+ 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@(?\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(`
+
+ `);
+ } else {
+ $(el).append(`
+
+ `);
+ }
+ });
+
+ // 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
@@ -28,6 +143,7 @@ const refreshPicker = function () {
* @returns {Object}
*/
export default function () {
+ let allreadySetup = false;
ReactNativeOnyx.init({
keys: ONYXKEYS,
});
@@ -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);
});
};
diff --git a/src/js/module/dashboard/Legend.js b/src/js/module/dashboard/Legend.js
index dec3ea04..e2f22a9d 100644
--- a/src/js/module/dashboard/Legend.js
+++ b/src/js/module/dashboard/Legend.js
@@ -41,6 +41,11 @@ const Legend = () => {
{' '}
External
+
+ ★
+ {' '}
+ Issue owner
+
I
{' '}