Skip to content
Merged
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
89 changes: 89 additions & 0 deletions apps/desktop/src/components/ForgeAccountConfig.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script lang="ts" generics="TAccount">
import { useSettingsModal } from '$lib/settings/settingsModal.svelte';
import { Button, CardGroup, Link, Select, SelectItem } from '@gitbutler/ui';
import type { Component } from 'svelte';

type Props = {
projectId: string;
displayName: string;
accounts: TAccount[];
preferredAccount: TAccount | undefined;
accountToString: (account: TAccount) => string;
stringToAccount: (value: string) => TAccount | null;
getUsername: (account: TAccount) => string;
updatePreferredAccount: (projectId: string, account: TAccount) => void;
AccountBadge: Component<{ account: TAccount; class?: string }>;
docsUrl: string;
requestType: 'pull request' | 'merge request';
};

const {
projectId,
displayName,
accounts,
preferredAccount,
accountToString,
stringToAccount,
getUsername,
updatePreferredAccount,
AccountBadge,
docsUrl,
requestType
}: Props = $props();

const { openGeneralSettings } = useSettingsModal();
const hasAccounts = $derived(accounts.length > 0 && preferredAccount);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasAccounts is derived from accounts.length > 0 && preferredAccount, which makes its type/value TAccount | false | undefined rather than a boolean. This works in {#if} but is harder to reason about and can confuse TS narrowing; consider deriving a boolean explicitly (e.g., accounts.length > 0 && preferredAccount !== undefined).

Suggested change
const hasAccounts = $derived(accounts.length > 0 && preferredAccount);
const hasAccounts = $derived(accounts.length > 0 && preferredAccount !== undefined);

Copilot uses AI. Check for mistakes.

function handleAccountChange(value: string) {
const parsedAccount = stringToAccount(value);
if (!parsedAccount) return;
updatePreferredAccount(projectId, parsedAccount);
}
</script>

<CardGroup.Item>
{#snippet title()}
{#if hasAccounts}
Configure {displayName} integration
{:else}
Connect your {displayName} account
{/if}
{/snippet}

{#snippet caption()}
Enable {requestType} creation. Read more in the <Link href={docsUrl}>docs</Link>
{/snippet}

{#if !hasAccounts}
<div class="flex">
<Button onclick={() => openGeneralSettings('integrations')} style="pop" icon="link"
>Set up in General Settings</Button
>
</div>
{:else}
{@const account = preferredAccount!}
{@const accountStr = accountToString(account)}
<Select
label="{displayName} account for this project"
value={accountStr}
options={accounts.map((acc) => ({
label: getUsername(acc),
value: accountToString(acc)
}))}
onselect={handleAccountChange}
disabled={accounts.length <= 1}
wide
>
{#snippet itemSnippet({ item, highlighted })}
{@const itemAccount = item.value && stringToAccount(item.value)}
<SelectItem selected={item.value === accountStr} {highlighted}>
{item.label}

{#if itemAccount}
<AccountBadge account={itemAccount} class="m-l-4" />
{/if}
</SelectItem>
{/snippet}
</Select>
{/if}
</CardGroup.Item>
237 changes: 70 additions & 167 deletions apps/desktop/src/components/ForgeForm.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import ForgeAccountConfig from '$components/ForgeAccountConfig.svelte';
import GitHubAccountBadge from '$components/GitHubAccountBadge.svelte';
import GitLabAccountBadge from '$components/GitLabAccountBadge.svelte';
import { DEFAULT_FORGE_FACTORY } from '$lib/forge/forgeFactory.svelte';
Expand All @@ -13,54 +14,41 @@
} from '$lib/forge/gitlab/gitlabUserService.svelte';
import { usePreferredGitLabUsername } from '$lib/forge/gitlab/hooks.svelte';
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
import { useSettingsModal } from '$lib/settings/settingsModal.svelte';
import { inject } from '@gitbutler/core/context';
import { reactive } from '@gitbutler/shared/reactiveUtils.svelte';
import { Button, CardGroup, InfoMessage, Link, Select, SelectItem } from '@gitbutler/ui';
import { CardGroup, Select, SelectItem } from '@gitbutler/ui';

import type { ForgeName } from '$lib/forge/interface/forge';
import type { Project } from '$lib/project/project';
import type { ButGitHubToken, ButGitLabToken } from '@gitbutler/core/api';

const FORGE_OPTIONS: { label: string; value: ForgeName }[] = [
{ label: 'None', value: 'default' },
{ label: 'GitHub', value: 'github' },
{ label: 'GitLab', value: 'gitlab' },
{ label: 'Azure', value: 'azure' },
{ label: 'BitBucket', value: 'bitbucket' }
];

const { projectId }: { projectId: string } = $props();

const forge = inject(DEFAULT_FORGE_FACTORY);
const projectsService = inject(PROJECTS_SERVICE);
const projectQuery = $derived(projectsService.getProject(projectId));
const project = $derived(projectQuery.response);

const selectedOption = $derived(project?.forge_override || 'default');

// GitHub hooks
const { preferredGitHubAccount, githubAccounts } = usePreferredGitHubUsername(
reactive(() => projectId)
);

// GitLab hooks
const { preferredGitLabAccount, gitlabAccounts } = usePreferredGitLabUsername(
reactive(() => projectId)
);

const { openGeneralSettings } = useSettingsModal();

const projectsService = inject(PROJECTS_SERVICE);
const projectQuery = $derived(projectsService.getProject(projectId));
const project = $derived(projectQuery.response);

const forgeOptions: { label: string; value: ForgeName }[] = [
{
label: 'None',
value: 'default'
},
{
label: 'GitHub',
value: 'github'
},
{
label: 'GitLab',
value: 'gitlab'
},
{
label: 'Azure',
value: 'azure'
},
{
label: 'BitBucket',
value: 'bitbucket'
}
];
let selectedOption = $derived(project?.forge_override || 'default');

function handleSelectionChange(selectedOption: ForgeName) {
if (!project) return;

Expand All @@ -73,6 +61,26 @@
}
projectsService.updateProject(mutableProject);
}

function updatePreferredGitHubAccount(
projectId: string,
account: ButGitHubToken.GithubAccountIdentifier
) {
projectsService.updatePreferredForgeUser(projectId, {
provider: 'github',
details: account
});
}

function updatePreferredGitLabAccount(
projectId: string,
account: ButGitLabToken.GitlabAccountIdentifier
) {
projectsService.updatePreferredForgeUser(projectId, {
provider: 'gitlab',
details: account
});
}
</script>

<CardGroup>
Expand Down Expand Up @@ -100,12 +108,9 @@
{#if forge.determinedForgeType === 'default'}
<Select
value={selectedOption}
options={forgeOptions}
options={FORGE_OPTIONS}
wide
onselect={(value) => {
selectedOption = value as ForgeName;
handleSelectionChange(selectedOption);
}}
onselect={(value) => handleSelectionChange(value as ForgeName)}
>
{#snippet itemSnippet({ item, highlighted })}
<SelectItem selected={item.value === selectedOption} {highlighted}>
Expand All @@ -116,137 +121,35 @@
{/if}
</CardGroup.Item>

{#if forge.current.name === 'gitlab'}
<CardGroup.Item>
{#snippet title()}
Configure GitLab integration
{/snippet}

{#snippet caption()}
Enable merge request creation. Read more in the <Link
href="https://docs.gitbutler.com/features/forge-integration/gitlab-integration">docs</Link
>
{/snippet}
{#if gitlabAccounts.current.length === 0 || !preferredGitLabAccount.current}
<InfoMessage style="warning" filled outlined={false}>
{#snippet title()}
No GitLab accounts found
{/snippet}
{#snippet content()}
Add a GitLab account in General Settings to enable GitLab integration
{/snippet}
</InfoMessage>
{@render openSettingsButton()}
{:else}
{@const account = preferredGitLabAccount.current}
<Select
label="GitLab account for this project"
value={gitlabAccountIdentifierToString(account)}
options={gitlabAccounts.current.map((account) => ({
label: account.info.username,
value: gitlabAccountIdentifierToString(account)
}))}
onselect={(value) => {
const account = stringToGitLabAccountIdentifier(value);
if (!account) return;
projectsService.updatePreferredForgeUser(projectId, {
provider: 'gitlab',
details: account
});
}}
disabled={gitlabAccounts.current.length <= 1}
wide
>
{#snippet itemSnippet({ item, highlighted })}
{@const itemAccount = item.value && stringToGitLabAccountIdentifier(item.value)}
<SelectItem
selected={item.value === gitlabAccountIdentifierToString(account)}
{highlighted}
>
{item.label}

{#if itemAccount}
<GitLabAccountBadge account={itemAccount} class="m-l-4" />
{/if}
</SelectItem>
{/snippet}
</Select>
{/if}
</CardGroup.Item>
{#if forge.current.name === 'github'}
<ForgeAccountConfig
{projectId}
displayName="GitHub"
accounts={githubAccounts.current}
preferredAccount={preferredGitHubAccount.current}
accountToString={githubAccountIdentifierToString}
stringToAccount={stringToGitHubAccountIdentifier}
getUsername={(account) => account.info.username}
updatePreferredAccount={updatePreferredGitHubAccount}
AccountBadge={GitHubAccountBadge}
docsUrl="https://docs.gitbutler.com/features/forge-integration/github-integration"
requestType="pull request"
/>
{/if}

{#if forge.current.name === 'github'}
<CardGroup.Item>
{#snippet title()}
Configure GitHub integration
{/snippet}

{#snippet caption()}
Enable pull request creation. Read more in the <Link
href="https://docs.gitbutler.com/features/forge-integration/github-integration">docs</Link
>
{/snippet}

{#if githubAccounts.current.length === 0 || !preferredGitHubAccount.current}
<InfoMessage style="warning" filled outlined={false}>
{#snippet title()}
No GitHub accounts found
{/snippet}
{#snippet content()}
Add a GitHub account in General Settings to enable GitHub integration
{/snippet}
</InfoMessage>
{@render openSettingsButton()}
{:else}
{@const account = preferredGitHubAccount.current}
<Select
label="GitHub account for this project"
value={githubAccountIdentifierToString(account)}
options={githubAccounts.current.map((account) => ({
label: account.info.username,
value: githubAccountIdentifierToString(account)
}))}
onselect={(value) => {
const account = stringToGitHubAccountIdentifier(value);
if (!account) return;
projectsService.updatePreferredForgeUser(projectId, {
provider: 'github',
details: account
});
}}
disabled={githubAccounts.current.length <= 1}
wide
>
{#snippet itemSnippet({ item, highlighted })}
{@const itemAccount = item.value && stringToGitHubAccountIdentifier(item.value)}
<SelectItem
selected={item.value === githubAccountIdentifierToString(account)}
{highlighted}
>
{item.label}

{#if itemAccount}
<GitHubAccountBadge account={itemAccount} class="m-l-4" />
{/if}
</SelectItem>
{/snippet}
</Select>
{/if}
</CardGroup.Item>
{#if forge.current.name === 'gitlab'}
<ForgeAccountConfig
{projectId}
displayName="GitLab"
accounts={gitlabAccounts.current}
preferredAccount={preferredGitLabAccount.current}
accountToString={gitlabAccountIdentifierToString}
stringToAccount={stringToGitLabAccountIdentifier}
getUsername={(account) => account.info.username}
updatePreferredAccount={updatePreferredGitLabAccount}
AccountBadge={GitLabAccountBadge}
docsUrl="https://docs.gitbutler.com/features/forge-integration/gitlab-integration"
requestType="merge request"
/>
{/if}
</CardGroup>

{#snippet openSettingsButton()}
<div class="forge-form__open-settings-container">
<Button onclick={() => openGeneralSettings('integrations')} style="pop"
>Go to General Settings</Button
>
</div>
{/snippet}

<style lang="scss">
.forge-form__open-settings-container {
display: flex;
justify-content: center;
}
</style>
Loading
Loading