您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Insert predefined review tags with emoji and tooltips (shown below) in GitLab MRs
// ==UserScript== // @name GitLab Review Tags // @namespace http://tampermonkey.net/ // @version 1.3 // @description Insert predefined review tags with emoji and tooltips (shown below) in GitLab MRs // @exclude https://gitlab.com/*/merge_requests/new* // @match https://gitlab.com/*/merge_requests/* // @icon https://www.google.com/s2/favicons?sz=64&domain=gitlab.com // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const TAGS = [ { label: '[question]', emoji: '❓', emojiCode: ':question:', title: 'I’m asking for clarification, not blocking approval.' }, { label: '[nitpick]', emoji: '🔍', emojiCode: ':mag:', title: 'Very minor; doesn’t block, can fix later or skip.' }, { label: '[info]', emoji: 'ℹ️', emojiCode: ':information_source:', title: 'Providing context or background, no action needed.' }, { label: '[suggestion]', emoji: '💡', emojiCode: ':bulb:', title: 'Optional improvement idea, not a requirement.' }, { label: '[blocking]', emoji: '⛔', emojiCode: ':no_entry:', title: 'This must be addressed before approval.' }, { label: '[trust]', emoji: '🤝', emojiCode: ':handshake:', title: 'I didn’t check this part deeply but I trust your expertise.' }, { label: '[follow-up]', emoji: '📌', emojiCode: ':pushpin:', title: 'Let’s merge, but create a ticket to improve or clean up later.' }, { label: '[praise]', emoji: '🙌', emojiCode: ':raised_hands:', title: 'Explicitly calling out something well done or elegant.' }, { label: '[discussion]', emoji: '🗣️', emojiCode: ':speaking_head:', title: 'Not blocking, but we should talk as a team about this approach or pattern.' } ]; const STYLE = ` :root { --tooltip-bg: #fafafa; --tooltip-color: #303030; --tag-bg: #fafafa; --tag-border: #bfbfbf; --button-border: #bfbfbf; --button-bg: #fafafa; --button-hover-bg: #dbdbdb; --button-hover-border: #428fdc; } body.gl-dark { --tooltip-bg: var(--gray-10); --tooltip-color: var(--gray-950); --tag-bg: var(--gray-10); --tag-border: var(--gray-100); --button-border: var(--gray-100); --button-bg: #1f1f1f; --button-hover-bg: #303030; --button-hover-border: #428fdc; } .gl-tag-inserter { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 6px; margin: 6px 0; padding: 6px; background-color: var(--tag-bg); border: 1px solid var(--tag-border); border-radius: 8px; font-family: system-ui, sans-serif; animation: fadeIn 0.3s ease-out; } .gl-tag-inserter button { background-color: var(--button-bg); border: 1px solid var(--button-border); border-radius: 20px; padding: 6px 10px; font-size: 13px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; position: relative; } .gl-tag-inserter button:hover { background-color: var(--button-hover-bg); border: 1px solid var(--button-hover-border); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } .gl-tag-inserter button::after { content: attr(data-tooltip); position: absolute; top: 30px; left: 50%; transform: translateX(-50%); opacity: 0; background-color: var(--tooltip-bg); color: var(--tooltip-color); padding: 6px 8px; border-radius: 6px; font-size: 12px; white-space: pre-wrap; pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease; z-index: 9999; width: 120px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); } .gl-tag-inserter button:hover::after { opacity: 1; transform: translateX(-50%) translateY(4px); } @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } `; function injectStyle() { if (document.getElementById('gl-tag-style')) return; const style = document.createElement('style'); style.id = 'gl-tag-style'; style.innerText = STYLE; document.head.appendChild(style); } function createTagButton(tag, textarea) { const btn = document.createElement('button'); btn.innerHTML = `${tag.emoji} ${tag.label}`; btn.setAttribute('data-tooltip', tag.title); btn.addEventListener('click', (e) => { e.preventDefault(); const prefix = `${tag.emojiCode} ${tag.label} `; if (!textarea.value.startsWith(prefix)) { const newValue = prefix + textarea.value; textarea.value = newValue; textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); textarea.focus(); } }); return btn; } function injectButtonsAboveTextarea(textarea) { if (textarea.dataset.glTagInjected) return; const wrapper = document.createElement('div'); wrapper.className = 'gl-tag-inserter'; TAGS.forEach(tag => wrapper.appendChild(createTagButton(tag, textarea))); textarea.parentNode.insertBefore(wrapper, textarea); textarea.dataset.glTagInjected = 'true'; } function scanAndInject() { document.querySelectorAll('textarea').forEach(injectButtonsAboveTextarea); } injectStyle(); const observer = new MutationObserver(() => { scanAndInject(); }); observer.observe(document.body, { childList: true, subtree: true }); scanAndInject(); })();