fix: optimize git log actions

This commit is contained in:
2026-02-17 13:24:25 +01:00
parent b13eba025a
commit 449374b79f
4 changed files with 76 additions and 23 deletions

View File

@@ -2,8 +2,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useAppStore } from '../../store'; import { useAppStore } from '../../store';
import './Panel.css'; import './Panel.css';
type PanelTab = 'tasks' | 'output' | 'git-log';
function getPostRelativePath(createdAt: string, slug: string): string | null { function getPostRelativePath(createdAt: string, slug: string): string | null {
const createdDate = new Date(createdAt); const createdDate = new Date(createdAt);
if (Number.isNaN(createdDate.getTime())) { if (Number.isNaN(createdDate.getTime())) {
@@ -36,8 +34,7 @@ function toRelativePath(absolutePath: string, projectPath: string): string {
} }
export const Panel: React.FC = () => { export const Panel: React.FC = () => {
const { panelVisible, tasks, tabs, activeTabId, posts, media, activeProject } = useAppStore(); const { panelVisible, panelActiveTab, setPanelActiveTab, tasks, tabs, activeTabId, posts, media, activeProject } = useAppStore();
const [activePanelTab, setActivePanelTab] = useState<PanelTab>('tasks');
const [gitLogLoading, setGitLogLoading] = useState(false); const [gitLogLoading, setGitLogLoading] = useState(false);
const [gitLogError, setGitLogError] = useState<string | null>(null); const [gitLogError, setGitLogError] = useState<string | null>(null);
const [gitLogTargetLabel, setGitLogTargetLabel] = useState<string | null>(null); const [gitLogTargetLabel, setGitLogTargetLabel] = useState<string | null>(null);
@@ -53,14 +50,17 @@ export const Panel: React.FC = () => {
const recentTasks = tasks.slice(-10).reverse(); const recentTasks = tasks.slice(-10).reverse();
const activeEditorTab = useMemo(() => tabs.find((tab) => tab.id === activeTabId) ?? null, [tabs, activeTabId]); const activeEditorTab = useMemo(() => tabs.find((tab) => tab.id === activeTabId) ?? null, [tabs, activeTabId]);
const canActivateGitLog = activeEditorTab?.type === 'post' || activeEditorTab?.type === 'media'; const canActivateGitLog = activeEditorTab?.type === 'post' || activeEditorTab?.type === 'media';
const effectiveActivePanelTab = panelActiveTab === 'git-log' && !canActivateGitLog
? 'tasks'
: panelActiveTab;
useEffect(() => { useEffect(() => {
if (!canActivateGitLog && activePanelTab === 'git-log') { if (!panelVisible || effectiveActivePanelTab !== 'git-log') {
setActivePanelTab('tasks'); setGitLogLoading(false);
setGitLogError(null);
return;
} }
}, [canActivateGitLog, activePanelTab]);
useEffect(() => {
const projectPath = activeProject?.dataPath; const projectPath = activeProject?.dataPath;
if (!projectPath || !activeEditorTab || (activeEditorTab.type !== 'post' && activeEditorTab.type !== 'media')) { if (!projectPath || !activeEditorTab || (activeEditorTab.type !== 'post' && activeEditorTab.type !== 'media')) {
setGitLogEntries([]); setGitLogEntries([]);
@@ -140,7 +140,7 @@ export const Panel: React.FC = () => {
}; };
void loadFileHistory(); void loadFileHistory();
}, [activeEditorTab, activeProject?.dataPath, posts, media]); }, [panelVisible, effectiveActivePanelTab, activeEditorTab, activeProject?.dataPath, posts, media]);
if (!panelVisible) { if (!panelVisible) {
return null; return null;
@@ -153,30 +153,30 @@ export const Panel: React.FC = () => {
<button <button
type="button" type="button"
role="tab" role="tab"
className={`panel-tab ${activePanelTab === 'tasks' ? 'active' : ''}`} className={`panel-tab ${effectiveActivePanelTab === 'tasks' ? 'active' : ''}`}
aria-selected={activePanelTab === 'tasks'} aria-selected={effectiveActivePanelTab === 'tasks'}
onClick={() => setActivePanelTab('tasks')} onClick={() => setPanelActiveTab('tasks')}
> >
Tasks Tasks
</button> </button>
<button <button
type="button" type="button"
role="tab" role="tab"
className={`panel-tab ${activePanelTab === 'output' ? 'active' : ''}`} className={`panel-tab ${effectiveActivePanelTab === 'output' ? 'active' : ''}`}
aria-selected={activePanelTab === 'output'} aria-selected={effectiveActivePanelTab === 'output'}
onClick={() => setActivePanelTab('output')} onClick={() => setPanelActiveTab('output')}
> >
Output Output
</button> </button>
<button <button
type="button" type="button"
role="tab" role="tab"
className={`panel-tab ${activePanelTab === 'git-log' ? 'active' : ''}`} className={`panel-tab ${effectiveActivePanelTab === 'git-log' ? 'active' : ''}`}
aria-selected={activePanelTab === 'git-log'} aria-selected={effectiveActivePanelTab === 'git-log'}
aria-disabled={!canActivateGitLog} aria-disabled={!canActivateGitLog}
onClick={() => { onClick={() => {
if (canActivateGitLog) { if (canActivateGitLog) {
setActivePanelTab('git-log'); setPanelActiveTab('git-log');
} }
}} }}
> >
@@ -192,7 +192,7 @@ export const Panel: React.FC = () => {
</button> </button>
</div> </div>
<div className="panel-content"> <div className="panel-content">
{activePanelTab === 'tasks' && ( {effectiveActivePanelTab === 'tasks' && (
recentTasks.length === 0 ? ( recentTasks.length === 0 ? (
<div className="panel-empty">No recent tasks</div> <div className="panel-empty">No recent tasks</div>
) : ( ) : (
@@ -230,11 +230,11 @@ export const Panel: React.FC = () => {
) )
)} )}
{activePanelTab === 'output' && ( {effectiveActivePanelTab === 'output' && (
<div className="panel-empty">No output</div> <div className="panel-empty">No output</div>
)} )}
{activePanelTab === 'git-log' && ( {effectiveActivePanelTab === 'git-log' && (
!canActivateGitLog ? ( !canActivateGitLog ? (
<div className="panel-empty">Open a post or media editor to view git log</div> <div className="panel-empty">Open a post or media editor to view git log</div>
) : gitLogLoading ? ( ) : gitLogLoading ? (

View File

@@ -39,6 +39,7 @@ export type { DeleteReference, ConfirmDeleteDetails };
export type EditorMode = 'wysiwyg' | 'markdown' | 'preview'; export type EditorMode = 'wysiwyg' | 'markdown' | 'preview';
export type GitDiffViewStyle = 'inline' | 'side-by-side'; export type GitDiffViewStyle = 'inline' | 'side-by-side';
export type PanelTab = 'tasks' | 'output' | 'git-log';
export interface GitDiffPreferences { export interface GitDiffPreferences {
wordWrap: boolean; wordWrap: boolean;
@@ -60,6 +61,7 @@ interface AppState {
activeView: 'posts' | 'pages' | 'media' | 'settings' | 'tags' | 'chat' | 'import' | 'git'; activeView: 'posts' | 'pages' | 'media' | 'settings' | 'tags' | 'chat' | 'import' | 'git';
sidebarVisible: boolean; sidebarVisible: boolean;
panelVisible: boolean; panelVisible: boolean;
panelActiveTab: PanelTab;
selectedPostId: string | null; selectedPostId: string | null;
selectedMediaId: string | null; selectedMediaId: string | null;
preferredEditorMode: EditorMode; preferredEditorMode: EditorMode;
@@ -107,6 +109,7 @@ interface AppState {
setActiveView: (view: 'posts' | 'pages' | 'media' | 'settings' | 'tags' | 'chat' | 'import' | 'git') => void; setActiveView: (view: 'posts' | 'pages' | 'media' | 'settings' | 'tags' | 'chat' | 'import' | 'git') => void;
toggleSidebar: () => void; toggleSidebar: () => void;
togglePanel: () => void; togglePanel: () => void;
setPanelActiveTab: (tab: PanelTab) => void;
setSelectedPost: (id: string | null) => void; setSelectedPost: (id: string | null) => void;
setSelectedMedia: (id: string | null) => void; setSelectedMedia: (id: string | null) => void;
setPreferredEditorMode: (mode: EditorMode) => void; setPreferredEditorMode: (mode: EditorMode) => void;
@@ -159,6 +162,7 @@ export const useAppStore = create<AppState>()(
activeView: 'posts', activeView: 'posts',
sidebarVisible: true, sidebarVisible: true,
panelVisible: false, panelVisible: false,
panelActiveTab: 'tasks',
selectedPostId: null, selectedPostId: null,
selectedMediaId: null, selectedMediaId: null,
preferredEditorMode: 'wysiwyg', preferredEditorMode: 'wysiwyg',
@@ -281,6 +285,7 @@ export const useAppStore = create<AppState>()(
setActiveView: (view) => set({ activeView: view }), setActiveView: (view) => set({ activeView: view }),
toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })), toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })),
togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })), togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })),
setPanelActiveTab: (panelActiveTab) => set({ panelActiveTab }),
setSelectedPost: (id) => set({ selectedPostId: id }), setSelectedPost: (id) => set({ selectedPostId: id }),
setSelectedMedia: (id) => set({ selectedMediaId: id }), setSelectedMedia: (id) => set({ selectedMediaId: id }),
setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }), setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }),
@@ -367,6 +372,7 @@ export const useAppStore = create<AppState>()(
activeView: state.activeView, activeView: state.activeView,
sidebarVisible: state.sidebarVisible, sidebarVisible: state.sidebarVisible,
panelVisible: state.panelVisible, panelVisible: state.panelVisible,
panelActiveTab: state.panelActiveTab,
selectedPostId: state.selectedPostId, selectedPostId: state.selectedPostId,
selectedMediaId: state.selectedMediaId, selectedMediaId: state.selectedMediaId,
preferredEditorMode: state.preferredEditorMode, preferredEditorMode: state.preferredEditorMode,
@@ -385,6 +391,7 @@ export const useAppStore = create<AppState>()(
...persistedState, ...persistedState,
tabs: persistedState.tabs || [], tabs: persistedState.tabs || [],
activeTabId: persistedState.activeTabId || null, activeTabId: persistedState.activeTabId || null,
panelActiveTab: persistedState.panelActiveTab || current.panelActiveTab,
dirtyPosts: new Set(persistedState.dirtyPosts || []), dirtyPosts: new Set(persistedState.dirtyPosts || []),
gitDiffPreferences: persistedState.gitDiffPreferences || current.gitDiffPreferences, gitDiffPreferences: persistedState.gitDiffPreferences || current.gitDiffPreferences,
}; };

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { act, render, screen } from '@testing-library/react'; import { act, render, screen, fireEvent } from '@testing-library/react';
import { Panel } from '../../../src/renderer/components/Panel/Panel'; import { Panel } from '../../../src/renderer/components/Panel/Panel';
import { useAppStore } from '../../../src/renderer/store'; import { useAppStore } from '../../../src/renderer/store';
import type { PostData, MediaData } from '../../../src/main/shared/electronApi'; import type { PostData, MediaData } from '../../../src/main/shared/electronApi';
@@ -54,6 +54,7 @@ describe('Panel', () => {
useAppStore.setState({ useAppStore.setState({
panelVisible: true, panelVisible: true,
panelActiveTab: 'tasks',
tasks: [], tasks: [],
activeProject: { activeProject: {
id: 'project-1', id: 'project-1',
@@ -72,7 +73,7 @@ describe('Panel', () => {
}); });
afterEach(() => { afterEach(() => {
useAppStore.setState({ panelVisible: false }); useAppStore.setState({ panelVisible: false, panelActiveTab: 'tasks' });
}); });
it('renders a Git Log tab label instead of Sync Log', () => { it('renders a Git Log tab label instead of Sync Log', () => {
@@ -107,6 +108,8 @@ describe('Panel', () => {
render(<Panel />); render(<Panel />);
fireEvent.click(screen.getByRole('tab', { name: 'Git Log' }));
await vi.waitFor(() => { await vi.waitFor(() => {
expect(getFileHistory).toHaveBeenCalledWith('/repo/path', 'posts/2026/02/first-post.md', 50); expect(getFileHistory).toHaveBeenCalledWith('/repo/path', 'posts/2026/02/first-post.md', 50);
}); });
@@ -123,6 +126,30 @@ describe('Panel', () => {
}); });
}); });
it('does not load git history when panel is closed', async () => {
const getFileHistory = vi.fn().mockResolvedValue([]);
(window as any).electronAPI.git.getFileHistory = getFileHistory;
useAppStore.setState({ panelVisible: false, panelActiveTab: 'git-log' });
render(<Panel />);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(getFileHistory).not.toHaveBeenCalled();
});
it('does not load git history when Git Log tab is not active', async () => {
const getFileHistory = vi.fn().mockResolvedValue([]);
(window as any).electronAPI.git.getFileHistory = getFileHistory;
useAppStore.setState({ panelActiveTab: 'tasks' });
render(<Panel />);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(getFileHistory).not.toHaveBeenCalled();
});
it('disables Git Log tab when focused tab is not a post or media editor', () => { it('disables Git Log tab when focused tab is not a post or media editor', () => {
useAppStore.setState({ useAppStore.setState({
tabs: [{ type: 'settings', id: 'settings', isTransient: false }], tabs: [{ type: 'settings', id: 'settings', isTransient: false }],
@@ -133,4 +160,17 @@ describe('Panel', () => {
expect(screen.getByRole('tab', { name: 'Git Log' })).toHaveAttribute('aria-disabled', 'true'); expect(screen.getByRole('tab', { name: 'Git Log' })).toHaveAttribute('aria-disabled', 'true');
}); });
it('restores the last selected panel tab after remounting the panel', () => {
const firstRender = render(<Panel />);
fireEvent.click(screen.getByRole('tab', { name: 'Output' }));
expect(screen.getByRole('tab', { name: 'Output' })).toHaveAttribute('aria-selected', 'true');
firstRender.unmount();
render(<Panel />);
expect(screen.getByRole('tab', { name: 'Output' })).toHaveAttribute('aria-selected', 'true');
});
}); });

View File

@@ -167,6 +167,12 @@ describe('AppStore', () => {
expect(getStore().preferredEditorMode).toBe('markdown'); expect(getStore().preferredEditorMode).toBe('markdown');
}); });
it('should set active panel tab', () => {
getStore().setPanelActiveTab('output');
expect(getStore().panelActiveTab).toBe('output');
});
it('should default git diff preferences to wrapped inline and visible unchanged regions', () => { it('should default git diff preferences to wrapped inline and visible unchanged regions', () => {
expect(getStore().gitDiffPreferences).toEqual({ expect(getStore().gitDiffPreferences).toEqual({
wordWrap: true, wordWrap: true,