fix(web): require hub URL without clearing input

This commit is contained in:
weishu
2026-02-06 17:03:46 +08:00
parent 4242c508c1
commit cc9b9279bd
6 changed files with 83 additions and 2 deletions
+6
View File
@@ -16,6 +16,7 @@ import { fetchLatestMessages } from '@/lib/message-window-store'
import { useAppGoBack } from '@/hooks/useAppGoBack'
import { useTranslation } from '@/lib/use-translation'
import { VoiceProvider } from '@/lib/voice-context'
import { requireHubUrlForLogin } from '@/lib/runtime-config'
import { LoginPrompt } from '@/components/LoginPrompt'
import { InstallPrompt } from '@/components/InstallPrompt'
import { OfflineBanner } from '@/components/OfflineBanner'
@@ -29,6 +30,8 @@ import type { SyncEvent } from '@/types/api'
type ToastEvent = Extract<SyncEvent, { type: 'toast' }>
const REQUIRE_SERVER_URL = requireHubUrlForLogin()
export function App() {
return (
<ToastProvider>
@@ -271,6 +274,7 @@ function AppInner() {
serverUrl={serverUrl}
setServerUrl={setServerUrl}
clearServerUrl={clearServerUrl}
requireServerUrl={REQUIRE_SERVER_URL}
/>
)
}
@@ -284,6 +288,7 @@ function AppInner() {
serverUrl={serverUrl}
setServerUrl={setServerUrl}
clearServerUrl={clearServerUrl}
requireServerUrl={REQUIRE_SERVER_URL}
error={authError ?? undefined}
/>
)
@@ -309,6 +314,7 @@ function AppInner() {
serverUrl={serverUrl}
setServerUrl={setServerUrl}
clearServerUrl={clearServerUrl}
requireServerUrl={REQUIRE_SERVER_URL}
error={authError ?? t('login.error.authFailed')}
/>
)
+48
View File
@@ -0,0 +1,48 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { I18nProvider } from '@/lib/i18n-context'
import { LoginPrompt } from './LoginPrompt'
function renderWithProviders(ui: React.ReactElement) {
return render(
<I18nProvider>
{ui}
</I18nProvider>
)
}
describe('LoginPrompt', () => {
beforeEach(() => {
vi.clearAllMocks()
const localStorageMock = {
getItem: vi.fn(() => 'en'),
setItem: vi.fn(),
removeItem: vi.fn(),
}
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
})
it('does not clear first hub URL edit when hub URL required', async () => {
renderWithProviders(
<LoginPrompt
baseUrl="https://app.example.com"
serverUrl={null}
setServerUrl={vi.fn((value: string) => ({ ok: true as const, value }))}
clearServerUrl={vi.fn()}
requireServerUrl={true}
onLogin={vi.fn()}
/>
)
fireEvent.change(screen.getByPlaceholderText('Access token'), { target: { value: 'token' } })
fireEvent.click(screen.getByRole('button', { name: 'Sign In' }))
const hubInput = await screen.findByPlaceholderText('https://hapi.example.com')
expect(screen.getByText('Hub URL required. Please set it before signing in.')).toBeInTheDocument()
fireEvent.change(hubInput, { target: { value: 'https://hub.example.com' } })
expect(hubInput).toHaveValue('https://hub.example.com')
expect(screen.queryByText('Hub URL required. Please set it before signing in.')).not.toBeInTheDocument()
})
})
+15 -2
View File
@@ -15,6 +15,7 @@ type LoginPromptProps = {
serverUrl: string | null
setServerUrl: (input: string) => ServerUrlResult
clearServerUrl: () => void
requireServerUrl?: boolean
error?: string | null
}
@@ -37,6 +38,12 @@ export function LoginPrompt(props: LoginPromptProps) {
return
}
if (!isBindMode && props.requireServerUrl && !props.serverUrl) {
setServerError(t('login.server.required'))
setIsServerDialogOpen(true)
return
}
setIsLoading(true)
setError(null)
@@ -71,7 +78,6 @@ export function LoginPrompt(props: LoginPromptProps) {
return
}
setServerInput(props.serverUrl ?? '')
setServerError(null)
}, [isServerDialogOpen, props.serverUrl])
const handleSaveServer = useCallback((e: React.FormEvent) => {
@@ -93,6 +99,13 @@ export function LoginPrompt(props: LoginPromptProps) {
setIsServerDialogOpen(false)
}, [props])
const handleServerDialogOpenChange = useCallback((open: boolean) => {
setIsServerDialogOpen(open)
if (!open) {
setServerError(null)
}
}, [])
const displayError = error || props.error
const serverSummary = props.serverUrl ?? `${props.baseUrl} ${t('login.server.default')}`
const title = isBindMode ? t('login.bind.title') : t('login.title')
@@ -158,7 +171,7 @@ export function LoginPrompt(props: LoginPromptProps) {
<a href="https://hapi.run/docs" target="_blank" rel="noopener noreferrer" className="underline hover:text-[var(--app-fg)]">
{t('login.help')}
</a>
<Dialog open={isServerDialogOpen} onOpenChange={setIsServerDialogOpen}>
<Dialog open={isServerDialogOpen} onOpenChange={handleServerDialogOpenChange}>
<DialogTrigger asChild>
<button type="button" className="underline hover:text-[var(--app-fg)]">
Hub {props.serverUrl ? `${t('login.server.custom')}` : `${t('login.server.default')}`}
+1
View File
@@ -31,6 +31,7 @@ export default {
'login.server.origin': 'Hub origin',
'login.server.placeholder': 'https://hapi.example.com',
'login.server.hint': 'Use http(s) only. Any path is ignored.',
'login.server.required': 'Hub URL required. Please set it before signing in.',
'login.server.useSameOrigin': 'Use same origin',
'login.server.save': 'Save hub',
'login.footer': 'Designed with',
+1
View File
@@ -31,6 +31,7 @@ export default {
'login.server.origin': '服务器地址',
'login.server.placeholder': 'https://hapi.example.com',
'login.server.hint': '仅使用 http(s) 协议。任何路径将被忽略。',
'login.server.required': '必须先填写服务器地址才能登录。',
'login.server.useSameOrigin': '使用同源',
'login.server.save': '保存',
'login.footer': '用',
+12
View File
@@ -0,0 +1,12 @@
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on'])
function parseBooleanFlag(value: string | undefined): boolean {
if (!value) {
return false
}
return TRUE_VALUES.has(value.trim().toLowerCase())
}
export function requireHubUrlForLogin(): boolean {
return parseBooleanFlag(import.meta.env.VITE_REQUIRE_HUB_URL)
}