From 9fe470d71d02e75cb5fbf8d1213f76567971ab91 Mon Sep 17 00:00:00 2001
From: Yume <2839681263@qq.com>
Date: Tue, 26 Aug 2025 16:09:18 +0800
Subject: [PATCH 1/2] feat(UI): add MergeBox component(only static)
---
.../web/components/MrBox/ChecksSection.tsx | 214 ++++++++++++++++++
moon/apps/web/components/MrBox/MergeBox.tsx | 77 +++++++
.../web/components/MrBox/MergeSection.tsx | 52 +++++
.../web/components/MrBox/ReviewerSection.tsx | 45 ++++
.../MrBox/components/CheckGroup.tsx | 40 ++++
.../MrBox/components/MergeCheckItem.tsx | 26 +++
.../components/MrBox/hooks/useMergeChecks.ts | 58 +++++
.../MrBox/types/mergeCheck.config.ts | 40 ++++
moon/apps/web/pages/[org]/mr/[link]/index.tsx | 46 ++--
9 files changed, 578 insertions(+), 20 deletions(-)
create mode 100644 moon/apps/web/components/MrBox/ChecksSection.tsx
create mode 100644 moon/apps/web/components/MrBox/MergeBox.tsx
create mode 100644 moon/apps/web/components/MrBox/MergeSection.tsx
create mode 100644 moon/apps/web/components/MrBox/ReviewerSection.tsx
create mode 100644 moon/apps/web/components/MrBox/components/CheckGroup.tsx
create mode 100644 moon/apps/web/components/MrBox/components/MergeCheckItem.tsx
create mode 100644 moon/apps/web/components/MrBox/hooks/useMergeChecks.ts
create mode 100644 moon/apps/web/components/MrBox/types/mergeCheck.config.ts
diff --git a/moon/apps/web/components/MrBox/ChecksSection.tsx b/moon/apps/web/components/MrBox/ChecksSection.tsx
new file mode 100644
index 000000000..c60534a27
--- /dev/null
+++ b/moon/apps/web/components/MrBox/ChecksSection.tsx
@@ -0,0 +1,214 @@
+import * as Collapsible from '@radix-ui/react-collapsible';
+import { ArrowDownIcon, ArrowUpIcon, AlertIcon, CheckIcon, ClockIcon, WarningTriangleIcon } from "@gitmono/ui";
+import { useEffect, useMemo, useState } from 'react';
+import { MergeCheckItem } from './components/MergeCheckItem';
+import type { TaskData, AdditionalCheckItem, AdditionalCheckStatus } from './types/mergeCheck.config';
+import { ADDITIONAL_CHECK_LABELS } from './types/mergeCheck.config';
+
+interface ChecksSectionProps {
+ checks: TaskData[];
+ onStatusChange: (hasFailures: boolean) => void;
+ additionalChecks?: AdditionalCheckItem[];
+}
+interface CheckStatusProps {
+ hasFailures: boolean;
+ failureCount: number;
+ inProgressCount: number;
+ successCount: number;
+ open: boolean;
+}
+interface CheckGroupProps {
+ title: string;
+ checks: TaskData[];
+}
+interface CheckListProps {
+ groupedChecks: {
+ failing: TaskData[];
+ pending: TaskData[];
+ success: TaskData[];
+ };
+}
+
+const CheckStatus = ({hasFailures, failureCount, inProgressCount, successCount, open}: CheckStatusProps) => {
+ let statusInfo: string[] = []
+
+ if (failureCount > 0) {
+ statusInfo.push(`${failureCount} failed`)
+ }
+ if (inProgressCount > 0) {
+ statusInfo.push(`${inProgressCount} in progress`)
+ }
+ if (successCount > 0) {
+ statusInfo.push(`${successCount} successful`)
+ }
+ if (statusInfo.length === 0) {
+ statusInfo.push('No checks have run yet')
+ }
+
+ return (
+
+ {hasFailures?
:
}
+
+
+ {hasFailures ? 'Some checks were not successful' : 'All checks have passed'}
+
+
+ {statusInfo.join(', ')}
+
+
+
+
+ )
+}
+
+const CheckGroup = ({ title, checks }: CheckGroupProps) => (
+
+
{title} ({checks.length})
+
+ {checks.map(check => )}
+
+
+);
+
+const CheckList = ({groupedChecks}: CheckListProps) => (
+
+ {groupedChecks.failing.length > 0 && }
+ {groupedChecks.pending.length > 0 && }
+ {groupedChecks.success.length > 0 && }
+
+)
+
+interface AdditionalCheckItemProps {
+ check: AdditionalCheckItem;
+}
+
+const getStatusIcon = (status: AdditionalCheckStatus) => {
+ switch (status) {
+ case 'Success':
+ return ;
+ case 'Failure':
+ return ;
+ case 'Warning':
+ return ;
+ case 'Pending':
+ return ;
+ default:
+ return ;
+ }
+};
+
+const AdditionalCheckItemComponent = ({ check }: AdditionalCheckItemProps) => (
+
+
+ {getStatusIcon(check.status)}
+
+
+
+
+ {ADDITIONAL_CHECK_LABELS[check.type]}
+
+
+ {check.status}
+
+
+ {check.message && (
+
{check.message}
+ )}
+ {check.errors && check.errors.length > 0 && (
+
+ {check.errors.map((error, index) => (
+ // eslint-disable-next-line react/no-array-index-key
+ - {error}
+ ))}
+
+ )}
+
+
+);
+
+interface AdditionalChecksSectionProps {
+ additionalChecks: AdditionalCheckItem[];
+}
+
+const AdditionalChecksSection = ({ additionalChecks }: AdditionalChecksSectionProps) => {
+ if (!additionalChecks || additionalChecks.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ Additional Checks ({additionalChecks.length})
+
+
+ {additionalChecks.map(check => (
+
+ ))}
+
+
+ );
+};
+
+export function ChecksSection({ checks, onStatusChange, additionalChecks }: ChecksSectionProps) {
+ const summary = useMemo(() => {
+ return checks.reduce((acc, check) => {
+ acc[check.status] = (acc[check.status] || 0) + 1;
+ return acc;
+ }, {} as Record);
+ }, [checks]);
+
+ const failureCount = summary.Failure || 0;
+ const inProgressCount = summary.Pending || 0;
+ const successCount = summary.Success || 0;
+ const hasFailures = failureCount > 0;
+
+ useEffect(() => {
+ onStatusChange(hasFailures);
+ }, [hasFailures, onStatusChange]);
+
+ const groupedChecks = useMemo(() => {
+ const failing = checks.filter(c => c.status === 'Failure');
+ const pending = checks.filter(c => c.status === 'Pending');
+ const success = checks.filter(c => c.status === 'Success');
+
+ return { failing, pending, success };
+ }, [checks]);
+
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+
+ {/* CheckStatus 部分 */}
+
+
+
+
+ {/* CheckList 部分 */}
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/moon/apps/web/components/MrBox/MergeBox.tsx b/moon/apps/web/components/MrBox/MergeBox.tsx
new file mode 100644
index 000000000..aa916e220
--- /dev/null
+++ b/moon/apps/web/components/MrBox/MergeBox.tsx
@@ -0,0 +1,77 @@
+import React, { useState, useCallback } from 'react';
+import { useMergeChecks } from './hooks/useMergeChecks';
+import { ReviewerSection } from './ReviewerSection';
+import { ChecksSection } from './ChecksSection';
+import { MergeSection } from './MergeSection';
+import { FeedMergedIcon } from "@primer/octicons-react";
+import { usePostMrMerge } from "@/hooks/usePostMrMerge";
+
+const REQUIRED_REVIEWERS = 2; // 假设需要2个 reviewer
+
+export function MergeBox({ prId }: { prId: string }) {
+ const { checks, refresh } = useMergeChecks(prId);
+ const [isReviewerApproved, setIsReviewerApproved] = useState(false);
+ const [hasCheckFailures, setHasCheckFailures] = useState(true);
+ const { mutate: approveMr, isPending: mrMergeIsPending } = usePostMrMerge(prId)
+
+ // 定义最终的合并处理函数
+ const handleMerge = useCallback(async () => {
+ console.log('Final validation before merge...');
+ // TODO: 再次发送校验请求
+ refresh();
+
+ // 模拟校验结果
+ const stillHasFailures = false;
+
+ if (stillHasFailures) {
+ alert("阻止合并:仍有检查项未通过,请刷新页面查看详情。");
+ } else {
+ console.log('All checks passed. Sending merge request to backend...');
+
+ approveMr(undefined)
+
+ alert("合并请求已发送!");
+ }
+ }, [approveMr, refresh]);
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/moon/apps/web/components/MrBox/MergeSection.tsx b/moon/apps/web/components/MrBox/MergeSection.tsx
new file mode 100644
index 000000000..647200411
--- /dev/null
+++ b/moon/apps/web/components/MrBox/MergeSection.tsx
@@ -0,0 +1,52 @@
+import { AlertIcon, WarningTriangleIcon, CheckCircleIcon, LoadingSpinner } from "@gitmono/ui";
+import React from "react";
+
+interface MergeSectionProps {
+ isReviewerApproved: boolean;
+ hasCheckFailures: boolean;
+ onMerge: () => Promise;
+ isMerging: boolean;
+}
+
+export function MergeSection({ isReviewerApproved, hasCheckFailures, onMerge, isMerging }: MergeSectionProps) {
+ let statusNode: React.ReactNode;
+ const isMergeable = isReviewerApproved && !hasCheckFailures;
+
+ if (!isReviewerApproved) {
+ statusNode = (
+
+
+ Merging is blocked
+
+ );
+ } else if (hasCheckFailures) {
+ statusNode = (
+
+ );
+ } else {
+ statusNode = (
+
+
+ Allow merging
+
+ );
+ }
+
+ return (
+
+ {statusNode}
+
+
+ );
+}
\ No newline at end of file
diff --git a/moon/apps/web/components/MrBox/ReviewerSection.tsx b/moon/apps/web/components/MrBox/ReviewerSection.tsx
new file mode 100644
index 000000000..7b9e8ed79
--- /dev/null
+++ b/moon/apps/web/components/MrBox/ReviewerSection.tsx
@@ -0,0 +1,45 @@
+import { useState, useEffect } from 'react';
+import { CheckCircleIcon, AlertIcon } from "@gitmono/ui";
+
+interface ReviewerSectionProps {
+ required: number;
+ onStatusChange: (isApproved: boolean) => void;
+}
+
+export function ReviewerSection({ required, onStatusChange }: ReviewerSectionProps) {
+ // 实际的 review 人数,暂时用 state 模拟,未来应由 API 获取
+ const [actual, _setActual] = useState(1);
+ const isApproved = actual >= required;
+
+ useEffect(() => {
+ // TODO: 调用 API 获取实际 review 人数并更新 setActual
+ // fetch('/api/.../reviewers').then(res => res.json()).then(data => setActual(data.count));
+ }, []);
+
+ useEffect(() => {
+ onStatusChange(isApproved);
+ }, [isApproved, onStatusChange]);
+
+ if (isApproved) {
+ return (
+
+
+
+ All required reviewers have approved
+
+
+ );
+ }
+
+ return (
+
+
+
+
Review required
+
+ {`At least ${required} reviewer${required > 1 ? 's' : ''} required with write access.`}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/moon/apps/web/components/MrBox/components/CheckGroup.tsx b/moon/apps/web/components/MrBox/components/CheckGroup.tsx
new file mode 100644
index 000000000..9043606dd
--- /dev/null
+++ b/moon/apps/web/components/MrBox/components/CheckGroup.tsx
@@ -0,0 +1,40 @@
+import * as Collapsible from '@radix-ui/react-collapsible';
+import { ArrowDownIcon, ArrowUpIcon } from "@gitmono/ui";
+import { useState } from 'react';
+import type { ReactNode } from 'react';
+import { GroupStatus } from "@/components/MrBox/types/mergeCheck.config";
+
+const groupStatusMap = {
+ Pending: { color: 'border-yellow-400' },
+ Success: { color: 'border-green-400' },
+ Failure: { color: 'border-red-400' },
+};
+
+interface CheckGroupProps {
+ title: string;
+ summary: string;
+ status: GroupStatus;
+ children: ReactNode;
+}
+
+export function CheckGroup({ title, summary, status, children }: CheckGroupProps) {
+ const [isOpen, setIsOpen] = useState(true); // 默认展开
+ const { color } = groupStatusMap[status];
+
+ return (
+
+
+
+
+
{ title }
+ { summary }
+
+ { isOpen? : }
+
+
+ { children }
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/moon/apps/web/components/MrBox/components/MergeCheckItem.tsx b/moon/apps/web/components/MrBox/components/MergeCheckItem.tsx
new file mode 100644
index 000000000..ee78dd8d1
--- /dev/null
+++ b/moon/apps/web/components/MrBox/components/MergeCheckItem.tsx
@@ -0,0 +1,26 @@
+import {CheckCircleIcon, AlertIcon, WarningTriangleIcon} from "@gitmono/ui";
+import {TaskData} from "../types/mergeCheck.config";
+
+const statusMap = {
+ Pending: { Icon: WarningTriangleIcon, className: 'text-yellow-600' },
+ Success: { Icon: CheckCircleIcon, className: 'text-green-600' },
+ Failure: { Icon: AlertIcon, className: 'text-red-600' },
+ Warning: { Icon: WarningTriangleIcon, className: 'text-yellow-600' }, // GitHub 通常用黄点表示 in-progress 或 warning
+};
+
+export function MergeCheckItem({ check }: { check: TaskData }) {
+ const { Icon, className } = statusMap[check.status];
+
+ return (
+
+
+
+ {check.repo_name}
+ {check.arguments && {check.arguments}}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/moon/apps/web/components/MrBox/hooks/useMergeChecks.ts b/moon/apps/web/components/MrBox/hooks/useMergeChecks.ts
new file mode 100644
index 000000000..f710ed21d
--- /dev/null
+++ b/moon/apps/web/components/MrBox/hooks/useMergeChecks.ts
@@ -0,0 +1,58 @@
+import { GetApiTasksData } from "@/components/MrBox/types/mergeCheck.config";
+
+export const useMergeChecks = (_prId: string) => {
+ const checks: GetApiTasksData = [
+ {
+ repo_name: "frontend-app",
+ status: "Success",
+ arguments: "--env=production",
+ build_id: "build-123",
+ end_at: "2025-08-18T10:00:00Z",
+ exit_code: 0,
+ mr: "MR-456",
+ output_file: "output.log",
+ start_at: "2025-08-18T09:50:00Z",
+ target: "main"
+ },
+ {
+ repo_name: "backend-service",
+ status: "Pending",
+ arguments: "--mode=test",
+ build_id: "build-124",
+ end_at: "2025-08-18T10:15:00Z",
+ exit_code: 0,
+ mr: "MR-457",
+ output_file: "test.log",
+ start_at: "2025-08-18T10:00:00Z",
+ target: "develop"
+ },
+ {
+ repo_name: "shared-lib",
+ status: "Failure",
+ arguments: "--check=lint",
+ build_id: "build-125",
+ end_at: "2025-08-18T10:30:00Z",
+ exit_code: 1,
+ mr: "MR-458",
+ output_file: "error.log",
+ start_at: "2025-08-18T10:20:00Z",
+ target: "feature"
+ },
+ {
+ repo_name: "config-repo",
+ status: "Warning",
+ arguments: "--validate",
+ build_id: "build-126",
+ end_at: "2025-08-18T10:45:00Z",
+ exit_code: 0,
+ mr: "MR-459",
+ output_file: "warning.log",
+ start_at: "2025-08-18T10:35:00Z",
+ target: "main"
+ }
+ ];
+ // eslint-disable-next-line no-empty-function
+ const refresh = () => {}
+
+ return { checks, refresh };
+};
\ No newline at end of file
diff --git a/moon/apps/web/components/MrBox/types/mergeCheck.config.ts b/moon/apps/web/components/MrBox/types/mergeCheck.config.ts
new file mode 100644
index 000000000..b529a256a
--- /dev/null
+++ b/moon/apps/web/components/MrBox/types/mergeCheck.config.ts
@@ -0,0 +1,40 @@
+export interface TaskData {
+ "arguments": string,
+ "build_id": string,
+ "end_at": string,
+ "exit_code": number,
+ "mr": string,
+ "output_file": string,
+ "repo_name": string,
+ "start_at": string,
+ "status": "Pending" | "Success" | "Failure" | "Warning",
+ "target": string
+}
+
+export type GetApiTasksData = TaskData[]
+
+export type GroupStatus = 'Success' | 'Failure' | 'Pending';
+
+export type AdditionalCheckStatus = 'Success' | 'Failure' | 'Pending' | 'Warning';
+
+export interface AdditionalCheckItem {
+ type: AdditionalCheckType;
+ status: AdditionalCheckStatus;
+ message?: string;
+ errors?: string[];
+}
+
+export type AdditionalCheckType =
+ | 'GPG_SIGNATURE'
+ | 'BRANCH_PROTECTION'
+ | 'COMMIT_MESSAGE_FORMAT'
+ | 'PR_UPDATE_STATUS'
+ | 'CONFLICT_DETECTION';
+
+export const ADDITIONAL_CHECK_LABELS: Record = {
+ GPG_SIGNATURE: 'GPG 签名验证',
+ BRANCH_PROTECTION: '分支保护规则',
+ COMMIT_MESSAGE_FORMAT: '提交信息规范',
+ PR_UPDATE_STATUS: 'PR 更新状态',
+ CONFLICT_DETECTION: '冲突检测'
+};
\ No newline at end of file
diff --git a/moon/apps/web/pages/[org]/mr/[link]/index.tsx b/moon/apps/web/pages/[org]/mr/[link]/index.tsx
index d909fcfec..8f9ebffa4 100644
--- a/moon/apps/web/pages/[org]/mr/[link]/index.tsx
+++ b/moon/apps/web/pages/[org]/mr/[link]/index.tsx
@@ -42,12 +42,13 @@ import { useGetMrFilesChanged } from '@/hooks/useGetMrFilesChanged'
import { usePostMrClose } from '@/hooks/usePostMrClose'
import { usePostMrComment } from '@/hooks/usePostMrComment'
import { usePostMRLabels } from '@/hooks/usePostMRLabels'
-import { usePostMrMerge } from '@/hooks/usePostMrMerge'
+// import { usePostMrMerge } from '@/hooks/usePostMrMerge'
import { usePostMrReopen } from '@/hooks/usePostMrReopen'
import { useUploadHelpers } from '@/hooks/useUploadHelpers'
import { apiErrorToast } from '@/utils/apiErrorToast'
import { trimHtml } from '@/utils/trimHtml'
import { PageWithLayout } from '@/utils/types'
+import { MergeBox } from "@/components/MrBox/MergeBox";
export interface MRDetail {
status: string
@@ -73,21 +74,21 @@ const MRDetailPage: PageWithLayout = () => {
const [loading, setLoading] = useState(false)
const Checks = dynamic(() => import('@/components/MrView/components/Checks'))
- if (mrDetail && typeof mrDetail.status === 'string') {
+ if (mrDetail) {
mrDetail.status = mrDetail.status.toLowerCase()
}
const { data: MrFilesChangedData, isLoading: fileChgIsLoading } = useGetMrFilesChanged(id)
const { mutate: modifyTitle } = usePostMrTitle()
- const { mutate: approveMr, isPending: mrMergeIsPending } = usePostMrMerge(id)
- const handleMrApprove = () => {
- approveMr(undefined, {
- onSuccess: () => {
- router.push(`/${scope}/mr`)
- }
- })
- }
+ // const { mutate: approveMr, isPending: mrMergeIsPending } = usePostMrMerge(id)
+ // const handleMrApprove = () => {
+ // approveMr(undefined, {
+ // onSuccess: () => {
+ // router.push(`/${scope}/mr`)
+ // }
+ // })
+ // }
const [_, setEditId] = useAtom(editIdAtom)
const [refresh, setRefresh] = useAtom(refreshAtom)
@@ -247,17 +248,22 @@ const MRDetailPage: PageWithLayout = () => {
mrDetail &&
)}
-
+ {/*
*/}
+ {/* {mrDetail && mrDetail.status === 'open' && (*/}
+ {/* */}
+ {/* )}*/}
+ {/*
*/}
+
{mrDetail && mrDetail.status === 'open' && (
-
+
)}
Add a comment
From 14f92e2a4ca76eb0fde976aff673a558cfdaa23d Mon Sep 17 00:00:00 2001
From: Yume <2839681263@qq.com>
Date: Tue, 26 Aug 2025 16:17:26 +0800
Subject: [PATCH 2/2] fix(UI): remove console.log
---
moon/apps/web/components/MrBox/MergeBox.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/moon/apps/web/components/MrBox/MergeBox.tsx b/moon/apps/web/components/MrBox/MergeBox.tsx
index aa916e220..bcb493a73 100644
--- a/moon/apps/web/components/MrBox/MergeBox.tsx
+++ b/moon/apps/web/components/MrBox/MergeBox.tsx
@@ -16,7 +16,7 @@ export function MergeBox({ prId }: { prId: string }) {
// 定义最终的合并处理函数
const handleMerge = useCallback(async () => {
- console.log('Final validation before merge...');
+ // console.log('Final validation before merge...');
// TODO: 再次发送校验请求
refresh();
@@ -26,7 +26,7 @@ export function MergeBox({ prId }: { prId: string }) {
if (stillHasFailures) {
alert("阻止合并:仍有检查项未通过,请刷新页面查看详情。");
} else {
- console.log('All checks passed. Sending merge request to backend...');
+ // console.log('All checks passed. Sending merge request to backend...');
approveMr(undefined)