diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index c027010..17e9af5 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -42,7 +42,10 @@ describe('tool center menu entries', () => { expect(appSource).toContain("key: 'snippet-settings'"); expect(appSource).toContain("title: t('app.tools.entry.snippets.title')"); expect(appSource).toContain("description: t('app.tools.entry.snippets.description')"); - expect(appSource).toContain('setIsSnippetModalOpen(true)'); + expect(appSource).toContain("handleOpenToolCenterPane('workspace', 'snippet-settings')"); + expect(appSource).toContain('gonavi:open-snippet-settings'); + expect(appSource).toContain("setIsSnippetModalOpen(false);"); + expect(appSource).not.toContain('setIsSnippetModalOpen(true)'); const snippetIndex = appSource.indexOf("key: 'snippet-settings'"); const shortcutIndex = appSource.indexOf("key: 'shortcut-settings'", snippetIndex); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eb6377e..96f31d1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1967,6 +1967,7 @@ function App() { setToolCenterBackGroupKey(group); setActiveToolCenterGroupKey(group); setActiveToolCenterPane({ key, group }); + setIsToolsModalOpen(true); }, []); const handleReturnToToolCenter = useCallback((closeChild?: () => void) => { const returnGroup = toolCenterBackGroupKey ?? 'config'; @@ -2472,13 +2473,14 @@ function App() { useEffect(() => { const handleOpenSnippetSettingsEvent = () => { - setIsSnippetModalOpen(true); + setIsSnippetModalOpen(false); + handleOpenToolCenterPane('workspace', 'snippet-settings'); }; window.addEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener); }; - }, []); + }, [handleOpenToolCenterPane]); useEffect(() => { const handleOpenTabDisplaySettingsEvent = () => { diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index aa6592d..199271b 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -7293,7 +7293,10 @@ describe('QueryEditor external SQL save', () => { expect(modalSource).toContain("defaultActiveKey={['snippet-help']}"); expect(modalSource).toContain('footer={null}'); expect(modalSource).toContain('data-sql-snippet-action-row="true"'); - expect(modalSource).toContain('body: { paddingTop: 8, paddingBottom: 24 }'); + expect(modalSource).toContain('data-sql-snippet-content-region="true"'); + expect(modalSource).toContain('data-sql-snippet-editor-scroll-region="true"'); + expect(modalSource).toContain("maxHeight: snippetModalBodyMaxHeight"); + expect(modalSource).toContain("flex: '0 0 auto'"); expect(modalSource).toContain("size=\"large\""); expect(modalSource).toContain('minWidth: 96'); expect(modalSource).toContain('syntaxHelp'); diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx index 5416478..f83cb4e 100644 --- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx +++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx @@ -2618,7 +2618,10 @@ describe('QueryEditor external SQL save', () => { expect(modalSource).toContain("defaultActiveKey={['snippet-help']}"); expect(modalSource).toContain('footer={null}'); expect(modalSource).toContain('data-sql-snippet-action-row="true"'); - expect(modalSource).toContain('body: { paddingTop: 8, paddingBottom: 24 }'); + expect(modalSource).toContain('data-sql-snippet-content-region="true"'); + expect(modalSource).toContain('data-sql-snippet-editor-scroll-region="true"'); + expect(modalSource).toContain("maxHeight: snippetModalBodyMaxHeight"); + expect(modalSource).toContain("flex: '0 0 auto'"); expect(modalSource).toContain("size=\"large\""); expect(modalSource).toContain('minWidth: 96'); expect(modalSource).toContain('syntaxHelp'); diff --git a/frontend/src/components/SnippetSettingsModal.i18n.test.tsx b/frontend/src/components/SnippetSettingsModal.i18n.test.tsx index 8a7b1f6..2ee7151 100644 --- a/frontend/src/components/SnippetSettingsModal.i18n.test.tsx +++ b/frontend/src/components/SnippetSettingsModal.i18n.test.tsx @@ -320,6 +320,15 @@ describe('SnippetSettingsModal i18n', () => { expect(source).not.toContain('示例:SELECT'); }); + it('keeps snippet editor content scrollable without clipping the action row', () => { + expect(source).toContain("const snippetModalBodyMaxHeight = 'calc(100vh - 128px)';"); + expect(source).toContain("maxHeight: snippetModalBodyMaxHeight"); + expect(source).toContain('data-sql-snippet-content-region="true"'); + expect(source).toContain('data-sql-snippet-editor-scroll-region="true"'); + expect(source).toContain("overflowY: 'auto'"); + expect(source).toContain("flex: '0 0 auto'"); + }); + it('keeps the shell and feedback keys available in every locale', () => { locales.forEach((locale) => { const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record; @@ -364,4 +373,33 @@ describe('SnippetSettingsModal i18n', () => { expect(messageApi.warning).toHaveBeenCalledWith('Prefix is required'); }); + + it('renders a bounded content region and fixed action row for long syntax help', async () => { + const renderer = await renderModal(); + + const newButton = renderer.root.findAll((node: any) => node.type === 'button' && getText(node).includes('New Snippet'))[0]; + + await act(async () => { + newButton.props.onClick(); + }); + + const contentRegion = renderer.root.findByProps({ 'data-sql-snippet-content-region': 'true' }); + const editorScrollRegion = renderer.root.findByProps({ 'data-sql-snippet-editor-scroll-region': 'true' }); + const actionRow = renderer.root.findByProps({ 'data-sql-snippet-action-row': 'true' }); + + expect(contentRegion.props.style).toMatchObject({ + flex: '1 1 420px', + minHeight: 0, + overflow: 'hidden', + }); + expect(editorScrollRegion.props.style).toMatchObject({ + flex: 1, + minHeight: 0, + overflowY: 'auto', + }); + expect(actionRow.props.style).toMatchObject({ + flex: '0 0 auto', + justifyContent: 'flex-end', + }); + }); }); diff --git a/frontend/src/components/SnippetSettingsModal.tsx b/frontend/src/components/SnippetSettingsModal.tsx index 036ff91..1e83f1b 100644 --- a/frontend/src/components/SnippetSettingsModal.tsx +++ b/frontend/src/components/SnippetSettingsModal.tsx @@ -77,6 +77,7 @@ export default function SnippetSettingsModal({ const mutedColor = darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)'; const selectedBg = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)'; const newSnippetAction = t('snippet_settings.action.new'); + const snippetModalBodyMaxHeight = 'calc(100vh - 128px)'; const sortedSnippets = useMemo( () => [...sqlSnippets].sort((a, b) => a.prefix.localeCompare(b.prefix)), @@ -248,16 +249,34 @@ export default function SnippetSettingsModal({ styles={{ content: shellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, - body: { paddingTop: 8, paddingBottom: 24 }, + body: { + paddingTop: 8, + paddingBottom: 0, + display: 'flex', + flexDirection: 'column', + maxHeight: snippetModalBodyMaxHeight, + minHeight: 0, + overflow: 'hidden', + }, }} footer={null} > -
+
{/* Left: snippet list */}
{/* Right: editor */} -
+
{showEditor ? (
@@ -379,7 +400,17 @@ export default function SnippetSettingsModal({ />
-
+
{t('snippet_settings.field.body.label')}