feat: add note moving functionality with UI controls

- Added new API endpoint /api/vault/notes/move for moving markdown files between folders
- Implemented setupMoveNoteEndpoint with path validation, error handling, and event broadcasting
- Added move note UI component to note header with folder selection
- Updated note viewer to handle note path changes after moving
- Added moveNoteToFolder method to VaultService for client-side integration
- Modified note header layout to include move trigger
This commit is contained in:
2025-10-25 21:39:31 -04:00
parent b1da9b111d
commit 0ae9cae1eb
10 changed files with 988 additions and 7 deletions
+79
View File
@@ -18,6 +18,7 @@ import path from 'path';
// ============================================================================
// ENDPOINT X: /api/files/rename - Rename a markdown file within the same folder
// ============================================================================
export function setupRenameFileEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache) {
app.put('/api/files/rename', express.json(), (req, res) => {
try {
@@ -84,6 +85,84 @@ export function setupRenameFileEndpoint(app, vaultDir, broadcastVaultEvent, meta
});
}
// ============================================================================
// ENDPOINT X+1: /api/vault/notes/move - Move a markdown file to another folder
// ============================================================================
export function setupMoveNoteEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache) {
app.post('/api/vault/notes/move', express.json(), (req, res) => {
try {
const { notePath, newFolderPath } = req.body || {};
if (!notePath || typeof notePath !== 'string') {
return res.status(400).json({ error: 'Missing or invalid notePath' });
}
const sanitizePath = (value = '') => String(value).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
const sanitizedNotePath = sanitizePath(notePath.endsWith('.md') ? notePath : `${notePath}.md`);
if (!sanitizedNotePath || sanitizedNotePath.includes('..')) {
return res.status(400).json({ error: 'Invalid notePath' });
}
const sourceAbs = path.join(vaultDir, sanitizedNotePath);
if (!fs.existsSync(sourceAbs) || !fs.statSync(sourceAbs).isFile()) {
return res.status(404).json({ error: 'Source note not found' });
}
const sanitizedFolder = sanitizePath(typeof newFolderPath === 'string' ? newFolderPath : '');
if (sanitizedFolder.includes('..')) {
return res.status(400).json({ error: 'Invalid destination folder' });
}
if (sanitizedFolder.startsWith('__builtin__') || sanitizedFolder.startsWith('.trash')) {
return res.status(400).json({ error: 'Destination folder is not allowed' });
}
const destinationDir = sanitizedFolder ? path.join(vaultDir, sanitizedFolder) : vaultDir;
try {
fs.mkdirSync(destinationDir, { recursive: true });
} catch (mkErr) {
console.error('[POST /api/vault/notes/move] Failed to ensure destination directory:', mkErr);
return res.status(500).json({ error: 'Failed to prepare destination folder' });
}
const fileName = path.basename(sourceAbs);
const destinationAbs = path.join(destinationDir, fileName);
if (sourceAbs === destinationAbs) {
return res.status(400).json({ error: 'Destination is same as source' });
}
if (fs.existsSync(destinationAbs)) {
return res.status(409).json({ error: 'A note with this name already exists in the destination folder' });
}
try {
fs.renameSync(sourceAbs, destinationAbs);
} catch (renameErr) {
console.error('[POST /api/vault/notes/move] Move operation failed:', renameErr);
return res.status(500).json({ error: 'Failed to move note' });
}
const newRelPath = path.relative(vaultDir, destinationAbs).replace(/\\/g, '/');
try { metadataCache?.clear?.(); } catch {}
try {
broadcastVaultEvent?.({
event: 'file-move',
oldPath: sanitizedNotePath,
newPath: newRelPath,
timestamp: Date.now()
});
} catch (evtErr) {
console.warn('[POST /api/vault/notes/move] Failed to broadcast event:', evtErr);
}
return res.json({ success: true, oldPath: sanitizedNotePath, newPath: newRelPath });
} catch (error) {
console.error('[POST /api/vault/notes/move] Unexpected error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
});
}
// ============================================================================
// ENDPOINT 5: /api/folders/rename - Rename folder with validation
// ============================================================================
+5 -1
View File
@@ -36,7 +36,8 @@ import {
setupRenameFolderEndpoint,
setupDeleteFolderEndpoint,
setupCreateFolderEndpoint,
setupRenameFileEndpoint
setupRenameFileEndpoint,
setupMoveNoteEndpoint
} from './index-phase3-patch.mjs';
const __filename = fileURLToPath(import.meta.url);
@@ -1540,6 +1541,9 @@ setupRenameFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
// Setup rename file endpoint (must be before catch-all)
setupRenameFileEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
// Setup move note endpoint (must be before catch-all)
setupMoveNoteEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
// Setup delete folder endpoint (must be before catch-all)
setupDeleteFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);