MergeRequest.tsx249 lines · main
1// @ts-nocheck
2import { LOCAL_STORAGE_KEYS, useParams } from 'common'
3import dayjs from 'dayjs'
4import { GitBranchIcon, GitMerge, MoreVertical, Shield } from 'lucide-react'
5import Link from 'next/link'
6import { useRouter } from 'next/router'
7import { useMemo } from 'react'
8import { toast } from 'sonner'
9import {
10 Button,
11 DropdownMenu,
12 DropdownMenuContent,
13 DropdownMenuItem,
14 DropdownMenuTrigger,
15} from 'ui'
16import { TimestampInfo } from 'ui-patterns'
17
18import { useIsPgDeltaDiffEnabled } from '../App/FeaturePreview/FeaturePreviewContext'
19import { ReviewWithAI } from '../BranchManagement/ReviewWithAI'
20import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
21import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge'
22import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation'
23import { useBranchesQuery } from '@/data/branches/branches-query'
24import { useProjectGitHubConnectionQuery } from '@/data/integrations/github-connections-query'
25import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
26import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
27import { useBranchMergeDiff } from '@/hooks/branches/useBranchMergeDiff'
28import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
29import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
30
31export const MergeTitle = () => {
32 const { ref } = useParams()
33 const { data: project } = useSelectedProjectQuery()
34 const pgDeltaDiffEnabled = useIsPgDeltaDiffEnabled()
35
36 const parentProjectRef = project?.parent_project_ref
37
38 const { data: branches } = useBranchesQuery(
39 { projectRef: parentProjectRef },
40 {
41 refetchOnMount: 'always',
42 refetchOnWindowFocus: true,
43 staleTime: 0,
44 }
45 )
46 const currentBranch = branches?.find((branch) => branch.project_ref === ref)
47 const mainBranch = branches?.find((branch) => branch.is_default)
48
49 return (
50 <div className="flex items-center gap-x-4">
51 <div className="flex items-center gap-x-2">
52 <span>Merge</span>
53
54 <code className="flex items-center text-code-inline gap-x-1.5 px-2 py-1 border border-border">
55 <GitBranchIcon strokeWidth={1.5} size={14} className="text-foreground-lighter" />
56 {currentBranch?.name}
57 </code>
58
59 <span>into</span>
60
61 <Link href={`/project/${mainBranch?.project_ref}`} className="font-mono inline-flex gap-4">
62 <code className="flex items-center text-code-inline font-mono gap-x-1.5 px-2 py-1 border border-border">
63 <Shield strokeWidth={1.5} size={14} className="text-warning" />
64 {mainBranch?.name || 'main'}
65 </code>
66 </Link>
67 </div>
68
69 {pgDeltaDiffEnabled && (
70 <FeaturePreviewBadge featureKey={LOCAL_STORAGE_KEYS.UI_PREVIEW_PG_DELTA_DIFF} />
71 )}
72 </div>
73 )
74}
75
76export const MergeSubtitle = () => {
77 const { ref } = useParams()
78 const { data: project } = useSelectedProjectQuery()
79 const parentProjectRef = project?.parent_project_ref
80
81 const { data: branches } = useBranchesQuery(
82 { projectRef: parentProjectRef },
83 {
84 refetchOnMount: 'always',
85 refetchOnWindowFocus: true,
86 staleTime: 0,
87 }
88 )
89 const currentBranch = branches?.find((branch) => branch.project_ref === ref)
90
91 const subtitle = useMemo(() => {
92 if (!currentBranch?.created_at) return 'Branch information unavailable'
93
94 if (!currentBranch?.review_requested_at) {
95 return 'Not ready for review'
96 }
97
98 const reviewRequestedTime = dayjs(currentBranch.review_requested_at).fromNow()
99 return (
100 <>
101 Request opened{' '}
102 <TimestampInfo
103 className="text-sm"
104 utcTimestamp={currentBranch.review_requested_at}
105 label={reviewRequestedTime}
106 />
107 </>
108 )
109 }, [currentBranch?.created_at, currentBranch?.review_requested_at])
110
111 return <p className="text-foreground-lighter text-sm">{subtitle}</p>
112}
113
114export const MergeActions = ({
115 isWorkflowRunning,
116 isSubmitting,
117 onSelectMerge,
118}: {
119 isWorkflowRunning: boolean
120 isSubmitting: boolean
121 onSelectMerge: () => void
122}) => {
123 const router = useRouter()
124 const { ref } = useParams()
125 const { data: project } = useSelectedProjectQuery()
126 const { data: selectedOrg } = useSelectedOrganizationQuery()
127
128 const { mutate: sendEvent } = useSendEventMutation()
129 const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({
130 onError: (error) => {
131 toast.error(`Failed to update branch: ${error.message}`)
132 },
133 })
134
135 const parentProjectRef = project?.parent_project_ref
136 const { data: parentProject } = useProjectDetailQuery({ ref: parentProjectRef })
137 const { data: ghConnection } = useProjectGitHubConnectionQuery({ ref: parentProjectRef })
138
139 const { data: branches } = useBranchesQuery(
140 { projectRef: parentProjectRef },
141 {
142 refetchOnMount: 'always',
143 refetchOnWindowFocus: true,
144 staleTime: 0,
145 }
146 )
147 const currentBranch = branches?.find((branch) => branch.project_ref === ref)
148 const mainBranch = branches?.find((branch) => branch.is_default)
149
150 const {
151 diffContent,
152 isBranchOutOfDateOverall,
153 isLoading: isCombinedDiffLoading,
154 hasChanges: combinedHasChanges,
155 } = useBranchMergeDiff({
156 currentBranchRef: ref,
157 parentProjectRef,
158 currentBranchConnectionString: project?.connectionString || undefined,
159 parentBranchConnectionString: parentProject?.connectionString || undefined,
160 currentBranchCreatedAt: currentBranch?.created_at,
161 })
162
163 const isMergeDisabled =
164 !combinedHasChanges ||
165 isCombinedDiffLoading ||
166 isBranchOutOfDateOverall ||
167 isWorkflowRunning ||
168 (!!ghConnection && Boolean(mainBranch?.git_branch))
169
170 return (
171 <div className="flex items-end gap-2">
172 <ReviewWithAI
173 currentBranch={currentBranch}
174 mainBranch={mainBranch}
175 parentProjectRef={parentProjectRef}
176 diffContent={diffContent}
177 disabled={!currentBranch || !mainBranch || isCombinedDiffLoading}
178 />
179 {isMergeDisabled ? (
180 <ButtonTooltip
181 tooltip={{
182 content: {
183 side: 'bottom',
184 text: !combinedHasChanges
185 ? 'No changes to merge'
186 : isWorkflowRunning
187 ? 'Workflow is currently running'
188 : !!ghConnection && Boolean(mainBranch?.git_branch)
189 ? 'Deploy to production from GitHub is enabled'
190 : 'Unable to merge at this time',
191 },
192 }}
193 type="primary"
194 loading={isSubmitting}
195 disabled={isMergeDisabled}
196 onClick={onSelectMerge}
197 icon={<GitMerge size={16} strokeWidth={1.5} className="text-brand" />}
198 >
199 Merge branch
200 </ButtonTooltip>
201 ) : (
202 <Button
203 type="primary"
204 loading={isSubmitting}
205 onClick={onSelectMerge}
206 icon={<GitMerge size={16} strokeWidth={1.5} className="text-brand" />}
207 >
208 Merge branch
209 </Button>
210 )}
211
212 <DropdownMenu>
213 <DropdownMenuTrigger asChild>
214 <Button type="default" loading={isUpdating} className="px-1.5" icon={<MoreVertical />} />
215 </DropdownMenuTrigger>
216 <DropdownMenuContent side="bottom" align="end" className="w-52">
217 <DropdownMenuItem
218 className="gap-x-2"
219 onClick={() => {
220 if (!ref || !parentProjectRef) return
221 updateBranch(
222 {
223 branchRef: ref,
224 projectRef: parentProjectRef,
225 requestReview: false,
226 },
227 {
228 onSuccess: () => {
229 toast.success('Successfully closed merge request')
230 router.push(`/project/${project?.ref}/branches?tab=prs`)
231 sendEvent({
232 action: 'branch_close_merge_request_button_clicked',
233 groups: {
234 project: parentProjectRef ?? 'Unknown',
235 organization: selectedOrg?.slug ?? 'Unknown',
236 },
237 })
238 },
239 }
240 )
241 }}
242 >
243 Close this merge request
244 </DropdownMenuItem>
245 </DropdownMenuContent>
246 </DropdownMenu>
247 </div>
248 )
249}