// ==UserScript==
// @name PR三思器
// @namespace http://tampermonkey.net/
// @version V1.2.2
// @description 创建PR前,提醒一下有没有一些遗漏的东西需要检查
// @author liaw
// @match https://code.fineres.com/*/pull-requests?create*
// @icon https://code.fineres.com/projects/FX/avatar.png?s=64&v=1452596397000
// @grant GM_addStyle
// @grant unsafeWindow
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const prCheckerStyle = `
:root {
--fd-color-border: #d7d9dc;
--fd-color-text: #141e31;
--fd-color-white: #ffffff;
--fd-color-text-light-solid: #ffffff;
--fd-color-primary: #00b899;
--fd-color-primary-hover: #4dcdb8;
}
.pr-checker-create-btn {
position: relative;
display: inline-block;
cursor: pointer;
margin-right: 9px;
}
.pr-checker-create-btn:hover #submit-form {
--aui-btn-bg: var(--aui-button-primary-hover-bg-color);
--aui-btn-text: var(--aui-button-primary-active-text-color);
}
#bitbucket-pr-checker {
font-size: 14px;
color: var(--fd-color-text);
border: none;
border-radius: 8px;
background: #ffffff;
width: 500px;
padding: 0;
box-shadow: 0 9px 28px 8px #0000000d, 0 3px 6px -4px #0000001f,
0 6px 16px #00000014;
}
#bitbucket-pr-checker::backdrop {
background-color: rgba(0, 10, 31, 0.29);
}
#bitbucket-pr-checker .pr-checker-title {
border-bottom: 1px solid var(--fd-color-border);
padding: 16px 20px;
font-size: 18px;
line-height: 26px;
font-weight: 700;
}
#pr-checker-btns {
margin-top: 14px;
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 12px 20px;
border-top: 1px solid var(--fd-color-border);
}
#pr-checker-btns .operate-btn {
border: 1px solid;
border-radius: 4px;
line-height: 32px;
padding: 0px 16px;
outline: none;
cursor: pointer;
transition: box-shadow 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
background 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
border-color 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
color 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
}
#pr-checker-btns .pr-checker-close-btn {
background: var(--fd-color-white);
border-color: var(--fd-color-border);
}
#pr-checker-btns .pr-checker-close-btn:hover {
color: var(--fd-color-primary-hover);
border-color: var(--fd-color-primary-hover);
}
#pr-checker-btns .pr-checker-ensure-btn {
color: var(--fd-color-text-light-solid);
background: var(--fd-color-primary);
border-color: var(--fd-color-primary);
}
#pr-checker-btns .pr-checker-ensure-btn:hover {
background: var(--fd-color-primary-hover);
}
.pr-checker-mask-btn {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
`;
GM_addStyle(prCheckerStyle);
// 最大寻找次数,脚本加载后,即在页面中寻找创建按钮,查找次数超过50次后即认为当前页面没有创建按钮
const MAX_FIND_COUNT = 50;
const USERNAME =
document.querySelector("[data-username]")?.dataset.username || "";
// 自定义check项的本地缓存
const CUSTOM_CHECK_ITEMS_KEY = `bitbucket.pr.checker.${USERNAME}`;
// 创建元素函数
const createElement = ({
parent = document.body,
tagName = "div",
text = "",
attributes = {},
}) => {
const element = document.createElement(tagName);
element.innerText = text;
Object.keys(attributes).forEach((key) =>
element.setAttribute(key, attributes[key])
);
if (parent) {
parent.appendChild(element);
}
return element;
};
/**
* 初始化PR检查器
* 脚本支持在浏览器控制台,通过 window.PrChecker.add('xxx') 的方式添加自定义check项
* 也支持通过 window.PrChecker.clear() 的方式清除所有自定义check项
*/
const initPrChecker = () => {
const addCustomCheckItems = (...checkItems) => {
const cachedItems =
JSON.parse(window.localStorage.getItem(CUSTOM_CHECK_ITEMS_KEY)) || [];
const uniqItems = [...new Set(checkItems.map((item) => item.toString()))];
const customCheckItems = [...new Set([...cachedItems, ...uniqItems])];
window.localStorage.setItem(
CUSTOM_CHECK_ITEMS_KEY,
JSON.stringify(customCheckItems)
);
};
const clearCustomCheckItems = () => {
window.localStorage.removeItem(CUSTOM_CHECK_ITEMS_KEY);
};
/**
* 注意这里必须用 unsafeWindow,否则无法在浏览器控制台访问 PrChecker
* @see https://www.tampermonkey.net/documentation.php#api:unsafeWindow
* @see https://bbs.tampermonkey.net.cn/thread-249-1-1.html#%E7%BB%99%E8%AE%BA%E5%9D%9B%E6%B7%BB%E5%8A%A0%E9%BB%91%E5%A4%9C%E6%A8%A1%E5%BC%8F
*/
unsafeWindow.PrChecker = {};
unsafeWindow.PrChecker.add = addCustomCheckItems;
unsafeWindow.PrChecker.clear = clearCustomCheckItems;
};
// 获取检查项
const getCheckItems = () => {
const customCheckItems =
JSON.parse(
window.localStorage.getItem(`bitbucket.pr.checker.${USERNAME}`)
) || [];
return [
"copy的代码检查了吗?",
"移动端漏了吗?",
"CRM漏了吗?",
"KMS漏了吗?",
"任务号有没有关联错?",
"目标分支提对了吗?",
"国际化有没有处理好?",
...customCheckItems,
];
};
// 创建检查项列表
const createCheckItems = (parent) => {
const fragment = document.createDocumentFragment();
getCheckItems().forEach((item) => {
createElement({ parent: fragment, tagName: "li", text: item });
});
parent.innerHTML = "";
parent.appendChild(fragment);
};
// 创建对话框
const createDialog = (createPrBtn) => {
const existingDialog = document.getElementById("bitbucket-pr-checker");
if (existingDialog) {
createCheckItems(existingDialog.querySelector("#pr-check-items"));
return existingDialog;
}
// 使用文档片段批量创建元素
const fragment = document.createDocumentFragment();
const dialog = createElement({
parent: fragment,
tagName: "dialog",
attributes: { id: "bitbucket-pr-checker" },
});
// 创建标题
createElement({
parent: dialog,
text: "创建PR前请检查以下几项!",
attributes: { class: "pr-checker-title" },
});
// 创建检查项列表
const checkItemsWrapper = createElement({
parent: dialog,
tagName: "ol",
attributes: { id: "pr-check-items" },
});
createCheckItems(checkItemsWrapper);
// 创建按钮组
const btnWrapper = createElement({
parent: dialog,
attributes: { id: "pr-checker-btns" },
});
// 创建按钮
const closeBtn = createElement({
parent: btnWrapper,
tagName: "button",
text: "还需调整",
attributes: { class: "operate-btn pr-checker-close-btn" },
});
closeBtn.onclick = () => dialog.close();
const ensureBtn = createElement({
parent: btnWrapper,
tagName: "button",
text: "确认创建",
attributes: { class: "operate-btn pr-checker-ensure-btn" },
});
ensureBtn.onclick = () => {
dialog.close();
createPrBtn.click();
};
// 将片段一次性插入文档
document.body.appendChild(fragment);
return dialog;
};
// 轮询查找创建PR按钮
const findCreatePrBtn = () => {
let findCount = 0;
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
const createPrBtn = document.getElementById("submit-form");
if (createPrBtn) {
clearInterval(interval);
// 因为创建PR的按钮是一个input标签,无法插入子元素,所以需要一个包装元素
const createBtnWrapper = createElement({
parent: null,
attributes: {
class: "pr-checker-create-btn",
},
});
createPrBtn.parentNode.insertBefore(createBtnWrapper, createPrBtn);
createPrBtn.parentNode.removeChild(createPrBtn);
createBtnWrapper.appendChild(createPrBtn);
resolve({ createBtnWrapper, createPrBtn });
} else if (findCount > MAX_FIND_COUNT) {
clearInterval(interval);
reject(new Error("Create PR button doesn't exist"));
}
findCount++;
}, 200);
});
};
findCreatePrBtn()
.then(({ createBtnWrapper, createPrBtn }) => {
initPrChecker();
const maskBtn = createElement({
parent: createBtnWrapper,
attributes: { class: "pr-checker-mask-btn" },
});
maskBtn.onclick = (e) => {
e.stopPropagation();
const dialog = createDialog(createPrBtn);
dialog.showModal();
};
})
.catch((err) => console.error(err));
})();