Skip to content

Instantly share code, notes, and snippets.

@ericboehs
Last active January 31, 2024 20:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericboehs/cd4c39beeb338e4582977657e7dde62e to your computer and use it in GitHub Desktop.
Save ericboehs/cd4c39beeb338e4582977657e7dde62e to your computer and use it in GitHub Desktop.
GitHub PR Status Markdowner
// ==UserScript==
// @name GitHub PR Status Markdowner
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Copy MD of GitHub PR Review Statuses
// @author Eric Boehs
// @match https://github.com/pulls*
// @grant GM.setClipboard
// @run-at document-ready
// @inject-into content
// ==/UserScript==
(function() {
const statuses = [
{ text: "Haven't even looked at it", emoji: ':question_block:' },
{ text: 'Awaiting my review', emoji: ':waiting:' },
{ text: "Needs review from author's team mate", emoji: ':one-team:' },
{ text: 'CI Failing', emoji: ':red-x:' },
{ text: 'Needs me to work on it', emoji: ':sad_robot:' },
{ text: 'Code conflict', emoji: ':spiderman-pointing:' }
]
function createStatusDropdown(issueRow, issueId) {
let select = document.createElement('select')
select.id = 'status-dropdown-' + issueId
select.onchange = function() { saveStatus(issueId, this.value) }
// Set the maximum width of the select element
select.style.maxWidth = '100px'
statuses.forEach(status => {
let option = document.createElement('option')
option.value = status.emoji // Use emoji as the value
option.textContent = status.text // Use text for display
select.appendChild(option)
})
// Locate the target element where the dropdown should be inserted
let targetDiv = issueRow.querySelector('.flex-shrink-0.col-4.col-md-3.pt-2.text-right.pr-3.no-wrap.d-flex.hide-sm')
if (targetDiv) {
// Check if targetDiv has any child nodes
if (targetDiv.firstElementChild) {
targetDiv.insertBefore(select, targetDiv.firstElementChild)
} else {
// If no child nodes, appendChild works just like insertBefore
targetDiv.appendChild(select)
}
} else {
console.error('Target div for dropdown not found in issue row')
}
initializeStatus(issueRow, issueId, select)
}
function saveStatus(issueId, status) {
localStorage.setItem('issueStatus-' + issueId, status)
}
function getStatus(issueId) {
return localStorage.getItem('issueStatus-' + issueId) || ':question_block:'
}
function initializeStatus(issueRow, issueId, select) {
select.value = getStatus(issueId)
}
function addStatusDropdowns() {
const issueRows = document.querySelectorAll('.js-issue-row')
issueRows.forEach(issueRow => {
let issueId = issueRow.dataset.id
createStatusDropdown(issueRow, issueId)
})
}
addStatusDropdowns()
function convertToMarkdown(){
const issueToolbar = document.querySelector('#js-issues-toolbar')
if (!issueToolbar) {
console.log('Element #js-issues-toolbar not found')
return
}
const prElements = issueToolbar.querySelectorAll('.js-issue-row')
const markdownLines = Array.from(prElements).map(pr => {
let repoNameElement = pr.querySelector('a[data-hovercard-type="repository"]')
let prLinkElement = pr.querySelector('a[data-hovercard-type="pull_request"]')
let openedByElement = pr.querySelector('.opened-by')
let issueId = pr.dataset.id // Assuming this is the unique identifier for the issue
let repoName = repoNameElement ? repoNameElement.textContent.trim() : ''
if (repoName) {
repoName = repoName.replace('department-of-veterans-affairs/', '')
}
let prLink = prLinkElement ? prLinkElement.getAttribute('href') : ''
let prId = openedByElement ? openedByElement.textContent.match(/#\d+/) : ''
let prTitle = prLinkElement ? prLinkElement.textContent.trim() : ''
let statusEmoji = getStatus(issueId) // Retrieve the status emoji using the getStatus function
return `* ${statusEmoji} [${prTitle} ${prId} (${repoName})](https://github.com${prLink})`
})
return markdownLines
}
function copyMdToClipboard() {
// Count the number of PRs
const prCount = document.querySelectorAll('.js-issue-row').length
// Prepend message
const userLogin = document.querySelector('[name=user-login]').content
const prUrl = document.URL.replace(/%40me/, userLogin)
const prependMessage = `[Current PR Statuses](${prUrl}) for Backend (${prCount} PRs in the queue):\n\n`
// Generate markdown for PRs
const prMarkdown = convertToMarkdown().join('\n')
// Generate key for statuses
const statusKey = generateStatusKey()
// Combine all parts
GM.setClipboard(prependMessage + prMarkdown + '\n\n' + statusKey)
var copyButton = document.getElementById("copyStatusToClipboard")
// Replace copy button with 'copied' version
copyButton.outerHTML = copiedButtonHtml
setTimeout(function() {
// Set the button back to the original 'copy' state after 5000ms (5 seconds)
document.getElementById("copyStatusToClipboard").outerHTML = copyButtonHtml
// Re-attach the event listener to the new copy button
document.getElementById("copyStatusToClipboard").addEventListener("click", copyMdToClipboard)
}, 5000)}
function isStatusUsed(statusEmoji) {
const issueRows = document.querySelectorAll('.js-issue-row')
return Array.from(issueRows).some(issueRow => {
let issueId = issueRow.dataset.id // unique identifier for the issue
return getStatus(issueId) === statusEmoji
})
}
function generateStatusKey() {
let statusKey = "Status Key:\n"
statuses.forEach(status => {
if (isStatusUsed(status.emoji)) {
statusKey += `${status.emoji} - ${status.text}\n`
}
})
return statusKey
}
var copyButtonHtml = '<a class="p-0 ml-3 btn-link" id="copyStatusToClipboard" style="align-items: center"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" height="16" style="position: relative; top: 4px"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" /></svg> Copy Status</a>'
var copiedButtonHtml = '<a class="p-0 ml-3 btn-link" id="copyStatusToClipboard" style="color: green; align-items: center"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="green" height="16" style="position: relative; top: 4px"><path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75" /></svg> Copied!</a>'
document.querySelectorAll('[data-ga-click="Pull Requests, Table state, Closed"]')[1].insertAdjacentHTML('afterend', copyButtonHtml)
document.getElementById("copyStatusToClipboard").addEventListener("click", copyMdToClipboard)
})()
@ericboehs
Copy link
Author

ericboehs commented Jan 31, 2024

This Safari Userscript adds status drop downs to github.com/pulls. You can then copy a status report in Markdown format for sharing in Slack.

It should work in Tampermonkey or other user script extensions in Chrome/FF but hasn't been tested.

Screenshot.2024-01-31.at.14.37.58.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment