feat: collection delete
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
149
src/components/DeleteFolderDialog.vue
Normal file
149
src/components/DeleteFolderDialog.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user