fixed favorites bug

This commit is contained in:
Gamosoft 2025-12-15 12:18:14 +01:00
parent d5ea6f717f
commit 0cfa752eb7
4 changed files with 160 additions and 66 deletions

View File

@ -641,10 +641,10 @@ async def move_note_endpoint(request: Request, data: dict):
if not old_path or not new_path:
raise HTTPException(status_code=400, detail="Both oldPath and newPath required")
success = move_note(config['storage']['notes_dir'], old_path, new_path)
success, error_msg = move_note(config['storage']['notes_dir'], old_path, new_path)
if not success:
raise HTTPException(status_code=500, detail="Failed to move note")
raise HTTPException(status_code=400, detail=error_msg or "Failed to move note")
# Run plugin hooks
plugin_manager.run_hook('on_note_save', note_path=new_path, content='')
@ -672,10 +672,10 @@ async def move_folder_endpoint(request: Request, data: dict):
if not old_path or not new_path:
raise HTTPException(status_code=400, detail="Both oldPath and newPath required")
success = move_folder(config['storage']['notes_dir'], old_path, new_path)
success, error_msg = move_folder(config['storage']['notes_dir'], old_path, new_path)
if not success:
raise HTTPException(status_code=500, detail="Failed to move folder")
raise HTTPException(status_code=400, detail=error_msg or "Failed to move folder")
return {
"success": True,
@ -700,10 +700,10 @@ async def rename_folder_endpoint(request: Request, data: dict):
if not old_path or not new_path:
raise HTTPException(status_code=400, detail="Both oldPath and newPath required")
success = rename_folder(config['storage']['notes_dir'], old_path, new_path)
success, error_msg = rename_folder(config['storage']['notes_dir'], old_path, new_path)
if not success:
raise HTTPException(status_code=500, detail="Failed to rename folder")
raise HTTPException(status_code=400, detail=error_msg or "Failed to rename folder")
return {
"success": True,

View File

@ -73,57 +73,70 @@ def get_all_folders(notes_dir: str) -> List[str]:
return sorted(folders)
def move_note(notes_dir: str, old_path: str, new_path: str) -> bool:
"""Move a note to a different location"""
def move_note(notes_dir: str, old_path: str, new_path: str) -> tuple[bool, str]:
"""Move a note to a different location
Returns:
Tuple of (success: bool, error_message: str)
"""
old_full_path = Path(notes_dir) / old_path
new_full_path = Path(notes_dir) / new_path
# Security checks
if not validate_path_security(notes_dir, old_full_path) or \
not validate_path_security(notes_dir, new_full_path):
return False
if not validate_path_security(notes_dir, old_full_path):
return False, "Invalid source path"
if not validate_path_security(notes_dir, new_full_path):
return False, "Invalid destination path"
if not old_full_path.exists():
return False
return False, f"Source note does not exist: {old_path}"
# Check if target already exists (prevent overwriting)
if new_full_path.exists():
return False
return False, f"A note already exists at: {new_path}"
# Invalidate cache for old path
old_key = str(old_full_path)
if old_key in _tag_cache:
del _tag_cache[old_key]
try:
# Create parent directory if needed
new_full_path.parent.mkdir(parents=True, exist_ok=True)
# Move the file
old_full_path.rename(new_full_path)
except Exception as e:
return False, f"Failed to move file: {str(e)}"
# Note: We don't automatically delete empty folders to preserve user's folder structure
return True
return True, ""
def move_folder(notes_dir: str, old_path: str, new_path: str) -> bool:
"""Move a folder to a different location"""
def move_folder(notes_dir: str, old_path: str, new_path: str) -> tuple[bool, str]:
"""Move a folder to a different location
Returns:
Tuple of (success: bool, error_message: str)
"""
import shutil
old_full_path = Path(notes_dir) / old_path
new_full_path = Path(notes_dir) / new_path
# Security checks
if not validate_path_security(notes_dir, old_full_path) or \
not validate_path_security(notes_dir, new_full_path):
return False
if not validate_path_security(notes_dir, old_full_path):
return False, "Invalid source path"
if not validate_path_security(notes_dir, new_full_path):
return False, "Invalid destination path"
if not old_full_path.exists() or not old_full_path.is_dir():
return False
return False, f"Source folder does not exist: {old_path}"
# Check if target already exists
if new_full_path.exists():
return False
return False, f"A folder already exists at: {new_path}"
# Invalidate cache for all notes in this folder
global _tag_cache
@ -132,18 +145,21 @@ def move_folder(notes_dir: str, old_path: str, new_path: str) -> bool:
for key in keys_to_delete:
del _tag_cache[key]
try:
# Create parent directory if needed
new_full_path.parent.mkdir(parents=True, exist_ok=True)
# Move the folder
shutil.move(str(old_full_path), str(new_full_path))
except Exception as e:
return False, f"Failed to move folder: {str(e)}"
# Note: We don't automatically delete empty folders to preserve user's folder structure
return True
return True, ""
def rename_folder(notes_dir: str, old_path: str, new_path: str) -> bool:
def rename_folder(notes_dir: str, old_path: str, new_path: str) -> tuple[bool, str]:
"""Rename a folder (same as move but for clarity)"""
return move_folder(notes_dir, old_path, new_path)

View File

@ -1066,14 +1066,13 @@ function noteApp() {
if (this.favoritesSet.has(path)) {
// Remove from favorites
this.favoritesSet.delete(path);
this.favorites = this.favorites.filter(f => f !== path);
} else {
// Add to favorites
this.favoritesSet.add(path);
this.favorites.push(path);
this.favorites = [...this.favorites, path];
}
// Recreate Set from array for consistency
this.favoritesSet = new Set(this.favorites);
this.saveFavorites();
},
@ -1081,10 +1080,14 @@ function noteApp() {
get favoriteNotes() {
return this.favorites
.map(path => {
const note = this.notes.find(n => n.path === path);
// Find note by exact path or case-insensitive match
let note = this.notes.find(n => n.path === path);
if (!note) {
note = this.notes.find(n => n.path.toLowerCase() === path.toLowerCase());
}
if (!note) return null;
return {
path: note.path,
path: note.path, // Use actual path from notes (fixes case issues)
name: note.path.split('/').pop().replace('.md', ''),
folder: note.folder || ''
};
@ -1936,15 +1939,18 @@ function noteApp() {
return;
}
// IMPORTANT: Capture dragged item info immediately before async operations
// because ondragend may fire and clear these values
const draggedNotePath = this.draggedNote || (this.draggedItem?.type === 'note' ? this.draggedItem.path : null);
const draggedFolderPath = this.draggedFolder;
// Handle note drop into folder
if (this.draggedNote) {
const note = this.notes.find(n => n.path === this.draggedNote);
if (draggedNotePath) {
const note = this.notes.find(n => n.path === draggedNotePath);
if (!note) return;
// Don't allow moving images to folders
if (note.type === 'image') {
this.draggedNote = null;
this.draggedItem = null;
return;
}
@ -1952,82 +1958,112 @@ function noteApp() {
const filename = note.path.split('/').pop();
const newPath = targetFolderPath ? `${targetFolderPath}/${filename}` : filename;
if (newPath === this.draggedNote) {
this.draggedNote = null;
if (newPath === draggedNotePath) {
return;
}
// Check if this note is favorited BEFORE the async call
const wasFavorited = this.favoritesSet.has(draggedNotePath);
try {
const response = await fetch('/api/notes/move', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
oldPath: this.draggedNote,
oldPath: draggedNotePath,
newPath: newPath
})
});
if (response.ok) {
await this.loadNotes();
// Update favorites if the moved note was favorited
if (wasFavorited) {
// Update Array (create new array for reactivity)
const newFavorites = this.favorites.map(f => f === draggedNotePath ? newPath : f);
this.favorites = newFavorites;
// Recreate Set from array for consistency
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
// Keep current note open if it was the moved note
if (this.currentNote === this.draggedNote) {
const wasCurrentNote = this.currentNote === draggedNotePath;
await this.loadNotes();
if (wasCurrentNote) {
this.currentNote = newPath;
}
} else {
alert('Failed to move note.');
const errorData = await response.json().catch(() => ({}));
alert(errorData.detail || 'Failed to move note.');
}
} catch (error) {
console.error('Failed to move note:', error);
alert('Failed to move note.');
}
this.draggedNote = null;
return;
}
// Handle folder drop into folder
if (this.draggedFolder) {
if (draggedFolderPath) {
// Prevent dropping folder into itself or its subfolders
if (targetFolderPath === this.draggedFolder ||
targetFolderPath.startsWith(this.draggedFolder + '/')) {
if (targetFolderPath === draggedFolderPath ||
targetFolderPath.startsWith(draggedFolderPath + '/')) {
alert('Cannot move folder into itself or its subfolder.');
this.draggedFolder = null;
return;
}
const folderName = this.draggedFolder.split('/').pop();
const folderName = draggedFolderPath.split('/').pop();
const newPath = targetFolderPath ? `${targetFolderPath}/${folderName}` : folderName;
if (newPath === this.draggedFolder) {
this.draggedFolder = null;
if (newPath === draggedFolderPath) {
return;
}
// Capture favorites info before async call
const oldPrefix = draggedFolderPath + '/';
const newPrefix = newPath + '/';
try {
const response = await fetch('/api/folders/move', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
oldPath: this.draggedFolder,
oldPath: draggedFolderPath,
newPath: newPath
})
});
if (response.ok) {
// Update favorites that were in the moved folder
const newFavorites = this.favorites.map(f => {
if (f.startsWith(oldPrefix)) {
return f.replace(oldPrefix, newPrefix);
}
return f;
});
// Check if anything changed
if (JSON.stringify(newFavorites) !== JSON.stringify(this.favorites)) {
this.favorites = newFavorites;
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
await this.loadNotes();
// Update current note path if it was in the moved folder
if (this.currentNote && this.currentNote.startsWith(this.draggedFolder + '/')) {
this.currentNote = this.currentNote.replace(this.draggedFolder, newPath);
if (this.currentNote && this.currentNote.startsWith(oldPrefix)) {
this.currentNote = this.currentNote.replace(oldPrefix, newPrefix);
}
} else {
alert('Failed to move folder.');
const errorData = await response.json().catch(() => ({}));
alert(errorData.detail || 'Failed to move folder.');
}
} catch (error) {
console.error('Failed to move folder:', error);
alert('Failed to move folder.');
}
this.draggedFolder = null;
this.dropTarget = null;
}
},
@ -2562,9 +2598,25 @@ function noteApp() {
this.expandedFolders.add(newPath);
}
// Update favorites that were in the renamed folder
const folderPrefix = folderPath + '/';
const newFolderPrefix = newPath + '/';
const newFavorites = this.favorites.map(f => {
if (f.startsWith(folderPrefix)) {
return f.replace(folderPrefix, newFolderPrefix);
}
return f;
});
// Check if anything changed
if (JSON.stringify(newFavorites) !== JSON.stringify(this.favorites)) {
this.favorites = newFavorites;
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
// Update current note path if it's in the renamed folder
if (this.currentNote && this.currentNote.startsWith(folderPath + '/')) {
this.currentNote = this.currentNote.replace(folderPath, newPath);
if (this.currentNote && this.currentNote.startsWith(folderPrefix)) {
this.currentNote = this.currentNote.replace(folderPrefix, newFolderPrefix);
}
await this.loadNotes();
@ -2599,8 +2651,17 @@ function noteApp() {
// Remove from expanded folders
this.expandedFolders.delete(folderPath);
// Remove any favorites that were in the deleted folder
const folderPrefix = folderPath + '/';
const newFavorites = this.favorites.filter(f => !f.startsWith(folderPrefix));
if (newFavorites.length !== this.favorites.length) {
this.favorites = newFavorites;
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
// Clear current note if it was in the deleted folder
if (this.currentNote && this.currentNote.startsWith(folderPath + '/')) {
if (this.currentNote && this.currentNote.startsWith(folderPrefix)) {
this.currentNote = '';
this.noteContent = '';
}
@ -2914,6 +2975,14 @@ function noteApp() {
// Delete old note
await fetch(`/api/notes/${oldPath}`, { method: 'DELETE' });
// Update favorites if the renamed note was favorited
if (this.favoritesSet.has(oldPath)) {
const newFavorites = this.favorites.map(f => f === oldPath ? newPath : f);
this.favorites = newFavorites;
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
this.currentNote = newPath;
await this.loadNotes();
} else {
@ -2942,6 +3011,14 @@ function noteApp() {
});
if (response.ok) {
// Remove from favorites if it was favorited
if (this.favoritesSet.has(notePath)) {
const newFavorites = this.favorites.filter(f => f !== notePath);
this.favorites = newFavorites;
this.favoritesSet = new Set(newFavorites);
this.saveFavorites();
}
// If the deleted note is currently open, clear it
if (this.currentNote === notePath) {
this.currentNote = '';

View File

@ -1262,6 +1262,7 @@
class="flex items-center justify-between px-2 py-1.5 rounded cursor-pointer group"
:class="{'bg-opacity-50': currentNote === fav.path}"
:style="currentNote === fav.path ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
:title="fav.path"
@mouseenter="if (currentNote !== fav.path) $el.style.backgroundColor = 'var(--bg-hover)'"
@mouseleave="if (currentNote !== fav.path) $el.style.backgroundColor = 'transparent'"
>