[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', () => {
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user