feat: collection move #19
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user