feat: collection delete

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-05-05 23:42:27 -04:00
parent 96002b6187
commit ab0a46f5e0
6 changed files with 253 additions and 12 deletions

View File

@@ -0,0 +1,149 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceObject } from '@MailManager/models'
interface Props {
modelValue: boolean
service: ServiceObject
folder: CollectionObject
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
deleted: [folder: CollectionObject]
}>()
const collectionsStore = useCollectionsStore()
const loading = ref(false)
const errorMessage = ref('')
const dialogValue = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value),
})
const folderLabel = computed(() => props.folder.properties.label || String(props.folder.identifier))
const hasChildren = computed(() => {
return collectionsStore.hasChildrenInCollection(props.folder.provider, props.folder.service, props.folder.identifier)
})
const resetState = () => {
loading.value = false
errorMessage.value = ''
}
watch(dialogValue, isOpen => {
if (isOpen) {
resetState()
}
})
const handleDelete = async () => {
loading.value = true
errorMessage.value = ''
try {
await collectionsStore.delete(props.folder.provider, props.folder.service, props.folder.identifier)
emit('deleted', props.folder)
dialogValue.value = false
resetState()
} catch (error: any) {
console.error('[DeleteFolderDialog] Failed to delete folder:', error)
errorMessage.value = error.message || 'Failed to delete folder. Please try again.'
} finally {
loading.value = false
}
}
const handleCancel = () => {
dialogValue.value = false
resetState()
}
</script>
<template>
<v-dialog
v-model="dialogValue"
max-width="520"
persistent
>
<v-card>
<v-card-title class="text-h5">
Delete Folder
</v-card-title>
<v-card-text>
<div class="mb-4">
<div class="text-caption text-medium-emphasis">Account</div>
<div class="text-body-2">
{{ service.label || service.primaryAddress || 'Mail Account' }}
</div>
</div>
<div class="mb-4">
<div class="text-caption text-medium-emphasis">Folder</div>
<div class="text-body-2">
{{ folderLabel }}
</div>
</div>
<p class="text-body-2 mb-0">
Confirm deletion of this folder.
</p>
<p
v-if="hasChildren"
class="text-body-2 mt-3 mb-0"
>
This folder contains subfolders. The delete request may be rejected if the provider does not allow deleting non-empty folders.
</p>
<v-alert
v-if="errorMessage"
type="error"
variant="tonal"
density="compact"
class="mt-4"
>
{{ errorMessage }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
:disabled="loading"
@click="handleCancel"
>
Cancel
</v-btn>
<v-btn
color="error"
variant="elevated"
:loading="loading"
@click="handleDelete"
>
Delete Folder
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.text-caption {
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.0333em;
}
</style>

View File

@@ -23,6 +23,7 @@ const emit = defineEmits<{
select: [folder: CollectionObject] select: [folder: CollectionObject]
createFolder: [service: ServiceObject, parentFolder: CollectionObject | null] createFolder: [service: ServiceObject, parentFolder: CollectionObject | null]
editFolder: [service: ServiceObject, folder: CollectionObject] editFolder: [service: ServiceObject, folder: CollectionObject]
deleteFolder: [service: ServiceObject, folder: CollectionObject]
}>() }>()
// Page-based navigation state per service account // Page-based navigation state per service account
@@ -56,6 +57,10 @@ const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
return collectionsStore.collectionsForService(service.provider, service.identifier) return collectionsStore.collectionsForService(service.provider, service.identifier)
} }
const canDeleteFolder = (folder: CollectionObject): boolean => {
return !folder.properties.role
}
// Get icon for folder based on role // Get icon for folder based on role
const getFolderIcon = (folder: CollectionObject): string => { const getFolderIcon = (folder: CollectionObject): string => {
switch (folder.properties.role) { switch (folder.properties.role) {
@@ -271,6 +276,14 @@ const getCurrentParentFolder = (service: ServiceObject): CollectionObject | null
> >
<v-list-item-title>New Subfolder</v-list-item-title> <v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item
v-if="canDeleteFolder(folder)"
prepend-icon="mdi-delete"
base-color="error"
@click="emit('deleteFolder', group.service, folder)"
>
<v-list-item-title>Delete Folder</v-list-item-title>
</v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
</template> </template>
@@ -420,6 +433,14 @@ const getCurrentParentFolder = (service: ServiceObject): CollectionObject | null
> >
<v-list-item-title>New Subfolder</v-list-item-title> <v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item
v-if="canDeleteFolder(folder)"
prepend-icon="mdi-delete"
base-color="error"
@click="emit('deleteFolder', group.service, folder)"
>
<v-list-item-title>Delete Folder</v-list-item-title>
</v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
</template> </template>

View File

@@ -7,6 +7,7 @@ import { useUser } from '@KTXC'
import FolderTreeView from './FolderTreeView.vue' import FolderTreeView from './FolderTreeView.vue'
import FolderPageView from './FolderPageView.vue' import FolderPageView from './FolderPageView.vue'
import CreateFolderDialog from './CreateFolderDialog.vue' import CreateFolderDialog from './CreateFolderDialog.vue'
import DeleteFolderDialog from './DeleteFolderDialog.vue'
import RenameFolderDialog from './RenameFolderDialog.vue' import RenameFolderDialog from './RenameFolderDialog.vue'
import type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceObject } from '@MailManager/models' import type { ServiceObject } from '@MailManager/models'
@@ -43,6 +44,9 @@ const createDialogParent = ref<CollectionObject | null>(null)
const renameDialogVisible = ref(false) const renameDialogVisible = ref(false)
const renameDialogService = ref<ServiceObject | null>(null) const renameDialogService = ref<ServiceObject | null>(null)
const renameDialogFolder = ref<CollectionObject | null>(null) const renameDialogFolder = ref<CollectionObject | null>(null)
const deleteDialogVisible = ref(false)
const deleteDialogService = ref<ServiceObject | null>(null)
const deleteDialogFolder = ref<CollectionObject | null>(null)
// Handle create folder event from child components // Handle create folder event from child components
const handleCreateFolder = (service: ServiceObject, parentFolder: CollectionObject | null = null) => { const handleCreateFolder = (service: ServiceObject, parentFolder: CollectionObject | null = null) => {
@@ -63,10 +67,34 @@ const handleEditFolder = (service: ServiceObject, folder: CollectionObject) => {
renameDialogVisible.value = true renameDialogVisible.value = true
} }
const handleDeleteFolder = (service: ServiceObject, folder: CollectionObject) => {
deleteDialogService.value = service
deleteDialogFolder.value = folder
deleteDialogVisible.value = true
}
const handleFolderRenamed = (updatedFolder: CollectionObject) => { const handleFolderRenamed = (updatedFolder: CollectionObject) => {
emit('select', updatedFolder) emit('select', updatedFolder)
} }
const isSameFolder = (left: CollectionObject | null | undefined, right: CollectionObject | null | undefined) => {
if (!left || !right) {
return false
}
return left.provider === right.provider &&
String(left.service) === String(right.service) &&
String(left.identifier) === String(right.identifier)
}
const handleFolderDeleted = (deletedFolder: CollectionObject) => {
if (isSameFolder(props.selectedFolder, deletedFolder)) {
mailStore.clearSelectedFolder()
}
mailStore.notify(`Folder "${deletedFolder.properties.label || String(deletedFolder.identifier)}" deleted`, 'success')
}
// Computed: all folders for validation // Computed: all folders for validation
const allFolders = computed(() => const allFolders = computed(() =>
servicesStore.servicesEnabled.flatMap(service => servicesStore.servicesEnabled.flatMap(service =>
@@ -106,6 +134,7 @@ const serviceGroups = computed(() => {
@select="emit('select', $event)" @select="emit('select', $event)"
@create-folder="handleCreateFolder" @create-folder="handleCreateFolder"
@edit-folder="handleEditFolder" @edit-folder="handleEditFolder"
@delete-folder="handleDeleteFolder"
/> />
<!-- Page-based View --> <!-- Page-based View -->
@@ -116,6 +145,7 @@ const serviceGroups = computed(() => {
@select="emit('select', $event)" @select="emit('select', $event)"
@create-folder="handleCreateFolder" @create-folder="handleCreateFolder"
@edit-folder="handleEditFolder" @edit-folder="handleEditFolder"
@delete-folder="handleDeleteFolder"
/> />
<!-- Empty state --> <!-- Empty state -->
@@ -144,6 +174,14 @@ const serviceGroups = computed(() => {
:all-folders="allFolders" :all-folders="allFolders"
@updated="handleFolderRenamed" @updated="handleFolderRenamed"
/> />
<DeleteFolderDialog
v-if="deleteDialogService && deleteDialogFolder"
v-model="deleteDialogVisible"
:service="deleteDialogService"
:folder="deleteDialogFolder"
@deleted="handleFolderDeleted"
/>
</template> </template>
<style scoped> <style scoped>

View File

@@ -18,6 +18,7 @@ const emit = defineEmits<{
select: [folder: CollectionObject] select: [folder: CollectionObject]
createSubfolder: [service: ServiceObject, parentFolder: CollectionObject] createSubfolder: [service: ServiceObject, parentFolder: CollectionObject]
editFolder: [service: ServiceObject, folder: CollectionObject] editFolder: [service: ServiceObject, folder: CollectionObject]
deleteFolder: [service: ServiceObject, folder: CollectionObject]
}>() }>()
const childFolders = computed(() => { const childFolders = computed(() => {
@@ -28,6 +29,8 @@ const hasChildren = computed(() => {
return collectionsStore.hasChildrenInCollection(props.service.provider, props.service.identifier, props.folder.identifier) return collectionsStore.hasChildrenInCollection(props.service.provider, props.service.identifier, props.folder.identifier)
}) })
const canDeleteFolder = computed(() => !props.folder.properties.role)
const handleExpandableFolderClick = () => { const handleExpandableFolderClick = () => {
expanded.value = !expanded.value expanded.value = !expanded.value
emit('select', props.folder) emit('select', props.folder)
@@ -110,7 +113,6 @@ const isSelected = (folder: CollectionObject): boolean => {
class="mr-2" class="mr-2"
/> />
<!-- Action menu -->
<v-menu> <v-menu>
<template v-slot:activator="{ props: menuProps }"> <template v-slot:activator="{ props: menuProps }">
<v-btn <v-btn
@@ -138,6 +140,14 @@ const isSelected = (folder: CollectionObject): boolean => {
> >
<v-list-item-title>New Subfolder</v-list-item-title> <v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item
v-if="canDeleteFolder"
prepend-icon="mdi-delete"
base-color="error"
@click="emit('deleteFolder', service, folder)"
>
<v-list-item-title>Delete Folder</v-list-item-title>
</v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
</template> </template>
@@ -154,6 +164,7 @@ const isSelected = (folder: CollectionObject): boolean => {
@select="emit('select', $event)" @select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createSubfolder', service, parentFolder)" @create-subfolder="(service, parentFolder) => emit('createSubfolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)" @edit-folder="(service, folder) => emit('editFolder', service, folder)"
@delete-folder="(service, folder) => emit('deleteFolder', service, folder)"
/> />
</div> </div>
</v-list-group> </v-list-group>
@@ -181,7 +192,6 @@ const isSelected = (folder: CollectionObject): boolean => {
class="mr-2" class="mr-2"
/> />
<!-- Action menu -->
<v-menu> <v-menu>
<template v-slot:activator="{ props: menuProps }"> <template v-slot:activator="{ props: menuProps }">
<v-btn <v-btn
@@ -209,6 +219,14 @@ const isSelected = (folder: CollectionObject): boolean => {
> >
<v-list-item-title>New Subfolder</v-list-item-title> <v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item
v-if="canDeleteFolder"
prepend-icon="mdi-delete"
base-color="error"
@click="emit('deleteFolder', service, folder)"
>
<v-list-item-title>Delete Folder</v-list-item-title>
</v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
</template> </template>

View File

@@ -23,6 +23,7 @@ const emit = defineEmits<{
select: [folder: CollectionObject] select: [folder: CollectionObject]
createFolder: [service: ServiceObject, parentFolder: CollectionObject | null] createFolder: [service: ServiceObject, parentFolder: CollectionObject | null]
editFolder: [service: ServiceObject, folder: CollectionObject] editFolder: [service: ServiceObject, folder: CollectionObject]
deleteFolder: [service: ServiceObject, folder: CollectionObject]
}>() }>()
const getRootFolders = (service: ServiceObject): CollectionObject[] => { const getRootFolders = (service: ServiceObject): CollectionObject[] => {
@@ -74,6 +75,7 @@ const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
@select="emit('select', $event)" @select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)" @create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)" @edit-folder="(service, folder) => emit('editFolder', service, folder)"
@delete-folder="(service, folder) => emit('deleteFolder', service, folder)"
/> />
<v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item"> <v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item">
@@ -155,6 +157,7 @@ const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
@select="emit('select', $event)" @select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)" @create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)" @edit-folder="(service, folder) => emit('editFolder', service, folder)"
@delete-folder="(service, folder) => emit('deleteFolder', service, folder)"
/> />
<v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item"> <v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item">

View File

@@ -297,6 +297,17 @@ export const useMailStore = defineStore('mailStore', () => {
_updateSyncSources() _updateSyncSources()
} }
function clearSelectedFolder() {
selectedFolder.value = null
selectedMessage.value = null
clearSelection()
selectionMode.value = false
composeMode.value = false
composeReplyTo.value = null
_updateSyncSources()
}
function selectMessage(entity: EntityObject, closeSidebar = false) { function selectMessage(entity: EntityObject, closeSidebar = false) {
selectedMessage.value = entity selectedMessage.value = entity
composeMode.value = false composeMode.value = false
@@ -555,6 +566,7 @@ export const useMailStore = defineStore('mailStore', () => {
// Actions // Actions
selectFolder, selectFolder,
clearSelectedFolder,
selectMessage, selectMessage,
isMessageSelected, isMessageSelected,
activateSelectionMode, activateSelectionMode,