Files
desktop/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs

262 lines
7.2 KiB
JavaScript

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { nsZenLiveFolderProvider } from "resource:///modules/zen/ZenLiveFolder.sys.mjs";
export class nsGithubLiveFolderProvider extends nsZenLiveFolderProvider {
static type = "github";
constructor({ id, state, manager }) {
super({ id, state, manager });
this.state.url = "https://github.com/issues/assigned";
this.state.type = state.type;
this.state.options = state.options ?? {};
this.state.repos = new Set(state.repos ?? []);
this.state.options.repoExcludes = new Set(state.options.repoExcludes ?? []);
}
async fetchItems() {
try {
const hasAnyFilterEnabled =
(this.state.options.authorMe ?? false) ||
(this.state.options.assignedMe ?? true) ||
(this.state.options.reviewRequested ?? false);
if (!hasAnyFilterEnabled) {
return "zen-live-folder-github-no-filter";
}
const searchParams = this.#buildSearchOptions();
const url = `${this.state.url}?${searchParams}`;
const { text, status } = await this.fetch(url);
// Assume no auth
if (status === 404) {
return "zen-live-folder-github-no-auth";
}
const document = new DOMParser().parseFromString(text, "text/html");
const issues = document.querySelectorAll(
"div[class^=IssueItem-module__defaultRepoContainer]"
);
const items = [];
const activeRepos = new Set();
if (issues.length) {
const authors = document.querySelectorAll("a[class^=IssueItem-module__authorCreatedLink]");
const titles = document.querySelectorAll("div[class^=Title-module__container]");
const links = document.querySelectorAll('[data-testid="issue-pr-title-link"]');
for (let i = 0; i < issues.length; i++) {
const [rawRepo, rawNumber] = issues[i].childNodes;
const author = authors[i]?.textContent;
const title = titles[i]?.textContent;
const issueUrl = links[i]?.href;
const repo = rawRepo.textContent?.trim();
if (repo) {
activeRepos.add(repo);
}
const numberMatch = rawNumber?.textContent?.match(/[0-9]+/);
const number = numberMatch?.[0] ?? "";
items.push({
title,
subtitle: author,
icon: "chrome://browser/content/zen-images/favicons/github.svg",
url: "https://github.com" + issueUrl,
id: `${repo}#${number}`,
});
}
}
this.state.repos = activeRepos;
return items;
} catch (error) {
console.error("Error fetching or parsing GitHub issues:", error);
return "zen-live-folder-failed-fetch";
}
}
#buildSearchOptions() {
let searchParams = new URLSearchParams();
const options = [
{
value: "state:open",
enabled: true,
},
{
value: "sort:updated-desc",
enabled: true,
},
[
{
value: "is:pr",
enabled: this.state.type === "pull-requests",
},
{
value: "is:issue",
enabled: this.state.type === "issues",
},
],
[
{
value: "author:@me",
enabled: this.state.options.authorMe ?? false,
},
{
value: "assignee:@me",
enabled: this.state.options.assignedMe ?? true,
},
{
value: "review-requested:@me",
enabled: this.state.options.reviewRequested ?? false,
},
],
];
const excluded = this.state.options.repoExcludes;
for (const repo of excluded) {
if (repo && repo.trim()) {
options.push({ value: `-repo:${repo.trim()}`, enabled: true });
}
}
let outputString = "";
for (const option of options) {
if (Array.isArray(option)) {
const enabledOptions = option.filter((x) => x.enabled).map((x) => x.value);
if (enabledOptions.length) {
outputString += ` (${enabledOptions.join(" OR ")}) `;
}
continue;
}
if (option.enabled) {
outputString += ` ${option.value} `;
}
}
searchParams.set("q", outputString.trim().replace(/ +(?= )/g, ""));
return searchParams.toString();
}
get options() {
const excluded = this.state.options.repoExcludes;
const repoOptions = Array.from(this.state.repos.union(excluded))
.sort((a, b) => a.localeCompare(b))
.map((repo) => ({
l10nId: "zen-live-folder-github-option-repo",
l10nArgs: { repo },
key: "repoExclude",
value: repo,
type: "checkbox",
checked: !excluded.has(repo),
}));
if (repoOptions.length) {
repoOptions.push({ type: "separator" });
}
repoOptions.push({
l10nId: "zen-live-folder-github-option-repo-list-note",
disabled: true,
});
return [
{
l10nId: "zen-live-folder-github-option-author-self",
key: "authorMe",
checked: this.state.options.authorMe ?? false,
},
{
l10nId: "zen-live-folder-github-option-assigned-self",
key: "assignedMe",
checked: this.state.options.assignedMe ?? true,
},
{
l10nId: "zen-live-folder-github-option-review-requested",
key: "reviewRequested",
checked: this.state.options.reviewRequested ?? false,
hidden: this.state.type === "issues",
},
{ type: "separator" },
{
l10nId: "zen-live-folder-github-option-repo-filter",
key: "repoExclude",
options: repoOptions,
// 1 repo + separator + note = 3 options, so if we have less than 4 options it means we don't have any repo to exclude
disabled: repoOptions.length < 4,
},
];
}
onOptionTrigger(option) {
super.onOptionTrigger(option);
const key = option.getAttribute("option-key");
const checked = option.getAttribute("checked") === "true";
if (!this.options.some((x) => x.key === key)) {
return;
}
if (key === "repoExclude") {
const repo = option.getAttribute("option-value");
if (!repo) {
return;
}
const excluded = this.state.options.repoExcludes;
if (checked) {
excluded.delete(repo);
} else {
excluded.add(repo);
}
this.state.options.repoExcludes = excluded;
} else {
this.state.options[key] = checked;
}
this.refresh();
this.requestSave();
}
async onActionButtonClick(errorId) {
super.onActionButtonClick(errorId);
switch (errorId) {
case "zen-live-folder-github-no-auth": {
const tab = this.manager.window.gBrowser.addTrustedTab("https://github.com/login");
this.manager.window.gBrowser.selectedTab = tab;
break;
}
case "zen-live-folder-github-no-filter": {
this.refresh();
break;
}
}
}
serialize() {
return {
state: {
...this.state,
repos: Array.from(this.state.repos),
options: {
...this.state.options,
repoExcludes: Array.from(this.state.options.repoExcludes),
},
},
};
}
}