chore: phase 4 refactor
This commit is contained in:
7
TODO.md
7
TODO.md
@@ -213,8 +213,8 @@ Pure tab-policy helpers that decide:
|
|||||||
- `titleStrategy`: shared title resolver hooks
|
- `titleStrategy`: shared title resolver hooks
|
||||||
|
|
||||||
## Phase 4 — Editor Routing Registry
|
## Phase 4 — Editor Routing Registry
|
||||||
- Replace long `if/else` tab-type rendering chain with declarative tab->component mapping.
|
- [x] 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] Keep special cases explicit (e.g., tab ID transforms such as git-diff commit/file parsing).
|
||||||
|
|
||||||
## Phase 5 — Menu/Event Harmonization
|
## Phase 5 — Menu/Event Harmonization
|
||||||
- Route menu-driven view/tab actions through the same behavior layer where applicable.
|
- 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 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 `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`.
|
- [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).
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { AutoSaveManager, getContrastColor } from '../../utils';
|
|||||||
import { InsertModal } from '../InsertModal';
|
import { InsertModal } from '../InsertModal';
|
||||||
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
||||||
import { openEntityTab } from '../../navigation/tabPolicy';
|
import { openEntityTab } from '../../navigation/tabPolicy';
|
||||||
|
import { EditorRoute, resolveEditorRoute } from '../../navigation/editorRouting';
|
||||||
import { useI18n } from '../../i18n';
|
import { useI18n } from '../../i18n';
|
||||||
import './Editor.css';
|
import './Editor.css';
|
||||||
|
|
||||||
@@ -1724,20 +1725,7 @@ export const Editor: React.FC = () => {
|
|||||||
// Get the active tab
|
// Get the active tab
|
||||||
const activeTab = tabs.find(t => t.id === activeTabId);
|
const activeTab = tabs.find(t => t.id === activeTabId);
|
||||||
|
|
||||||
// Determine what to show based on active tab
|
const editorRoute = resolveEditorRoute(activeTab);
|
||||||
// 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';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activePostId = activeTab?.type === 'post' ? activeTab.id : null;
|
const activePostId = activeTab?.type === 'post' ? activeTab.id : null;
|
||||||
@@ -1789,129 +1777,30 @@ export const Editor: React.FC = () => {
|
|||||||
<ConfirmDeleteModal details={confirmDeleteModal} onClose={hideConfirmDeleteModal} />
|
<ConfirmDeleteModal details={confirmDeleteModal} onClose={hideConfirmDeleteModal} />
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show settings only if settings tab is active
|
const editorViewRenderers: Record<EditorRoute, () => React.ReactNode> = {
|
||||||
if (showSettings) {
|
settings: () => <SettingsView />,
|
||||||
return (
|
style: () => <StyleView />,
|
||||||
<div className="editor">
|
tags: () => <TagsView />,
|
||||||
<SettingsView />
|
chat: () => (editorRoute.tabId ? <ChatPanel key={editorRoute.tabId} conversationId={editorRoute.tabId} /> : <Dashboard />),
|
||||||
{renderErrorModal()}
|
import: () =>
|
||||||
{renderConfirmDeleteModal()}
|
editorRoute.tabId ? <ImportAnalysisView key={editorRoute.tabId} definitionId={editorRoute.tabId} /> : <Dashboard />,
|
||||||
</div>
|
'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) {
|
const editorContent = editorViewRenderers[editorRoute.route]();
|
||||||
return (
|
|
||||||
<div className="editor">
|
|
||||||
<StyleView />
|
|
||||||
{renderErrorModal()}
|
|
||||||
{renderConfirmDeleteModal()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show tags if tags tab is active
|
|
||||||
if (showTags) {
|
|
||||||
return (
|
return (
|
||||||
<div className="editor">
|
<div className="editor">
|
||||||
<TagsView />
|
{editorContent}
|
||||||
{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 />
|
|
||||||
{renderErrorModal()}
|
{renderErrorModal()}
|
||||||
{renderConfirmDeleteModal()}
|
{renderConfirmDeleteModal()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
60
src/renderer/navigation/editorRouting.ts
Normal file
60
src/renderer/navigation/editorRouting.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
63
tests/renderer/navigation/editorRouting.test.ts
Normal file
63
tests/renderer/navigation/editorRouting.test.ts
Normal 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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user