feat: add category renaming functionality (v0.1.5)

- Add double-click to rename categories (deprecated in favor of pencil icon)
- Add pencil icon on hover for intuitive category renaming
- Click pencil icon to enter inline rename mode
- Show helpful hint (Enter to save, Esc to cancel)
- Update all notes with old category name to new name
- Sync category changes to server
- Update selected category if currently viewing renamed category
- Bump version to 0.1.5
This commit is contained in:
drelich
2026-03-21 22:34:05 +01:00
parent 3e93cf2408
commit 4ef0814ccd
4 changed files with 114 additions and 16 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "nextcloud-notes-tauri", "name": "nextcloud-notes-tauri",
"private": true, "private": true,
"version": "0.1.4", "version": "0.1.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Nextcloud Notes", "productName": "Nextcloud Notes",
"version": "0.1.4", "version": "0.1.5",
"identifier": "com.davidrelich.nextcloud-notes", "identifier": "com.davidrelich.nextcloud-notes",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -220,6 +220,31 @@ function App() {
} }
}; };
const handleRenameCategory = async (oldName: string, newName: string) => {
// Update all notes with the old category to the new category
const notesToUpdate = notes.filter(note => note.category === oldName);
for (const note of notesToUpdate) {
try {
const updatedNote = { ...note, category: newName };
await syncManager.updateNote(updatedNote);
setNotes(prevNotes => prevNotes.map(n => n.id === note.id ? updatedNote : n));
} catch (error) {
console.error(`Failed to update note ${note.id}:`, error);
}
}
// Update manual categories list
setManualCategories(prev =>
prev.map(cat => cat === oldName ? newName : cat)
);
// Update selected category if it was the renamed one
if (selectedCategory === oldName) {
setSelectedCategory(newName);
}
};
const handleUpdateNote = async (updatedNote: Note) => { const handleUpdateNote = async (updatedNote: Note) => {
try { try {
await syncManager.updateNote(updatedNote); await syncManager.updateNote(updatedNote);
@@ -271,6 +296,7 @@ function App() {
selectedCategory={selectedCategory} selectedCategory={selectedCategory}
onSelectCategory={setSelectedCategory} onSelectCategory={setSelectedCategory}
onCreateCategory={handleCreateCategory} onCreateCategory={handleCreateCategory}
onRenameCategory={handleRenameCategory}
isCollapsed={isCategoriesCollapsed} isCollapsed={isCategoriesCollapsed}
onToggleCollapse={() => setIsCategoriesCollapsed(!isCategoriesCollapsed)} onToggleCollapse={() => setIsCategoriesCollapsed(!isCategoriesCollapsed)}
username={username} username={username}

View File

@@ -20,6 +20,7 @@ interface CategoriesSidebarProps {
selectedCategory: string; selectedCategory: string;
onSelectCategory: (category: string) => void; onSelectCategory: (category: string) => void;
onCreateCategory: (name: string) => void; onCreateCategory: (name: string) => void;
onRenameCategory: (oldName: string, newName: string) => void;
isCollapsed: boolean; isCollapsed: boolean;
onToggleCollapse: () => void; onToggleCollapse: () => void;
username: string; username: string;
@@ -41,6 +42,7 @@ export function CategoriesSidebar({
selectedCategory, selectedCategory,
onSelectCategory, onSelectCategory,
onCreateCategory, onCreateCategory,
onRenameCategory,
isCollapsed, isCollapsed,
onToggleCollapse, onToggleCollapse,
username, username,
@@ -58,8 +60,11 @@ export function CategoriesSidebar({
}: CategoriesSidebarProps) { }: CategoriesSidebarProps) {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [newCategoryName, setNewCategoryName] = useState(''); const [newCategoryName, setNewCategoryName] = useState('');
const [renamingCategory, setRenamingCategory] = useState<string | null>(null);
const [renameCategoryValue, setRenameCategoryValue] = useState('');
const [isSettingsCollapsed, setIsSettingsCollapsed] = useState(true); const [isSettingsCollapsed, setIsSettingsCollapsed] = useState(true);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const renameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
if (isCreating && inputRef.current) { if (isCreating && inputRef.current) {
@@ -67,6 +72,13 @@ export function CategoriesSidebar({
} }
}, [isCreating]); }, [isCreating]);
useEffect(() => {
if (renamingCategory && renameInputRef.current) {
renameInputRef.current.focus();
renameInputRef.current.select();
}
}, [renamingCategory]);
const handleCreateCategory = () => { const handleCreateCategory = () => {
if (newCategoryName.trim()) { if (newCategoryName.trim()) {
onCreateCategory(newCategoryName.trim()); onCreateCategory(newCategoryName.trim());
@@ -75,6 +87,19 @@ export function CategoriesSidebar({
} }
}; };
const handleRenameCategory = () => {
if (renameCategoryValue.trim() && renamingCategory && renameCategoryValue.trim() !== renamingCategory) {
onRenameCategory(renamingCategory, renameCategoryValue.trim());
}
setRenamingCategory(null);
setRenameCategoryValue('');
};
const startRenaming = (category: string) => {
setRenamingCategory(category);
setRenameCategoryValue(category);
};
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleCreateCategory(); handleCreateCategory();
@@ -84,6 +109,15 @@ export function CategoriesSidebar({
} }
}; };
const handleRenameKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleRenameCategory();
} else if (e.key === 'Escape') {
setRenamingCategory(null);
setRenameCategoryValue('');
}
};
if (isCollapsed) { if (isCollapsed) {
return ( return (
<button <button
@@ -143,20 +177,58 @@ export function CategoriesSidebar({
</button> </button>
{categories.map((category) => ( {categories.map((category) => (
<button renamingCategory === category ? (
key={category} <div key={category} className="space-y-1">
onClick={() => onSelectCategory(category)} <div className="flex items-center gap-2 px-3 py-2 bg-white dark:bg-gray-700 rounded-lg border border-blue-500">
className={`w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center ${ <svg className="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
selectedCategory === category <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300' </svg>
: 'hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300' <input
}`} ref={renameInputRef}
> type="text"
<svg className="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> value={renameCategoryValue}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /> onChange={(e) => setRenameCategoryValue(e.target.value)}
</svg> onKeyDown={handleRenameKeyDown}
<span className="text-sm truncate">{category}</span> onBlur={handleRenameCategory}
</button> className="flex-1 text-sm px-2 py-1 border-none bg-transparent text-gray-900 dark:text-gray-100 focus:outline-none"
/>
</div>
<div className="px-3 text-xs text-gray-500 dark:text-gray-400">
Press <kbd className="px-1 py-0.5 bg-gray-200 dark:bg-gray-600 rounded text-xs">Enter</kbd> to save, <kbd className="px-1 py-0.5 bg-gray-200 dark:bg-gray-600 rounded text-xs">Esc</kbd> to cancel
</div>
</div>
) : (
<div
key={category}
className={`group w-full px-3 py-2 rounded-lg transition-colors flex items-center ${
selectedCategory === category
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
: 'hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
<button
onClick={() => onSelectCategory(category)}
className="flex items-center flex-1 min-w-0 text-left"
>
<svg className="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<span className="text-sm truncate">{category}</span>
</button>
<button
onClick={(e) => {
e.stopPropagation();
startRenaming(category);
}}
className="ml-2 p-1 opacity-0 group-hover:opacity-100 hover:bg-gray-300 dark:hover:bg-gray-600 rounded transition-opacity flex-shrink-0"
title="Rename category"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</button>
</div>
)
))} ))}
{isCreating && ( {isCreating && (