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
+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)}`,