Skip to content

Commit 7096f90

Browse files
committed
feat(pingdotgg#6315): add Issues to Repository with shared filters
1 parent 0de5387 commit 7096f90

14 files changed

Lines changed: 710 additions & 329 deletions

‎apps/web/src/components/ChatView.tsx‎

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ import {
214214
usePreviewMiniPlayerStore,
215215
} from "../previewMiniPlayerStore";
216216
import { IssueDetailPanel } from "./issue/IssueDetailPanel";
217-
import { IssuesPanel } from "./issue/IssuesPanel";
218217
import { IssuesUnavailableState } from "./issue/IssuesUnavailableState";
219218
import { pullRequestPanelContext } from "./pullRequest/pullRequestDetail.logic";
220219
import { PullRequestDetailPanel } from "./pullRequest/PullRequestDetailPanel";
@@ -2036,8 +2035,10 @@ export default function ChatView(props: ChatViewProps) {
20362035
const activeIssueSurfaceId =
20372036
activeRightPanelSurface?.kind === "issue"
20382037
? activeRightPanelSurface.id
2039-
: activeRightPanelSurface?.kind === "issues" && activeRightPanelSurface.selected
2040-
? issueSurfaceId(activeRightPanelSurface.selected)
2038+
: activeRightPanelSurface?.kind === "repository" &&
2039+
activeRightPanelSurface.view === "issues" &&
2040+
activeRightPanelSurface.selectedIssue
2041+
? issueSurfaceId(activeRightPanelSurface.selectedIssue)
20412042
: undefined;
20422043
const handleIssueTabStatusChange = useCallback(
20432044
(status: IssueTabStatus) => {
@@ -4611,15 +4612,30 @@ export default function ChatView(props: ChatViewProps) {
46114612
useRightPanelStore.getState().openRepository(activeThreadRef, "pull-requests");
46124613
}, [activeThreadRef, pullRequestsSurfaceAvailable]);
46134614
const addRepositorySurface = useCallback(() => {
4614-
if (!activeThreadRef || (!(isGitRepo && supportsGitHistory) && !pullRequestsSurfaceAvailable))
4615+
const issuesAvailable = supportsIssues && activeProject !== null;
4616+
if (
4617+
!activeThreadRef ||
4618+
(!(isGitRepo && supportsGitHistory) && !pullRequestsSurfaceAvailable && !issuesAvailable)
4619+
)
46154620
return;
46164621
useRightPanelStore
46174622
.getState()
46184623
.openRepository(
46194624
activeThreadRef,
4620-
isGitRepo && supportsGitHistory ? "history" : "pull-requests",
4625+
isGitRepo && supportsGitHistory
4626+
? "history"
4627+
: pullRequestsSurfaceAvailable
4628+
? "pull-requests"
4629+
: "issues",
46214630
);
4622-
}, [activeThreadRef, isGitRepo, pullRequestsSurfaceAvailable, supportsGitHistory]);
4631+
}, [
4632+
activeProject,
4633+
activeThreadRef,
4634+
isGitRepo,
4635+
pullRequestsSurfaceAvailable,
4636+
supportsGitHistory,
4637+
supportsIssues,
4638+
]);
46234639
const { state: deviceState, loaded: deviceStateLoaded } = useDeviceState(
46244640
activeThreadRef?.environmentId ?? null,
46254641
);
@@ -4869,7 +4885,7 @@ export default function ChatView(props: ChatViewProps) {
48694885
) {
48704886
panels.openProactive(
48714887
activeThreadRef,
4872-
{ id: "repository", kind: "repository", view: "pull-requests" },
4888+
{ id: "repository", kind: "repository", view: "pull-requests", selectedIssue: null },
48734889
userActionRevision,
48744890
);
48754891
} else if (
@@ -5998,22 +6014,6 @@ export default function ChatView(props: ChatViewProps) {
59986014
},
59996015
[composerOverlayElement],
60006016
);
6001-
// Which issue is not something the chooser can know, so it opens the browser and the reader
6002-
// picks inside it — in the same tab, rather than as one more of them.
6003-
const addIssueSurface = useCallback(() => {
6004-
if (!activeThreadRef) return;
6005-
useRightPanelStore.getState().openIssues(activeThreadRef);
6006-
}, [activeThreadRef]);
6007-
const selectIssueInPanel = useCallback(
6008-
(
6009-
target: { projectId: string; provider?: string; repository: string; number: number } | null,
6010-
) => {
6011-
if (!activeThreadRef) return;
6012-
useRightPanelStore.getState().selectIssueInPanel(activeThreadRef, target);
6013-
},
6014-
[activeThreadRef],
6015-
);
6016-
const issueSurfaceAvailable = supportsIssues && activeProject !== null;
60176017
const publishComposerOverlayHeight = useCallback(
60186018
(height: number) => {
60196019
const nextHeight = Math.ceil(height);
@@ -9761,10 +9761,29 @@ export default function ChatView(props: ChatViewProps) {
97619761
environmentId={environmentId}
97629762
cwd={isGitRepo ? gitCwd : null}
97639763
threadRef={activeThreadRef}
9764+
issueContext={
9765+
activeProject && activeProjectRef
9766+
? {
9767+
projectId: activeProject.id,
9768+
handoffTarget: {
9769+
kind: "existing-thread",
9770+
projectRef: activeProjectRef,
9771+
draftId: composerDraftTarget,
9772+
},
9773+
}
9774+
: null
9775+
}
9776+
selectedIssue={renderedRightPanelSurface.selectedIssue ?? null}
97649777
view={renderedRightPanelSurface.view}
97659778
active={rightPanelOpen}
97669779
gitHistoryAvailable={supportsGitHistory && isGitRepo}
97679780
gitHistoryPanelStore={gitHistoryPanelStore}
9781+
issuesAvailable={supportsIssues && activeProject !== null && activeProjectRef !== null}
9782+
onSelectIssue={(selected) =>
9783+
useRightPanelStore.getState().selectRepositoryIssue(activeThreadRef, selected)
9784+
}
9785+
onIssueStateChange={handleIssueTabStatusChange}
9786+
onOpenLinkedPullRequest={openLinkedPullRequest}
97689787
onViewChange={(view) =>
97699788
useRightPanelStore.getState().selectRepositoryView(activeThreadRef, view)
97709789
}
@@ -9823,12 +9842,10 @@ export default function ChatView(props: ChatViewProps) {
98239842
}
98249843
onOpenLinkedIssue={openLinkedIssue}
98259844
/>
9826-
) : (renderedRightPanelSurface?.kind === "issue" ||
9827-
renderedRightPanelSurface?.kind === "issues") &&
9845+
) : renderedRightPanelSurface?.kind === "issue" &&
98289846
issuesSurfaceCapabilityState === "loading" ? (
98299847
<DetailGhost label="Loading issues" />
9830-
) : (renderedRightPanelSurface?.kind === "issue" ||
9831-
renderedRightPanelSurface?.kind === "issues") &&
9848+
) : renderedRightPanelSurface?.kind === "issue" &&
98329849
issuesSurfaceCapabilityState === "unavailable" ? (
98339850
<IssuesUnavailableState
98349851
title="Issues unavailable"
@@ -9859,19 +9876,6 @@ export default function ChatView(props: ChatViewProps) {
98599876
onStateChange={handleIssueTabStatusChange}
98609877
onOpenLinkedPullRequest={openLinkedPullRequest}
98619878
/>
9862-
) : renderedRightPanelSurface?.kind === "issues" && activeProject && activeProjectRef ? (
9863-
<IssuesPanel
9864-
environmentId={activeThread.environmentId}
9865-
projectId={activeProject.id}
9866-
selected={renderedRightPanelSurface.selected}
9867-
onSelect={selectIssueInPanel}
9868-
handoffTarget={{
9869-
kind: "existing-thread",
9870-
projectRef: activeProjectRef,
9871-
draftId: composerDraftTarget,
9872-
}}
9873-
onStateChange={handleIssueTabStatusChange}
9874-
/>
98759879
) : renderedRightPanelSurface?.kind === "agents" ? (
98769880
<AgentsPanel
98779881
model={agentPanelModel}
@@ -10536,17 +10540,18 @@ export default function ChatView(props: ChatViewProps) {
1053610540
onAddRepository={addRepositorySurface}
1053710541
onAddFiles={addFilesSurface}
1053810542
onAddPullRequest={addPullRequestSurface}
10539-
onAddIssue={addIssueSurface}
1054010543
onAddAgents={addAgentsSurface}
1054110544
onAddDevice={addDeviceSurface}
1054210545
browserAvailable={isPreviewSupportedInRuntime()}
1054310546
terminalAvailable={activeProject !== null}
1054410547
diffAvailable={isServerThread && isGitRepo}
10545-
repositoryAvailable={(isGitRepo && supportsGitHistory) || pullRequestsSurfaceAvailable}
10548+
repositoryAvailable={
10549+
(isGitRepo && supportsGitHistory) ||
10550+
pullRequestsSurfaceAvailable ||
10551+
(supportsIssues && activeProject !== null)
10552+
}
1054610553
filesAvailable={activeProject !== null}
1054710554
pullRequestAvailable={pullRequestSurfaceAvailable}
10548-
issueAvailable={issueSurfaceAvailable}
10549-
pullRequestsAvailable={pullRequestsSurfaceAvailable}
1055010555
agentsAvailable
1055110556
issueStatuses={issueTabStatuses}
1055210557
deviceAvailable={activeThreadRef !== null}
@@ -10598,17 +10603,18 @@ export default function ChatView(props: ChatViewProps) {
1059810603
onAddRepository={addRepositorySurface}
1059910604
onAddFiles={addFilesSurface}
1060010605
onAddPullRequest={addPullRequestSurface}
10601-
onAddIssue={addIssueSurface}
1060210606
onAddAgents={addAgentsSurface}
1060310607
onAddDevice={addDeviceSurface}
1060410608
browserAvailable={isPreviewSupportedInRuntime()}
1060510609
terminalAvailable={activeProject !== null}
1060610610
diffAvailable={isServerThread && isGitRepo}
10607-
repositoryAvailable={(isGitRepo && supportsGitHistory) || pullRequestsSurfaceAvailable}
10611+
repositoryAvailable={
10612+
(isGitRepo && supportsGitHistory) ||
10613+
pullRequestsSurfaceAvailable ||
10614+
(supportsIssues && activeProject !== null)
10615+
}
1060810616
filesAvailable={activeProject !== null}
1060910617
pullRequestAvailable={pullRequestSurfaceAvailable}
10610-
issueAvailable={issueSurfaceAvailable}
10611-
pullRequestsAvailable={pullRequestsSurfaceAvailable}
1061210618
agentsAvailable
1061310619
issueStatuses={issueTabStatuses}
1061410620
deviceAvailable={activeThreadRef !== null}

‎apps/web/src/components/RepositoryPanel.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { repositoryViewFromKey } from "./RepositoryPanel";
44

55
describe("RepositoryPanel", () => {
66
it("moves through repository views with the tablist keys", () => {
7-
expect(repositoryViewFromKey("history", "ArrowRight")).toBe("pull-requests");
8-
expect(repositoryViewFromKey("pull-requests", "ArrowLeft")).toBe("history");
7+
expect(repositoryViewFromKey("history", "ArrowRight")).toBe("issues");
8+
expect(repositoryViewFromKey("pull-requests", "ArrowLeft")).toBe("issues");
99
expect(repositoryViewFromKey("pull-requests", "Home")).toBe("history");
1010
expect(repositoryViewFromKey("history", "End")).toBe("pull-requests");
1111
});

‎apps/web/src/components/RepositoryPanel.tsx‎

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import type { EnvironmentId, ScopedThreadRef } from "@t3tools/contracts";
1+
import type { EnvironmentId, ProjectId, ScopedThreadRef } from "@t3tools/contracts";
22
import { Activity, lazy, Suspense, useRef, useState, type KeyboardEvent } from "react";
33

4-
import type { RepositoryView } from "~/rightPanelStore";
4+
import type { IssueSelection, RepositoryView } from "~/rightPanelStore";
55
import type { GitHistoryPanelStore } from "./git-history/GitHistoryPanelState";
66

7+
import type { IssueHandoffTarget } from "./issue/IssueDetailPanel";
78
import { ThreadPullRequestsPanel } from "./pullRequest/ThreadPullRequestsPanel";
9+
import type { IssueTabStatus } from "./RightPanelTabs";
810
import { Button } from "./ui/button";
911

1012
const GitHistoryPanel = lazy(() => import("./GitHistoryPanel"));
13+
const IssuesPanel = lazy(() =>
14+
import("./issue/IssuesPanel").then(({ IssuesPanel }) => ({ default: IssuesPanel })),
15+
);
1116

12-
const views = ["history", "pull-requests"] as const;
17+
const views = ["history", "issues", "pull-requests"] as const;
1318

1419
export function repositoryViewFromKey(view: RepositoryView, key: string): RepositoryView | null {
1520
const index = views.indexOf(view);
@@ -20,25 +25,39 @@ export function repositoryViewFromKey(view: RepositoryView, key: string): Reposi
2025
return null;
2126
}
2227

23-
export default function RepositoryPanel(props: {
28+
interface RepositoryPanelProps {
2429
readonly environmentId: EnvironmentId;
2530
readonly cwd: string | null;
2631
readonly threadRef: ScopedThreadRef;
32+
readonly issueContext: { projectId: ProjectId; handoffTarget: IssueHandoffTarget } | null;
33+
readonly selectedIssue: IssueSelection | null;
2734
readonly view: RepositoryView;
2835
readonly active: boolean;
2936
readonly gitHistoryAvailable: boolean;
3037
readonly gitHistoryPanelStore: GitHistoryPanelStore;
38+
readonly issuesAvailable: boolean;
3139
readonly onViewChange: (view: RepositoryView) => void;
32-
}) {
40+
readonly onSelectIssue: (selected: IssueSelection | null) => void;
41+
readonly onIssueStateChange: (status: IssueTabStatus) => void;
42+
readonly onOpenLinkedPullRequest: (link: {
43+
repository: string;
44+
number: number;
45+
url: string;
46+
}) => void;
47+
}
48+
49+
export default function RepositoryPanel(props: RepositoryPanelProps) {
3350
const tabs = useRef<Array<HTMLButtonElement | null>>([]);
3451
const [historyActivated, setHistoryActivated] = useState(props.view === "history");
52+
const [issuesActivated, setIssuesActivated] = useState(props.view === "issues");
3553
if (props.view === "history" && !historyActivated) setHistoryActivated(true);
36-
const select = (view: RepositoryView) => props.onViewChange(view);
37-
const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>, view: RepositoryView) => {
54+
if (props.view === "issues" && !issuesActivated) setIssuesActivated(true);
55+
56+
const move = (event: KeyboardEvent<HTMLButtonElement>, view: RepositoryView) => {
3857
const next = repositoryViewFromKey(view, event.key);
39-
if (next === null) return;
58+
if (!next) return;
4059
event.preventDefault();
41-
select(next);
60+
props.onViewChange(next);
4261
tabs.current[views.indexOf(next)]?.focus();
4362
};
4463

@@ -65,10 +84,10 @@ export default function RepositoryPanel(props: {
6584
aria-selected={props.view === view}
6685
aria-controls={`repository-panel-${view}`}
6786
tabIndex={props.view === view ? 0 : -1}
68-
onClick={() => select(view)}
69-
onKeyDown={(event) => onKeyDown(event, view)}
87+
onClick={() => props.onViewChange(view)}
88+
onKeyDown={(event) => move(event, view)}
7089
>
71-
{view === "history" ? "History" : "Pull Requests"}
90+
{view === "history" ? "History" : view === "issues" ? "Issues" : "Pull Requests"}
7291
</Button>
7392
))}
7493
</div>
@@ -80,7 +99,7 @@ export default function RepositoryPanel(props: {
8099
aria-labelledby="repository-tab-history"
81100
className="min-h-0 flex-1"
82101
>
83-
{props.gitHistoryAvailable && props.cwd !== null ? (
102+
{props.gitHistoryAvailable && props.cwd ? (
84103
<Suspense fallback={null}>
85104
<GitHistoryPanel
86105
environmentId={props.environmentId}
@@ -98,6 +117,36 @@ export default function RepositoryPanel(props: {
98117
</div>
99118
</Activity>
100119
) : null}
120+
{issuesActivated ? (
121+
<Activity mode={props.view === "issues" ? "visible" : "hidden"}>
122+
<div
123+
id="repository-panel-issues"
124+
role="tabpanel"
125+
aria-labelledby="repository-tab-issues"
126+
className="min-h-0 flex-1"
127+
>
128+
{props.issuesAvailable && props.issueContext ? (
129+
<Suspense fallback={null}>
130+
<IssuesPanel
131+
environmentId={props.environmentId}
132+
projectId={props.issueContext.projectId}
133+
selected={props.selectedIssue}
134+
onSelect={props.onSelectIssue}
135+
handoffTarget={props.issueContext.handoffTarget}
136+
onStateChange={props.onIssueStateChange}
137+
onOpenLinkedPullRequest={props.onOpenLinkedPullRequest}
138+
/>
139+
</Suspense>
140+
) : (
141+
<div className="flex size-full items-center justify-center p-6 text-center text-sm text-muted-foreground">
142+
{props.issuesAvailable
143+
? "Open a project thread to browse issues."
144+
: "Update the environment server to browse issues."}
145+
</div>
146+
)}
147+
</div>
148+
</Activity>
149+
) : null}
101150
<Activity mode={props.view === "pull-requests" ? "visible" : "hidden"}>
102151
<div
103152
id="repository-panel-pull-requests"

‎apps/web/src/components/RightPanelTabs.test.tsx‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ function renderTabs(
121121
onAddPullRequest={() => undefined}
122122
onAddRepository={() => undefined}
123123
repositoryAvailable={false}
124-
onAddIssue={() => undefined}
125124
onAddDiff={() => undefined}
126125
onAddFiles={() => undefined}
127126
onAddAgents={() => undefined}
@@ -132,8 +131,6 @@ function renderTabs(
132131
diffAvailable={false}
133132
filesAvailable={false}
134133
pullRequestAvailable={false}
135-
issueAvailable={false}
136-
pullRequestsAvailable={false}
137134
agentsAvailable={false}
138135
deviceAvailable={false}
139136
>

0 commit comments

Comments
 (0)