feat: collection move #19

Merged
Sebastian merged 1 commits from feat/collection-move into main 2026-05-06 16:21:46 +00:00
6 changed files with 148 additions and 10 deletions

View File

@@ -23,6 +23,7 @@ const emit = defineEmits<{
select: [folder: CollectionObject]
createFolder: [service: ServiceObject, parentFolder: CollectionObject | null]
editFolder: [service: ServiceObject, folder: CollectionObject]
moveFolder: [service: ServiceObject, folder: CollectionObject]
deleteFolder: [service: ServiceObject, folder: CollectionObject]
}>()
@@ -43,6 +44,10 @@ const getCurrentPageLevel = (service: ServiceObject): (string | number | null)[]
// Get folders for current page level
const getCurrentPageFolders = (service: ServiceObject): CollectionObject[] => {
if (service.identifier === null) {
return []
}
const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1]
return collectionsStore.collectionsInCollection(service.provider, service.identifier, currentParent)
@@ -54,6 +59,10 @@ const hasChildren = (folder: CollectionObject): boolean => {
}
const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
if (service.identifier === null) {
return []
}
return collectionsStore.collectionsForService(service.provider, service.identifier)
}
@@ -132,6 +141,10 @@ const navigateBack = (service: ServiceObject) => {
// Get breadcrumb label for current page
const getCurrentBreadcrumb = (service: ServiceObject): string => {
if (service.identifier === null) {
return 'Folders'
}
const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1]
if (currentParent === null) return 'All Folders'
@@ -148,6 +161,10 @@ const getCurrentBreadcrumb = (service: ServiceObject): string => {
// Get current parent folder for dialog context
const getCurrentParentFolder = (service: ServiceObject): CollectionObject | null => {
if (service.identifier === null) {
return null
}
const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1]
if (currentParent === null) return null
@@ -276,6 +293,12 @@ const getCurrentParentFolder = (service: ServiceObject): CollectionObject | null
>
<v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-folder-move"
@click="emit('moveFolder', group.service, folder)"
>
<v-list-item-title>Move Folder</v-list-item-title>
</v-list-item>
<v-list-item
v-if="canDeleteFolder(folder)"
prepend-icon="mdi-delete"
@@ -433,6 +456,12 @@ const getCurrentParentFolder = (service: ServiceObject): CollectionObject | null
>
<v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-folder-move"
@click="emit('moveFolder', group.service, folder)"
>
<v-list-item-title>Move Folder</v-list-item-title>
</v-list-item>
<v-list-item
v-if="canDeleteFolder(folder)"
prepend-icon="mdi-delete"

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useMailStore } from '@/stores/mailStore'
import type { ServiceObject, CollectionObject } from '@MailManager/models'
import FolderSelectionTreeNode from './FolderSelectionTreeNode.vue'
@@ -12,6 +11,8 @@ interface Props {
title?: string
confirmText?: string
emptyText?: string
service?: ServiceObject | null
disabledFolderKeys?: string[]
}
const props = withDefaults(defineProps<Props>(), {
@@ -19,6 +20,8 @@ const props = withDefaults(defineProps<Props>(), {
title: 'Move To',
confirmText: 'Move',
emptyText: 'No folders are available.',
service: null,
disabledFolderKeys: () => [],
})
const emit = defineEmits<{
@@ -28,7 +31,6 @@ const emit = defineEmits<{
}>()
const collectionsStore = useCollectionsStore()
const servicesStore = useServicesStore()
const mailStore = useMailStore()
const selectedFolderKey = ref<string | null>(null)
@@ -50,13 +52,7 @@ interface ServiceGroup {
}
const serviceGroups = computed<ServiceGroup[]>(() => {
const context = mailStore.moveDialogService
if (!context) {
return []
}
const service = servicesStore.serviceByIdentifier(mailStore.moveDialogService)
const service = props.service ?? mailStore.moveDialogService
if (!service) {
return []
@@ -173,6 +169,7 @@ const handleConfirm = () => {
:folder="folder"
:service="group.service"
:selected-folder-key="selectedFolderKey"
:disabled-folder-keys="disabledFolderKeys"
@select="handleSelect"
/>

View File

@@ -8,6 +8,7 @@ interface Props {
folder: CollectionObject
service: ServiceObject
selectedFolderKey: string | null
disabledFolderKeys?: string[]
}
const props = defineProps<Props>()
@@ -66,6 +67,7 @@ const folderColorFor = (folder: CollectionObject): string | undefined => {
}
const key = computed(() => folderKeyFor(props.folder))
const isDisabled = computed(() => (props.disabledFolderKeys ?? []).includes(key.value))
const childFolders = computed(() => {
const serviceIdentifier = props.service.identifier
@@ -89,6 +91,10 @@ const hasChildren = computed(() => {
const isSelected = computed(() => props.selectedFolderKey === key.value)
const onSelect = () => {
if (isDisabled.value) {
return
}
emit('select', props.folder)
}
@@ -111,6 +117,7 @@ const onGroupDoubleClick = () => {
class="folder-node"
:title="folderLabelFor(folder)"
:active="isSelected"
:disabled="isDisabled"
@click="onSelect"
@dblclick.stop="onGroupDoubleClick"
>
@@ -152,6 +159,7 @@ const onGroupDoubleClick = () => {
class="folder-node"
:title="folderLabelFor(folder)"
:active="isSelected"
:disabled="isDisabled"
@click="onSelect"
>
<template #prepend>

View File

@@ -8,6 +8,7 @@ import FolderTreeView from './FolderTreeView.vue'
import FolderPageView from './FolderPageView.vue'
import CreateFolderDialog from './CreateFolderDialog.vue'
import DeleteFolderDialog from './DeleteFolderDialog.vue'
import FolderSelectionDialog from './FolderSelectionDialog.vue'
import RenameFolderDialog from './RenameFolderDialog.vue'
import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceObject } from '@MailManager/models'
@@ -44,6 +45,9 @@ const createDialogParent = ref<CollectionObject | null>(null)
const renameDialogVisible = ref(false)
const renameDialogService = ref<ServiceObject | null>(null)
const renameDialogFolder = ref<CollectionObject | null>(null)
const moveDialogVisible = ref(false)
const moveDialogService = ref<ServiceObject | null>(null)
const moveDialogFolder = ref<CollectionObject | null>(null)
const deleteDialogVisible = ref(false)
const deleteDialogService = ref<ServiceObject | null>(null)
const deleteDialogFolder = ref<CollectionObject | null>(null)
@@ -67,6 +71,12 @@ const handleEditFolder = (service: ServiceObject, folder: CollectionObject) => {
renameDialogVisible.value = true
}
const handleMoveFolder = (service: ServiceObject, folder: CollectionObject) => {
moveDialogService.value = service
moveDialogFolder.value = folder
moveDialogVisible.value = true
}
const handleDeleteFolder = (service: ServiceObject, folder: CollectionObject) => {
deleteDialogService.value = service
deleteDialogFolder.value = folder
@@ -77,6 +87,39 @@ const handleFolderRenamed = (updatedFolder: CollectionObject) => {
emit('select', updatedFolder)
}
const folderKeyFor = (folder: CollectionObject): string => {
return `${folder.provider}:${String(folder.service)}:${String(folder.identifier)}`
}
const moveDialogInvalidFolderKeys = computed(() => {
const sourceFolder = moveDialogFolder.value
if (!sourceFolder) {
return []
}
const invalidKeys = new Set<string>()
const queue: CollectionObject[] = [sourceFolder]
while (queue.length > 0) {
const currentFolder = queue.shift()
if (!currentFolder) {
continue
}
invalidKeys.add(folderKeyFor(currentFolder))
collectionsStore
.collectionsInCollection(currentFolder.provider, currentFolder.service, currentFolder.identifier)
.forEach(childFolder => {
queue.push(childFolder)
})
}
return Array.from(invalidKeys)
})
const isSameFolder = (left: CollectionObject | null | undefined, right: CollectionObject | null | undefined) => {
if (!left || !right) {
return false
@@ -95,6 +138,35 @@ const handleFolderDeleted = (deletedFolder: CollectionObject) => {
mailStore.notify(`Folder "${deletedFolder.properties.label || String(deletedFolder.identifier)}" deleted`, 'success')
}
const handleMoveDialogCancel = () => {
moveDialogVisible.value = false
}
const handleFolderMove = async (targetFolder: CollectionObject) => {
const sourceFolder = moveDialogFolder.value
if (!sourceFolder) {
return
}
try {
const movedFolder = await collectionsStore.move(folderKeyFor(targetFolder), folderKeyFor(sourceFolder))
moveDialogVisible.value = false
if (isSameFolder(props.selectedFolder, sourceFolder)) {
emit('select', movedFolder)
}
mailStore.notify(
`Folder "${sourceFolder.properties.label || String(sourceFolder.identifier)}" moved to "${targetFolder.properties.label || String(targetFolder.identifier)}"`,
'success',
)
} catch (error: unknown) {
console.error('[FolderTree] Failed to move folder:', error)
mailStore.notify(error instanceof Error ? error.message : 'Failed to move folder', 'error')
}
}
// Computed: all folders for validation
const allFolders = computed(() =>
servicesStore.servicesEnabled.flatMap(service =>
@@ -134,6 +206,7 @@ const serviceGroups = computed(() => {
@select="emit('select', $event)"
@create-folder="handleCreateFolder"
@edit-folder="handleEditFolder"
@move-folder="handleMoveFolder"
@delete-folder="handleDeleteFolder"
/>
@@ -145,6 +218,7 @@ const serviceGroups = computed(() => {
@select="emit('select', $event)"
@create-folder="handleCreateFolder"
@edit-folder="handleEditFolder"
@move-folder="handleMoveFolder"
@delete-folder="handleDeleteFolder"
/>
@@ -175,6 +249,19 @@ const serviceGroups = computed(() => {
@updated="handleFolderRenamed"
/>
<FolderSelectionDialog
v-if="moveDialogService && moveDialogFolder"
v-model="moveDialogVisible"
:service="moveDialogService"
:loading="collectionsStore.transceiving"
title="Move Folder"
confirm-text="Move Folder"
empty-text="No valid target folders are available."
:disabled-folder-keys="moveDialogInvalidFolderKeys"
@cancel="handleMoveDialogCancel"
@select="handleFolderMove"
/>
<DeleteFolderDialog
v-if="deleteDialogService && deleteDialogFolder"
v-model="deleteDialogVisible"

View File

@@ -18,6 +18,7 @@ const emit = defineEmits<{
select: [folder: CollectionObject]
createSubfolder: [service: ServiceObject, parentFolder: CollectionObject]
editFolder: [service: ServiceObject, folder: CollectionObject]
moveFolder: [service: ServiceObject, folder: CollectionObject]
deleteFolder: [service: ServiceObject, folder: CollectionObject]
}>()
@@ -140,6 +141,12 @@ const isSelected = (folder: CollectionObject): boolean => {
>
<v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-folder-move"
@click="emit('moveFolder', service, folder)"
>
<v-list-item-title>Move Folder</v-list-item-title>
</v-list-item>
<v-list-item
v-if="canDeleteFolder"
prepend-icon="mdi-delete"
@@ -164,6 +171,7 @@ const isSelected = (folder: CollectionObject): boolean => {
@select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createSubfolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)"
@move-folder="(service, folder) => emit('moveFolder', service, folder)"
@delete-folder="(service, folder) => emit('deleteFolder', service, folder)"
/>
</div>
@@ -219,6 +227,12 @@ const isSelected = (folder: CollectionObject): boolean => {
>
<v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-folder-move"
@click="emit('moveFolder', service, folder)"
>
<v-list-item-title>Move Folder</v-list-item-title>
</v-list-item>
<v-list-item
v-if="canDeleteFolder"
prepend-icon="mdi-delete"

View File

@@ -15,7 +15,7 @@ interface Props {
}>
}
const props = defineProps<Props>()
defineProps<Props>()
const collectionsStore = useCollectionsStore()
// Emits
@@ -23,6 +23,7 @@ const emit = defineEmits<{
select: [folder: CollectionObject]
createFolder: [service: ServiceObject, parentFolder: CollectionObject | null]
editFolder: [service: ServiceObject, folder: CollectionObject]
moveFolder: [service: ServiceObject, folder: CollectionObject]
deleteFolder: [service: ServiceObject, folder: CollectionObject]
}>()
@@ -75,6 +76,7 @@ const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
@select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)"
@move-folder="(service, folder) => emit('moveFolder', service, folder)"
@delete-folder="(service, folder) => emit('deleteFolder', service, folder)"
/>
@@ -157,6 +159,7 @@ const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
@select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)"
@move-folder="(service, folder) => emit('moveFolder', service, folder)"
@delete-folder="(service, folder) => emit('deleteFolder', service, folder)"
/>