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.
-
-
-
-
-
- GitHub organization (optional)
-
-
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 ? (
+
+
+ Back
+
+ ) : null
+ }
+ trailingActions={({ isBusy }) => (
setShowManualGitHubValues(true)}
+ disabled={isBusy}
>
-
- Back
+
+ Enter values manually
- ) : null}
-
- createGitHubAppManifest.mutate({
- redirect: '/setup?step=source-control-connect',
- organization: githubOrganization.trim() || null,
- })
- }
- disabled={
- createGitHubAppManifest.isPending || manifestForm !== null
- }
- >
- {createGitHubAppManifest.isPending || manifestForm ? (
-
- ) : (
-
- )}
- Create GitHub App
-
-
setShowManualGitHubValues(true)}
- disabled={
- createGitHubAppManifest.isPending || manifestForm !== null
- }
- >
-
- Enter values manually
-
-
+ )}
+ />
);
}
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}
+
+
+
+ GitHub organization (optional)
+
+
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}
+
+ createGitHubAppManifest.mutate({
+ redirect,
+ organization: githubOrganization.trim() || null,
+ })
+ }
+ disabled={isBusy}
+ >
+ {isBusy ? : }
+ Create GitHub App
+
+ {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.
-
-
-
-
- GitHub organization (optional)
-
-
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}
-
-
- createGitHubAppManifest.mutate({
- redirect: redirectTarget,
- organization: githubOrganization.trim() || null,
- })
- }
- disabled={createGitHubAppManifest.isPending || manifestForm !== null}
- >
- {createGitHubAppManifest.isPending || manifestForm ? (
-
- ) : (
-
- )}
- Create GitHub App
-
-
-
- );
-}
-
function GitHubAppSettingsSetupPanel({
setup,
showManualValues,