// ==UserScript==
// @name Octopus GitHub
// @version 0.90
// @description A userscript for GitHub
// @author Oreo
// @homepage https://github.com/Oreoxmt/octopus-github
// @updateURL https://github.com/Oreoxmt/octopus-github/raw/main/gh-util.user.js
// @downloadURL https://github.com/Oreoxmt/octopus-github/raw/main/gh-util.user.js
// @supportURL https://github.com/Oreoxmt/octopus-github
// @match https://github.com/*/pulls*
// @match https://github.com/*/pull/*
// @run-at document-start
// @require https://cdnjs.cloudflare.com/ajax/libs/rest.js/15.2.6/octokit-rest.js
// ==/UserScript==
(function () {
'use strict';
const ATTR = 'octopus-github-util-mark'
const STORAGEKEY = 'octopus-github-util:token'
const TARGET_REPO_OWNER = 'pingcap'
function GetRepositoryInformation() {
// Get the pathname of the current page
var pathname = location.pathname;
// Split the pathname into an array of parts
var parts = pathname.split('/');
// Return an object containing the user name and repository name
return {
owner: parts[1],
name: parts[2],
}
}
function EnsureToken() {
var token = localStorage.getItem(STORAGEKEY)
if (!token) {
// Prompt user to set token
// TODO: Use HTML element instead of prompt
token = prompt('Enter your GitHub token:');
if (!token) {
throw 'No token set'
}
localStorage.setItem(STORAGEKEY, token);
}
return token;
}
// This function can be used to leave a comment on a specific PR
function LeaveCommentOnPR(commentLink, comment) {
// Send the POST request to the GitHub API
// TODO: Use Octokit to create requests
fetch(commentLink, {
method: 'POST',
headers: {
'Authorization': `Bearer ${EnsureToken()}`,
'Accept': 'application/vnd.github+json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'body': comment
})
}).then((response) => {
console.log('response to ', commentLink, response)
}).catch((error) => {
console.log('error on ', commentLink, error)
})
}
// This function can be used to get the GitHub login name of the current user
async function GetMyGitHubID() {
try {
const userURL = 'https://api.github.com/user';
const response = await fetch(userURL, {
headers: {'Authorization': `Bearer ${EnsureToken()}`,},
});
if (response.ok) {
const userData = await response.json();
return userData.login;
} else {
throw new Error('Failed to fetch current user login name.');
}
} catch (error) {
console.error('An error occurred:', error);
throw error;
}
}
// This function can be used to get the PR title, description, label, base, and head information
function GetPRInfo(octokit, messageTextElement, RepoOwner, RepoName, PRNumber) {
return new Promise((resolve, reject) => {
octokit.pullRequests.get({
owner: RepoOwner,
repo: RepoName,
number: PRNumber,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
})
.then(response => {
const PRData = response.data;
const sourceTitle = PRData.title;
const sourceDescription = PRData.body;
const sourceLabels = PRData.labels.map(label => label.name);
const baseRepo = PRData.base.repo.full_name;
const baseBranch = PRData.base.ref;
const headRepo = PRData.head.repo.full_name;
const headBranch = PRData.head.ref;
messageTextElement.innerHTML += `[Log]: Getting the source language PR information... `;
console.log(`Getting source language PR information was successful. The head branch name is: ${headBranch}`);
const result = [sourceTitle, sourceDescription, sourceLabels, baseRepo, baseBranch, headRepo, headBranch];
resolve(result);
})
.catch(error => {
messageTextElement.innerHTML += ` [Error]: Failed to get the source language PR information: ${error.message}`;
reject(error);
});
});
}
// This function can be used to sync the latest content from the upstream branch to your own branch
async function SyncMyRepoBranch(octokit, messageTextElement, targetRepoOwner, targetRepoName, myRepoOwner, myRepoName, baseBranch) {
try {
const upstreamRef = await octokit.gitdata.getReference({
owner: targetRepoOwner,
repo: targetRepoName,
ref: `heads/${baseBranch}`,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
const upstreamSHA = upstreamRef.data.object.sha;
console.log(upstreamSHA);
messageTextElement.innerHTML += `[Log]: Syncing the latest content from the upstream branch... `;
await octokit.gitdata.updateReference({
owner: myRepoOwner,
repo: myRepoName,
ref: `heads/${baseBranch}`,
sha: upstreamSHA,
force: true,
headers: { 'Authorization': `Bearer ${EnsureToken()}` }
});
console.log("The content sync is successful!");
} catch (error) {
const myRepoUrl = `https://github.com/${myRepoOwner}/${myRepoName}`;
const myBranchesUrl = `${myRepoUrl}/branches`;
const baseBranchUrl = `${myRepoUrl}/tree/${baseBranch}`;
const myRepoResponse = await fetch(myRepoUrl, { method: 'HEAD' });
if (myRepoResponse.ok) {
const baseBranchResponse = await fetch(baseBranchUrl, { method: 'HEAD' });
messageTextElement.innerHTML += baseBranchResponse.ok ? ` [Error]: Failed to sync the ${baseBranch} branch of your forked ${myRepoOwner} repo. You need to manually sync your ${baseBranch} branch from the upstream branch first. ` : ` [Error]: Your forked ${myRepoName} repo does not have the ${baseBranch} branch yet. You need to manually create the ${baseBranch} branch in your repo based on the upstream. `;
} else {
messageTextElement.innerHTML += ` [Error]: Failed to sync the latest content from the upstream branch to your branch. Please check whether you have forked the ${targetRepoOwner}/${targetRepoName} repo. If not, you need to fork the ${targetRepoOwner}/${targetRepoName} repo with all its branches first. `;
}
console.log(error);
throw error;
}
}
// This function can be used to create a new branch in your repo
async function CreateBranch(octokit, messageTextElement, repoOwner, repoName, branchName, baseBranch) {
try {
const baseRef = await octokit.gitdata.getReference({
owner: repoOwner,
repo: repoName,
ref: `heads/${baseBranch}`,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
const baseSha = baseRef.data.object.sha;
console.log(baseSha);
messageTextElement.innerHTML += `[Log]: Creating a branch for the translation PR... `;
await octokit.gitdata.createReference({
owner: repoOwner,
repo: repoName,
ref: `refs/heads/${branchName}`,
sha: baseSha,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
const branchUrl = `https://github.com/${repoOwner}/${repoName}/tree/${branchName}`;
console.log(`A new branch is created successfully. The branch address is: ${branchUrl}`);
} catch (error) {
const branchesUrl = `https://github.com/${repoOwner}/${repoName}/branches`;
const targetBranchUrl = `https://github.com/${repoOwner}/${repoName}/tree/${branchName}`;
console.log(targetBranchUrl)
fetch(targetBranchUrl, { method: 'HEAD' })
.then(response => {
messageTextElement.innerHTML += response.ok ? ` [Error]: Failed to create the branch for the translation PR. The target branch ${branchName} already exists in your repo. You need to manually create a new translation PR with a different branch name.` : ` [Error]: Failed to create the branch for the translation PR: ${error.message}`;
});
console.error(error);
throw error;
}
}
// This function can be used to create a temp file in your specified branch
async function CreateFileInBranch(octokit, messageTextElement, repoOwner, repoName, branchName, filePath, fileContent, commitMessage) {
try {
const contentBase64 = btoa(fileContent);
const response = await octokit.repos.createFile({
owner: repoOwner,
repo: repoName,
branch: branchName,
path: filePath,
message: commitMessage,
content: contentBase64,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
console.log('A temp file is created successfully!');
} catch (error) {
//console.log('Failed to create the temp file.');
messageTextElement.innerHTML += ` [Error]: Failed to create a temp file in the new branch: ${error.message} `;
console.error(error);
}
}
// This function can be used to modify the description of the translation PR
function UpdatePRDescription(sourceRepoOwner, sourceRepoName, sourcePRNumber, sourceDescription, targetRepoName) {
const sourcePRCLA = "https://cla-assistant.io/pingcap/" + sourceRepoName;
const newPRCLA = "https://cla-assistant.io/pingcap/" + targetRepoName;
const sourcePRURL = `https://github.com/${sourceRepoOwner}/${sourceRepoName}/pull/${sourcePRNumber}`;
let newPRDescription = sourceDescription.replace(sourcePRCLA, newPRCLA);
newPRDescription = newPRDescription.replace("This PR is translated from:", "This PR is translated from: " + sourcePRURL);
const regexConstructor = new RegExp(".*?\\tips for choosing the affected versions.*?\\n\\n?", "g");
newPRDescription = newPRDescription.replace(regexConstructor, "");
console.log(newPRDescription)
return newPRDescription;
}
// This function can be used to create a pull request based on your specified branch
async function CreatePullRequest(octokit, messageTextElement, targetRepoOwner, targetRepoName, baseBranch, myRepoOwner, myRepoName, newBranchName, title, body, labels) {
try {
messageTextElement.innerHTML += `[Log]: Creating the empty translation PR... `;
const prResponse = await octokit.pullRequests.create({
owner: targetRepoOwner,
repo: targetRepoName,
title: title,
body: body,
head: `${myRepoOwner}:${newBranchName}`,
base: baseBranch,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
try {
console.log(prResponse);
const prUrl = prResponse.data.html_url;
//console.log(`Your target PR is created successfully. The PR address is: ${prUrl}`);
messageTextElement.innerHTML += ` Your target PR is created successfully. The PR address is: ${prUrl} `;
const urlParts = prUrl.split("/");
const prNumber = urlParts[6];
// Add labels to the created PR
const labelsResponse = await octokit.issues.addLabels({
owner: targetRepoOwner,
repo: targetRepoName,
number: prNumber,
labels: labels,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
console.log('Labels are added successfully.');
return prUrl;
} catch (error) {
console.log('Failed to add the PR labels.');
console.error(error);
}
} catch (error) {
console.log('Failed to create the translation PR.');
messageTextElement.innerHTML += ` [Error]: Failed to create the translation PR: ${error.message} `;
console.error(error);
}
}
async function DeleteFileInBranch(octokit, repoOwner, repoName, branchName, filePath, commitMessage) {
try {
const { data: fileInfo } = await octokit.repos.getContent({
owner: repoOwner,
repo: repoName,
path: filePath,
ref: branchName,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
await octokit.repos.deleteFile({
owner: repoOwner,
repo: repoName,
path: filePath,
message: commitMessage,
sha: fileInfo.sha,
branch: branchName,
headers: {'Authorization': `Bearer ${EnsureToken()}`}
});
console.log("The temp.md is deleted successfully!");
} catch (error) {
console.log(`Failed to delete temp.md. Error message: ${error.message}`);
throw error;
}
}
// This function can be used to check if the current user has write permission to the target repository
async function CheckRepositoryWritePermission(targetRepoOwner, targetRepoName) {
try {
const repoUrl = `https://api.github.com/repos/${targetRepoOwner}/${targetRepoName}`;
const response = await fetch(repoUrl, {
headers: {
'Authorization': `Bearer ${EnsureToken()}`,
'Accept': 'application/vnd.github+json'
}
});
if (response.ok) {
const repoData = await response.json();
// Check if user has write permission (push access)
return repoData.permissions && (repoData.permissions.push || repoData.permissions.admin);
}
return false;
} catch (error) {
console.error('Failed to check repository permissions:', error);
return false;
}
}
// This function can be used to trigger a workflow in the forked repository
async function TriggerWorkflow(octokit, messageTextElement, targetRepoOwner, targetRepoName, baseBranch, sourcePRURL, targetPRURL) {
try {
// Check if user has write permission to the forked repository
const hasWritePermission = await CheckRepositoryWritePermission(targetRepoOwner, targetRepoName);
if (!hasWritePermission) {
messageTextElement.innerHTML += ` [Error]: You don't have write permission for the repository ${targetRepoOwner}/${targetRepoName}, so the translation workflow cannot be triggered automatically. `;
messageTextElement.innerHTML += `[Info]: Please check your repository permissions and ensure the workflow file exists in that repository. `;
return;
}
let workflowFileName;
// Determine which workflow to trigger based on the target repository name
if (targetRepoName === "docs") {
workflowFileName = "sync-doc-pr-zh-to-en.yml";
} else if (targetRepoName === "docs-cn") {
workflowFileName = "sync-doc-pr-en-to-zh.yml";
} else {
console.log(`No workflow configured for repository: ${targetRepoName}`);
return;
}
messageTextElement.innerHTML += ` [Log]: Triggering workflow ${workflowFileName} in ${targetRepoOwner}/${targetRepoName} to translate the current PR... `;
const workflowDispatchUrl = `https://api.github.com/repos/${targetRepoOwner}/${targetRepoName}/actions/workflows/${workflowFileName}/dispatches`;
const requestBody = {
ref: baseBranch,
inputs: {
source_pr_url: sourcePRURL,
target_pr_url: targetPRURL,
ai_provider: 'gemini'
}
};
console.log(`Triggering workflow with URL: ${workflowDispatchUrl}`);
console.log(`Request body:`, requestBody);
const response = await fetch(workflowDispatchUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${EnsureToken()}`,
'Accept': 'application/vnd.github+json',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
console.log(`Response status: ${response.status} ${response.statusText}`);
if (!response.ok) {
const errorText = await response.text();
console.log(`Response error text:`, errorText);
// Provide helpful error message for common issues
if (response.status === 422) {
messageTextElement.innerHTML += ` [Error]: Failed to trigger workflow in ${targetRepoOwner}/${targetRepoName}. `;
messageTextElement.innerHTML += `[Info]: The workflow file ${workflowFileName} may not exist in the repository or it doesn't have the workflow_dispatch trigger configured. `;
messageTextElement.innerHTML += `[Info]: Please ensure: `;
messageTextElement.innerHTML += `1. The repository ${targetRepoOwner}/${targetRepoName} has the workflow file .github/workflows/${workflowFileName} `;
messageTextElement.innerHTML += `2. The workflow file contains workflow_dispatch: in the on: section `;
messageTextElement.innerHTML += `3. GitHub Actions is enabled in the repository settings `;
return;
}
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
}
messageTextElement.innerHTML += `[Log]: Workflow ${workflowFileName} triggered successfully! `;
//messageTextElement.innerHTML += `[Log]: Source PR: ${sourcePRURL} `;
//messageTextElement.innerHTML += `[Log]: Target PR: ${targetPRURL} `;
// Provide direct link to the workflow page where user can check the status
const workflowPageUrl = `https://github.com/${targetRepoOwner}/${targetRepoName}/actions/workflows/${workflowFileName}`;
messageTextElement.innerHTML += `[Log]: Check workflow status at: ${workflowPageUrl} `;
messageTextElement.innerHTML += `[Info]: To monitor the translation progress, check the preceding workflow page. After the workflow completes successfully, the translation result will be automatically applied to the target PR. `;
console.log(`Workflow ${workflowFileName} triggered successfully in ${targetRepoOwner}/${targetRepoName}`);
} catch (error) {
messageTextElement.innerHTML += ` [Error]: Failed to trigger workflow: ${error.message} `;
console.error(`Failed to trigger workflow:`, error);
}
}
async function CreateTransPR(triggerWorkflow = true) {
try {
const messageBox = document.createElement("div");
messageBox.style.position = "fixed";
messageBox.style.top = "50%";
messageBox.style.left = "50%";
messageBox.style.transform = "translate(-50%, -50%)";
messageBox.style.padding = "30px";
messageBox.style.backgroundColor = "white";
messageBox.style.border = "1px solid #e1e4e8";
messageBox.style.borderRadius = "6px";
messageBox.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.1)";
messageBox.style.zIndex = "9999";
messageBox.style.width = "480px";
messageBox.style.minHeight = "300px";
messageBox.style.maxHeight = "80vh";
messageBox.style.overflow = "auto";
messageBox.style.marginTop = "10px";
document.body.appendChild(messageBox);
const messageTextElement = document.createElement("span");
messageTextElement.innerHTML = `Start creating an empty translation PR for you. Wait for a few seconds....
`;
messageTextElement.style.fontSize = "14px";
messageTextElement.style.color = "#24292e";
messageTextElement.style.marginBottom = "10px";
messageBox.appendChild(messageTextElement);
const closeButton = document.createElement("span");
closeButton.innerText = "X";
closeButton.style.position = "absolute";
closeButton.style.top = "8px";
closeButton.style.right = "10px";
closeButton.style.right = "8px";
closeButton.style.fontSize = "12px";
closeButton.style.fontWeight = "bold";
closeButton.style.color = "#586069";
closeButton.style.border = "none";
closeButton.style.backgroundColor = "transparent";
closeButton.style.cursor = "pointer";
closeButton.addEventListener("click", () => {
messageBox.style.display = "none";
});
messageBox.appendChild(closeButton);
// Show the message box
messageBox.style.display = "block";
const octokit = new Octokit({ auth: EnsureToken() });
console.log(octokit);
// TODO: Define a new function to parse the current URL and return the repo owner, repo name, PR number, etc.
const currentURL = window.location.pathname;
const currentURLSplit = currentURL.split("/");
const currentRepoOwner = currentURLSplit[1];
const currentRepoName = currentURLSplit[2];
const currentPRNumber = currentURLSplit[4];
const targetRepoOwner = TARGET_REPO_OWNER
let myRepoName, targetRepoName, translationLabel;
switch (currentRepoName) {
case "docs-cn":
myRepoName = "docs";
targetRepoName = "docs";
translationLabel = "translation/from-docs-cn";
break;
case "docs":
myRepoName = "docs-cn";
targetRepoName = "docs-cn";
translationLabel = "translation/from-docs";
break;
}
//1.Get the GitHub login name of the current user
const myRepoOwner = await GetMyGitHubID();
//2.Get the source PR information
const [sourceTitle, sourceDescription, sourceLabels, baseRepo, baseBranch, headRepo, headBranch] = await GetPRInfo(octokit, messageTextElement, currentRepoOwner, currentRepoName, currentPRNumber);
const excludeLabels = ["size", "translation", "status", "first-time-contributor", "contribution", "lgtm", "approved"];
const targetLabels = sourceLabels.filter(label => !excludeLabels.some(excludeLabel => label.includes(excludeLabel)));
if (!sourceLabels.includes("translation/done")) {
// Proceed with the PR creation only if the translation/done label is not added for the current source PR
//3. Sync the base branch of my forked repository
await SyncMyRepoBranch(octokit, messageTextElement, targetRepoOwner, targetRepoName, myRepoOwner, myRepoName, baseBranch);
//4. Create a new branch in the repository that I forked
const newBranchName = `${headBranch}-${currentPRNumber}`;
await CreateBranch(octokit, messageTextElement, myRepoOwner, myRepoName, newBranchName, baseBranch);
//5. Create a temporary temp.md file in the new branch
const filePath = "temp.md";
const FileContent = "This is a test file.";
const CommitMessage = "Add temp.md";
await CreateFileInBranch(octokit, messageTextElement, myRepoOwner, myRepoName, newBranchName, filePath, FileContent, CommitMessage);
//6. Create a pull request
const title = sourceTitle;
const body = UpdatePRDescription(currentRepoOwner, currentRepoName, currentPRNumber, sourceDescription, targetRepoName);
targetLabels.push(translationLabel);
const labels = targetLabels;
const targetPRURL = await CreatePullRequest(octokit, messageTextElement, targetRepoOwner, targetRepoName, baseBranch, myRepoOwner, myRepoName, newBranchName, title, body, labels);
//7. Delete the temporary temp.md file
const CommitMessage2 = "Delete temp.md";
await DeleteFileInBranch(octokit, myRepoOwner, myRepoName, newBranchName, filePath, CommitMessage2);
//8. Trigger the workflow in the forked repository (only if triggerWorkflow is true)
if (triggerWorkflow) {
const sourcePRURL = `https://github.com/${currentRepoOwner}/${currentRepoName}/pull/${currentPRNumber}`;
await TriggerWorkflow(octokit, messageTextElement, myRepoOwner, myRepoName, baseBranch, sourcePRURL, targetPRURL);
} else {
messageTextElement.innerHTML += ` [Info]: Translation PR created successfully without triggering the workflow. `;
}
}
else {
messageTextElement.innerHTML += ` [Error]: The current PR already has the translation/done label, which means that there is already a translation PR for it. Please check if you still need to create another translation PR. If yes, you need to change the translation/done label to translation/doing first. `;
}
} catch (error) {
console.error("An error occurred:", error);
return error;
}
}
// TODO: Use toggle instead of button, and add more features to the toggle, e.g., editing tokens.
function EnsureCommentButton() {
const MARK = 'comment-button'
if (document.querySelector(`button[${ATTR}="${MARK}"]`)) {
return;
}
// First, find the "table-list-header-toggle" div
var toggleDiv = document.querySelector('.table-list-header-toggle.float-right');
if (!toggleDiv) {
return;
}
// Next, create a button element and add it to the page
var button = document.createElement('button');
button.innerHTML = 'Comment';
button.setAttribute('class', 'btn btn-sm js-details-target d-inline-block float-left float-none m-0 mr-md-0 js-title-edit-button');
button.setAttribute(ATTR, MARK);
toggleDiv.appendChild(button);
// Next, add an event listener to the button to listen for clicks
button.addEventListener('click', function () {
EnsureToken();
// Get a list of all the checkboxes on the page (these are used to select PRs)
var checkboxes = document.querySelectorAll('input[type=checkbox][data-check-all-item]');
// Iterate through the checkboxes and get the ones that are checked
var selectedPRs = [];
checkboxes.forEach(function (checkbox) {
if (checkbox.checked) {
selectedPRs.push(checkbox.value);
}
})
// Prompt the user for a comment to leave on the selected PRs
var comment = prompt('Enter a comment to leave on the selected PRs:');
if (!comment) {
return;
}
var repo = GetRepositoryInformation();
// Leave the comment on each selected PR
selectedPRs.forEach(function (pr) {
var commentLink = `https://api.github.com/repos/${repo.owner}/${repo.name}/issues/${pr}/comments`;
// Leave a comment on the PR
LeaveCommentOnPR(commentLink, comment);
});
});
}
function EnsureCommentButtonOnPR() {
const MARK = "comment-button-pr";
if (document.querySelector(`button[${ATTR}="${MARK}"]`)) {
return;
}
// First, find the "table-list-header-toggle" div
var headerActions = document.querySelector(".gh-header-actions");
if (!headerActions) {
return;
}
// Next, create a button element and add it to the page
var button = document.createElement("button");
button.innerHTML = "Comment";
button.setAttribute(
"class",
"flex-md-order-2 Button--secondary Button--small Button m-0 mr-md-0"
);
button.setAttribute(ATTR, MARK);
headerActions.appendChild(button);
// Next, add an event listener to the button to listen for clicks
button.addEventListener("click", function () {
EnsureToken();
// get the pr number
const url = window.location.pathname;
const urlSplit = url.split("/");
const index = urlSplit.indexOf("pull");
const pr = urlSplit[index + 1];
// Prompt the user for a comment to leave on the selected PRs
var comment = prompt("Enter a comment to leave on the selected PRs:");
if (!comment) {
return;
}
var repo = GetRepositoryInformation();
// Leave the comment on this PR
var commentLink = `https://api.github.com/repos/${repo.owner}/${repo.name}/issues/${pr}/comments`;
LeaveCommentOnPR(commentLink, comment);
});
}
function EnsureFileLink(issueElement) {
const MARK = 'file-link-span'
if (issueElement.querySelector(`span[${ATTR}="${MARK}"]`)) {
return; // Already added
}
var issueId = issueElement.getAttribute("id")
var originalLinkElement = document.getElementById(issueId + "_link")
if (!originalLinkElement) {
return; // Element is not ready
}
var originalLink = originalLinkElement.getAttribute("href")
var newLink = originalLink + "/files"
var openedByElement = issueElement.querySelectorAll('span[class="opened-by"]');
if (openedByElement.length == 1) {
var openedBy = openedByElement[0];
var linkSpanElement = document.createElement('span');
linkSpanElement.setAttribute('class', 'd-inline-block mr-1 custom')
linkSpanElement.setAttribute(ATTR, MARK)
var dotSpanElement = document.createElement('span');
dotSpanElement.innerHTML = ' • ';
dotSpanElement.setAttribute('class', 'd-inline-block mr-1 custom')
var linkElement = document.createElement('a')
linkElement.setAttribute('href', newLink)
linkElement.setAttribute('class', 'Link--muted')
linkElement.innerHTML = "Files"
linkSpanElement.appendChild(linkElement)
openedBy.insertAdjacentElement('beforebegin', linkSpanElement)
openedBy.insertAdjacentElement('beforebegin', dotSpanElement);
}
}
// This function creates a button that scrolls to top of the page
function EnsureScrollToTopButton() {
const MARK = 'scroll-to-top-button';
if (document.querySelector(`button[${ATTR}="${MARK}"]`)) {
return;
}
// create the button
var button = document.createElement('button');
button.innerHTML = '↑';
// set position and style for the button
button.style.position = "fixed";
button.style.bottom = "55px";
button.style.right = "20px";
button.style.zIndex = "999"; // always on top
button.style.width = "30px";
button.style.display = "none"; // initially hidden
button.className = "js-details-target js-title-edit-button flex-md-order-2 Button--secondary Button--small Button m-0 mr-md-0";
// trigger scrolling to top when button is clicked
button.addEventListener('click', function () {
window.scrollTo(0, 0);
});
// add the button to the page
document.body.appendChild(button);
// show the button only when not at the top
window.addEventListener("scroll", function() {
if (window.pageYOffset > 0) {
button.style.display = "block";
} else {
button.style.display = "none";
}
});
}
// This function creates a button that scrolls to bottom of the page
function EnsureScrollToBottomButton() {
const MARK = 'scroll-to-bottom-button';
if (document.querySelector(`button[${ATTR}="${MARK}"]`)) {
return;
}
// create the button
var button = document.createElement('button');
button.innerHTML = '↓';
// set position and style for the button
button.style.position = "fixed";
button.style.bottom = "20px";
button.style.right = "20px";
button.style.zIndex = "999"; // always on top
button.style.width = "30px";
button.className = "js-details-target js-title-edit-button flex-md-order-2 Button--secondary Button--small Button m-0 mr-md-0";
// trigger scrolling to bottom when button is clicked
button.addEventListener('click', function () {
window.scrollTo(0, document.body.scrollHeight);
});
// add the button to the page
document.body.appendChild(button);
// show the button only when not at the bottom
window.addEventListener("scroll", function() {
if (window.pageYOffset + window.innerHeight < document.body.scrollHeight) {
button.style.display = "block";
} else {
button.style.display = "none";
}
});
}
function EnsureCreateTransPRButtonOnPR() {
const MARK = 'create-trans-pr-button';
// Check if the button already exists
if (document.querySelector(`div[${ATTR}="${MARK}"]`)) {
return;
}
// Find the header actions container
var headerActions = document.querySelector(".gh-header-actions");
if (!headerActions) {
return;
}
// Create a container for the dropdown
var dropdownContainer = document.createElement("div");
dropdownContainer.setAttribute("class", "flex-md-order-2 position-relative");
dropdownContainer.setAttribute(ATTR, MARK);
dropdownContainer.style.display = "inline-block";
// Create the main button
var button = document.createElement("button");
button.innerHTML = 'Create Translation PR ';
button.setAttribute(
"class",
"Button--secondary Button--small Button m-0"
);
button.style.cursor = "pointer";
// Create the dropdown menu
var dropdownMenu = document.createElement("div");
dropdownMenu.style.display = "none";
dropdownMenu.style.position = "absolute";
dropdownMenu.style.right = "0";
dropdownMenu.style.top = "100%";
dropdownMenu.style.marginTop = "4px";
dropdownMenu.style.backgroundColor = "white";
dropdownMenu.style.border = "1px solid #d0d7de";
dropdownMenu.style.borderRadius = "6px";
dropdownMenu.style.boxShadow = "0 8px 24px rgba(140,149,159,0.2)";
dropdownMenu.style.zIndex = "1000";
dropdownMenu.style.minWidth = "200px";
dropdownMenu.style.padding = "4px 0";
// Create first option: Create Synced Translation PR
var syncedOption = document.createElement("div");
syncedOption.innerHTML = "Create Synced Translation PR";
syncedOption.style.padding = "8px 16px";
syncedOption.style.cursor = "pointer";
syncedOption.style.fontSize = "14px";
syncedOption.style.color = "#24292e";
syncedOption.style.whiteSpace = "nowrap";
syncedOption.addEventListener("mouseenter", function() {
syncedOption.style.backgroundColor = "#f6f8fa";
});
syncedOption.addEventListener("mouseleave", function() {
syncedOption.style.backgroundColor = "white";
});
syncedOption.addEventListener("click", function(e) {
e.stopPropagation();
dropdownMenu.style.display = "none";
EnsureToken();
CreateTransPR(true); // Create PR and trigger workflow
});
// Create second option: Create Empty Translation PR
var emptyOption = document.createElement("div");
emptyOption.innerHTML = "Create Empty Translation PR";
emptyOption.style.padding = "8px 16px";
emptyOption.style.cursor = "pointer";
emptyOption.style.fontSize = "14px";
emptyOption.style.color = "#24292e";
emptyOption.style.whiteSpace = "nowrap";
emptyOption.addEventListener("mouseenter", function() {
emptyOption.style.backgroundColor = "#f6f8fa";
});
emptyOption.addEventListener("mouseleave", function() {
emptyOption.style.backgroundColor = "white";
});
emptyOption.addEventListener("click", function(e) {
e.stopPropagation();
dropdownMenu.style.display = "none";
EnsureToken();
CreateTransPR(false); // Create PR without triggering workflow
});
// Append options to dropdown menu
dropdownMenu.appendChild(syncedOption);
dropdownMenu.appendChild(emptyOption);
// Toggle dropdown menu on button click
button.addEventListener("click", function(e) {
e.stopPropagation();
if (dropdownMenu.style.display === "none") {
dropdownMenu.style.display = "block";
} else {
dropdownMenu.style.display = "none";
}
});
// Close dropdown when clicking outside
document.addEventListener("click", function(e) {
if (!dropdownContainer.contains(e.target)) {
dropdownMenu.style.display = "none";
}
});
// Append button and dropdown to container
dropdownContainer.appendChild(button);
dropdownContainer.appendChild(dropdownMenu);
// Append container to header actions
headerActions.appendChild(dropdownContainer);
}
function Init() {
const url = window.location.href;
// If we are on the PR list page, add the comment button and file link
if (url.includes('/pulls')) {
const observer = new MutationObserver(() => {
document.querySelectorAll('div[id^="issue_"]').forEach((element) => {
EnsureFileLink(element);
})
EnsureCommentButton();
});
const config = { childList: true, subtree: true };
observer.observe(document, config);
}
// If we are on the PR details page, add the scroll to top and bottom buttons
if (url.includes('/pull/')) {
EnsureScrollToTopButton();
EnsureScrollToBottomButton();
EnsureCommentButtonOnPR();
const observer = new MutationObserver(() => {
EnsureCommentButtonOnPR();
});
const targetNode = document.body;
const observerOptions = { childList: true, subtree: true };
observer.observe(targetNode, observerOptions);
// If we are on the PR details page of pingcap/docs-cn or pingcap/docs, add the CreateTranslationPR button
if (url.includes(`${TARGET_REPO_OWNER}/docs-cn/pull`) || url.includes(`${TARGET_REPO_OWNER}/docs/pull`)) {
EnsureCreateTransPRButtonOnPR();
const observerCreateTransPR = new MutationObserver(() => {
EnsureCreateTransPRButtonOnPR();
});
observerCreateTransPR.observe(targetNode, observerOptions);
}
}
}
Init();
})();