diff --git a/apps/web/src/app/(onboarding)/setup/StepSourceControlConfig.tsx b/apps/web/src/app/(onboarding)/setup/StepSourceControlConfig.tsx index aae52260..e6be95cb 100644 --- a/apps/web/src/app/(onboarding)/setup/StepSourceControlConfig.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepSourceControlConfig.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import Link from 'next/link'; import { toast } from 'sonner'; @@ -10,6 +10,7 @@ import type { } from '@roomote/types'; import { useTRPC } from '@/trpc/client'; +import { CreateGitHubAppManifestForm } from '@/components/github/CreateGitHubAppManifestForm'; import { ArrowLeft, ArrowRight, @@ -18,25 +19,15 @@ import { CopyIconButton, ExternalLink, Input, - Label, Pencil, - Sparkles, Spinner, } from '@/components/system'; -import { useCreateGitHubAppManifest } from '@/hooks/github'; import { StepTitle } from './StepTitle'; import { getSourceControlSetupCopy } from './sourceControlSetupCopy'; const MASKED_VALUE = '••••••••••••••••••••••••••••'; -type GitHubAppManifestForm = { - postTarget: string; - values: { - manifest: string; - }; -}; - export function StepSourceControlConfig({ sourceControlSetup, selectedProviderId, @@ -59,10 +50,6 @@ export function StepSourceControlConfig({ Record >({}); const [showManualGitHubValues, setShowManualGitHubValues] = useState(false); - const [githubOrganization, setGithubOrganization] = useState(''); - const [manifestForm, setManifestForm] = - useState(null); - const manifestFormRef = useRef(null); const saveSourceControlConfig = useMutation( trpc.setupNew.saveSourceControlConfig.mutationOptions({ onSuccess: async () => { @@ -76,32 +63,13 @@ export function StepSourceControlConfig({ }, }), ); - const createGitHubAppManifest = useCreateGitHubAppManifest({ - onSuccess: (result) => { - if (result.success) { - setManifestForm(result); - } else { - toast.error(result.error); - } - }, - onError: () => - toast.error('Failed to start GitHub App creation. Please try again.'), - }); useEffect(() => { setValues({}); setEditingSavedValues({}); setShowManualGitHubValues(false); - setGithubOrganization(''); - setManifestForm(null); }, [effectiveSelectedProviderId]); - useEffect(() => { - if (manifestForm) { - manifestFormRef.current?.submit(); - } - }, [manifestForm]); - const selectedProvider = useMemo( () => sourceControlSetup.providers.find( @@ -172,98 +140,49 @@ export function StepSourceControlConfig({
-
-

- Because Roomote is self-hosted, we can't offer you an - out-of-the-box GitHub app - you need to create your own. -

-

- Roomote can create it for you automatically or you can enter values - manually if you already have an app or want to do each step - yourself. -

-
- -
- - setGithubOrganization(event.target.value)} - placeholder="your-organization" - disabled={ - createGitHubAppManifest.isPending || manifestForm !== null - } - data-1p-ignore - /> -

- The app can only be installed on the account that owns it. Enter an - organization name to create the app there, or leave this blank to - create it on your personal GitHub account. -

-
- - {manifestForm ? ( - - ) : null} - -
- {onBack ? ( + +

+ Because Roomote is self-hosted, we can't offer you an + out-of-the-box GitHub app - you need to create your own. +

+

+ Roomote can create it for you automatically or you can enter + values manually if you already have an app or want to do each + step yourself. +

+
+ } + leadingActions={({ isBusy }) => + onBack ? ( + + ) : null + } + trailingActions={({ isBusy }) => ( - ) : null} - - -
+ )} + /> ); } diff --git a/apps/web/src/components/github/CreateGitHubAppManifestForm.tsx b/apps/web/src/components/github/CreateGitHubAppManifestForm.tsx new file mode 100644 index 00000000..89481cb0 --- /dev/null +++ b/apps/web/src/components/github/CreateGitHubAppManifestForm.tsx @@ -0,0 +1,135 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; + +import { useCreateGitHubAppManifest } from '@/hooks/github'; +import { Button, Input, Label, Sparkles, Spinner } from '@/components/system'; +import { cn } from '@/lib/utils'; + +type ManifestFormState = { + postTarget: string; + values: { + manifest: string; + }; +}; + +type CreateGitHubAppManifestFormProps = { + redirect: string; + organizationInputId?: string; + title?: ReactNode; + description?: ReactNode; + createButtonSize?: 'default' | 'sm'; + className?: string; + leadingActions?: ReactNode | ((context: { isBusy: boolean }) => ReactNode); + trailingActions?: ReactNode | ((context: { isBusy: boolean }) => ReactNode); + actionsClassName?: string; + organizationFieldClassName?: string; +}; + +export function CreateGitHubAppManifestForm({ + redirect, + organizationInputId = 'github-app-organization', + title, + description, + createButtonSize = 'default', + className, + leadingActions, + trailingActions, + actionsClassName = 'flex flex-col gap-2 sm:flex-row sm:items-center', + organizationFieldClassName = 'space-y-1', +}: CreateGitHubAppManifestFormProps) { + const [githubOrganization, setGithubOrganization] = useState(''); + const [manifestForm, setManifestForm] = useState( + null, + ); + const manifestFormRef = useRef(null); + const createGitHubAppManifest = useCreateGitHubAppManifest({ + onSuccess: (result) => { + if (result.success) { + setManifestForm(result); + } else { + toast.error(result.error); + } + }, + onError: () => + toast.error('Failed to start GitHub App creation. Please try again.'), + }); + const isBusy = createGitHubAppManifest.isPending || manifestForm !== null; + + useEffect(() => { + if (manifestForm) { + manifestFormRef.current?.submit(); + } + }, [manifestForm]); + + return ( +
+ {title || description ? ( +
+ {title} + {description} +
+ ) : null} + +
+ + setGithubOrganization(event.target.value)} + placeholder="your-organization" + disabled={isBusy} + data-1p-ignore + /> +

+ The app can only be installed on the account that owns it. Enter an + organization name to create the app there, or leave this blank to + create it on your personal GitHub account. +

+
+ + {manifestForm ? ( + + ) : null} + +
+ {typeof leadingActions === 'function' + ? leadingActions({ isBusy }) + : leadingActions} + + {typeof trailingActions === 'function' + ? trailingActions({ isBusy }) + : trailingActions} +
+
+ ); +} diff --git a/apps/web/src/components/settings/SourceControl.tsx b/apps/web/src/components/settings/SourceControl.tsx index c6cc8ddf..fbab9c8f 100644 --- a/apps/web/src/components/settings/SourceControl.tsx +++ b/apps/web/src/components/settings/SourceControl.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ReactNode } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; import { toast } from 'sonner'; @@ -25,11 +25,11 @@ import { useAuthorizedUser } from '@/hooks/useUser'; import { getSourceControlSetupCopy } from '@/app/(onboarding)/setup/sourceControlSetupCopy'; import { - useCreateGitHubAppManifest, useEnableGitHubApp, useGitHubInstallations, useSyncGitHubInstallations, } from '@/hooks/github'; +import { CreateGitHubAppManifestForm } from '@/components/github/CreateGitHubAppManifestForm'; import { Alert, AlertDescription, @@ -40,8 +40,6 @@ import { ChevronUp, ExternalLink, GitMerge, - Input, - Label, Pencil, Plug, RefreshCw, @@ -50,8 +48,6 @@ import { SelectItem, SelectTrigger, SelectValue, - Sparkles, - Spinner, } from '@/components/system'; import { Section } from '@/components/settings'; import { SourceControlConfigForm } from './SourceControlConfigForm'; @@ -328,7 +324,24 @@ export function SourceControl() { /> ) : null, githubSetup: isAdmin ? ( - + + Create a GitHub App for this deployment. +

+ } + description={ +

+ Self-hosted Roomote needs its own GitHub App. Create one + automatically, or enter values manually if you already have an + app. +

+ } + /> ) : null, }, ...sourceControlTokenBackedProviders.map((provider) => @@ -693,103 +706,6 @@ function SourceControlProviderBlock({ ); } -function GitHubAppSettingsSetup({ - redirectTarget, -}: { - redirectTarget: string; -}) { - const [githubOrganization, setGithubOrganization] = useState(''); - const [manifestForm, setManifestForm] = useState<{ - postTarget: string; - values: { manifest: string }; - } | null>(null); - const manifestFormRef = useRef(null); - const createGitHubAppManifest = useCreateGitHubAppManifest({ - onSuccess: (result) => { - if (result.success) { - setManifestForm(result); - } else { - toast.error(result.error); - } - }, - onError: () => - toast.error('Failed to start GitHub App creation. Please try again.'), - }); - - useEffect(() => { - if (manifestForm) { - manifestFormRef.current?.submit(); - } - }, [manifestForm]); - - return ( -
-
-

- Create a GitHub App for this deployment. -

-

- Self-hosted Roomote needs its own GitHub App. Create one - automatically, or enter values manually if you already have an app. -

-
-
- - setGithubOrganization(event.target.value)} - placeholder="your-organization" - disabled={createGitHubAppManifest.isPending || manifestForm !== null} - data-1p-ignore - /> -

- The app can only be installed on the account that owns it. Enter an - organization name to create the app there, or leave this blank to - create it on your personal GitHub account. -

-
- {manifestForm ? ( - - ) : null} -
- -
-
- ); -} - function GitHubAppSettingsSetupPanel({ setup, showManualValues,