// @ts-nocheck import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { AnimatePresence, motion } from 'framer-motion' import { Loader2 } from 'lucide-react' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Card, CardContent, CardFooter, cn, Form, FormControl, FormField, Input, Switch, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' import { GitHubRepositoryField, useGitHubRepositoryOptions, } from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField' import { InlineLink } from '@/components/ui/InlineLink' import { UpgradeToPro } from '@/components/ui/UpgradeToPro' import { useBranchCreateMutation } from '@/data/branches/branch-create-mutation' import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation' import { useBranchesQuery } from '@/data/branches/branches-query' import { useCheckGithubBranchValidity } from '@/data/integrations/github-branch-check-query' import { useGitHubConnectionCreateMutation } from '@/data/integrations/github-connection-create-mutation' import { useGitHubConnectionDeleteMutation } from '@/data/integrations/github-connection-delete-mutation' import { useGitHubConnectionUpdateMutation } from '@/data/integrations/github-connection-update-mutation' import type { GitHubConnection } from '@/data/integrations/integrations.types' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' interface GitHubIntegrationConnectionFormProps { connection?: GitHubConnection } export const GitHubIntegrationConnectionForm = ({ connection, }: GitHubIntegrationConnectionFormProps) => { const { data: selectedProject } = useSelectedProjectQuery() const { data: selectedOrganization } = useSelectedOrganizationQuery() const [isConfirmingBranchChange, setIsConfirmingBranchChange] = useState(false) const [isConfirmingRepoChange, setIsConfirmingRepoChange] = useState(false) const isParentProject = !selectedProject?.parent_project_ref const { hasAccess: hasAccessToGitHubIntegration, isLoading: isLoadingEntitlements } = useCheckEntitlements('integrations.github_connections') const { hasAccess: hasAccessToBranching } = useCheckEntitlements('branching_limit') const { can: canUpdateGitHubConnection } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'integrations.github_connections' ) const { can: canCreateGitHubConnection } = useAsyncCheckPermissions( PermissionAction.CREATE, 'integrations.github_connections' ) const { gitHubAuthorization, githubRepos, hasPartialResponseDueToSSO, isLoading: isLoadingRepositoryOptions, refetch: refetchRepositoryOptions, } = useGitHubRepositoryOptions() const { mutate: updateBranch } = useBranchUpdateMutation({ onSuccess: () => { toast.success('Production branch settings successfully updated') }, }) const { mutate: createBranch } = useBranchCreateMutation({ onSuccess: () => { toast.success('Production branch settings successfully updated') }, onError: (error) => { console.error('Failed to enable branching:', error) }, }) const { data: existingBranches } = useBranchesQuery( { projectRef: selectedProject?.ref }, { enabled: !!selectedProject?.ref } ) const { mutateAsync: checkGithubBranchValidity, isPending: isCheckingBranch } = useCheckGithubBranchValidity({ onError: () => {} }) const { mutate: createConnection, isPending: isCreatingConnection } = useGitHubConnectionCreateMutation({ onSuccess: () => { toast.success('GitHub integration successfully updated') }, onError: (error) => { // Don't show error toast when connection already exists - the branch // settings update will still proceed and show its own success toast if (!error.message?.includes('already exists')) { toast.error(`Failed to create GitHub connection: ${error.message}`) } }, }) const { mutateAsync: deleteConnection, isPending: isDeletingConnection } = useGitHubConnectionDeleteMutation({ onSuccess: () => { toast.success('Successfully removed GitHub integration') }, }) const { mutate: updateConnectionSettings, isPending: isUpdatingConnection } = useGitHubConnectionUpdateMutation() const prodBranch = existingBranches?.find((branch) => branch.is_default) // Combined GitHub Settings Form const GitHubSettingsSchema = z .object({ repositoryId: z.string().min(1, 'Please select a repository'), enableProductionSync: z.boolean().default(true), branchName: z.string().default('main'), new_branch_per_pr: z.boolean().default(true), brivenDirectory: z.string().default('.'), brivenChangesOnly: z.boolean().default(true), branchLimit: z.string().default('50'), }) .superRefine(async (val, ctx) => { if (val.enableProductionSync && val.branchName && val.branchName.length > 0) { const repositoryId = val.repositoryId || connection?.repository.id.toString() if (repositoryId) { try { await checkGithubBranchValidity({ repositoryId: Number(repositoryId), branchName: val.branchName, }) } catch { const selectedRepo = githubRepos.find((repo) => repo.id === repositoryId) const repoName = selectedRepo?.name || connection?.repository.name || 'selected repository' ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Branch "${val.branchName}" not found in ${repoName}`, path: ['branchName'], }) } } } }) const githubSettingsForm = useForm>({ resolver: zodResolver(GitHubSettingsSchema as any), mode: 'onSubmit', reValidateMode: 'onBlur', defaultValues: { repositoryId: connection?.repository.id.toString() || '', enableProductionSync: true, branchName: 'main', new_branch_per_pr: true, brivenDirectory: '.', brivenChangesOnly: true, branchLimit: '3', }, }) const enableProductionSync = githubSettingsForm.watch('enableProductionSync') const newBranchPerPr = githubSettingsForm.watch('new_branch_per_pr') const currentRepositoryId = githubSettingsForm.watch('repositoryId') const handleCreateOrUpdateConnection = async (data: z.infer) => { if (!selectedProject?.ref || !selectedOrganization?.id) return try { if (connection) { // Check if repository is being changed if (connection.repository.id.toString() !== data.repositoryId) { setIsConfirmingRepoChange(true) return } // Update existing connection await handleUpdateConnection(data, connection) } else { // Create new connection const selectedRepo = githubRepos.find((repo) => repo.id === data.repositoryId) if (!selectedRepo) { toast.error('Please select a repository') return } await handleCreateConnection(data, selectedRepo) } } catch (error) { console.error('Error managing connection:', error) } } const handleCreateConnection = async ( data: z.infer, selectedRepo: { id: string; installation_id: number } ) => { if (!selectedProject?.ref || !selectedOrganization?.id) return createConnection({ organizationId: selectedOrganization.id, connection: { installation_id: selectedRepo.installation_id, project_ref: selectedProject.ref, repository_id: Number(selectedRepo.id), workdir: data.brivenDirectory, briven_changes_only: data.brivenChangesOnly, branch_limit: Number(data.branchLimit), new_branch_per_pr: data.new_branch_per_pr, }, }) if (!prodBranch) { createBranch({ projectRef: selectedProject.ref, branchName: 'main', gitBranch: data.branchName, is_default: true, }) } else { updateBranch({ branchRef: prodBranch.project_ref, projectRef: selectedProject.ref, gitBranch: data.branchName, }) } } const handleUpdateConnection = async ( data: z.infer, currentConnection: GitHubConnection ) => { if (!selectedProject?.ref || !selectedOrganization?.id) return const originalBranchName = prodBranch?.git_branch if (originalBranchName && data.branchName !== originalBranchName && data.enableProductionSync) { setIsConfirmingBranchChange(true) return } await executeUpdate(data, currentConnection) } const executeUpdate = async ( data: z.infer, currentConnection: GitHubConnection ) => { if (!selectedProject?.ref || !selectedOrganization?.id) return updateConnectionSettings({ connectionId: currentConnection.id, organizationId: selectedOrganization.id, connection: { workdir: data.brivenDirectory, briven_changes_only: data.brivenChangesOnly, branch_limit: Number(data.branchLimit), new_branch_per_pr: data.new_branch_per_pr, }, }) if (prodBranch) { updateBranch({ branchRef: prodBranch.project_ref, projectRef: selectedProject.ref, gitBranch: data.enableProductionSync ? data.branchName : '', branchName: data.branchName || 'main', }) } else { // if for some reason, the project doesn't have a default branch yet, create it. createBranch({ projectRef: selectedProject.ref, gitBranch: data.enableProductionSync ? data.branchName : '', branchName: data.branchName || 'main', is_default: true, }) } setIsConfirmingBranchChange(false) } const onConfirmBranchChange = async () => { if (connection) { await executeUpdate(githubSettingsForm.getValues(), connection) } } const handleRemoveIntegration = async () => { if (!connection || !selectedOrganization?.id) return try { await deleteConnection({ organizationId: selectedOrganization.id, connectionId: connection.id, }) githubSettingsForm.reset({ repositoryId: '', enableProductionSync: true, branchName: 'main', new_branch_per_pr: true, brivenDirectory: '.', brivenChangesOnly: true, branchLimit: '3', }) } catch (error) { console.error('Error removing integration:', error) toast.error('Failed to remove integration') } } const onConfirmRepoChange = async () => { const data = githubSettingsForm.getValues() const selectedRepo = githubRepos.find((repo) => repo.id === data.repositoryId) if (!selectedRepo || !connection || !selectedOrganization?.id) return try { await deleteConnection({ organizationId: selectedOrganization.id, connectionId: connection.id, }) await handleCreateConnection(data, selectedRepo) setIsConfirmingRepoChange(false) } catch (error) { console.error('Error changing repository:', error) toast.error('Failed to change repository') } } useEffect(() => { if (connection) { const hasGitBranch = Boolean(prodBranch?.git_branch?.trim()) githubSettingsForm.reset({ repositoryId: connection.repository.id.toString(), enableProductionSync: hasGitBranch, branchName: prodBranch?.git_branch || 'main', new_branch_per_pr: connection.new_branch_per_pr, brivenDirectory: connection.workdir || '', brivenChangesOnly: connection.briven_changes_only, branchLimit: String(connection.branch_limit), }) } }, [connection, prodBranch, githubSettingsForm]) // Handle clearing branch name when production sync is disabled useEffect(() => { if (!enableProductionSync) { githubSettingsForm.setValue('branchName', '') } else if (enableProductionSync && !githubSettingsForm.getValues().branchName) { githubSettingsForm.setValue('branchName', 'main') } }, [enableProductionSync, githubSettingsForm]) const isLoading = isLoadingEntitlements || isCreatingConnection || isUpdatingConnection || isDeletingConnection || isLoadingRepositoryOptions return ( <>
{ githubSettingsForm.setValue('branchName', repo.default_branch || 'main') }} /> {gitHubAuthorization !== null && !!currentRepositoryId && ( ( Relative path to the directory containing your{' '} briven/{' '} folder.{' '} Learn more } > )} /> {/* Production Branch Sync Section */}
( )} />
(
{isCheckingBranch && ( )}
)} />
{hasAccessToBranching ? ( Branching Compute is not covered by your organization's Spend Cap. Costs should be closely monitored, as they may be incurred.{' '} Learn more ) : ( )} {/* Automatic Branching Section */}
( )} />
( )} /> ( field.onChange(val)} disabled={ !hasAccessToBranching || !newBranchPerPr || !canUpdateGitHubConnection } /> )} />
{connection && ( )}
{githubSettingsForm.formState.isDirty && ( )}
)}
setIsConfirmingBranchChange(false)} onConfirm={onConfirmBranchChange} loading={isUpdatingConnection} >

Open pull requests will only update your Briven project on merge if the git base branch matches this new production git branch.

setIsConfirmingRepoChange(false)} onConfirm={onConfirmRepoChange} loading={isLoading} >

This will disconnect your current repository and create a new connection with the selected repository. All existing Briven branches that are connected to the old repository will no longer be synced.

) }