feat(ui): rich forge entity views — commits, files/diff, timeline, checks, metadata chips

- server: new read routes for PR/MR commits, timeline, reviews, commit-statuses across github/gitlab/gitea; enrich PR/issue summaries with labels/assignees/milestone/commentsCount
- ui: forge facade gains getCommits/getTimeline/getChecks; shared ForgeEntityDetailView + section components; mounted into PR/MR views and issue sections
This commit is contained in:
2026-08-16 16:29:24 +00:00
parent f02c33700b
commit 92f0eced34
44 changed files with 3996 additions and 684 deletions
+34
View File
@@ -6,12 +6,14 @@ import type {
GitLabIssueGetResult,
GitLabIssuesListResult,
GitLabMergeRequest,
GitLabMergeRequestCommitsResult,
GitLabMergeRequestContextResult,
GitLabMergeRequestCreateInput,
GitLabMergeRequestCreateResult,
GitLabMergeRequestMergeInput,
GitLabMergeRequestMergeResult,
GitLabMergeRequestsListResult,
GitLabMergeRequestTimelineResult,
GitLabMergeRequestUpdateInput,
GitLabMergeRequestUpdateResult,
GitLabUserSummary,
@@ -182,6 +184,38 @@ export const createWebGitLabAPI = ({ urls }: WebGitLabAPIOptions): GitLabAPI =>
return payload;
},
async mrCommits(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabMergeRequestCommitsResult> {
const params = new URLSearchParams({ directory, number: String(number) });
if (options?.namespace) {
params.set('namespace', options.namespace);
}
if (options?.project) {
params.set('project', options.project);
}
const response = await runtimeFetch(urls.api('/api/gitlab/mrs/commits', params), { method: 'GET', headers: { Accept: 'application/json' } });
const payload = await jsonOrNull<GitLabMergeRequestCommitsResult & { error?: string }>(response);
if (!response.ok || !payload) {
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge request commits');
}
return payload;
},
async mrTimeline(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabMergeRequestTimelineResult> {
const params = new URLSearchParams({ directory, number: String(number) });
if (options?.namespace) {
params.set('namespace', options.namespace);
}
if (options?.project) {
params.set('project', options.project);
}
const response = await runtimeFetch(urls.api('/api/gitlab/mrs/timeline', params), { method: 'GET', headers: { Accept: 'application/json' } });
const payload = await jsonOrNull<GitLabMergeRequestTimelineResult & { error?: string }>(response);
if (!response.ok || !payload) {
throw new Error(payload?.error || response.statusText || 'Failed to load GitLab merge request timeline');
}
return payload;
},
async mrCreate(input: GitLabMergeRequestCreateInput): Promise<GitLabMergeRequest> {
const response = await runtimeFetch('/api/gitlab/mrs/create', {
method: 'POST',