🐛 fix(tool-center): 移除内嵌工具重复标题头

- 工具中心详情页统一承载入口标题和说明
- 数据同步、表结构比对和数据比对嵌入时隐藏内部 hero
- 数据目录、快捷键、连接包和安全更新嵌入时隐藏内部 Modal 标题
- 补充工具中心全 pane 标题归属回归断言
This commit is contained in:
Syngnat
2026-06-25 18:42:53 +08:00
parent bdb60a656a
commit 8dde4c3c6d
8 changed files with 138 additions and 73 deletions

View File

@@ -103,6 +103,60 @@ describe('tool center menu entries', () => {
expect(appSource).toContain("borderBottom: `1px solid ${overlayTheme.divider}`");
});
it('lets the tool center detail header own embedded tool titles', () => {
const renderPaneStart = appSource.indexOf('const renderToolCenterPane = () => {');
const renderPaneSource = appSource.slice(
renderPaneStart,
appSource.indexOf('};\n\n return (', renderPaneStart),
);
const connectionPackageSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'connection-package')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'data-root')"),
);
const dataRootSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'data-root')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'security-update')"),
);
const securityUpdateSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'security-update')"),
renderPaneSource.indexOf("activeToolCenterPane.key === 'schema-compare'"),
);
const dataSyncSource = renderPaneSource.slice(
renderPaneSource.indexOf("activeToolCenterPane.key === 'schema-compare'"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'drivers')"),
);
const driverSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'drivers')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'snippet-settings')"),
);
const snippetSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'snippet-settings')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')"),
);
const shortcutSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')"),
renderPaneSource.indexOf('return null;', renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')")),
);
expect(appSource).toContain('activeToolCenterPaneItem?.title ?? activeToolCenterGroup.title');
expect(connectionPackageSource).toContain('<ConnectionPackagePasswordModal');
expect(connectionPackageSource).not.toContain('renderUtilityModalTitle');
expect(dataRootSource).toContain('title={null}');
expect(dataRootSource).toContain('closable={false}');
expect(dataRootSource).not.toContain('renderUtilityModalTitle');
expect(securityUpdateSource).toContain('<SecurityUpdateSettingsModal');
expect(securityUpdateSource).not.toContain('renderUtilityModalTitle');
expect(dataSyncSource).toContain('<DataSyncModal');
expect(dataSyncSource).not.toContain('renderUtilityModalTitle');
expect(driverSource).toContain('<DriverManagerModal');
expect(driverSource).not.toContain('renderUtilityModalTitle');
expect(snippetSource).toContain('<SnippetSettingsModal');
expect(snippetSource).not.toContain('renderUtilityModalTitle');
expect(shortcutSource).toContain('title={null}');
expect(shortcutSource).toContain('closable={false}');
expect(shortcutSource).not.toContain('renderUtilityModalTitle');
});
it('keeps the v2 AI entry in the sidebar and the legacy AI entry on the content edge', () => {
expect(appSource).toContain('onToggleAI={toggleAIPanel}');
expect(appSource).toContain('renderLegacyAIEdgeHandle');

View File

@@ -3401,11 +3401,8 @@ function App() {
<Modal
embedded
open
title={renderUtilityModalTitle(
<HddOutlined />,
t('app.data_root.title'),
t('app.data_root.description'),
)}
title={null}
closable={false}
onCancel={closeToolCenterPane}
footer={[
<Button key="close" onClick={closeToolCenterPane}>
@@ -3551,11 +3548,8 @@ function App() {
<Modal
embedded
open
title={renderUtilityModalTitle(
<LinkOutlined />,
t('app.shortcuts.title'),
t('app.shortcuts.description'),
)}
title={null}
closable={false}
onCancel={() => {
setCapturingShortcutAction(null);
closeToolCenterPane();

View File

@@ -64,9 +64,10 @@ export default function ConnectionPackagePasswordModal({
<Modal
open={open}
embedded={embedded}
title={(
title={embedded ? null : (
<span style={{ minWidth: 0 }}>{title}</span>
)}
closable={embedded ? false : undefined}
onCancel={onCancel}
destroyOnHidden={false}
maskClosable={false}

View File

@@ -109,4 +109,10 @@ describe("DataSyncModal i18n", () => {
/tr\(\s*(['"])data_sync\.compare_entry\.preview\.sql\.data_help\1/,
);
});
it('hides the modal hero when embedded in the tool center', () => {
expect(source).toContain('{!embedded && (');
expect(source).toContain('<div style={heroPanelStyle}>');
expect(source).toMatch(/embedded\s*\?\s*\(\s*dataSyncContent\s*\)/);
});
});

View File

@@ -1388,71 +1388,73 @@ const DataSyncModal: React.FC<{
const dataSyncContent = (
<div style={modalWorkspaceStyle}>
<div style={{ flex: "0 0 auto" }}>
<div style={heroPanelStyle}>
<div
style={{
display: "flex",
justifyContent: "space-between",
gap: 12,
alignItems: "flex-start",
flexWrap: "wrap",
}}
>
<div style={{ minWidth: 0 }}>
<div
style={{
fontSize: 18,
fontWeight: 700,
color: darkMode ? "#f8fafc" : "#0f172a",
}}
>
{isMigrationWorkflow
? tr("data_sync.title.migration")
: isCompareEntry
? entryPresentation.heroTitle
: tr("data_sync.title.sync")}
{!embedded && (
<div style={heroPanelStyle}>
<div
style={{
display: "flex",
justifyContent: "space-between",
gap: 12,
alignItems: "flex-start",
flexWrap: "wrap",
}}
>
<div style={{ minWidth: 0 }}>
<div
style={{
fontSize: 18,
fontWeight: 700,
color: darkMode ? "#f8fafc" : "#0f172a",
}}
>
{isMigrationWorkflow
? tr("data_sync.title.migration")
: isCompareEntry
? entryPresentation.heroTitle
: tr("data_sync.title.sync")}
</div>
<div
style={{
marginTop: 6,
fontSize: 13,
lineHeight: 1.7,
color: darkMode
? "rgba(255,255,255,0.62)"
: "rgba(15,23,42,0.62)",
}}
>
{isMigrationWorkflow
? tr("data_sync.title.migration_description")
: isCompareEntry
? entryPresentation.heroDescription
: tr("data_sync.title.sync_description")}
</div>
</div>
<div
style={{
marginTop: 6,
fontSize: 13,
lineHeight: 1.7,
color: darkMode
? "rgba(255,255,255,0.62)"
: "rgba(15,23,42,0.62)",
}}
>
{isMigrationWorkflow
? tr("data_sync.title.migration_description")
: isCompareEntry
? entryPresentation.heroDescription
: tr("data_sync.title.sync_description")}
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
<span style={badgeStyle}>
{isMigrationWorkflow ? <RocketOutlined /> : <SwapOutlined />}{" "}
{isMigrationWorkflow
? tr("data_sync.badge.migration_mode")
: isCompareEntry
? entryPresentation.badgeText
: tr("data_sync.badge.sync_mode")}
</span>
<span style={badgeStyle}>
<DatabaseOutlined />{" "}
{sourceConnId
? tr("data_sync.badge.source_selected")
: tr("data_sync.badge.source_pending")}
</span>
<span style={badgeStyle}>
<TableOutlined />{" "}
{tr("data_sync.badge.table_count", {
count: selectedTables.length || 0,
})}
</span>
</div>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
<span style={badgeStyle}>
{isMigrationWorkflow ? <RocketOutlined /> : <SwapOutlined />}{" "}
{isMigrationWorkflow
? tr("data_sync.badge.migration_mode")
: isCompareEntry
? entryPresentation.badgeText
: tr("data_sync.badge.sync_mode")}
</span>
<span style={badgeStyle}>
<DatabaseOutlined />{" "}
{sourceConnId
? tr("data_sync.badge.source_selected")
: tr("data_sync.badge.source_pending")}
</span>
<span style={badgeStyle}>
<TableOutlined />{" "}
{tr("data_sync.badge.table_count", {
count: selectedTables.length || 0,
})}
</span>
</div>
</div>
</div>
)}
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title={tr("data_sync.step.configure")} />
<Step title={tr("data_sync.step.select_tables")} />

View File

@@ -134,6 +134,11 @@ describe('SecurityUpdateSettingsModal i18n source guards', () => {
expect(source).not.toContain('当前项需要进一步处理后才能完成安全更新。');
});
it('lets the tool center provide the title when embedded', () => {
expect(source).toContain('title={embedded ? null : (');
expect(source).toContain('closable={embedded ? false : undefined}');
});
it('localizes settings chrome while preserving raw issue details, backup path and error text', async () => {
const modalText = await renderSettingsModalText();
expect(modalText).toContain('Security Update');

View File

@@ -130,7 +130,7 @@ const SecurityUpdateSettingsModal = ({
return (
<Modal
rootClassName={SECURITY_UPDATE_MODAL_CLASS}
title={(
title={embedded ? null : (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, minWidth: 0 }}>
<div
style={{
@@ -159,6 +159,7 @@ const SecurityUpdateSettingsModal = ({
)}
open={open}
embedded={embedded}
closable={embedded ? false : undefined}
onCancel={onClose}
footer={[
showRetry ? (

View File

@@ -931,6 +931,8 @@ describe("i18n catalog", () => {
expect(passwordModalSource).not.toContain("密码已加密保护。如需通过公网传输,建议设置文件保护密码。");
expect(passwordModalSource).not.toContain("导出连接密码");
expect(passwordModalSource).not.toContain("设置文件保护密码");
expect(passwordModalSource).toContain("title={embedded ? null : (");
expect(passwordModalSource).toContain("closable={embedded ? false : undefined}");
});
it("keeps QueryEditor format settings menu labels in catalogs instead of source literals", () => {