Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions moon/apps/web/components/ClBox/MergeBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FeedMergedIcon } from '@primer/octicons-react'
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/router'

import { CheckType, ConditionResult } from '@gitmono/types'
import { LoadingSpinner } from '@gitmono/ui'

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

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

const claCondition = additionalChecks.find((c) => c.type === CheckType.ClaSign)
const claCheck = claCondition ? claCondition.result === ConditionResult.PASSED : true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block queue action until CLA check is loaded

This defaults claCheck to true whenever the ClaSign condition is absent, which also happens before merge-box data has finished loading. In that window MergeSection treats the CL as mergeable and enables “Add to Queue”, so unsigned contributors can enqueue a CL before the CLA requirement arrives from the API. This is risky because queue admission currently validates open status but does not enforce merge requirements in add_to_merge_queue (ceres/src/api_service/mono_api_service.rs).

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

正常是需要MergeBox全部校验通过,才显示add按钮,但是因为一些校验没做完整,要测试add,就把校验逻辑注释了,后续统一开启校验逻辑

Comment on lines 65 to +68

return (
<div className='flex'>
<FeedMergedIcon size={24} className='text-tertiary ml-1' />
Expand All @@ -88,6 +92,7 @@ export const MergeBox = React.memo<{ prId: string; status?: string }>(({ prId, s
onApprove={handleApprove}
clStatus={status}
clLink={id}
claCheck={claCheck}
/>
</div>
)}
Expand Down
22 changes: 20 additions & 2 deletions moon/apps/web/components/ClBox/MergeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ interface MergeSectionProps {
onApprove: () => void
clLink: string
clStatus?: string
claCheck?: boolean
}

const STATUS_POLL_INTERVAL_MS = 3000

export const MergeSection = React.memo<MergeSectionProps>(
({ isAllReviewerApproved, hasCheckFailures: _hasCheckFailures, isNowUserApprove, onApprove, clLink, clStatus }) => {
({
isAllReviewerApproved,
hasCheckFailures: _hasCheckFailures,
isNowUserApprove,
onApprove,
clLink,
clStatus,
claCheck = true
}) => {
const router = useRouter()
const { scope } = useScope()
const queryClient = useQueryClient()
Expand Down Expand Up @@ -69,7 +78,7 @@ export const MergeSection = React.memo<MergeSectionProps>(

let statusNode: React.ReactNode

const isMergeable = isAllReviewerApproved
const isMergeable = isAllReviewerApproved && claCheck !== false
const isDraft = clStatus?.toLowerCase() === 'draft'

if (isDraft) {
Expand Down Expand Up @@ -140,6 +149,15 @@ export const MergeSection = React.memo<MergeSectionProps>(
</button>
)}

{claCheck === false && (
<button
onClick={() => router.push(`/me/settings/cla/sign`)}
className='w-full rounded-md bg-blue-600 px-4 py-2 font-bold text-white duration-500 hover:bg-blue-800'
>
Sign CLA
</button>
)}

{isDraft ? (
<Tooltip
label={
Expand Down
3 changes: 2 additions & 1 deletion moon/apps/web/components/ClBox/hooks/useGetMergeBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const useGetMergeBox = (link: string) => {
const response = await fetchMergeBox.request(link)

return response.data ?? {}
}
},
refetchOnMount: 'always'
})

useEffect(() => {
Expand Down
3 changes: 2 additions & 1 deletion moon/apps/web/components/ClBox/types/mergeCheck.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ export const ADDITIONAL_CHECK_LABELS: Record<AdditionalCheckType, string> = {
[CheckType.CiStatus]: 'CI Status',
[CheckType.ClSync]: 'CL Sync Status',
[CheckType.MergeConflict]: 'Merge Conflict Detection',
[CheckType.CodeReview]: 'Code Review Status'
[CheckType.CodeReview]: 'Code Review Status',
[CheckType.ClaSign]: 'CLA Status'
}
12 changes: 10 additions & 2 deletions moon/apps/web/components/CodeView/NewCodeView/NewCodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ const NewCodeView = ({ currentPath = '', onClose, defaultType = 'file' }: NewCod
const { data: currentUser } = useGetCurrentUser()

const handleSubmit = () => {
const fullPath = path
const lastSlashIndex = fullPath.lastIndexOf('/')

const parentPath = lastSlashIndex > 0 ? fullPath.substring(0, lastSlashIndex) : lastSlashIndex === 0 ? '/' : ''

// Use explicit name if provided, otherwise extract from path
const fileName = name || (lastSlashIndex >= 0 ? fullPath.substring(lastSlashIndex + 1) : fullPath)

createEntryHook.mutate(
{
name: path,
path: '/',
name: fileName,
path: parentPath,
is_directory: fileType === 'folder',
content: fileType === 'file' ? content : '',
author_email: currentUser?.email,
Expand Down
29 changes: 13 additions & 16 deletions moon/apps/web/components/CodeView/NewCodeView/PathInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import React, { KeyboardEvent, useEffect, useState } from 'react'
import React, { KeyboardEvent, useEffect, useRef, useState } from 'react'
import { useParams } from 'next/navigation'

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

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

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

useEffect(() => {
const raw = (params as any)?.path
const raw = params?.path
let newBase: string[] = []

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

setBasePath(newBase)
fixedBasePathRef.current = newBase
updatePath(newBase, userSegments, current)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify((params as any)?.path)])
}, [params?.path])
Comment on lines 30 to +44

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

setUserSegments(nextUser)
setCurrent(last)
updatePath(basePath, nextUser, last)
updatePath(fixedBasePathRef.current, nextUser, last)
} else {
setCurrent('')
updatePath(basePath, userSegments, '')
updatePath(fixedBasePathRef.current, userSegments, '')
}
} else {
setCurrent(value)
updatePath(basePath, userSegments, value)
updatePath(fixedBasePathRef.current, userSegments, value)
}
}

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

setUserSegments(nextUser)
updatePath(basePath, nextUser, '')
} else if (basePath.length > 1) {
const nextBase = basePath.slice(0, -1)

setBasePath(nextBase)
updatePath(nextBase, userSegments, '')
updatePath(fixedBasePathRef.current, nextUser, '')
}
// basePath is now protected - cannot be deleted
}
}

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

<input
type='text'
Expand Down
62 changes: 62 additions & 0 deletions moon/apps/web/components/Setting/ClaStatusSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useRouter } from 'next/navigation'

import { Button } from '@gitmono/ui'

import * as SettingsSection from '@/components/SettingsSection'
import { useClaStatus } from '@/hooks/Cla/useClaStatus'

const ClaStatusSection = () => {
const { data: claStatusData, isLoading } = useClaStatus()
const signed = claStatusData?.data?.cla_signed ?? false
const signedAt = claStatusData?.data?.cla_signed_at
const router = useRouter()

return (
<SettingsSection.Section>
<SettingsSection.Header>
<SettingsSection.Title>Contributor License Agreement (CLA)</SettingsSection.Title>
</SettingsSection.Header>

<SettingsSection.Description>
Your CLA signing status. A signed CLA is required to contribute to projects.
</SettingsSection.Description>

<SettingsSection.Separator />

<SettingsSection.Body>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
{isLoading ? (
<span className='text-secondary text-sm'>Loading status...</span>
) : signed ? (
<>
<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'>
<span className='h-2 w-2 rounded-full bg-green-500' />
Signed
</span>
{signedAt && (
<span className='text-secondary text-xs'>
{new Date(signedAt * 1000).toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
)}
</>
) : (
<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'>
<span className='h-2 w-2 rounded-full bg-amber-500' />
Not Signed
</span>
)}
</div>

{!signed && !isLoading && <Button onClick={() => router.push('/me/settings/cla/sign')}>Sign CLA</Button>}
</div>
</SettingsSection.Body>
</SettingsSection.Section>
)
}

export default ClaStatusSection
6 changes: 0 additions & 6 deletions moon/apps/web/components/Sidebar/SidebarMenu/MenuPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import toast from 'react-hot-toast'
import { Vec } from '@gitmono/types/generated'
import * as Dialog from '@gitmono/ui/src/Dialog'

import { useAdminCheck } from '@/hooks/admin/useAdminCheck'
import { useDeleteSidebarById } from '@/hooks/Sidebar/useDeleteSidebarById'
import { useGetSidebarList } from '@/hooks/Sidebar/useGetSidebarList'
import { usePostSidebarSync } from '@/hooks/Sidebar/usePostSidebarSync'

export const MenuPicker = () => {
const { data: adminCheck } = useAdminCheck()
const { data, refetch, isFetching } = useGetSidebarList()
const queryClient = useQueryClient()
const syncSidebar = usePostSidebarSync()
Expand Down Expand Up @@ -154,10 +152,6 @@ export const MenuPicker = () => {
}))
}

if (!adminCheck?.data?.is_admin || !data?.data) {
return null
}

return (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block menu edits until sidebar data has loaded

Rendering the editor unconditionally here allows interaction before useGetSidebarList() has returned real data (hard refresh / slow network), so sortedData is treated as empty and admins can click New + Apply against an incomplete items list. That can overwrite the saved menu with partial data and drop existing entries, and the late fetch will not repopulate once items.length > 0 because initialization only runs for an empty local list.

Useful? React with 👍 / 👎.

<SettingsSection.Section>
<SettingsSection.Header>
Expand Down
12 changes: 12 additions & 0 deletions moon/apps/web/hooks/Cla/useClaStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query'

import { legacyApiClient } from '@/utils/queryClient'

export function useClaStatus() {
const api = legacyApiClient.v1.getApiUserClaStatus()

return useQuery({
queryKey: api.requestKey(),
queryFn: () => api.request()
})
}
12 changes: 12 additions & 0 deletions moon/apps/web/hooks/Cla/useGetClaContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query'

import { legacyApiClient } from '@/utils/queryClient'

export function useGetClaContent() {
const api = legacyApiClient.v1.getApiUserClaContent()

return useQuery({
queryKey: api.requestKey(),
queryFn: () => api.request()
})
}
17 changes: 17 additions & 0 deletions moon/apps/web/hooks/Cla/usePostClaChangeSignStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'

import { legacyApiClient } from '@/utils/queryClient'

export function usePostClaChangeSignStatus() {
const queryClient = useQueryClient()
const api = legacyApiClient.v1.postApiUserClaChangeSignStatus()
const statusApi = legacyApiClient.v1.getApiUserClaStatus()

return useMutation({
mutationKey: api.requestKey(),
mutationFn: () => api.request(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: statusApi.baseKey })
}
})
}
19 changes: 19 additions & 0 deletions moon/apps/web/hooks/Cla/usePostClaContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'

import { UpdateClaContentPayload } from '@gitmono/types'

import { legacyApiClient } from '@/utils/queryClient'

export function usePostClaContent() {
const queryClient = useQueryClient()
const api = legacyApiClient.v1.postApiUserClaContent()
const getApi = legacyApiClient.v1.getApiUserClaContent()

return useMutation({
mutationKey: api.requestKey(),
mutationFn: (data: UpdateClaContentPayload) => api.request(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getApi.baseKey })
}
})
}
Loading
Loading