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:
@@ -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
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user