diff --git a/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts b/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts index 9ad8e8787..5b156992c 100644 --- a/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts +++ b/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts @@ -1,7 +1,13 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { _ } from '@joplin/lib/locale'; -import Folder from '@joplin/lib/models/Folder'; +import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder'; import Note from '@joplin/lib/models/Note'; +import BaseItem from '@joplin/lib/models/BaseItem'; +import { ModelType } from '@joplin/lib/BaseModel'; +import Logger from '@joplin/utils/Logger'; +import shim from '@joplin/lib/shim'; + +const logger = Logger.create('commands/moveToFolder'); export const declaration: CommandDeclaration = { name: 'moveToFolder', @@ -11,19 +17,44 @@ export const declaration: CommandDeclaration = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export const runtime = (comp: any): CommandRuntime => { return { - execute: async (context: CommandContext, noteIds: string[] = null) => { - noteIds = noteIds || context.state.selectedNoteIds; + execute: async (context: CommandContext, itemIds: string[] = null) => { + itemIds = itemIds || context.state.selectedNoteIds; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const folders: any[] = await Folder.sortFolderTree(); + let allAreFolders = true; + const itemIdToType = new Map(); + for (const id of itemIds) { + const item = await BaseItem.loadItemById(id); + itemIdToType.set(id, item.type_); + + if (item.type_ !== ModelType.Folder) { + allAreFolders = false; + } + } + + const folders = await Folder.sortFolderTree(); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const startFolders: any[] = []; const maxDepth = 15; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const addOptions = (folders: any[], depth: number) => { + // It's okay for folders (but not notes) to have no parent folder: + if (allAreFolders) { + startFolders.push({ + key: '', + value: '', + label: _('None'), + indentDepth: 0, + }); + } + + const addOptions = (folders: FolderEntityWithChildren[], depth: number) => { for (let i = 0; i < folders.length; i++) { const folder = folders[i]; + + // Disallow making a folder a subfolder of itself. + if (itemIdToType.has(folder.id)) { + continue; + } + startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth }); if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth); } @@ -40,8 +71,25 @@ export const runtime = (comp: any): CommandRuntime => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied onClose: async (answer: any) => { if (answer) { - for (let i = 0; i < noteIds.length; i++) { - await Note.moveToFolder(noteIds[i], answer.value); + try { + const targetFolderId = answer.value; + for (const id of itemIds) { + if (id === targetFolderId) { + continue; + } + + const itemType = itemIdToType.get(id); + if (itemType === ModelType.Note) { + await Note.moveToFolder(id, targetFolderId); + } else if (itemType === ModelType.Folder) { + await Folder.moveToFolder(id, targetFolderId); + } else { + throw new Error(`Cannot move item with type ${itemType}`); + } + } + } catch (error) { + logger.error('Error moving items', error); + void shim.showMessageBox(`Error: ${error}`); } } comp.setState({ promptOptions: null }); diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx index 7cbb11d00..aad5bf709 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx @@ -151,6 +151,13 @@ const useOnRenderItem = (props: Props) => { } if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { + menu.append(new MenuItem({ + ...menuUtils.commandToStatefulMenuItem('moveToFolder', [itemId]), + // By default, enabled is based on the selected folder. However, the right-click + // menu can be shown for unselected folders. + enabled: true, + })); + menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', { folderId: itemId }))); menu.append(new MenuItem({ type: 'separator' }));