chore: phase 3 refactorings

This commit is contained in:
2026-02-21 18:10:26 +01:00
parent 87200a8ad9
commit 9df081965b
9 changed files with 252 additions and 46 deletions

View File

@@ -21,6 +21,7 @@ import { SiteValidationView } from '../SiteValidationView';
import { AutoSaveManager, getContrastColor } from '../../utils';
import { InsertModal } from '../InsertModal';
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
import { openEntityTab } from '../../navigation/tabPolicy';
import { useI18n } from '../../i18n';
import './Editor.css';
@@ -1102,7 +1103,7 @@ const MediaEditor: React.FC<{ mediaId: string }> = ({ mediaId }) => {
// Handle click on a post to navigate to it
const handlePostClick = (postId: string) => {
openTab({ type: 'post', id: postId, isTransient: true });
openEntityTab(openTab, 'post', postId, 'preview');
};
// Get unlinked posts for picker, filtered by search
@@ -1680,12 +1681,12 @@ const Dashboard: React.FC = () => {
onClick={() => {
useAppStore.getState().setActiveView('posts');
useAppStore.getState().setSelectedPost(post.id);
useAppStore.getState().openTab({ type: 'post', id: post.id, isTransient: true });
openEntityTab(useAppStore.getState().openTab, 'post', post.id, 'preview');
}}
onDoubleClick={() => {
useAppStore.getState().setActiveView('posts');
useAppStore.getState().setSelectedPost(post.id);
useAppStore.getState().openTab({ type: 'post', id: post.id, isTransient: false });
openEntityTab(useAppStore.getState().openTab, 'post', post.id, 'pin');
}}
>
<span className="recent-post-title">{post.title || tr('editor.untitled')}</span>

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useAppStore } from '../../store';
import { openGitDiffCommitTab, openGitDiffFileTab } from '../../navigation/tabPolicy';
import { useI18n } from '../../i18n';
import type { GitInitProgress, GitHistoryEntry, GitRemoteStateDto } from '../../../main/shared/electronApi';
import './GitSidebar.css';
@@ -125,9 +126,6 @@ export const GitSidebar: React.FC = () => {
[tr],
);
const getDiffTabId = (filePath: string): string => `git-diff:${filePath}`;
const getCommitDiffTabId = (commitHash: string): string => `git-diff:commit:${commitHash}`;
const getActionProgressMessage = (action: 'fetch' | 'pull' | 'push' | 'prune-lfs' | 'commit'): string => {
if (action === 'push') {
return tr('gitSidebar.progress.pushingRemote');
@@ -156,22 +154,14 @@ export const GitSidebar: React.FC = () => {
const openDiffTab = useCallback(
(filePath: string, isTransient: boolean) => {
openTab({
type: 'git-diff',
id: getDiffTabId(filePath),
isTransient,
});
openGitDiffFileTab(openTab, filePath, isTransient ? 'preview' : 'pin');
},
[openTab],
);
const openCommitDiffTab = useCallback(
(commitHash: string, isTransient: boolean) => {
openTab({
type: 'git-diff',
id: getCommitDiffTabId(commitHash),
isTransient,
});
openGitDiffCommitTab(openTab, commitHash, isTransient ? 'preview' : 'pin');
},
[openTab],
);

View File

@@ -11,6 +11,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useAppStore, MediaData } from '../../store';
import { openEntityTab } from '../../navigation/tabPolicy';
import { showToast } from '../Toast';
import { useI18n } from '../../i18n';
import './LinkedMediaPanel.css';
@@ -197,7 +198,7 @@ export const LinkedMediaPanel: React.FC<LinkedMediaPanelProps> = ({
// Handle click on media item to open media viewer
const handleMediaClick = (mediaId: string) => {
useAppStore.getState().openTab({ type: 'media', id: mediaId, isTransient: true });
openEntityTab(useAppStore.getState().openTab, 'media', mediaId, 'preview');
};
// Get thumbnail URL for a media item

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useAppStore } from '../../store';
import type { TaskProgress } from '../../../main/shared/electronApi';
import { openEntityTab } from '../../navigation/tabPolicy';
import { useI18n } from '../../i18n';
import './Panel.css';
@@ -296,7 +297,7 @@ export const Panel: React.FC = () => {
}
const handlePostLinkClick = (postId: string) => {
openTab({ type: 'post', id: postId, isTransient: false });
openEntityTab(openTab, 'post', postId, 'pin');
setSelectedPost(postId);
setActiveView('posts');
};

View File

@@ -8,7 +8,7 @@ import { scrollToSettingsSection, SettingsCategory } from '../SettingsView/Setti
import { scrollToTagsSection, TagsCategory } from '../TagsView';
import { activateSidebarSection } from '../../navigation/sectionActivation';
import { getPersistedSidebarSection, setPersistedSidebarSection } from '../../navigation/sidebarUiPersistence';
import { openSingletonToolTab } from '../../navigation/tabPolicy';
import { openChatTab, openEntityTab, openImportTab, openSingletonToolTab } from '../../navigation/tabPolicy';
import type { SidebarView } from '../../navigation/sidebarViewRegistry';
import { useI18n } from '../../i18n';
import './Sidebar.css';
@@ -805,11 +805,11 @@ const PostsList: React.FC<PostsListProps> = ({ mode, isActive }) => {
// Click handlers for tabs
const handlePostClick = (postId: string) => {
openTab({ type: 'post', id: postId, isTransient: true });
openEntityTab(openTab, 'post', postId, 'preview');
};
const handlePostDoubleClick = (postId: string) => {
openTab({ type: 'post', id: postId, isTransient: false });
openEntityTab(openTab, 'post', postId, 'pin');
};
return (
@@ -1104,11 +1104,11 @@ const MediaList: React.FC = () => {
};
const handleMediaClick = (mediaId: string) => {
openTab({ type: 'media', id: mediaId, isTransient: true });
openEntityTab(openTab, 'media', mediaId, 'preview');
};
const handleMediaDoubleClick = (mediaId: string) => {
openTab({ type: 'media', id: mediaId, isTransient: false });
openEntityTab(openTab, 'media', mediaId, 'pin');
};
// Determine which media to display
@@ -1430,7 +1430,7 @@ const ChatList: React.FC = () => {
const conversation = await window.electronAPI?.chat.createConversation();
if (conversation) {
setConversations(prev => [conversation, ...prev]);
openTab({ type: 'chat', id: conversation.id, isTransient: false });
openChatTab(openTab, conversation.id);
}
} catch (error) {
console.error('Failed to create conversation:', error);
@@ -1439,7 +1439,7 @@ const ChatList: React.FC = () => {
};
const handleOpenChat = (conversationId: string) => {
openTab({ type: 'chat', id: conversationId, isTransient: false });
openChatTab(openTab, conversationId);
};
const handleDeleteChat = async (conversationId: string) => {
@@ -1580,7 +1580,7 @@ const ImportList: React.FC = () => {
const def = await window.electronAPI?.importDefinitions.create();
if (def) {
setDefinitions(prev => [def, ...prev]);
openTab({ type: 'import', id: def.id, isTransient: false });
openImportTab(openTab, def.id);
}
} catch (error) {
console.error('Failed to create import definition:', error);
@@ -1589,7 +1589,7 @@ const ImportList: React.FC = () => {
};
const handleOpenDefinition = (definitionId: string) => {
openTab({ type: 'import', id: definitionId, isTransient: false });
openImportTab(openTab, definitionId);
};
const handleDeleteDefinition = async (e: React.MouseEvent, definitionId: string) => {

View File

@@ -1,22 +1,11 @@
import React, { useRef, useState, useEffect, useCallback } from 'react';
import { useAppStore, Tab } from '../../store';
import { parseGitDiffTabId } from '../../navigation/tabPolicy';
import { useI18n } from '../../i18n';
import './TabBar.css';
const MAX_CHAT_TITLE_LENGTH = 18;
function getGitDiffResource(tabId: string): string {
return tabId.startsWith('git-diff:') ? tabId.slice('git-diff:'.length) : tabId;
}
function getCommitHashFromGitDiffTabId(tabId: string): string | null {
const resource = getGitDiffResource(tabId);
if (!resource.startsWith('commit:')) {
return null;
}
return resource.slice('commit:'.length);
}
const getTabTitle = (
tab: Tab,
postTitles: Map<string, string>,
@@ -27,8 +16,7 @@ const getTabTitle = (
tr: (key: string, vars?: Record<string, string | number>) => string,
): string => {
if (tab.type === 'git-diff') {
const filePath = getGitDiffResource(tab.id);
const commitHash = getCommitHashFromGitDiffTabId(tab.id);
const { resource: filePath, commitHash } = parseGitDiffTabId(tab.id);
if (commitHash) {
const commitTitle = commitTitles.get(commitHash);
if (commitTitle) {
@@ -380,7 +368,7 @@ export const TabBar: React.FC = () => {
useEffect(() => {
const commitHashes = tabs
.filter((tab) => tab.type === 'git-diff')
.map((tab) => getCommitHashFromGitDiffTabId(tab.id))
.map((tab) => parseGitDiffTabId(tab.id).commitHash)
.filter((hash): hash is string => Boolean(hash));
if (commitHashes.length === 0 || !activeProject) {

View File

@@ -14,6 +14,10 @@ export interface CanonicalTabSpec {
isTransient: boolean;
}
export type EntityTabType = 'post' | 'media';
export type EntityTabOpenIntent = 'preview' | 'pin';
export type GitDiffResourceOpenIntent = 'preview' | 'pin';
const SINGLETON_TOOL_TAB_REGISTRY: Record<SingletonToolTabKey, CanonicalTabSpec> = {
settings: { type: 'settings', id: 'settings', isTransient: false },
tags: { type: 'tags', id: 'tags', isTransient: false },
@@ -33,3 +37,112 @@ export function openSingletonToolTab(
): void {
openTab(getSingletonToolTabSpec(key));
}
export function getEntityTabSpec(
type: EntityTabType,
id: string,
intent: EntityTabOpenIntent,
): CanonicalTabSpec {
return {
type,
id,
isTransient: intent === 'preview',
};
}
export function openEntityTab(
openTab: (tab: CanonicalTabSpec) => void,
type: EntityTabType,
id: string,
intent: EntityTabOpenIntent,
): void {
openTab(getEntityTabSpec(type, id, intent));
}
export function getChatTabSpec(conversationId: string): CanonicalTabSpec {
return {
type: 'chat',
id: conversationId,
isTransient: false,
};
}
export function openChatTab(
openTab: (tab: CanonicalTabSpec) => void,
conversationId: string,
): void {
openTab(getChatTabSpec(conversationId));
}
export function getImportTabSpec(definitionId: string): CanonicalTabSpec {
return {
type: 'import',
id: definitionId,
isTransient: false,
};
}
export function openImportTab(
openTab: (tab: CanonicalTabSpec) => void,
definitionId: string,
): void {
openTab(getImportTabSpec(definitionId));
}
export function getGitDiffFileTabId(filePath: string): string {
return `git-diff:${filePath}`;
}
export function getGitDiffCommitTabId(commitHash: string): string {
return `git-diff:commit:${commitHash}`;
}
export function getGitDiffFileTabSpec(
filePath: string,
intent: GitDiffResourceOpenIntent,
): CanonicalTabSpec {
return {
type: 'git-diff',
id: getGitDiffFileTabId(filePath),
isTransient: intent === 'preview',
};
}
export function getGitDiffCommitTabSpec(
commitHash: string,
intent: GitDiffResourceOpenIntent,
): CanonicalTabSpec {
return {
type: 'git-diff',
id: getGitDiffCommitTabId(commitHash),
isTransient: intent === 'preview',
};
}
export function openGitDiffFileTab(
openTab: (tab: CanonicalTabSpec) => void,
filePath: string,
intent: GitDiffResourceOpenIntent,
): void {
openTab(getGitDiffFileTabSpec(filePath, intent));
}
export function openGitDiffCommitTab(
openTab: (tab: CanonicalTabSpec) => void,
commitHash: string,
intent: GitDiffResourceOpenIntent,
): void {
openTab(getGitDiffCommitTabSpec(commitHash, intent));
}
export function parseGitDiffTabId(tabId: string): { resource: string; commitHash: string | null } {
const resource = tabId.startsWith('git-diff:') ? tabId.slice('git-diff:'.length) : tabId;
if (!resource.startsWith('commit:')) {
return { resource, commitHash: null };
}
return {
resource,
commitHash: resource.slice('commit:'.length),
};
}