From 5c28581269e768866dec69e9cda84cce2434d68b Mon Sep 17 00:00:00 2001 From: Pavel Laptev Date: Sat, 7 Feb 2026 00:37:18 +0100 Subject: [PATCH 1/4] feat(ui): simplify forge form account messaging and CTA Refactor ForgeForm UI to remove InfoMessage components and adjust titles and call-to-action for missing GitLab/GitHub accounts. Replace the verbose warning blocks with inline conditional titles ("No GitLab accounts found" /"No GitHub accounts found") and a unified "Set up in General Settings" button that uses an icon and concise label. Remove the dedicated open-settings container CSS and streamline markup by deleting InfoMessage wrappers and related styles. This makes the form copy more compact and consistent across forges, reduces duplicated markup, and emphasizes a single actionable path to open integrations settings. --- apps/desktop/src/components/ForgeForm.svelte | 44 +++++++------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/components/ForgeForm.svelte b/apps/desktop/src/components/ForgeForm.svelte index 9fcf82d909d..93f109d7fe2 100644 --- a/apps/desktop/src/components/ForgeForm.svelte +++ b/apps/desktop/src/components/ForgeForm.svelte @@ -16,7 +16,7 @@ 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 { Button, CardGroup, Link, Select, SelectItem } from '@gitbutler/ui'; import type { ForgeName } from '$lib/forge/interface/forge'; import type { Project } from '$lib/project/project'; @@ -119,7 +119,11 @@ {#if forge.current.name === 'gitlab'} {#snippet title()} - Configure GitLab integration + {#if gitlabAccounts.current.length === 0 || !preferredGitLabAccount.current} + No GitLab accounts found + {:else} + Configure GitLab integration + {/if} {/snippet} {#snippet caption()} @@ -127,15 +131,8 @@ href="https://docs.gitbutler.com/features/forge-integration/gitlab-integration">docs {/snippet} + {#if gitlabAccounts.current.length === 0 || !preferredGitLabAccount.current} - - {#snippet title()} - No GitLab accounts found - {/snippet} - {#snippet content()} - Add a GitLab account in General Settings to enable GitLab integration - {/snippet} - {@render openSettingsButton()} {:else} {@const account = preferredGitLabAccount.current} @@ -178,7 +175,11 @@ {#if forge.current.name === 'github'} {#snippet title()} - Configure GitHub integration + {#if githubAccounts.current.length === 0 || !preferredGitHubAccount.current} + No GitHub accounts found + {:else} + Configure GitHub integration + {/if} {/snippet} {#snippet caption()} @@ -188,14 +189,6 @@ {/snippet} {#if githubAccounts.current.length === 0 || !preferredGitHubAccount.current} - - {#snippet title()} - No GitHub accounts found - {/snippet} - {#snippet content()} - Add a GitHub account in General Settings to enable GitHub integration - {/snippet} - {@render openSettingsButton()} {:else} {@const account = preferredGitHubAccount.current} @@ -237,16 +230,9 @@ {#snippet openSettingsButton()} -
- +
{/snippet} - - From 0259d68ff18f9c752590e610edcb329a9d2dbad2 Mon Sep 17 00:00:00 2001 From: Pavel Laptev Date: Sat, 7 Feb 2026 01:17:14 +0100 Subject: [PATCH 2/4] feat(ui): consolidate forge options and extract account config - Replace duplicate forgeOptions with a single FORGE_OPTIONS const to avoid repetition and centralize available forge choices (None, GitHub, GitLab, Azure, BitBucket). - Add AccountIdentifier union type and getAccountUsername helper for consistent access to account usernames across providers. - Simplify Select onselect handler to call handleSelectionChange directly. - Extract repeated provider account configuration markup into a reusable forgeAccountConfig render invocation for GitLab (and begin same pattern for GitHub). This removes duplicated CardGroup.Item blocks and their internal select/item rendering logic. - Pass provider-specific utilities (account lists, conversion functions, badge component, docs URL and request type) into the reusable renderer so provider UI remains customizable. Motivation: reduce duplication, improve readability and maintainability, and make it easier to add or update forge provider UI in one place. --- apps/desktop/src/components/ForgeForm.svelte | 245 +++++++++---------- 1 file changed, 112 insertions(+), 133 deletions(-) diff --git a/apps/desktop/src/components/ForgeForm.svelte b/apps/desktop/src/components/ForgeForm.svelte index 93f109d7fe2..2c483f908ae 100644 --- a/apps/desktop/src/components/ForgeForm.svelte +++ b/apps/desktop/src/components/ForgeForm.svelte @@ -20,6 +20,23 @@ import type { ForgeName } from '$lib/forge/interface/forge'; import type { Project } from '$lib/project/project'; + import type { ButGitHubToken, ButGitLabToken } from '@gitbutler/core/api'; + + type AccountIdentifier = + | ButGitHubToken.GithubAccountIdentifier + | ButGitLabToken.GitlabAccountIdentifier; + + function getAccountUsername(account: AccountIdentifier): string { + return account.info.username; + } + + 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(); @@ -37,28 +54,6 @@ 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) { @@ -100,12 +95,9 @@ {#if forge.determinedForgeType === 'default'} ({ - 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)} - - {item.label} - - {#if itemAccount} - - {/if} - - {/snippet} - - {/if} -
+ {@render forgeAccountConfig({ + providerName: 'gitlab', + displayName: 'GitLab', + accounts: gitlabAccounts.current, + preferredAccount: preferredGitLabAccount.current, + accountToString: gitlabAccountIdentifierToString, + stringToAccount: stringToGitLabAccountIdentifier, + AccountBadge: GitLabAccountBadge, + docsUrl: 'https://docs.gitbutler.com/features/forge-integration/gitlab-integration', + requestType: 'merge request' + })} {/if} {#if forge.current.name === 'github'} - - {#snippet title()} - {#if githubAccounts.current.length === 0 || !preferredGitHubAccount.current} - No GitHub accounts found - {:else} - Configure GitHub integration - {/if} - {/snippet} - - {#snippet caption()} - Enable pull request creation. Read more in the docs - {/snippet} - - {#if githubAccounts.current.length === 0 || !preferredGitHubAccount.current} - {@render openSettingsButton()} - {:else} - {@const account = preferredGitHubAccount.current} - - {/if} - + {@render forgeAccountConfig({ + providerName: 'github', + displayName: 'GitHub', + accounts: githubAccounts.current, + preferredAccount: preferredGitHubAccount.current, + accountToString: githubAccountIdentifierToString, + stringToAccount: stringToGitHubAccountIdentifier, + AccountBadge: GitHubAccountBadge, + docsUrl: 'https://docs.gitbutler.com/features/forge-integration/github-integration', + requestType: 'pull request' + })} {/if} +{#snippet forgeAccountConfig({ + providerName, + displayName, + accounts, + preferredAccount, + accountToString, + stringToAccount, + AccountBadge, + docsUrl, + requestType +}: { + providerName: 'github' | 'gitlab'; + displayName: string; + accounts: AccountIdentifier[]; + preferredAccount: AccountIdentifier | undefined; + accountToString: (account: any) => string; + stringToAccount: (value: string) => any; + AccountBadge: typeof GitHubAccountBadge | typeof GitLabAccountBadge; + docsUrl: string; + requestType: string; +})} + + {#snippet title()} + {#if accounts.length === 0 || !preferredAccount} + No {displayName} accounts found + {:else} + Configure {displayName} integration + {/if} + {/snippet} + + {#snippet caption()} + Enable {requestType} creation. Read more in the docs + {/snippet} + + {#if accounts.length === 0 || !preferredAccount} + {@render openSettingsButton()} + {:else} + {@const account = preferredAccount} + + {/if} + +{/snippet} + {#snippet openSettingsButton()}
+
+ {:else} + {@const account = preferredAccount!} + {@const accountStr = accountToString(account)} + + {/if} +
diff --git a/apps/desktop/src/components/ForgeForm.svelte b/apps/desktop/src/components/ForgeForm.svelte index 9d2558fd162..69d8f6311f6 100644 --- a/apps/desktop/src/components/ForgeForm.svelte +++ b/apps/desktop/src/components/ForgeForm.svelte @@ -1,4 +1,5 @@ @@ -108,110 +121,35 @@ {/if} - {#if forge.current.name === 'gitlab'} - {@render forgeAccountConfig({ - providerName: 'gitlab', - displayName: 'GitLab', - accounts: gitlabAccounts.current, - preferredAccount: preferredGitLabAccount.current, - accountToString: gitlabAccountIdentifierToString, - stringToAccount: stringToGitLabAccountIdentifier, - AccountBadge: GitLabAccountBadge, - docsUrl: 'https://docs.gitbutler.com/features/forge-integration/gitlab-integration', - requestType: 'merge request' - })} + {#if forge.current.name === 'github'} + 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'} - {@render forgeAccountConfig({ - providerName: 'github', - displayName: 'GitHub', - accounts: githubAccounts.current, - preferredAccount: preferredGitHubAccount.current, - accountToString: githubAccountIdentifierToString, - stringToAccount: stringToGitHubAccountIdentifier, - AccountBadge: GitHubAccountBadge, - docsUrl: 'https://docs.gitbutler.com/features/forge-integration/github-integration', - requestType: 'pull request' - })} + {#if forge.current.name === 'gitlab'} + account.info.username} + updatePreferredAccount={updatePreferredGitLabAccount} + AccountBadge={GitLabAccountBadge} + docsUrl="https://docs.gitbutler.com/features/forge-integration/gitlab-integration" + requestType="merge request" + /> {/if} - -{#snippet forgeAccountConfig({ - providerName, - displayName, - accounts, - preferredAccount, - accountToString, - stringToAccount, - AccountBadge, - docsUrl, - requestType -}: { - providerName: 'github' | 'gitlab'; - displayName: string; - accounts: AccountIdentifier[]; - preferredAccount: AccountIdentifier | undefined; - accountToString: (account: any) => string; - stringToAccount: (value: string) => any; - AccountBadge: typeof GitHubAccountBadge | typeof GitLabAccountBadge; - docsUrl: string; - requestType: string; -})} - - {#snippet title()} - {#if accounts.length === 0 || !preferredAccount} - Connect your {displayName} account - {:else} - Configure {displayName} integration - {/if} - {/snippet} - - {#snippet caption()} - Enable {requestType} creation. Read more in the docs - {/snippet} - - {#if accounts.length === 0 || !preferredAccount} - {@render openSettingsButton()} - {:else} - {@const account = preferredAccount} - - {/if} - -{/snippet} - -{#snippet openSettingsButton()} -
- -
-{/snippet} diff --git a/apps/desktop/src/components/UnassignedViewForgePrompt.svelte b/apps/desktop/src/components/UnassignedViewForgePrompt.svelte index ac893810ff2..1c3f1479f78 100644 --- a/apps/desktop/src/components/UnassignedViewForgePrompt.svelte +++ b/apps/desktop/src/components/UnassignedViewForgePrompt.svelte @@ -6,8 +6,7 @@ availableForgeDocsLink, availableForgeLabel, availableForgeReviewUnit, - DEFAULT_FORGE_FACTORY, - type AvailableForge + DEFAULT_FORGE_FACTORY } from '$lib/forge/forgeFactory.svelte'; import { useSettingsModal } from '$lib/settings/settingsModal.svelte'; import { inject } from '@gitbutler/core/context'; @@ -19,7 +18,7 @@ const { projectId }: Props = $props(); - const { openGeneralSettings, openProjectSettings } = useSettingsModal(); + const { openGeneralSettings } = useSettingsModal(); const forgeFactory = inject(DEFAULT_FORGE_FACTORY); const dismissedTheIntegrationPrompt = $derived( persistedDismissedForgeIntegrationPrompt(projectId) @@ -48,15 +47,8 @@ return () => clearTimeout(timeoutId); }); - function configureIntegration(forge: AvailableForge): void { - switch (forge) { - case 'github': - openGeneralSettings('integrations'); - break; - case 'gitlab': - openProjectSettings(projectId); - break; - } + function configureIntegration(): void { + openGeneralSettings('integrations'); } function dismissPrompt() { @@ -82,9 +74,7 @@ {/if}