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
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"dependencies": {
"@cloudflare/vite-plugin": "^1.26.0",
"@pierre/diffs": "^1.1.12",
"@quickhub/icons": "workspace:*",
"@quickhub/ui": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
Expand Down
16 changes: 15 additions & 1 deletion apps/dashboard/src/components/layouts/dashboard-topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const themeOptions = [
const tabIconMap = {
pull: GitPullRequestIcon,
issue: IssuesIcon,
review: ReviewsIcon,
} as const;

const primaryNavRoutes = ["/", "/pulls", "/issues", "/reviews"] as const;
Expand Down Expand Up @@ -341,7 +342,20 @@ function DetailTab({
>
<Icon size={13} strokeWidth={2} className={`shrink-0 ${tab.iconColor}`} />
<span className="max-w-32 truncate">{tab.title}</span>
<span className="tabular-nums opacity-60">#{tab.number}</span>
{tab.type === "review" ? (
<span className="flex items-center gap-1 font-mono text-[11px] font-medium tabular-nums">
{tab.additions != null && (
<span className="text-green-500">+{tab.additions}</span>
)}
{tab.deletions != null && (
<span className="text-red-500">-{tab.deletions}</span>
)}
</span>
) : (
<span className="tabular-nums text-muted-foreground text-[11px]">
#{tab.number}
</span>
)}
<button
type="button"
onClick={(e) => {
Expand Down
179 changes: 179 additions & 0 deletions apps/dashboard/src/lib/github.functions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createServerFn } from "@tanstack/react-start";
import { type Octokit as OctokitType, RequestError } from "octokit";
import type {
CreateReviewCommentInput,
GitHubActor,
GitHubLabel,
IssueComment,
Expand All @@ -11,10 +12,13 @@ import type {
MyPullsResult,
PullComment,
PullDetail,
PullFile,
PullPageData,
PullReviewComment,
PullStatus,
PullSummary,
RepositoryRef,
SubmitReviewInput,
UserRepoSummary,
} from "./github.types";
import {
Expand Down Expand Up @@ -60,6 +64,12 @@ type RepoIssueDetail = Awaited<
type AuthenticatedUser = Awaited<
ReturnType<GitHubClient["rest"]["users"]["getAuthenticated"]>
>["data"];
type RepoPullFile = Awaited<
ReturnType<GitHubClient["rest"]["pulls"]["listFiles"]>
>["data"][number];
type RepoPullReviewComment = Awaited<
ReturnType<GitHubClient["rest"]["pulls"]["listReviewComments"]>
>["data"][number];

type RepoState = "all" | "closed" | "open";
type PullSort = "created" | "long-running" | "popularity" | "updated";
Expand Down Expand Up @@ -1364,3 +1374,172 @@ export const updatePullBranch = createServerFn({ method: "POST" })
return false;
}
});

async function getPullFilesResult(
context: GitHubContext,
data: PullFromRepoInput,
): Promise<PullFile[]> {
return getCachedGitHubRequest<RepoPullFile[], PullFile[]>({
context,
resource: "pulls.files",
params: data,
freshForMs: githubCachePolicy.detail.staleTimeMs,
request: (headers) =>
context.octokit.rest.pulls.listFiles({
owner: data.owner,
repo: data.repo,
pull_number: data.pullNumber,
per_page: 300,
headers,
}),
mapData: (files) =>
files.map((file) => ({
sha: file.sha,
filename: file.filename,
status: file.status as PullFile["status"],
additions: file.additions,
deletions: file.deletions,
changes: file.changes,
patch: file.patch ?? null,
previousFilename: file.previous_filename ?? null,
})),
});
}

export const getPullFiles = createServerFn({ method: "GET" })
.inputValidator(identityValidator<PullFromRepoInput>)
.handler(async ({ data }): Promise<PullFile[]> => {
const context = await getGitHubContext();
if (!context) {
return [];
}

return getPullFilesResult(context, data);
});

async function getPullReviewCommentsResult(
context: GitHubContext,
data: PullFromRepoInput,
): Promise<PullReviewComment[]> {
return getCachedGitHubRequest<RepoPullReviewComment[], PullReviewComment[]>({
context,
resource: "pulls.reviewComments",
params: data,
freshForMs: githubCachePolicy.activity.staleTimeMs,
request: (headers) =>
context.octokit.rest.pulls.listReviewComments({
owner: data.owner,
repo: data.repo,
pull_number: data.pullNumber,
per_page: 100,
headers,
}),
mapData: (comments) =>
comments.map((comment) => ({
id: comment.id,
body: comment.body,
path: comment.path,
line: comment.line ?? null,
side: (comment.side?.toUpperCase() as "LEFT" | "RIGHT") ?? "RIGHT",
createdAt: comment.created_at,
updatedAt: comment.updated_at,
author: comment.user
? {
login: comment.user.login,
avatarUrl: comment.user.avatar_url,
url: comment.user.html_url,
type: comment.user.type ?? "User",
}
: null,
inReplyToId: comment.in_reply_to_id ?? null,
diffHunk: comment.diff_hunk,
})),
});
}

export const getPullReviewComments = createServerFn({ method: "GET" })
.inputValidator(identityValidator<PullFromRepoInput>)
.handler(async ({ data }): Promise<PullReviewComment[]> => {
const context = await getGitHubContext();
if (!context) {
return [];
}

return getPullReviewCommentsResult(context, data);
});

export const submitPullReview = createServerFn({ method: "POST" })
.inputValidator(identityValidator<SubmitReviewInput>)
.handler(async ({ data }): Promise<boolean> => {
const context = await getGitHubContext();
if (!context) {
return false;
}

try {
await context.octokit.rest.pulls.createReview({
owner: data.owner,
repo: data.repo,
pull_number: data.pullNumber,
body: data.body,
event: data.event,
comments: data.comments?.map((c) => ({
path: c.path,
line: c.line,
side: c.side,
body: c.body,
...(c.startLine != null && c.startLine !== c.line
? { start_line: c.startLine, start_side: c.startSide ?? c.side }
: {}),
})),
});
return true;
} catch {
return false;
}
});

export const createReviewComment = createServerFn({ method: "POST" })
.inputValidator(identityValidator<CreateReviewCommentInput>)
.handler(async ({ data }): Promise<PullReviewComment | null> => {
const context = await getGitHubContext();
if (!context) {
return null;
}

try {
const response = await context.octokit.rest.pulls.createReviewComment({
owner: data.owner,
repo: data.repo,
pull_number: data.pullNumber,
body: data.body,
commit_id: data.commitId,
path: data.path,
line: data.line,
side: data.side,
});

const comment = response.data;
return {
id: comment.id,
body: comment.body,
path: comment.path,
line: comment.line ?? null,
side: (comment.side?.toUpperCase() as "LEFT" | "RIGHT") ?? "RIGHT",
createdAt: comment.created_at,
updatedAt: comment.updated_at,
author: comment.user
? {
login: comment.user.login,
avatarUrl: comment.user.avatar_url,
url: comment.user.html_url,
type: comment.user.type ?? "User",
}
: null,
inReplyToId: comment.in_reply_to_id ?? null,
diffHunk: comment.diff_hunk,
};
} catch {
return null;
}
});
34 changes: 34 additions & 0 deletions apps/dashboard/src/lib/github.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
getMyIssues,
getMyPulls,
getPullComments,
getPullFiles,
getPullFromRepo,
getPullPageData,
getPullReviewComments,
getPullStatus,
getPullsFromRepo,
getPullsFromUser,
Expand Down Expand Up @@ -117,6 +119,10 @@ export const githubQueryKeys = {
["github", scope.userId, "pulls", "comments", input] as const,
status: (scope: GitHubQueryScope, input: PullFromRepoQueryInput) =>
["github", scope.userId, "pulls", "status", input] as const,
files: (scope: GitHubQueryScope, input: PullFromRepoQueryInput) =>
["github", scope.userId, "pulls", "files", input] as const,
reviewComments: (scope: GitHubQueryScope, input: PullFromRepoQueryInput) =>
["github", scope.userId, "pulls", "reviewComments", input] as const,
},
issues: {
mine: (scope: GitHubQueryScope) =>
Expand Down Expand Up @@ -247,6 +253,34 @@ export function githubPullStatusQueryOptions(
});
}

export function githubPullFilesQueryOptions(
scope: GitHubQueryScope,
input: PullFromRepoQueryInput,
) {
return queryOptions({
queryKey: githubQueryKeys.pulls.files(scope, input),
queryFn: () => getPullFiles({ data: input }),
staleTime: githubCachePolicy.detail.staleTimeMs,
gcTime: githubCachePolicy.detail.gcTimeMs,
refetchOnMount: "always",
meta: tabPersistedMeta,
});
}

export function githubPullReviewCommentsQueryOptions(
scope: GitHubQueryScope,
input: PullFromRepoQueryInput,
) {
return queryOptions({
queryKey: githubQueryKeys.pulls.reviewComments(scope, input),
queryFn: () => getPullReviewComments({ data: input }),
staleTime: githubCachePolicy.activity.staleTimeMs,
gcTime: githubCachePolicy.activity.gcTimeMs,
refetchOnMount: "always",
meta: tabPersistedMeta,
});
}

export function githubMyIssuesQueryOptions(scope: GitHubQueryScope) {
return queryOptions({
queryKey: githubQueryKeys.issues.mine(scope),
Expand Down
58 changes: 58 additions & 0 deletions apps/dashboard/src/lib/github.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,61 @@ export type PullPageData = {
detail: PullDetail | null;
comments: PullComment[];
};

export type PullFile = {
sha: string | null;
filename: string;
status:
| "added"
| "removed"
| "modified"
| "renamed"
| "copied"
| "changed"
| "unchanged";
additions: number;
deletions: number;
changes: number;
patch: string | null;
previousFilename: string | null;
};

export type PullReviewComment = {
id: number;
body: string;
path: string;
line: number | null;
side: "LEFT" | "RIGHT";
createdAt: string;
updatedAt: string;
author: GitHubActor | null;
inReplyToId: number | null;
diffHunk: string;
};

export type SubmitReviewInput = {
owner: string;
repo: string;
pullNumber: number;
body: string;
event: "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
comments?: Array<{
path: string;
line: number;
side: "LEFT" | "RIGHT";
body: string;
startLine?: number;
startSide?: "LEFT" | "RIGHT";
}>;
};

export type CreateReviewCommentInput = {
owner: string;
repo: string;
pullNumber: number;
body: string;
commitId: string;
path: string;
line: number;
side: "LEFT" | "RIGHT";
};
Loading