✨ context menu for file tree
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { readdir, readFile, writeFile, mkdir, unlink } from 'fs/promises'
|
import { readdir, readFile, writeFile, mkdir, unlink, rename as fsRename, rm } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join, dirname } from 'path'
|
||||||
import type { FileNode, RevisionMeta } from '../renderer/types/editor'
|
import type { FileNode, RevisionMeta } from '../renderer/types/editor'
|
||||||
|
|
||||||
const DRAFT_ROOT =
|
const DRAFT_ROOT =
|
||||||
@@ -215,3 +215,37 @@ export async function deleteRevision(filePath: string, revisionId: string): Prom
|
|||||||
const revPath = join(REVISIONS_DIR, slug, `${revisionId}.json`)
|
const revPath = join(REVISIONS_DIR, slug, `${revisionId}.json`)
|
||||||
await unlink(revPath)
|
await unlink(revPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── File tree mutations ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function renameFileOrDir(oldPath: string, newName: string): Promise<string> {
|
||||||
|
assertInDraftRoot(oldPath)
|
||||||
|
const isFile = oldPath.endsWith('.md')
|
||||||
|
const newPath = join(dirname(oldPath), isFile ? `${newName}.md` : newName)
|
||||||
|
assertInDraftRoot(newPath)
|
||||||
|
await fsRename(oldPath, newPath)
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFileOrDir(targetPath: string): Promise<void> {
|
||||||
|
assertInDraftRoot(targetPath)
|
||||||
|
await rm(targetPath, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMarkdownFile(parentPath: string, name: string): Promise<string> {
|
||||||
|
const dir = parentPath === '__root__' ? DRAFT_ROOT : parentPath
|
||||||
|
assertInDraftRoot(dir)
|
||||||
|
const filePath = join(dir, `${name}.md`)
|
||||||
|
assertInDraftRoot(filePath)
|
||||||
|
await writeFile(filePath, '', 'utf-8')
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSubdirectory(parentPath: string, name: string): Promise<string> {
|
||||||
|
const parent = parentPath === '__root__' ? DRAFT_ROOT : parentPath
|
||||||
|
assertInDraftRoot(parent)
|
||||||
|
const newDir = join(parent, name)
|
||||||
|
assertInDraftRoot(newDir)
|
||||||
|
await mkdir(newDir, { recursive: true })
|
||||||
|
return newDir
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ipcMain, dialog, BrowserWindow } from 'electron'
|
import { ipcMain, dialog, BrowserWindow } from 'electron'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { extname, basename } from 'path'
|
import { extname, basename } from 'path'
|
||||||
import { listDraftFiles, readMarkdownFile, writeMarkdownFile, getProjectWordCount, saveOrderFile, readSession, writeSession, saveRevision, listRevisions, loadRevision, deleteRevision } from './fileSystem'
|
import { listDraftFiles, readMarkdownFile, writeMarkdownFile, getProjectWordCount, saveOrderFile, readSession, writeSession, saveRevision, listRevisions, loadRevision, deleteRevision, renameFileOrDir, deleteFileOrDir, createMarkdownFile, createSubdirectory } from './fileSystem'
|
||||||
import { streamMessage } from './aiService'
|
import { streamMessage } from './aiService'
|
||||||
import type { AIPayload, Attachment } from '../renderer/types/editor'
|
import type { AIPayload, Attachment } from '../renderer/types/editor'
|
||||||
|
|
||||||
@@ -50,6 +50,22 @@ export function registerIpcHandlers(): void {
|
|||||||
await deleteRevision(filePath, revisionId)
|
await deleteRevision(filePath, revisionId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('fs:rename', async (_event, oldPath: string, newName: string) => {
|
||||||
|
return await renameFileOrDir(oldPath, newName)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('fs:delete', async (_event, targetPath: string) => {
|
||||||
|
await deleteFileOrDir(targetPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('fs:createFile', async (_event, parentPath: string, name: string) => {
|
||||||
|
return await createMarkdownFile(parentPath, name)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('fs:createDir', async (_event, parentPath: string, name: string) => {
|
||||||
|
return await createSubdirectory(parentPath, name)
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('fs:pickAttachments', async (event): Promise<Attachment[]> => {
|
ipcMain.handle('fs:pickAttachments', async (event): Promise<Attachment[]> => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
const result = await dialog.showOpenDialog(win!, {
|
const result = await dialog.showOpenDialog(win!, {
|
||||||
|
|||||||
@@ -69,5 +69,17 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
ipcRenderer.invoke('revisions:load', filePath, revisionId),
|
ipcRenderer.invoke('revisions:load', filePath, revisionId),
|
||||||
|
|
||||||
deleteRevision: (filePath: string, revisionId: string): Promise<void> =>
|
deleteRevision: (filePath: string, revisionId: string): Promise<void> =>
|
||||||
ipcRenderer.invoke('revisions:delete', filePath, revisionId)
|
ipcRenderer.invoke('revisions:delete', filePath, revisionId),
|
||||||
|
|
||||||
|
renameNode: (oldPath: string, newName: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('fs:rename', oldPath, newName),
|
||||||
|
|
||||||
|
deleteNode: (targetPath: string): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('fs:delete', targetPath),
|
||||||
|
|
||||||
|
createFile: (parentPath: string, name: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('fs:createFile', parentPath, name),
|
||||||
|
|
||||||
|
createDir: (parentPath: string, name: string): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('fs:createDir', parentPath, name)
|
||||||
})
|
})
|
||||||
|
|||||||
58
src/renderer/components/FileTree/ContextMenu.tsx
Normal file
58
src/renderer/components/FileTree/ContextMenu.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
label: string
|
||||||
|
action: () => void
|
||||||
|
danger?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
items: (MenuItem | 'separator')[]
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContextMenu({ x, y, items, onClose }: Props): JSX.Element {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handlePointerDown(e: MouseEvent): void {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleKeyDown(e: KeyboardEvent): void {
|
||||||
|
if (e.key === 'Escape') onClose()
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handlePointerDown)
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handlePointerDown)
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="context-menu" style={{ left: x, top: y }} ref={ref}>
|
||||||
|
{items.map((item, i) =>
|
||||||
|
item === 'separator' ? (
|
||||||
|
<div key={i} className="context-menu-separator" />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className={`context-menu-item${item.danger ? ' danger' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
onClose()
|
||||||
|
item.action()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -92,3 +92,67 @@
|
|||||||
.tree-dir-header.drag-over {
|
.tree-dir-header.drag-over {
|
||||||
box-shadow: inset 0 -2px 0 var(--accent);
|
box-shadow: inset 0 -2px 0 var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Inline rename / create input ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.tree-rename-input {
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
background: var(--bg-secondary, #1e1e1e);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
margin: 1px 4px;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Context menu ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 140px;
|
||||||
|
background: var(--bg-secondary, #1e1e1e);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 4px 0;
|
||||||
|
z-index: 9999;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.danger {
|
||||||
|
color: #e06c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.danger:hover {
|
||||||
|
background: rgba(224, 108, 108, 0.12);
|
||||||
|
color: #e06c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-separator {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useEditorStore } from '../../store/editorStore'
|
import { useEditorStore } from '../../store/editorStore'
|
||||||
import type { FileNode } from '../../types/editor'
|
import type { FileNode } from '../../types/editor'
|
||||||
|
import { ContextMenu } from './ContextMenu'
|
||||||
|
import type { MenuItem } from './ContextMenu'
|
||||||
|
|
||||||
interface DndPayload {
|
interface DndPayload {
|
||||||
name: string
|
name: string
|
||||||
@@ -16,7 +18,13 @@ interface Props {
|
|||||||
|
|
||||||
export function FileTreeNode({ node, depth, dirPath, siblings }: Props): JSX.Element {
|
export function FileTreeNode({ node, depth, dirPath, siblings }: Props): JSX.Element {
|
||||||
const [expanded, setExpanded] = useState(true)
|
const [expanded, setExpanded] = useState(true)
|
||||||
const { activeFilePath, setActiveFile, moveNode } = useEditorStore()
|
const [menuPos, setMenuPos] = useState<{ x: number; y: number } | null>(null)
|
||||||
|
const [renaming, setRenaming] = useState(false)
|
||||||
|
const [renameValue, setRenameValue] = useState('')
|
||||||
|
const [creatingChild, setCreatingChild] = useState<'file' | 'dir' | null>(null)
|
||||||
|
const [createValue, setCreateValue] = useState('')
|
||||||
|
|
||||||
|
const { activeFilePath, setActiveFile, moveNode, setFileTree, clearActiveFile } = useEditorStore()
|
||||||
|
|
||||||
const openFile = async (): Promise<void> => {
|
const openFile = async (): Promise<void> => {
|
||||||
if (node.type === 'file') {
|
if (node.type === 'file') {
|
||||||
@@ -59,13 +67,119 @@ export function FileTreeNode({ node, depth, dirPath, siblings }: Props): JSX.Ele
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleContextMenu(e: React.MouseEvent): void {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setMenuPos({ x: e.clientX, y: e.clientY })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTree(): Promise<void> {
|
||||||
|
const tree = await window.api.listFiles()
|
||||||
|
setFileTree(tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRename(): void {
|
||||||
|
setRenameValue(node.name)
|
||||||
|
setRenaming(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRename(): Promise<void> {
|
||||||
|
const trimmed = renameValue.trim()
|
||||||
|
setRenaming(false)
|
||||||
|
if (!trimmed || trimmed === node.name) return
|
||||||
|
try {
|
||||||
|
const newPath = await window.api.renameNode(node.path, trimmed)
|
||||||
|
if (activeFilePath === node.path) {
|
||||||
|
const content = await window.api.readFile(newPath)
|
||||||
|
setActiveFile(newPath, content)
|
||||||
|
}
|
||||||
|
await refreshTree()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Rename failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDelete(): Promise<void> {
|
||||||
|
const label = node.type === 'directory' ? `folder "${node.name}"` : `"${node.name}"`
|
||||||
|
if (!confirm(`Delete ${label}? This cannot be undone.`)) return
|
||||||
|
try {
|
||||||
|
if (activeFilePath && (activeFilePath === node.path || activeFilePath.startsWith(node.path + '/'))) {
|
||||||
|
clearActiveFile()
|
||||||
|
}
|
||||||
|
await window.api.deleteNode(node.path)
|
||||||
|
await refreshTree()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Delete failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCreateChild(): Promise<void> {
|
||||||
|
const trimmed = createValue.trim()
|
||||||
|
setCreatingChild(null)
|
||||||
|
setCreateValue('')
|
||||||
|
if (!trimmed) return
|
||||||
|
try {
|
||||||
|
if (creatingChild === 'file') {
|
||||||
|
await window.api.createFile(node.path, trimmed)
|
||||||
|
} else {
|
||||||
|
await window.api.createDir(node.path, trimmed)
|
||||||
|
}
|
||||||
|
await refreshTree()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Create failed:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems: (MenuItem | 'separator')[] =
|
||||||
|
node.type === 'directory'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'New File',
|
||||||
|
action: () => {
|
||||||
|
setExpanded(true)
|
||||||
|
setCreatingChild('file')
|
||||||
|
setCreateValue('')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'New Folder',
|
||||||
|
action: () => {
|
||||||
|
setExpanded(true)
|
||||||
|
setCreatingChild('dir')
|
||||||
|
setCreateValue('')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'separator',
|
||||||
|
{ label: 'Rename', action: startRename },
|
||||||
|
{ label: 'Delete', action: submitDelete, danger: true }
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ label: 'Rename', action: startRename },
|
||||||
|
{ label: 'Delete', action: submitDelete, danger: true }
|
||||||
|
]
|
||||||
|
|
||||||
if (node.type === 'directory') {
|
if (node.type === 'directory') {
|
||||||
return (
|
return (
|
||||||
<div className="tree-dir">
|
<div className="tree-dir">
|
||||||
|
{renaming ? (
|
||||||
|
<input
|
||||||
|
className="tree-rename-input"
|
||||||
|
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') submitRename()
|
||||||
|
if (e.key === 'Escape') setRenaming(false)
|
||||||
|
}}
|
||||||
|
onBlur={submitRename}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
className="tree-dir-header"
|
className="tree-dir-header"
|
||||||
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
@@ -76,7 +190,31 @@ export function FileTreeNode({ node, depth, dirPath, siblings }: Props): JSX.Ele
|
|||||||
<span className="tree-arrow">{expanded ? '▾' : '▸'}</span>
|
<span className="tree-arrow">{expanded ? '▾' : '▸'}</span>
|
||||||
{node.name}
|
{node.name}
|
||||||
</button>
|
</button>
|
||||||
{expanded && node.children?.map((child) => (
|
)}
|
||||||
|
{expanded && (
|
||||||
|
<>
|
||||||
|
{creatingChild && (
|
||||||
|
<input
|
||||||
|
className="tree-rename-input"
|
||||||
|
style={{ paddingLeft: `${(depth + 1) * 12 + 20}px` }}
|
||||||
|
value={createValue}
|
||||||
|
onChange={(e) => setCreateValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') submitCreateChild()
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setCreatingChild(null)
|
||||||
|
setCreateValue('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setCreatingChild(null)
|
||||||
|
setCreateValue('')
|
||||||
|
}}
|
||||||
|
placeholder={creatingChild === 'file' ? 'file name' : 'folder name'}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{node.children?.map((child) => (
|
||||||
<FileTreeNode
|
<FileTreeNode
|
||||||
key={child.path}
|
key={child.path}
|
||||||
node={child}
|
node={child}
|
||||||
@@ -85,17 +223,46 @@ export function FileTreeNode({ node, depth, dirPath, siblings }: Props): JSX.Ele
|
|||||||
siblings={node.children!}
|
siblings={node.children!}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{menuPos && (
|
||||||
|
<ContextMenu
|
||||||
|
x={menuPos.x}
|
||||||
|
y={menuPos.y}
|
||||||
|
items={menuItems}
|
||||||
|
onClose={() => setMenuPos(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActive = activeFilePath === node.path
|
const isActive = activeFilePath === node.path
|
||||||
|
|
||||||
|
if (renaming) {
|
||||||
return (
|
return (
|
||||||
|
<input
|
||||||
|
className="tree-rename-input"
|
||||||
|
style={{ paddingLeft: `${depth * 12 + 20}px` }}
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') submitRename()
|
||||||
|
if (e.key === 'Escape') setRenaming(false)
|
||||||
|
}}
|
||||||
|
onBlur={submitRename}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
className={`tree-file${isActive ? ' active' : ''}`}
|
className={`tree-file${isActive ? ' active' : ''}`}
|
||||||
style={{ paddingLeft: `${depth * 12 + 20}px` }}
|
style={{ paddingLeft: `${depth * 12 + 20}px` }}
|
||||||
onClick={openFile}
|
onClick={openFile}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
title={node.name}
|
title={node.name}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
@@ -106,5 +273,14 @@ export function FileTreeNode({ node, depth, dirPath, siblings }: Props): JSX.Ele
|
|||||||
>
|
>
|
||||||
{node.name}
|
{node.name}
|
||||||
</button>
|
</button>
|
||||||
|
{menuPos && (
|
||||||
|
<ContextMenu
|
||||||
|
x={menuPos.x}
|
||||||
|
y={menuPos.y}
|
||||||
|
items={menuItems}
|
||||||
|
onClose={() => setMenuPos(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ interface EditorState {
|
|||||||
// File tree reordering
|
// File tree reordering
|
||||||
moveNode: (dirPath: string, fromIdx: number, toIdx: number) => void
|
moveNode: (dirPath: string, fromIdx: number, toIdx: number) => void
|
||||||
|
|
||||||
|
// Clear active file (e.g. after deletion)
|
||||||
|
clearActiveFile: () => void
|
||||||
|
|
||||||
// Word counts
|
// Word counts
|
||||||
projectWordCount: number
|
projectWordCount: number
|
||||||
setProjectWordCount: (count: number) => void
|
setProjectWordCount: (count: number) => void
|
||||||
@@ -115,6 +118,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearActiveFile: () => set({ activeFilePath: null, activeFileContent: '', isDirty: false }),
|
||||||
|
|
||||||
activeFilePath: null,
|
activeFilePath: null,
|
||||||
activeFileContent: '',
|
activeFileContent: '',
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
|
|||||||
4
src/renderer/types/global.d.ts
vendored
4
src/renderer/types/global.d.ts
vendored
@@ -18,6 +18,10 @@ declare global {
|
|||||||
listRevisions: (filePath: string) => Promise<RevisionMeta[]>
|
listRevisions: (filePath: string) => Promise<RevisionMeta[]>
|
||||||
loadRevision: (filePath: string, revisionId: string) => Promise<string>
|
loadRevision: (filePath: string, revisionId: string) => Promise<string>
|
||||||
deleteRevision: (filePath: string, revisionId: string) => Promise<void>
|
deleteRevision: (filePath: string, revisionId: string) => Promise<void>
|
||||||
|
renameNode: (oldPath: string, newName: string) => Promise<string>
|
||||||
|
deleteNode: (targetPath: string) => Promise<void>
|
||||||
|
createFile: (parentPath: string, name: string) => Promise<string>
|
||||||
|
createDir: (parentPath: string, name: string) => Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user