Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions components/common/editor/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,14 @@
color: var(--color-text-light-1) !important;
}
}

/* GitHub's attach control leads with a paperclip; Dropzone renders the button, so draw it in CSS. */
.page-content.repository.view.issue.github-conversation .comment.form .ui.dropzone .dz-button::before {
content: "";
flex-shrink: 0;
width: 16px;
height: 16px;
background-color: currentcolor;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M12.212 3.02a1.753 1.753 0 0 0-2.478.003l-5.83 5.83a3 3 0 0 0-.88 2.127c0 .795.315 1.551.88 2.116.567.567 1.333.89 2.126.89.79 0 1.548-.321 2.116-.89l5.48-5.48a.75.75 0 0 1 1.061 1.06l-5.48 5.48a4.5 4.5 0 0 1-3.177 1.33c-1.2 0-2.345-.487-3.187-1.33a4.48 4.48 0 0 1-1.32-3.177c0-1.195.475-2.341 1.32-3.186l5.83-5.83a3.25 3.25 0 0 1 5.553 2.297c0 .863-.343 1.691-.953 2.301L7.439 12.39c-.375.377-.884.59-1.416.593a2 2 0 0 1-1.412-.593 1.99 1.99 0 0 1 0-2.828l5.48-5.48a.75.75 0 0 1 1.042.018.75.75 0 0 1 .018 1.042l-5.48 5.48a.49.49 0 0 0 0 .707.5.5 0 0 0 .352.154.5.5 0 0 0 .356-.154l5.833-5.827a1.755 1.755 0 0 0 0-2.481Z'/%3E%3C/svg%3E")
center / 16px no-repeat;
}
10 changes: 10 additions & 0 deletions components/repository/label/label.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{- /* An issue label whose colour reaches the theme as a variable, so its stylesheet can adapt it to light and dark
mode. Forgejo's RenderLabel writes the colours inline with !important, which no stylesheet can override. Scoped
labels keep Forgejo's two-part rendering. */ -}}
{{if .ExclusiveScope -}}
{{RenderLabel ctx .}}
{{- else -}}
{{- $description := .Description -}}
{{- if .IsArchived}}{{$description = print "(" (ctx.Locale.Tr "archived") ") " .Description}}{{end -}}
<div class="ui label repository-label{{if .IsArchived}} archived-label{{end}}" style="--repository-label-color: {{.Color}}; --repository-label-foreground: {{ContrastColor .Color}}" data-tooltip-content="{{$description}}" aria-description="{{$description}}">{{RenderEmoji ctx .Name}}</div>
{{- end}}
17 changes: 0 additions & 17 deletions public/assets/js/diff-file-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,6 @@
toggleAttribute(group, "data-tree-filtered", !visible);
visit(group, `${path}/`, level + 1);
}
if (!row.querySelector(".tree-directory-icon")) {
const icon = document.getElementById("diff-tree-icon-directory").content.firstElementChild.cloneNode(true);
icon.classList.add("tree-directory-icon");
row.insertBefore(icon, row.querySelector("span"));
}
// Reveal matching descendants when searching, including Vue's lazy subtrees.
if (revealMatches && visible && row.getAttribute("aria-expanded") === "false") row.click();
} else if (row.matches(".item-file")) {
Expand All @@ -127,18 +122,6 @@
setAttribute(row, "aria-level", level);
setAttribute(row, "aria-selected", row.classList.contains("selected"));
setAttribute(row, "tabindex", -1);
if (!row.querySelector(".tree-file-icon")) {
const kind = row.querySelector(".octicon-diff-added")
? "added"
: row.querySelector(".octicon-diff-removed")
? "removed"
: "diff";
const icon = document.getElementById(`diff-tree-icon-${kind}`)?.content.firstElementChild.cloneNode(true);
if (icon) {
icon.classList.add("tree-file-icon");
row.prepend(icon);
}
}
} else if (row.matches(".diff-file-tree-items")) visit(row, prefix, level);
}
}
Expand Down
4 changes: 0 additions & 4 deletions public/assets/js/diff-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,6 @@
});
const boxes = document.getElementById("diff-file-boxes");
function updateFoldLabels() {
// Forgejo's initial hunk labels omit the first @, while loaded excerpts include both.
for (const code of container.querySelectorAll(".tag-code .code-inner")) {
if (/^@ [+-]/.test(code.textContent)) code.prepend(document.createTextNode("@"));
}
for (const box of boxes?.querySelectorAll(".diff-file-box") || []) {
const expand = box.querySelector(".diff-expand-file");
if (expand) {
Expand Down
90 changes: 12 additions & 78 deletions public/assets/js/issue-conversation.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,14 @@
(() => {
const page = document.querySelector(".github-conversation");
if (!page) return;
const form = page.querySelector(".comment.form");
if (form && !form.querySelector(".comment-editor-heading")) {
const heading = document.createElement("h2");
heading.className = "comment-editor-heading";
heading.textContent = "Add a comment";
form.prepend(heading);
}
if (form) {
const dropzone = form.querySelector(".dropzone");
const icon = page.parentElement.querySelector("#conversation-upload-icon")?.content.firstElementChild;
if (dropzone && icon) {
const decorateUpload = () => {
const button = dropzone.querySelector(".dz-button");
if (button && !button.querySelector("svg")) button.prepend(icon.cloneNode(true));
};
decorateUpload();
new MutationObserver(decorateUpload).observe(dropzone, { childList: true, subtree: true });
}

const textarea = form.querySelector("textarea.markdown-text-editor");
const submit = form.querySelector(".button-sequence > .primary.button");
const files = form.querySelector(".dropzone .files");
if (textarea && submit) {
const updateSubmit = () => {
submit.disabled = !textarea.value.trim() && !files?.children.length;
};
textarea.addEventListener("input", updateSubmit);
if (files) new MutationObserver(updateSubmit).observe(files, { childList: true, subtree: true });
updateSubmit();
}
}
const first = page.querySelector(".comment.first");
if (first && page.classList.contains("github-issue-conversation")) {
const header = first.querySelector(".comment-header-left .text");
if (header)
for (const node of header.childNodes)
if (node.nodeType === Node.TEXT_NODE) node.textContent = node.textContent.replace(/commented/, "opened");
}
// Place the existing reaction selector beside the body, preserving its native handlers.
for (const comment of page.querySelectorAll(".timeline-item.comment:not(.form)")) {
const reaction = comment.querySelector(".comment-header .select-reaction");
const body = comment.querySelector(".comment-body");
if (reaction && body) {
const footer = document.createElement("div");
footer.className = "conversation-body-actions";
reaction.classList.remove("right");
footer.append(reaction);
body.append(footer);
}
}
const sidebar = page.querySelector(".issue-content-right");
if (sidebar) {
// Keep Forgejo's controls and their sibling relationships together.
const groups = [];
let group = [];
for (const child of [...sidebar.children]) {
if (child.classList.contains("divider")) {
groups.push({ nodes: group, divider: child });
group = [];
} else group.push(child);
}
if (group.length) groups.push({ nodes: group });
const priority = group => {
if (group.nodes.some(n => n.matches(".select-assignees-modify, .assignees"))) return 0;
if (group.nodes.some(n => n.matches(".select-label, .labels"))) return 1;
if (group.nodes.some(n => n.matches(".select-project"))) return 2;
if (group.nodes.some(n => n.id === "milestone-section")) return 3;
if (group.nodes.some(n => n.matches(".depending"))) return 4;
if (group.nodes.some(n => n.matches(".select-branch"))) return 5;
if (group.nodes.some(n => n.matches(".watching"))) return 6;
if (group.nodes.some(n => n.matches("span.text") && /participant/i.test(n.textContent))) return 7;
return 8;
};
groups.sort((a, b) => priority(a) - priority(b));
for (const group of groups) sidebar.append(...group.nodes, ...(group.divider ? [group.divider] : []));
}
// GitHub keeps Comment disabled until there is text or an attachment to send.
const form = document.querySelector(".github-conversation .comment.form");
const textarea = form?.querySelector("textarea.markdown-text-editor");
const submit = form?.querySelector(".button-sequence > .primary.button");
if (!textarea || !submit) return;
const files = form.querySelector(".dropzone .files");
const updateSubmit = () => {
submit.disabled = !textarea.value.trim() && !files?.children.length;
};
textarea.addEventListener("input", updateSubmit);
if (files) new MutationObserver(updateSubmit).observe(files, { childList: true, subtree: true });
updateSubmit();
})();
79 changes: 6 additions & 73 deletions public/assets/js/issue-list.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,8 @@
(() => {
const page = document.querySelector(".github-issues, .github-pull-list");
if (!page) return;
const isPullList = page.classList.contains("github-pull-list");
const form = page.querySelector(".issue-list-search");
const input = form?.querySelector('input[name="q"]');
if (!input) return;
// The dashboard's milestone list reuses this layout and keeps its own translated placeholder.
if (!page.classList.contains("github-milestones")) {
input.setAttribute("aria-label", isPullList ? "Search pull requests" : "Search Issues");
input.placeholder = isPullList ? "Search all pull requests" : "Search Issues";
}
const toolbar = page.querySelector("#issue-filters");
const filterRow = toolbar?.querySelector(".issue-list-toolbar-right");
if (filterRow) {
filterRow.id = "issue-filter-controls";
const toggle = document.createElement("button");
toggle.type = "button";
toggle.className = "theme-icon-button issue-mobile-filter-toggle";
toggle.setAttribute("aria-label", "Filter options");
toggle.setAttribute("aria-controls", filterRow.id);
toggle.setAttribute("aria-expanded", "false");
toggle.textContent = "…";
toolbar.append(toggle);
toggle.addEventListener("click", () => {
const open = toolbar.classList.toggle("issue-mobile-filters-open");
toggle.setAttribute("aria-expanded", open);
});
const typeFilter = filterRow.querySelector(".list-header-type");
if (typeFilter) {
const all = [...typeFilter.querySelectorAll("a")].find(a => a.href.includes("type=all"));
if (all) {
const allStates = document.createElement("a");
allStates.className = "item";
allStates.textContent = "All states";
allStates.href = toolbar.querySelector('[data-test-name="all-issue-count"]').href;
typeFilter.querySelector(".menu").append(allStates);
}
}
}
const decorateRows = () => {
for (const row of page.querySelectorAll("#issue-list > .flex-item")) {
const labels = row.querySelector(".labels-list");
const main = row.querySelector(".flex-item-main");
if (!isPullList && labels?.textContent.trim() && labels.parentElement !== main) main.append(labels);
}
};
decorateRows();
const issues = page.querySelector("#issue-list");
if (issues) new MutationObserver(decorateRows).observe(issues, { childList: true, subtree: true });
const empty = page.querySelector("#issue-list > .tw-text-center");
if (empty) {
empty.classList.add("theme-empty", "theme-empty-compact");
if (isPullList) empty.querySelector("h3").textContent = "No pull requests matched your search";
const icon = page.querySelector('[data-test-name="open-issue-count"] svg')?.cloneNode(true);
if (icon) {
icon.setAttribute("width", "24");
icon.setAttribute("height", "24");
empty.prepend(icon);
}
}
const filters = page.querySelector("#issue-filters");
if (filters) {
for (const anchor of filters.querySelectorAll(".switch > a")) {
const text = [...anchor.childNodes].find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
const match = text?.textContent.trim().match(/^([\d.,\s]+)\s+(.+)$/);
if (!match) continue;
text.textContent = match[2];
const badge = document.createElement("span");
badge.className = "theme-counter issue-state-count";
badge.textContent = match[1].trim();
anchor.append(badge);
}
}
const toolbar = document.querySelector("#issue-filters");
const toggle = toolbar?.querySelector(".issue-mobile-filter-toggle");
toggle?.addEventListener("click", () => {
const open = toolbar.classList.toggle("issue-mobile-filters-open");
toggle.setAttribute("aria-expanded", open);
});
})();
52 changes: 13 additions & 39 deletions public/assets/js/pull-review.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,16 @@
(() => {
const decorate = () => {
const box = document.querySelector(".github-pull-review .timeline-item.merge");
const section = box?.querySelector(".merge-section");
const form = section?.querySelector(":scope > #pull-request-merge-form");
if (!form) return;
const state = window.config?.pageData?.pullRequestMergeForm;
const status = section.querySelector(":scope > .item");
const check = status?.querySelector(":scope > .octicon-check");
if (check && state?.canMergeNow && state?.allOverridableChecksOk) {
box.classList.add("merge-ready");
status.classList.add("pull-merge-status");
const content = document.createElement("div");
const title = document.createElement("h3");
for (const node of [...status.childNodes]) if (node !== check) title.append(node);
content.append(title);
const note = status.nextElementSibling;
if (note?.matches(".item:has(> .octicon-unlock, > .octicon-lock)")) {
note.classList.add("pull-merge-note");
content.append(note);
}
status.append(content);
}
const instructions = section.querySelector(":scope > details.collapsible");
const actions = document.createElement("div");
actions.className = "pull-merge-actions";
actions.append(form);
if (instructions) actions.append(instructions);
section.append(actions);
// Forgejo colours the collapsed button red whenever checks are unmet, even when the action
// only schedules the merge. A label longer than the plain merge action is a scheduled merge.
const plain = (state?.mergeStyles ?? []).map(style => style.textDoMerge?.trim());
const sync = () => {
const label = form.querySelector(".merge-button .button-text")?.textContent.replace(/\s+/g, " ").trim();
actions.classList.toggle("merge-scheduled", Boolean(label) && !plain.includes(label));
};
sync();
new MutationObserver(sync).observe(form, { childList: true, subtree: true, characterData: true });
const actions = document.querySelector(".github-pull-review .merge-section > .pull-merge-actions");
const form = actions?.querySelector(":scope > #pull-request-merge-form");
if (!form) return;
// Forgejo colours the collapsed button red whenever checks are unmet, even when the action
// only schedules the merge. A label longer than the plain merge action is a scheduled merge.
const plain = (window.config?.pageData?.pullRequestMergeForm?.mergeStyles ?? []).map(style =>
style.textDoMerge?.trim()
);
const sync = () => {
const label = form.querySelector(".merge-button .button-text")?.textContent.replace(/\s+/g, " ").trim();
actions.classList.toggle("merge-scheduled", Boolean(label) && !plain.includes(label));
};
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", decorate, { once: true });
else decorate();
sync();
new MutationObserver(sync).observe(form, { childList: true, subtree: true, characterData: true });
})();
11 changes: 0 additions & 11 deletions public/assets/js/repo-releases.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,6 @@ for (const digest of document.querySelectorAll(".github-release-digest[data-dige
})
.catch(() => digest.remove());
}
for (const status of document.querySelectorAll('a[data-tippy="commit-statuses"]')) {
const icon = status.querySelector("svg");
const label = icon?.classList.contains("yellow")
? "Checks pending"
: icon?.classList.contains("green")
? "Checks passed"
: icon?.classList.contains("red")
? "Checks failed"
: "Checks";
status.setAttribute("aria-label", label);
}
const releaseMenu = document.querySelector(".github-release-mobile-toc");
releaseMenu?.addEventListener("click", event => {
if (event.target.closest("a")) releaseMenu.open = false;
Expand Down
38 changes: 0 additions & 38 deletions public/assets/js/repository-labels.js

This file was deleted.

Loading
Loading