Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 40 additions & 121 deletions apps/web/src/app/(onboarding)/setup/StepSourceControlConfig.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +10,7 @@ import type {
} from '@roomote/types';

import { useTRPC } from '@/trpc/client';
import { CreateGitHubAppManifestForm } from '@/components/github/CreateGitHubAppManifestForm';
import {
ArrowLeft,
ArrowRight,
Expand All @@ -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,
Expand All @@ -59,10 +50,6 @@ export function StepSourceControlConfig({
Record<string, boolean>
>({});
const [showManualGitHubValues, setShowManualGitHubValues] = useState(false);
const [githubOrganization, setGithubOrganization] = useState('');
const [manifestForm, setManifestForm] =
useState<GitHubAppManifestForm | null>(null);
const manifestFormRef = useRef<HTMLFormElement | null>(null);
const saveSourceControlConfig = useMutation(
trpc.setupNew.saveSourceControlConfig.mutationOptions({
onSuccess: async () => {
Expand All @@ -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(
Expand Down Expand Up @@ -172,98 +140,49 @@ export function StepSourceControlConfig({
<div className="relative w-full max-w-2xl space-y-4 py-2 md:py-0">
<StepTitle text="Create GitHub App" />

<div className="space-y-3 max-w-xl">
<p>
Because Roomote is self-hosted, we can&apos;t offer you an
out-of-the-box GitHub app - you need to create your own.
</p>
<p>
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.
</p>
</div>

<div className="space-y-1 max-w-xl mt-6">
<Label htmlFor="github-app-organization">
GitHub organization (optional)
</Label>
<Input
id="github-app-organization"
value={githubOrganization}
onChange={(event) => setGithubOrganization(event.target.value)}
placeholder="your-organization"
disabled={
createGitHubAppManifest.isPending || manifestForm !== null
}
data-1p-ignore
/>
<p className="text-sm text-muted-foreground">
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.
</p>
</div>

{manifestForm ? (
<form
ref={manifestFormRef}
action={manifestForm.postTarget}
method="post"
className="hidden"
aria-hidden="true"
>
<input
name="manifest"
value={manifestForm.values.manifest}
readOnly
/>
</form>
) : null}

<div className="flex flex-col gap-2 sm:flex-row sm:items-center mt-8">
{onBack ? (
<CreateGitHubAppManifestForm
redirect="/setup?step=source-control-connect"
organizationInputId="github-app-organization"
organizationFieldClassName="space-y-1 max-w-xl"
actionsClassName="flex flex-col gap-2 sm:flex-row sm:items-center mt-4"
description={
<div className="space-y-3 max-w-xl">
<p>
Because Roomote is self-hosted, we can&apos;t offer you an
out-of-the-box GitHub app - you need to create your own.
</p>
<p>
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.
</p>
</div>
}
leadingActions={({ isBusy }) =>
onBack ? (
<Button
type="button"
variant="outline"
onClick={onBack}
disabled={isBusy}
>
<ArrowLeft />
Back
</Button>
) : null
}
trailingActions={({ isBusy }) => (
<Button
type="button"
variant="outline"
onClick={onBack}
disabled={createGitHubAppManifest.isPending}
onClick={() => setShowManualGitHubValues(true)}
disabled={isBusy}
>
<ArrowLeft />
Back
<Pencil />
Enter values manually
</Button>
) : null}
<Button
type="button"
onClick={() =>
createGitHubAppManifest.mutate({
redirect: '/setup?step=source-control-connect',
organization: githubOrganization.trim() || null,
})
}
disabled={
createGitHubAppManifest.isPending || manifestForm !== null
}
>
{createGitHubAppManifest.isPending || manifestForm ? (
<Spinner />
) : (
<Sparkles />
)}
Create GitHub App
</Button>
<Button
type="button"
variant="outline"
onClick={() => setShowManualGitHubValues(true)}
disabled={
createGitHubAppManifest.isPending || manifestForm !== null
}
>
<Pencil />
Enter values manually
</Button>
</div>
)}
/>
</div>
);
}
Expand Down
135 changes: 135 additions & 0 deletions apps/web/src/components/github/CreateGitHubAppManifestForm.tsx
Original file line number Diff line number Diff line change
@@ -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<ManifestFormState | null>(
null,
);
const manifestFormRef = useRef<HTMLFormElement | null>(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 (
<div className={cn('space-y-4', className)}>
{title || description ? (
<div className="space-y-2">
{title}
{description}
</div>
) : null}

<div className={organizationFieldClassName}>
<Label htmlFor={organizationInputId}>
GitHub organization (optional)
</Label>
<Input
id={organizationInputId}
value={githubOrganization}
onChange={(event) => setGithubOrganization(event.target.value)}
placeholder="your-organization"
disabled={isBusy}
data-1p-ignore
/>
<p className="text-sm text-muted-foreground">
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.
</p>
</div>

{manifestForm ? (
<form
ref={manifestFormRef}
action={manifestForm.postTarget}
method="post"
className="hidden"
aria-hidden="true"
>
<input
name="manifest"
value={manifestForm.values.manifest}
readOnly
/>
</form>
) : null}

<div className={actionsClassName}>
{typeof leadingActions === 'function'
? leadingActions({ isBusy })
: leadingActions}
<Button
type="button"
size={createButtonSize}
onClick={() =>
createGitHubAppManifest.mutate({
redirect,
organization: githubOrganization.trim() || null,
})
}
disabled={isBusy}
>
{isBusy ? <Spinner /> : <Sparkles />}
Create GitHub App
</Button>
{typeof trailingActions === 'function'
? trailingActions({ isBusy })
: trailingActions}
</div>
</div>
);
}
Loading