283
src/components/FolderSelectionDialog.vue
Normal file
283
src/components/FolderSelectionDialog.vue
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<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'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
loading?: boolean
|
||||||
|
title?: string
|
||||||
|
confirmText?: string
|
||||||
|
emptyText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
title: 'Move To',
|
||||||
|
confirmText: 'Move',
|
||||||
|
emptyText: 'No folders are available.',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
select: [folder: CollectionObject]
|
||||||
|
cancel: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const collectionsStore = useCollectionsStore()
|
||||||
|
const servicesStore = useServicesStore()
|
||||||
|
const mailStore = useMailStore()
|
||||||
|
|
||||||
|
const selectedFolderKey = ref<string | null>(null)
|
||||||
|
|
||||||
|
const dialogValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: boolean) => emit('update:modelValue', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
const folderKeyFor = (folder: CollectionObject): string => {
|
||||||
|
return `${folder.provider}:${String(folder.service)}:${String(folder.identifier)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceGroup {
|
||||||
|
service: ServiceObject
|
||||||
|
loading: boolean
|
||||||
|
loaded: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceGroups = computed<ServiceGroup[]>(() => {
|
||||||
|
const moveCandidate = mailStore.moveMessageCandidate
|
||||||
|
|
||||||
|
if (!moveCandidate) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = servicesStore.services.find(entry =>
|
||||||
|
entry.provider === moveCandidate.provider &&
|
||||||
|
String(entry.identifier) === String(moveCandidate.service),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service.identifier === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
service,
|
||||||
|
loading: mailStore.isServiceFolderLoading(service.provider, service.identifier),
|
||||||
|
loaded: mailStore.hasServiceFoldersLoaded(service.provider, service.identifier),
|
||||||
|
error: mailStore.getServiceFolderError(service.provider, service.identifier),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
const getRootFolders = (service: ServiceObject): CollectionObject[] => {
|
||||||
|
if (service.identifier === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionsStore.collectionsInCollection(service.provider, service.identifier, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
|
||||||
|
if (service.identifier === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionsStore.collectionsForService(service.provider, service.identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedFolder = computed(() => {
|
||||||
|
if (!selectedFolderKey.value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = serviceGroups.value[0]
|
||||||
|
if (!group) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return getServiceFolders(group.service).find(folder => folderKeyFor(folder) === selectedFolderKey.value) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const canConfirm = computed(() => {
|
||||||
|
return selectedFolder.value !== null && !props.loading
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.modelValue, mailStore.moveMessageCandidate],
|
||||||
|
([isOpen]) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFolderKey.value = null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSelect = (folder: CollectionObject) => {
|
||||||
|
selectedFolderKey.value = folderKeyFor(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('cancel')
|
||||||
|
dialogValue.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!selectedFolder.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('select', selectedFolder.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialogValue"
|
||||||
|
max-width="680"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h6">
|
||||||
|
{{ title }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<div class="folder-tree-card">
|
||||||
|
<v-list
|
||||||
|
density="compact"
|
||||||
|
nav
|
||||||
|
class="folder-tree-list"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="group in serviceGroups"
|
||||||
|
:key="`${group.service.provider}-${group.service.identifier}`"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
class="account-header-item account-header-static"
|
||||||
|
:title="group.service.label || 'Mail Account'"
|
||||||
|
:subtitle="group.service.primaryAddress || undefined"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-email-outline" />
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<FolderSelectionTreeNode
|
||||||
|
v-for="folder in getRootFolders(group.service)"
|
||||||
|
:key="folderKeyFor(folder)"
|
||||||
|
:folder="folder"
|
||||||
|
:service="group.service"
|
||||||
|
:selected-folder-key="selectedFolderKey"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-if="group.loading && getServiceFolders(group.service).length === 0"
|
||||||
|
disabled
|
||||||
|
class="folder-status-item"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-progress-circular indeterminate size="18" width="2" color="primary" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Loading folders</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-else-if="group.error && getServiceFolders(group.service).length === 0"
|
||||||
|
disabled
|
||||||
|
class="folder-status-item"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-alert-circle-outline" color="error" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Folders unavailable</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ group.error }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-else-if="group.loaded && getServiceFolders(group.service).length === 0"
|
||||||
|
:title="emptyText"
|
||||||
|
disabled
|
||||||
|
class="folder-status-item"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-folder-off-outline" />
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-if="serviceGroups.length === 0"
|
||||||
|
:title="emptyText"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-folder-off-outline" />
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="elevated"
|
||||||
|
:disabled="!canConfirm"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>
|
||||||
|
{{ confirmText }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.folder-tree-card {
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree-list {
|
||||||
|
max-height: 380px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header-item {
|
||||||
|
--v-list-item-prepend-size: 22px;
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header-static {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header-item :deep(.v-list-item__prepend) {
|
||||||
|
padding-inline-start: 4px;
|
||||||
|
margin-inline-end: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-status-item {
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
185
src/components/FolderSelectionTreeNode.vue
Normal file
185
src/components/FolderSelectionTreeNode.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
|
||||||
|
import type { CollectionObject } from '@MailManager/models/collection'
|
||||||
|
import type { ServiceObject } from '@MailManager/models'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
folder: CollectionObject
|
||||||
|
service: ServiceObject
|
||||||
|
selectedFolderKey: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const collectionsStore = useCollectionsStore()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: [folder: CollectionObject]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const expanded = ref(false)
|
||||||
|
|
||||||
|
const folderKeyFor = (folder: CollectionObject): string => {
|
||||||
|
return `${folder.provider}:${String(folder.service)}:${String(folder.identifier)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderLabelFor = (folder: CollectionObject): string => {
|
||||||
|
return folder.properties.label || String(folder.identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderIconFor = (folder: CollectionObject): string => {
|
||||||
|
switch (folder.properties.role) {
|
||||||
|
case 'inbox':
|
||||||
|
return 'mdi-inbox'
|
||||||
|
case 'sent':
|
||||||
|
return 'mdi-send'
|
||||||
|
case 'drafts':
|
||||||
|
return 'mdi-file-document'
|
||||||
|
case 'trash':
|
||||||
|
return 'mdi-delete'
|
||||||
|
case 'junk':
|
||||||
|
return 'mdi-alert-octagon'
|
||||||
|
case 'archive':
|
||||||
|
return 'mdi-archive'
|
||||||
|
case 'outbox':
|
||||||
|
return 'mdi-tray-arrow-up'
|
||||||
|
default:
|
||||||
|
return 'mdi-folder'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderColorFor = (folder: CollectionObject): string | undefined => {
|
||||||
|
switch (folder.properties.role) {
|
||||||
|
case 'inbox':
|
||||||
|
return 'primary'
|
||||||
|
case 'sent':
|
||||||
|
return 'success'
|
||||||
|
case 'drafts':
|
||||||
|
return 'warning'
|
||||||
|
case 'trash':
|
||||||
|
return 'error'
|
||||||
|
case 'junk':
|
||||||
|
return 'orange'
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = computed(() => folderKeyFor(props.folder))
|
||||||
|
|
||||||
|
const childFolders = computed(() => {
|
||||||
|
const serviceIdentifier = props.service.identifier
|
||||||
|
|
||||||
|
if (serviceIdentifier === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionsStore.collectionsInCollection(props.service.provider, serviceIdentifier, props.folder.identifier)
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasChildren = computed(() => {
|
||||||
|
const serviceIdentifier = props.service.identifier
|
||||||
|
|
||||||
|
if (serviceIdentifier === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionsStore.hasChildrenInCollection(props.service.provider, serviceIdentifier, props.folder.identifier)
|
||||||
|
})
|
||||||
|
const isSelected = computed(() => props.selectedFolderKey === key.value)
|
||||||
|
|
||||||
|
const onSelect = () => {
|
||||||
|
emit('select', props.folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleExpanded = () => {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGroupDoubleClick = () => {
|
||||||
|
toggleExpanded()
|
||||||
|
onSelect()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="hasChildren"
|
||||||
|
class="folder-node-group"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
class="folder-node"
|
||||||
|
:title="folderLabelFor(folder)"
|
||||||
|
:active="isSelected"
|
||||||
|
@click="onSelect"
|
||||||
|
@dblclick.stop="onGroupDoubleClick"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon
|
||||||
|
:icon="folderIconFor(folder)"
|
||||||
|
:color="folderColorFor(folder)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
density="compact"
|
||||||
|
size="x-small"
|
||||||
|
:icon="expanded ? 'mdi-chevron-down' : 'mdi-chevron-right'"
|
||||||
|
@click.stop="toggleExpanded"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="expanded"
|
||||||
|
class="folder-node-children"
|
||||||
|
>
|
||||||
|
<FolderSelectionTreeNode
|
||||||
|
v-for="childFolder in childFolders"
|
||||||
|
:key="folderKeyFor(childFolder)"
|
||||||
|
:folder="childFolder"
|
||||||
|
:service="service"
|
||||||
|
:selected-folder-key="selectedFolderKey"
|
||||||
|
@select="emit('select', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-else
|
||||||
|
class="folder-node"
|
||||||
|
:title="folderLabelFor(folder)"
|
||||||
|
:active="isSelected"
|
||||||
|
@click="onSelect"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon
|
||||||
|
:icon="folderIconFor(folder)"
|
||||||
|
:color="folderColorFor(folder)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.folder-node {
|
||||||
|
--v-list-item-prepend-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-node.v-list-item--active {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-node :deep(.v-list-item__prepend) {
|
||||||
|
padding-inline-start: 4px;
|
||||||
|
margin-inline-end: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-node-children {
|
||||||
|
padding-left: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
border-left: 2px solid rgba(var(--v-border-color), 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -24,6 +24,7 @@ const { getSetting } = useUser()
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reply: [message: EntityInterface<MessageInterface>]
|
reply: [message: EntityInterface<MessageInterface>]
|
||||||
forward: [message: EntityInterface<MessageInterface>]
|
forward: [message: EntityInterface<MessageInterface>]
|
||||||
|
move: [message: EntityInterface<MessageInterface>]
|
||||||
delete: [message: EntityInterface<MessageInterface>]
|
delete: [message: EntityInterface<MessageInterface>]
|
||||||
flag: [message: EntityInterface<MessageInterface>]
|
flag: [message: EntityInterface<MessageInterface>]
|
||||||
compose: []
|
compose: []
|
||||||
@@ -105,6 +106,12 @@ const handleDelete = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMove = () => {
|
||||||
|
if (props.message) {
|
||||||
|
emit('move', props.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleFlag = () => {
|
const handleFlag = () => {
|
||||||
if (props.message) {
|
if (props.message) {
|
||||||
emit('flag', props.message)
|
emit('flag', props.message)
|
||||||
@@ -134,6 +141,7 @@ const handleCompose = () => {
|
|||||||
:is-security-overridden="overrideSecurityLevel !== null"
|
:is-security-overridden="overrideSecurityLevel !== null"
|
||||||
@reply="handleReply"
|
@reply="handleReply"
|
||||||
@forward="handleForward"
|
@forward="handleForward"
|
||||||
|
@move="handleMove"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
@flag="handleFlag"
|
@flag="handleFlag"
|
||||||
@toggle-images="toggleImages"
|
@toggle-images="toggleImages"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const props = defineProps<Props>()
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reply: []
|
reply: []
|
||||||
forward: []
|
forward: []
|
||||||
|
move: []
|
||||||
delete: []
|
delete: []
|
||||||
flag: []
|
flag: []
|
||||||
toggleImages: []
|
toggleImages: []
|
||||||
@@ -146,13 +147,31 @@ const currentSecurityLevel = computed(() => {
|
|||||||
|
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|
||||||
|
<v-menu>
|
||||||
|
<template #activator="{ props: menuProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
|
v-bind="menuProps"
|
||||||
icon="mdi-dots-vertical"
|
icon="mdi-dots-vertical"
|
||||||
variant="text"
|
variant="text"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-dots-vertical</v-icon>
|
<v-icon>mdi-dots-vertical</v-icon>
|
||||||
<v-tooltip activator="parent" location="bottom">More Actions</v-tooltip>
|
<v-tooltip activator="parent" location="bottom">More Actions</v-tooltip>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-folder-move-outline"
|
||||||
|
title="Move To Folder"
|
||||||
|
@click="emit('move')"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-delete-outline"
|
||||||
|
title="Delete"
|
||||||
|
@click="emit('delete')"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useModuleStore } from '@KTXC'
|
import { useModuleStore } from '@KTXC'
|
||||||
import { useMailStore } from '@/stores/mailStore'
|
import { useMailStore } from '@/stores/mailStore'
|
||||||
|
import type { CollectionObject, EntityObject } from '@MailManager/models'
|
||||||
import FolderTree from '@/components/FolderTree.vue'
|
import FolderTree from '@/components/FolderTree.vue'
|
||||||
import MessageList from '@/components/MessageList.vue'
|
import MessageList from '@/components/MessageList.vue'
|
||||||
import MessageReader from '@/components/MessageReader.vue'
|
import MessageReader from '@/components/MessageReader.vue'
|
||||||
import MessageComposer from '@/components/MessageComposer.vue'
|
import MessageComposer from '@/components/MessageComposer.vue'
|
||||||
|
import FolderSelectionDialog from '@/components/FolderSelectionDialog.vue'
|
||||||
import SettingsDialog from '@/components/settings/SettingsDialog.vue'
|
import SettingsDialog from '@/components/settings/SettingsDialog.vue'
|
||||||
import type { EntityInterface } from '@MailManager/types/entity'
|
|
||||||
import type { MessageInterface } from '@MailManager/types/message'
|
|
||||||
import type { CollectionObject } from '@MailManager/models'
|
|
||||||
|
|
||||||
// Vuetify display for responsive behavior
|
// Vuetify display for responsive behavior
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -36,6 +35,7 @@ const {
|
|||||||
composeMode,
|
composeMode,
|
||||||
composeReplyTo,
|
composeReplyTo,
|
||||||
currentMessages,
|
currentMessages,
|
||||||
|
moveDialogVisible,
|
||||||
} = storeToRefs(mailStore)
|
} = storeToRefs(mailStore)
|
||||||
|
|
||||||
// Complex store/composable objects accessed directly (not simple refs)
|
// Complex store/composable objects accessed directly (not simple refs)
|
||||||
@@ -50,17 +50,25 @@ onMounted(async () => {
|
|||||||
// Handlers — thin wrappers that delegate to the store
|
// Handlers — thin wrappers that delegate to the store
|
||||||
const handleFolderSelect = (folder: CollectionObject) => mailStore.selectFolder(folder)
|
const handleFolderSelect = (folder: CollectionObject) => mailStore.selectFolder(folder)
|
||||||
|
|
||||||
const handleMessageSelect = (message: EntityInterface<MessageInterface>) => mailStore.selectMessage(message, isMobile.value)
|
const handleMessageSelect = (message: EntityObject) => mailStore.selectMessage(message, isMobile.value)
|
||||||
|
|
||||||
const handleCompose = (replyTo?: EntityInterface<MessageInterface>) => mailStore.openCompose(replyTo)
|
const handleCompose = (replyTo?: EntityObject) => mailStore.openCompose(replyTo)
|
||||||
|
|
||||||
const handleComposeClose = () => mailStore.closeCompose()
|
const handleComposeClose = () => mailStore.closeCompose()
|
||||||
|
|
||||||
const handleComposeSent = () => mailStore.afterSent()
|
const handleComposeSent = () => mailStore.afterSent()
|
||||||
|
|
||||||
const handleReply = (message: EntityInterface<MessageInterface>) => mailStore.openCompose(message)
|
const handleReply = (message: EntityObject) => mailStore.openCompose(message)
|
||||||
|
|
||||||
const handleDelete = (message: EntityInterface<MessageInterface>) => mailStore.deleteMessage(message)
|
const handleDelete = (message: EntityObject) => mailStore.deleteMessage(message)
|
||||||
|
|
||||||
|
const handleMove = (message: EntityObject) => mailStore.openMoveDialog(message)
|
||||||
|
|
||||||
|
const handleMoveConfirm = async (folder: CollectionObject) => {
|
||||||
|
await mailStore.moveMessage(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMoveCancel = () => mailStore.closeMoveDialog()
|
||||||
|
|
||||||
const toggleSidebar = () => mailStore.toggleSidebar()
|
const toggleSidebar = () => mailStore.toggleSidebar()
|
||||||
|
|
||||||
@@ -193,6 +201,7 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
|||||||
v-else
|
v-else
|
||||||
:message="selectedMessage"
|
:message="selectedMessage"
|
||||||
@reply="handleReply"
|
@reply="handleReply"
|
||||||
|
@move="handleMove"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
@compose="handleCompose()"
|
@compose="handleCompose()"
|
||||||
/>
|
/>
|
||||||
@@ -203,6 +212,16 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
|||||||
|
|
||||||
<!-- Settings Dialog -->
|
<!-- Settings Dialog -->
|
||||||
<SettingsDialog v-model="settingsDialogVisible" />
|
<SettingsDialog v-model="settingsDialogVisible" />
|
||||||
|
|
||||||
|
<FolderSelectionDialog
|
||||||
|
v-model="moveDialogVisible"
|
||||||
|
:loading="loading"
|
||||||
|
title="Move To"
|
||||||
|
confirm-text="Move"
|
||||||
|
empty-text="No other folders are available in this account."
|
||||||
|
@select="handleMoveConfirm"
|
||||||
|
@cancel="handleMoveCancel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
|
|||||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||||
import { useMailSync } from '@MailManager/composables/useMailSync'
|
import { useMailSync } from '@MailManager/composables/useMailSync'
|
||||||
import { useSnackbar } from '@KTXC'
|
import { useSnackbar } from '@KTXC'
|
||||||
import type { CollectionObject } from '@MailManager/models/collection'
|
import type { CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
||||||
import type { EntityInterface } from '@MailManager/types/entity'
|
|
||||||
import type { MessageInterface } from '@MailManager/types/message'
|
import type { MessageInterface } from '@MailManager/types/message'
|
||||||
import type { ServiceObject } from '@MailManager/models'
|
import type { ServiceObject, CollectionObject, EntityObject } from '@MailManager/models'
|
||||||
|
|
||||||
export const useMailStore = defineStore('mailStore', () => {
|
export const useMailStore = defineStore('mailStore', () => {
|
||||||
const collectionsStore = useCollectionsStore()
|
const collectionsStore = useCollectionsStore()
|
||||||
@@ -42,11 +41,15 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
|
|
||||||
// ── Selection State ───────────────────────────────────────────────────────
|
// ── Selection State ───────────────────────────────────────────────────────
|
||||||
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
||||||
const selectedMessage = shallowRef<EntityInterface<MessageInterface> | null>(null)
|
const selectedMessage = shallowRef<EntityObject | null>(null)
|
||||||
|
|
||||||
// ── Compose State ─────────────────────────────────────────────────────────
|
// ── Compose State ─────────────────────────────────────────────────────────
|
||||||
const composeMode = ref(false)
|
const composeMode = ref(false)
|
||||||
const composeReplyTo = shallowRef<EntityInterface<MessageInterface> | null>(null)
|
const composeReplyTo = shallowRef<EntityObject | null>(null)
|
||||||
|
|
||||||
|
// ── Move State ────────────────────────────────────────────────────────────
|
||||||
|
const moveDialogVisible = ref(false)
|
||||||
|
const moveMessageCandidate = shallowRef<EntityObject | null>(null)
|
||||||
|
|
||||||
// ── Computed ──────────────────────────────────────────────────────────────
|
// ── Computed ──────────────────────────────────────────────────────────────
|
||||||
const currentMessages = computed(() => {
|
const currentMessages = computed(() => {
|
||||||
@@ -205,6 +208,27 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
|
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _entityIdentifier(entity: EntityObject): EntityIdentifier {
|
||||||
|
return `${entity.provider}:${String(entity.service)}:${String(entity.collection)}:${String(entity.identifier)}` as EntityIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
function _collectionIdentifier(collection: CollectionObject): CollectionIdentifier {
|
||||||
|
return `${collection.provider}:${String(collection.service)}:${String(collection.identifier)}` as CollectionIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isSameMessage(left: EntityObject | null, right: EntityObject): boolean {
|
||||||
|
if (!left) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
left.provider === right.provider &&
|
||||||
|
String(left.service) === String(right.service) &&
|
||||||
|
String(left.collection) === String(right.collection) &&
|
||||||
|
String(left.identifier) === String(right.identifier)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Actions ───────────────────────────────────────────────────────────────
|
// ── Actions ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function selectFolder(folder: CollectionObject) {
|
async function selectFolder(folder: CollectionObject) {
|
||||||
@@ -227,7 +251,7 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
_updateSyncSources()
|
_updateSyncSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMessage(message: EntityInterface<MessageInterface>, closeSidebar = false) {
|
function selectMessage(message: EntityObject, closeSidebar = false) {
|
||||||
selectedMessage.value = message
|
selectedMessage.value = message
|
||||||
composeMode.value = false
|
composeMode.value = false
|
||||||
|
|
||||||
@@ -236,12 +260,22 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCompose(replyTo?: EntityInterface<MessageInterface>) {
|
function openCompose(replyTo?: EntityObject) {
|
||||||
composeMode.value = true
|
composeMode.value = true
|
||||||
composeReplyTo.value = replyTo ?? null
|
composeReplyTo.value = replyTo ?? null
|
||||||
selectedMessage.value = null
|
selectedMessage.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openMoveDialog(message: EntityObject) {
|
||||||
|
moveMessageCandidate.value = message
|
||||||
|
moveDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMoveDialog() {
|
||||||
|
moveDialogVisible.value = false
|
||||||
|
moveMessageCandidate.value = null
|
||||||
|
}
|
||||||
|
|
||||||
function closeCompose() {
|
function closeCompose() {
|
||||||
composeMode.value = false
|
composeMode.value = false
|
||||||
composeReplyTo.value = null
|
composeReplyTo.value = null
|
||||||
@@ -257,11 +291,64 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMessage(message: EntityInterface<MessageInterface>) {
|
async function deleteMessage(message: EntityObject) {
|
||||||
// TODO: implement delete via entity / collection store
|
// TODO: implement delete via entity / collection store
|
||||||
console.log('[Mail] Delete message:', message.identifier)
|
console.log('[Mail] Delete message:', message.identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function moveMessage(targetFolder: CollectionObject) {
|
||||||
|
const message = moveMessageCandidate.value
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameCollection =
|
||||||
|
targetFolder.provider === message.provider &&
|
||||||
|
String(targetFolder.service) === String(message.service) &&
|
||||||
|
String(targetFolder.identifier) === String(message.collection)
|
||||||
|
|
||||||
|
if (isSameCollection) {
|
||||||
|
closeMoveDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceIdentifier = _entityIdentifier(message)
|
||||||
|
const response = await entitiesStore.move(_collectionIdentifier(targetFolder), [sourceIdentifier])
|
||||||
|
const result = response[sourceIdentifier]
|
||||||
|
|
||||||
|
if (!result || !result.success) {
|
||||||
|
throw new Error(result && 'error' in result ? result.error : 'Failed to move message')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isSameMessage(selectedMessage.value, message)) {
|
||||||
|
selectedMessage.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = servicesStore.services.find(entry =>
|
||||||
|
entry.provider === message.provider &&
|
||||||
|
String(entry.identifier) === String(message.service),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (service) {
|
||||||
|
void loadFoldersForService(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(`Message moved to "${targetFolder.properties.label || targetFolder.identifier}"`, 'success')
|
||||||
|
closeMoveDialog()
|
||||||
|
} catch (error) {
|
||||||
|
const messageText = error instanceof Error ? error.message : 'Failed to move message'
|
||||||
|
console.error('[Mail] Failed to move message:', error)
|
||||||
|
notify(messageText, 'error')
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
sidebarVisible.value = !sidebarVisible.value
|
sidebarVisible.value = !sidebarVisible.value
|
||||||
}
|
}
|
||||||
@@ -291,6 +378,8 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
selectedMessage,
|
selectedMessage,
|
||||||
composeMode,
|
composeMode,
|
||||||
composeReplyTo,
|
composeReplyTo,
|
||||||
|
moveDialogVisible,
|
||||||
|
moveMessageCandidate,
|
||||||
serviceFolderLoadingState,
|
serviceFolderLoadingState,
|
||||||
serviceFolderLoadedState,
|
serviceFolderLoadedState,
|
||||||
serviceFolderErrorState,
|
serviceFolderErrorState,
|
||||||
@@ -302,9 +391,12 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
selectFolder,
|
selectFolder,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
openCompose,
|
openCompose,
|
||||||
|
openMoveDialog,
|
||||||
|
closeMoveDialog,
|
||||||
closeCompose,
|
closeCompose,
|
||||||
afterSent,
|
afterSent,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
|
moveMessage,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
openSettings,
|
openSettings,
|
||||||
notify,
|
notify,
|
||||||
|
|||||||
Reference in New Issue
Block a user