Add git revision history panel to inspector (M4 Task 4)
Show mock git history per file: commit hash (monospace), relative date, commit message, author. Added GitCommit type, mock git history generator, and 'View all revisions' placeholder. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
src/App.tsx
27
src/App.tsx
@@ -6,7 +6,7 @@ import { Editor } from './components/Editor'
|
||||
import { Inspector } from './components/Inspector'
|
||||
import { ResizeHandle } from './components/ResizeHandle'
|
||||
import { isTauri, mockInvoke } from './mock-tauri'
|
||||
import type { VaultEntry, SidebarSelection } from './types'
|
||||
import type { VaultEntry, SidebarSelection, GitCommit } from './types'
|
||||
import './App.css'
|
||||
|
||||
// TODO: Make vault path configurable via settings
|
||||
@@ -24,6 +24,7 @@ function App() {
|
||||
const [inspectorWidth, setInspectorWidth] = useState(280)
|
||||
const [inspectorCollapsed, setInspectorCollapsed] = useState(false)
|
||||
const [allContent, setAllContent] = useState<Record<string, string>>({})
|
||||
const [gitHistory, setGitHistory] = useState<GitCommit[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const loadVault = async () => {
|
||||
@@ -56,6 +57,29 @@ function App() {
|
||||
loadVault()
|
||||
}, [])
|
||||
|
||||
// Load git history when active tab changes
|
||||
useEffect(() => {
|
||||
if (!activeTabPath) {
|
||||
setGitHistory([])
|
||||
return
|
||||
}
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
let history: GitCommit[]
|
||||
if (isTauri()) {
|
||||
history = await invoke<GitCommit[]>('get_git_history', { path: activeTabPath })
|
||||
} else {
|
||||
history = await mockInvoke<GitCommit[]>('get_git_history', { path: activeTabPath })
|
||||
}
|
||||
setGitHistory(history)
|
||||
} catch (err) {
|
||||
console.warn('Failed to load git history:', err)
|
||||
setGitHistory([])
|
||||
}
|
||||
}
|
||||
loadHistory()
|
||||
}, [activeTabPath])
|
||||
|
||||
const handleSelectNote = useCallback(async (entry: VaultEntry) => {
|
||||
// If tab already open, just switch to it
|
||||
setTabs((prev) => {
|
||||
@@ -174,6 +198,7 @@ function App() {
|
||||
content={activeTab?.content ?? null}
|
||||
entries={entries}
|
||||
allContent={allContent}
|
||||
gitHistory={gitHistory}
|
||||
onNavigate={handleNavigateWikilink}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -189,3 +189,55 @@
|
||||
color: #555;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Git History */
|
||||
.inspector__commits {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inspector__commit {
|
||||
border-left: 2px solid #333;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.inspector__commit-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.inspector__commit-hash {
|
||||
font-family: 'SF Mono', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
color: #7eb8da;
|
||||
}
|
||||
|
||||
.inspector__commit-date {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.inspector__commit-msg {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inspector__view-all {
|
||||
margin-top: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.inspector__view-all:hover {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { Inspector } from './Inspector'
|
||||
import type { VaultEntry } from '../types'
|
||||
import type { VaultEntry, GitCommit } from '../types'
|
||||
|
||||
const mockEntry: VaultEntry = {
|
||||
path: '/vault/project/test.md',
|
||||
@@ -49,6 +49,13 @@ const allContent: Record<string, string> = {
|
||||
'/vault/project/test.md': '# Test Project\n\nSome content.',
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const mockGitHistory: GitCommit[] = [
|
||||
{ hash: 'a1b2c3d', message: 'Update test with latest changes', author: 'Luca Rossi', date: now - 86400 * 2 },
|
||||
{ hash: 'e4f5g6h', message: 'Add new section to test', author: 'Luca Rossi', date: now - 86400 * 5 },
|
||||
{ hash: 'i7j8k9l', message: 'Create test', author: 'Luca Rossi', date: now - 86400 * 12 },
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
collapsed: false,
|
||||
onToggle: () => {},
|
||||
@@ -56,6 +63,7 @@ const defaultProps = {
|
||||
content: null as string | null,
|
||||
entries: [] as VaultEntry[],
|
||||
allContent: {} as Record<string, string>,
|
||||
gitHistory: [] as GitCommit[],
|
||||
onNavigate: () => {},
|
||||
}
|
||||
|
||||
@@ -180,4 +188,45 @@ describe('Inspector', () => {
|
||||
fireEvent.click(screen.getByText('Referrer Note'))
|
||||
expect(onNavigate).toHaveBeenCalledWith('Referrer Note')
|
||||
})
|
||||
|
||||
it('shows git history with commit hashes and messages', () => {
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={mockEntry}
|
||||
content={mockContent}
|
||||
gitHistory={mockGitHistory}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('History')).toBeInTheDocument()
|
||||
expect(screen.getByText('a1b2c3d')).toBeInTheDocument()
|
||||
expect(screen.getByText('Update test with latest changes')).toBeInTheDocument()
|
||||
expect(screen.getByText('e4f5g6h')).toBeInTheDocument()
|
||||
expect(screen.getByText('i7j8k9l')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows "View all revisions" placeholder button', () => {
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={mockEntry}
|
||||
content={mockContent}
|
||||
gitHistory={mockGitHistory}
|
||||
/>
|
||||
)
|
||||
const btn = screen.getByText('View all revisions')
|
||||
expect(btn).toBeDisabled()
|
||||
})
|
||||
|
||||
it('shows "No revision history" when no commits', () => {
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={mockEntry}
|
||||
content={mockContent}
|
||||
gitHistory={[]}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('No revision history')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import type { VaultEntry, GitCommit } from '../types'
|
||||
import './Inspector.css'
|
||||
|
||||
interface InspectorProps {
|
||||
@@ -9,6 +9,7 @@ interface InspectorProps {
|
||||
content: string | null
|
||||
entries: VaultEntry[]
|
||||
allContent: Record<string, string>
|
||||
gitHistory: GitCommit[]
|
||||
onNavigate: (target: string) => void
|
||||
}
|
||||
|
||||
@@ -196,7 +197,47 @@ function BacklinksPanel({ backlinks, onNavigate }: { backlinks: VaultEntry[]; on
|
||||
)
|
||||
}
|
||||
|
||||
export function Inspector({ collapsed, onToggle, entry, content, entries, allContent, onNavigate }: InspectorProps) {
|
||||
function formatRelativeDate(timestamp: number): string {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const diff = now - timestamp
|
||||
if (diff < 86400) return 'today'
|
||||
const days = Math.floor(diff / 86400)
|
||||
if (days === 1) return 'yesterday'
|
||||
if (days < 30) return `${days}d ago`
|
||||
const months = Math.floor(days / 30)
|
||||
if (months === 1) return '1mo ago'
|
||||
return `${months}mo ago`
|
||||
}
|
||||
|
||||
function GitHistoryPanel({ commits }: { commits: GitCommit[] }) {
|
||||
return (
|
||||
<div className="inspector__section">
|
||||
<h4>History</h4>
|
||||
{commits.length === 0 ? (
|
||||
<p className="inspector__empty">No revision history</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="inspector__commits">
|
||||
{commits.map((c) => (
|
||||
<div key={c.hash} className="inspector__commit">
|
||||
<div className="inspector__commit-top">
|
||||
<span className="inspector__commit-hash">{c.hash}</span>
|
||||
<span className="inspector__commit-date">{formatRelativeDate(c.date)}</span>
|
||||
</div>
|
||||
<div className="inspector__commit-msg">{c.message}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="inspector__view-all" disabled>
|
||||
View all revisions
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Inspector({ collapsed, onToggle, entry, content, entries, allContent, gitHistory, onNavigate }: InspectorProps) {
|
||||
const backlinks = useBacklinks(entry, entries, allContent)
|
||||
|
||||
return (
|
||||
@@ -214,6 +255,7 @@ export function Inspector({ collapsed, onToggle, entry, content, entries, allCon
|
||||
<PropertiesPanel entry={entry} content={content} />
|
||||
<RelationshipsPanel entry={entry} onNavigate={onNavigate} />
|
||||
<BacklinksPanel backlinks={backlinks} onNavigate={onNavigate} />
|
||||
<GitHistoryPanel commits={gitHistory} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -229,6 +271,10 @@ export function Inspector({ collapsed, onToggle, entry, content, entries, allCon
|
||||
<h4>Backlinks</h4>
|
||||
<p className="inspector__empty">No backlinks</p>
|
||||
</div>
|
||||
<div className="inspector__section">
|
||||
<h4>History</h4>
|
||||
<p className="inspector__empty">No revision history</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* this provides realistic test data so the UI can be verified visually.
|
||||
*/
|
||||
|
||||
import type { VaultEntry } from './types'
|
||||
import type { VaultEntry, GitCommit } from './types'
|
||||
|
||||
const MOCK_CONTENT: Record<string, string> = {
|
||||
'/Users/luca/Laputa/project/26q1-laputa-app.md': `---
|
||||
@@ -490,10 +490,42 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
]
|
||||
|
||||
function mockGitHistory(path: string): GitCommit[] {
|
||||
const filename = path.split('/').pop()?.replace('.md', '') ?? 'unknown'
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
return [
|
||||
{
|
||||
hash: 'a1b2c3d',
|
||||
message: `Update ${filename} with latest changes`,
|
||||
author: 'Luca Rossi',
|
||||
date: now - 86400 * 2,
|
||||
},
|
||||
{
|
||||
hash: 'e4f5g6h',
|
||||
message: `Add new section to ${filename}`,
|
||||
author: 'Luca Rossi',
|
||||
date: now - 86400 * 5,
|
||||
},
|
||||
{
|
||||
hash: 'i7j8k9l',
|
||||
message: `Fix formatting in ${filename}`,
|
||||
author: 'Luca Rossi',
|
||||
date: now - 86400 * 12,
|
||||
},
|
||||
{
|
||||
hash: 'm0n1o2p',
|
||||
message: `Create ${filename}`,
|
||||
author: 'Luca Rossi',
|
||||
date: now - 86400 * 30,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const mockHandlers: Record<string, (args: any) => any> = {
|
||||
list_vault: () => MOCK_ENTRIES,
|
||||
get_note_content: (args: { path: string }) => MOCK_CONTENT[args.path] ?? '',
|
||||
get_all_content: () => MOCK_CONTENT,
|
||||
get_git_history: (args: { path: string }) => mockGitHistory(args.path),
|
||||
}
|
||||
|
||||
export function isTauri(): boolean {
|
||||
|
||||
@@ -13,6 +13,13 @@ export interface VaultEntry {
|
||||
fileSize: number
|
||||
}
|
||||
|
||||
export interface GitCommit {
|
||||
hash: string
|
||||
message: string
|
||||
author: string
|
||||
date: number // unix timestamp
|
||||
}
|
||||
|
||||
export type SidebarSelection =
|
||||
| { kind: 'filter'; filter: 'all' | 'people' | 'events' | 'favorites' | 'trash' }
|
||||
| { kind: 'sectionGroup'; type: string }
|
||||
|
||||
Reference in New Issue
Block a user