import { zodResolver } from '@hookform/resolvers/zod' import { useDebounce } from '@uidotdev/usehooks' import { useParams } from 'common' import { Check, Github, Loader2 } from 'lucide-react' import Image from 'next/image' import { useRouter } from 'next/router' import { useCallback, useEffect, useState } from 'react' import { useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Button, cn, Dialog, DialogContent, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, Form, FormControl, FormField, Input, Label, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import * as z from 'zod' import { AlertError } from '@/components/ui/AlertError' import { InlineLink } from '@/components/ui/InlineLink' import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation' import { Branch, useBranchesQuery } from '@/data/branches/branches-query' import { useCheckGithubBranchValidity } from '@/data/integrations/github-branch-check-query' import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { BASE_PATH } from '@/lib/constants' interface EditBranchModalProps { branch?: Branch visible: boolean onClose: () => void } export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalProps) => { const { ref } = useParams() const router = useRouter() const { data: projectDetails } = useSelectedProjectQuery() const { data: selectedOrg } = useSelectedOrganizationQuery() const [isGitBranchValid, setIsGitBranchValid] = useState(true) const isBranch = projectDetails?.parent_project_ref !== undefined const projectRef = projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined const { data: connections, error: connectionsError, isPending: isLoadingConnections, isSuccess: isSuccessConnections, isError: isErrorConnections, } = useGitHubConnectionsQuery({ organizationId: selectedOrg?.id, }) const { data: branches } = useBranchesQuery({ projectRef }) const { mutate: checkGithubBranchValidity, isPending: isChecking } = useCheckGithubBranchValidity( { onError: () => {} } ) const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({ onSuccess: (data) => { toast.success(`Successfully updated branch "${data.name}"`) onClose() }, onError: (error) => { toast.error(`Failed to update branch: ${error.message}`) }, }) const githubConnection = connections?.find((connection) => connection.project.ref === projectRef) const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? [] const formId = 'edit-branch-form' const FormSchema = z.object({ branchName: z .string() .min(1, 'Branch name cannot be empty') .refine( (val) => /^[a-zA-Z0-9\-_]+$/.test(val), 'Branch name can only contain alphanumeric characters, hyphens, and underscores.' ) .refine( (val) => // Allow the current branch name during edit val === branch?.name || (branches ?? []).every((b) => b.name !== val), 'A branch with this name already exists' ), gitBranchName: z.string().optional(), }) const form = useForm>({ mode: 'onChange', reValidateMode: 'onChange', resolver: zodResolver(FormSchema as any), defaultValues: { branchName: '', gitBranchName: '' }, }) const gitBranchName = useWatch({ control: form.control, name: 'gitBranchName' }) const debouncedGitBranchName = useDebounce(gitBranchName, 500) const isFormValid = form.formState.isValid && (!gitBranchName || isGitBranchValid) const canSubmit = isFormValid && !isUpdating && !isChecking const openLinkerPanel = () => { onClose() if (projectRef) { router.push(`/project/${projectRef}/settings/integrations`) } } const onSubmit = (data: z.infer) => { if (!projectRef) return console.error('Project ref is required') if (!branch?.project_ref) return console.error('Branch ref is required') const payload: { branchRef: string projectRef: string branchName: string gitBranch?: string } = { branchRef: branch.project_ref, projectRef, branchName: data.branchName, } // Only add gitBranch to the payload if it is present and valid // If gitBranchName is empty or invalid, gitBranch remains undefined in the payload if (data.gitBranchName && isGitBranchValid) { payload.gitBranch = data.gitBranchName } updateBranch(payload) } const validateGitBranchName = useCallback( (branchName: string) => { if (!githubConnection) return console.error( '[EditBranchModal > validateGitBranchName] GitHub Connection is missing' ) const repositoryId = githubConnection.repository.id const requested = branchName checkGithubBranchValidity( { repositoryId, branchName }, { onSuccess: () => { if (form.getValues('gitBranchName') !== requested) return // Check if another branch is already linked to this git branch const existingBranch = (branches ?? []).find( (b) => b.git_branch === branchName && b.id !== branch?.id ) if (existingBranch) { setIsGitBranchValid(false) form.setError('gitBranchName', { message: `Branch "${existingBranch.name}" is already linked to git branch "${branchName}"`, }) return } setIsGitBranchValid(true) form.clearErrors('gitBranchName') }, onError: (error) => { if (form.getValues('gitBranchName') !== requested) return setIsGitBranchValid(false) form.setError('gitBranchName', { ...error, message: `Unable to find branch "${branchName}" in ${repoOwner}/${repoName}`, }) }, } ) }, [githubConnection, form, checkGithubBranchValidity, repoOwner, repoName, branches, branch] ) // Pre-fill form when the modal becomes visible and branch data is available useEffect(() => { if (visible && branch) { form.reset({ branchName: branch.name ?? '', gitBranchName: branch.git_branch ?? '', }) } }, [branch, visible, form]) useEffect(() => { if (!githubConnection || !debouncedGitBranchName) { return form.clearErrors('gitBranchName') } form.clearErrors('gitBranchName') validateGitBranchName(debouncedGitBranchName) }, [debouncedGitBranchName, validateGitBranchName, form, githubConnection]) return ( !open && onClose()}> Edit branch "{branch?.name}"
( )} /> {isLoadingConnections && (
)} {isErrorConnections && ( )} {isSuccessConnections && (githubConnection ? ( ( Sync with Git branch
{`GitHub {repoOwner}/{repoName}
} labelOptional="Optional" description="Automatically deploy changes on every commit" >
{ field.onChange(e) setIsGitBranchValid(false) }} />
{field.value ? ( isChecking ? ( ) : isGitBranchValid ? ( ) : null ) : null}
)} /> ) : (

Optionally connect to a GitHub repository to manage migrations automatically for this branch.

))}
) }