feat: detect external file renames and offer wikilink update via banner
When the app regains focus, checks git diff for renames (--diff-filter=R). If renamed .md files are found, shows a non-blocking banner with "Update wikilinks" and "Ignore" buttons. The update reuses the existing vault-wide wikilink replacement logic from rename.rs. New Tauri commands: detect_renames, update_wikilinks_for_renames. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
41
src/components/RenameDetectedBanner.test.tsx
Normal file
41
src/components/RenameDetectedBanner.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { RenameDetectedBanner } from './RenameDetectedBanner'
|
||||
|
||||
describe('RenameDetectedBanner', () => {
|
||||
const renames = [
|
||||
{ old_path: 'old-note.md', new_path: 'new-note.md' },
|
||||
{ old_path: 'folder/draft.md', new_path: 'folder/published.md' },
|
||||
]
|
||||
|
||||
it('renders nothing when renames is empty', () => {
|
||||
const { container } = render(
|
||||
<RenameDetectedBanner renames={[]} onUpdate={vi.fn()} onDismiss={vi.fn()} />
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('shows banner with rename count', () => {
|
||||
render(<RenameDetectedBanner renames={renames} onUpdate={vi.fn()} onDismiss={vi.fn()} />)
|
||||
expect(screen.getByText(/2 files? renamed/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows Update wikilinks button', () => {
|
||||
render(<RenameDetectedBanner renames={renames} onUpdate={vi.fn()} onDismiss={vi.fn()} />)
|
||||
expect(screen.getByText('Update wikilinks')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onUpdate when Update button clicked', () => {
|
||||
const onUpdate = vi.fn()
|
||||
render(<RenameDetectedBanner renames={renames} onUpdate={onUpdate} onDismiss={vi.fn()} />)
|
||||
fireEvent.click(screen.getByText('Update wikilinks'))
|
||||
expect(onUpdate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('calls onDismiss when Ignore button clicked', () => {
|
||||
const onDismiss = vi.fn()
|
||||
render(<RenameDetectedBanner renames={renames} onUpdate={vi.fn()} onDismiss={onDismiss} />)
|
||||
fireEvent.click(screen.getByText('Ignore'))
|
||||
expect(onDismiss).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
38
src/components/RenameDetectedBanner.tsx
Normal file
38
src/components/RenameDetectedBanner.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ArrowsClockwise } from '@phosphor-icons/react'
|
||||
|
||||
export interface DetectedRename {
|
||||
old_path: string
|
||||
new_path: string
|
||||
}
|
||||
|
||||
interface RenameDetectedBannerProps {
|
||||
renames: DetectedRename[]
|
||||
onUpdate: () => void
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function RenameDetectedBanner({ renames, onUpdate, onDismiss }: RenameDetectedBannerProps) {
|
||||
if (renames.length === 0) return null
|
||||
|
||||
const count = renames.length
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b border-border bg-accent/50 px-4 py-2 text-[13px]">
|
||||
<ArrowsClockwise size={16} className="shrink-0 text-accent-foreground" />
|
||||
<span className="flex-1 text-foreground">
|
||||
{count} file{count !== 1 ? 's' : ''} renamed outside Laputa. Update wikilinks?
|
||||
</span>
|
||||
<button
|
||||
className="shrink-0 cursor-pointer rounded-md bg-primary px-3 py-1 text-[12px] font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
onClick={onUpdate}
|
||||
>
|
||||
Update wikilinks
|
||||
</button>
|
||||
<button
|
||||
className="shrink-0 cursor-pointer rounded-md border border-border bg-transparent px-3 py-1 text-[12px] font-medium text-muted-foreground transition-colors hover:bg-muted"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
Ignore
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user