diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx
index 455aaad2ddd..47e0bfb380b 100644
--- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx
+++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx
@@ -266,7 +266,7 @@ export const CreateBranchModal = () => {
- Automatic branching
+ Git branch
{!gitlessBranching && (
Required
@@ -274,8 +274,7 @@ export const CreateBranchModal = () => {
)}
- Automatically create, sync, and merge branches in Supabase when you make
- changes to your GitHub repository.
+ Keep this Supabase branch in sync with a Git branch
} onClick={openLinkerPanel}>
diff --git a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoLinker.tsx b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoLinker.tsx
index c61c9e34973..574ba32dbaa 100644
--- a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoLinker.tsx
+++ b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoLinker.tsx
@@ -1,68 +1,40 @@
-import { zodResolver } from '@hookform/resolvers/zod'
-import { Check, ChevronDown, DollarSign, Github, Loader2 } from 'lucide-react'
-import { useEffect, useMemo, useState } from 'react'
-import { useForm } from 'react-hook-form'
+import { useMemo } from 'react'
import { toast } from 'sonner'
-import * as z from 'zod'
-import { ForeignProject } from 'components/interfaces/Integrations/VercelGithub/ProjectLinker'
-import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
-import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
-import { useBranchesQuery } from 'data/branches/branches-query'
+import ProjectLinker from 'components/interfaces/Integrations/VercelGithub/ProjectLinker'
+import { Markdown } from 'components/interfaces/Markdown'
import { useGitHubAuthorizationQuery } from 'data/integrations/github-authorization-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 { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
import { useGitHubRepositoriesQuery } from 'data/integrations/github-repositories-query'
+import type { IntegrationConnectionsCreateVariables } from 'data/integrations/integrations.types'
+import { useProjectsQuery } from 'data/projects/projects-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
-import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { openInstallGitHubIntegrationWindow } from 'lib/github'
import { EMPTY_ARR } from 'lib/void'
import { useSidePanelsStateSnapshot } from 'state/side-panels'
-import {
- Button,
- cn,
- Command_Shadcn_,
- CommandEmpty_Shadcn_,
- CommandGroup_Shadcn_,
- CommandInput_Shadcn_,
- CommandItem_Shadcn_,
- CommandList_Shadcn_,
- Form_Shadcn_,
- FormControl_Shadcn_,
- FormField_Shadcn_,
- FormMessage_Shadcn_,
- Input_Shadcn_,
- Popover_Shadcn_,
- PopoverContent_Shadcn_,
- PopoverTrigger_Shadcn_,
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetSection,
- SheetTitle,
- Switch,
-} from 'ui'
-import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
-import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { Button, SidePanel } from 'ui'
+
+const GITHUB_ICON = (
+
+
+
+)
export type SidePanelGitHubRepoLinkerProps = {
projectRef?: string
}
const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProps) => {
- const selectedProject = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
const sidePanelStateSnapshot = useSidePanelsStateSnapshot()
- const [foreignProjectsComboBoxOpen, setForeignProjectsComboboxOpen] = useState(false)
- const [selectedRepoId, setSelectedRepoId] = useState
()
- const [isConfirmingBranchChange, setIsConfirmingBranchChange] = useState(false)
-
const visible = sidePanelStateSnapshot.githubConnectionsOpen
const { data: gitHubAuthorization, isLoading: isLoadingGitHubAuthorization } =
@@ -75,20 +47,22 @@ const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProp
enabled: visible && Boolean(gitHubAuthorization),
})
- const { data: connections } = useGitHubConnectionsQuery(
- { organizationId: selectedOrganization?.id },
- { enabled: !!projectRef && visible }
+ /**
+ * Supabase projects available
+ */
+ const { data: supabaseProjectsData, isLoading: isLoadingSupabaseProjects } = useProjectsQuery({
+ enabled: visible,
+ })
+
+ const supabaseProjects = useMemo(
+ () =>
+ supabaseProjectsData
+ ?.filter((project) => project.organization_id === selectedOrganization?.id)
+ .map((project) => ({ name: project.name, ref: project.ref })) ?? EMPTY_ARR,
+ [selectedOrganization?.id, supabaseProjectsData]
)
- const existingConnection = useMemo(
- () => connections?.find((c) => c.project.ref === projectRef),
- [connections, projectRef]
- )
-
- const { mutate: createBranch } = useBranchCreateMutation()
- const { mutate: updateBranch } = useBranchUpdateMutation()
-
- const githubRepos: ForeignProject[] = useMemo(
+ const githubRepos = useMemo(
() =>
githubReposData?.map((repo: any) => ({
id: repo.id.toString(),
@@ -98,558 +72,108 @@ const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProp
[githubReposData]
)
- const selectedRepo = useMemo(() => {
- return (githubReposData as any[])?.find((r) => r.id.toString() === selectedRepoId)
- }, [githubReposData, selectedRepoId])
-
- const { data: existingBranches } = useBranchesQuery(
- { projectRef },
- { enabled: !!projectRef && visible }
+ const { data: connections } = useGitHubConnectionsQuery(
+ {
+ organizationId: selectedOrganization?.id,
+ },
+ {
+ enabled: visible,
+ }
)
- const isBranchingEnabled = selectedProject?.is_branch_enabled ?? false
-
const { mutate: createConnection, isLoading: isCreatingConnection } =
- useGitHubConnectionCreateMutation({})
+ useGitHubConnectionCreateMutation({
+ onSuccess() {
+ toast.success('Successfully linked project to repository!')
+ sidePanelStateSnapshot.setGithubConnectionsOpen(false)
+ },
+ })
const { mutateAsync: deleteConnection } = useGitHubConnectionDeleteMutation()
- // Form and config state
- const { mutateAsync: checkGithubBranchValidity, isLoading: isCheckingBranch } =
- useCheckGithubBranchValidity({ onError: () => {} })
+ const createGithubConnection = async (variables: IntegrationConnectionsCreateVariables) => {
+ if (!selectedOrganization?.id) {
+ throw new Error('No organization id')
+ }
+ if (!variables.new) {
+ throw new Error('No new connection')
+ }
- const { mutate: updateConnectionSettings, isLoading: isUpdatingConnection } =
- useGitHubConnectionUpdateMutation()
+ const existingConnection = connections?.find(
+ (connection) => connection.project.ref === projectRef
+ )
- const FormSchema = z
- .object({
- repo: z.string().optional(),
- autoBranchingEnabled: z.boolean().default(false),
- branchName: z.string(),
- supabaseDirectory: z.string().default(''),
- supabaseChangesOnly: z.boolean().default(false),
- branchLimit: z.string().default('50'),
- })
- .superRefine(async (val, ctx) => {
- if (val.autoBranchingEnabled) {
- if (!selectedRepo) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'Please select a repository',
- path: ['repo'],
- })
- }
- if (val.branchName.length === 0) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'Branch name cannot be empty',
- path: ['branchName'],
- })
- return
- }
-
- if (selectedRepo?.id && val.branchName && val.branchName.length > 0) {
- try {
- await checkGithubBranchValidity({
- repositoryId: Number(selectedRepo.id),
- branchName: val.branchName,
- })
- } catch (error) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `Branch "${val.branchName}" not found in repository`,
- path: ['branchName'],
- })
- }
- }
- }
- })
-
- const form = useForm>({
- resolver: zodResolver(FormSchema),
- mode: 'onSubmit',
- reValidateMode: 'onBlur',
- defaultValues: {
- autoBranchingEnabled: false,
- branchName: 'main',
- supabaseDirectory: '',
- supabaseChangesOnly: false,
- branchLimit: '50',
- },
- })
-
- const autoBranchingEnabled = form.watch('autoBranchingEnabled')
- const prodBranch = existingBranches?.find((b: any) => b.is_default)
- const isCurrentlyEnabled = Boolean(existingConnection)
-
- const submitButtonText =
- autoBranchingEnabled !== isCurrentlyEnabled
- ? autoBranchingEnabled
- ? 'Enable'
- : 'Disable'
- : 'Save'
-
- // This saves connection settings and creates/updates branches which also enables branching if it's not already enabled
- const executeSave = async (data: z.infer) => {
- if (!projectRef) return console.error('Project ref missing')
- if (!selectedOrganization) return console.error('Organization not selected')
-
- const defaultBranch = data.branchName
-
- if (data.autoBranchingEnabled) {
- if (!selectedRepo) return toast.error('Please select a repository to connect to.')
-
- if (!isBranchingEnabled) {
- // create production branch (enables branching)
- createBranch({
- projectRef,
- branchName: defaultBranch,
- gitBranch: defaultBranch,
- })
- } else {
- if (prodBranch?.id) {
- updateBranch({
- id: prodBranch.id,
- projectRef,
- branchName: defaultBranch,
- gitBranch: defaultBranch,
- })
- }
- }
-
- if (existingConnection) {
- if (existingConnection.repository.id.toString() !== selectedRepo.id.toString()) {
- await deleteConnection({
- organizationId: selectedOrganization.id,
- connectionId: existingConnection.id,
- })
- createConnection({
- organizationId: selectedOrganization.id,
- connection: {
- installation_id: selectedRepo.installation_id!,
- project_ref: projectRef,
- repository_id: Number(selectedRepo.id),
- workdir: data.supabaseDirectory,
- supabase_changes_only: data.supabaseChangesOnly,
- branch_limit: Number(data.branchLimit),
- },
- })
- } else {
- updateConnectionSettings({
- connectionId: existingConnection.id,
- organizationId: selectedOrganization.id,
- workdir: data.supabaseDirectory,
- supabaseChangesOnly: data.supabaseChangesOnly,
- branchLimit: Number(data.branchLimit),
- })
- }
- } else {
- createConnection({
- organizationId: selectedOrganization.id,
- connection: {
- installation_id: selectedRepo.installation_id!,
- project_ref: projectRef,
- repository_id: Number(selectedRepo.id),
- workdir: data.supabaseDirectory,
- supabase_changes_only: data.supabaseChangesOnly,
- branch_limit: Number(data.branchLimit),
- },
- })
- }
- toast.success('Automatic branching enabled')
- } else {
- if (existingConnection) {
+ if (existingConnection) {
+ // remove existing connection so we can recreate it or update it
+ try {
await deleteConnection({
organizationId: selectedOrganization.id,
connectionId: existingConnection.id,
})
- toast.success('Automatic branching disabled')
+ } catch (error) {
+ // ignore the error to let createConnection handle it
}
}
- setIsConfirmingBranchChange(false)
- sidePanelStateSnapshot.setGithubConnectionsOpen(false)
+
+ createConnection({
+ organizationId: selectedOrganization.id,
+ connection: variables.new,
+ })
}
- const handleSave = (data: z.infer) => {
- const originalBranchName = prodBranch?.git_branch
-
- if (
- existingConnection &&
- originalBranchName &&
- data.branchName !== originalBranchName &&
- data.autoBranchingEnabled
- ) {
- setIsConfirmingBranchChange(true)
- } else {
- executeSave(data)
- }
- }
-
- const onConfirmBranchChange = async () => {
- await executeSave(form.getValues())
- }
-
- useEffect(() => {
- if (existingConnection) {
- setSelectedRepoId(existingConnection.repository.id.toString())
- }
- }, [existingConnection])
-
- // Reset form whenever the repo / connection / branches change
- useEffect(() => {
- if (visible) {
- const isAutoBranchingEnabled = Boolean(existingConnection)
-
- if (existingConnection) {
- const defaults = {
- autoBranchingEnabled: isAutoBranchingEnabled,
- repo: existingConnection.repository.name,
- branchName: prodBranch?.git_branch ?? 'main',
- supabaseDirectory:
- (existingConnection as any)?.workdir ??
- (existingConnection as any)?.metadata?.supabaseConfig?.supabaseDirectory ??
- '',
- supabaseChangesOnly:
- (existingConnection as any)?.supabase_changes_only ??
- (existingConnection as any)?.metadata?.supabaseConfig?.supabaseChangesOnly ??
- false,
- branchLimit: String(
- ((existingConnection as any)?.branch_limit as number | undefined) ??
- (existingConnection as any)?.metadata?.supabaseConfig?.branchLimit ??
- 50
- ),
- }
-
- form.reset(defaults)
- } else {
- form.reset({
- autoBranchingEnabled: isAutoBranchingEnabled,
- repo: '',
- branchName: 'main',
- supabaseDirectory: '',
- supabaseChangesOnly: false,
- branchLimit: '50',
- })
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [existingConnection?.id, existingBranches, visible, prodBranch])
-
- useEffect(() => {
- if (selectedRepo && !existingConnection) {
- form.setValue('repo', selectedRepo.name)
- form.setValue('branchName', selectedRepo.default_branch ?? 'main')
- }
- }, [selectedRepo, existingConnection, form])
-
return (
- {
- if (!open) sidePanelStateSnapshot.setGithubConnectionsOpen(false)
- }}
+ sidePanelStateSnapshot.setGithubConnectionsOpen(false)}
>
-
-
-
)
}
diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/AutomaticBranchingRow.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/AutomaticBranchingRow.tsx
deleted file mode 100644
index 4a5ae20f996..00000000000
--- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/AutomaticBranchingRow.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { PermissionAction } from '@supabase/shared-types/out/constants'
-
-import { useParams } from 'common'
-import { useBranchesQuery } from 'data/branches/branches-query'
-import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
-import { useProjectsQuery } from 'data/projects/projects-query'
-import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
-import { useSelectedProject } from 'hooks/misc/useSelectedProject'
-import { useSidePanelsStateSnapshot } from 'state/side-panels'
-import { Badge, Button, Card, CardContent } from 'ui'
-
-export const AutomaticBranchingRow = () => {
- const { ref: projectRef } = useParams()
- const project = useSelectedProject()
- const org = useSelectedOrganization()
- const sidePanelsStateSnapshot = useSidePanelsStateSnapshot()
-
- const isBranch = project?.parent_project_ref !== undefined
-
- // Permissions for reading and creating GitHub connections
- const canReadGitHubConnection = useCheckPermissions(
- PermissionAction.READ,
- 'integrations.github_connections'
- )
- const canCreateGitHubConnection = useCheckPermissions(
- PermissionAction.CREATE,
- 'integrations.github_connections'
- )
-
- const { data: allConnections } = useGitHubConnectionsQuery({ organizationId: org?.id })
-
- // Determine automatic branching status
- const baseProjectRef = isBranch ? project?.parent_project_ref : projectRef
- const { data: branches } = useBranchesQuery(
- { projectRef: baseProjectRef },
- { enabled: Boolean(baseProjectRef) }
- )
-
- const prodBranch = branches?.find((b) => b.is_default) ?? branches?.[0]
-
- const { data: projects } = useProjectsQuery()
- const baseProject = isBranch
- ? projects?.find((p) => p.ref === project?.parent_project_ref)
- : project
-
- // Filter connections for current (base) project
- const connections =
- allConnections?.filter((connection) =>
- isBranch
- ? connection.project.ref === project?.parent_project_ref
- : connection.project.ref === projectRef
- ) ?? []
-
- const autoBranchingEnabled =
- connections.length > 0 &&
- Boolean(baseProject?.is_branch_enabled) &&
- Boolean(prodBranch?.git_branch && prodBranch.git_branch.trim().length > 0)
-
- /*
- If the user does not have permission to read GitHub connections, we simply do not
- render the row. The surrounding page should handle showing a more global
- NoPermission component if needed.
- */
- if (!canReadGitHubConnection) {
- return null
- }
-
- return (
-
-
- {/* Left: label and description */}
-
-
Automatic Branching
-
- Create a Supabase branch for every GitHub pull request and sync them on every commit and
- merge.
-
-
-
- {/* Right: status badge + configure button */}
-
- {autoBranchingEnabled ? (
- Enabled
- ) : (
- Disabled
- )}
-
- sidePanelsStateSnapshot.setGithubConnectionsOpen(true)}
- disabled={isBranch || (!canCreateGitHubConnection && connections.length === 0)}
- >
- Configure
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx
index 04ed5e793df..e7ddf80f298 100644
--- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx
+++ b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx
@@ -1,15 +1,51 @@
+import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { Check, Github, Loader2 } from 'lucide-react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+import { useParams } from 'common'
import {
ScaffoldContainer,
ScaffoldSection,
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from 'components/layouts/Scaffold'
+import { EmptyIntegrationConnection } from 'components/interfaces/Integrations/VercelGithub/IntegrationPanels'
import NoPermission from 'components/ui/NoPermission'
+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 { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
+import type { IntegrationProjectConnection } from 'data/integrations/integrations.types'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
+import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { BASE_PATH } from 'lib/constants'
-import { AutomaticBranchingRow } from './AutomaticBranchingRow'
+import { useSidePanelsStateSnapshot } from 'state/side-panels'
+import {
+ Badge,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ cn,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ FormMessage_Shadcn_,
+ Input_Shadcn_,
+ Switch,
+} from 'ui'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { IntegrationConnectionItem } from 'components/interfaces/Integrations/VercelGithub/IntegrationConnection'
const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => {
return (
@@ -22,13 +58,260 @@ const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => {
}
const GitHubSection = () => {
+ const { ref: projectRef } = useParams()
+ const selectedProject = useSelectedProject()
+ const selectedOrganization = useSelectedOrganization()
+ const sidePanelStateSnapshot = useSidePanelsStateSnapshot()
+
+ const [isConfirmingBranchChange, setIsConfirmingBranchChange] = useState(false)
+
const canReadGitHubConnection = useCheckPermissions(
PermissionAction.READ,
'integrations.github_connections'
)
+ const canCreateGitHubConnection = useCheckPermissions(
+ PermissionAction.CREATE,
+ 'integrations.github_connections'
+ )
+ const canUpdateGitHubConnection = useCheckPermissions(
+ PermissionAction.UPDATE,
+ 'integrations.github_connections'
+ )
+
+ const { data: connections } = useGitHubConnectionsQuery(
+ { organizationId: selectedOrganization?.id },
+ { enabled: !!projectRef && !!selectedOrganization?.id }
+ )
+
+ const existingConnection = useMemo(
+ () => connections?.find((c) => c.project.ref === projectRef),
+ [connections, projectRef]
+ )
+
+ const { mutate: createBranch } = useBranchCreateMutation()
+ const { mutate: updateBranch } = useBranchUpdateMutation()
+
+ const { data: existingBranches } = useBranchesQuery({ projectRef }, { enabled: !!projectRef })
+
+ const isBranchingEnabled = selectedProject?.is_branch_enabled ?? false
+
+ const { mutate: createConnection, isLoading: isCreatingConnection } =
+ useGitHubConnectionCreateMutation({})
+
+ const { mutate: deleteConnection } = useGitHubConnectionDeleteMutation({
+ onSuccess: () => {
+ toast.success('Successfully deleted GitHub connection')
+ },
+ })
+
+ const { mutateAsync: checkGithubBranchValidity, isLoading: isCheckingBranch } =
+ useCheckGithubBranchValidity({ onError: () => {} })
+
+ const { mutate: updateConnectionSettings, isLoading: isUpdatingConnection } =
+ useGitHubConnectionUpdateMutation()
+
+ const prodBranch = existingBranches?.find((b: any) => b.is_default)
+ const isConnected = Boolean(existingConnection)
const GitHubTitle = `GitHub Integration`
+ // Production Branch Form
+ const ProductionBranchSchema = z
+ .object({
+ branchName: z.string(),
+ })
+ .superRefine(async (val, ctx) => {
+ if (isConnected && existingConnection && val.branchName && val.branchName.length > 0) {
+ try {
+ await checkGithubBranchValidity({
+ repositoryId: existingConnection.repository.id,
+ branchName: val.branchName,
+ })
+ } catch (error) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `Branch "${val.branchName}" not found in repository`,
+ path: ['branchName'],
+ })
+ }
+ }
+ })
+
+ const productionBranchForm = useForm>({
+ resolver: zodResolver(ProductionBranchSchema),
+ mode: 'onSubmit',
+ reValidateMode: 'onBlur',
+ defaultValues: {
+ branchName: 'main',
+ },
+ })
+
+ // Automatic Branching Form
+ const AutomaticBranchingSchema = z.object({
+ autoBranchingEnabled: z.boolean().default(false),
+ supabaseDirectory: z.string().default(''),
+ supabaseChangesOnly: z.boolean().default(false),
+ branchLimit: z.string().default('50'),
+ })
+
+ const automaticBranchingForm = useForm>({
+ resolver: zodResolver(AutomaticBranchingSchema),
+ defaultValues: {
+ autoBranchingEnabled: false,
+ supabaseDirectory: '',
+ supabaseChangesOnly: false,
+ branchLimit: '50',
+ },
+ })
+
+ const autoBranchingEnabled = automaticBranchingForm.watch('autoBranchingEnabled')
+
+ const onSubmitProductionBranch = async (data: z.infer) => {
+ const originalBranchName = prodBranch?.git_branch
+
+ if (originalBranchName && data.branchName !== originalBranchName) {
+ setIsConfirmingBranchChange(true)
+ } else {
+ await executeProductionBranchSave(data)
+ }
+ }
+
+ const executeProductionBranchSave = async (data: z.infer) => {
+ if (!projectRef) return
+
+ if (!isBranchingEnabled) {
+ // create production branch (enables branching)
+ createBranch({
+ projectRef,
+ branchName: data.branchName,
+ gitBranch: data.branchName,
+ })
+ } else {
+ if (prodBranch?.id) {
+ updateBranch({
+ id: prodBranch.id,
+ projectRef,
+ branchName: prodBranch.name,
+ gitBranch: data.branchName,
+ })
+ }
+ }
+
+ toast.success('Production branch configuration updated')
+ setIsConfirmingBranchChange(false)
+ }
+
+ const onConfirmBranchChange = async () => {
+ await executeProductionBranchSave(productionBranchForm.getValues())
+ }
+
+ const onSubmitAutomaticBranching = async (data: z.infer) => {
+ if (!projectRef || !selectedOrganization) return console.error('Missing required data')
+ if (!existingConnection) return toast.error('Please connect to a repository first.')
+
+ if (data.autoBranchingEnabled) {
+ const defaultBranch = productionBranchForm.getValues().branchName
+
+ if (!isBranchingEnabled) {
+ // create production branch (enables branching)
+ createBranch({
+ projectRef,
+ branchName: defaultBranch,
+ gitBranch: defaultBranch,
+ })
+ }
+ }
+
+ updateConnectionSettings({
+ connectionId: existingConnection.id,
+ organizationId: selectedOrganization.id,
+ workdir: data.supabaseDirectory,
+ supabaseChangesOnly: data.supabaseChangesOnly,
+ branchLimit: Number(data.branchLimit),
+ })
+
+ if (data.autoBranchingEnabled) {
+ toast.success('Automatic branching enabled')
+ } else {
+ toast.success('Automatic branching disabled')
+ }
+ }
+
+ const openLinkerPanel = () => {
+ sidePanelStateSnapshot.setGithubConnectionsOpen(true)
+ }
+
+ const onDeleteGitHubConnection = useCallback(
+ async (connection: IntegrationProjectConnection) => {
+ if (!selectedOrganization?.id) {
+ toast.error('Organization not found')
+ return
+ }
+
+ deleteConnection({
+ connectionId: Number(connection.id),
+ organizationId: selectedOrganization.id,
+ })
+ },
+ [deleteConnection, selectedOrganization?.id]
+ )
+
+ useEffect(() => {
+ if (existingConnection) {
+ automaticBranchingForm.reset({
+ autoBranchingEnabled: true,
+ supabaseDirectory:
+ (existingConnection as any)?.workdir ??
+ (existingConnection as any)?.metadata?.supabaseConfig?.supabaseDirectory ??
+ '',
+ supabaseChangesOnly:
+ (existingConnection as any)?.supabase_changes_only ??
+ (existingConnection as any)?.metadata?.supabaseConfig?.supabaseChangesOnly ??
+ false,
+ branchLimit: String(
+ ((existingConnection as any)?.branch_limit as number | undefined) ??
+ (existingConnection as any)?.metadata?.supabaseConfig?.branchLimit ??
+ 50
+ ),
+ })
+ } else {
+ automaticBranchingForm.reset({
+ autoBranchingEnabled: false,
+ supabaseDirectory: '',
+ supabaseChangesOnly: false,
+ branchLimit: '50',
+ })
+ }
+ }, [existingConnection, automaticBranchingForm])
+
+ useEffect(() => {
+ if (prodBranch?.git_branch) {
+ productionBranchForm.reset({
+ branchName: prodBranch.git_branch,
+ })
+ } else {
+ productionBranchForm.reset({
+ branchName: 'main',
+ })
+ }
+ }, [prodBranch, productionBranchForm])
+
+ if (!canReadGitHubConnection) {
+ return (
+
+
+
+ Connect any of your GitHub repositories to a project.
+
+
+
+
+
+
+
+ )
+ }
+
return (
@@ -37,14 +320,268 @@ const GitHubSection = () => {
- {!canReadGitHubConnection ? (
-
- ) : (
+
+ {/* Section 1: GitHub Connection */}
-
Integration features
-
+
GitHub Repository
+ {existingConnection ? (
+
+ ) : (
+
+ Connect repository
+
+ )}
- )}
+
+ {/* Section 2: Production Branch Configuration */}
+
+
+
+
+ {/* Section 3: Automatic Branching Configuration */}
+
+
+
+
+
+ setIsConfirmingBranchChange(false)}
+ onConfirm={onConfirmBranchChange}
+ loading={isUpdatingConnection || isCreatingConnection}
+ >
+
+ Open pull requests will only update your Supabase project on merge if the git base
+ branch matches this new production git branch.
+
+