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:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
26
src/App.tsx
26
src/App.tsx
@@ -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}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
<div key={category} className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 bg-white dark:bg-gray-700 rounded-lg border border-blue-500">
|
||||||
|
<svg className="w-4 h-4 text-gray-400 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>
|
||||||
|
<input
|
||||||
|
ref={renameInputRef}
|
||||||
|
type="text"
|
||||||
|
value={renameCategoryValue}
|
||||||
|
onChange={(e) => setRenameCategoryValue(e.target.value)}
|
||||||
|
onKeyDown={handleRenameKeyDown}
|
||||||
|
onBlur={handleRenameCategory}
|
||||||
|
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}
|
key={category}
|
||||||
onClick={() => onSelectCategory(category)}
|
className={`group w-full px-3 py-2 rounded-lg transition-colors flex items-center ${
|
||||||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center ${
|
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
? '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'
|
: '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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<span className="text-sm truncate">{category}</span>
|
<span className="text-sm truncate">{category}</span>
|
||||||
</button>
|
</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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user