import { zodResolver } from '@hookform/resolvers/zod' import { Check, Github, Loader2 } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import * as z from 'zod' import { useParams } from 'common' import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import AlertError from 'components/ui/AlertError' 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' import { useRouter } from 'next/router' import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, FormControl_Shadcn_, FormField_Shadcn_, FormMessage_Shadcn_, Form_Shadcn_, Input_Shadcn_, Label_Shadcn_ as Label, cn, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' 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 gitlessBranching = useIsBranching2Enabled() const [isGitBranchValid, setIsGitBranchValid] = useState(false) 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 { mutateAsync: 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() .refine( (val) => gitlessBranching || !githubConnection || (val && val.length > 0), 'Git branch name is required when GitHub is connected' ), }) .superRefine(async (val, ctx) => { if (val.gitBranchName && val.gitBranchName.length > 0 && githubConnection?.repository.id) { try { await checkGithubBranchValidity({ repositoryId: githubConnection.repository.id, branchName: val.gitBranchName, }) setIsGitBranchValid(true) } catch (error) { setIsGitBranchValid(false) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Unable to find branch "${val.gitBranchName}" in ${repoOwner}/${repoName}`, path: ['gitBranchName'], }) } } else { // If git branch is empty or removed, it's valid for gitless branching setIsGitBranchValid( !val.gitBranchName || val.gitBranchName.length === 0 || gitlessBranching ) } }) const form = useForm>({ mode: 'onBlur', reValidateMode: 'onChange', resolver: zodResolver(FormSchema), defaultValues: { branchName: '', gitBranchName: '' }, }) const isFormValid = form.formState.isValid && (!form.getValues('gitBranchName') || isGitBranchValid) const canSubmit = isFormValid && !isUpdating && !isChecking const onSubmit = (data: z.infer) => { if (!projectRef) return console.error('Project ref is required') if (!ref) return console.error('Branch ref is required') const payload: { branchRef: string projectRef: string branchName: string gitBranch?: string } = { branchRef: 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) } // Pre-fill form when the modal becomes visible and branch data is available useEffect(() => { if (visible && branch) { setIsGitBranchValid(!!branch.git_branch || gitlessBranching) // Initial validity based on existing link or gitless branching form.reset({ branchName: branch.name ?? '', gitBranchName: branch.git_branch ?? '', }) } }, [branch, visible, form, gitlessBranching]) // Handle initial state and changes for git branch validity useEffect(() => { setIsGitBranchValid( !form.getValues('gitBranchName') || form.getValues('gitBranchName')?.length === 0 || gitlessBranching ) // Trigger validation if a git branch name exists initially or is entered if (form.getValues('gitBranchName')) { form.trigger('gitBranchName') } }, [ githubConnection?.id, form.getValues('gitBranchName'), form.trigger, visible, branch, gitlessBranching, ]) const openLinkerPanel = () => { onClose() router.push(`/project/${projectRef}/settings/integrations`) } return ( !open && onClose()}> Edit branch "{branch?.name}"
( )} /> {githubConnection && ( ( Sync with Git branch {gitlessBranching ? '(optional)' : ''}
{`GitHub {repoOwner}/{repoName}
} description={ gitlessBranching ? 'Automatically deploy changes on every commit' : 'If linked, migrations from this Git branch will be automatically deployed.' } >
{isChecking && } {field.value && !isChecking && isGitBranchValid && ( )}
)} /> )} {isLoadingConnections && } {isErrorConnections && ( )} {isSuccessConnections && !githubConnection && (
{!gitlessBranching && Required}

{gitlessBranching ? 'Optionally connect to a GitHub repository to manage migrations automatically for this branch.' : 'Keep this preview branch in sync with a chosen GitHub branch'}

)}
) }