Article écrit par Martin Catty
GitLab
For the past years we, at Synbioz, have been heavy users of GitLab. It progressively became our central tool for day to day job.
This choice has mostly been driven by its devops capabilities.
Otherwise, I have still found its UX quite painful when it comes to change a label on a given issue.
What’s wrong with label change on GitLab ?
We have a common workflow based on boards, moving an issue to the right or to the left. Original, isn’t it ?
We also have a few automations to help us create boards, milestones etc… automatically ; even define permissions on the whole GitLab, but that’s another story.
For now, here is what our boards headers look like :
Sure we can easily drag’n’drop the issue from a column to an other.
When we are doing some testing we can’t satisfied with the issue’s title only.
So let’s consider this issue is OK. I will have to remove the current label to validate::functionally
and put another one : to validate::technically
.
To do so I have to go through no less than 6 (!) steps :
- click edit
- type a few characters
- scroll to the wanted label or use the arrows
- click or hit enter to select it
- do not forget to «exit» the dropdown to apply the label (you can’t use
esc
key) - remove the old label (this one is easy, just click on the cross)
This is far from ideal from my point of view. When you process a lot of issues this is a massive waste of time and error-prone.
Label proposal
Considering there is always a human decision behind label changes, we can’t automatically apply a label based on another one removal or addition.
That’s why I decided to make a tiny userscript offering a new section right into the sidebar, called Labels proposal
.
Based on the current label and some predefined «states machine», the interface offers to apply the next label.
Clicking on a proposed label will set it on the issue and remove the other ones (only those related to the states we defined).
Removing the labels (by simulating a click on the cross) has the advantage of refreshing this part of the toolbar.
Show me the code
This userscript needs you to install an extension offering the ability to run userscripts beforehand.
I have been a long time user of Greasemonkey that now seems to be superseeded by Tampermonkey.
So this is just JS code that will run inside your browser. Tampermonkey lets you decide when you want to run the script. Could be once the document starts to load, ends to load, or everything has been loaded and executed (especially other scripts). There is also a few other choices detailed in the documentation.
I chose the last one because I needed to ensure labels have been loaded before trying to manipulate them.
// ==UserScript== // @name GitLab label proposal // @namespace https://www.synbioz.com/ // @version 0.2.0 // @description Label proposal for GitLab // @author Martin Catty // @include /^https://git.synbioz.com/.*/issues/d // @icon https://www.google.com/s2/favicons?domain=gitlab.com // @run-at document-idle // @grant none // ==/UserScript== (function () { Array.prototype.compact = function () { return this.filter((x) => x !== undefined && x !== null); }; const states = [ ["to do", "doing"], ["doing", "to validate::functionally"], ["to validate::functionally", ["to do", "to validate::technically"]], ["to validate::technically", "to deploy::staging"], ["to deploy::staging", "to deploy::production"] ]; const initialStates = states.map((node) => node[0]); const transitions = new Map(states); const label = document.querySelector(".labels"); // Create a map from existing labels, using this form : // { "label" => [node1, node2] } const labelsToNodes = Array.from( document.querySelectorAll(".labels .gl-label-text") ).reduce((map, node) => { const sibling = node.nextElementSibling; let scope = ""; if (sibling !== undefined && sibling !== null) { scope = "::" + sibling.textContent.trim(); } const content = node.textContent.trim() + scope; // we don't want to keep track of label's node not part // of the states we are watching if (initialStates.includes(content)) { map.set(content, [node, sibling].compact()); } return map; }, new Map()); const nextLabels = Array.from(labelsToNodes.keys()) .map((label) => transitions.get(label)) .compact() .flat(); const header = ` <div class="title hide-collapsed gl-mb-3"> Labels proposal </div> `; const html = nextLabels.reduce((acc, label) => { acc += ` <div class="hide-collapsed value issuable-show-labels js-value has-labels"> <span data-qa-selector="selected_label_content" data-qa-label-name="to do" class="gl-label gl-label-text-light" style="--label-background-color:#D10069; --label-inset-border:inset 0 0 0 2px #D10069;"> <a href="#" class="gl-link gl-label-link"> <span class="gl-label-text label-proposal"> ${label} </span> </a> </span> </div> `; return acc; }, header); const block = document.createElement("div"); block.className = "block"; label.after(block); block.innerHTML = html; const csrf = document .querySelector('meta[name="csrf-token"]') .getAttribute("content"); document.querySelectorAll(".label-proposal").forEach((node) => { node.addEventListener("click", (event) => { event.preventDefault(); const label = event.target.textContent.trim(); const url = `/api/v4/projects/${document.body.dataset.projectId}/issues/${ document.body.dataset.pageTypeId }?add_labels=${encodeURIComponent(label)}`; const headers = new Headers({ "Content-Type": "application/json", "x-csrf-token": csrf, Accept: "application/json" }); fetch(url, { method: "PUT", headers: headers, withCredentials: true }).then((response) => { if (response.ok) { node.remove(); // remove existing labels labelsToNodes.forEach((nodes, key) => { // there can be 2 nodes (including the scope), we only need one, // they have the same parent const node = nodes[0]; const cross = node.parentNode.nextElementSibling; if (cross) { const event = new Event("click"); cross.dispatchEvent(event); } }); } }); }); }); })();
You may not be able to use this script as is, as it’s meant for our own workflow, but it should be easy to update and enhance.
I hope this will help some, do not hesitate to leave a message on Twitter.