@volter/twin-github 0.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +230 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +101 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +159 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +223 -95
- package/src/github-git-http.ts +47 -83
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +47 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +186 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- package/src/github-graphql.ts +0 -398
package/client/github-mirror.tsx
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import { GITHUB_MIRROR_STORE_PATH, githubIssuePath, githubPullPath } from "../src/github-shared.ts";
|
|
4
|
+
import { loadPulls, type Check, type RepoFacts, type StateEvent } from "./pulls-rest.ts";
|
|
5
|
+
import { PullPage, PullsListPage, RepoHeader, SiteFooter, pullState, type PullActions, type PullTab, type RepoTab } from "./pulls-workspace.tsx";
|
|
6
|
+
export { Aside, CiStatus, Conversation, FilesDiff, PrList } from "./pulls-workspace.tsx";
|
|
7
|
+
|
|
8
|
+
// WHERE THIS MIRROR LIVES. Served at a vendor root the base is "" (fetches are root-relative, as
|
|
9
|
+
// before); served under a path prefix with a <base> tag every read and write resolves inside that
|
|
10
|
+
// prefix instead of escaping it.
|
|
11
|
+
const WIRE_BASE = typeof document === "undefined" || document.querySelector("base[href]") === null ? "" : new URL(".", document.baseURI).pathname.replace(/\/$/, "");
|
|
12
|
+
// The app's own routes live under the mirror's mount: `<base>/mirror` when served under a prefix
|
|
13
|
+
// (the base names the twin, the mirror sits at `mirror/` beneath it), the root otherwise. Route
|
|
14
|
+
// parsing strips the mount; every pushed path is prefixed by it.
|
|
15
|
+
const ROUTE_MOUNT = WIRE_BASE === "" ? "" : new URL("mirror/", document.baseURI).pathname.replace(/\/$/, "");
|
|
16
|
+
function appPath(pathname: string): string {
|
|
17
|
+
if (!pathname.startsWith(ROUTE_MOUNT)) return "/";
|
|
18
|
+
return pathname.slice(ROUTE_MOUNT.length) || "/";
|
|
19
|
+
}
|
|
20
|
+
function mountedPath(path: string): string {
|
|
21
|
+
return `${ROUTE_MOUNT}${path}`;
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
export type PullRequest = {
|
|
6
25
|
repo: string;
|
|
@@ -34,6 +53,10 @@ export type PullRequest = {
|
|
|
34
53
|
requestedReviewers?: string[];
|
|
35
54
|
milestone?: string;
|
|
36
55
|
ciStatus?: string;
|
|
56
|
+
closedAt?: string;
|
|
57
|
+
mergedBy?: string;
|
|
58
|
+
mergeable?: boolean;
|
|
59
|
+
draft?: boolean;
|
|
37
60
|
};
|
|
38
61
|
export type LinkedRef = { type: "pull_request" | "issue"; number: number; url?: string };
|
|
39
62
|
export type Issue = {
|
|
@@ -57,6 +80,7 @@ export type Issue = {
|
|
|
57
80
|
export type Review = {
|
|
58
81
|
repo: string;
|
|
59
82
|
number: number;
|
|
83
|
+
id?: number;
|
|
60
84
|
reviewer?: string;
|
|
61
85
|
state?: string;
|
|
62
86
|
body?: string;
|
|
@@ -79,6 +103,8 @@ export type Commit = {
|
|
|
79
103
|
export type Comment = {
|
|
80
104
|
repo: string;
|
|
81
105
|
number: number;
|
|
106
|
+
id?: number;
|
|
107
|
+
authorAssociation?: string;
|
|
82
108
|
author?: string;
|
|
83
109
|
authorAvatarUrl?: string;
|
|
84
110
|
body?: string;
|
|
@@ -220,6 +246,10 @@ type Payload = {
|
|
|
220
246
|
projectBoards?: ProjectBoard[];
|
|
221
247
|
insights?: RepoInsights[];
|
|
222
248
|
settings?: RepoSettings[];
|
|
249
|
+
viewer?: string;
|
|
250
|
+
checks?: Check[];
|
|
251
|
+
repoFacts?: Record<string, RepoFacts>;
|
|
252
|
+
stateEvents?: StateEvent[];
|
|
223
253
|
};
|
|
224
254
|
fetchedAt?: string;
|
|
225
255
|
};
|
|
@@ -249,14 +279,6 @@ function shortSha(value?: string) {
|
|
|
249
279
|
return value ? value.slice(0, 7) : "unknown";
|
|
250
280
|
}
|
|
251
281
|
|
|
252
|
-
// CI rollup glyph per PR head_sha (✓ success, ✗ failure, ● pending). Undefined for
|
|
253
|
-
// observed-only PRs with no statuses/check-runs — they show no CI indicator (honesty).
|
|
254
|
-
const CI_GLYPH: Record<string, string> = { success: "✓", failure: "✗", pending: "●" };
|
|
255
|
-
export function CiStatus({ status }: { status?: string }) {
|
|
256
|
-
if (!status) return null;
|
|
257
|
-
return <span className={`ci-status ${status}`} title={`checks: ${status}`}>{CI_GLYPH[status] ?? "●"}</span>;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
282
|
function prKey(pr: PullRequest) {
|
|
261
283
|
return `${pr.repo}#${pr.number}`;
|
|
262
284
|
}
|
|
@@ -270,18 +292,6 @@ export function RunStatusBadge({ status, conclusion }: { status?: string; conclu
|
|
|
270
292
|
return <span className={`run-badge ${effective}`} title={`run: ${effective}`}>{RUN_GLYPH[effective] ?? "●"} {effective}</span>;
|
|
271
293
|
}
|
|
272
294
|
|
|
273
|
-
function initials(name?: string): string {
|
|
274
|
-
return (name || "?").split(/[-_\s]+/).map((part) => part[0]).join("").slice(0, 2).toUpperCase() || "?";
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function Avatar({ src, label }: { src?: string; label?: string }) {
|
|
278
|
-
return (
|
|
279
|
-
<div className="avatar">
|
|
280
|
-
{src ? <img src={src} alt="" loading="lazy" referrerPolicy="no-referrer" /> : initials(label)}
|
|
281
|
-
</div>
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
295
|
// ── Real-app URL routing (TWIN-49 / H3) ─────────────────────────────────────────────────
|
|
286
296
|
// Vendor-faithful path shapes (credible-sim fidelity — recognizable to an agent that knows
|
|
287
297
|
// the real GitHub UI):
|
|
@@ -294,8 +304,10 @@ function Avatar({ src, label }: { src?: string; label?: string }) {
|
|
|
294
304
|
// all stay in sync through one small function pair.
|
|
295
305
|
type ViewName = "pulls" | "issues" | "actions" | "releases" | "discussions" | "code" | "search" | "notifications" | "deployments" | "projects" | "insights" | "settings";
|
|
296
306
|
const VIEW_NAMES: ViewName[] = ["pulls", "issues", "actions", "releases", "discussions", "code", "search", "notifications", "deployments", "projects", "insights", "settings"];
|
|
297
|
-
type RouteState = { repo: string; view: ViewName; selectedKey: string; selectedIssueKey: string };
|
|
307
|
+
type RouteState = { repo: string; view: ViewName; selectedKey: string; selectedIssueKey: string; pullTab?: PullTab };
|
|
298
308
|
const DEFAULT_ROUTE: RouteState = { repo: "all", view: "pulls", selectedKey: "", selectedIssueKey: "" };
|
|
309
|
+
// A pull request's tabs live at GitHub's own sub-paths: /pull/{n}, /pull/{n}/commits, /pull/{n}/checks, /pull/{n}/files.
|
|
310
|
+
const PULL_TAB_SEGMENT: Record<PullTab, string> = { conversation: "", commits: "commits", checks: "checks", files: "files" };
|
|
299
311
|
|
|
300
312
|
/** `selectedKey`/`selectedIssueKey` are `${repo}#${number}` — split on the LAST `#` (repo
|
|
301
313
|
* names never contain one) to recover the PR/issue's OWN repo, independent of whichever
|
|
@@ -315,13 +327,15 @@ function pathFor(route: RouteState): string {
|
|
|
315
327
|
// each PR/issue `url` (github-shared.ts), so a rendered link and the address bar agree.
|
|
316
328
|
if (view === "pulls" && selectedKey) {
|
|
317
329
|
const { repo: prRepo, number } = splitRepoScopedKey(selectedKey);
|
|
318
|
-
|
|
330
|
+
const segment = PULL_TAB_SEGMENT[route.pullTab ?? "conversation"];
|
|
331
|
+
return `${githubPullPath(prRepo, number)}${segment ? `/${segment}` : ""}`;
|
|
319
332
|
}
|
|
320
333
|
if (view === "issues" && selectedIssueKey) {
|
|
321
334
|
const { repo: issueRepo, number } = splitRepoScopedKey(selectedIssueKey);
|
|
322
335
|
return githubIssuePath(issueRepo, number);
|
|
323
336
|
}
|
|
324
337
|
if (repo === "all" || !repo) return `/${view}`;
|
|
338
|
+
if (view === "code") return `/${repo}`;
|
|
325
339
|
return `/${repo}/${view}`;
|
|
326
340
|
}
|
|
327
341
|
|
|
@@ -333,9 +347,12 @@ function stateFor(pathname: string): RouteState {
|
|
|
333
347
|
}
|
|
334
348
|
if (segments.length >= 2) {
|
|
335
349
|
const repo = `${segments[0]}/${segments[1]}`;
|
|
336
|
-
if (segments.length === 2) return { ...DEFAULT_ROUTE, repo, view: "
|
|
350
|
+
if (segments.length === 2) return { ...DEFAULT_ROUTE, repo, view: "code" };
|
|
337
351
|
const tab = segments[2];
|
|
338
|
-
if (tab === "pull" && segments[3])
|
|
352
|
+
if (tab === "pull" && segments[3]) {
|
|
353
|
+
const pullTab = (Object.keys(PULL_TAB_SEGMENT) as PullTab[]).find((t) => PULL_TAB_SEGMENT[t] === (segments[4] ?? "")) ?? "conversation";
|
|
354
|
+
return { ...DEFAULT_ROUTE, repo, view: "pulls", selectedKey: `${repo}#${segments[3]}`, pullTab };
|
|
355
|
+
}
|
|
339
356
|
if (tab === "pulls") return { ...DEFAULT_ROUTE, repo, view: "pulls" };
|
|
340
357
|
if (tab === "issues" && segments[3]) return { ...DEFAULT_ROUTE, repo, view: "issues", selectedIssueKey: `${repo}#${segments[3]}` };
|
|
341
358
|
if (tab === "issues") return { ...DEFAULT_ROUTE, repo, view: "issues" };
|
|
@@ -347,7 +364,7 @@ function stateFor(pathname: string): RouteState {
|
|
|
347
364
|
|
|
348
365
|
function App() {
|
|
349
366
|
const [payload, setPayload] = useState<Payload | null>(null);
|
|
350
|
-
const initialRoute = typeof window !== "undefined" ? stateFor(window.location.pathname) : DEFAULT_ROUTE;
|
|
367
|
+
const initialRoute = typeof window !== "undefined" ? stateFor(appPath(window.location.pathname)) : DEFAULT_ROUTE;
|
|
351
368
|
const [repo, setRepo] = useState(initialRoute.repo);
|
|
352
369
|
const [selectedKey, setSelectedKey] = useState(initialRoute.selectedKey);
|
|
353
370
|
const [query, setQuery] = useState("");
|
|
@@ -357,20 +374,32 @@ function App() {
|
|
|
357
374
|
const [selectedReleaseId, setSelectedReleaseId] = useState<number | undefined>(undefined);
|
|
358
375
|
const [selectedDiscussionKey, setSelectedDiscussionKey] = useState<string | undefined>(undefined);
|
|
359
376
|
const [selectedIssueKey, setSelectedIssueKey] = useState<string>(initialRoute.selectedIssueKey);
|
|
360
|
-
const [
|
|
377
|
+
const [pullTab, setPullTab] = useState<PullTab>(initialRoute.pullTab ?? "conversation");
|
|
378
|
+
// The list's Open/Closed toggle is GitHub's query: /pulls?q=is%3Apr+is%3Aclosed
|
|
379
|
+
const listStateOf = (search: string): "open" | "closed" => (/is:closed|is:merged/.test(new URLSearchParams(search).get("q") ?? "") ? "closed" : "open");
|
|
380
|
+
const [listState, setListState] = useState<"open" | "closed">(typeof window !== "undefined" ? listStateOf(window.location.search) : "open");
|
|
361
381
|
const [error, setError] = useState("");
|
|
362
382
|
// Set while applying a popstate-derived route, so the pushState effect below (which reacts
|
|
363
383
|
// to the same state) skips re-pushing the entry the browser just navigated to.
|
|
364
384
|
const skipNextPush = React.useRef(false);
|
|
365
385
|
|
|
366
|
-
// The
|
|
367
|
-
//
|
|
368
|
-
//
|
|
386
|
+
// The pull-request workspace reads through GitHub's own REST API (pulls-rest.ts), as a GitHub client
|
|
387
|
+
// does; the console's other views still read the twin's store door (`GET /twin/store/mirror`) until
|
|
388
|
+
// each is declared a workspace and moves the same way.
|
|
389
|
+
const selectedRef = React.useRef(selectedKey);
|
|
390
|
+
selectedRef.current = selectedKey;
|
|
391
|
+
const refreshSeq = React.useRef(0);
|
|
369
392
|
async function refresh() {
|
|
393
|
+
const seq = ++refreshSeq.current;
|
|
370
394
|
try {
|
|
371
|
-
const response = await fetch(GITHUB_MIRROR_STORE_PATH);
|
|
395
|
+
const response = await fetch(`${WIRE_BASE}${GITHUB_MIRROR_STORE_PATH}`);
|
|
372
396
|
const json = await response.json();
|
|
373
397
|
if (!response.ok) throw new Error(json.error ?? "Failed to load GitHub state");
|
|
398
|
+
const open = /^(.+)#(\d+)$/.exec(selectedRef.current ?? "");
|
|
399
|
+
const pulls = await loadPulls(WIRE_BASE, open ? { repo: open[1]!, number: Number(open[2]) } : undefined);
|
|
400
|
+
json.github = { ...json.github, ...pulls };
|
|
401
|
+
// a slower, older read never overwrites a newer one (a write's read-back races the poll)
|
|
402
|
+
if (seq !== refreshSeq.current) return;
|
|
374
403
|
setPayload(json);
|
|
375
404
|
setError("");
|
|
376
405
|
} catch (err) {
|
|
@@ -380,30 +409,35 @@ function App() {
|
|
|
380
409
|
|
|
381
410
|
useEffect(() => {
|
|
382
411
|
void refresh();
|
|
383
|
-
const timer = window.setInterval(() => void refresh(),
|
|
412
|
+
const timer = window.setInterval(() => void refresh(), 3000);
|
|
384
413
|
return () => window.clearInterval(timer);
|
|
385
414
|
}, []);
|
|
415
|
+
// opening a pull request loads its thread now, not at the next poll
|
|
416
|
+
useEffect(() => { void refresh(); }, [selectedKey]);
|
|
386
417
|
|
|
387
418
|
// pushState on navigation: any click that changes repo/view/selection updates the address
|
|
388
419
|
// bar (no reload) to the vendor-faithful path for the new state.
|
|
389
420
|
useEffect(() => {
|
|
390
|
-
const path = pathFor({ repo, view, selectedKey, selectedIssueKey });
|
|
421
|
+
const path = mountedPath(pathFor({ repo, view, selectedKey, selectedIssueKey, pullTab }));
|
|
422
|
+
const search = view === "pulls" && !selectedKey && listState === "closed" ? `?q=${encodeURIComponent("is:pr is:closed").replace(/%20/g, "+")}` : "";
|
|
391
423
|
if (skipNextPush.current) {
|
|
392
424
|
skipNextPush.current = false;
|
|
393
425
|
return;
|
|
394
426
|
}
|
|
395
|
-
if (window.location.pathname !== path) window.history.pushState(null, "", path);
|
|
396
|
-
}, [repo, view, selectedKey, selectedIssueKey]);
|
|
427
|
+
if (window.location.pathname !== path || window.location.search !== search) window.history.pushState(null, "", `${path}${search}`);
|
|
428
|
+
}, [repo, view, selectedKey, selectedIssueKey, pullTab, listState]);
|
|
397
429
|
|
|
398
430
|
// popstate (back/forward): re-derive the view + selection from the restored pathname.
|
|
399
431
|
useEffect(() => {
|
|
400
432
|
function onPopState() {
|
|
401
|
-
const route = stateFor(window.location.pathname);
|
|
433
|
+
const route = stateFor(appPath(window.location.pathname));
|
|
402
434
|
skipNextPush.current = true;
|
|
403
435
|
setRepo(route.repo);
|
|
404
436
|
setView(route.view);
|
|
405
437
|
setSelectedKey(route.selectedKey);
|
|
406
438
|
setSelectedIssueKey(route.selectedIssueKey);
|
|
439
|
+
setPullTab(route.pullTab ?? "conversation");
|
|
440
|
+
setListState(listStateOf(window.location.search));
|
|
407
441
|
}
|
|
408
442
|
window.addEventListener("popstate", onPopState);
|
|
409
443
|
return () => window.removeEventListener("popstate", onPopState);
|
|
@@ -554,75 +588,87 @@ function App() {
|
|
|
554
588
|
// used to implement is REPLACED by real path routing (see pathFor/stateFor above) — no
|
|
555
589
|
// dual/legacy path.
|
|
556
590
|
|
|
591
|
+
// The pull-request workspace's writes: GitHub's REST API on this origin, then a read-back so the page shows the change.
|
|
592
|
+
async function write(method: string, path: string, body: unknown): Promise<void> {
|
|
593
|
+
const response = await fetch(`${WIRE_BASE}${path}`, { method, headers: { accept: "application/vnd.github+json", "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
594
|
+
if (!response.ok) {
|
|
595
|
+
const answer = await response.json().catch(() => ({}));
|
|
596
|
+
throw new Error(answer.message ?? `${method} ${path} answered ${response.status}`);
|
|
597
|
+
}
|
|
598
|
+
await refresh();
|
|
599
|
+
}
|
|
600
|
+
const openPull = current && selectedKey && prKey(current) === selectedKey ? current : undefined;
|
|
601
|
+
const pullActions: PullActions = {
|
|
602
|
+
comment: (body) => write("POST", `/repos/${openPull!.repo}/issues/${openPull!.number}/comments`, { body }),
|
|
603
|
+
merge: (commit_title, commit_message) => write("PUT", `/repos/${openPull!.repo}/pulls/${openPull!.number}/merge`, { commit_title, commit_message, merge_method: "merge" }),
|
|
604
|
+
close: async (body) => {
|
|
605
|
+
if (body) await write("POST", `/repos/${openPull!.repo}/issues/${openPull!.number}/comments`, { body });
|
|
606
|
+
await write("PATCH", `/repos/${openPull!.repo}/pulls/${openPull!.number}`, { state: "closed" });
|
|
607
|
+
},
|
|
608
|
+
reopen: () => write("PATCH", `/repos/${openPull!.repo}/pulls/${openPull!.number}`, { state: "open" }),
|
|
609
|
+
};
|
|
610
|
+
const repoFacts = payload?.github.repoFacts ?? {};
|
|
611
|
+
const facts = repo === "all" ? undefined : repoFacts[repo];
|
|
612
|
+
const labelColors: Record<string, string> = {};
|
|
613
|
+
for (const [name, f] of Object.entries(repoFacts)) for (const l of f.labels) labelColors[`${name}:${l.name}`] = l.color ?? "ededed";
|
|
614
|
+
const allPulls = (payload?.github.pullRequests ?? []).filter((pr) => repo === "all" || !repo || pr.repo === repo);
|
|
615
|
+
const openPulls = allPulls.filter((pr) => pullState(pr) === "open" || pullState(pr) === "draft").length;
|
|
616
|
+
const TAB_VIEW: Partial<Record<RepoTab, ViewName>> = { code: "code", issues: "issues", pulls: "pulls", actions: "actions", projects: "projects", insights: "insights", settings: "settings", discussions: "discussions" };
|
|
617
|
+
const activeTab: RepoTab | undefined = view === "code" ? "code" : view === "issues" ? "issues" : view === "pulls" ? "pulls" : view === "actions" ? "actions" : view === "projects" ? "projects" : view === "insights" ? "insights" : view === "settings" ? "settings" : view === "discussions" ? "discussions" : undefined;
|
|
618
|
+
const header = (
|
|
619
|
+
<RepoHeader
|
|
620
|
+
repo={repo}
|
|
621
|
+
viewer={payload?.github.viewer}
|
|
622
|
+
active={activeTab}
|
|
623
|
+
facts={facts}
|
|
624
|
+
openPulls={openPulls}
|
|
625
|
+
projects={projectBoards.length}
|
|
626
|
+
onTab={(tab) => { const next = TAB_VIEW[tab]; if (next) { setView(next); setSelectedKey(""); setPullTab("conversation"); } }}
|
|
627
|
+
onHome={() => { setRepo("all"); setView("pulls"); setSelectedKey(""); }}
|
|
628
|
+
onInbox={() => { setView("notifications"); setSelectedKey(""); }}
|
|
629
|
+
onSearch={() => { setView("search"); setSelectedKey(""); }}
|
|
630
|
+
/>
|
|
631
|
+
);
|
|
632
|
+
if (view === "pulls") {
|
|
633
|
+
return (
|
|
634
|
+
<div className="gh-app">
|
|
635
|
+
{header}
|
|
636
|
+
{error ? <div className="gh-container"><div className="gh-flash-error">{error}</div></div> : null}
|
|
637
|
+
{selectedKey ? (
|
|
638
|
+
<PullPage
|
|
639
|
+
pr={openPull}
|
|
640
|
+
reviews={currentReviews}
|
|
641
|
+
commits={currentCommits}
|
|
642
|
+
comments={currentComments}
|
|
643
|
+
reviewComments={currentReviewComments}
|
|
644
|
+
files={currentFiles}
|
|
645
|
+
checks={payload?.github.checks ?? []}
|
|
646
|
+
stateEvents={payload?.github.stateEvents ?? []}
|
|
647
|
+
viewer={payload?.github.viewer}
|
|
648
|
+
labelColors={labelColors}
|
|
649
|
+
tab={pullTab}
|
|
650
|
+
onTab={setPullTab}
|
|
651
|
+
actions={pullActions}
|
|
652
|
+
/>
|
|
653
|
+
) : (
|
|
654
|
+
<PullsListPage
|
|
655
|
+
repo={repo}
|
|
656
|
+
prs={allPulls}
|
|
657
|
+
reviews={payload?.github.reviews ?? []}
|
|
658
|
+
facts={facts}
|
|
659
|
+
listState={listState}
|
|
660
|
+
onListState={setListState}
|
|
661
|
+
onSelect={(key) => { setPullTab("conversation"); setRepo(key.slice(0, key.lastIndexOf("#"))); setSelectedKey(key); }}
|
|
662
|
+
/>
|
|
663
|
+
)}
|
|
664
|
+
<SiteFooter />
|
|
665
|
+
</div>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
557
669
|
return (
|
|
558
|
-
<
|
|
559
|
-
|
|
560
|
-
<button
|
|
561
|
-
className={`browser-tab ${repo === "all" ? "active" : ""}`}
|
|
562
|
-
onClick={() => {
|
|
563
|
-
setRepo("all");
|
|
564
|
-
setSelectedKey("");
|
|
565
|
-
}}
|
|
566
|
-
>
|
|
567
|
-
<span>All repositories</span>
|
|
568
|
-
<strong>{payload?.github.pullRequests?.length ?? 0}</strong>
|
|
569
|
-
</button>
|
|
570
|
-
{repos.map((name) => {
|
|
571
|
-
const count = (payload?.github.pullRequests ?? []).filter((pr) => pr.repo === name).length;
|
|
572
|
-
return (
|
|
573
|
-
<button
|
|
574
|
-
key={name}
|
|
575
|
-
className={`browser-tab ${name === repo ? "active" : ""}`}
|
|
576
|
-
onClick={() => {
|
|
577
|
-
setRepo(name);
|
|
578
|
-
setSelectedKey("");
|
|
579
|
-
}}
|
|
580
|
-
>
|
|
581
|
-
<span>{name}</span>
|
|
582
|
-
<strong>{count}</strong>
|
|
583
|
-
</button>
|
|
584
|
-
);
|
|
585
|
-
})}
|
|
586
|
-
</section>
|
|
587
|
-
<section className="browser-address">
|
|
588
|
-
<span className="browser-dot red" />
|
|
589
|
-
<span className="browser-dot yellow" />
|
|
590
|
-
<span className="browser-dot green" />
|
|
591
|
-
<div className="address-bar">github.local{pathFor({ repo, view, selectedKey, selectedIssueKey })}</div>
|
|
592
|
-
</section>
|
|
593
|
-
<section className="github-shell">
|
|
594
|
-
<header className="global-header">
|
|
595
|
-
<div className="octo">GH</div>
|
|
596
|
-
<input className="search" value="Search or jump to..." readOnly />
|
|
597
|
-
<nav className="global-nav"><span>Pull requests</span><span>Issues</span><span>Marketplace</span><span>Explore</span></nav>
|
|
598
|
-
<div className="global-spacer" />
|
|
599
|
-
<span className="pill-dark">mock github</span>
|
|
600
|
-
</header>
|
|
601
|
-
<section className="repo-head">
|
|
602
|
-
<div className="repo-line">
|
|
603
|
-
{repo === "all" ? (
|
|
604
|
-
<><button>all</button><span>/</span><strong>repositories</strong></>
|
|
605
|
-
) : (
|
|
606
|
-
<><button>{repo.split("/")[0] || "owner"}</button><span>/</span><strong>{repo.split("/")[1] || "repository"}</strong></>
|
|
607
|
-
)}
|
|
608
|
-
</div>
|
|
609
|
-
<nav className="repo-tabs">
|
|
610
|
-
<button>Code</button>
|
|
611
|
-
<button className={view === "issues" ? "active" : ""} onClick={() => setView("issues")}>Issues <span className="count">{issues.length}</span></button>
|
|
612
|
-
<button className={view === "pulls" ? "active" : ""} onClick={() => setView("pulls")}>Pull requests <span className="count">{prs.length}</span></button>
|
|
613
|
-
<button className={view === "actions" ? "active" : ""} onClick={() => setView("actions")}>Actions <span className="count">{runs.length}</span></button>
|
|
614
|
-
<button className={view === "releases" ? "active" : ""} onClick={() => setView("releases")}>Releases <span className="count">{releases.length}</span></button>
|
|
615
|
-
<button className={view === "discussions" ? "active" : ""} onClick={() => setView("discussions")}>Discussions <span className="count">{discussions.length}</span></button>
|
|
616
|
-
<button className={view === "code" ? "active" : ""} onClick={() => setView("code")}>Code <span className="count">{(payload?.github.contents ?? []).length}</span></button>
|
|
617
|
-
<button className={view === "search" ? "active" : ""} onClick={() => setView("search")}>Search</button>
|
|
618
|
-
<button className={view === "notifications" ? "active" : ""} onClick={() => setView("notifications")}>Notifications <span className="count">{(payload?.github.notifications ?? []).filter((n) => n.unread).length}</span></button>
|
|
619
|
-
<button className={view === "deployments" ? "active" : ""} onClick={() => setView("deployments")}>Deployments <span className="count">{deployments.length}</span></button>
|
|
620
|
-
<button className={view === "projects" ? "active" : ""} onClick={() => setView("projects")}>Projects <span className="count">{projectBoards.length}</span></button>
|
|
621
|
-
<button>Security</button>
|
|
622
|
-
<button className={view === "insights" ? "active" : ""} onClick={() => setView("insights")}>Insights</button>
|
|
623
|
-
<button className={view === "settings" ? "active" : ""} onClick={() => setView("settings")}>Settings <span className="count">{settings.length}</span></button>
|
|
624
|
-
</nav>
|
|
625
|
-
</section>
|
|
670
|
+
<div className="gh-app">
|
|
671
|
+
{header}
|
|
626
672
|
<div className="content">
|
|
627
673
|
{error ? <div className="error">{error}</div> : null}
|
|
628
674
|
<div className="toolbar">
|
|
@@ -630,9 +676,9 @@ function App() {
|
|
|
630
676
|
className="pr-filter"
|
|
631
677
|
value={query}
|
|
632
678
|
onChange={(event) => setQuery(event.target.value)}
|
|
633
|
-
placeholder={view === "issues" ? "Search issues by title, number, repo" : view === "actions" ? "Search runs by workflow, number, branch, status" : view === "releases" ? "Search releases by tag, name, draft/prerelease" : view === "discussions" ? "Search discussions by title, category, answered" : "Search
|
|
679
|
+
placeholder={view === "issues" ? "Search issues by title, number, repo" : view === "actions" ? "Search runs by workflow, number, branch, status" : view === "releases" ? "Search releases by tag, name, draft/prerelease" : view === "discussions" ? "Search discussions by title, category, answered" : "Search"}
|
|
634
680
|
/>
|
|
635
|
-
|
|
681
|
+
{view === "issues" ? <button className="new-pr">New issue</button> : view === "actions" ? <button className="new-pr">Run workflow</button> : view === "releases" ? <button className="new-pr">Draft a new release</button> : view === "discussions" ? <button className="new-pr">New discussion</button> : null}
|
|
636
682
|
</div>
|
|
637
683
|
{view === "issues" ? (
|
|
638
684
|
<div className="split">
|
|
@@ -661,74 +707,9 @@ function App() {
|
|
|
661
707
|
<InsightsView insights={insights} />
|
|
662
708
|
) : view === "settings" ? (
|
|
663
709
|
<SettingsView settings={settings} />
|
|
664
|
-
) :
|
|
665
|
-
<div className="split">
|
|
666
|
-
<main>
|
|
667
|
-
<PrList prs={prs} reviews={payload?.github.reviews ?? []} selectedKey={selectedKey} onSelect={setSelectedKey} />
|
|
668
|
-
<div className="pr-tabs">
|
|
669
|
-
<button className={showFilesDiff ? "" : "active"} onClick={() => setShowFilesDiff(false)}>Conversation</button>
|
|
670
|
-
<button className={showFilesDiff ? "active" : ""} onClick={() => setShowFilesDiff(true)}>Files changed <span className="count">{current?.changedFiles ?? currentFiles.length}</span></button>
|
|
671
|
-
</div>
|
|
672
|
-
{showFilesDiff ? (
|
|
673
|
-
<FilesDiff files={currentFiles} />
|
|
674
|
-
) : (
|
|
675
|
-
<Conversation
|
|
676
|
-
pr={current}
|
|
677
|
-
reviews={currentReviews}
|
|
678
|
-
commits={currentCommits}
|
|
679
|
-
comments={currentComments}
|
|
680
|
-
reviewComments={currentReviewComments}
|
|
681
|
-
files={currentFiles}
|
|
682
|
-
/>
|
|
683
|
-
)}
|
|
684
|
-
</main>
|
|
685
|
-
<Aside pr={current} reviews={currentReviews} commits={currentCommits} files={currentFiles} />
|
|
686
|
-
</div>
|
|
687
|
-
)}
|
|
710
|
+
) : null}
|
|
688
711
|
</div>
|
|
689
|
-
|
|
690
|
-
</main>
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
export function PrList({ prs, reviews, selectedKey, onSelect }: {
|
|
695
|
-
prs: PullRequest[];
|
|
696
|
-
reviews: Review[];
|
|
697
|
-
selectedKey?: string;
|
|
698
|
-
onSelect?: (key: string) => void;
|
|
699
|
-
}) {
|
|
700
|
-
return (
|
|
701
|
-
<section className="list-box">
|
|
702
|
-
<div className="list-head"><span><strong>{prs.length} pull requests</strong></span><span>Updated</span></div>
|
|
703
|
-
{prs.length ? prs.map((pr) => {
|
|
704
|
-
const approvedCount = reviews.filter((review) => review.repo === pr.repo && Number(review.number) === Number(pr.number) && review.state === "approved").length;
|
|
705
|
-
const state = pr.merged ? "merged" : (pr.state ?? "open");
|
|
706
|
-
return (
|
|
707
|
-
<button key={prKey(pr)} className={`pr-row ${prKey(pr) === selectedKey ? "active" : ""}`} onClick={() => onSelect?.(prKey(pr))}>
|
|
708
|
-
<span className={`status-icon ${state}`}>o</span>
|
|
709
|
-
<span>
|
|
710
|
-
<span className="pr-title">{pr.title || pr.url || `${pr.repo}#${pr.number}`}</span>
|
|
711
|
-
<div className="pr-meta">
|
|
712
|
-
{pr.repo} #{pr.number} · {state}
|
|
713
|
-
{pr.author ? ` · opened by ${pr.author}` : ""}
|
|
714
|
-
{pr.updatedAt ? ` · updated ${new Date(pr.updatedAt).toLocaleString()}` : pr.createdAt ? ` · ${new Date(pr.createdAt).toLocaleString()}` : ""}
|
|
715
|
-
{pr.sourceIssue ? ` · ${pr.sourceIssue}` : ""}
|
|
716
|
-
{pr.changedFiles != null ? ` · ${pr.changedFiles} files` : ""}
|
|
717
|
-
{pr.commitsCount != null ? ` · ${pr.commitsCount} commits` : ""}
|
|
718
|
-
</div>
|
|
719
|
-
</span>
|
|
720
|
-
<span className="labels">
|
|
721
|
-
<CiStatus status={pr.ciStatus} />
|
|
722
|
-
<span className={`state-badge ${state}`}>{state}</span>
|
|
723
|
-
{approvedCount ? <span className="label approved">{approvedCount} approved</span> : null}
|
|
724
|
-
{pr.milestone ? <span className="label milestone" title="Milestone">🏷 {pr.milestone}</span> : null}
|
|
725
|
-
{(pr.labels ?? []).slice(0, 2).map((label) => label.name ? <span className="label" key={label.name}>{label.name}</span> : null)}
|
|
726
|
-
{(pr.assignees ?? []).slice(0, 2).map((login) => <span className="label assignee" key={login}>@{login}</span>)}
|
|
727
|
-
</span>
|
|
728
|
-
</button>
|
|
729
|
-
);
|
|
730
|
-
}) : <div className="empty">No pull requests match this view.</div>}
|
|
731
|
-
</section>
|
|
712
|
+
</div>
|
|
732
713
|
);
|
|
733
714
|
}
|
|
734
715
|
|
|
@@ -771,40 +752,6 @@ export function IssuesTable({ issues, onSelect }: { issues: Issue[]; onSelect?:
|
|
|
771
752
|
);
|
|
772
753
|
}
|
|
773
754
|
|
|
774
|
-
// The PR Files-changed DIFF view: renders each changed file's unified-diff hunks as
|
|
775
|
-
// +/- lines (the GitHub "Files changed" tab). Hunk text comes from the LOCAL PR write's
|
|
776
|
-
// `patch`; observed PRs carry no per-file diff, so a file with no patch shows a summary row
|
|
777
|
-
// only (no fabricated hunks). Each diff line is classed add/del/context so it renders like GitHub.
|
|
778
|
-
export function FilesDiff({ files }: { files: ChangedFile[] }) {
|
|
779
|
-
if (!files.length) return <div className="empty">No file diffs in this snapshot.</div>;
|
|
780
|
-
return (
|
|
781
|
-
<section className="files-diff" data-testid="files-diff">
|
|
782
|
-
<h3>Files changed <span className="count">{files.length}</span></h3>
|
|
783
|
-
{files.map((file) => (
|
|
784
|
-
<article className="diff-file" key={`${file.repo}#${file.number}#${file.filename}`}>
|
|
785
|
-
<div className="diff-file-head">
|
|
786
|
-
<span className={`diff-status ${file.status ?? "modified"}`}>{file.status ?? "modified"}</span>
|
|
787
|
-
<strong className="diff-filename">{file.filename}</strong>
|
|
788
|
-
<span className="diff-stat">+{file.additions ?? 0} −{file.deletions ?? 0}</span>
|
|
789
|
-
</div>
|
|
790
|
-
{file.patch ? (
|
|
791
|
-
<pre className="diff-hunk" data-testid="diff-hunk">
|
|
792
|
-
{file.patch.split("\n").map((line, idx) => {
|
|
793
|
-
const cls = line.startsWith("+") && !line.startsWith("+++") ? "diff-line-add"
|
|
794
|
-
: line.startsWith("-") && !line.startsWith("---") ? "diff-line-del"
|
|
795
|
-
: line.startsWith("@@") ? "diff-line-hunk" : "diff-line-ctx";
|
|
796
|
-
return <div className={cls} key={idx}>{line || " "}</div>;
|
|
797
|
-
})}
|
|
798
|
-
</pre>
|
|
799
|
-
) : (
|
|
800
|
-
<div className="sha diff-no-patch">No diff hunk in snapshot (observed PRs carry counts only).</div>
|
|
801
|
-
)}
|
|
802
|
-
</article>
|
|
803
|
-
))}
|
|
804
|
-
</section>
|
|
805
|
-
);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
755
|
// The Issue DETAIL page: the selected issue's header (state/labels/assignees/milestone),
|
|
809
756
|
// its derived state-change Timeline (labeled/assigned/locked/closed events), and its
|
|
810
757
|
// comments feed. Mirrors GitHub's single-issue screen. Observed comments are labelled.
|
|
@@ -1360,157 +1307,6 @@ export function DiscussionsView({ discussions, selectedKey, onSelect }: {
|
|
|
1360
1307
|
);
|
|
1361
1308
|
}
|
|
1362
1309
|
|
|
1363
|
-
export function Conversation({ pr, reviews, commits, comments, reviewComments, files }: {
|
|
1364
|
-
pr?: PullRequest;
|
|
1365
|
-
reviews: Review[];
|
|
1366
|
-
commits: Commit[];
|
|
1367
|
-
comments: Comment[];
|
|
1368
|
-
reviewComments: Comment[];
|
|
1369
|
-
files: ChangedFile[];
|
|
1370
|
-
}) {
|
|
1371
|
-
if (!pr) return <div className="empty">Select a pull request.</div>;
|
|
1372
|
-
const state = pr.merged ? "merged" : (pr.state ?? "open");
|
|
1373
|
-
const labels = pr.labels ?? [];
|
|
1374
|
-
const assignees = pr.assignees ?? [];
|
|
1375
|
-
return (
|
|
1376
|
-
<section className="conversation">
|
|
1377
|
-
<h2>{pr.title || pr.url || `${pr.repo}#${pr.number}`} <span>#{pr.number}</span></h2>
|
|
1378
|
-
<div className="state-line">
|
|
1379
|
-
<span className={`state-badge ${state}`}>{state}</span>
|
|
1380
|
-
<CiStatus status={pr.ciStatus} />
|
|
1381
|
-
<strong>{pr.repo}</strong>
|
|
1382
|
-
<span className="sha">
|
|
1383
|
-
{pr.changedFiles ?? 0} files changed · {pr.commitsCount ?? 0} commits · {pr.reviewCount ?? 0} reviews · {pr.commentsCount ?? 0} comments · {pr.reviewCommentsCount ?? 0} review comments
|
|
1384
|
-
</span>
|
|
1385
|
-
{pr.milestone ? <span className="label milestone" title="Milestone">🏷 {pr.milestone}</span> : null}
|
|
1386
|
-
{pr.mergedAt ? <span className="sha">merged {new Date(pr.mergedAt).toLocaleString()}</span> : null}
|
|
1387
|
-
{pr.url ? <a className="branch" href={pr.url} target="_blank" rel="noreferrer">View on GitHub</a> : null}
|
|
1388
|
-
</div>
|
|
1389
|
-
{labels.length || assignees.length ? (
|
|
1390
|
-
<div className="meta-row">
|
|
1391
|
-
{labels.map((label) => label.name ? <span className="label" key={`l-${label.name}`}>{label.name}</span> : null)}
|
|
1392
|
-
{assignees.map((login) => <span className="label assignee" key={`a-${login}`}>@{login}</span>)}
|
|
1393
|
-
</div>
|
|
1394
|
-
) : null}
|
|
1395
|
-
<div className="timeline-item">
|
|
1396
|
-
<Avatar src={pr.authorAvatarUrl} label={pr.author || "PR"} />
|
|
1397
|
-
<article className="comment">
|
|
1398
|
-
<div className="comment-head"><strong>{pr.author || "GitHub"}</strong> opened this pull request</div>
|
|
1399
|
-
<div className="comment-body">
|
|
1400
|
-
{pr.body || "No real PR body was included in the synced seed."}
|
|
1401
|
-
{pr.sourceIssue ? <div className="sha">source issue: {pr.sourceIssue}{pr.sourceIssueTitle ? ` ${pr.sourceIssueTitle}` : ""}</div> : null}
|
|
1402
|
-
{pr.mergeCommit ? <div className="sha">merge commit: {pr.mergeCommit}</div> : null}
|
|
1403
|
-
</div>
|
|
1404
|
-
</article>
|
|
1405
|
-
</div>
|
|
1406
|
-
{commits.length ? (
|
|
1407
|
-
<section className="snapshot-section">
|
|
1408
|
-
<h3>Commits</h3>
|
|
1409
|
-
{commits.map((commit) => (
|
|
1410
|
-
<div className="timeline-item" key={commit.sha}>
|
|
1411
|
-
<Avatar src={commit.authorAvatarUrl} label={commit.authorLogin || commit.author} />
|
|
1412
|
-
<article className="comment">
|
|
1413
|
-
<div className="comment-head"><strong>{commit.authorLogin || commit.author || "Git author"}</strong> committed {commit.date ? new Date(commit.date).toLocaleString() : ""}</div>
|
|
1414
|
-
<div className="comment-body">{commit.message || commit.sha}<div className="sha">{shortSha(commit.sha)}</div></div>
|
|
1415
|
-
</article>
|
|
1416
|
-
</div>
|
|
1417
|
-
))}
|
|
1418
|
-
</section>
|
|
1419
|
-
) : null}
|
|
1420
|
-
{comments.map((comment, index) => (
|
|
1421
|
-
<div className="timeline-item" key={`comment-${comment.url || index}`}>
|
|
1422
|
-
<Avatar src={comment.authorAvatarUrl} label={comment.author} />
|
|
1423
|
-
<article className="comment">
|
|
1424
|
-
<div className="comment-head"><strong>{comment.author || "GitHub user"}</strong> commented {comment.createdAt ? new Date(comment.createdAt).toLocaleString() : ""}</div>
|
|
1425
|
-
<div className="comment-body">{comment.body || ""}</div>
|
|
1426
|
-
</article>
|
|
1427
|
-
</div>
|
|
1428
|
-
))}
|
|
1429
|
-
{reviews.map((review, index) => (
|
|
1430
|
-
<div className="timeline-item" key={`${review.reviewer}-${index}`}>
|
|
1431
|
-
<Avatar src={review.reviewerAvatarUrl} label={review.reviewer} />
|
|
1432
|
-
<article className="review-card">
|
|
1433
|
-
<div className="review-head">{review.reviewer || "GitHub user"} {review.state || "reviewed"} {review.submittedAt ? new Date(review.submittedAt).toLocaleString() : ""}</div>
|
|
1434
|
-
<div className="review-body">{review.body || "No review body."}</div>
|
|
1435
|
-
</article>
|
|
1436
|
-
</div>
|
|
1437
|
-
))}
|
|
1438
|
-
{reviewComments.map((comment, index) => (
|
|
1439
|
-
<div className="timeline-item" key={`review-comment-${comment.url || index}`}>
|
|
1440
|
-
<Avatar src={comment.authorAvatarUrl} label={comment.author} />
|
|
1441
|
-
<article className="comment">
|
|
1442
|
-
<div className="comment-head"><strong>{comment.author || "GitHub user"}</strong> commented on {comment.path || "a file"}</div>
|
|
1443
|
-
<div className="comment-body">{comment.body || ""}</div>
|
|
1444
|
-
</article>
|
|
1445
|
-
</div>
|
|
1446
|
-
))}
|
|
1447
|
-
{files.length ? (
|
|
1448
|
-
<section className="snapshot-section">
|
|
1449
|
-
<h3>Files changed</h3>
|
|
1450
|
-
<div className="file-list">
|
|
1451
|
-
{files.map((file) => (
|
|
1452
|
-
<div className="file-row" key={file.filename}>
|
|
1453
|
-
<span>{file.status || "modified"}</span>
|
|
1454
|
-
<strong>{file.filename}</strong>
|
|
1455
|
-
<span>+{file.additions ?? 0} -{file.deletions ?? 0}</span>
|
|
1456
|
-
</div>
|
|
1457
|
-
))}
|
|
1458
|
-
</div>
|
|
1459
|
-
</section>
|
|
1460
|
-
) : null}
|
|
1461
|
-
</section>
|
|
1462
|
-
);
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
export function Aside({ pr, reviews, commits, files }: { pr?: PullRequest; reviews: Review[]; commits: Commit[]; files: ChangedFile[] }) {
|
|
1466
|
-
if (!pr) return <aside className="sidebar" />;
|
|
1467
|
-
return (
|
|
1468
|
-
<aside className="sidebar">
|
|
1469
|
-
<section className="side-section">
|
|
1470
|
-
<h3>Reviewers</h3>
|
|
1471
|
-
{reviews.length ? reviews.map((review, index) => <div className={`label ${review.observed ? "" : "approved"}`} key={`${review.reviewer}-${index}`}>{review.state} {review.reviewer}</div>) : <p className="sha">No reviews in snapshot.</p>}
|
|
1472
|
-
{(pr.requestedReviewers ?? []).length ? (
|
|
1473
|
-
<div className="requested-reviewers">
|
|
1474
|
-
{(pr.requestedReviewers ?? []).map((login) => <span className="label requested" key={`req-${login}`} title="Requested reviewer">@{login} (requested)</span>)}
|
|
1475
|
-
</div>
|
|
1476
|
-
) : null}
|
|
1477
|
-
</section>
|
|
1478
|
-
<section className="side-section">
|
|
1479
|
-
<h3>Checks</h3>
|
|
1480
|
-
{pr.ciStatus ? <div className={`ci-line ${pr.ciStatus}`}><CiStatus status={pr.ciStatus} /> {pr.ciStatus}</div> : <p className="sha">No CI checks in snapshot.</p>}
|
|
1481
|
-
</section>
|
|
1482
|
-
<section className="side-section">
|
|
1483
|
-
<h3>Milestone</h3>
|
|
1484
|
-
{pr.milestone ? <span className="label milestone">🏷 {pr.milestone}</span> : <p className="sha">No milestone.</p>}
|
|
1485
|
-
</section>
|
|
1486
|
-
<section className="side-section">
|
|
1487
|
-
<h3>Labels</h3>
|
|
1488
|
-
{(pr.labels ?? []).length ? (pr.labels ?? []).map((label) => label.name ? <span className="label" key={label.name}>{label.name}</span> : null) : <p className="sha">None.</p>}
|
|
1489
|
-
</section>
|
|
1490
|
-
<section className="side-section">
|
|
1491
|
-
<h3>Assignees</h3>
|
|
1492
|
-
{(pr.assignees ?? []).length ? (pr.assignees ?? []).map((login) => <div className="label assignee" key={login}>@{login}</div>) : <p className="sha">No one assigned.</p>}
|
|
1493
|
-
</section>
|
|
1494
|
-
<section className="side-section">
|
|
1495
|
-
<h3>Development</h3>
|
|
1496
|
-
{pr.baseBranch ? <div className="branch">base: {pr.baseBranch}</div> : null}
|
|
1497
|
-
{pr.headBranch ? <div className="branch">head: {pr.headBranch}</div> : null}
|
|
1498
|
-
{pr.headSha ? <p className="sha">{shortSha(pr.headSha)}</p> : <p className="sha">No branch or commit list in synced seed.</p>}
|
|
1499
|
-
</section>
|
|
1500
|
-
<section className="side-section">
|
|
1501
|
-
<h3>Repository</h3>
|
|
1502
|
-
<p>{pr.repo}</p>
|
|
1503
|
-
</section>
|
|
1504
|
-
<section className="side-section">
|
|
1505
|
-
<h3>Snapshot Counts</h3>
|
|
1506
|
-
<p className="sha">{commits.length} commits</p>
|
|
1507
|
-
<p className="sha">{files.length} files</p>
|
|
1508
|
-
<p className="sha">{pr.additions ?? 0} additions / {pr.deletions ?? 0} deletions</p>
|
|
1509
|
-
</section>
|
|
1510
|
-
</aside>
|
|
1511
|
-
);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1310
|
// Guard the top-level mount: the browser bundle has a DOM and mounts exactly as before,
|
|
1515
1311
|
// but importing this module in a non-DOM test (for renderToStaticMarkup of the exported
|
|
1516
1312
|
// presentational components) must not touch `document`.
|