feat(ui): forge write operations — comments, replies, close/reopen, edit, reviews, draft, metadata

- server: write routes for all three providers (issue/PR comments, inline review-comment replies, issue/MR updates w/ labels-assignees-milestone, review submit, draft toggle)
- ui: ForgeProvider gains six write ops; shared action components (composer, thread reply, state/review/draft/metadata/edit) wired into ForgeEntityDetailView and GitHub PR Overview
This commit is contained in:
2026-08-16 16:29:24 +00:00
parent 92f0eced34
commit 8f5cfdcd62
44 changed files with 5383 additions and 62 deletions
+78
View File
@@ -2,9 +2,13 @@ import type {
GiteaAPI,
GiteaAuthStatus,
GiteaBranchesResult,
GiteaIssueCommentInput,
GiteaIssueCommentResult,
GiteaIssueCommentsResult,
GiteaIssueGetResult,
GiteaIssuesListResult,
GiteaIssueUpdateInput,
GiteaIssueUpdateResult,
GiteaPullRequest,
GiteaPullRequestCommitsResult,
GiteaPullRequestContextResult,
@@ -15,6 +19,9 @@ import type {
GiteaPullRequestsListResult,
GiteaPullRequestStatusesResult,
GiteaPullRequestUpdateInput,
GiteaPullReviewInput,
GiteaPullReviewResult,
GiteaRepoLabelsResult,
GiteaUserSummary,
} from '@openchamber/ui/lib/api/types';
import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch';
@@ -287,6 +294,77 @@ export const createWebGiteaAPI = ({ urls }: WebGiteaAPIOptions): GiteaAPI => ({
};
},
async issueComment(input: GiteaIssueCommentInput): Promise<GiteaIssueCommentResult> {
const response = await runtimeFetch('/api/gitea/issues/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GiteaIssueCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post Gitea issue comment');
}
return body;
},
async issueUpdate(input: GiteaIssueUpdateInput): Promise<GiteaIssueUpdateResult> {
const response = await runtimeFetch('/api/gitea/issues/update', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GiteaIssueUpdateResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to update Gitea issue');
}
return body;
},
async prComment(input: GiteaIssueCommentInput): Promise<GiteaIssueCommentResult> {
const response = await runtimeFetch('/api/gitea/prs/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GiteaIssueCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post Gitea pull request comment');
}
return body;
},
async prSubmitReview(input: GiteaPullReviewInput): Promise<GiteaPullReviewResult> {
const response = await runtimeFetch('/api/gitea/prs/review', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GiteaPullReviewResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to submit Gitea pull request review');
}
return body;
},
async repoLabels(directory: string, options?: { owner?: string; repo?: string }): Promise<GiteaRepoLabelsResult> {
const params = new URLSearchParams({ directory });
if (options?.owner) {
params.set('owner', options.owner);
}
if (options?.repo) {
params.set('repo', options.repo);
}
const response = await runtimeFetch(
`/api/gitea/repo/labels?${params.toString()}`,
{ method: 'GET', headers: { Accept: 'application/json' } }
);
const body = await jsonOrNull<GiteaRepoLabelsResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to fetch Gitea repo labels');
}
return body;
},
async repoBranches(owner: string, repo: string): Promise<GiteaBranchesResult> {
const response = await runtimeFetch(
`/api/gitea/repo/branches?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`,
+73
View File
@@ -2,7 +2,11 @@ import type {
GitHubAPI,
GitHubAuthStatus,
GitHubIssueCommentsResult,
GitHubIssueCommentInput,
GitHubIssueCommentResult,
GitHubIssueGetResult,
GitHubIssueUpdateInput,
GitHubIssueUpdateResult,
GitHubIssuesListResult,
GitHubPullRequestContextResult,
GitHubPullRequestCommitsResult,
@@ -16,7 +20,11 @@ import type {
GitHubPullRequestReadyResult,
GitHubPullRequestUpdateInput,
GitHubPullRequestStatus,
GitHubPullRequestReviewInput,
GitHubPullRequestReviewResult,
GitHubRepoUpstreamResult,
GitHubReviewCommentInput,
GitHubReviewCommentResult,
GitHubDeviceFlowComplete,
GitHubDeviceFlowStart,
GitHubUserSummary,
@@ -325,4 +333,69 @@ export const createWebGitHubAPI = ({ urls }: WebGitHubAPIOptions): GitHubAPI =>
}
return payload;
},
async issueComment(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult> {
const response = await runtimeFetch('/api/github/issues/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubIssueCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitHub comment');
}
return body;
},
async issueUpdate(input: GitHubIssueUpdateInput): Promise<GitHubIssueUpdateResult> {
const response = await runtimeFetch('/api/github/issues/update', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubIssueUpdateResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to update GitHub issue');
}
return body;
},
async prComment(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult> {
const response = await runtimeFetch('/api/github/pulls/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubIssueCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitHub PR comment');
}
return body;
},
async prReviewComment(input: GitHubReviewCommentInput): Promise<GitHubReviewCommentResult> {
const response = await runtimeFetch('/api/github/pulls/review-comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubReviewCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitHub review comment');
}
return body;
},
async prSubmitReview(input: GitHubPullRequestReviewInput): Promise<GitHubPullRequestReviewResult> {
const response = await runtimeFetch('/api/github/pulls/review', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubPullRequestReviewResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to submit GitHub review');
}
return body;
},
});
+60
View File
@@ -2,9 +2,13 @@ import type {
GitLabAPI,
GitLabAuthStatus,
GitLabBranchesResult,
GitLabIssueCommentResult,
GitLabIssueCommentsResult,
GitLabIssueCommentInput,
GitLabIssueGetResult,
GitLabIssuesListResult,
GitLabIssueUpdateInput,
GitLabIssueUpdateResult,
GitLabMergeRequest,
GitLabMergeRequestCommitsResult,
GitLabMergeRequestContextResult,
@@ -16,6 +20,10 @@ import type {
GitLabMergeRequestTimelineResult,
GitLabMergeRequestUpdateInput,
GitLabMergeRequestUpdateResult,
GitLabMrApproveInput,
GitLabMrApproveResult,
GitLabMrNoteInput,
GitLabMrNoteResult,
GitLabUserSummary,
} from '@openchamber/ui/lib/api/types';
import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch';
@@ -266,6 +274,58 @@ export const createWebGitLabAPI = ({ urls }: WebGitLabAPIOptions): GitLabAPI =>
};
},
async issueComment(input: GitLabIssueCommentInput): Promise<GitLabIssueCommentResult> {
const response = await runtimeFetch('/api/gitlab/issues/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitLabIssueCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitLab issue comment');
}
return body;
},
async issueUpdate(input: GitLabIssueUpdateInput): Promise<GitLabIssueUpdateResult> {
const response = await runtimeFetch('/api/gitlab/issues/update', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitLabIssueUpdateResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to update GitLab issue');
}
return body;
},
async mrComment(input: GitLabMrNoteInput): Promise<GitLabMrNoteResult> {
const response = await runtimeFetch('/api/gitlab/mrs/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitLabMrNoteResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitLab merge request comment');
}
return body;
},
async mrApprove(input: GitLabMrApproveInput): Promise<GitLabMrApproveResult> {
const response = await runtimeFetch('/api/gitlab/mrs/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitLabMrApproveResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to approve GitLab merge request');
}
return body;
},
async repoBranches(namespace: string, project: string): Promise<GitLabBranchesResult> {
const response = await runtimeFetch(
`/api/gitlab/repo/branches?namespace=${encodeURIComponent(namespace)}&project=${encodeURIComponent(project)}`,