Created
September 2, 2024 11:41
-
-
Save fgilio/fa2ac736c9453f40551e199343f42c3e to your computer and use it in GitHub Desktop.
GitLab MR "Expand and Collapse all"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.gitlab-mreca-top-0 { | |
top: 0 !important; | |
} | |
.gitlab-mreca-mr-version-controls-sticky { | |
position: sticky; | |
top: 72px; | |
z-index: 999; | |
padding-bottom: 3px; | |
background-color: white; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* GitLab MR "Expand and Collapse all" Extension Script | |
* | |
* This script enhances the GitLab interface by adding a "Collapse all" button | |
* next to the "Expand all" button in merge request views. Additionally, | |
* it changes the "Expand all files" button text to "Expand all". | |
* | |
* GitLab has not provided this functionality for at least the last 10 years (as of 2024.09.02), | |
* leaving users to manually collapse file contents after expanding them. This script automates | |
* that process, improving workflow efficiency. | |
* | |
* The script observes the DOM to detect when relevant elements are loaded, since GitLab renders | |
* the page dynamically. It uses MutationObservers to identify when these elements appear and | |
* modifies them accordingly. | |
* | |
* Note: This script is designed for use as an Arc Browser boost, but should | |
* be compatible with Tampermonkey or similar browser extensions. | |
*/ | |
// Function to log debug information | |
const logDebug = (message) => { | |
console.log(`[GitLab Extension Debug] ${message}`); | |
}; | |
// Function to modify the "Expand all files" span text | |
const modifyExpandAllText = (buttonElement) => { | |
logDebug("Attempting to modify the text of the 'Expand all files' button."); | |
const expandAllSpan = Array.from(buttonElement.querySelectorAll('span')) | |
.find(span => span.textContent.trim() === "Expand all files"); | |
if (expandAllSpan && expandAllSpan.textContent !== "Expand all") { | |
expandAllSpan.textContent = "Expand all"; | |
logDebug("Text successfully modified to 'Expand all'."); | |
} else { | |
logDebug("Span with text 'Expand all files' not found or already modified."); | |
} | |
}; | |
// Function to add a "Collapse all" button next to the "Expand all" button | |
const addCollapseAllButton = (expandAllButton) => { | |
logDebug("Attempting to add the 'Collapse all' button."); | |
if (expandAllButton.nextElementSibling && expandAllButton.nextElementSibling.textContent === "Collapse all") { | |
logDebug("'Collapse all' button already exists."); | |
return; | |
} | |
const collapseButton = document.createElement('button'); | |
collapseButton.className = "btn gl-mr-3 btn-default btn-md gl-button"; | |
collapseButton.textContent = "Collapse all"; | |
collapseButton.addEventListener('click', () => { | |
logDebug("'Collapse all' button clicked."); | |
const collapseAllFiles = () => { | |
const hideFileElements = document.querySelectorAll('[aria-label="Hide file contents"]'); | |
hideFileElements.forEach(element => element.click()); | |
logDebug(`Clicked ${hideFileElements.length} elements to hide file contents.`); | |
if (hideFileElements.length > 0) { | |
setTimeout(collapseAllFiles, 50); | |
} | |
}; | |
collapseAllFiles(); | |
}); | |
expandAllButton.parentElement.insertBefore(collapseButton, expandAllButton.nextSibling); | |
logDebug("'Collapse all' button successfully added."); | |
}; | |
// Debounce function to limit the rate of function execution | |
const debounce = (func, wait) => { | |
let timeout; | |
return (...args) => { | |
clearTimeout(timeout); | |
timeout = setTimeout(() => func.apply(this, args), wait); | |
}; | |
}; | |
// Function to observe the mr-version-controls div and the "Expand all" button | |
const observeElements = () => { | |
logDebug("Starting observation for mr-version-controls div and 'Expand all' button."); | |
const observer = new MutationObserver(debounce((mutationsList) => { | |
const mrVersionControls = document.querySelector('.mr-version-controls'); | |
if (mrVersionControls) { | |
logDebug("mr-version-controls div found."); | |
const expandAllButton = Array.from(mrVersionControls.querySelectorAll('button')) | |
.find(button => button.textContent.trim().includes("Expand all")); | |
if (expandAllButton) { | |
logDebug("'Expand all' button found."); | |
modifyExpandAllText(expandAllButton); | |
addCollapseAllButton(expandAllButton); | |
observer.disconnect(); // Stop observing after modifications | |
logDebug("Observer disconnected after modifications."); | |
} | |
} | |
}, 100)); | |
observer.observe(document.body, { childList: true, subtree: true }); | |
}; | |
// Function to toggle classes based on the visibility of the issue-sticky-header | |
const toggleStickyHeaderClasses = (isVisible) => { | |
const topBarFixed = document.querySelector('div.top-bar-fixed.container-fluid'); | |
const contentWrapper = document.querySelector('div.content-wrapper'); | |
const issueStickyHeader = document.querySelector('div.issue-sticky-header.merge-request-sticky-header'); | |
const mrVersionControls = document.querySelector('div.mr-version-controls'); | |
const diffsTabPane = document.querySelector('div.diffs.tab-pane.active'); | |
if (!topBarFixed || !contentWrapper || !issueStickyHeader || !mrVersionControls || !diffsTabPane) { | |
logDebug("One or more elements not found, skipping toggleStickyHeaderClasses."); | |
return; | |
} | |
if (isVisible) { | |
topBarFixed.classList.add('gl-invisible'); | |
contentWrapper.classList.add('pt-0'); | |
issueStickyHeader.classList.add('gitlab-mreca-top-0'); | |
mrVersionControls.classList.add('gitlab-mreca-mr-version-controls-sticky'); | |
diffsTabPane.classList.add('pt-8'); | |
} else { | |
topBarFixed.classList.remove('gl-invisible'); | |
contentWrapper.classList.remove('pt-0'); | |
issueStickyHeader.classList.remove('gitlab-mreca-top-0'); | |
mrVersionControls.classList.remove('gitlab-mreca-mr-version-controls-sticky'); | |
diffsTabPane.classList.remove('pt-8'); | |
} | |
}; | |
// Function to observe the issue-sticky-header element | |
const observeStickyHeader = () => { | |
logDebug("Starting observation for issue-sticky-header visibility changes."); | |
const observer = new MutationObserver(debounce((mutationsList) => { | |
const stickyHeader = document.querySelector('div.merge-request div.issue-sticky-header'); | |
if (stickyHeader) { | |
const isVisible = !stickyHeader.classList.contains('gl-invisible'); | |
toggleStickyHeaderClasses(isVisible); | |
} | |
}, 100)); | |
const targetNode = document.querySelector('div.merge-request'); | |
if (targetNode) { | |
observer.observe(targetNode, { attributes: true, subtree: true, attributeFilter: ['class'] }); | |
} | |
}; | |
const inMrPage = window.location.pathname.includes('merge_requests'); | |
// Start the initial observer when the DOM content is loaded | |
document.addEventListener('DOMContentLoaded', () => { | |
logDebug("DOM fully loaded, initializing observer for mr-version-controls and 'Expand all' button."); | |
if (inMrPage) { | |
observeElements(); | |
observeStickyHeader(); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment