fix(web): satisfy provider model gates

This commit is contained in:
Yichen Jiang
2026-08-06 17:31:25 +08:00
parent 66b136e6e3
commit e43e4f187e
2 changed files with 54 additions and 24 deletions
@@ -18,7 +18,7 @@ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
import { CustomProviderCard } from './CustomProviderCard.tsx' import { CustomProviderCard } from './CustomProviderCard.tsx'
import { deriveKeyRef, messageOf, protocolChoices } from './store.ts' import { deriveKeyRef, messageOf, protocolChoices } from './store.ts'
import type { ModelsSettingsState, ModelsSettingsStore, ProviderRow } from './store.ts' import type { ModelsSettingsState, ModelsSettingsStore, ProviderRow } from './store.ts'
import { ProviderEditor } from './ProviderEditor.tsx' import { ProviderEditor, type ProviderEditorProps } from './ProviderEditor.tsx'
import type { en } from './locales.ts' import type { en } from './locales.ts'
import styles from './ModelsSection.module.css' import styles from './ModelsSection.module.css'
@@ -56,6 +56,26 @@ interface EditorTarget extends ProviderIdentity {
credentialRef?: string credentialRef?: string
} }
/** Values that vary around the shared provider-editor rendering. */
interface ProviderEditorRenderProps extends Pick<
ProviderEditorProps,
'namespace' | 'api' | 't' | 'readOnly' | 'onClose'
> {
target: EditorTarget
}
/** Render an editor for either the setup posture or an expanded provider row. */
function renderProviderEditor({ target, ...props }: ProviderEditorRenderProps): ReactNode {
return (
<ProviderEditor
provider={target.provider}
displayName={target.displayName}
settingsPath={target.settingsPath}
{...props}
/>
)
}
/** /**
* Remove one user-added provider and its page-managed credential. Credential * Remove one user-added provider and its page-managed credential. Credential
* removal comes first so a second-step failure leaves the provider row visible * removal comes first so a second-step failure leaves the provider row visible
@@ -232,16 +252,14 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
// setup card IS its presence on the page. // setup card IS its presence on the page.
return ( return (
<li key={row.entry.provider} className={styles['setupCard']}> <li key={row.entry.provider} className={styles['setupCard']}>
<ProviderEditor {renderProviderEditor({
provider={target.provider} target,
displayName={target.displayName} namespace,
namespace={namespace} api,
settingsPath={target.settingsPath} t,
api={api} readOnly: !state.writable,
t={t} onClose: (changed) => { closeEditor(changed, target) },
readOnly={!state.writable} })}
onClose={(changed) => { closeEditor(changed, target) }}
/>
</li> </li>
) )
} }
@@ -312,18 +330,14 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
</span> </span>
</div> </div>
{open {open
? ( ? renderProviderEditor({
<ProviderEditor target,
provider={target.provider} namespace,
displayName={target.displayName} api,
namespace={namespace} t,
settingsPath={target.settingsPath} readOnly: !state.writable,
api={api} onClose: (changed) => { closeEditor(changed, target) },
t={t} })
readOnly={!state.writable}
onClose={(changed) => { closeEditor(changed, target) }}
/>
)
: null} : null}
</li> </li>
) )
@@ -142,7 +142,7 @@ async function mountSection(options: Parameters<typeof scriptedFace>[0] = {}) {
t, t,
} }
render(<ModelsSection {...injected} />) render(<ModelsSection {...injected} />)
return scripted return { ...scripted, controller }
} }
/** Open the editor of one configured row and expand its customized fold. */ /** Open the editor of one configured row and expand its customized fold. */
@@ -862,4 +862,20 @@ describe('hand-declared providers', () => {
await waitFor(() => { expect(screen.queryByText(en.customTitle)).toBeNull() }) await waitFor(() => { expect(screen.queryByText(en.customTitle)).toBeNull() })
expect(screen.getByRole('button', { name: en.customAdd })).toBeTruthy() expect(screen.getByRole('button', { name: en.customAdd })).toBeTruthy()
}) })
it('reloads the section after creating a hand-declared provider', async () => {
const { controller, mutate } = await mountSection()
const load = vi.spyOn(controller, 'load')
fireEvent.click(screen.getByRole('button', { name: en.customAdd }))
fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } })
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } })
fireEvent.click(screen.getByRole('button', { name: en.addModel }))
fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } })
fireEvent.click(screen.getByText(en.create))
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
await waitFor(() => { expect(load).toHaveBeenCalledOnce() })
expect(screen.queryByText(en.customTitle)).toBeNull()
})
}) })