fix: small reworks on git log sidebar
This commit is contained in:
@@ -390,6 +390,7 @@ export class PreviewServer {
|
|||||||
categorySettings,
|
categorySettings,
|
||||||
page_title: pageContext.pageTitle,
|
page_title: pageContext.pageTitle,
|
||||||
language: pageContext.language,
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
html_theme_attribute: pageContext.htmlThemeAttribute,
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
}, this.postEngine);
|
}, this.postEngine);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
{% render 'partials/head', page_title: page_title, pico_stylesheet_href: pico_stylesheet_href %}
|
{% render 'partials/head', page_title: page_title, pico_stylesheet_href: pico_stylesheet_href %}
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<article class="single-post" data-template="single-post">
|
|
||||||
<h1>{{ post.title }}</h1>
|
<h1>{{ post.title }}</h1>
|
||||||
{% render 'partials/menu', menu_items: menu_items, language: language %}
|
{% render 'partials/menu', menu_items: menu_items, language: language %}
|
||||||
|
<article class="single-post" data-template="single-post">
|
||||||
<div class="post">{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path, language }}</div>
|
<div class="post">{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path, language }}</div>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -42,7 +42,15 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.git-sidebar-section--changes {
|
||||||
|
flex: 1 1 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.git-sidebar-history {
|
.git-sidebar-history {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-height: 50%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
border-top: 1px solid var(--vscode-editorWidget-border);
|
border-top: 1px solid var(--vscode-editorWidget-border);
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
@@ -60,6 +68,8 @@
|
|||||||
.git-sidebar-file-list {
|
.git-sidebar-file-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +120,11 @@
|
|||||||
.git-sidebar-history-list {
|
.git-sidebar-history-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 0 12px 8px;
|
padding: 0 12px 8px;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.git-sidebar-history-legend {
|
.git-sidebar-history-legend {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const GitSidebar: React.FC = () => {
|
|||||||
const commitMessageInputRef = useRef<HTMLInputElement | null>(null);
|
const commitMessageInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const statusRefreshInFlightRef = useRef(false);
|
const statusRefreshInFlightRef = useRef(false);
|
||||||
const remoteRefreshInFlightRef = useRef(false);
|
const remoteRefreshInFlightRef = useRef(false);
|
||||||
|
const loadRepoStateRequestRef = useRef(0);
|
||||||
|
|
||||||
const refreshRepoDetails = useCallback(
|
const refreshRepoDetails = useCallback(
|
||||||
async (targetProjectPath: string, options?: { background?: boolean; historyLimit?: number }) => {
|
async (targetProjectPath: string, options?: { background?: boolean; historyLimit?: number }) => {
|
||||||
@@ -189,12 +190,19 @@ export const GitSidebar: React.FC = () => {
|
|||||||
}, [activeProject]);
|
}, [activeProject]);
|
||||||
|
|
||||||
const loadRepoState = useCallback(async () => {
|
const loadRepoState = useCallback(async () => {
|
||||||
|
const requestId = ++loadRepoStateRequestRef.current;
|
||||||
|
const isCurrentRequest = () => requestId === loadRepoStateRequestRef.current;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setErrorGuidance([]);
|
setErrorGuidance([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const availability = await window.electronAPI.git.checkAvailability();
|
const availability = await window.electronAPI.git.checkAvailability();
|
||||||
|
if (!isCurrentRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!availability.gitFound) {
|
if (!availability.gitFound) {
|
||||||
setError(tr('gitSidebar.error.gitMissing'));
|
setError(tr('gitSidebar.error.gitMissing'));
|
||||||
setIsRepo(false);
|
setIsRepo(false);
|
||||||
@@ -203,6 +211,10 @@ export const GitSidebar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolvedProjectPath = await resolveProjectPath();
|
const resolvedProjectPath = await resolveProjectPath();
|
||||||
|
if (!isCurrentRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setProjectPath(resolvedProjectPath);
|
setProjectPath(resolvedProjectPath);
|
||||||
|
|
||||||
if (!resolvedProjectPath) {
|
if (!resolvedProjectPath) {
|
||||||
@@ -213,14 +225,25 @@ export const GitSidebar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const repoState = await window.electronAPI.git.getRepoState(resolvedProjectPath);
|
const repoState = await window.electronAPI.git.getRepoState(resolvedProjectPath);
|
||||||
|
if (!isCurrentRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsRepo(repoState.isRepo);
|
setIsRepo(repoState.isRepo);
|
||||||
setHasRemote(repoState.hasRemote);
|
setHasRemote(repoState.hasRemote);
|
||||||
setCurrentBranch(repoState.currentBranch || null);
|
setCurrentBranch(repoState.currentBranch || null);
|
||||||
|
|
||||||
if (repoState.isRepo) {
|
if (repoState.isRepo) {
|
||||||
await refreshRepoDetails(resolvedProjectPath);
|
await refreshRepoDetails(resolvedProjectPath);
|
||||||
|
if (!isCurrentRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (repoState.hasRemote) {
|
if (repoState.hasRemote) {
|
||||||
await refreshRemoteState(resolvedProjectPath);
|
await refreshRemoteState(resolvedProjectPath);
|
||||||
|
if (!isCurrentRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setRemoteState(null);
|
setRemoteState(null);
|
||||||
setRemoteStateError(null);
|
setRemoteStateError(null);
|
||||||
@@ -233,6 +256,10 @@ export const GitSidebar: React.FC = () => {
|
|||||||
setRemoteStateError(null);
|
setRemoteStateError(null);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
if (!isCurrentRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setError(tr('gitSidebar.error.loadRepoStatus'));
|
setError(tr('gitSidebar.error.loadRepoStatus'));
|
||||||
setIsRepo(false);
|
setIsRepo(false);
|
||||||
setHasRemote(false);
|
setHasRemote(false);
|
||||||
@@ -242,8 +269,10 @@ export const GitSidebar: React.FC = () => {
|
|||||||
setRemoteState(null);
|
setRemoteState(null);
|
||||||
setRemoteStateError(null);
|
setRemoteStateError(null);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (isCurrentRequest()) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [refreshRemoteState, refreshRepoDetails, resolveProjectPath, tr]);
|
}, [refreshRemoteState, refreshRepoDetails, resolveProjectPath, tr]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -499,7 +528,7 @@ export const GitSidebar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="git-sidebar-section">
|
<div className="git-sidebar-section git-sidebar-section--changes">
|
||||||
<div className="sidebar-section-title">{tr('gitSidebar.openChanges', { count: statusFiles.length })}</div>
|
<div className="sidebar-section-title">{tr('gitSidebar.openChanges', { count: statusFiles.length })}</div>
|
||||||
|
|
||||||
<div className="git-sidebar-commit-row">
|
<div className="git-sidebar-commit-row">
|
||||||
|
|||||||
@@ -264,6 +264,34 @@ describe('BlogGenerationEngine', () => {
|
|||||||
expect(singleContentIndex).toBeGreaterThan(singleMenuIndex);
|
expect(singleContentIndex).toBeGreaterThan(singleMenuIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders menu on generated category and tag archive pages', async () => {
|
||||||
|
const posts = [
|
||||||
|
makePost({
|
||||||
|
id: '1',
|
||||||
|
slug: 'news-post',
|
||||||
|
title: 'News Post',
|
||||||
|
categories: ['news'],
|
||||||
|
tags: ['dev'],
|
||||||
|
createdAt: new Date('2025-03-15T10:00:00Z'),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
await generate(posts, {
|
||||||
|
menu: {
|
||||||
|
items: [
|
||||||
|
{ id: 'home', title: 'Home', kind: 'home', pageSlug: 'home', children: [] },
|
||||||
|
{ id: 'news', title: 'News', kind: 'category-archive', categoryName: 'news', children: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryHtml = await readFile(path.join(tempDir, 'html', 'category', 'news', 'index.html'), 'utf-8');
|
||||||
|
const tagHtml = await readFile(path.join(tempDir, 'html', 'tag', 'dev', 'index.html'), 'utf-8');
|
||||||
|
|
||||||
|
expect(categoryHtml).toContain('class="blog-menu"');
|
||||||
|
expect(tagHtml).toContain('class="blog-menu"');
|
||||||
|
});
|
||||||
|
|
||||||
it('copies all required asset files to html/assets/ and html/images/', async () => {
|
it('copies all required asset files to html/assets/ and html/images/', async () => {
|
||||||
const result = await generate([]);
|
const result = await generate([]);
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,32 @@ describe('PreviewServer', () => {
|
|||||||
expect(singleTextIndex).toBeGreaterThan(singleMenuIndex);
|
expect(singleTextIndex).toBeGreaterThan(singleMenuIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders menu on category and tag archive pages', async () => {
|
||||||
|
const posts = [
|
||||||
|
makePost({ id: '1', slug: 'news-post', title: 'News Post', categories: ['news'], tags: ['dev'], createdAt: new Date('2025-01-03T10:00:00.000Z') }),
|
||||||
|
];
|
||||||
|
|
||||||
|
server = new PreviewServer({
|
||||||
|
postEngine: makeEngine(posts),
|
||||||
|
settingsEngine: makeSettings(50),
|
||||||
|
menuEngine: makeMenuEngine({
|
||||||
|
items: [
|
||||||
|
{ id: 'home', title: 'Home', kind: 'home', pageSlug: 'home', children: [] },
|
||||||
|
{ id: 'dev', title: 'Dev', kind: 'category-archive', categoryName: 'news', children: [] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start(0);
|
||||||
|
|
||||||
|
const categoryHtml = await (await fetch(`${server.getBaseUrl()}/category/news`)).text();
|
||||||
|
expect(categoryHtml).toContain('class="blog-menu"');
|
||||||
|
|
||||||
|
const tagHtml = await (await fetch(`${server.getBaseUrl()}/tag/dev`)).text();
|
||||||
|
expect(tagHtml).toContain('class="blog-menu"');
|
||||||
|
});
|
||||||
|
|
||||||
it('uses local CSS/JS assets and serves them from the preview server', async () => {
|
it('uses local CSS/JS assets and serves them from the preview server', async () => {
|
||||||
server = new PreviewServer({
|
server = new PreviewServer({
|
||||||
postEngine: makeEngine([makePost()]),
|
postEngine: makeEngine([makePost()]),
|
||||||
@@ -591,6 +617,14 @@ describe('PreviewServer', () => {
|
|||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
expect(html).toContain('<h1>Explicit Single Post Title</h1>');
|
expect(html).toContain('<h1>Explicit Single Post Title</h1>');
|
||||||
|
|
||||||
|
const mainIndex = html.indexOf('<main>');
|
||||||
|
const h1Index = html.indexOf('<h1>Explicit Single Post Title</h1>');
|
||||||
|
const articleIndex = html.indexOf('<article class="single-post" data-template="single-post">');
|
||||||
|
expect(mainIndex).toBeGreaterThan(-1);
|
||||||
|
expect(h1Index).toBeGreaterThan(mainIndex);
|
||||||
|
expect(articleIndex).toBeGreaterThan(mainIndex);
|
||||||
|
expect(h1Index).toBeLessThan(articleIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses blog description as h1 on first date archive page and date range h1 on later pages', async () => {
|
it('uses blog description as h1 on first date archive page and date range h1 on later pages', async () => {
|
||||||
|
|||||||
22
tests/renderer/components/GitSidebar.styles.test.ts
Normal file
22
tests/renderer/components/GitSidebar.styles.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
|
describe('GitSidebar styles', () => {
|
||||||
|
const cssPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../../src/renderer/components/GitSidebar/GitSidebar.css'
|
||||||
|
);
|
||||||
|
|
||||||
|
it('limits version history section to 50% of available sidebar room', () => {
|
||||||
|
const css = fs.readFileSync(cssPath, 'utf8');
|
||||||
|
|
||||||
|
expect(css).toMatch(/\.git-sidebar-history\s*\{[^}]*flex:\s*0\s+0\s+50%;[^}]*max-height:\s*50%;[^}]*\}/s);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps commit list scrollable within the bounded history section', () => {
|
||||||
|
const css = fs.readFileSync(cssPath, 'utf8');
|
||||||
|
|
||||||
|
expect(css).toMatch(/\.git-sidebar-history-list\s*\{[^}]*overflow:\s*auto;[^}]*\}/s);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -73,6 +73,49 @@ describe('GitSidebar', () => {
|
|||||||
expect(screen.getByRole('button', { name: /initialize git/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /initialize git/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores stale load results when active project becomes available during async load', async () => {
|
||||||
|
useAppStore.setState({ activeProject: null });
|
||||||
|
|
||||||
|
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
||||||
|
isRepo: true,
|
||||||
|
rootPath: '/repo/path',
|
||||||
|
currentBranch: 'main',
|
||||||
|
hasRemote: false,
|
||||||
|
});
|
||||||
|
(window as any).electronAPI.git.getStatus = vi.fn().mockResolvedValue({
|
||||||
|
files: [{ path: 'posts/first.md', status: 'modified' }],
|
||||||
|
counts: { untracked: 0, modified: 1, deleted: 0, renamed: 0, staged: 0, total: 1 },
|
||||||
|
});
|
||||||
|
(window as any).electronAPI.git.getHistory = vi.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
|
let checkAvailabilityCall = 0;
|
||||||
|
(window as any).electronAPI.git.checkAvailability = vi.fn().mockImplementation(async () => {
|
||||||
|
checkAvailabilityCall += 1;
|
||||||
|
if (checkAvailabilityCall === 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||||
|
}
|
||||||
|
return { gitFound: true, version: '2.49.0' };
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<GitSidebar />);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
useAppStore.setState({
|
||||||
|
activeProject: {
|
||||||
|
id: 'project-1',
|
||||||
|
name: 'Test Project',
|
||||||
|
slug: 'test-project',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByText(/open changes/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/no active project selected/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders open changes list when repository exists', async () => {
|
it('renders open changes list when repository exists', async () => {
|
||||||
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
||||||
isRepo: true,
|
isRepo: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user