OXYGENHUB
# 3b8b25ec33f9 oxygenhub-vscode / src / issues.ts
4.2 KiB · last changed 0.1.0: sign in, hub client with generated types, remote provider, proposals and issues views, review threads on atoms, seal and land through oxy, checks status bar 1 month ago source blame history raw
f0b06dea Andy McCall 1
// The Issues view (spec §8).
2
3
import * as vscode from "vscode";
4
import type * as api from "./hubapi";
5
import { HubAuth } from "./auth";
6
import { hubUrl } from "./hub";
7
import type { HubRepo } from "./proposals";
8
9
type Node = { kind: "repo"; repo: HubRepo } | { kind: "issue"; repo: HubRepo; i: api.Issue };
10
export type IssueNode = Extract<Node, { kind: "issue" }>;
11
12
export class IssuesProvider implements vscode.TreeDataProvider<Node> {
13
  private readonly emitter = new vscode.EventEmitter<void>();
14
  readonly onDidChangeTreeData = this.emitter.event;
15
16
  constructor(
17
    private readonly repos: () => Promise<HubRepo[]>,
18
    private readonly auth: HubAuth,
19
  ) {}
20
21
  refresh(): void {
22
    this.emitter.fire();
23
  }
24
25
  getTreeItem(node: Node): vscode.TreeItem {
26
    if (node.kind === "repo") {
27
      const item = new vscode.TreeItem(`${node.repo.owner}/${node.repo.name}`, vscode.TreeItemCollapsibleState.Expanded);
28
      item.iconPath = new vscode.ThemeIcon("repo");
29
      return item;
30
    }
31
    const i = node.i;
32
    const item = new vscode.TreeItem(`#${i.number} ${i.title}`, vscode.TreeItemCollapsibleState.None);
33
    item.description = [i.labels.map((l) => l.name).join(", "), i.assignees.length ? "→ " + i.assignees.join(", ") : "", i.milestone ?? ""].filter(Boolean).join(" · ");
34
    item.iconPath = new vscode.ThemeIcon(i.state === "open" ? "issue-opened" : "issue-closed");
35
    item.contextValue = "issue";
36
    item.tooltip = new vscode.MarkdownString(`**${i.title}**\n\nopened by ${i.author} · ${i.comments} comment(s)\n\n${i.body.slice(0, 400)}`);
37
    item.command = { title: "Open", command: "oxygenhub.openIssue", arguments: [node] };
38
    return item;
39
  }
40
41
  async getChildren(node?: Node): Promise<Node[]> {
42
    if (!node) {
43
      return (await this.repos()).map((repo) => ({ kind: "repo", repo }));
44
    }
45
    if (node.kind === "repo") {
46
      try {
47
        const me = this.auth.sessionFor(node.repo.base)?.username;
48
        const list = await this.auth.clientFor(node.repo.base).issues(node.repo.owner, node.repo.name, "open");
49
        list.sort((a, b) => Number(b.assignees.includes(me ?? "")) - Number(a.assignees.includes(me ?? "")) || b.updated_ms - a.updated_ms);
50
        return list.map((i) => ({ kind: "issue", repo: node.repo, i }));
51
      } catch (e) {
52
        void vscode.window.showWarningMessage(`Oxygenhub: ${(e as Error).message}`);
53
        return [];
54
      }
55
    }
56
    return [];
57
  }
58
}
59
60
export async function showIssue(auth: HubAuth, repo: HubRepo, i: api.Issue): Promise<void> {
61
  const comments = await auth.clientFor(repo.base).issueComments(repo.owner, repo.name, i.number);
62
  const panel = vscode.window.createWebviewPanel("oxygenhub.issue", `#${i.number} ${i.title}`, vscode.ViewColumn.Beside, { enableCommandUris: true });
63
  const esc = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
64
  const arg = encodeURIComponent(JSON.stringify([repo.base, repo.owner, repo.name, i.number]));
65
  const thread = comments.map((c) => `<div class=c><b>${esc(c.author)}</b> <span class=mute>${new Date(c.created_ms).toLocaleString()}</span><pre>${esc(c.body)}</pre></div>`).join("");
66
  panel.webview.html = `<!doctype html><html><body style="font-family: var(--vscode-font-family); padding: 12px">
67
  <h2>#${i.number} ${esc(i.title)} <small>${i.state}${i.reason ? ": " + i.reason : ""}</small></h2>
68
  <p class=mute>opened by ${esc(i.author)} · ${i.labels.map((l) => esc(l.name)).join(", ") || "no labels"} · ${i.assignees.length ? "assigned to " + i.assignees.map(esc).join(", ") : "unassigned"}${i.milestone ? " · " + esc(i.milestone) : ""}</p>
69
  <p><a href="${hubUrl(repo.base, repo.owner, repo.name, { kind: "issue", number: i.number })}">Open on hub</a> · <a href="command:oxygenhub.commentIssue?${arg}">Comment</a> · <a href="command:oxygenhub.${i.state === "open" ? "closeIssue" : "reopenIssue"}?${arg}">${i.state === "open" ? "Close" : "Reopen"}</a> · <a href="command:oxygenhub.assignMe?${arg}">Assign to me</a></p>
70
  <pre style="white-space: pre-wrap">${esc(i.body) || "<i>No description.</i>"}</pre>
71
  ${thread}
72
  <style>.mute{opacity:.7} .c{border-top:1px solid var(--vscode-widget-border);padding:8px 0} pre{white-space:pre-wrap} small{font-weight:normal;opacity:.7}</style>
73
  </body></html>`;
74
}