Merge pull request 'feat: move and delete' (#15) from feat/move-and-delete into main
Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
@@ -23,7 +23,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: boolean]
|
'update:modelValue': [value: boolean]
|
||||||
select: [folder: CollectionObject]
|
select: [target: CollectionObject]
|
||||||
cancel: []
|
cancel: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -50,16 +50,13 @@ interface ServiceGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const serviceGroups = computed<ServiceGroup[]>(() => {
|
const serviceGroups = computed<ServiceGroup[]>(() => {
|
||||||
const moveCandidate = mailStore.moveMessageCandidates[0]
|
const context = mailStore.moveDialogService
|
||||||
|
|
||||||
if (!moveCandidate) {
|
if (!context) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = servicesStore.services.find(entry =>
|
const service = servicesStore.serviceByIdentifier(mailStore.moveDialogService)
|
||||||
entry.provider === moveCandidate.provider &&
|
|
||||||
String(entry.identifier) === String(moveCandidate.service),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!service) {
|
if (!service) {
|
||||||
return []
|
return []
|
||||||
@@ -111,7 +108,7 @@ const canConfirm = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.modelValue, mailStore.moveMessageCandidates],
|
() => [props.modelValue, mailStore.moveDialogService],
|
||||||
([isOpen]) => {
|
([isOpen]) => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,40 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
import type { EntityIdentifier } from '@MailManager/types/common'
|
import type { EntityIdentifier } from '@MailManager/types/common'
|
||||||
import type { EntityInterface } from '@MailManager/types/entity'
|
import type { EntityObject } from '@MailManager/models'
|
||||||
import type { MessageInterface } from '@MailManager/types/message'
|
|
||||||
import type { CollectionObject } from '@MailManager/models/collection'
|
import type { CollectionObject } from '@MailManager/models/collection'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
messages: EntityInterface<MessageInterface>[]
|
messages: EntityObject[]
|
||||||
selectedMessage?: EntityInterface<MessageInterface> | null
|
selectedMessage?: EntityObject | null
|
||||||
selectedMessageIds?: EntityIdentifier[]
|
selectionList?: EntityIdentifier[]
|
||||||
selectionModeActive?: boolean
|
selectionMode?: boolean
|
||||||
selectionCount?: number
|
|
||||||
hasSelection?: boolean
|
|
||||||
allCurrentMessagesSelected?: boolean
|
|
||||||
selectedCollection?: CollectionObject | null
|
selectedCollection?: CollectionObject | null
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
loading: false,
|
loading: false,
|
||||||
selectedMessageIds: () => [],
|
selectionList: () => [],
|
||||||
selectionModeActive: false,
|
selectionMode: false,
|
||||||
selectionCount: 0,
|
|
||||||
hasSelection: false,
|
|
||||||
allCurrentMessagesSelected: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
open: [message: EntityInterface<MessageInterface>]
|
open: [message: EntityObject]
|
||||||
toggleSelection: [message: EntityInterface<MessageInterface>]
|
toggleSelection: [message: EntityObject]
|
||||||
activateSelectionMode: [message: EntityInterface<MessageInterface>]
|
activateSelectionMode: [message: EntityObject]
|
||||||
toggleSelectAll: [value: boolean]
|
toggleSelectAll: [value: boolean]
|
||||||
clearSelection: []
|
clearSelection: []
|
||||||
moveSelection: []
|
moveSelection: []
|
||||||
|
deleteSelection: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const longPressTimer = ref<number | null>(null)
|
const longPressTimer = ref<number | null>(null)
|
||||||
@@ -42,19 +36,19 @@ const longPressActivated = ref(false)
|
|||||||
const suppressNextClick = ref(false)
|
const suppressNextClick = ref(false)
|
||||||
const LONG_PRESS_MS = 450
|
const LONG_PRESS_MS = 450
|
||||||
|
|
||||||
const selectedIdSet = computed(() => new Set(props.selectedMessageIds))
|
const selectedIdSet = computed(() => new Set(props.selectionList))
|
||||||
|
|
||||||
const isOpened = (message: EntityInterface<MessageInterface>): boolean => {
|
const isOpened = (message: EntityObject): boolean => {
|
||||||
if (!props.selectedMessage) return false
|
if (!props.selectedMessage) return false
|
||||||
return (
|
return (
|
||||||
message.provider === selectedMessage.value.provider &&
|
message.provider === props.selectedMessage.provider &&
|
||||||
message.service === selectedMessage.value.service &&
|
message.service === props.selectedMessage.service &&
|
||||||
message.collection === selectedMessage.value.collection &&
|
message.collection === props.selectedMessage.collection &&
|
||||||
message.identifier === selectedMessage.value.identifier
|
message.identifier === props.selectedMessage.identifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelected = (message: EntityInterface<MessageInterface>): boolean => {
|
const isSelected = (message: EntityObject): boolean => {
|
||||||
return selectedIdSet.value.has(
|
return selectedIdSet.value.has(
|
||||||
`${message.provider}:${String(message.service)}:${String(message.collection)}:${String(message.identifier)}` as EntityIdentifier,
|
`${message.provider}:${String(message.service)}:${String(message.collection)}:${String(message.identifier)}` as EntityIdentifier,
|
||||||
)
|
)
|
||||||
@@ -70,6 +64,16 @@ const isFlagged = (message: EntityObject): boolean => {
|
|||||||
return message.properties.flags?.flagged || false
|
return message.properties.flags?.flagged || false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentMessages = computed(() => props.messages ?? [])
|
||||||
|
|
||||||
|
const selectionCount = computed(() => props.selectionList.length)
|
||||||
|
|
||||||
|
const hasSelection = computed(() => selectionCount.value > 0)
|
||||||
|
|
||||||
|
const allCurrentMessagesSelected = computed(() => {
|
||||||
|
return currentMessages.value.length > 0 && currentMessages.value.every(message => isSelected(message))
|
||||||
|
})
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatDate = (date: Date | string | null | undefined): string => {
|
const formatDate = (date: Date | string | null | undefined): string => {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
@@ -102,6 +106,7 @@ const formatDate = (date: Date | string | null | undefined): string => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Other years - show full date
|
// Other years - show full date
|
||||||
return messageDate.toLocaleDateString('en-US', {
|
return messageDate.toLocaleDateString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -116,8 +121,18 @@ const truncate = (text: string | null | undefined, length: number = 100): string
|
|||||||
return text.length > length ? text.substring(0, length) + '...' : text
|
return text.length > length ? text.substring(0, length) + '...' : text
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle message click
|
const handleSelectionToggle = (message: EntityObject) => {
|
||||||
const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
emit('toggleSelection', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMessageMouseClick = (event: MouseEvent | KeyboardEvent, message: EntityObject) => {
|
||||||
|
if (event.shiftKey && !props.selectionMode) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
emit('activateSelectionMode', message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (longPressActivated.value) {
|
if (longPressActivated.value) {
|
||||||
longPressActivated.value = false
|
longPressActivated.value = false
|
||||||
return
|
return
|
||||||
@@ -128,7 +143,7 @@ const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.selectionModeActive) {
|
if (props.selectionMode) {
|
||||||
emit('toggleSelection', message)
|
emit('toggleSelection', message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -136,23 +151,8 @@ const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
|||||||
emit('open', message)
|
emit('open', message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectionToggle = (message: EntityInterface<MessageInterface>) => {
|
const handleMessageMouseDown = (event: MouseEvent, message: EntityObject) => {
|
||||||
emit('toggleSelection', message)
|
if (!event.shiftKey || props.selectionMode) {
|
||||||
}
|
|
||||||
|
|
||||||
const handleMessageMouseClick = (event: MouseEvent | KeyboardEvent, message: EntityInterface<MessageInterface>) => {
|
|
||||||
if (event.shiftKey && !props.selectionModeActive) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
emit('activateSelectionMode', message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMessageClick(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMessageMouseDown = (event: MouseEvent, message: EntityInterface<MessageInterface>) => {
|
|
||||||
if (!event.shiftKey || props.selectionModeActive) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,12 +169,12 @@ const clearLongPressTimer = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTouchStart = (message: EntityInterface<MessageInterface>) => {
|
const handleTouchStart = (message: EntityObject) => {
|
||||||
clearLongPressTimer()
|
clearLongPressTimer()
|
||||||
longPressActivated.value = false
|
longPressActivated.value = false
|
||||||
|
|
||||||
longPressTimer.value = window.setTimeout(() => {
|
longPressTimer.value = window.setTimeout(() => {
|
||||||
if (!props.selectionModeActive) {
|
if (!props.selectionMode) {
|
||||||
emit('activateSelectionMode', message)
|
emit('activateSelectionMode', message)
|
||||||
} else {
|
} else {
|
||||||
emit('toggleSelection', message)
|
emit('toggleSelection', message)
|
||||||
@@ -216,12 +216,12 @@ const unreadCount = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const totalCount = computed(() => {
|
const totalCount = computed(() => {
|
||||||
return selectedFolder.value?.properties.total ?? 0
|
return props.selectedCollection?.properties.total ?? 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// True only when the collection explicitly provides total/unread counts
|
// True only when the collection explicitly provides total/unread counts
|
||||||
const hasCountData = computed(() => {
|
const hasCountData = computed(() => {
|
||||||
return selectedFolder.value?.properties.total != null
|
return props.selectedCollection?.properties.total != null
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ const hasCountData = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectionModeActive && messages.length > 0" class="selection-summary">
|
<div v-if="selectionMode && messages.length > 0" class="selection-summary">
|
||||||
<div class="selection-controls">
|
<div class="selection-controls">
|
||||||
<v-checkbox-btn
|
<v-checkbox-btn
|
||||||
:model-value="allCurrentMessagesSelected"
|
:model-value="allCurrentMessagesSelected"
|
||||||
@@ -267,6 +267,15 @@ const hasCountData = computed(() => {
|
|||||||
>
|
>
|
||||||
Move
|
Move
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
prepend-icon="mdi-delete-outline"
|
||||||
|
:disabled="!hasSelection"
|
||||||
|
@click="emit('deleteSelection')"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -307,12 +316,12 @@ const hasCountData = computed(() => {
|
|||||||
>
|
>
|
||||||
<template v-slot:default="{ item: message }">
|
<template v-slot:default="{ item: message }">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
:key="`${message.provider}-${message.service}-${message.collection}-${message.identifier}`"
|
:key="`${message.provider}:${message.service}:${message.collection}:${message.identifier}`"
|
||||||
class="message-item"
|
class="message-item"
|
||||||
:class="{
|
:class="{
|
||||||
'opened': isOpened(message),
|
'opened': isOpened(message),
|
||||||
'selected': isSelected(message),
|
'selected': isSelected(message),
|
||||||
'selection-mode': selectionModeActive,
|
'selection-mode': selectionMode,
|
||||||
'unread': isUnread(message)
|
'unread': isUnread(message)
|
||||||
}"
|
}"
|
||||||
@mousedown="handleMessageMouseDown($event, message)"
|
@mousedown="handleMessageMouseDown($event, message)"
|
||||||
@@ -326,7 +335,7 @@ const hasCountData = computed(() => {
|
|||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<div class="message-item-prepend">
|
<div class="message-item-prepend">
|
||||||
<v-checkbox-btn
|
<v-checkbox-btn
|
||||||
v-if="selectionModeActive || isSelected(message)"
|
v-if="selectionMode || isSelected(message)"
|
||||||
:model-value="isSelected(message)"
|
:model-value="isSelected(message)"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||||
import { AddAccountDialog, EditAccountDialog } from '@MailManager/main'
|
import { AddAccountDialog, EditAccountDialog } from '@MailManager/main'
|
||||||
|
import type { ServiceObject } from '@MailManager/models'
|
||||||
|
|
||||||
const servicesStore = useServicesStore()
|
const servicesStore = useServicesStore()
|
||||||
|
|
||||||
// Dialog state
|
// Dialog state
|
||||||
const showAddDialog = ref(false)
|
const showAddDialog = ref(false)
|
||||||
const showEditDialog = ref(false)
|
const showEditDialog = ref(false)
|
||||||
const editServiceProvider = ref<string>('')
|
const editService = ref<ServiceObject>()
|
||||||
const editServiceIdentifier = ref<string | number>('')
|
|
||||||
|
|
||||||
// Load services on mount
|
// Load services on mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -22,11 +22,8 @@ const handleAddAccount = () => {
|
|||||||
showAddDialog.value = true
|
showAddDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfigureAccount = (serviceKey: string) => {
|
const handleConfigureAccount = (service: ServiceObject) => {
|
||||||
// Service key is in format "provider:identifier"
|
editService.value = service
|
||||||
const [provider, identifier] = serviceKey.split(':')
|
|
||||||
editServiceProvider.value = provider
|
|
||||||
editServiceIdentifier.value = identifier
|
|
||||||
showEditDialog.value = true
|
showEditDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +55,7 @@ const handleAccountSaved = async () => {
|
|||||||
icon="mdi-cog"
|
icon="mdi-cog"
|
||||||
variant="text"
|
variant="text"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleConfigureAccount(`${service.provider}:${service.identifier}`)"
|
@click="handleConfigureAccount(service)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
@@ -87,10 +84,10 @@ const handleAccountSaved = async () => {
|
|||||||
|
|
||||||
<!-- Edit Account Dialog -->
|
<!-- Edit Account Dialog -->
|
||||||
<EditAccountDialog
|
<EditAccountDialog
|
||||||
v-if="editServiceProvider && editServiceIdentifier"
|
v-if="editService"
|
||||||
v-model="showEditDialog"
|
v-model="showEditDialog"
|
||||||
:service-provider="editServiceProvider"
|
:service-provider="editService.provider"
|
||||||
:service-identifier="editServiceIdentifier"
|
:service-identifier="editService.identifier"
|
||||||
@saved="handleAccountSaved"
|
@saved="handleAccountSaved"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 type { CollectionObject, EntityObject } from '@MailManager/models'
|
||||||
|
import type { EntityIdentifier } from '@MailManager/types/common'
|
||||||
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'
|
||||||
@@ -32,19 +33,23 @@ const {
|
|||||||
loading,
|
loading,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
selectedMessage,
|
selectedMessage,
|
||||||
selectedMessageIds,
|
selectionList,
|
||||||
selectionModeActive,
|
selectionMode,
|
||||||
composeMode,
|
composeMode,
|
||||||
composeReplyTo,
|
composeReplyTo,
|
||||||
|
currentMessages,
|
||||||
moveDialogVisible,
|
moveDialogVisible,
|
||||||
selectionCount,
|
moveDialogCandidates,
|
||||||
hasSelection,
|
|
||||||
allCurrentMessagesSelected,
|
|
||||||
} = storeToRefs(mailStore)
|
} = storeToRefs(mailStore)
|
||||||
|
|
||||||
// Complex store/composable objects accessed directly (not simple refs)
|
// Complex store/composable objects accessed directly (not simple refs)
|
||||||
const { mailSync, entitiesStore } = mailStore
|
const { mailSync, entitiesStore } = mailStore
|
||||||
|
|
||||||
|
const lastSyncLabel = computed(() => {
|
||||||
|
if (!mailSync.lastSync.value) return ''
|
||||||
|
return `(Last: ${new Date(mailSync.lastSync.value).toLocaleTimeString()})`
|
||||||
|
})
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!isMailManagerAvailable.value) return
|
if (!isMailManagerAvailable.value) return
|
||||||
@@ -71,23 +76,24 @@ const handleSelectAllToggle = (value: boolean) => {
|
|||||||
|
|
||||||
const handleSelectionClear = () => mailStore.deactivateSelectionMode()
|
const handleSelectionClear = () => mailStore.deactivateSelectionMode()
|
||||||
|
|
||||||
const handleSelectionMove = () => mailStore.openMoveDialogForSelection()
|
const handleSelectionMove = () => mailStore.openMoveDialog()
|
||||||
|
|
||||||
const handleCompose = (replyTo?: EntityObject) => mailStore.openCompose(replyTo)
|
const handleSelectionDelete = () => mailStore.deleteMessages([...selectionList.value])
|
||||||
|
|
||||||
|
const handleCompose = (message?: EntityObject) => mailStore.openCompose(message)
|
||||||
|
|
||||||
const handleComposeClose = () => mailStore.closeCompose()
|
const handleComposeClose = () => mailStore.closeCompose()
|
||||||
|
|
||||||
const handleComposeSent = () => mailStore.afterSent()
|
const handleComposeSent = () => mailStore.afterSent()
|
||||||
|
|
||||||
const handleReply = (message: EntityObject) => mailStore.openCompose(message)
|
const handleDelete = (message: EntityObject) => {
|
||||||
|
const id = `${message.provider}:${String(message.service)}:${String(message.collection)}:${String(message.identifier)}` as EntityIdentifier
|
||||||
const handleDelete = (message: EntityObject) => mailStore.deleteMessage(message)
|
mailStore.deleteMessages([id])
|
||||||
|
}
|
||||||
|
|
||||||
const handleMove = (message: EntityObject) => mailStore.openMoveDialog(message)
|
const handleMove = (message: EntityObject) => mailStore.openMoveDialog(message)
|
||||||
|
|
||||||
const handleMoveConfirm = async (folder: CollectionObject) => {
|
const handleMoveConfirm = async (target: CollectionObject) => { await mailStore.moveMessages(target, moveDialogCandidates.value ?? []) }
|
||||||
await mailStore.moveMessages(folder)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMoveCancel = () => mailStore.closeMoveDialog()
|
const handleMoveCancel = () => mailStore.closeMoveDialog()
|
||||||
|
|
||||||
@@ -150,7 +156,7 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
|||||||
>
|
>
|
||||||
<v-icon>mdi-refresh</v-icon>
|
<v-icon>mdi-refresh</v-icon>
|
||||||
<v-tooltip activator="parent" location="bottom">
|
<v-tooltip activator="parent" location="bottom">
|
||||||
Refresh {{ mailSync.lastSync ? `(Last: ${new Date(mailSync.lastSync).toLocaleTimeString()})` : '' }}
|
Refresh {{ lastSyncLabel }}
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
@@ -201,13 +207,10 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
|||||||
<div class="mail-list-panel">
|
<div class="mail-list-panel">
|
||||||
<MessageList
|
<MessageList
|
||||||
:messages="currentMessages"
|
:messages="currentMessages"
|
||||||
:selected-message="selectedMessage"
|
|
||||||
:selected-message-ids="selectedMessageIds"
|
|
||||||
:selection-mode-active="selectionModeActive"
|
|
||||||
:selection-count="selectionCount"
|
|
||||||
:has-selection="hasSelection"
|
|
||||||
:all-current-messages-selected="allCurrentMessagesSelected"
|
|
||||||
:selected-collection="selectedFolder"
|
:selected-collection="selectedFolder"
|
||||||
|
:selected-message="selectedMessage"
|
||||||
|
:selection-list="selectionList"
|
||||||
|
:selection-mode="selectionMode"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@open="handleMessageOpen"
|
@open="handleMessageOpen"
|
||||||
@toggle-selection="handleMessageSelectionToggle"
|
@toggle-selection="handleMessageSelectionToggle"
|
||||||
@@ -215,6 +218,7 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
|||||||
@toggle-select-all="handleSelectAllToggle"
|
@toggle-select-all="handleSelectAllToggle"
|
||||||
@clear-selection="handleSelectionClear"
|
@clear-selection="handleSelectionClear"
|
||||||
@move-selection="handleSelectionMove"
|
@move-selection="handleSelectionMove"
|
||||||
|
@delete-selection="handleSelectionDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -235,7 +239,6 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
|||||||
@reply="handleCompose"
|
@reply="handleCompose"
|
||||||
@move="handleMove"
|
@move="handleMove"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
@compose="handleCompose()"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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 { CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
import type { ServiceIdentifier, CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
||||||
import type { ServiceObject, CollectionObject, EntityObject } from '@MailManager/models'
|
import type { ServiceObject, CollectionObject, EntityObject } from '@MailManager/models'
|
||||||
|
|
||||||
export const useMailStore = defineStore('mailStore', () => {
|
export const useMailStore = defineStore('mailStore', () => {
|
||||||
@@ -41,8 +41,8 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
// ── Selection State ───────────────────────────────────────────────────────
|
// ── Selection State ───────────────────────────────────────────────────────
|
||||||
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
||||||
const selectedMessage = shallowRef<EntityObject | null>(null)
|
const selectedMessage = shallowRef<EntityObject | null>(null)
|
||||||
const selectedMessageIds = ref<EntityIdentifier[]>([])
|
const selectionMode = ref(false)
|
||||||
const selectionModeActive = ref(false)
|
const selectionList = ref<EntityIdentifier[]>([])
|
||||||
|
|
||||||
// ── Compose State ─────────────────────────────────────────────────────────
|
// ── Compose State ─────────────────────────────────────────────────────────
|
||||||
const composeMode = ref(false)
|
const composeMode = ref(false)
|
||||||
@@ -50,7 +50,8 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
|
|
||||||
// ── Move State ────────────────────────────────────────────────────────────
|
// ── Move State ────────────────────────────────────────────────────────────
|
||||||
const moveDialogVisible = ref(false)
|
const moveDialogVisible = ref(false)
|
||||||
const moveMessageCandidates = shallowRef<EntityObject[]>([])
|
const moveDialogService = ref<ServiceIdentifier | null>(null)
|
||||||
|
const moveDialogCandidates = ref<EntityIdentifier[] | null>(null)
|
||||||
|
|
||||||
// ── Computed ──────────────────────────────────────────────────────────────
|
// ── Computed ──────────────────────────────────────────────────────────────
|
||||||
const currentMessages = computed(() => {
|
const currentMessages = computed(() => {
|
||||||
@@ -65,31 +66,6 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedMessageIdSet = computed(() => new Set(selectedMessageIds.value))
|
|
||||||
|
|
||||||
const selectedMessageMap = computed(() => {
|
|
||||||
const messageMap = new Map<EntityIdentifier, EntityObject>()
|
|
||||||
|
|
||||||
currentMessages.value.forEach(message => {
|
|
||||||
const identifier = _entityIdentifier(message)
|
|
||||||
if (selectedMessageIdSet.value.has(identifier)) {
|
|
||||||
messageMap.set(identifier, message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return messageMap
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedMessages = computed(() => Array.from(selectedMessageMap.value.values()))
|
|
||||||
|
|
||||||
const selectionCount = computed(() => selectedMessageIds.value.length)
|
|
||||||
|
|
||||||
const hasSelection = computed(() => selectionCount.value > 0)
|
|
||||||
|
|
||||||
const allCurrentMessagesSelected = computed(() => {
|
|
||||||
return currentMessages.value.length > 0 && currentMessages.value.every(message => isMessageSelected(message))
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Initialization ────────────────────────────────────────────────────────
|
// ── Initialization ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
@@ -162,36 +138,36 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _serviceKey(provider: string, service: string | number) {
|
function _serviceIdentifier(item: ServiceObject | CollectionObject | EntityObject | { provider: string, service: string | number }): ServiceIdentifier {
|
||||||
return `${provider}:${String(service)}`
|
return `${item.provider}:${String(item.service)}` as ServiceIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
function _collectionIdentifier(collection: CollectionObject): CollectionIdentifier {
|
function _collectionIdentifier(item: CollectionObject | EntityObject): CollectionIdentifier {
|
||||||
return `${collection.provider}:${String(collection.service)}:${String(collection.identifier)}` as CollectionIdentifier
|
return `${item.provider}:${String(item.service)}:${String(item.identifier)}` as CollectionIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
function _entityIdentifier(entity: EntityObject): EntityIdentifier {
|
function _entityIdentifier(item: EntityObject): EntityIdentifier {
|
||||||
return `${entity.provider}:${String(entity.service)}:${String(entity.collection)}:${String(entity.identifier)}` as EntityIdentifier
|
return `${item.provider}:${String(item.service)}:${String(item.collection)}:${String(item.identifier)}` as EntityIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
function _setServiceFolderLoading(provider: string, service: string | number, loadingState: boolean) {
|
function _setServiceFolderLoading(provider: string, service: string | number, loadingState: boolean) {
|
||||||
serviceFolderLoadingState.value = {
|
serviceFolderLoadingState.value = {
|
||||||
...serviceFolderLoadingState.value,
|
...serviceFolderLoadingState.value,
|
||||||
[_serviceKey(provider, service)]: loadingState,
|
[_serviceIdentifier({ provider, service })]: loadingState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _setServiceFolderLoaded(provider: string, service: string | number, loaded: boolean) {
|
function _setServiceFolderLoaded(provider: string, service: string | number, loaded: boolean) {
|
||||||
serviceFolderLoadedState.value = {
|
serviceFolderLoadedState.value = {
|
||||||
...serviceFolderLoadedState.value,
|
...serviceFolderLoadedState.value,
|
||||||
[_serviceKey(provider, service)]: loaded,
|
[_serviceIdentifier({ provider, service })]: loaded,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _setServiceFolderError(provider: string, service: string | number, error: string | null) {
|
function _setServiceFolderError(provider: string, service: string | number, error: string | null) {
|
||||||
serviceFolderErrorState.value = {
|
serviceFolderErrorState.value = {
|
||||||
...serviceFolderErrorState.value,
|
...serviceFolderErrorState.value,
|
||||||
[_serviceKey(provider, service)]: error,
|
[_serviceIdentifier({ provider, service })]: error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,22 +207,15 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isServiceFolderLoading(provider: string, service: string | number) {
|
function isServiceFolderLoading(provider: string, service: string | number) {
|
||||||
return serviceFolderLoadingState.value[_serviceKey(provider, service)] === true
|
return serviceFolderLoadingState.value[_serviceIdentifier({ provider, service })] === true
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasServiceFoldersLoaded(provider: string, service: string | number) {
|
function hasServiceFoldersLoaded(provider: string, service: string | number) {
|
||||||
return serviceFolderLoadedState.value[_serviceKey(provider, service)] === true
|
return serviceFolderLoadedState.value[_serviceIdentifier({ provider, service })] === true
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServiceFolderError(provider: string, service: string | number) {
|
function getServiceFolderError(provider: string, service: string | number) {
|
||||||
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
|
return serviceFolderErrorState.value[_serviceIdentifier({ provider, service })] ?? null
|
||||||
}
|
|
||||||
|
|
||||||
function _serviceFor(provider: string, serviceIdentifier: string | number) {
|
|
||||||
return servicesStore.services.find(service =>
|
|
||||||
service.provider === provider &&
|
|
||||||
String(service.identifier) === String(serviceIdentifier),
|
|
||||||
) ?? null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _reloadFolderMessages(folder: CollectionObject) {
|
function _reloadFolderMessages(folder: CollectionObject) {
|
||||||
@@ -259,17 +228,12 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function _setSelectedMessageIds(nextIds: EntityIdentifier[]) {
|
function _setSelectionList(nextIds: EntityIdentifier[]) {
|
||||||
selectedMessageIds.value = Array.from(new Set(nextIds))
|
selectionList.value = Array.from(new Set(nextIds))
|
||||||
}
|
|
||||||
|
|
||||||
function _removeSelection(sourceIdentifiers: EntityIdentifier[]) {
|
if (selectionList.value.length === 0) {
|
||||||
if (sourceIdentifiers.length === 0 || selectedMessageIds.value.length === 0) {
|
selectionMode.value = false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removedIds = new Set(sourceIdentifiers)
|
|
||||||
selectedMessageIds.value = selectedMessageIds.value.filter(identifier => !removedIds.has(identifier))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _reconcileSelection() {
|
function _reconcileSelection() {
|
||||||
@@ -280,10 +244,10 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentMessageIdentifiers = new Set(currentMessages.value.map(message => _entityIdentifier(message)))
|
const currentMessageIdentifiers = new Set(currentMessages.value.map(message => _entityIdentifier(message)))
|
||||||
const nextSelectedIds = selectedMessageIds.value.filter(identifier => currentMessageIdentifiers.has(identifier))
|
const nextSelectedIds = selectionList.value.filter(identifier => currentMessageIdentifiers.has(identifier))
|
||||||
|
|
||||||
if (nextSelectedIds.length !== selectedMessageIds.value.length) {
|
if (nextSelectedIds.length !== selectionList.value.length) {
|
||||||
selectedMessageIds.value = nextSelectedIds
|
_setSelectionList(nextSelectedIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMessage.value && !currentMessageIdentifiers.has(_entityIdentifier(selectedMessage.value))) {
|
if (selectedMessage.value && !currentMessageIdentifiers.has(_entityIdentifier(selectedMessage.value))) {
|
||||||
@@ -321,7 +285,7 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
selectedFolder.value = folder
|
selectedFolder.value = folder
|
||||||
selectedMessage.value = null
|
selectedMessage.value = null
|
||||||
clearSelection()
|
clearSelection()
|
||||||
selectionModeActive.value = false
|
selectionMode.value = false
|
||||||
composeMode.value = false
|
composeMode.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -348,81 +312,6 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
selectedMessage.value = null
|
selectedMessage.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMessageSelected(message: EntityObject) {
|
|
||||||
return selectedMessageIdSet.value.has(_entityIdentifier(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMessageSelection(message: EntityObject) {
|
|
||||||
const identifier = _entityIdentifier(message)
|
|
||||||
|
|
||||||
selectionModeActive.value = true
|
|
||||||
|
|
||||||
if (selectedMessageIdSet.value.has(identifier)) {
|
|
||||||
selectedMessageIds.value = selectedMessageIds.value.filter(selectedId => selectedId !== identifier)
|
|
||||||
|
|
||||||
if (selectedMessageIds.value.length === 0) {
|
|
||||||
selectionModeActive.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_setSelectedMessageIds([...selectedMessageIds.value, identifier])
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectAllCurrentMessages() {
|
|
||||||
selectionModeActive.value = true
|
|
||||||
_setSelectedMessageIds(currentMessages.value.map(message => _entityIdentifier(message)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateSelectionMode(message?: EntityObject) {
|
|
||||||
selectionModeActive.value = true
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
const identifier = _entityIdentifier(message)
|
|
||||||
|
|
||||||
if (!selectedMessageIdSet.value.has(identifier)) {
|
|
||||||
_setSelectedMessageIds([...selectedMessageIds.value, identifier])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deactivateSelectionMode() {
|
|
||||||
selectionModeActive.value = false
|
|
||||||
clearSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSelection() {
|
|
||||||
selectedMessageIds.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function openMoveDialog(messages: EntityObject | EntityObject[]) {
|
|
||||||
const nextCandidates = Array.isArray(messages) ? messages : [messages]
|
|
||||||
|
|
||||||
moveMessageCandidates.value = Array.from(
|
|
||||||
new Map(nextCandidates.map(candidate => [_entityIdentifier(candidate), candidate])).values(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (moveMessageCandidates.value.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
moveDialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function openMoveDialogForSelection() {
|
|
||||||
if (selectedMessages.value.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
openMoveDialog(selectedMessages.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeMoveDialog() {
|
|
||||||
moveDialogVisible.value = false
|
|
||||||
moveMessageCandidates.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeCompose() {
|
function closeCompose() {
|
||||||
composeMode.value = false
|
composeMode.value = false
|
||||||
composeReplyTo.value = null
|
composeReplyTo.value = null
|
||||||
@@ -438,20 +327,91 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveMessages(targetFolder: CollectionObject) {
|
function isMessageSelected(message: EntityObject) {
|
||||||
const candidates = moveMessageCandidates.value
|
return selectionList.value.includes(_entityIdentifier(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMessageSelection(message: EntityObject) {
|
||||||
|
const identifier = _entityIdentifier(message)
|
||||||
|
|
||||||
|
selectionMode.value = true
|
||||||
|
|
||||||
|
if (selectionList.value.includes(identifier)) {
|
||||||
|
_setSelectionList(selectionList.value.filter(selectedId => selectedId !== identifier))
|
||||||
|
|
||||||
if (candidates.length === 0) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const movableCandidates = candidates.filter(message => !(
|
_setSelectionList([...selectionList.value, identifier])
|
||||||
targetFolder.provider === message.provider &&
|
}
|
||||||
String(targetFolder.service) === String(message.service) &&
|
|
||||||
String(targetFolder.identifier) === String(message.collection)
|
|
||||||
))
|
|
||||||
|
|
||||||
if (movableCandidates.length === 0) {
|
function selectAllCurrentMessages() {
|
||||||
|
selectionMode.value = true
|
||||||
|
_setSelectionList(currentMessages.value.map(message => _entityIdentifier(message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSelectionMode(message?: EntityObject) {
|
||||||
|
selectionMode.value = true
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
const identifier = _entityIdentifier(message)
|
||||||
|
|
||||||
|
if (!selectionList.value.includes(identifier)) {
|
||||||
|
_setSelectionList([...selectionList.value, identifier])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateSelectionMode() {
|
||||||
|
selectionMode.value = false
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
_setSelectionList([])
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMoveDialog(entities?: EntityObject | EntityObject[]) {
|
||||||
|
|
||||||
|
moveDialogCandidates.value = []
|
||||||
|
|
||||||
|
if (entities) {
|
||||||
|
if (Array.isArray(entities)) {
|
||||||
|
moveDialogCandidates.value = entities.map(entity => _entityIdentifier(entity))
|
||||||
|
moveDialogService.value = _serviceIdentifier(entities[0])
|
||||||
|
} else {
|
||||||
|
moveDialogCandidates.value = [_entityIdentifier(entities)]
|
||||||
|
moveDialogService.value = _serviceIdentifier(entities)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
moveDialogCandidates.value = selectionList.value
|
||||||
|
moveDialogService.value = _serviceIdentifier(selectedFolder.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
moveDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMoveDialog() {
|
||||||
|
moveDialogVisible.value = false
|
||||||
|
moveDialogService.value = null
|
||||||
|
moveDialogCandidates.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveMessages(target: CollectionObject, entityIdentifiers: EntityIdentifier[]) {
|
||||||
|
const movableIdentifiers = entityIdentifiers.filter(identifier => {
|
||||||
|
const entity = entitiesStore.entityByIdentifier(identifier)
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow moving messages within the same service and disallow moving into the same folder
|
||||||
|
return entity.provider === target.provider &&
|
||||||
|
String(entity.service) === String(target.service) &&
|
||||||
|
String(entity.collection) !== String(target.identifier)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (movableIdentifiers.length === 0) {
|
||||||
closeMoveDialog()
|
closeMoveDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -459,55 +419,31 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sourceIdentifiers = movableCandidates.map(message => _entityIdentifier(message))
|
const response = await entitiesStore.move(_collectionIdentifier(target), movableIdentifiers)
|
||||||
const response = await entitiesStore.move(_collectionIdentifier(targetFolder), sourceIdentifiers)
|
const operationSucceeded: EntityIdentifier[] = []
|
||||||
const successfulMoves: EntityIdentifier[] = []
|
const operationFailures: EntityIdentifier[] = []
|
||||||
const failedMoves: string[] = []
|
|
||||||
|
|
||||||
Object.entries(response).forEach(([sourceIdentifier, result]) => {
|
Object.entries(response).forEach(([sourceIdentifier, result]) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
successfulMoves.push(sourceIdentifier as EntityIdentifier)
|
operationSucceeded.push(sourceIdentifier as EntityIdentifier)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
failedMoves.push(result.error)
|
operationFailures.push(sourceIdentifier as EntityIdentifier)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (successfulMoves.length === 0) {
|
if (operationSucceeded.length === 0) {
|
||||||
throw new Error(failedMoves[0] ?? 'Failed to move messages')
|
throw new Error(operationFailures[0] ?? 'Failed to move messages')
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeSelection(successfulMoves)
|
if (selectedMessage.value && operationSucceeded.includes(_entityIdentifier(selectedMessage.value))) {
|
||||||
|
|
||||||
if (selectedMessage.value && successfulMoves.includes(_entityIdentifier(selectedMessage.value))) {
|
|
||||||
selectedMessage.value = null
|
selectedMessage.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMessageIds.value.length === 0) {
|
clearSelection()
|
||||||
selectionModeActive.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
closeMoveDialog()
|
closeMoveDialog()
|
||||||
|
|
||||||
const servicesToRefresh = new Map<string, ServiceObject>()
|
const notification = _formatMoveNotification(operationSucceeded.length, operationFailures.length, target)
|
||||||
movableCandidates.forEach(message => {
|
|
||||||
const service = _serviceFor(message.provider, message.service)
|
|
||||||
if (service && service.identifier !== null) {
|
|
||||||
servicesToRefresh.set(`${service.provider}:${String(service.identifier)}`, service)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const targetService = _serviceFor(targetFolder.provider, targetFolder.service)
|
|
||||||
if (targetService && targetService.identifier !== null) {
|
|
||||||
servicesToRefresh.set(`${targetService.provider}:${String(targetService.identifier)}`, targetService)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.allSettled([
|
|
||||||
...Array.from(servicesToRefresh.values()).map(service => loadFoldersForService(service)),
|
|
||||||
...(selectedFolder.value ? [_reloadFolderMessages(selectedFolder.value)] : []),
|
|
||||||
])
|
|
||||||
|
|
||||||
const notification = _formatMoveNotification(successfulMoves.length, failedMoves.length, targetFolder)
|
|
||||||
notify(notification.message, notification.color)
|
notify(notification.message, notification.color)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const messageText = error instanceof Error ? error.message : 'Failed to move messages'
|
const messageText = error instanceof Error ? error.message : 'Failed to move messages'
|
||||||
@@ -519,9 +455,61 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMessage(message: EntityObject) {
|
async function deleteMessages(entityIdentifiers: EntityIdentifier[]) {
|
||||||
// TODO: implement delete via entity / collection store
|
if (entityIdentifiers.length === 0) {
|
||||||
console.log('[Mail] Delete message:', message.identifier)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await entitiesStore.delete(entityIdentifiers)
|
||||||
|
const operationSucceeded: EntityIdentifier[] = []
|
||||||
|
const operationFailures: EntityIdentifier[] = []
|
||||||
|
|
||||||
|
Object.entries(response).forEach(([sourceIdentifier, result]) => {
|
||||||
|
if (result.success) {
|
||||||
|
operationSucceeded.push(sourceIdentifier as EntityIdentifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
operationFailures.push(sourceIdentifier as EntityIdentifier)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (operationSucceeded.length === 0) {
|
||||||
|
throw new Error(operationFailures[0] ?? 'Failed to delete messages')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMessage.value && operationSucceeded.includes(_entityIdentifier(selectedMessage.value))) {
|
||||||
|
selectedMessage.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection()
|
||||||
|
|
||||||
|
const successCount = operationSucceeded.length
|
||||||
|
const failureCount = operationFailures.length
|
||||||
|
|
||||||
|
if (failureCount === 0) {
|
||||||
|
notify(
|
||||||
|
successCount === 1 ? 'Message deleted' : `${successCount} messages deleted`,
|
||||||
|
'success',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
notify(
|
||||||
|
successCount === 0
|
||||||
|
? `Delete failed for ${failureCount === 1 ? '1 message' : `${failureCount} messages`}`
|
||||||
|
: `Deleted ${successCount} ${successCount === 1 ? 'message' : 'messages'}. ${failureCount} failed.`,
|
||||||
|
successCount === 0 ? 'error' : 'warning',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const messageText = error instanceof Error ? error.message : 'Failed to delete messages'
|
||||||
|
console.error('[Mail] Failed to delete messages:', error)
|
||||||
|
notify(messageText, 'error')
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
@@ -551,23 +539,19 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
loading,
|
loading,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
selectedMessage,
|
selectedMessage,
|
||||||
selectedMessageIds,
|
selectionList,
|
||||||
selectionModeActive,
|
selectionMode,
|
||||||
composeMode,
|
composeMode,
|
||||||
composeReplyTo,
|
composeReplyTo,
|
||||||
moveDialogVisible,
|
moveDialogVisible,
|
||||||
moveMessageCandidates,
|
moveDialogService,
|
||||||
|
moveDialogCandidates,
|
||||||
serviceFolderLoadingState,
|
serviceFolderLoadingState,
|
||||||
serviceFolderLoadedState,
|
serviceFolderLoadedState,
|
||||||
serviceFolderErrorState,
|
serviceFolderErrorState,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
currentMessages,
|
currentMessages,
|
||||||
selectedMessageMap,
|
|
||||||
selectedMessages,
|
|
||||||
selectionCount,
|
|
||||||
hasSelection,
|
|
||||||
allCurrentMessagesSelected,
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
selectFolder,
|
selectFolder,
|
||||||
@@ -580,11 +564,10 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
clearSelection,
|
clearSelection,
|
||||||
openCompose,
|
openCompose,
|
||||||
openMoveDialog,
|
openMoveDialog,
|
||||||
openMoveDialogForSelection,
|
|
||||||
closeMoveDialog,
|
closeMoveDialog,
|
||||||
closeCompose,
|
closeCompose,
|
||||||
afterSent,
|
afterSent,
|
||||||
deleteMessage,
|
deleteMessages,
|
||||||
moveMessages,
|
moveMessages,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
openSettings,
|
openSettings,
|
||||||
|
|||||||
Reference in New Issue
Block a user