feat: finished phase 3
This commit is contained in:
@@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
69
tests/renderer/components/SettingsView.test.tsx
Normal file
69
tests/renderer/components/SettingsView.test.tsx
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user