fix: optimize git log actions
This commit is contained in:
@@ -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 ? (
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user