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

Fix K2 for new GitHub UI #226

Merged
merged 17 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 15 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
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module.exports = {
'comma-dangle': ['error', 'always-multiline'],
'rulesdir/no-api-in-views': 'off',
'rulesdir/no-multiple-api-calls': 'off',
'@lwc/lwc/no-async-await': 'off',
'es/no-nullish-coalescing-operators' : 'off'
},
settings: {
'import/resolver': {
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#1.4.0
- Updated the extension to work with new GitHub UI
- Updated the extension to work with GitHub's new PR merge experience

#1.3.74
- Moved the previous query string params to Onyx

Expand Down
2 changes: 1 addition & 1 deletion assets/manifest-firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,

"name": "K2 for GitHub",
"version": "1.3.74",
"version": "1.4.0",
"description": "Manage your Kernel Scheduling from directly inside GitHub",

"browser_specific_settings": {
Expand Down
2 changes: 1 addition & 1 deletion assets/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,

"name": "K2 for GitHub",
"version": "1.3.74",
"version": "1.4.0",
"description": "Manage your Kernel Scheduling from directly inside GitHub",

"icons": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "k2-extension",
"version": "1.3.74",
"version": "1.4.0",
"description": "A Chrome Extension for Kernel Schedule",
"private": true,
"scripts": {
Expand Down
25 changes: 25 additions & 0 deletions src/css/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,28 @@ $color-dark-yellow: #DAA520;
color: rgb(230, 237, 243)
}
}

.loader {
width: 14px;
aspect-ratio: 1;
border-radius: 50%;
border: 2px solid #514b82;
animation:
l20-1 0.8s infinite linear alternate,
l20-2 1.6s infinite linear;
}
@keyframes l20-1{
0% {clip-path: polygon(50% 50%,0 0, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0% )}
12.5% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 0%, 100% 0%, 100% 0% )}
25% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 100% 100%, 100% 100% )}
50% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )}
62.5% {clip-path: polygon(50% 50%,100% 0, 100% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )}
75% {clip-path: polygon(50% 50%,100% 100%, 100% 100%, 100% 100%, 100% 100%, 50% 100%, 0% 100% )}
100% {clip-path: polygon(50% 50%,50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 0% 100% )}
}
@keyframes l20-2{
0% {transform:scaleY(1) rotate(0deg)}
49.99%{transform:scaleY(1) rotate(135deg)}
50% {transform:scaleY(-1) rotate(0deg)}
100% {transform:scaleY(-1) rotate(-135deg)}
}
74 changes: 72 additions & 2 deletions src/js/lib/pages/github/_base.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from 'jquery';
import * as API from '../../api';

/**
* This class is to be extended by each of the distinct types of webpages that the extension works on
Expand All @@ -7,6 +8,54 @@ import $ from 'jquery';
export default function () {
const Page = {};

const REVIEWER_CHECKLIST_URL = 'https://raw.githubusercontent.com/Expensify/App/main/contributingGuides/REVIEWER_CHECKLIST.md';
const BUGZERO_CHECKLIST_URL = 'https://raw.githubusercontent.com/Expensify/App/main/contributingGuides/BUGZERO_CHECKLIST.md';

/**
* Gets the contents of the reviewer checklist from GitHub and then posts it as a comment to the current PR
* @param {Event} e
* @param {'bugzero' | 'reviewer'} checklistType Type of target checklist
*/
const copyReviewerChecklist = async (e, checklistType) => {
const checklistUrl = checklistType === 'bugzero' ? BUGZERO_CHECKLIST_URL : REVIEWER_CHECKLIST_URL;

e.preventDefault();

// Get the button element
const button = e.target;

// Save the original content of the button
const originalContent = button.innerHTML;

// Replace the button content with a loader
button.innerHTML = '<div class="loader" />';

try {
// Fetch the checklist contents
const response = await fetch(checklistUrl);

if (!response.ok) {
console.error(`Failed to load contents of ${checklistUrl}: ${response.statusText}`);
return;
}

const fileContents = await response.text();

if (!fileContents) {
console.error(`Could not load contents of ${checklistUrl} for some reason`);
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

In these error cases we should revert the loader. So instead of logging and returning we could throw an error and have the catch and finally block handle things.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was the original code moved to a different function, but I changed it to:

        try {
            // Fetch the checklist contents
            const response = await fetch(checklistUrl);
            const fileContents = await response.text();

            // Call the API to add the comment
            await API.addComment(fileContents);
        } catch (error) {
            console.error('Error fetching the checklist:', error);
        } finally {
            // Restore the original button content
            button.innerHTML = originalContent;
        }

this way the loader will always be reverted, and:

  1. If fetch() returns something other than 200, it will throw anyway
  2. If fetched text is empty, then API.addComment() will throw due to incorrect parameters

So all error cases are covered now.

}

// Call the API to add the comment
await API.addComment(fileContents);
} catch (error) {
console.error('Error fetching the checklist:', error);
} finally {
// Restore the original button content
button.innerHTML = originalContent;
}
};

/**
* A unique identifier for each page
*/
Expand Down Expand Up @@ -47,11 +96,32 @@ export default function () {
Page.setup = function () {};

Page.getRepoOwner = function () {
return $('.author a span').text();
return document.querySelectorAll('.AppHeader-context-item-label.Truncate-text')[0].textContent.trim();
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB, I wonder if we could keep the repeatedly used selectors as constant with meaningful names of what we are selecting - For example - const selectorForRepoLabel = '.AppHeader-context-item-label.Truncate-text'; , so next time github changes UI, we could simply change the constant instead of going all over the code, and hopefully it should be simple to transition and works 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For sure, I agree with that. Once we get this PR of the gate I'll spend some time to reorganize the selectors a little bit.

Copy link
Contributor

Choose a reason for hiding this comment

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

That would be great. Thank you!!

Copy link
Contributor

Choose a reason for hiding this comment

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

NAB ➕ I would also like to see this. Is it easy enough to do in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The repeating selectors are now removed, but I've added a bunch of comments for existing selectors to clarify what exactly they are selecting.

I chose comments over variables to not have to introduce additional null checks to satisfy ESLint for selectors that turn out empty.

};

Page.getRepo = function () {
return $('.js-current-repository').text();
return document.querySelectorAll('.AppHeader-context-item-label.Truncate-text')[1].textContent.trim();
};

/**
* Renders buttons for copying checklists in issue/PR bodies
* @param {'bugzero' | 'reviewer'} checklistType Type of target checklist
*/
Page.renderCopyChecklistButtons = function (checklistType) {
// Look through all the comments on the page to find one that has the template for the copy/paste checklist button
// eslint-disable-next-line rulesdir/prefer-underscore-method
$('.markdown-body > p').each((i, el) => {
const commentHtml = $(el).html();

// When the button template is found, replace it with an HTML button and then put that back into the DOM so someone can click on it
if (commentHtml && commentHtml.indexOf('you can simply click: [this button]') > -1) {
const newHtml = commentHtml.replace('[this button]', '<button type="button" class="btn btn-sm k2-copy-checklist">HERE</button>');
$(el).html(newHtml);

// Now that the button is on the page, add a click handler to it (always remove all handlers first so that we know there will always be one handler attached)
$('.k2-copy-checklist').off().on('click', e => copyReviewerChecklist(e, checklistType));
}
});
};

return Page;
Expand Down
30 changes: 0 additions & 30 deletions src/js/lib/pages/github/createpr.js

This file was deleted.

90 changes: 30 additions & 60 deletions src/js/lib/pages/github/issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,15 @@ 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>');
$('div[data-component="PH_Actions"] .k2-element').remove();
$('div[data-component="PH_Actions"]').append('<span class="alert k2-element">OOPS!</span>');
console.error(e);
clearTimeout(clearErrorTimeoutID);
clearErrorTimeoutID = setTimeout(() => {
$('.gh-header-actions .k2-element').remove();
$('div[data-component="PH_Actions"] .k2-element').remove();
}, 30000);
}

/**
* Gets the contents of the reviewer checklist from GitHub and then posts it as a comment to the current PR
* @param {Event} e
*/
const copyReviewerChecklist = (e) => {
e.preventDefault();
const pathToChecklist = 'https://raw.githubusercontent.com/Expensify/App/main/contributingGuides/BUGZERO_CHECKLIST.md';
$.get(pathToChecklist)
.done((fileContents) => {
if (!fileContents) {
console.error(`could not load contents of ${pathToChecklist} for some reason`);
return;
}

API.addComment(fileContents);
});
};

const renderCopyChecklistButton = () => {
// Look through all the comments on the page to find one that has the template for the copy/paste checklist button
// eslint-disable-next-line rulesdir/prefer-underscore-method
$('.js-comment-body').each((i, el) => {
const commentHtml = $(el).html();

// When the button template is found, replace it with an HTML button and then put that back into the DOM so someone can click on it
if (commentHtml && commentHtml.indexOf('you can simply click: [this button]') > -1) {
const newHtml = commentHtml.replace('[this button]', '<button type="button" class="btn btn-sm k2-copy-checklist">HERE</button>');
$(el).html(newHtml);

// Now that the button is on the page, add a click handler to it (always remove all handlers first so that we know there will always be one handler attached)
$('.k2-copy-checklist').off().on('click', copyReviewerChecklist);
}
});
};

/**
* Sets the owner of an issue when it doesn't have an owner yet
* @param {String} owner to set
Expand Down Expand Up @@ -104,27 +69,32 @@ function replaceOwner(oldOwner, newOwner) {

/**
* 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.
* @param {String | null} [issueOwner] GitHub username of the issue owner. Null means no owner, undefined means get the owner from issue body
*/
const refreshAssignees = () => {
const renderAssignees = (issueOwner) => {
// Always start by erasing whatever was drawn before (so it always starts from a clean slate)
$('.js-issue-assignees .k2-element').remove();
$('div[data-testid="sidebar-section"] .k2-element').remove();

let currentOwner = issueOwner;

// 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;
// if issue owner is not provided, then try to get it from the issue body
if (currentOwner === undefined) {
const ghDescription = $('.markdown-body').first().text();
const regexResult = ghDescription.match(/Current Issue Owner:\s@(?<owner>\S+)/i);
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();
$('div[data-testid="issue-assignees"]').each((i, el) => {
const assignee = $(el).text();
if (assignee === currentOwner) {
$(el).append(`
$(el).closest('li').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(`
$(el).closest('li').append(`
<button type="button" class="Button flex-md-order-2 m-0 k2-element k2-button k2-button-make-owner" data-owner="${assignee}">
</button>
Expand All @@ -133,30 +103,30 @@ const refreshAssignees = () => {
});

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

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

const refreshPicker = function () {
// Add our wrappers to the DOM which all the React components will be rendered into
if (!$('.k2picker-wrapper').length) {
$('.js-issue-labels').after(sidebarWrapperHTML);
$('div[data-testid="issue-viewer-metadata-pane"] > :nth-child(3)').after(sidebarWrapperHTML);
}

new K2picker().draw();
Expand All @@ -172,7 +142,7 @@ const refreshPicker = function () {
* @returns {Object}
*/
export default function () {
let allreadySetup = false;
let alreadySetup = false;
ReactNativeOnyx.init({
keys: ONYXKEYS,
});
Expand All @@ -183,27 +153,27 @@ export default function () {

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

// Draw them once when the page is loaded
setTimeout(refreshPicker, 500);
setTimeout(refreshAssignees, 500);
setTimeout(renderAssignees, 500);

// Every second, check to see if the pickers are still there, and if not, redraw them
setInterval(() => {
if (!$('.k2picker-wrapper').length) {
refreshPicker();
}
if (!$('.js-issue-assignees .k2-element').length) {
refreshAssignees();
if (!$('div[data-testid="issue-viewer-metadata-pane"] > :nth-child(2) .k2-element').length) {
renderAssignees();
}
}, 1000);

// Waiting 2 seconds to call this gives the page enough time to load so that there is a better chance that all the comments will be rendered
setInterval(renderCopyChecklistButton, 2000);
setInterval(() => IssuePage.renderCopyChecklistButtons('bugzero'), 2000);
};

return IssuePage;
Expand Down
Loading
Loading