diff --git a/src/rovo-dev/api/extensionApi.ts b/src/rovo-dev/api/extensionApi.ts index bffcfb8fc..97eb24911 100644 --- a/src/rovo-dev/api/extensionApi.ts +++ b/src/rovo-dev/api/extensionApi.ts @@ -155,11 +155,11 @@ export class ExtensionApi { } }, - saveRovoDevAuthInfo: async (authInfo: any): Promise => { + saveRovoDevAuthInfo: async (authInfo: AuthInfo): Promise => { await Container.credentialManager.saveRovoDevAuthInfo(authInfo); }, - getRovoDevAuthInfo: async (): Promise => { + getRovoDevAuthInfo: async (): Promise => { return await Container.credentialManager.getRovoDevAuthInfo(); }, @@ -168,12 +168,12 @@ export class ExtensionApi { }, /** - * Get credential hints (host + email) from all configured Jira sites. + * Get credential hints (email) from all configured Jira sites. * Used for autocomplete suggestions in credential forms. * - * @returns Array of unique {host, email} combinations from all sites + * @returns Array of unique {email} objects from all sites */ - getCredentialHints: async (): Promise<{ host: string; email: string }[]> => { + getCredentialHints: async (): Promise<{ email: string }[]> => { const sites = Container.siteManager.getSitesAvailable(ProductJira); const credentialsPromises = sites.map(async (site) => { try { @@ -182,7 +182,6 @@ export class ExtensionApi { return null; } return { - host: site.host, email: authInfo.user.email, }; } catch { @@ -191,10 +190,10 @@ export class ExtensionApi { }); const allCredentials = await Promise.all(credentialsPromises); - const credentials = allCredentials.filter((c): c is { host: string; email: string } => c !== null); + const credentials = allCredentials.filter((c): c is { email: string } => c !== null); - // Remove duplicates by creating a unique key - return Array.from(new Map(credentials.map((c) => [`${c.host}-${c.email}`, c])).values()); + // Remove duplicates by email + return Array.from(new Map(credentials.map((c) => [c.email, c])).values()); }, }; diff --git a/src/rovo-dev/rovoDevAuthValidator.ts b/src/rovo-dev/rovoDevAuthValidator.ts index 1fc56ef83..b1cf2844d 100644 --- a/src/rovo-dev/rovoDevAuthValidator.ts +++ b/src/rovo-dev/rovoDevAuthValidator.ts @@ -1,8 +1,8 @@ -import { AuthInfoState } from 'src/atlclients/authInfo'; +import { AuthInfo, AuthInfoState } from 'src/atlclients/authInfo'; import { RovoDevLogger } from './util/rovoDevLogger'; -export interface RovoDevAuthInfo { +export interface RovoDevAuthInfo extends AuthInfo { user: { id: string; displayName: string; @@ -12,8 +12,7 @@ export interface RovoDevAuthInfo { state: AuthInfoState; username: string; password: string; - host: string; - cloudId: string; + isStaging?: boolean; } interface GraphQLUserInfo { @@ -25,30 +24,14 @@ interface GraphQLUserInfo { /** * Validates RovoDev credentials and creates an AuthInfo object with user information and cloud ID. * - * @param host - The Atlassian Cloud host (with or without protocol/trailing slashes) * @param email - The user's email address * @param apiToken - The API token for authentication * @returns Promise that resolves to AuthInfo if validation succeeds * @throws Error with descriptive message if validation fails */ -export async function createValidatedRovoDevAuthInfo( - host: string, - email: string, - apiToken: string, -): Promise { - // Normalize host to remove protocol and trailing slashes - const normalizedHost = host.replace(/^https?:\/\//, '').replace(/\/$/, ''); - - // Validate that it's an atlassian.net domain - if (!normalizedHost.endsWith('.atlassian.net')) { - throw new Error('Please enter a valid Atlassian Cloud site (*.atlassian.net)'); - } - +export async function createValidatedRovoDevAuthInfo(email: string, apiToken: string): Promise { // Fetch user information (validates credentials implicitly) - const userInfo = await fetchUserInfo(normalizedHost, email, apiToken); - - // Fetch cloud ID for the site - const cloudId = await fetchCloudId(normalizedHost); + const userInfo = await fetchUserInfo(email, apiToken); // Create and return AuthInfo with validated information return { @@ -61,8 +44,6 @@ export async function createValidatedRovoDevAuthInfo( state: AuthInfoState.Valid, username: email, password: apiToken, - host: normalizedHost, - cloudId: cloudId, }; } @@ -70,9 +51,11 @@ export async function createValidatedRovoDevAuthInfo( * Fetches user information from the GraphQL API using the provided credentials. * This is done mainly to validate that the credentials are correct. */ -async function fetchUserInfo(host: string, email: string, apiToken: string): Promise { +async function fetchUserInfo(email: string, apiToken: string): Promise { + // Use api.atlassian.com as the endpoint for authentication + // This works regardless of the user's specific site try { - const response = await fetch(`https://${host}/gateway/api/graphql`, { + const response = await fetch(`https://api.atlassian.com/graphql`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -127,27 +110,3 @@ async function fetchUserInfo(host: string, email: string, apiToken: string): Pro throw new Error('Network error occurred while validating credentials. Please check your connection.'); } } - -/** - * Fetches the cloud ID for the given Atlassian Cloud host. - */ -async function fetchCloudId(host: string): Promise { - try { - const response = await fetch(`https://${host}/_edge/tenant_info`); - if (!response.ok) { - RovoDevLogger.error(new Error(`HTTP ${response.status}`), 'Failed to fetch cloud ID'); - throw new Error('Failed to retrieve site information. Please try again.'); - } - const data = await response.json(); - if (!data.cloudId) { - throw new Error('Site information does not contain cloud ID.'); - } - return data.cloudId; - } catch (error) { - if (error instanceof Error && error.message.includes('Site information')) { - throw error; - } - RovoDevLogger.error(error, 'Error fetching cloud ID'); - throw new Error('Failed to retrieve site information. Please check your connection and try again.'); - } -} diff --git a/src/rovo-dev/rovoDevLanguageServerProvider.test.ts b/src/rovo-dev/rovoDevLanguageServerProvider.test.ts index ae89cc451..75492b4ec 100644 --- a/src/rovo-dev/rovoDevLanguageServerProvider.test.ts +++ b/src/rovo-dev/rovoDevLanguageServerProvider.test.ts @@ -154,7 +154,6 @@ describe('RovoDevLanguageServerProvider', () => { // Default to Started state so LSP can start mockCurrentState = { state: 'Started', - jiraSiteHostname: 'test.atlassian.net', jiraSiteUserInfo: { id: '123', displayName: 'Test User', email: 'test@example.com', avatarUrl: '' }, pid: 12345, hostname: '127.0.0.1', @@ -199,7 +198,6 @@ describe('RovoDevLanguageServerProvider', () => { // Simulate state change to Started setMockProcessState({ state: 'Started', - jiraSiteHostname: 'test.atlassian.net', jiraSiteUserInfo: { id: '123', displayName: 'Test User', email: 'test@example.com', avatarUrl: '' }, pid: 12345, hostname: '127.0.0.1', diff --git a/src/rovo-dev/rovoDevProcessManager.ts b/src/rovo-dev/rovoDevProcessManager.ts index 32fa7dcfa..c728b3bee 100644 --- a/src/rovo-dev/rovoDevProcessManager.ts +++ b/src/rovo-dev/rovoDevProcessManager.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import net from 'net'; import packageJson from 'package.json'; import path from 'path'; -import { AuthInfoState } from 'src/atlclients/authInfo'; +import { AuthInfoState, BasicAuthInfo } from 'src/atlclients/authInfo'; import { UserInfo } from 'src/rovo-dev/api/extensionApiTypes'; import { downloadAndUnzip } from 'src/rovo-dev/util/downloadFile'; import { getFsPromise } from 'src/rovo-dev/util/fsPromises'; @@ -14,7 +14,8 @@ import { Disposable, Event, EventEmitter, ExtensionContext, Uri, workspace } fro import { FeatureFlagClient } from '../util/featureFlags/featureFlagClient'; import { Features } from '../util/features'; -import { ExtensionApi, ValidBasicAuthSiteData } from './api/extensionApi'; +import { ExtensionApi } from './api/extensionApi'; +import { RovoDevAuthInfo } from './rovoDevAuthValidator'; import { RovoDevDisabledReason, RovoDevEntitlementCheckFailedDetail } from './rovoDevWebviewProviderMessages'; import { RovoDevLogger } from './util/rovoDevLogger'; @@ -112,7 +113,7 @@ async function getOrAssignPortForWorkspace(): Promise { throw new Error('unable to find an available port.'); } -function areCredentialsEqual(cred1?: ValidBasicAuthSiteData, cred2?: ValidBasicAuthSiteData) { +function areCredentialsEqual(cred1?: RovoDevAuthInfo, cred2?: RovoDevAuthInfo) { if (cred1 === cred2) { return true; } @@ -121,11 +122,7 @@ function areCredentialsEqual(cred1?: ValidBasicAuthSiteData, cred2?: ValidBasicA return false; } - return ( - cred1.host === cred2.host && - cred1.authInfo.password === cred2.authInfo.password && - cred1.authInfo.username === cred2.authInfo.username - ); + return cred1.password === cred2.password && cred1.username === cred2.username; } export interface RovoDevProcessNotStartedState { @@ -134,7 +131,6 @@ export interface RovoDevProcessNotStartedState { export interface RovoDevProcessDownloadingState { state: 'Downloading'; - jiraSiteHostname: string; jiraSiteUserInfo: UserInfo; totalBytes: number; downloadedBytes: number; @@ -142,13 +138,11 @@ export interface RovoDevProcessDownloadingState { export interface RovoDevProcessStartingState { state: 'Starting'; - jiraSiteHostname: string; jiraSiteUserInfo: UserInfo; } export interface RovoDevProcessStartedState { state: 'Started'; - jiraSiteHostname: string; jiraSiteUserInfo: UserInfo; pid: number | undefined; hostname: string; @@ -204,7 +198,7 @@ export abstract class RovoDevProcessManager { return this._onStateChanged.event; } - private static currentCredentials: ValidBasicAuthSiteData | undefined; + private static currentCredentials: RovoDevAuthInfo | undefined; private static extensionApi: ExtensionApi = new ExtensionApi(); /** This lock ensures this class is async-safe, preventing repeated invocations @@ -242,7 +236,7 @@ export abstract class RovoDevProcessManager { this.stopRovoDevInstance(); } - private static async downloadBinaryThenInitialize(credentials: ValidBasicAuthSiteData, rovoDevURIs: RovoDevURIs) { + private static async downloadBinaryThenInitialize(credentials: RovoDevAuthInfo, rovoDevURIs: RovoDevURIs) { const baseDir = rovoDevURIs.RovoDevBaseDir; const versionDir = rovoDevURIs.RovoDevVersionDir; const zipUrl = rovoDevURIs.RovoDevZipUrl; @@ -259,8 +253,7 @@ export abstract class RovoDevProcessManager { // and we want to show 0% downloaded this.setState({ state: 'Downloading', - jiraSiteHostname: credentials.host, - jiraSiteUserInfo: credentials.authInfo.user, + jiraSiteUserInfo: credentials.user, totalBytes: 1, downloadedBytes: 0, }); @@ -273,8 +266,7 @@ export abstract class RovoDevProcessManager { if (totalBytes) { this.setState({ state: 'Downloading', - jiraSiteHostname: credentials.host, - jiraSiteUserInfo: credentials.authInfo.user, + jiraSiteUserInfo: credentials.user, totalBytes, downloadedBytes, }); @@ -287,14 +279,13 @@ export abstract class RovoDevProcessManager { this.setState({ state: 'Starting', - jiraSiteHostname: credentials.host, - jiraSiteUserInfo: credentials.authInfo.user, + jiraSiteUserInfo: credentials.user, }); } private static async internalInitializeRovoDev( context: ExtensionContext, - credentials: ValidBasicAuthSiteData | undefined, + credentials: RovoDevAuthInfo | undefined, forceNewInstance?: boolean, ) { if (!workspace.workspaceFolders?.length) { @@ -319,7 +310,7 @@ export abstract class RovoDevProcessManager { subState: 'NeedAuth', }); return; - } else if (!credentials.isValid) { + } else if (credentials.state !== AuthInfoState.Valid) { this.setState({ state: 'Disabled', subState: 'UnauthorizedAuth', @@ -329,8 +320,7 @@ export abstract class RovoDevProcessManager { this.setState({ state: 'Starting', - jiraSiteHostname: credentials.host, - jiraSiteUserInfo: credentials.authInfo.user, + jiraSiteUserInfo: credentials.user, }); let rovoDevURIs: ReturnType; @@ -404,36 +394,58 @@ export abstract class RovoDevProcessManager { } } - private static async getCredentials(): Promise { + private static async getCredentials(): Promise { // Check if dedicated RovoDev auth feature flag is enabled const featureFlagClient = FeatureFlagClient.getInstance(); if (featureFlagClient.checkGate(Features.DedicatedRovoDevAuth)) { // Use dedicated RovoDev credentials - const rovoDevAuth = await RovoDevProcessManager.extensionApi.auth.getRovoDevAuthInfo(); + const rovoDevAuth = (await RovoDevProcessManager.extensionApi.auth.getRovoDevAuthInfo()) as + | BasicAuthInfo + | undefined; // If we have RovoDev credentials, use them - if (rovoDevAuth && rovoDevAuth.host) { + if (rovoDevAuth) { // Convert RovoDev auth to ValidBasicAuthSiteData format + // Use a placeholder host since RovoDev doesn't require it return { - host: rovoDevAuth.host, - authInfo: rovoDevAuth, - isValid: rovoDevAuth.state === AuthInfoState.Valid, - isStaging: false, - siteCloudId: rovoDevAuth.cloudId, + user: rovoDevAuth.user, + state: rovoDevAuth.state, + username: rovoDevAuth.username, + password: rovoDevAuth.password, }; } // If RequireDedicatedRovoDevAuth is disabled and no RovoDev credentials exist, // fall back to Jira credentials for migration if (!featureFlagClient.checkGate(Features.RequireDedicatedRovoDevAuth)) { - return await RovoDevProcessManager.extensionApi.auth.getCloudPrimaryAuthSite(); + const creds = await RovoDevProcessManager.extensionApi.auth.getCloudPrimaryAuthSite(); + if (!creds) { + return undefined; + } + return { + user: creds.authInfo.user, + state: creds.authInfo.state, + username: creds.authInfo.username, + password: creds.authInfo.password, + isStaging: creds.isStaging, + }; } return undefined; } else { // Fall back to primary Jira auth - return await RovoDevProcessManager.extensionApi.auth.getCloudPrimaryAuthSite(); + const creds = await RovoDevProcessManager.extensionApi.auth.getCloudPrimaryAuthSite(); + if (!creds) { + return undefined; + } + return { + user: creds.authInfo.user, + state: creds.authInfo.state, + username: creds.authInfo.username, + password: creds.authInfo.password, + isStaging: creds.isStaging, + }; } } @@ -455,7 +467,7 @@ export abstract class RovoDevProcessManager { private static async startRovoDev( context: ExtensionContext, - credentials: ValidBasicAuthSiteData, + credentials: RovoDevAuthInfo, rovoDevURIs: RovoDevURIs, ) { // skip if there is no workspace folder open @@ -489,10 +501,7 @@ class RovoDevSubprocessInstance extends Disposable { super(() => this.stop()); } - public async start( - credentials: ValidBasicAuthSiteData, - setState: (newState: RovoDevProcessState) => void, - ): Promise { + public async start(credentials: RovoDevAuthInfo, setState: (newState: RovoDevProcessState) => void): Promise { if (this.started) { throw new Error('Instance already started'); } @@ -511,14 +520,11 @@ class RovoDevSubprocessInstance extends Disposable { } try { - const siteUrl = `https://${credentials.host}`; const shellArgs = [ 'serve', `${port}`, '--xid', 'rovodev-ide-vscode', - '--site-url', - siteUrl, '--respect-configured-permissions', ]; @@ -535,18 +541,17 @@ class RovoDevSubprocessInstance extends Disposable { env: { ...process.env, USER: process.env.USER || process.env.USERNAME, - USER_EMAIL: credentials.authInfo.username, + USER_EMAIL: credentials.username, ROVODEV_SANDBOX_ID: this.extensionApi.metadata.appInstanceId(), ROVODEV_SERVE_SESSION_TOKEN: sessionToken, - ...(credentials.authInfo.password ? { USER_API_TOKEN: credentials.authInfo.password } : {}), + ...(credentials.password ? { USER_API_TOKEN: credentials.password } : {}), }, }) .on('spawn', () => { const timeStarted = new Date(); setState({ state: 'Started', - jiraSiteHostname: credentials.host, - jiraSiteUserInfo: credentials.authInfo.user, + jiraSiteUserInfo: credentials.user, pid: this.rovoDevProcess?.pid, hostname: RovoDevInfo.hostname, httpPort: port, diff --git a/src/rovo-dev/rovoDevWebviewProvider.ts b/src/rovo-dev/rovoDevWebviewProvider.ts index 00337cea7..69866f22b 100644 --- a/src/rovo-dev/rovoDevWebviewProvider.ts +++ b/src/rovo-dev/rovoDevWebviewProvider.ts @@ -476,7 +476,7 @@ export class RovoDevWebviewProvider extends Disposable implements WebviewViewPro break; case RovoDevViewResponseType.SubmitRovoDevAuth: - await this.handleRovoDevAuth(e.host, e.email, e.apiToken); + await this.handleRovoDevAuth(e.email, e.apiToken); break; case RovoDevViewResponseType.OpenFolder: @@ -845,7 +845,7 @@ export class RovoDevWebviewProvider extends Disposable implements WebviewViewPro } } - private async handleRovoDevAuth(host: string, email: string, apiToken: string): Promise { + private async handleRovoDevAuth(email: string, apiToken: string): Promise { const webview = this._webView!; try { @@ -855,7 +855,7 @@ export class RovoDevWebviewProvider extends Disposable implements WebviewViewPro }); // Validate credentials and create AuthInfo (will throw on failure) - const authInfo = await createValidatedRovoDevAuthInfo(host, email, apiToken); + const authInfo = await createValidatedRovoDevAuthInfo(email, apiToken); // Save to RovoDev credential store await this.extensionApi.auth.saveRovoDevAuthInfo(authInfo); @@ -1218,7 +1218,7 @@ export class RovoDevWebviewProvider extends Disposable implements WebviewViewPro private async handleProcessStateChanged(newState: RovoDevProcessState) { if (newState.state === 'Downloading' || newState.state === 'Starting' || newState.state === 'Started') { this._userInfo = newState.jiraSiteUserInfo; - this._jiraItemsProvider.setJiraSite(newState.jiraSiteHostname); + // this._jiraItemsProvider.setJiraSite(newState.jiraSiteHostname); if (this._webviewReady) { // refresh the isAtlassianUser flag diff --git a/src/rovo-dev/rovoDevWebviewProviderMessages.ts b/src/rovo-dev/rovoDevWebviewProviderMessages.ts index b62a208e3..3ed411ef7 100644 --- a/src/rovo-dev/rovoDevWebviewProviderMessages.ts +++ b/src/rovo-dev/rovoDevWebviewProviderMessages.ts @@ -113,10 +113,7 @@ export type RovoDevProviderMessage = RovoDevProviderMessageType.SetJiraWorkItems, { issues: MinimalIssue[] | undefined } > - | ReducerAction< - RovoDevProviderMessageType.SetExistingJiraCredentials, - { credentials: { host: string; email: string }[] } - > + | ReducerAction | ReducerAction< RovoDevProviderMessageType.CheckFileExistsComplete, { requestId: string; filePath: string; exists: boolean } diff --git a/src/rovo-dev/ui/landing-page/RovoDevLanding.tsx b/src/rovo-dev/ui/landing-page/RovoDevLanding.tsx index 18bf98afa..e328802e9 100644 --- a/src/rovo-dev/ui/landing-page/RovoDevLanding.tsx +++ b/src/rovo-dev/ui/landing-page/RovoDevLanding.tsx @@ -32,7 +32,7 @@ export const RovoDevLanding: React.FC<{ currentState: State; isHistoryEmpty: boolean; onLoginClick: (openApiTokenLogin: boolean) => void; - onRovoDevAuthSubmit: (host: string, email: string, apiToken: string) => void; + onRovoDevAuthSubmit: (email: string, apiToken: string) => void; onOpenFolder: () => void; onMcpChoice: (choice: McpConsentChoice, serverName?: string) => void; setPromptText: (context: string) => void; diff --git a/src/rovo-dev/ui/landing-page/disabled-messages/DisabledMessage.tsx b/src/rovo-dev/ui/landing-page/disabled-messages/DisabledMessage.tsx index 1560bbdc6..010dec98d 100644 --- a/src/rovo-dev/ui/landing-page/disabled-messages/DisabledMessage.tsx +++ b/src/rovo-dev/ui/landing-page/disabled-messages/DisabledMessage.tsx @@ -21,7 +21,7 @@ const loginFormContainerStyles: React.CSSProperties = { export const DisabledMessage: React.FC<{ currentState: State; onLoginClick: (openApiTokenLogin: boolean) => void; - onRovoDevAuthSubmit: (host: string, email: string, apiToken: string) => void; + onRovoDevAuthSubmit: (email: string, apiToken: string) => void; onOpenFolder: () => void; onLinkClick: (url: string) => void; onMcpChoice: (choice: McpConsentChoice, serverName?: string) => void; @@ -44,8 +44,8 @@ export const DisabledMessage: React.FC<{
Sign in to Rovo Dev with an API token
{ - onRovoDevAuthSubmit(host, email, apiToken); + onSubmit={(email, apiToken) => { + onRovoDevAuthSubmit(email, apiToken); }} credentialHints={credentialHints} /> diff --git a/src/rovo-dev/ui/landing-page/disabled-messages/RovoDevLoginForm.test.tsx b/src/rovo-dev/ui/landing-page/disabled-messages/RovoDevLoginForm.test.tsx deleted file mode 100644 index e496fcbfa..000000000 --- a/src/rovo-dev/ui/landing-page/disabled-messages/RovoDevLoginForm.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react'; -import React from 'react'; -import { act } from 'react-dom/test-utils'; - -import { RovoDevLoginForm } from './RovoDevLoginForm'; - -// A minimal mock for Atlaskit CreatableSelect to allow us to test Tab selection behavior. -jest.mock('@atlaskit/select', () => ({ - __esModule: true, - CreatableSelect: React.forwardRef( - ({ inputId, inputValue, onInputChange, onChange, onKeyDown, isDisabled, tabSelectsValue }: any, ref: any) => { - const handleKeyDown = (e: any) => { - // Simulate tabSelectsValue: when Tab is pressed with input, trigger onChange - if (e.key === 'Tab' && tabSelectsValue && inputValue?.trim()) { - onChange?.({ label: inputValue.trim(), value: inputValue.trim() }); - } - // Also call the component's onKeyDown for focus management - onKeyDown?.(e); - }; - - return ( - onInputChange?.(e.target.value)} - onKeyDown={handleKeyDown} - /> - ); - }, - ), -})); - -jest.mock('@atlaskit/button/new', () => ({ - __esModule: true, - default: ({ children, isDisabled, ...props }: any) => ( - - ), -})); - -jest.mock('@atlaskit/textfield', () => ({ - __esModule: true, - default: React.forwardRef(({ isDisabled, ...props }: any, ref: any) => ( - - )), -})); - -describe('RovoDevLoginForm', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it('commits typed host value when tabbing out (AXON-1804)', () => { - const onSubmit = jest.fn(); - render(); - - const hostInput = screen.getByTestId('host') as HTMLInputElement; - const emailInput = screen.getByTestId('email') as HTMLInputElement; - const apiTokenInput = document.getElementById('apiToken') as HTMLInputElement; - - fireEvent.change(hostInput, { target: { value: 'my-site.atlassian.net' } }); - - // Pressing Tab should commit the typed value and move focus to email. - fireEvent.keyDown(hostInput, { key: 'Tab' }); - act(() => { - jest.runOnlyPendingTimers(); - }); - - expect(document.activeElement).toBe(emailInput); - - // The sign-in button should still be disabled until email/token are set, but the host should now be committed. - // We verify indirectly by setting email/token and submitting. - fireEvent.change(emailInput, { target: { value: 'a@b.com' } }); - fireEvent.keyDown(emailInput, { key: 'Tab' }); - act(() => { - jest.runOnlyPendingTimers(); - }); - - expect(document.activeElement).toBe(apiTokenInput); - - fireEvent.change(apiTokenInput, { target: { value: 'token' } }); - - fireEvent.click(screen.getByRole('button', { name: 'Sign In' })); - - expect(onSubmit).toHaveBeenCalledWith('my-site.atlassian.net', 'a@b.com', 'token'); - }); -}); diff --git a/src/rovo-dev/ui/landing-page/disabled-messages/RovoDevLoginForm.tsx b/src/rovo-dev/ui/landing-page/disabled-messages/RovoDevLoginForm.tsx index 7efd4c812..47387663f 100644 --- a/src/rovo-dev/ui/landing-page/disabled-messages/RovoDevLoginForm.tsx +++ b/src/rovo-dev/ui/landing-page/disabled-messages/RovoDevLoginForm.tsx @@ -55,17 +55,13 @@ const textFieldStyles: React.CSSProperties = { }; export interface CredentialHint { - host: string; email: string; } export const RovoDevLoginForm: React.FC<{ - onSubmit: (host: string, email: string, apiToken: string) => void; + onSubmit: (email: string, apiToken: string) => void; credentialHints?: CredentialHint[]; }> = ({ onSubmit, credentialHints = [] }) => { - const [host, setHost] = React.useState(''); - const [hostInputValue, setHostInputValue] = React.useState(''); - const [email, setEmail] = React.useState(''); const [emailInputValue, setEmailInputValue] = React.useState(''); @@ -75,7 +71,6 @@ export const RovoDevLoginForm: React.FC<{ error?: string; }>({ isValidating: false }); - const hostSelectRef = React.useRef(null); const emailSelectRef = React.useRef(null); const apiTokenInputRef = React.useRef(null); @@ -101,18 +96,14 @@ export const RovoDevLoginForm: React.FC<{ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!host || !email || !apiToken) { + if (!email || !apiToken) { return; } - onSubmit(host, email, apiToken); + onSubmit(email, apiToken); }; - // Create unique host and email options from existing credentials - const hostOptions = React.useMemo( - () => Array.from(new Set(credentialHints.map((c) => c.host))).map((h) => ({ label: h, value: h })), - [credentialHints], - ); + // Create unique email options from existing credentials const emailOptions = React.useMemo( () => Array.from(new Set(credentialHints.map((c) => c.email))).map((e) => ({ label: e, value: e })), [credentialHints], @@ -136,49 +127,6 @@ export const RovoDevLoginForm: React.FC<{ {authValidationState.error}
)} -
- - { - setHostInputValue(newValue); - }} - onKeyDown={(e: React.KeyboardEvent) => { - // Move focus to next field on Tab - if (e.key === 'Tab' && !e.shiftKey) { - setTimeout(() => emailSelectRef.current?.focus(), 0); - } - }} - onBlur={() => { - // If the user clicks away, keep what they typed. - if (hostInputValue.trim().length > 0) { - setHost(hostInputValue.trim()); - setHostInputValue(''); - } - }} - onChange={(option: any) => { - setHost(option?.value || ''); - setHostInputValue(''); - if (option?.value) { - setTimeout(() => emailSelectRef.current?.focus(), 0); - } - }} - formatCreateLabel={(inputValue: any) => `Use "${inputValue}"`} - styles={selectStyles} - menuPortalTarget={document.body} - /> -
-