[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:
@@ -10,7 +10,7 @@ function collectText(node: ReactTestInstance): string {
|
||||
}
|
||||
|
||||
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 onClose = vi.fn();
|
||||
const root = create(
|
||||
@@ -21,15 +21,28 @@ describe('SiteCreatedModal', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = root.root.findByType('dialog');
|
||||
expect(typeof dialog.props.onCancel).toBe('function');
|
||||
expect(root.root.findAllByType('dialog')).toHaveLength(0);
|
||||
|
||||
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 () => {
|
||||
dialog.props.onCancel({ preventDefault });
|
||||
closeButton.props.onClick();
|
||||
});
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
expect(onChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -50,18 +63,43 @@ describe('SiteCreatedModal', () => {
|
||||
const choiceButtons = root.root.findAll((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
&& node.props['aria-label'] !== '关闭弹框'
|
||||
&& collectText(node) !== '稍后配置'
|
||||
));
|
||||
|
||||
expect(choiceButtons.map((button) => collectText(button))).toEqual([
|
||||
'添加 API Key(推荐)',
|
||||
'添加账号(用户名密码登录)',
|
||||
]);
|
||||
expect(choiceButtons.map((button) => collectText(button))).toEqual(
|
||||
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 () => {
|
||||
choiceButtons[1]!.props.onClick();
|
||||
sessionButton!.props.onClick();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import CenteredModal from './CenteredModal.js';
|
||||
import { getSiteInitializationPreset } from '../../shared/siteInitializationPresets.js';
|
||||
|
||||
type NextStepChoice = 'session' | 'apikey' | 'later';
|
||||
@@ -7,6 +7,7 @@ type Props = {
|
||||
siteName: string;
|
||||
initializationPresetId?: string | null;
|
||||
initialSegment?: 'session' | 'apikey';
|
||||
sessionLabel?: string;
|
||||
onChoice: (choice: NextStepChoice) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -15,122 +16,88 @@ export default function SiteCreatedModal({
|
||||
siteName,
|
||||
initializationPresetId,
|
||||
initialSegment = 'session',
|
||||
sessionLabel = '添加账号(用户名密码登录)',
|
||||
onChoice,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const preset = getSiteInitializationPreset(initializationPresetId);
|
||||
const apiKeyFirst = initialSegment === 'apikey';
|
||||
const helperText = preset?.description
|
||||
|| (apiKeyFirst
|
||||
? '该平台更适合直接通过 Base URL + API Key 接入,后续再补模型初始化。'
|
||||
: '接下来您可以继续补充登录连接或 API Key。');
|
||||
const actionButtons = apiKeyFirst
|
||||
? [
|
||||
{
|
||||
choice: 'apikey' as const,
|
||||
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();
|
||||
const primaryAction = apiKeyFirst
|
||||
? {
|
||||
choice: 'apikey' as const,
|
||||
label: '添加 API Key(推荐)',
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (dialog.open) {
|
||||
dialog.close();
|
||||
}
|
||||
: {
|
||||
choice: 'session' as const,
|
||||
label: sessionLabel,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onChoice('later');
|
||||
const secondaryAction = apiKeyFirst
|
||||
? {
|
||||
choice: 'session' as const,
|
||||
label: sessionLabel,
|
||||
}
|
||||
};
|
||||
: {
|
||||
choice: 'apikey' as const,
|
||||
label: '添加 API Key',
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className="modal"
|
||||
onCancel={(e) => {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={(e) => {
|
||||
if (e.target === dialogRef.current) {
|
||||
onChoice('later');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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')}
|
||||
>
|
||||
<CenteredModal
|
||||
open
|
||||
onClose={onClose}
|
||||
title="站点创建成功"
|
||||
maxWidth={520}
|
||||
closeOnBackdrop
|
||||
closeOnEscape
|
||||
bodyStyle={{ display: 'flex', flexDirection: 'column', gap: 12 }}
|
||||
footer={(
|
||||
<>
|
||||
<button onClick={() => onChoice('later')} className="btn btn-ghost">
|
||||
稍后配置
|
||||
</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>
|
||||
|
||||
<p className="text-xs mt-3" style={{ color: 'var(--color-text-muted)' }}>
|
||||
提示:您可以随时在"站点管理"页面配置账号信息
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3455,6 +3455,24 @@ textarea {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
@@ -93,6 +93,12 @@ function formatUsd(value?: number | null): string {
|
||||
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 {
|
||||
if (!value) return '';
|
||||
const parsed = Date.parse(value);
|
||||
@@ -977,6 +983,7 @@ export default function Sites() {
|
||||
getSiteInitializationPreset(createdSiteForChoice.initializationPresetId)?.initialSegment
|
||||
|| resolveInitialConnectionSegment(createdSiteForChoice.platform)
|
||||
}
|
||||
sessionLabel={resolveSiteCreatedSessionLabel(createdSiteForChoice.platform)}
|
||||
onChoice={handleSiteCreatedChoice}
|
||||
onClose={() => {
|
||||
setCreatedSiteForChoice(null);
|
||||
|
||||
@@ -106,26 +106,36 @@ async function createSiteAndClickModalChoice(
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
// Find the modal dialog and click the appropriate button
|
||||
const modalDialog = root.root.find((node) => node.type === 'dialog');
|
||||
const modalButtons = modalDialog.findAll((node) => node.type === 'button');
|
||||
// Find the created-site modal and click the appropriate button
|
||||
const modalContent = root.root.find((node) => (
|
||||
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
|
||||
let targetButton;
|
||||
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') {
|
||||
targetButton = modalButtons.find((btn) => collectText(btn).includes('添加 API Key'));
|
||||
} else {
|
||||
targetButton = modalButtons.find((btn) => collectText(btn).includes('稍后配置'));
|
||||
}
|
||||
|
||||
if (targetButton) {
|
||||
await act(async () => {
|
||||
targetButton.props.onClick();
|
||||
});
|
||||
await flushMicrotasks();
|
||||
}
|
||||
expect(targetButton).toBeTruthy();
|
||||
await act(async () => {
|
||||
targetButton!.props.onClick();
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
return JSON.stringify(root.toJSON());
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user