Bugzilla - Merge "Updated" changes

Merge auxiliary changes by the same author

Устаревшая версия за 03.01.2025. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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();
	}
});