Merge auxiliary changes by the same author
Ajankohdalta
// ==UserScript==
// @name Bugzilla - Merge "Updated" changes
// @description Merge auxiliary changes by the same author
// @namespace RainSlide
// @author RainSlide
// @license AGPL-3.0-or-later
// @version 1.0
// @icon https://bugzilla.mozilla.org/extensions/BMO/web/images/favicon.ico
// @match https://bugzilla.mozilla.org/show_bug.cgi?*
// @grant none
// @inject-into content
// @run-at document-end
// ==/UserScript==
"use strict";
const $ = (tagName, ...props) => Object.assign(
document.createElement(tagName), ...props
);
const css = `.activity .changes-container {
display: flex;
align-items: center;
}
.activity .changes-separator {
display: inline-block;
transform: scaleY(2.5);
white-space: pre;
}
.activity .change-name,
.activity .change-time {
font-size: var(--font-size-medium);
}
.changes-container:target,
.change:target {
outline: 2px solid var(--focused-control-border-color);
}`;
document.head.append($("style", { textContent: css }));
// Continuous groups of:
// 1. auxiliary .change-set (.change-set with no comment text, id starts with "a")
// 2. by the same author
const aGroups = [];
let currentAuthor = null;
let newGroup = true;
document.querySelectorAll("#main-inner > .change-set").forEach(changeSet => {
// check if is auxiliary change set
if (changeSet.id[0] !== "a") {
// no, no longer continuous, add a new group for next auxiliary change set
newGroup = true;
return;
}
// get & check author vcard
const author = changeSet.querySelector(":scope .change-author > .vcard");
if (author === null) {
throw new TypeError('Element ".change-set .change-author > .vcard" not found!');
}
// check if is the same author
if (author.textContent !== currentAuthor) {
// different author, set currentAuthor, add a new group directly
currentAuthor = author.textContent;
aGroups.push([changeSet]);
newGroup = false;
} else if (!newGroup) {
// same author, push to current group
aGroups.at(-1).push(changeSet);
} else {
// same author, add a new group
aGroups.push([changeSet]);
newGroup = false;
}
});
// "move" an id, from an element, to another element
const moveId = (from, to) => {
const id = from.id;
from.removeAttribute("id");
to.id = id;
};
// append .change to .activity, create container if needed
const appendChanges = (changeSet, activity, isFirst) => {
// get & check .change element(s)
const changes = changeSet.querySelectorAll(":scope > .activity > .change");
if (changes.length === 0) {
throw new TypeError('Element(s) ".change-set > .activity > .change" not found!');
}
// get name & time
const tr = changeSet.querySelector(
':scope > .change > .change-head > tbody > tr[id^="ar-a"]:nth-of-type(2)'
);
const td = tr?.querySelector(":scope > td:only-child");
// move name & time into .change or .changes-container, append .changes-container
if (tr && td) {
if (changes.length > 1) {
// a group of .change, create container for nameTime & themselves
const container = $("div", { className: "changes-container" });
const group = $("div", { className: "changes" });
const nameTime = $("div", { id: tr.id });
const separator = $("span", { className: "changes-separator", textContent: "| " });
nameTime.append(...td.childNodes, separator);
group.append(...changes);
container.append(nameTime, group);
tr.remove();
// appending .changes-container
// "move" an id onto another existing element might mess up the :target highlight,
// so skip that for the first
if (!isFirst) {
moveId(changeSet, container);
}
// but, first .changes-container needs append!
activity.append(container);
return;
} else {
// only one .change, don't create container, just move nameTime to changes[0]
const nameTime = $("span", { id: tr.id });
nameTime.append(...td.childNodes, "| ");
changes[0].prepend(nameTime);
tr.remove();
// no return here, append in if (!isFirst) ... below
}
}
// appending .change / a group of .change
// first doesn't need move id, see before;
// first .change is already in .activity, doesn't need append either.
if (!isFirst) {
moveId(changeSet, changes[0]);
activity.append(...changes);
}
};
// merge the .change of each aGroup into the first .change-set with appendChanges()
aGroups.forEach(group => {
if (group.length < 2) return;
const first = group[0];
const activity = first.querySelector(":scope > .activity");
appendChanges(first, activity, true);
// starts from 1 to skip the first change set
for (let i = 1; i < group.length; i++) {
appendChanges(group[i], activity);
group[i].remove();
}
});