GitHubIntegrationConnectionForm.tsx702 lines · main
| 1 | // @ts-nocheck |
| 2 | import { zodResolver } from '@hookform/resolvers/zod' |
| 3 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 4 | import { AnimatePresence, motion } from 'framer-motion' |
| 5 | import { Loader2 } from 'lucide-react' |
| 6 | import { useEffect, useState } from 'react' |
| 7 | import { useForm } from 'react-hook-form' |
| 8 | import { toast } from 'sonner' |
| 9 | import { |
| 10 | Button, |
| 11 | Card, |
| 12 | CardContent, |
| 13 | CardFooter, |
| 14 | cn, |
| 15 | Form, |
| 16 | FormControl, |
| 17 | FormField, |
| 18 | Input, |
| 19 | Switch, |
| 20 | } from 'ui' |
| 21 | import { Admonition } from 'ui-patterns/admonition' |
| 22 | import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' |
| 23 | import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' |
| 24 | import * as z from 'zod' |
| 25 | |
| 26 | import { |
| 27 | GitHubRepositoryField, |
| 28 | useGitHubRepositoryOptions, |
| 29 | } from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField' |
| 30 | import { InlineLink } from '@/components/ui/InlineLink' |
| 31 | import { UpgradeToPro } from '@/components/ui/UpgradeToPro' |
| 32 | import { useBranchCreateMutation } from '@/data/branches/branch-create-mutation' |
| 33 | import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation' |
| 34 | import { useBranchesQuery } from '@/data/branches/branches-query' |
| 35 | import { useCheckGithubBranchValidity } from '@/data/integrations/github-branch-check-query' |
| 36 | import { useGitHubConnectionCreateMutation } from '@/data/integrations/github-connection-create-mutation' |
| 37 | import { useGitHubConnectionDeleteMutation } from '@/data/integrations/github-connection-delete-mutation' |
| 38 | import { useGitHubConnectionUpdateMutation } from '@/data/integrations/github-connection-update-mutation' |
| 39 | import type { GitHubConnection } from '@/data/integrations/integrations.types' |
| 40 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 41 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 42 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 43 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 44 | import { DOCS_URL } from '@/lib/constants' |
| 45 | |
| 46 | interface GitHubIntegrationConnectionFormProps { |
| 47 | connection?: GitHubConnection |
| 48 | } |
| 49 | |
| 50 | export const GitHubIntegrationConnectionForm = ({ |
| 51 | connection, |
| 52 | }: GitHubIntegrationConnectionFormProps) => { |
| 53 | const { data: selectedProject } = useSelectedProjectQuery() |
| 54 | const { data: selectedOrganization } = useSelectedOrganizationQuery() |
| 55 | const [isConfirmingBranchChange, setIsConfirmingBranchChange] = useState(false) |
| 56 | const [isConfirmingRepoChange, setIsConfirmingRepoChange] = useState(false) |
| 57 | const isParentProject = !selectedProject?.parent_project_ref |
| 58 | |
| 59 | const { hasAccess: hasAccessToGitHubIntegration, isLoading: isLoadingEntitlements } = |
| 60 | useCheckEntitlements('integrations.github_connections') |
| 61 | |
| 62 | const { hasAccess: hasAccessToBranching } = useCheckEntitlements('branching_limit') |
| 63 | |
| 64 | const { can: canUpdateGitHubConnection } = useAsyncCheckPermissions( |
| 65 | PermissionAction.UPDATE, |
| 66 | 'integrations.github_connections' |
| 67 | ) |
| 68 | const { can: canCreateGitHubConnection } = useAsyncCheckPermissions( |
| 69 | PermissionAction.CREATE, |
| 70 | 'integrations.github_connections' |
| 71 | ) |
| 72 | |
| 73 | const { |
| 74 | gitHubAuthorization, |
| 75 | githubRepos, |
| 76 | hasPartialResponseDueToSSO, |
| 77 | isLoading: isLoadingRepositoryOptions, |
| 78 | refetch: refetchRepositoryOptions, |
| 79 | } = useGitHubRepositoryOptions() |
| 80 | |
| 81 | const { mutate: updateBranch } = useBranchUpdateMutation({ |
| 82 | onSuccess: () => { |
| 83 | toast.success('Production branch settings successfully updated') |
| 84 | }, |
| 85 | }) |
| 86 | const { mutate: createBranch } = useBranchCreateMutation({ |
| 87 | onSuccess: () => { |
| 88 | toast.success('Production branch settings successfully updated') |
| 89 | }, |
| 90 | onError: (error) => { |
| 91 | console.error('Failed to enable branching:', error) |
| 92 | }, |
| 93 | }) |
| 94 | |
| 95 | const { data: existingBranches } = useBranchesQuery( |
| 96 | { projectRef: selectedProject?.ref }, |
| 97 | { enabled: !!selectedProject?.ref } |
| 98 | ) |
| 99 | |
| 100 | const { mutateAsync: checkGithubBranchValidity, isPending: isCheckingBranch } = |
| 101 | useCheckGithubBranchValidity({ onError: () => {} }) |
| 102 | |
| 103 | const { mutate: createConnection, isPending: isCreatingConnection } = |
| 104 | useGitHubConnectionCreateMutation({ |
| 105 | onSuccess: () => { |
| 106 | toast.success('GitHub integration successfully updated') |
| 107 | }, |
| 108 | onError: (error) => { |
| 109 | // Don't show error toast when connection already exists - the branch |
| 110 | // settings update will still proceed and show its own success toast |
| 111 | if (!error.message?.includes('already exists')) { |
| 112 | toast.error(`Failed to create GitHub connection: ${error.message}`) |
| 113 | } |
| 114 | }, |
| 115 | }) |
| 116 | |
| 117 | const { mutateAsync: deleteConnection, isPending: isDeletingConnection } = |
| 118 | useGitHubConnectionDeleteMutation({ |
| 119 | onSuccess: () => { |
| 120 | toast.success('Successfully removed GitHub integration') |
| 121 | }, |
| 122 | }) |
| 123 | |
| 124 | const { mutate: updateConnectionSettings, isPending: isUpdatingConnection } = |
| 125 | useGitHubConnectionUpdateMutation() |
| 126 | |
| 127 | const prodBranch = existingBranches?.find((branch) => branch.is_default) |
| 128 | |
| 129 | // Combined GitHub Settings Form |
| 130 | const GitHubSettingsSchema = z |
| 131 | .object({ |
| 132 | repositoryId: z.string().min(1, 'Please select a repository'), |
| 133 | enableProductionSync: z.boolean().default(true), |
| 134 | branchName: z.string().default('main'), |
| 135 | new_branch_per_pr: z.boolean().default(true), |
| 136 | brivenDirectory: z.string().default('.'), |
| 137 | brivenChangesOnly: z.boolean().default(true), |
| 138 | branchLimit: z.string().default('50'), |
| 139 | }) |
| 140 | .superRefine(async (val, ctx) => { |
| 141 | if (val.enableProductionSync && val.branchName && val.branchName.length > 0) { |
| 142 | const repositoryId = val.repositoryId || connection?.repository.id.toString() |
| 143 | if (repositoryId) { |
| 144 | try { |
| 145 | await checkGithubBranchValidity({ |
| 146 | repositoryId: Number(repositoryId), |
| 147 | branchName: val.branchName, |
| 148 | }) |
| 149 | } catch { |
| 150 | const selectedRepo = githubRepos.find((repo) => repo.id === repositoryId) |
| 151 | const repoName = |
| 152 | selectedRepo?.name || connection?.repository.name || 'selected repository' |
| 153 | ctx.addIssue({ |
| 154 | code: z.ZodIssueCode.custom, |
| 155 | message: `Branch "${val.branchName}" not found in ${repoName}`, |
| 156 | path: ['branchName'], |
| 157 | }) |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | }) |
| 162 | |
| 163 | const githubSettingsForm = useForm<z.infer<typeof GitHubSettingsSchema>>({ |
| 164 | resolver: zodResolver(GitHubSettingsSchema as any), |
| 165 | mode: 'onSubmit', |
| 166 | reValidateMode: 'onBlur', |
| 167 | defaultValues: { |
| 168 | repositoryId: connection?.repository.id.toString() || '', |
| 169 | enableProductionSync: true, |
| 170 | branchName: 'main', |
| 171 | new_branch_per_pr: true, |
| 172 | brivenDirectory: '.', |
| 173 | brivenChangesOnly: true, |
| 174 | branchLimit: '3', |
| 175 | }, |
| 176 | }) |
| 177 | |
| 178 | const enableProductionSync = githubSettingsForm.watch('enableProductionSync') |
| 179 | const newBranchPerPr = githubSettingsForm.watch('new_branch_per_pr') |
| 180 | const currentRepositoryId = githubSettingsForm.watch('repositoryId') |
| 181 | |
| 182 | const handleCreateOrUpdateConnection = async (data: z.infer<typeof GitHubSettingsSchema>) => { |
| 183 | if (!selectedProject?.ref || !selectedOrganization?.id) return |
| 184 | |
| 185 | try { |
| 186 | if (connection) { |
| 187 | // Check if repository is being changed |
| 188 | if (connection.repository.id.toString() !== data.repositoryId) { |
| 189 | setIsConfirmingRepoChange(true) |
| 190 | return |
| 191 | } |
| 192 | // Update existing connection |
| 193 | await handleUpdateConnection(data, connection) |
| 194 | } else { |
| 195 | // Create new connection |
| 196 | const selectedRepo = githubRepos.find((repo) => repo.id === data.repositoryId) |
| 197 | if (!selectedRepo) { |
| 198 | toast.error('Please select a repository') |
| 199 | return |
| 200 | } |
| 201 | await handleCreateConnection(data, selectedRepo) |
| 202 | } |
| 203 | } catch (error) { |
| 204 | console.error('Error managing connection:', error) |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | const handleCreateConnection = async ( |
| 209 | data: z.infer<typeof GitHubSettingsSchema>, |
| 210 | selectedRepo: { id: string; installation_id: number } |
| 211 | ) => { |
| 212 | if (!selectedProject?.ref || !selectedOrganization?.id) return |
| 213 | |
| 214 | createConnection({ |
| 215 | organizationId: selectedOrganization.id, |
| 216 | connection: { |
| 217 | installation_id: selectedRepo.installation_id, |
| 218 | project_ref: selectedProject.ref, |
| 219 | repository_id: Number(selectedRepo.id), |
| 220 | workdir: data.brivenDirectory, |
| 221 | briven_changes_only: data.brivenChangesOnly, |
| 222 | branch_limit: Number(data.branchLimit), |
| 223 | new_branch_per_pr: data.new_branch_per_pr, |
| 224 | }, |
| 225 | }) |
| 226 | |
| 227 | if (!prodBranch) { |
| 228 | createBranch({ |
| 229 | projectRef: selectedProject.ref, |
| 230 | branchName: 'main', |
| 231 | gitBranch: data.branchName, |
| 232 | is_default: true, |
| 233 | }) |
| 234 | } else { |
| 235 | updateBranch({ |
| 236 | branchRef: prodBranch.project_ref, |
| 237 | projectRef: selectedProject.ref, |
| 238 | gitBranch: data.branchName, |
| 239 | }) |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | const handleUpdateConnection = async ( |
| 244 | data: z.infer<typeof GitHubSettingsSchema>, |
| 245 | currentConnection: GitHubConnection |
| 246 | ) => { |
| 247 | if (!selectedProject?.ref || !selectedOrganization?.id) return |
| 248 | |
| 249 | const originalBranchName = prodBranch?.git_branch |
| 250 | |
| 251 | if (originalBranchName && data.branchName !== originalBranchName && data.enableProductionSync) { |
| 252 | setIsConfirmingBranchChange(true) |
| 253 | return |
| 254 | } |
| 255 | |
| 256 | await executeUpdate(data, currentConnection) |
| 257 | } |
| 258 | |
| 259 | const executeUpdate = async ( |
| 260 | data: z.infer<typeof GitHubSettingsSchema>, |
| 261 | currentConnection: GitHubConnection |
| 262 | ) => { |
| 263 | if (!selectedProject?.ref || !selectedOrganization?.id) return |
| 264 | |
| 265 | updateConnectionSettings({ |
| 266 | connectionId: currentConnection.id, |
| 267 | organizationId: selectedOrganization.id, |
| 268 | connection: { |
| 269 | workdir: data.brivenDirectory, |
| 270 | briven_changes_only: data.brivenChangesOnly, |
| 271 | branch_limit: Number(data.branchLimit), |
| 272 | new_branch_per_pr: data.new_branch_per_pr, |
| 273 | }, |
| 274 | }) |
| 275 | |
| 276 | if (prodBranch) { |
| 277 | updateBranch({ |
| 278 | branchRef: prodBranch.project_ref, |
| 279 | projectRef: selectedProject.ref, |
| 280 | gitBranch: data.enableProductionSync ? data.branchName : '', |
| 281 | branchName: data.branchName || 'main', |
| 282 | }) |
| 283 | } else { |
| 284 | // if for some reason, the project doesn't have a default branch yet, create it. |
| 285 | createBranch({ |
| 286 | projectRef: selectedProject.ref, |
| 287 | gitBranch: data.enableProductionSync ? data.branchName : '', |
| 288 | branchName: data.branchName || 'main', |
| 289 | is_default: true, |
| 290 | }) |
| 291 | } |
| 292 | |
| 293 | setIsConfirmingBranchChange(false) |
| 294 | } |
| 295 | |
| 296 | const onConfirmBranchChange = async () => { |
| 297 | if (connection) { |
| 298 | await executeUpdate(githubSettingsForm.getValues(), connection) |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | const handleRemoveIntegration = async () => { |
| 303 | if (!connection || !selectedOrganization?.id) return |
| 304 | |
| 305 | try { |
| 306 | await deleteConnection({ |
| 307 | organizationId: selectedOrganization.id, |
| 308 | connectionId: connection.id, |
| 309 | }) |
| 310 | |
| 311 | githubSettingsForm.reset({ |
| 312 | repositoryId: '', |
| 313 | enableProductionSync: true, |
| 314 | branchName: 'main', |
| 315 | new_branch_per_pr: true, |
| 316 | brivenDirectory: '.', |
| 317 | brivenChangesOnly: true, |
| 318 | branchLimit: '3', |
| 319 | }) |
| 320 | } catch (error) { |
| 321 | console.error('Error removing integration:', error) |
| 322 | toast.error('Failed to remove integration') |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | const onConfirmRepoChange = async () => { |
| 327 | const data = githubSettingsForm.getValues() |
| 328 | const selectedRepo = githubRepos.find((repo) => repo.id === data.repositoryId) |
| 329 | |
| 330 | if (!selectedRepo || !connection || !selectedOrganization?.id) return |
| 331 | |
| 332 | try { |
| 333 | await deleteConnection({ |
| 334 | organizationId: selectedOrganization.id, |
| 335 | connectionId: connection.id, |
| 336 | }) |
| 337 | |
| 338 | await handleCreateConnection(data, selectedRepo) |
| 339 | |
| 340 | setIsConfirmingRepoChange(false) |
| 341 | } catch (error) { |
| 342 | console.error('Error changing repository:', error) |
| 343 | toast.error('Failed to change repository') |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | useEffect(() => { |
| 348 | if (connection) { |
| 349 | const hasGitBranch = Boolean(prodBranch?.git_branch?.trim()) |
| 350 | |
| 351 | githubSettingsForm.reset({ |
| 352 | repositoryId: connection.repository.id.toString(), |
| 353 | enableProductionSync: hasGitBranch, |
| 354 | branchName: prodBranch?.git_branch || 'main', |
| 355 | new_branch_per_pr: connection.new_branch_per_pr, |
| 356 | brivenDirectory: connection.workdir || '', |
| 357 | brivenChangesOnly: connection.briven_changes_only, |
| 358 | branchLimit: String(connection.branch_limit), |
| 359 | }) |
| 360 | } |
| 361 | }, [connection, prodBranch, githubSettingsForm]) |
| 362 | |
| 363 | // Handle clearing branch name when production sync is disabled |
| 364 | useEffect(() => { |
| 365 | if (!enableProductionSync) { |
| 366 | githubSettingsForm.setValue('branchName', '') |
| 367 | } else if (enableProductionSync && !githubSettingsForm.getValues().branchName) { |
| 368 | githubSettingsForm.setValue('branchName', 'main') |
| 369 | } |
| 370 | }, [enableProductionSync, githubSettingsForm]) |
| 371 | |
| 372 | const isLoading = |
| 373 | isLoadingEntitlements || |
| 374 | isCreatingConnection || |
| 375 | isUpdatingConnection || |
| 376 | isDeletingConnection || |
| 377 | isLoadingRepositoryOptions |
| 378 | |
| 379 | return ( |
| 380 | <> |
| 381 | <Form {...githubSettingsForm}> |
| 382 | <form |
| 383 | onSubmit={githubSettingsForm.handleSubmit(handleCreateOrUpdateConnection)} |
| 384 | className={cn(!isParentProject && 'opacity-25 pointer-events-none')} |
| 385 | > |
| 386 | <Card> |
| 387 | <CardContent className="space-y-6"> |
| 388 | <GitHubRepositoryField |
| 389 | form={githubSettingsForm} |
| 390 | name="repositoryId" |
| 391 | label="GitHub Repository" |
| 392 | layout="flex-row-reverse" |
| 393 | description={ |
| 394 | connection |
| 395 | ? 'Change the connected repository' |
| 396 | : 'Select the repository to connect to your project' |
| 397 | } |
| 398 | disabled={ |
| 399 | (!connection && !canCreateGitHubConnection) || |
| 400 | (connection && !canUpdateGitHubConnection) |
| 401 | } |
| 402 | selectedRepositoryName={connection?.repository.name} |
| 403 | repositories={githubRepos} |
| 404 | gitHubAuthorization={gitHubAuthorization} |
| 405 | hasPartialResponseDueToSSO={hasPartialResponseDueToSSO} |
| 406 | isLoading={isLoadingRepositoryOptions} |
| 407 | refetch={refetchRepositoryOptions} |
| 408 | onRepositorySelect={(repo) => { |
| 409 | githubSettingsForm.setValue('branchName', repo.default_branch || 'main') |
| 410 | }} |
| 411 | /> |
| 412 | </CardContent> |
| 413 | |
| 414 | <AnimatePresence> |
| 415 | {gitHubAuthorization !== null && !!currentRepositoryId && ( |
| 416 | <motion.div |
| 417 | initial={{ opacity: 0, y: -16 }} |
| 418 | animate={{ opacity: 1, y: 0 }} |
| 419 | exit={{ opacity: 0, y: -16 }} |
| 420 | > |
| 421 | <CardContent> |
| 422 | <FormField |
| 423 | control={githubSettingsForm.control} |
| 424 | name="brivenDirectory" |
| 425 | render={({ field }) => ( |
| 426 | <FormItemLayout |
| 427 | layout="flex-row-reverse" |
| 428 | label="Working directory" |
| 429 | description={ |
| 430 | <> |
| 431 | Relative path to the directory containing your{' '} |
| 432 | <code className="text-code-inline whitespace-nowrap">briven/</code>{' '} |
| 433 | folder.{' '} |
| 434 | <InlineLink |
| 435 | href={`${DOCS_URL}/guides/deployment/branching/github-integration#set-the-working-directory`} |
| 436 | > |
| 437 | Learn more |
| 438 | </InlineLink> |
| 439 | </> |
| 440 | } |
| 441 | > |
| 442 | <FormControl> |
| 443 | <Input |
| 444 | {...field} |
| 445 | placeholder="." |
| 446 | autoComplete="off" |
| 447 | disabled={!canUpdateGitHubConnection} |
| 448 | /> |
| 449 | </FormControl> |
| 450 | </FormItemLayout> |
| 451 | )} |
| 452 | /> |
| 453 | </CardContent> |
| 454 | <CardContent> |
| 455 | {/* Production Branch Sync Section */} |
| 456 | <div className="space-y-4"> |
| 457 | <FormField |
| 458 | control={githubSettingsForm.control} |
| 459 | name="enableProductionSync" |
| 460 | render={({ field }) => ( |
| 461 | <FormItemLayout |
| 462 | layout="flex-row-reverse" |
| 463 | label="Deploy to production" |
| 464 | description="Deploy changes to production on push including PR merges" |
| 465 | > |
| 466 | <FormControl> |
| 467 | <Switch |
| 468 | checked={field.value} |
| 469 | onCheckedChange={field.onChange} |
| 470 | disabled={!canUpdateGitHubConnection} |
| 471 | /> |
| 472 | </FormControl> |
| 473 | </FormItemLayout> |
| 474 | )} |
| 475 | /> |
| 476 | |
| 477 | <div |
| 478 | className={cn( |
| 479 | 'space-y-4 pl-6 border-l', |
| 480 | !enableProductionSync && 'opacity-25 pointer-events-none' |
| 481 | )} |
| 482 | > |
| 483 | <FormField |
| 484 | control={githubSettingsForm.control} |
| 485 | name="branchName" |
| 486 | render={({ field }) => ( |
| 487 | <FormItemLayout |
| 488 | layout="flex-row-reverse" |
| 489 | label="Production branch name" |
| 490 | description="The GitHub branch to sync with your production database (e.g., main, master)" |
| 491 | > |
| 492 | <div className="relative w-full"> |
| 493 | <FormControl> |
| 494 | <Input |
| 495 | {...field} |
| 496 | autoComplete="off" |
| 497 | disabled={!canUpdateGitHubConnection || !enableProductionSync} |
| 498 | /> |
| 499 | </FormControl> |
| 500 | <div className="absolute top-2.5 right-3 flex items-center gap-2"> |
| 501 | {isCheckingBranch && ( |
| 502 | <Loader2 size={14} className="animate-spin" /> |
| 503 | )} |
| 504 | </div> |
| 505 | </div> |
| 506 | </FormItemLayout> |
| 507 | )} |
| 508 | /> |
| 509 | </div> |
| 510 | </div> |
| 511 | </CardContent> |
| 512 | <CardContent> |
| 513 | {hasAccessToBranching ? ( |
| 514 | <Admonition type="warning" title="Branching and billing" className="mb-4"> |
| 515 | Branching Compute is not covered by your organization's Spend Cap. |
| 516 | Costs should be closely monitored, as they may be incurred.{' '} |
| 517 | <InlineLink |
| 518 | href={`${DOCS_URL}/guides/platform/cost-control#usage-items-not-covered-by-the-spend-cap`} |
| 519 | > |
| 520 | Learn more |
| 521 | </InlineLink> |
| 522 | </Admonition> |
| 523 | ) : ( |
| 524 | <UpgradeToPro |
| 525 | className="mb-4" |
| 526 | layout="vertical" |
| 527 | source="projectIntegrations" |
| 528 | featureProposition="automatically create preview branches from pull requests" |
| 529 | primaryText="Branching with GitHub integration" |
| 530 | secondaryText="Upgrade to the Pro Plan to enable branching and automatically create preview branches for every pull request" |
| 531 | docsUrl={`${DOCS_URL}/guides/deployment/branching`} |
| 532 | /> |
| 533 | )} |
| 534 | |
| 535 | {/* Automatic Branching Section */} |
| 536 | <div className="space-y-4"> |
| 537 | <FormField |
| 538 | disabled={!hasAccessToBranching} |
| 539 | control={githubSettingsForm.control} |
| 540 | name="new_branch_per_pr" |
| 541 | render={({ field }) => ( |
| 542 | <FormItemLayout |
| 543 | layout="flex-row-reverse" |
| 544 | label="Automatic branching" |
| 545 | className={cn(!hasAccessToBranching && 'opacity-25')} |
| 546 | description="Create preview branches for every pull request" |
| 547 | > |
| 548 | <FormControl> |
| 549 | <Switch |
| 550 | checked={!hasAccessToBranching ? false : field.value} |
| 551 | onCheckedChange={field.onChange} |
| 552 | disabled={!hasAccessToBranching || !canCreateGitHubConnection} |
| 553 | /> |
| 554 | </FormControl> |
| 555 | </FormItemLayout> |
| 556 | )} |
| 557 | /> |
| 558 | |
| 559 | <div |
| 560 | className={cn( |
| 561 | 'space-y-4 pl-6 border-l', |
| 562 | (!hasAccessToBranching || !newBranchPerPr) && |
| 563 | 'opacity-25 pointer-events-none' |
| 564 | )} |
| 565 | > |
| 566 | <FormField |
| 567 | control={githubSettingsForm.control} |
| 568 | name="branchLimit" |
| 569 | render={({ field }) => ( |
| 570 | <FormItemLayout |
| 571 | layout="flex-row-reverse" |
| 572 | label="Branch limit" |
| 573 | description="Maximum number of preview branches" |
| 574 | > |
| 575 | <FormControl> |
| 576 | <Input |
| 577 | {...field} |
| 578 | type="number" |
| 579 | autoComplete="off" |
| 580 | value={!hasAccessToBranching ? 0 : field.value} |
| 581 | disabled={ |
| 582 | !hasAccessToBranching || |
| 583 | !newBranchPerPr || |
| 584 | !canUpdateGitHubConnection |
| 585 | } |
| 586 | /> |
| 587 | </FormControl> |
| 588 | </FormItemLayout> |
| 589 | )} |
| 590 | /> |
| 591 | |
| 592 | <FormField |
| 593 | control={githubSettingsForm.control} |
| 594 | name="brivenChangesOnly" |
| 595 | render={({ field }) => ( |
| 596 | <FormItemLayout |
| 597 | layout="flex-row-reverse" |
| 598 | label="Briven changes only" |
| 599 | description="Only create branches when Briven files change" |
| 600 | > |
| 601 | <FormControl> |
| 602 | <Switch |
| 603 | checked={!hasAccessToBranching ? false : field.value} |
| 604 | onCheckedChange={(val) => field.onChange(val)} |
| 605 | disabled={ |
| 606 | !hasAccessToBranching || |
| 607 | !newBranchPerPr || |
| 608 | !canUpdateGitHubConnection |
| 609 | } |
| 610 | /> |
| 611 | </FormControl> |
| 612 | </FormItemLayout> |
| 613 | )} |
| 614 | /> |
| 615 | </div> |
| 616 | </div> |
| 617 | </CardContent> |
| 618 | <CardFooter className="flex justify-between items-center"> |
| 619 | <div> |
| 620 | {connection && ( |
| 621 | <Button |
| 622 | type="outline" |
| 623 | onClick={handleRemoveIntegration} |
| 624 | disabled={isDeletingConnection || isCheckingBranch} |
| 625 | loading={isDeletingConnection} |
| 626 | > |
| 627 | Disable integration |
| 628 | </Button> |
| 629 | )} |
| 630 | </div> |
| 631 | <div className="flex space-x-2"> |
| 632 | {githubSettingsForm.formState.isDirty && ( |
| 633 | <Button |
| 634 | type="default" |
| 635 | onClick={() => githubSettingsForm.reset()} |
| 636 | disabled={!canUpdateGitHubConnection || isCheckingBranch} |
| 637 | > |
| 638 | Cancel |
| 639 | </Button> |
| 640 | )} |
| 641 | <Button |
| 642 | type="primary" |
| 643 | htmlType="submit" |
| 644 | disabled={ |
| 645 | !hasAccessToGitHubIntegration || |
| 646 | (!connection && !canCreateGitHubConnection) || |
| 647 | (connection && !canUpdateGitHubConnection) || |
| 648 | isCheckingBranch || |
| 649 | isLoading || |
| 650 | (!connection && !githubSettingsForm.getValues().repositoryId) || |
| 651 | (connection && !githubSettingsForm.formState.isDirty) |
| 652 | } |
| 653 | loading={isLoading} |
| 654 | > |
| 655 | {connection ? 'Save changes' : 'Enable integration'} |
| 656 | </Button> |
| 657 | </div> |
| 658 | </CardFooter> |
| 659 | </motion.div> |
| 660 | )} |
| 661 | </AnimatePresence> |
| 662 | </Card> |
| 663 | </form> |
| 664 | </Form> |
| 665 | |
| 666 | <ConfirmationModal |
| 667 | variant="warning" |
| 668 | visible={isConfirmingBranchChange} |
| 669 | title="Changing production git branch" |
| 670 | confirmLabel="Confirm" |
| 671 | size="medium" |
| 672 | onCancel={() => setIsConfirmingBranchChange(false)} |
| 673 | onConfirm={onConfirmBranchChange} |
| 674 | loading={isUpdatingConnection} |
| 675 | > |
| 676 | <p className="text-sm text-foreground-light"> |
| 677 | Open pull requests will only update your Briven project on merge if the git base branch |
| 678 | matches this new production git branch. |
| 679 | </p> |
| 680 | </ConfirmationModal> |
| 681 | |
| 682 | <ConfirmationModal |
| 683 | variant="warning" |
| 684 | visible={isConfirmingRepoChange} |
| 685 | title="Changing GitHub repository" |
| 686 | confirmLabel="Change repository" |
| 687 | size="medium" |
| 688 | onCancel={() => setIsConfirmingRepoChange(false)} |
| 689 | onConfirm={onConfirmRepoChange} |
| 690 | loading={isLoading} |
| 691 | > |
| 692 | <div className="space-y-3"> |
| 693 | <p className="text-sm text-foreground-light"> |
| 694 | This will disconnect your current repository and create a new connection with the |
| 695 | selected repository. All existing Briven branches that are connected to the old |
| 696 | repository will no longer be synced. |
| 697 | </p> |
| 698 | </div> |
| 699 | </ConfirmationModal> |
| 700 | </> |
| 701 | ) |
| 702 | } |