Skip to content

Commit bbcf18c

Browse files
zyd123-cmdgenedna
andauthored
feat: integrate Draft status API and UI (#1658)
Co-authored-by: Quanyi Ma <eli@patch.sh>
1 parent b93d6a6 commit bbcf18c

13 files changed

Lines changed: 405 additions & 42 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import { useGetCurrentUser } from '@/hooks/useGetCurrentUser'
1414
import { legacyApiClient } from '@/utils/queryClient'
1515

1616
import { ChecksSection } from './ChecksSection'
17+
import { DraftStatusBanner } from './components/DraftStatusBanner'
1718
import { useMergeChecks } from './hooks/useMergeChecks'
1819
import { MergeSection } from './MergeSection'
1920
import { ReviewerSection } from './ReviewerSection'
2021

21-
export const MergeBox = React.memo<{ prId: string }>(({ prId }) => {
22+
export const MergeBox = React.memo<{ prId: string; status?: string }>(({ prId, status }) => {
2223
const { scope } = useScope()
2324
const { checks, refresh } = useMergeChecks(prId)
2425
const [hasCheckFailures, setHasCheckFailures] = useState(true)
@@ -95,6 +96,8 @@ export const MergeBox = React.memo<{ prId: string }>(({ prId }) => {
9596
<div className='ml-3 w-full divide-y rounded-lg border bg-white'>
9697
<ReviewerSection required={required} actual={actual} />
9798
<ChecksSection checks={checks} onStatusChange={setHasCheckFailures} additionalChecks={additionalChecks} />
99+
{status === 'Draft' && <DraftStatusBanner link={id} />}
100+
98101
<MergeSection
99102
isNowUserApprove={isNowUserApprove}
100103
isAllReviewerApproved={isAllReviewerApproved}
@@ -103,6 +106,7 @@ export const MergeBox = React.memo<{ prId: string }>(({ prId }) => {
103106
onApprove={handleApprove}
104107
isMerging={clMergeIsPending}
105108
clLink={clLink}
109+
clStatus={status}
106110
/>
107111
</div>
108112
)}

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

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import { useRouter } from 'next/router'
33

4-
import { CheckCircleIcon, LoadingSpinner, WarningTriangleIcon } from '@gitmono/ui'
4+
import { CheckCircleIcon, LoadingSpinner, Tooltip, WarningTriangleIcon } from '@gitmono/ui'
55

66
import { useScope } from '@/contexts/scope'
77
import { useGetMergeQueueStatus } from '@/hooks/MergeQueue/useGetMergeQueueStatus'
@@ -15,6 +15,7 @@ interface MergeSectionProps {
1515
onApprove: () => void
1616
isMerging: boolean
1717
clLink: string
18+
clStatus?: string
1819
}
1920

2021
export const MergeSection = React.memo<MergeSectionProps>(
@@ -25,7 +26,8 @@ export const MergeSection = React.memo<MergeSectionProps>(
2526
onMerge,
2627
onApprove,
2728
isMerging,
28-
clLink
29+
clLink,
30+
clStatus
2931
}) => {
3032
const router = useRouter()
3133
const { scope } = useScope()
@@ -38,8 +40,16 @@ export const MergeSection = React.memo<MergeSectionProps>(
3840
let statusNode: React.ReactNode
3941

4042
const isMergeable = isAllReviewerApproved
43+
const isDraft = clStatus?.toLowerCase() === 'draft'
4144

42-
if (!isAllReviewerApproved) {
45+
if (isDraft) {
46+
statusNode = (
47+
<div className='flex items-center text-yellow-700'>
48+
<WarningTriangleIcon className='mr-3 h-5 w-5' />
49+
<span className='font-semibold'>CL has not yet prepared for the review</span>
50+
</div>
51+
)
52+
} else if (!isAllReviewerApproved) {
4353
statusNode = (
4454
<div className='flex items-center text-yellow-700'>
4555
<WarningTriangleIcon className='mr-3 h-5 w-5' />
@@ -82,22 +92,43 @@ export const MergeSection = React.memo<MergeSectionProps>(
8292
)}
8393

8494
<div className='ClBox-MergeSection flex items-center justify-center gap-4' style={{ marginTop: '12px' }}>
85-
<button
86-
onClick={onApprove}
87-
disabled={isNowUserApprove === undefined || isNowUserApprove}
88-
className='w-full rounded-md bg-green-600 px-4 py-2 font-bold text-white duration-500 hover:bg-green-800 disabled:cursor-not-allowed disabled:bg-gray-400'
89-
>
90-
Approve
91-
</button>
92-
93-
{!inQueue ? (
95+
{!isDraft && (
9496
<button
95-
onClick={handleAddToQueue}
96-
disabled={isAddingToQueue || !isMergeable}
97-
className='w-full rounded-md bg-purple-600 px-4 py-2 font-bold text-white duration-500 hover:bg-purple-800 disabled:cursor-not-allowed disabled:bg-gray-400'
97+
onClick={onApprove}
98+
disabled={isNowUserApprove === undefined || isNowUserApprove}
99+
className='w-full rounded-md bg-green-600 px-4 py-2 font-bold text-white duration-500 hover:bg-green-800 disabled:cursor-not-allowed disabled:bg-gray-400'
98100
>
99-
{isAddingToQueue ? <LoadingSpinner /> : 'Add to Queue'}
101+
Approve
100102
</button>
103+
)}
104+
105+
{!inQueue ? (
106+
isDraft ? (
107+
<Tooltip
108+
label={
109+
<div className='rounded-md bg-[#25292e] px-3 py-1 text-xs text-white'>
110+
Merging is blocked due to failing merge requirements
111+
</div>
112+
}
113+
side='top'
114+
>
115+
<button
116+
onClick={handleAddToQueue}
117+
disabled
118+
className='w-full rounded-md bg-purple-600 px-4 py-2 font-bold text-white duration-500 hover:bg-purple-800 disabled:cursor-not-allowed disabled:bg-gray-400'
119+
>
120+
Add to Queue
121+
</button>
122+
</Tooltip>
123+
) : (
124+
<button
125+
onClick={handleAddToQueue}
126+
disabled={isAddingToQueue || !isMergeable}
127+
className='w-full rounded-md bg-purple-600 px-4 py-2 font-bold text-white duration-500 hover:bg-purple-800 disabled:cursor-not-allowed disabled:bg-gray-400'
128+
>
129+
{isAddingToQueue ? <LoadingSpinner /> : 'Add to Queue'}
130+
</button>
131+
)
101132
) : (
102133
<button
103134
onClick={onMerge}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react'
2+
import { GitPullRequestDraftIcon } from '@primer/octicons-react'
3+
4+
import { useUpdateClStatus } from '@/components/ClView/hook/useUpdateClStatus'
5+
6+
interface DraftStatusBannerProps {
7+
link: string
8+
}
9+
10+
export const DraftStatusBanner: React.FC<DraftStatusBannerProps> = ({ link }) => {
11+
const { mutate: updateStatus, isPending } = useUpdateClStatus()
12+
13+
const handleReadyForReview = () => {
14+
updateStatus({ link, status: 'Open' })
15+
}
16+
17+
return (
18+
<div className='flex items-center justify-between px-4 py-3'>
19+
<div className='flex items-start gap-3'>
20+
<div className='mt-1'>
21+
<GitPullRequestDraftIcon className='text-[#6e7781]' size={24} />
22+
</div>
23+
<div>
24+
<div className='text-sm font-semibold text-[#24292f]'>This pull request is still a work in progress</div>
25+
<div className='mt-1 text-xs text-[#57606a]'>Draft pull requests cannot be merged.</div>
26+
</div>
27+
</div>
28+
<button
29+
type='button'
30+
onClick={handleReadyForReview}
31+
disabled={isPending}
32+
className='rounded-md border border-[#d0d7de] bg-[#f6f8fa] px-3 py-1 text-sm font-semibold text-[#24292f] hover:bg-[#eef1f4] disabled:cursor-not-allowed disabled:opacity-60'
33+
>
34+
Ready for review
35+
</button>
36+
</div>
37+
)
38+
}

moon/apps/web/components/ClView/ClList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export function IndexTabFilter({
7474
>
7575
Open
7676
</Button>
77+
<Button size='sm' onClick={() => setPart('draft')} variant={part === 'draft' ? 'flat' : 'plain'}>
78+
Draft
79+
</Button>
7780
<Button
7881
size='sm'
7982
onClick={() => setPart('closed')}

moon/apps/web/components/ClView/ConversationTab.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { cn } from '@gitmono/ui/utils'
66

77
import { EMPTY_HTML } from '@/atoms/markdown'
88
import { MergeBox } from '@/components/ClBox/MergeBox'
9+
import { ConvertToDraftDialog } from '@/components/ClView/components/ConvertToDraftDialog'
910
import TimelineItems from '@/components/ClView/TimelineItems'
1011
import { BadgeItem } from '@/components/Issues/IssueNewPage'
1112
import { splitFun } from '@/components/Issues/utils/sideEffect'
@@ -115,7 +116,11 @@ export const ConversationTab = React.memo<ConversationTabProps>(
115116
)
116117
)}
117118
<div style={{ marginTop: '12px' }} className='prose'>
118-
<div className='w-full'>{clDetail && clDetail.status === 'Open' && <MergeBox prId={id} />}</div>
119+
<div className='w-full'>
120+
{clDetail && (clDetail.status === 'Open' || clDetail.status === 'Draft') && (
121+
<MergeBox prId={id} status={clDetail.status} />
122+
)}
123+
</div>
119124
<h2 style={{ marginTop: '15px', marginBottom: '15px' }}>Add a comment</h2>
120125
<input {...dropzone.getInputProps()} />
121126
<div className='rounded-lg border p-6' style={{ marginTop: '15px', marginBottom: '15px' }}>
@@ -184,6 +189,20 @@ export const ConversationTab = React.memo<ConversationTabProps>(
184189
onOpenChange={(open) => review_handleOpenChange(open)}
185190
handleGroup={(selected) => handleReviewers(selected)}
186191
selected={review_fetchSelected}
192+
emptyExtra={
193+
clDetail &&
194+
clDetail.status === 'Open' && (
195+
<div className='mt-2 text-xs text-gray-900'>
196+
<span>Still in progress? </span>
197+
<ConvertToDraftDialog
198+
trigger={
199+
<span className='cursor-pointer text-gray-500 underline hover:text-gray-700'>Convert to draft</span>
200+
}
201+
link={clDetail.link}
202+
/>
203+
</div>
204+
)
205+
}
187206
>
188207
{(el) => {
189208
const names = Array.from(new Set(splitFun(el)))
@@ -217,6 +236,19 @@ export const ConversationTab = React.memo<ConversationTabProps>(
217236
</div>
218237
)
219238
})}
239+
{clDetail && clDetail.status === 'Open' && (
240+
<div className='pointer-events-auto mt-2 px-4 text-xs text-gray-900'>
241+
<span>Still in progress? </span>
242+
<ConvertToDraftDialog
243+
trigger={
244+
<span className='cursor-pointer text-gray-500 underline hover:text-gray-700'>
245+
Convert to draft
246+
</span>
247+
}
248+
link={clDetail.link}
249+
/>
250+
</div>
251+
)}
220252
</div>
221253
)
222254
}}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ConversationItem } from '@gitmono/types/generated'
2+
import { ConditionalWrap } from '@gitmono/ui'
3+
4+
import { useGetOrganizationMember } from '@/hooks/useGetOrganizationMember'
5+
6+
import { MemberHovercard } from '../InlinePost/MemberHovercard'
7+
import { MemberAvatar } from '../MemberAvatar'
8+
import HandleTime from './components/HandleTime'
9+
import { UserLinkByName } from './components/UserLinkByName'
10+
11+
interface EditItemProps {
12+
conv: ConversationItem
13+
}
14+
15+
const EditItem = ({ conv }: EditItemProps) => {
16+
const { data: member } = useGetOrganizationMember({ username: conv.username })
17+
18+
return (
19+
<>
20+
<div className='flex items-center space-x-2'>
21+
<div className='cursor-pointer'>
22+
<ConditionalWrap
23+
condition
24+
wrap={(c) => (
25+
<MemberHovercard username={conv?.username}>
26+
<UserLinkByName username={conv?.username} className='relative'>
27+
{c}
28+
</UserLinkByName>
29+
</MemberHovercard>
30+
)}
31+
>
32+
{member ? <MemberAvatar member={member} size='sm' /> : 'Avatar not found'}
33+
</ConditionalWrap>
34+
</div>
35+
<div>{conv.comment}</div>
36+
<div className='text-sm text-gray-500 hover:text-gray-700'>
37+
<HandleTime created_at={conv.created_at} />
38+
</div>
39+
</div>
40+
</>
41+
)
42+
}
43+
44+
export default EditItem

moon/apps/web/components/ClView/TimelineItems.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
FeedPullRequestClosedIcon,
1010
FeedPullRequestOpenIcon,
1111
FeedTagIcon,
12+
GitPullRequestDraftIcon,
1213
PersonIcon,
1314
RepoPushIcon
1415
} from '@primer/octicons-react'
@@ -24,6 +25,7 @@ import { CommonDetailData } from '@/utils/types'
2425
import { SimpleNoteContentRef } from '../SimpleNoteEditor/SimpleNoteContent'
2526
import AssigneeItem from './AssigneeItem'
2627
import CloseItem from './CloseItem'
28+
import EditItem from './EditItem'
2729
import ForcePushItem from './item/ForcePushItem'
2830
import MergedItem from './MergedItem'
2931
import ReopenItem from './ReopenItem'
@@ -117,6 +119,10 @@ const TimelineItems = React.memo<{
117119
children = <CloseItem conv={conv} />
118120
isOver = true
119121
break
122+
case 'Edit':
123+
icon = <GitPullRequestDraftIcon size={24} className='text-[#6e7781]' />
124+
children = <EditItem conv={conv} />
125+
break
120126
case 'Reopen':
121127
icon = <FeedPullRequestOpenIcon size={24} className='text-green-500' />
122128
children = <ReopenItem conv={conv} />
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useState } from 'react'
2+
3+
import { Dialog } from '@gitmono/ui'
4+
5+
import { useUpdateClStatus } from '@/components/ClView/hook/useUpdateClStatus'
6+
7+
interface ConvertToDraftDialogProps {
8+
trigger: React.ReactNode
9+
link: string
10+
}
11+
12+
export const ConvertToDraftDialog: React.FC<ConvertToDraftDialogProps> = ({ trigger, link }) => {
13+
const [open, setOpen] = useState(false)
14+
const { mutate: updateClStatus, isPending } = useUpdateClStatus()
15+
16+
const handleOpenChange = (nextOpen: boolean) => {
17+
setOpen(nextOpen)
18+
}
19+
20+
const handleConfirm = () => {
21+
updateClStatus(
22+
{ link, status: 'draft' },
23+
{
24+
onSuccess: () => {
25+
setOpen(false)
26+
}
27+
}
28+
)
29+
}
30+
31+
const handleConfirmClick = (event: React.MouseEvent<HTMLButtonElement>) => {
32+
event.stopPropagation()
33+
handleConfirm()
34+
}
35+
36+
const handleTriggerClick = (event: React.MouseEvent) => {
37+
event.stopPropagation()
38+
setOpen(true)
39+
}
40+
41+
return (
42+
<>
43+
<span onClick={handleTriggerClick}>{trigger}</span>
44+
<Dialog.Root open={open} onOpenChange={handleOpenChange} size='lg' align='top'>
45+
<Dialog.Header className='flex items-center rounded-b-none border-b bg-white p-4 text-sm'>
46+
<Dialog.Title className='text-sm font-semibold text-[#22262b]'>
47+
Convert this pull request to draft?
48+
</Dialog.Title>
49+
<Dialog.CloseButton />
50+
</Dialog.Header>
51+
<Dialog.Content className='flex min-h-[70px] justify-center bg-[#fff8c5] px-4 py-6'>
52+
<div className='flex items-center text-xs text-[#22262b]'>
53+
People who are already subscribed will not be unsubscribed.
54+
</div>
55+
</Dialog.Content>
56+
<Dialog.Footer
57+
variant='secondary'
58+
className='flex justify-center rounded-t-none border-t bg-[#f6f8fa] px-4 pb-3 pt-3'
59+
>
60+
<button
61+
type='button'
62+
onClick={handleConfirmClick}
63+
disabled={isPending}
64+
className='w-full rounded-md border border-[#dce2e7] bg-[#f6f8fa] px-4 py-2 text-center text-sm font-semibold text-[#cf222e] transition-colors hover:bg-[#cf222e] hover:text-[#f6f8fa] disabled:cursor-not-allowed disabled:opacity-60'
65+
>
66+
Convert to draft
67+
</button>
68+
</Dialog.Footer>
69+
</Dialog.Root>
70+
</>
71+
)
72+
}

0 commit comments

Comments
 (0)