From b723fc5365afbc53f825cadbd9baa2ffa7b52119 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Fri, 10 Jul 2026 16:49:36 +0200 Subject: [PATCH 1/2] refactor(login): organize login routes and components by forge --- src/renderer/App.tsx | 22 +- .../LoginWithPersonalAccessTokenForm.tsx} | 106 +++-- src/renderer/routes/Accounts.test.tsx | 125 ++++- src/renderer/routes/Accounts.tsx | 123 ++--- src/renderer/routes/Login.test.tsx | 14 +- src/renderer/routes/Login.tsx | 12 +- .../LoginWithPersonalAccessToken.test.tsx | 88 ++++ .../gitea/LoginWithPersonalAccessToken.tsx | 16 + ...LoginWithPersonalAccessToken.test.tsx.snap | 439 ++++++++++++++++++ .../{ => github}/LoginWithDeviceFlow.test.tsx | 22 +- .../{ => github}/LoginWithDeviceFlow.tsx | 36 +- .../{ => github}/LoginWithOAuthApp.test.tsx | 28 +- .../routes/{ => github}/LoginWithOAuthApp.tsx | 32 +- .../LoginWithPersonalAccessToken.test.tsx | 38 +- .../github/LoginWithPersonalAccessToken.tsx | 30 ++ .../LoginWithOAuthApp.test.tsx.snap | 2 +- ...LoginWithPersonalAccessToken.test.tsx.snap | 2 +- .../utils/forges/gitea/adapter.test.ts | 4 +- src/renderer/utils/forges/gitea/adapter.ts | 4 +- src/renderer/utils/forges/github/adapter.ts | 11 +- src/renderer/utils/forges/types.ts | 20 +- 21 files changed, 906 insertions(+), 268 deletions(-) rename src/renderer/{routes/LoginWithPersonalAccessToken.tsx => components/login/LoginWithPersonalAccessTokenForm.tsx} (70%) create mode 100644 src/renderer/routes/gitea/LoginWithPersonalAccessToken.test.tsx create mode 100644 src/renderer/routes/gitea/LoginWithPersonalAccessToken.tsx create mode 100644 src/renderer/routes/gitea/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap rename src/renderer/routes/{ => github}/LoginWithDeviceFlow.test.tsx (87%) rename src/renderer/routes/{ => github}/LoginWithDeviceFlow.tsx (87%) rename src/renderer/routes/{ => github}/LoginWithOAuthApp.test.tsx (84%) rename src/renderer/routes/{ => github}/LoginWithOAuthApp.tsx (89%) rename src/renderer/routes/{ => github}/LoginWithPersonalAccessToken.test.tsx (78%) create mode 100644 src/renderer/routes/github/LoginWithPersonalAccessToken.tsx rename src/renderer/routes/{ => github}/__snapshots__/LoginWithOAuthApp.test.tsx.snap (99%) rename src/renderer/routes/{ => github}/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap (99%) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a83973fb1..1834213f0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -7,10 +7,11 @@ import { useAppContext } from './hooks/useAppContext'; import { AccountsRoute } from './routes/Accounts'; import { AccountScopesRoute } from './routes/AccountScopes'; import { FiltersRoute } from './routes/Filters'; +import { GiteaLoginWithPersonalAccessTokenRoute } from './routes/gitea/LoginWithPersonalAccessToken'; +import { GitHubLoginWithDeviceFlowRoute } from './routes/github/LoginWithDeviceFlow'; +import { GitHubLoginWithOAuthAppRoute } from './routes/github/LoginWithOAuthApp'; +import { GitHubLoginWithPersonalAccessTokenRoute } from './routes/github/LoginWithPersonalAccessToken'; import { LoginRoute } from './routes/Login'; -import { LoginWithDeviceFlowRoute } from './routes/LoginWithDeviceFlow'; -import { LoginWithOAuthAppRoute } from './routes/LoginWithOAuthApp'; -import { LoginWithPersonalAccessTokenRoute } from './routes/LoginWithPersonalAccessToken'; import { NotificationsRoute } from './routes/Notifications'; import { SettingsRoute } from './routes/Settings'; @@ -77,12 +78,19 @@ export const App = () => { path="/account-scopes" /> } path="/login" /> - } path="/login-device-flow" /> } - path="/login-personal-access-token" + element={} + path="/login/github/device-flow" + /> + } + path="/login/github/personal-access-token" + /> + } path="/login/github/oauth-app" /> + } + path="/login/gitea/personal-access-token" /> - } path="/login-oauth-app" /> diff --git a/src/renderer/routes/LoginWithPersonalAccessToken.tsx b/src/renderer/components/login/LoginWithPersonalAccessTokenForm.tsx similarity index 70% rename from src/renderer/routes/LoginWithPersonalAccessToken.tsx rename to src/renderer/components/login/LoginWithPersonalAccessTokenForm.tsx index 67525dd0a..c331a21ea 100644 --- a/src/renderer/routes/LoginWithPersonalAccessToken.tsx +++ b/src/renderer/components/login/LoginWithPersonalAccessTokenForm.tsx @@ -1,26 +1,24 @@ -import { type FC, useCallback, useState } from 'react'; +import { type FC, type ReactNode, useCallback, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { BookIcon, EyeClosedIcon, EyeIcon, KeyIcon, SignInIcon } from '@primer/octicons-react'; import { Banner, Button, FormControl, Stack, Text, TextInput, Tooltip } from '@primer/react'; -import { useAppContext } from '../hooks/useAppContext'; +import { useAppContext } from '../../hooks/useAppContext'; -import { Contents } from '../components/layout/Contents'; -import { Page } from '../components/layout/Page'; -import { Footer } from '../components/primitives/Footer'; -import { Header } from '../components/primitives/Header'; +import { Contents } from '../layout/Contents'; +import { Page } from '../layout/Page'; +import { Footer } from '../primitives/Footer'; +import { Header } from '../primitives/Header'; -import type { Account, Forge, Hostname, Token } from '../types'; -import type { LoginRouteState } from '../utils/forges/types'; +import type { Account, Forge, Hostname, Token } from '../../types'; -import { formatRecommendedOAuthScopes } from '../utils/auth/scopes'; -import { isValidHostname } from '../utils/auth/utils'; -import { rendererLogError, toError } from '../utils/core/logger'; -import { getAdapter } from '../utils/forges/registry'; -import { openExternalLink } from '../utils/system/comms'; +import { isValidHostname } from '../../utils/auth/utils'; +import { rendererLogError, toError } from '../../utils/core/logger'; +import { getAdapter } from '../../utils/forges/registry'; +import { openExternalLink } from '../../utils/system/comms'; -interface LocationState extends LoginRouteState { +interface LocationState { account?: Account; } @@ -54,14 +52,46 @@ export const validateForm = (values: IFormData, forge: Forge = 'github'): IFormE return errors; }; -export const LoginWithPersonalAccessTokenRoute: FC = () => { +export interface LoginWithPersonalAccessTokenFormProps { + forge: Forge; + /** Page header title. */ + title: string; + /** Caption below the hostname label. */ + hostnameCaption: string; + hostnamePlaceholder: string; + /** Label for the button that opens the forge's token settings page. */ + tokenSettingsLabel: string; + /** Text rendered beside the token settings button. */ + tokenSettingsCaption: string; + tokenPlaceholder: string; + /** Tooltip for the documentation button. */ + docsTooltip: string; + /** Forge-specific content rendered below the token settings row (e.g. scope hints). */ + children?: ReactNode; +} + +/** + * Shared personal-access-token login form. + * + * Forge-specific route components (`routes/github`, `routes/gitea`) supply + * their own copy via props so this form stays free of forge conditionals. + */ +export const LoginWithPersonalAccessTokenForm: FC = ({ + forge, + title, + hostnameCaption, + hostnamePlaceholder, + tokenSettingsLabel, + tokenSettingsCaption, + tokenPlaceholder, + docsTooltip, + children, +}) => { const navigate = useNavigate(); const location = useLocation(); - const { account: reAuthAccount, forge: stateForge } = (location.state ?? {}) as LocationState; + const { account: reAuthAccount } = (location.state ?? {}) as LocationState; - const forge: Forge = reAuthAccount?.forge ?? stateForge ?? 'github'; const adapter = getAdapter(forge); - const isGitea = forge === 'gitea'; const { loginWithPersonalAccessToken } = useAppContext(); @@ -116,9 +146,7 @@ export const LoginWithPersonalAccessTokenRoute: FC = () => { return ( -
- {isGitea ? 'Login to Gitea with Personal Access Token' : 'Login with Personal Access Token'} -
+
{title}
{errors.invalidCredentialsForHost && ( @@ -140,11 +168,7 @@ export const LoginWithPersonalAccessTokenRoute: FC = () => { Hostname - - {isGitea - ? 'Your Gitea instance hostname (for example gitea.example.com)' - : 'Change only if you are using GitHub Enterprise Server'} - + {hostnameCaption} { data-testid="login-hostname" name="hostname" onChange={handleInputChange} - placeholder={isGitea ? 'gitea.example.com' : 'github.com'} + placeholder={hostnamePlaceholder} value={formData.hostname} /> {errors.hostname && ( @@ -171,26 +195,12 @@ export const LoginWithPersonalAccessTokenRoute: FC = () => { } size="small" > - {isGitea ? 'Open token settings' : 'Generate a PAT'} + {tokenSettingsLabel} - - {isGitea - ? 'on your Gitea instance to create a token, then paste it below.' - : 'on GitHub to paste the token below.'} - + {tokenSettingsCaption} - {!isGitea && ( - - The{' '} - - - {' '} - will be automatically selected for you. - - )} + {children} @@ -202,11 +212,7 @@ export const LoginWithPersonalAccessTokenRoute: FC = () => { data-testid="login-token" name="token" onChange={handleInputChange} - placeholder={ - isGitea - ? 'Your Gitea personal access token' - : 'Your generated token (40 characters)' - } + placeholder={tokenPlaceholder} trailingAction={ {