Merge pull request 'feat: entity move' (#10) from feat/entity-move into main
Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
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<{
|
||||
reply: [message: EntityInterface<MessageInterface>]
|
||||
forward: [message: EntityInterface<MessageInterface>]
|
||||
move: [message: EntityInterface<MessageInterface>]
|
||||
delete: [message: EntityInterface<MessageInterface>]
|
||||
flag: [message: EntityInterface<MessageInterface>]
|
||||
compose: []
|
||||
@@ -105,6 +106,12 @@ const handleDelete = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleMove = () => {
|
||||
if (props.message) {
|
||||
emit('move', props.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFlag = () => {
|
||||
if (props.message) {
|
||||
emit('flag', props.message)
|
||||
@@ -134,6 +141,7 @@ const handleCompose = () => {
|
||||
:is-security-overridden="overrideSecurityLevel !== null"
|
||||
@reply="handleReply"
|
||||
@forward="handleForward"
|
||||
@move="handleMove"
|
||||
@delete="handleDelete"
|
||||
@flag="handleFlag"
|
||||
@toggle-images="toggleImages"
|
||||
|
||||
@@ -17,6 +17,7 @@ const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
reply: []
|
||||
forward: []
|
||||
move: []
|
||||
delete: []
|
||||
flag: []
|
||||
toggleImages: []
|
||||
@@ -146,13 +147,31 @@ const currentSecurityLevel = computed(() => {
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-menu>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
icon="mdi-dots-vertical"
|
||||
variant="text"
|
||||
>
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">More Actions</v-tooltip>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@ import { storeToRefs } from 'pinia'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useModuleStore } from '@KTXC'
|
||||
import { useMailStore } from '@/stores/mailStore'
|
||||
import type { CollectionObject, EntityObject } from '@MailManager/models'
|
||||
import FolderTree from '@/components/FolderTree.vue'
|
||||
import MessageList from '@/components/MessageList.vue'
|
||||
import MessageReader from '@/components/MessageReader.vue'
|
||||
import MessageComposer from '@/components/MessageComposer.vue'
|
||||
import FolderSelectionDialog from '@/components/FolderSelectionDialog.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
|
||||
const display = useDisplay()
|
||||
@@ -36,6 +35,7 @@ const {
|
||||
composeMode,
|
||||
composeReplyTo,
|
||||
currentMessages,
|
||||
moveDialogVisible,
|
||||
} = storeToRefs(mailStore)
|
||||
|
||||
// Complex store/composable objects accessed directly (not simple refs)
|
||||
@@ -50,17 +50,25 @@ onMounted(async () => {
|
||||
// Handlers — thin wrappers that delegate to the store
|
||||
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 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()
|
||||
|
||||
@@ -193,6 +201,7 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
||||
v-else
|
||||
:message="selectedMessage"
|
||||
@reply="handleReply"
|
||||
@move="handleMove"
|
||||
@delete="handleDelete"
|
||||
@compose="handleCompose()"
|
||||
/>
|
||||
@@ -203,6 +212,16 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
||||
|
||||
<!-- Settings Dialog -->
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
|
||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||
import { useMailSync } from '@MailManager/composables/useMailSync'
|
||||
import { useSnackbar } from '@KTXC'
|
||||
import type { CollectionObject } from '@MailManager/models/collection'
|
||||
import type { EntityInterface } from '@MailManager/types/entity'
|
||||
import type { CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
||||
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', () => {
|
||||
const collectionsStore = useCollectionsStore()
|
||||
@@ -42,11 +41,15 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
|
||||
// ── Selection State ───────────────────────────────────────────────────────
|
||||
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
||||
const selectedMessage = shallowRef<EntityInterface<MessageInterface> | null>(null)
|
||||
const selectedMessage = shallowRef<EntityObject | null>(null)
|
||||
|
||||
// ── Compose State ─────────────────────────────────────────────────────────
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
const currentMessages = computed(() => {
|
||||
@@ -205,6 +208,27 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function selectFolder(folder: CollectionObject) {
|
||||
@@ -227,7 +251,7 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
_updateSyncSources()
|
||||
}
|
||||
|
||||
function selectMessage(message: EntityInterface<MessageInterface>, closeSidebar = false) {
|
||||
function selectMessage(message: EntityObject, closeSidebar = false) {
|
||||
selectedMessage.value = message
|
||||
composeMode.value = false
|
||||
|
||||
@@ -236,12 +260,22 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function openCompose(replyTo?: EntityInterface<MessageInterface>) {
|
||||
function openCompose(replyTo?: EntityObject) {
|
||||
composeMode.value = true
|
||||
composeReplyTo.value = replyTo ?? null
|
||||
selectedMessage.value = null
|
||||
}
|
||||
|
||||
function openMoveDialog(message: EntityObject) {
|
||||
moveMessageCandidate.value = message
|
||||
moveDialogVisible.value = true
|
||||
}
|
||||
|
||||
function closeMoveDialog() {
|
||||
moveDialogVisible.value = false
|
||||
moveMessageCandidate.value = null
|
||||
}
|
||||
|
||||
function closeCompose() {
|
||||
composeMode.value = false
|
||||
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
|
||||
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() {
|
||||
sidebarVisible.value = !sidebarVisible.value
|
||||
}
|
||||
@@ -291,6 +378,8 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
selectedMessage,
|
||||
composeMode,
|
||||
composeReplyTo,
|
||||
moveDialogVisible,
|
||||
moveMessageCandidate,
|
||||
serviceFolderLoadingState,
|
||||
serviceFolderLoadedState,
|
||||
serviceFolderErrorState,
|
||||
@@ -302,9 +391,12 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
selectFolder,
|
||||
selectMessage,
|
||||
openCompose,
|
||||
openMoveDialog,
|
||||
closeMoveDialog,
|
||||
closeCompose,
|
||||
afterSent,
|
||||
deleteMessage,
|
||||
moveMessage,
|
||||
toggleSidebar,
|
||||
openSettings,
|
||||
notify,
|
||||
|
||||
Reference in New Issue
Block a user