chore: phase 4 refactor

This commit is contained in:
2026-02-21 18:13:41 +01:00
parent 9df081965b
commit 78a163a0c9
4 changed files with 149 additions and 136 deletions

View File

@@ -213,8 +213,8 @@ Pure tab-policy helpers that decide:
- `titleStrategy`: shared title resolver hooks
## Phase 4 — Editor Routing Registry
- Replace long `if/else` tab-type rendering chain with declarative tab->component mapping.
- Keep special cases explicit (e.g., tab ID transforms such as git-diff commit/file parsing).
- [x] Replace long `if/else` tab-type rendering chain with declarative tab->component mapping.
- [x] Keep special cases explicit (e.g., tab ID transforms such as git-diff commit/file parsing).
## Phase 5 — Menu/Event Harmonization
- Route menu-driven view/tab actions through the same behavior layer where applicable.
@@ -258,4 +258,5 @@ Pure tab-policy helpers that decide:
- [x] Phase 3 continuation slice complete: migrated post/media open paths in Sidebar, Editor, LinkedMediaPanel, and Panel to shared entity-tab policy.
- [x] Phase 3 completion slice: centralized `chat` and `import` tab-open specs and migrated sidebar call sites.
- [x] Phase 3 completion slice: centralized `git-diff` file/commit ID/spec/open helpers and reused shared parsing in `TabBar`.
- [ ] Next implementation slice: Phase 4 (editor routing registry).
- [x] Phase 4 complete: introduced `editorRouting` registry/resolver and migrated `Editor` to declarative route->view rendering.
- [ ] Next implementation slice: Phase 5 (menu/event harmonization).

View File

@@ -22,6 +22,7 @@ import { AutoSaveManager, getContrastColor } from '../../utils';
import { InsertModal } from '../InsertModal';
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
import { openEntityTab } from '../../navigation/tabPolicy';
import { EditorRoute, resolveEditorRoute } from '../../navigation/editorRouting';
import { useI18n } from '../../i18n';
import './Editor.css';
@@ -1724,20 +1725,7 @@ export const Editor: React.FC = () => {
// Get the active tab
const activeTab = tabs.find(t => t.id === activeTabId);
// Determine what to show based on active tab
// Settings and tags should only show when their tab is active, not based on activeView
// (activeView controls the sidebar, not the main content area)
const showPost = activeTab?.type === 'post';
const showMedia = activeTab?.type === 'media';
const showSettings = activeTab?.type === 'settings';
const showStyle = activeTab?.type === 'style';
const showTags = activeTab?.type === 'tags';
const showChat = activeTab?.type === 'chat';
const showImport = activeTab?.type === 'import';
const showMetadataDiff = activeTab?.type === 'metadata-diff';
const showGitDiff = activeTab?.type === 'git-diff';
const showDocumentation = activeTab?.type === 'documentation';
const showSiteValidation = activeTab?.type === 'site-validation';
const editorRoute = resolveEditorRoute(activeTab);
useEffect(() => {
const activePostId = activeTab?.type === 'post' ? activeTab.id : null;
@@ -1789,129 +1777,30 @@ export const Editor: React.FC = () => {
<ConfirmDeleteModal details={confirmDeleteModal} onClose={hideConfirmDeleteModal} />
);
// Show settings only if settings tab is active
if (showSettings) {
return (
<div className="editor">
<SettingsView />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
const editorViewRenderers: Record<EditorRoute, () => React.ReactNode> = {
settings: () => <SettingsView />,
style: () => <StyleView />,
tags: () => <TagsView />,
chat: () => (editorRoute.tabId ? <ChatPanel key={editorRoute.tabId} conversationId={editorRoute.tabId} /> : <Dashboard />),
import: () =>
editorRoute.tabId ? <ImportAnalysisView key={editorRoute.tabId} definitionId={editorRoute.tabId} /> : <Dashboard />,
'metadata-diff': () => <MetadataDiffPanel />,
'git-diff': () =>
editorRoute.tabId && editorRoute.gitDiffResource
? <GitDiffView key={editorRoute.tabId} filePath={editorRoute.gitDiffResource} />
: <Dashboard />,
documentation: () => <DocumentationView />,
'site-validation': () => <SiteValidationView />,
post: () => (editorRoute.tabId ? <PostEditor key={editorRoute.tabId} postId={editorRoute.tabId} /> : <Dashboard />),
media: () => (editorRoute.tabId ? <MediaEditor key={editorRoute.tabId} mediaId={editorRoute.tabId} /> : <Dashboard />),
dashboard: () => <Dashboard />,
};
if (showStyle) {
return (
<div className="editor">
<StyleView />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
const editorContent = editorViewRenderers[editorRoute.route]();
// Show tags if tags tab is active
if (showTags) {
return (
<div className="editor">
<TagsView />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Show chat if chat tab is active
if (showChat && activeTabId) {
return (
<div className="editor">
<ChatPanel key={activeTabId} conversationId={activeTabId} />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Show import analysis if import tab is active
if (showImport && activeTabId) {
return (
<div className="editor">
<ImportAnalysisView key={activeTabId} definitionId={activeTabId} />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Show metadata diff if metadata-diff tab is active
if (showMetadataDiff) {
return (
<div className="editor">
<MetadataDiffPanel />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Show git diff view if git-diff tab is active
if (showGitDiff && activeTabId) {
const filePath = activeTabId.startsWith('git-diff:') ? activeTabId.slice('git-diff:'.length) : activeTabId;
return (
<div className="editor">
<GitDiffView key={activeTabId} filePath={filePath} />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
if (showDocumentation) {
return (
<div className="editor">
<DocumentationView />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
if (showSiteValidation) {
return (
<div className="editor">
<SiteValidationView />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Show post editor if a post tab is active
if (showPost && activeTabId) {
return (
<div className="editor">
<PostEditor key={activeTabId} postId={activeTabId} />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Show media editor if a media tab is active
if (showMedia && activeTabId) {
return (
<div className="editor">
<MediaEditor key={activeTabId} mediaId={activeTabId} />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// No tab active - show dashboard
return (
<div className="editor">
<Dashboard />
{editorContent}
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>

View File

@@ -0,0 +1,60 @@
import type { Tab, TabType } from '../store/appStore';
import { parseGitDiffTabId } from './tabPolicy';
export type EditorRoute =
| 'dashboard'
| 'post'
| 'media'
| 'settings'
| 'style'
| 'tags'
| 'chat'
| 'import'
| 'metadata-diff'
| 'git-diff'
| 'documentation'
| 'site-validation';
export const EDITOR_TAB_ROUTE_REGISTRY: Record<TabType, Exclude<EditorRoute, 'dashboard'>> = {
post: 'post',
media: 'media',
settings: 'settings',
style: 'style',
tags: 'tags',
chat: 'chat',
import: 'import',
'metadata-diff': 'metadata-diff',
'git-diff': 'git-diff',
documentation: 'documentation',
'site-validation': 'site-validation',
};
export interface EditorRouteResolution {
route: EditorRoute;
tabId: string | null;
gitDiffResource: string | null;
}
export function resolveEditorRoute(activeTab: Tab | undefined): EditorRouteResolution {
if (!activeTab) {
return {
route: 'dashboard',
tabId: null,
gitDiffResource: null,
};
}
if (activeTab.type === 'git-diff') {
return {
route: 'git-diff',
tabId: activeTab.id,
gitDiffResource: parseGitDiffTabId(activeTab.id).resource,
};
}
return {
route: EDITOR_TAB_ROUTE_REGISTRY[activeTab.type],
tabId: activeTab.id,
gitDiffResource: null,
};
}

View File

@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest';
import type { Tab } from '../../../src/renderer/store/appStore';
import {
EDITOR_TAB_ROUTE_REGISTRY,
resolveEditorRoute,
} from '../../../src/renderer/navigation/editorRouting';
describe('editorRouting', () => {
it('defines canonical tab-type to editor-route mapping', () => {
expect(EDITOR_TAB_ROUTE_REGISTRY).toEqual({
post: 'post',
media: 'media',
settings: 'settings',
style: 'style',
tags: 'tags',
chat: 'chat',
import: 'import',
'metadata-diff': 'metadata-diff',
'git-diff': 'git-diff',
documentation: 'documentation',
'site-validation': 'site-validation',
});
});
it('resolves dashboard route when no active tab is present', () => {
expect(resolveEditorRoute(undefined)).toEqual({
route: 'dashboard',
tabId: null,
gitDiffResource: null,
});
});
it('resolves post and media routes with active tab id', () => {
const postTab: Tab = { type: 'post', id: 'post-1', isTransient: true };
const mediaTab: Tab = { type: 'media', id: 'media-1', isTransient: false };
expect(resolveEditorRoute(postTab)).toEqual({
route: 'post',
tabId: 'post-1',
gitDiffResource: null,
});
expect(resolveEditorRoute(mediaTab)).toEqual({
route: 'media',
tabId: 'media-1',
gitDiffResource: null,
});
});
it('resolves git-diff route and extracts resource from tab id', () => {
const tab: Tab = {
type: 'git-diff',
id: 'git-diff:commit:abc123',
isTransient: true,
};
expect(resolveEditorRoute(tab)).toEqual({
route: 'git-diff',
tabId: 'git-diff:commit:abc123',
gitDiffResource: 'commit:abc123',
});
});
});