[codex] align site created modal with shared actions (#425)

* align site created modal with shared actions

* address site created modal review comments
This commit is contained in:
Cita
2026-04-05 19:34:10 +08:00
committed by GitHub
parent ef1962d10b
commit b3f6fa0d4f
5 changed files with 161 additions and 121 deletions
+49 -11
View File
@@ -10,7 +10,7 @@ function collectText(node: ReactTestInstance): string {
} }
describe('SiteCreatedModal', () => { describe('SiteCreatedModal', () => {
it('routes native dialog cancel events through onClose cleanup', async () => { it('uses the shared centered modal shell and close button instead of a native dialog skin', async () => {
const onChoice = vi.fn(); const onChoice = vi.fn();
const onClose = vi.fn(); const onClose = vi.fn();
const root = create( const root = create(
@@ -21,15 +21,28 @@ describe('SiteCreatedModal', () => {
/>, />,
); );
const dialog = root.root.findByType('dialog'); expect(root.root.findAllByType('dialog')).toHaveLength(0);
expect(typeof dialog.props.onCancel).toBe('function');
const backdrop = root.root.find((node) => (
typeof node.props.className === 'string'
&& node.props.className.includes('modal-backdrop')
));
const footer = root.root.find((node) => (
typeof node.props.className === 'string'
&& node.props.className.includes('modal-footer')
));
const closeButton = root.root.find((node) => (
node.type === 'button'
&& node.props['aria-label'] === '关闭弹框'
));
expect(backdrop).toBeTruthy();
expect(footer).toBeTruthy();
const preventDefault = vi.fn();
await act(async () => { await act(async () => {
dialog.props.onCancel({ preventDefault }); closeButton.props.onClick();
}); });
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledTimes(1); expect(onClose).toHaveBeenCalledTimes(1);
expect(onChoice).not.toHaveBeenCalled(); expect(onChoice).not.toHaveBeenCalled();
}); });
@@ -50,18 +63,43 @@ describe('SiteCreatedModal', () => {
const choiceButtons = root.root.findAll((node) => ( const choiceButtons = root.root.findAll((node) => (
node.type === 'button' node.type === 'button'
&& typeof node.props.onClick === 'function' && typeof node.props.onClick === 'function'
&& node.props['aria-label'] !== '关闭弹框'
&& collectText(node) !== '稍后配置' && collectText(node) !== '稍后配置'
)); ));
expect(choiceButtons.map((button) => collectText(button))).toEqual([ expect(choiceButtons.map((button) => collectText(button))).toEqual(
'添加 API Key(推荐)', expect.arrayContaining([
'添加账号(用户名密码登录)', '添加 API Key(推荐)',
]); '添加账号(用户名密码登录)',
]),
);
expect(choiceButtons.every((button) => !String(button.props.className || '').includes('btn-outline'))).toBe(true);
const sessionButton = choiceButtons.find((button) => collectText(button) === '添加账号(用户名密码登录)');
await act(async () => { await act(async () => {
choiceButtons[1]!.props.onClick(); sessionButton!.props.onClick();
}); });
expect(onChoice).toHaveBeenCalledWith('session'); expect(onChoice).toHaveBeenCalledWith('session');
}); });
it('uses the supplied session label for OAuth-style session actions', () => {
const root = create(
<SiteCreatedModal
siteName="Codex Site"
initialSegment="session"
sessionLabel="添加 OAuth 连接"
onChoice={() => {}}
onClose={() => {}}
/>,
);
const buttons = root.root.findAll((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& node.props['aria-label'] !== '关闭弹框'
));
expect(buttons.some((button) => collectText(button) === '添加 OAuth 连接')).toBe(true);
});
}); });
+67 -100
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'; import CenteredModal from './CenteredModal.js';
import { getSiteInitializationPreset } from '../../shared/siteInitializationPresets.js'; import { getSiteInitializationPreset } from '../../shared/siteInitializationPresets.js';
type NextStepChoice = 'session' | 'apikey' | 'later'; type NextStepChoice = 'session' | 'apikey' | 'later';
@@ -7,6 +7,7 @@ type Props = {
siteName: string; siteName: string;
initializationPresetId?: string | null; initializationPresetId?: string | null;
initialSegment?: 'session' | 'apikey'; initialSegment?: 'session' | 'apikey';
sessionLabel?: string;
onChoice: (choice: NextStepChoice) => void; onChoice: (choice: NextStepChoice) => void;
onClose: () => void; onClose: () => void;
}; };
@@ -15,122 +16,88 @@ export default function SiteCreatedModal({
siteName, siteName,
initializationPresetId, initializationPresetId,
initialSegment = 'session', initialSegment = 'session',
sessionLabel = '添加账号(用户名密码登录)',
onChoice, onChoice,
onClose, onClose,
}: Props) { }: Props) {
const dialogRef = useRef<HTMLDialogElement>(null);
const preset = getSiteInitializationPreset(initializationPresetId); const preset = getSiteInitializationPreset(initializationPresetId);
const apiKeyFirst = initialSegment === 'apikey'; const apiKeyFirst = initialSegment === 'apikey';
const helperText = preset?.description const helperText = preset?.description
|| (apiKeyFirst || (apiKeyFirst
? '该平台更适合直接通过 Base URL + API Key 接入,后续再补模型初始化。' ? '该平台更适合直接通过 Base URL + API Key 接入,后续再补模型初始化。'
: '接下来您可以继续补充登录连接或 API Key。'); : '接下来您可以继续补充登录连接或 API Key。');
const actionButtons = apiKeyFirst const primaryAction = apiKeyFirst
? [ ? {
{ choice: 'apikey' as const,
choice: 'apikey' as const, label: '添加 API Key(推荐)',
className: 'btn btn-primary btn-block',
label: '添加 API Key(推荐)',
},
{
choice: 'session' as const,
className: 'btn btn-outline btn-block',
label: '添加账号(用户名密码登录)',
},
]
: [
{
choice: 'session' as const,
className: 'btn btn-primary btn-block',
label: '添加账号(用户名密码登录)',
},
{
choice: 'apikey' as const,
className: 'btn btn-outline btn-block',
label: '添加 API Key',
},
];
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (!dialog.open) {
dialog.showModal();
} }
: {
return () => { choice: 'session' as const,
if (dialog.open) { label: sessionLabel,
dialog.close();
}
}; };
}, []); const secondaryAction = apiKeyFirst
? {
const handleKeyDown = (e: React.KeyboardEvent) => { choice: 'session' as const,
if (e.key === 'Escape') { label: sessionLabel,
e.preventDefault();
onChoice('later');
} }
}; : {
choice: 'apikey' as const,
label: '添加 API Key',
};
return ( return (
<dialog <CenteredModal
ref={dialogRef} open
className="modal" onClose={onClose}
onCancel={(e) => { title="站点创建成功"
e.preventDefault(); maxWidth={520}
onClose(); closeOnBackdrop
}} closeOnEscape
onKeyDown={handleKeyDown} bodyStyle={{ display: 'flex', flexDirection: 'column', gap: 12 }}
onClick={(e) => { footer={(
if (e.target === dialogRef.current) { <>
onChoice('later'); <button onClick={() => onChoice('later')} className="btn btn-ghost">
}
}}
>
<div className="modal-box" style={{ maxWidth: 480 }}>
<h3 className="font-bold text-lg mb-2">
站点创建成功
</h3>
<p className="py-2 text-sm" style={{ color: 'var(--color-text-muted)' }}>
站点 <strong>"{siteName}"</strong> 已添加成功。
</p>
{preset && (
<div className="alert alert-info" style={{ marginBottom: 12 }}>
<div className="alert-title">{preset.label}</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 4, lineHeight: 1.7 }}>
{helperText}
</div>
</div>
)}
{!preset && (
<p className="py-1 text-sm" style={{ color: 'var(--color-text-muted)' }}>
{helperText}
</p>
)}
<div className="modal-action" style={{ flexDirection: 'column', gap: 12 }}>
{actionButtons.map((action) => (
<button
key={action.choice}
className={action.className}
onClick={() => onChoice(action.choice)}
>
{action.label}
</button>
))}
<button
className="btn btn-ghost btn-block"
onClick={() => onChoice('later')}
>
稍后配置 稍后配置
</button> </button>
<button
onClick={() => onChoice(secondaryAction.choice)}
className="btn btn-ghost"
style={{ border: '1px solid var(--color-border)' }}
>
{secondaryAction.label}
</button>
<button
onClick={() => onChoice(primaryAction.choice)}
className="btn btn-primary"
>
{primaryAction.label}
</button>
</>
)}
>
<div className="alert alert-success animate-scale-in" style={{ margin: 0 }}>
<div className="alert-title">站点已添加成功</div>
<div className="site-created-summary">
站点 <strong>"{siteName}"</strong> 已加入列表,您现在可以继续补充连接信息。
</div> </div>
<p className="text-xs mt-3" style={{ color: 'var(--color-text-muted)' }}>
提示:您可以随时在"站点管理"页面配置账号信息
</p>
</div> </div>
</dialog>
{preset ? (
<div className="alert alert-info" style={{ margin: 0 }}>
<div className="alert-title">{preset.label}</div>
<div className="site-created-helper-text">
{helperText}
</div>
</div>
) : (
<p className="site-created-helper-text">
{helperText}
</p>
)}
<p className="site-created-note">
提示:您可以随时在“站点管理”页面补充账号或 API Key。
</p>
</CenteredModal>
); );
} }
+18
View File
@@ -3455,6 +3455,24 @@ textarea {
flex-wrap: wrap; flex-wrap: wrap;
} }
.site-created-summary,
.site-created-helper-text {
margin: 0;
font-size: 12px;
line-height: 1.7;
}
.site-created-helper-text {
color: var(--color-text-muted);
}
.site-created-note {
margin: 0;
font-size: 12px;
line-height: 1.6;
color: var(--color-text-muted);
}
.responsive-form-grid { .responsive-form-grid {
display: grid; display: grid;
gap: 12px; gap: 12px;
+7
View File
@@ -93,6 +93,12 @@ function formatUsd(value?: number | null): string {
return `$${(value || 0).toFixed(2)}`; return `$${(value || 0).toFixed(2)}`;
} }
function resolveSiteCreatedSessionLabel(platform?: string | null): string {
const normalized = String(platform || '').trim().toLowerCase();
if (normalized === 'codex') return '添加 OAuth 连接';
return '添加账号(用户名密码登录)';
}
function formatSubscriptionDate(value?: string | null): string { function formatSubscriptionDate(value?: string | null): string {
if (!value) return ''; if (!value) return '';
const parsed = Date.parse(value); const parsed = Date.parse(value);
@@ -977,6 +983,7 @@ export default function Sites() {
getSiteInitializationPreset(createdSiteForChoice.initializationPresetId)?.initialSegment getSiteInitializationPreset(createdSiteForChoice.initializationPresetId)?.initialSegment
|| resolveInitialConnectionSegment(createdSiteForChoice.platform) || resolveInitialConnectionSegment(createdSiteForChoice.platform)
} }
sessionLabel={resolveSiteCreatedSessionLabel(createdSiteForChoice.platform)}
onChoice={handleSiteCreatedChoice} onChoice={handleSiteCreatedChoice}
onClose={() => { onClose={() => {
setCreatedSiteForChoice(null); setCreatedSiteForChoice(null);
+20 -10
View File
@@ -106,26 +106,36 @@ async function createSiteAndClickModalChoice(
}); });
await flushMicrotasks(); await flushMicrotasks();
// Find the modal dialog and click the appropriate button // Find the created-site modal and click the appropriate button
const modalDialog = root.root.find((node) => node.type === 'dialog'); const modalContent = root.root.find((node) => (
const modalButtons = modalDialog.findAll((node) => node.type === 'button'); typeof node.props.className === 'string'
&& node.props.className.includes('modal-content')
&& collectText(node).includes('站点创建成功')
));
const modalButtons = modalContent.findAll((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& node.props['aria-label'] !== '关闭弹框'
));
// Find the button based on choice // Find the button based on choice
let targetButton; let targetButton;
if (choice === 'session') { if (choice === 'session') {
targetButton = modalButtons.find((btn) => collectText(btn).includes('添加账号(用户名密码登录)')); const expectedSessionLabel = createdSite.platform === 'codex'
? '添加 OAuth 连接'
: '添加账号(用户名密码登录)';
targetButton = modalButtons.find((btn) => collectText(btn).includes(expectedSessionLabel));
} else if (choice === 'apikey') { } else if (choice === 'apikey') {
targetButton = modalButtons.find((btn) => collectText(btn).includes('添加 API Key')); targetButton = modalButtons.find((btn) => collectText(btn).includes('添加 API Key'));
} else { } else {
targetButton = modalButtons.find((btn) => collectText(btn).includes('稍后配置')); targetButton = modalButtons.find((btn) => collectText(btn).includes('稍后配置'));
} }
if (targetButton) { expect(targetButton).toBeTruthy();
await act(async () => { await act(async () => {
targetButton.props.onClick(); targetButton!.props.onClick();
}); });
await flushMicrotasks(); await flushMicrotasks();
}
return JSON.stringify(root.toJSON()); return JSON.stringify(root.toJSON());
} finally { } finally {