Skip to content
This repository was archived by the owner on Oct 19, 2026. It is now read-only.

Commit cce2838

Browse files
authored
feat(CLA): add CLA signing functionality (#2020)
* feat(CLA): add CLA signing functionality - 在设置页面集成 CLA 状态展示入口 - 在 MergeBox 中集成 CLA 签署检查逻辑 - MergeSection 新增 claCheck 参数,未签署时显示"Sign CLA"按钮 - 修复NewCodeView下创建文件路径解析问题 * feat(CLA): add CLA signing functionality * fix:cla Edit AdminCheck
1 parent f48ecd0 commit cce2838

16 files changed

Lines changed: 711 additions & 38 deletions

File tree

‎moon/apps/web/components/ClBox/MergeBox.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FeedMergedIcon } from '@primer/octicons-react'
33
import { useQueryClient } from '@tanstack/react-query'
44
import { useRouter } from 'next/router'
55

6+
import { CheckType, ConditionResult } from '@gitmono/types'
67
import { LoadingSpinner } from '@gitmono/ui'
78

89
import { useGetMergeBox } from '@/components/ClBox/hooks/useGetMergeBox'
@@ -63,6 +64,9 @@ export const MergeBox = React.memo<{ prId: string; status?: string }>(({ prId, s
6364

6465
const additionalChecks = mergeBoxData?.merge_requirements?.conditions ?? []
6566

67+
const claCondition = additionalChecks.find((c) => c.type === CheckType.ClaSign)
68+
const claCheck = claCondition ? claCondition.result === ConditionResult.PASSED : true
69+
6670
return (
6771
<div className='flex'>
6872
<FeedMergedIcon size={24} className='text-tertiary ml-1' />
@@ -88,6 +92,7 @@ export const MergeBox = React.memo<{ prId: string; status?: string }>(({ prId, s
8892
onApprove={handleApprove}
8993
clStatus={status}
9094
clLink={id}
95+
claCheck={claCheck}
9196
/>
9297
</div>
9398
)}

‎moon/apps/web/components/ClBox/MergeSection.tsx‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,21 @@ interface MergeSectionProps {
1717
onApprove: () => void
1818
clLink: string
1919
clStatus?: string
20+
claCheck?: boolean
2021
}
2122

2223
const STATUS_POLL_INTERVAL_MS = 3000
2324

2425
export const MergeSection = React.memo<MergeSectionProps>(
25-
({ isAllReviewerApproved, hasCheckFailures: _hasCheckFailures, isNowUserApprove, onApprove, clLink, clStatus }) => {
26+
({
27+
isAllReviewerApproved,
28+
hasCheckFailures: _hasCheckFailures,
29+
isNowUserApprove,
30+
onApprove,
31+
clLink,
32+
clStatus,
33+
claCheck = true
34+
}) => {
2635
const router = useRouter()
2736
const { scope } = useScope()
2837
const queryClient = useQueryClient()
@@ -69,7 +78,7 @@ export const MergeSection = React.memo<MergeSectionProps>(
6978

7079
let statusNode: React.ReactNode
7180

72-
const isMergeable = isAllReviewerApproved
81+
const isMergeable = isAllReviewerApproved && claCheck !== false
7382
const isDraft = clStatus?.toLowerCase() === 'draft'
7483

7584
if (isDraft) {
@@ -140,6 +149,15 @@ export const MergeSection = React.memo<MergeSectionProps>(
140149
</button>
141150
)}
142151

152+
{claCheck === false && (
153+
<button
154+
onClick={() => router.push(`/me/settings/cla/sign`)}
155+
className='w-full rounded-md bg-blue-600 px-4 py-2 font-bold text-white duration-500 hover:bg-blue-800'
156+
>
157+
Sign CLA
158+
</button>
159+
)}
160+
143161
{isDraft ? (
144162
<Tooltip
145163
label={

‎moon/apps/web/components/ClBox/hooks/useGetMergeBox.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const useGetMergeBox = (link: string) => {
1919
const response = await fetchMergeBox.request(link)
2020

2121
return response.data ?? {}
22-
}
22+
},
23+
refetchOnMount: 'always'
2324
})
2425

2526
useEffect(() => {

‎moon/apps/web/components/ClBox/types/mergeCheck.config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ export const ADDITIONAL_CHECK_LABELS: Record<AdditionalCheckType, string> = {
3030
[CheckType.CiStatus]: 'CI Status',
3131
[CheckType.ClSync]: 'CL Sync Status',
3232
[CheckType.MergeConflict]: 'Merge Conflict Detection',
33-
[CheckType.CodeReview]: 'Code Review Status'
33+
[CheckType.CodeReview]: 'Code Review Status',
34+
[CheckType.ClaSign]: 'CLA Status'
3435
}

‎moon/apps/web/components/CodeView/NewCodeView/NewCodeView.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,18 @@ const NewCodeView = ({ currentPath = '', onClose, defaultType = 'file' }: NewCod
3030
const { data: currentUser } = useGetCurrentUser()
3131

3232
const handleSubmit = () => {
33+
const fullPath = path
34+
const lastSlashIndex = fullPath.lastIndexOf('/')
35+
36+
const parentPath = lastSlashIndex > 0 ? fullPath.substring(0, lastSlashIndex) : lastSlashIndex === 0 ? '/' : ''
37+
38+
// Use explicit name if provided, otherwise extract from path
39+
const fileName = name || (lastSlashIndex >= 0 ? fullPath.substring(lastSlashIndex + 1) : fullPath)
40+
3341
createEntryHook.mutate(
3442
{
35-
name: path,
36-
path: '/',
43+
name: fileName,
44+
path: parentPath,
3745
is_directory: fileType === 'folder',
3846
content: fileType === 'file' ? content : '',
3947
author_email: currentUser?.email,

‎moon/apps/web/components/CodeView/NewCodeView/PathInput.tsx‎

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import React, { KeyboardEvent, useEffect, useState } from 'react'
3+
import React, { KeyboardEvent, useEffect, useRef, useState } from 'react'
44
import { useParams } from 'next/navigation'
55

66
interface PathInputProps {
@@ -13,7 +13,8 @@ export default function PathInput({ pathState, nameState }: PathInputProps) {
1313
const [, setName] = nameState
1414
const params = useParams()
1515

16-
const [basePath, setBasePath] = useState<string[]>([])
16+
// Store fixed base path in ref (no re-renders needed)
17+
const fixedBasePathRef = useRef<string[]>([])
1718

1819
const [userSegments, setUserSegments] = useState<string[]>([])
1920
const [current, setCurrent] = useState<string>('')
@@ -27,7 +28,7 @@ export default function PathInput({ pathState, nameState }: PathInputProps) {
2728
}
2829

2930
useEffect(() => {
30-
const raw = (params as any)?.path
31+
const raw = params?.path
3132
let newBase: string[] = []
3233

3334
if (Array.isArray(raw)) {
@@ -37,10 +38,10 @@ export default function PathInput({ pathState, nameState }: PathInputProps) {
3738
}
3839
newBase.unshift('')
3940

40-
setBasePath(newBase)
41+
fixedBasePathRef.current = newBase
4142
updatePath(newBase, userSegments, current)
4243
// eslint-disable-next-line react-hooks/exhaustive-deps
43-
}, [JSON.stringify((params as any)?.path)])
44+
}, [params?.path])
4445

4546
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
4647
const value = e.target.value
@@ -55,14 +56,14 @@ export default function PathInput({ pathState, nameState }: PathInputProps) {
5556

5657
setUserSegments(nextUser)
5758
setCurrent(last)
58-
updatePath(basePath, nextUser, last)
59+
updatePath(fixedBasePathRef.current, nextUser, last)
5960
} else {
6061
setCurrent('')
61-
updatePath(basePath, userSegments, '')
62+
updatePath(fixedBasePathRef.current, userSegments, '')
6263
}
6364
} else {
6465
setCurrent(value)
65-
updatePath(basePath, userSegments, value)
66+
updatePath(fixedBasePathRef.current, userSegments, value)
6667
}
6768
}
6869

@@ -73,26 +74,22 @@ export default function PathInput({ pathState, nameState }: PathInputProps) {
7374
const nextUser = userSegments.slice(0, -1)
7475

7576
setUserSegments(nextUser)
76-
updatePath(basePath, nextUser, '')
77-
} else if (basePath.length > 1) {
78-
const nextBase = basePath.slice(0, -1)
79-
80-
setBasePath(nextBase)
81-
updatePath(nextBase, userSegments, '')
77+
updatePath(fixedBasePathRef.current, nextUser, '')
8278
}
79+
// basePath is now protected - cannot be deleted
8380
}
8481
}
8582

8683
return (
8784
<div className='text-primary flex max-w-[900px] flex-wrap items-center gap-x-1 gap-y-2'>
88-
{[...basePath, ...userSegments].map((seg, i, arr) => (
85+
{[...fixedBasePathRef.current, ...userSegments].map((seg, i, arr) => (
8986
// eslint-disable-next-line react/no-array-index-key
9087
<React.Fragment key={i}>
9188
<span className='text-accent font-medium'>{seg}</span>
9289
{i < arr.length - 1 && <span className='text-secondary'>/</span>}
9390
</React.Fragment>
9491
))}
95-
{[...basePath, ...userSegments].length > 0 && <span className='text-secondary'>/</span>}
92+
{[...fixedBasePathRef.current, ...userSegments].length > 0 && <span className='text-secondary'>/</span>}
9693

9794
<input
9895
type='text'
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useRouter } from 'next/navigation'
2+
3+
import { Button } from '@gitmono/ui'
4+
5+
import * as SettingsSection from '@/components/SettingsSection'
6+
import { useClaStatus } from '@/hooks/Cla/useClaStatus'
7+
8+
const ClaStatusSection = () => {
9+
const { data: claStatusData, isLoading } = useClaStatus()
10+
const signed = claStatusData?.data?.cla_signed ?? false
11+
const signedAt = claStatusData?.data?.cla_signed_at
12+
const router = useRouter()
13+
14+
return (
15+
<SettingsSection.Section>
16+
<SettingsSection.Header>
17+
<SettingsSection.Title>Contributor License Agreement (CLA)</SettingsSection.Title>
18+
</SettingsSection.Header>
19+
20+
<SettingsSection.Description>
21+
Your CLA signing status. A signed CLA is required to contribute to projects.
22+
</SettingsSection.Description>
23+
24+
<SettingsSection.Separator />
25+
26+
<SettingsSection.Body>
27+
<div className='flex items-center justify-between'>
28+
<div className='flex items-center gap-3'>
29+
{isLoading ? (
30+
<span className='text-secondary text-sm'>Loading status...</span>
31+
) : signed ? (
32+
<>
33+
<span className='inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-sm font-medium text-green-800 dark:bg-green-900/30 dark:text-green-400'>
34+
<span className='h-2 w-2 rounded-full bg-green-500' />
35+
Signed
36+
</span>
37+
{signedAt && (
38+
<span className='text-secondary text-xs'>
39+
{new Date(signedAt * 1000).toLocaleDateString(undefined, {
40+
year: 'numeric',
41+
month: 'long',
42+
day: 'numeric'
43+
})}
44+
</span>
45+
)}
46+
</>
47+
) : (
48+
<span className='inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-3 py-1 text-sm font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400'>
49+
<span className='h-2 w-2 rounded-full bg-amber-500' />
50+
Not Signed
51+
</span>
52+
)}
53+
</div>
54+
55+
{!signed && !isLoading && <Button onClick={() => router.push('/me/settings/cla/sign')}>Sign CLA</Button>}
56+
</div>
57+
</SettingsSection.Body>
58+
</SettingsSection.Section>
59+
)
60+
}
61+
62+
export default ClaStatusSection

‎moon/apps/web/components/Sidebar/SidebarMenu/MenuPicker.tsx‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ import toast from 'react-hot-toast'
66
import { Vec } from '@gitmono/types/generated'
77
import * as Dialog from '@gitmono/ui/src/Dialog'
88

9-
import { useAdminCheck } from '@/hooks/admin/useAdminCheck'
109
import { useDeleteSidebarById } from '@/hooks/Sidebar/useDeleteSidebarById'
1110
import { useGetSidebarList } from '@/hooks/Sidebar/useGetSidebarList'
1211
import { usePostSidebarSync } from '@/hooks/Sidebar/usePostSidebarSync'
1312

1413
export const MenuPicker = () => {
15-
const { data: adminCheck } = useAdminCheck()
1614
const { data, refetch, isFetching } = useGetSidebarList()
1715
const queryClient = useQueryClient()
1816
const syncSidebar = usePostSidebarSync()
@@ -154,10 +152,6 @@ export const MenuPicker = () => {
154152
}))
155153
}
156154

157-
if (!adminCheck?.data?.is_admin || !data?.data) {
158-
return null
159-
}
160-
161155
return (
162156
<SettingsSection.Section>
163157
<SettingsSection.Header>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
3+
import { legacyApiClient } from '@/utils/queryClient'
4+
5+
export function useClaStatus() {
6+
const api = legacyApiClient.v1.getApiUserClaStatus()
7+
8+
return useQuery({
9+
queryKey: api.requestKey(),
10+
queryFn: () => api.request()
11+
})
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
3+
import { legacyApiClient } from '@/utils/queryClient'
4+
5+
export function useGetClaContent() {
6+
const api = legacyApiClient.v1.getApiUserClaContent()
7+
8+
return useQuery({
9+
queryKey: api.requestKey(),
10+
queryFn: () => api.request()
11+
})
12+
}

0 commit comments

Comments
 (0)