feat: finished phase 3

This commit is contained in:
2026-02-16 12:11:27 +01:00
parent c5c3a55a5c
commit 9f3b5d0867
6 changed files with 193 additions and 5 deletions

View File

@@ -36,7 +36,7 @@ function detectLanguage(filePath: string): string {
} }
export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => { export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
const { activeProject } = useAppStore(); const { activeProject, gitDiffPreferences } = useAppStore();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [original, setOriginal] = useState(''); const [original, setOriginal] = useState('');
@@ -105,14 +105,17 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
height="100%" height="100%"
options={{ options={{
readOnly: true, readOnly: true,
renderSideBySide: false, renderSideBySide: gitDiffPreferences.viewStyle === 'side-by-side',
minimap: { enabled: false }, minimap: { enabled: false },
lineNumbers: 'on', lineNumbers: 'on',
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
renderOverviewRuler: true, renderOverviewRuler: true,
originalEditable: false, originalEditable: false,
diffCodeLens: false, diffCodeLens: false,
wordWrap: 'off', wordWrap: gitDiffPreferences.wordWrap ? 'on' : 'off',
hideUnchangedRegions: {
enabled: gitDiffPreferences.hideUnchangedRegions,
},
ignoreTrimWhitespace: false, ignoreTrimWhitespace: false,
}} }}
/> />

View File

@@ -90,7 +90,14 @@ const SettingSection: React.FC<{
}; };
export const SettingsView: React.FC = () => { export const SettingsView: React.FC = () => {
const { preferredEditorMode, setPreferredEditorMode, activeProject, setActiveProject } = useAppStore(); const {
preferredEditorMode,
setPreferredEditorMode,
gitDiffPreferences,
setGitDiffPreferences,
activeProject,
setActiveProject,
} = useAppStore();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [credentials, setCredentials] = useState<Credentials>(defaultCredentials); const [credentials, setCredentials] = useState<Credentials>(defaultCredentials);
const [showSecrets, setShowSecrets] = useState(false); const [showSecrets, setShowSecrets] = useState(false);
@@ -410,6 +417,65 @@ export const SettingsView: React.FC = () => {
<option value="preview">Preview (Read-only)</option> <option value="preview">Preview (Read-only)</option>
</select> </select>
</SettingRow> </SettingRow>
<SettingRow
id="diff-view-style"
label="Diff View Style"
description="Choose how Git diffs are shown by default."
>
<select
id="diff-view-style"
aria-label="Diff View Style"
value={gitDiffPreferences.viewStyle}
onChange={(e) =>
setGitDiffPreferences({
...gitDiffPreferences,
viewStyle: e.target.value as 'inline' | 'side-by-side',
})
}
>
<option value="inline">Inline</option>
<option value="side-by-side">Side by Side</option>
</select>
</SettingRow>
<SettingRow
id="diff-wrap-long-lines"
label="Wrap Long Lines in Diff"
description="Enable word wrapping for long lines in Git diffs."
>
<input
id="diff-wrap-long-lines"
aria-label="Wrap long lines in diff"
type="checkbox"
checked={gitDiffPreferences.wordWrap}
onChange={(e) =>
setGitDiffPreferences({
...gitDiffPreferences,
wordWrap: e.target.checked,
})
}
/>
</SettingRow>
<SettingRow
id="diff-hide-unchanged-regions"
label="Hide Unchanged Regions"
description="Collapse unchanged regions in Git diffs."
>
<input
id="diff-hide-unchanged-regions"
aria-label="Hide unchanged regions"
type="checkbox"
checked={gitDiffPreferences.hideUnchangedRegions}
onChange={(e) =>
setGitDiffPreferences({
...gitDiffPreferences,
hideUnchangedRegions: e.target.checked,
})
}
/>
</SettingRow>
</SettingSection> </SettingSection>
); );

View File

@@ -38,6 +38,13 @@ export interface ErrorDetails {
export type { DeleteReference, ConfirmDeleteDetails }; export type { DeleteReference, ConfirmDeleteDetails };
export type EditorMode = 'wysiwyg' | 'markdown' | 'preview'; export type EditorMode = 'wysiwyg' | 'markdown' | 'preview';
export type GitDiffViewStyle = 'inline' | 'side-by-side';
export interface GitDiffPreferences {
wordWrap: boolean;
viewStyle: GitDiffViewStyle;
hideUnchangedRegions: boolean;
}
// App State Store // App State Store
interface AppState { interface AppState {
@@ -56,6 +63,7 @@ interface AppState {
selectedPostId: string | null; selectedPostId: string | null;
selectedMediaId: string | null; selectedMediaId: string | null;
preferredEditorMode: EditorMode; preferredEditorMode: EditorMode;
gitDiffPreferences: GitDiffPreferences;
// Data // Data
posts: PostData[]; posts: PostData[];
@@ -102,6 +110,7 @@ interface AppState {
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;
setGitDiffPreferences: (preferences: GitDiffPreferences) => void;
setPosts: (posts: PostData[], hasMore?: boolean, total?: number) => void; setPosts: (posts: PostData[], hasMore?: boolean, total?: number) => void;
appendPosts: (posts: PostData[], hasMore: boolean) => void; appendPosts: (posts: PostData[], hasMore: boolean) => void;
@@ -153,6 +162,11 @@ export const useAppStore = create<AppState>()(
selectedPostId: null, selectedPostId: null,
selectedMediaId: null, selectedMediaId: null,
preferredEditorMode: 'wysiwyg', preferredEditorMode: 'wysiwyg',
gitDiffPreferences: {
wordWrap: true,
viewStyle: 'inline',
hideUnchangedRegions: false,
},
// Initial Data // Initial Data
posts: [], posts: [],
@@ -270,6 +284,7 @@ export const useAppStore = create<AppState>()(
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 }),
setGitDiffPreferences: (preferences) => set({ gitDiffPreferences: preferences }),
// Post Actions // Post Actions
setPosts: (posts, hasMore = false, total = 0) => set({ posts, hasMorePosts: hasMore, totalPosts: total }), setPosts: (posts, hasMore = false, total = 0) => set({ posts, hasMorePosts: hasMore, totalPosts: total }),
@@ -355,6 +370,7 @@ export const useAppStore = create<AppState>()(
selectedPostId: state.selectedPostId, selectedPostId: state.selectedPostId,
selectedMediaId: state.selectedMediaId, selectedMediaId: state.selectedMediaId,
preferredEditorMode: state.preferredEditorMode, preferredEditorMode: state.preferredEditorMode,
gitDiffPreferences: state.gitDiffPreferences,
// Tabs are persisted here for now (project-specific persistence handled separately) // Tabs are persisted here for now (project-specific persistence handled separately)
tabs: state.tabs, tabs: state.tabs,
activeTabId: state.activeTabId, activeTabId: state.activeTabId,
@@ -370,6 +386,7 @@ export const useAppStore = create<AppState>()(
tabs: persistedState.tabs || [], tabs: persistedState.tabs || [],
activeTabId: persistedState.activeTabId || null, activeTabId: persistedState.activeTabId || null,
dirtyPosts: new Set(persistedState.dirtyPosts || []), dirtyPosts: new Set(persistedState.dirtyPosts || []),
gitDiffPreferences: persistedState.gitDiffPreferences || current.gitDiffPreferences,
}; };
}, },
} }

View File

@@ -7,11 +7,14 @@ import { useAppStore } from '../../../src/renderer/store';
vi.mock('@monaco-editor/react', () => ({ vi.mock('@monaco-editor/react', () => ({
__esModule: true, __esModule: true,
default: (_props: unknown) => null, default: (_props: unknown) => null,
DiffEditor: (props: { original: string; modified: string; language?: string }) => ( DiffEditor: (props: { original: string; modified: string; language?: string; options?: Record<string, unknown> }) => (
<div data-testid="monaco-diff-editor"> <div data-testid="monaco-diff-editor">
<div>original:{props.original}</div> <div>original:{props.original}</div>
<div>modified:{props.modified}</div> <div>modified:{props.modified}</div>
<div>language:{props.language}</div> <div>language:{props.language}</div>
<div>renderSideBySide:{String(props.options?.renderSideBySide)}</div>
<div>wordWrap:{String(props.options?.wordWrap)}</div>
<div>hideUnchanged:{String((props.options?.hideUnchangedRegions as { enabled?: boolean } | undefined)?.enabled)}</div>
</div> </div>
), ),
})); }));
@@ -30,6 +33,11 @@ describe('GitDiffView', () => {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}, },
gitDiffPreferences: {
wordWrap: true,
viewStyle: 'inline',
hideUnchangedRegions: false,
},
}); });
(window as any).electronAPI = { (window as any).electronAPI = {
@@ -56,5 +64,8 @@ describe('GitDiffView', () => {
expect((window as any).electronAPI.git.getDiffContent).toHaveBeenCalledWith('/repo/path', 'posts/first.md'); expect((window as any).electronAPI.git.getDiffContent).toHaveBeenCalledWith('/repo/path', 'posts/first.md');
expect(screen.getByText('original:# old line')).toBeInTheDocument(); expect(screen.getByText('original:# old line')).toBeInTheDocument();
expect(screen.getByText('modified:# new line')).toBeInTheDocument(); expect(screen.getByText('modified:# new line')).toBeInTheDocument();
expect(screen.getByText('renderSideBySide:false')).toBeInTheDocument();
expect(screen.getByText('wordWrap:on')).toBeInTheDocument();
expect(screen.getByText('hideUnchanged:false')).toBeInTheDocument();
}); });
}); });

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { SettingsView } from '../../../src/renderer/components/SettingsView/SettingsView';
import { useAppStore } from '../../../src/renderer/store';
describe('SettingsView Diff Preferences', () => {
beforeEach(() => {
vi.clearAllMocks();
useAppStore.setState({
activeProject: {
id: 'project-1',
name: 'Test Project',
slug: 'test-project',
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
gitDiffPreferences: {
wordWrap: true,
viewStyle: 'inline',
hideUnchangedRegions: false,
},
});
(window as any).electronAPI = {
...(window as any).electronAPI,
app: {
...(window as any).electronAPI?.app,
getDefaultProjectPath: vi.fn().mockResolvedValue('/repo/path'),
},
meta: {
...(window as any).electronAPI?.meta,
getCategories: vi.fn().mockResolvedValue(['article', 'picture', 'aside', 'page']),
getProjectMetadata: vi.fn().mockResolvedValue({}),
},
chat: {
...(window as any).electronAPI?.chat,
getSystemPrompt: vi.fn().mockResolvedValue({ success: true, prompt: '' }),
getApiKey: vi.fn().mockResolvedValue({ hasKey: false, maskedKey: '' }),
getAvailableModels: vi.fn().mockResolvedValue({ success: true, models: [], selectedModel: '' }),
},
projects: {
...(window as any).electronAPI?.projects,
update: vi.fn().mockResolvedValue(null),
},
};
});
it('updates git diff preferences from settings controls', async () => {
render(<SettingsView />);
const viewStyle = await screen.findByLabelText(/diff view style/i);
fireEvent.change(viewStyle, { target: { value: 'side-by-side' } });
const wrapCheckbox = screen.getByLabelText(/wrap long lines in diff/i);
fireEvent.click(wrapCheckbox);
const hideCheckbox = screen.getByLabelText(/hide unchanged regions/i);
fireEvent.click(hideCheckbox);
expect(useAppStore.getState().gitDiffPreferences).toEqual({
wordWrap: false,
viewStyle: 'side-by-side',
hideUnchangedRegions: true,
});
});
});

View File

@@ -166,6 +166,28 @@ describe('AppStore', () => {
expect(getStore().preferredEditorMode).toBe('markdown'); expect(getStore().preferredEditorMode).toBe('markdown');
}); });
it('should default git diff preferences to wrapped inline and visible unchanged regions', () => {
expect(getStore().gitDiffPreferences).toEqual({
wordWrap: true,
viewStyle: 'inline',
hideUnchangedRegions: false,
});
});
it('should update git diff preferences', () => {
getStore().setGitDiffPreferences({
wordWrap: false,
viewStyle: 'side-by-side',
hideUnchangedRegions: true,
});
expect(getStore().gitDiffPreferences).toEqual({
wordWrap: false,
viewStyle: 'side-by-side',
hideUnchangedRegions: true,
});
});
}); });
describe('Type Contract', () => { describe('Type Contract', () => {