Files
mail/src/pages/MailPage.vue
2026-05-22 11:56:29 -04:00

487 lines
14 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, unref } from 'vue'
import { storeToRefs } from 'pinia'
import { useDisplay } from 'vuetify'
import { useModuleStore } from '@KTXC'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useMailStore } from '@/stores/mailStore'
import { useMailUiStore } from '@/stores/mailUiStore'
import type { CollectionObject, EntityObject } from '@MailManager/models'
import { ComposerMode } from '@/types/composer'
import MessageList from '@/components/MessageList.vue'
import MessageReader from '@/components/MessageReader.vue'
import MessageComposer from '@/components/MessageComposer.vue'
import CreateFolderDialog from '@/components/CreateFolderDialog.vue'
import DeleteFolderDialog from '@/components/DeleteFolderDialog.vue'
import FolderSelectionDialog from '@/components/FolderSelectionDialog.vue'
import RenameFolderDialog from '@/components/RenameFolderDialog.vue'
import SettingsDialog from '@/components/settings/SettingsDialog.vue'
import FolderView from '@/components/FolderView.vue'
// Vuetify display for responsive behavior
const display = useDisplay()
const isMobile = computed(() => display.mdAndDown.value)
// Check if mail manager is available
const moduleStore = useModuleStore()
const isManagerAvailable = computed(() => {
return moduleStore.has('mail_manager') || moduleStore.has('MailManager')
})
const collectionsStore = useCollectionsStore()
// Mail module store
const mailStore = useMailStore()
const mailUiStore = useMailUiStore()
// storeToRefs preserves reactivity for state and computed properties
const {
loading,
selectedMessage,
currentMessages,
} = storeToRefs(mailStore)
const {
sidebarVisible,
settingsDialogVisible,
selectedFolder,
composerMode,
composerSource,
composerVisible,
selectionList,
selectionMode,
moveMessagesDialogVisible,
moveMessagesDialogService,
createFolderDialogVisible,
createFolderDialogService,
createFolderDialogLoading,
createFolderDialogError,
renameFolderDialogVisible,
renameFolderDialogService,
renameFolderDialogFolder,
renameFolderDialogLoading,
renameFolderDialogError,
moveFolderDialogVisible,
moveFolderDialogService,
deleteFolderDialogVisible,
deleteFolderDialogService,
deleteFolderDialogFolder,
deleteFolderDialogLoading,
deleteFolderDialogError,
} = storeToRefs(mailUiStore)
// Complex store/composable objects accessed directly (not simple refs)
const { mailSync, entitiesStore } = mailStore
const lastSyncLabel = computed(() => {
const lastSync = unref(unref(mailSync.lastSync))
if (!(lastSync instanceof Date)) return ''
return `(Last: ${lastSync.toLocaleTimeString()})`
})
// Initialize
onMounted(async () => {
if (!isManagerAvailable.value) return
await mailUiStore.initialize()
})
// Handlers — thin wrappers that delegate to the store
const {
validateCreateFolderName,
validateRenameFolderName,
} = mailUiStore
const sidebarToggle = () => mailUiStore.sidebarToggle()
const handleSettingsOpen = () => mailUiStore.settingsOpen()
const handleFolderSelect = (folder: CollectionObject) => mailUiStore.selectFolder(folder)
const handleFolderCreateConfirm = async (folderName: string) => {
try {
const mutatedFolder = await mailUiStore.confirmCreateFolder(folderName)
if (mutatedFolder) {
handleFolderSelect(mutatedFolder)
}
} catch (error: unknown) {
console.error('[MailPage] Failed to create folder:', error)
}
}
const handleFolderEditConfirm = async (folderName: string) => {
try {
const mutatedFolder = await mailUiStore.confirmRenameFolder(folderName)
if (mutatedFolder) {
handleFolderSelect(mutatedFolder)
}
} catch (error: unknown) {
console.error('[MailPage] Failed to rename folder:', error)
}
}
const handleFolderDeleteConfirm = async () => {
try {
await mailUiStore.confirmDeleteFolder()
} catch (error: unknown) {
console.error('[MailPage] Failed to delete folder:', error)
}
}
const handleFolderMoveConfirm = async (targetFolder: CollectionObject) => {
try {
await mailUiStore.confirmMoveFolder(targetFolder)
} catch (error: unknown) {
console.error('[MailPage] Failed to move folder:', error)
}
}
const handleFolderMoveCancel = () => mailUiStore.closeMoveFolderDialog()
const handleMessageOpen = (message: EntityObject) => {
mailStore.selectMessage(message)
if (isMobile.value) {
mailUiStore.sidebarHide()
}
}
const handleMessageComposeFresh = () => mailUiStore.openComposer()
const handleMessageComposeReply = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Reply)
const handleMessageComposeForward = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Forward)
const handleMessageComposeClose = () => mailUiStore.closeComposer()
const handleMessageFlag = (message: EntityObject, flag: string, value: boolean) => {
mailStore.flagMessages([message.identifier], { [flag]: value })
}
const handleMessageDelete = (message: EntityObject) => {
mailStore.deleteMessages([message.identifier])
}
const handleMessageMove = (message: EntityObject) => mailUiStore.openMoveMessagesDialog(message)
const handleMessageMoveConfirm = async (target: CollectionObject) => { await mailUiStore.confirmMoveMessages(target) }
const handleMessageMoveCancel = () => mailUiStore.closeMoveMessagesDialog()
const handleMessageSelectionMode = (message: EntityObject) => mailUiStore.messageSelectionModeActivate(message)
const handleMessageSelectionToggleOne = (message: EntityObject) => mailUiStore.messageSelectionToggleOne(message)
const handleMessageSelectionToggleAll = (value: boolean) => {
mailUiStore.messageSelectionToggleAll(value)
}
const handleMessageSelectionClear = () => mailUiStore.messageSelectionModeDeactivate()
const handleMessageSelectionMove = () => mailUiStore.openMoveMessagesDialog()
const handleMessageSelectionFlag = (flag: string, value: boolean) => mailUiStore.flagSelectedMessages(flag, value)
const handleMessageSelectionDelete = () => mailUiStore.deleteSelectedMessages()
</script>
<template>
<!-- Manager Unavailable -->
<div v-if="!isManagerAvailable" class="mail-unavailable">
<v-alert
type="warning"
variant="outlined"
class="mail-unavailable-alert"
>
<v-icon size="64" color="warning" class="mb-6">mdi-email-off-outline</v-icon>
<h2 class="text-h5 font-weight-bold mb-4">Mail Manager Not Available</h2>
<p>
The Mail Manager module is not installed or enabled.
This module requires the <strong>mail_manager</strong> module to function properly.
</p>
<p class="mb-0 mt-2">
Please contact your system administrator to install and enable the
<code>mail_manager</code> module.
</p>
</v-alert>
</div>
<!-- Normal UI -->
<div v-else class="mail-container">
<!-- Top toolbar -->
<v-app-bar class="mail-toolbar" elevation="0" density="compact">
<v-app-bar-nav-icon
v-if="isMobile"
@click="sidebarToggle"
/>
<v-app-bar-title>Mail</v-app-bar-title>
<v-spacer />
<v-btn
icon="mdi-pencil"
@click="handleMessageComposeFresh()"
color="primary"
variant="text"
>
<v-icon>mdi-pencil</v-icon>
<v-tooltip activator="parent" location="bottom">Compose</v-tooltip>
</v-btn>
<v-btn
icon="mdi-refresh"
@click="mailSync.sync()"
:loading="mailSync.isRunning && entitiesStore.transceiving"
variant="text"
>
<v-icon>mdi-refresh</v-icon>
<v-tooltip activator="parent" location="bottom">
Refresh {{ lastSyncLabel }}
</v-tooltip>
</v-btn>
<v-icon
v-if="mailSync.isRunning"
color="success"
size="small"
class="ml-2"
>
mdi-sync
</v-icon>
</v-app-bar>
<!-- Main content area -->
<div class="mail-content">
<!-- Folder tree sidebar -->
<v-navigation-drawer
v-model="sidebarVisible"
:permanent="!isMobile"
:temporary="isMobile"
width="280"
class="mail-sidebar"
>
<FolderView
:selected-folder="selectedFolder"
@select="handleFolderSelect"
/>
<template #append>
<div class="pa-2">
<v-btn
block
variant="text"
prepend-icon="mdi-cog"
@click="handleSettingsOpen"
>
Settings
</v-btn>
</div>
</template>
</v-navigation-drawer>
<!-- Main area with message list and reader -->
<div class="mail-main">
<div class="mail-wrapper">
<!-- Message list panel -->
<div class="mail-list-panel">
<MessageList
:messages="currentMessages"
:selected-collection="selectedFolder"
:selected-message="selectedMessage"
:selection-list="selectionList"
:selection-mode="selectionMode"
:loading="loading"
@open="handleMessageOpen"
@selection-mode="handleMessageSelectionMode"
@selection-toggle-one="handleMessageSelectionToggleOne"
@selection-toggle-all="handleMessageSelectionToggleAll"
@selection-clear="handleMessageSelectionClear"
@selection-flag="handleMessageSelectionFlag"
@selection-move="handleMessageSelectionMove"
@selection-delete="handleMessageSelectionDelete"
/>
</div>
<!-- Reader/Composer panel -->
<div class="mail-reader-panel">
<MessageComposer
v-if="composerVisible"
:mode="composerMode"
:source="composerSource"
:folder="selectedFolder"
@close="handleMessageComposeClose"
/>
<MessageReader
v-else
:entity="selectedMessage"
@compose="handleMessageComposeFresh"
@reply="handleMessageComposeReply"
@forward="handleMessageComposeForward"
@move="handleMessageMove"
@delete="handleMessageDelete"
/>
</div>
</div>
</div>
</div>
<!-- Settings Dialog -->
<SettingsDialog v-model="settingsDialogVisible" />
<FolderSelectionDialog
v-if="moveMessagesDialogService"
v-model="moveMessagesDialogVisible"
:service="moveMessagesDialogService"
:loading="loading"
title="Move Messages To"
confirm-text="Move"
empty-text="No other folders are available in this account."
@select="handleMessageMoveConfirm"
@cancel="handleMessageMoveCancel"
/>
<FolderSelectionDialog
v-if="moveFolderDialogService"
v-model="moveFolderDialogVisible"
:service="moveFolderDialogService"
:loading="collectionsStore.transceiving"
title="Move Folder To"
confirm-text="Move"
empty-text="No other folders are available in this account."
:disabled-folder-keys="mailUiStore.moveFolderDialogInvalidFolderKeys"
@select="handleFolderMoveConfirm"
@cancel="handleFolderMoveCancel"
/>
<CreateFolderDialog
v-if="createFolderDialogService"
v-model="createFolderDialogVisible"
:service="createFolderDialogService"
:parent-folder-label="mailUiStore.createFolderDialogParentLabel"
:validate-name="validateCreateFolderName"
:loading="createFolderDialogLoading"
:error-message="createFolderDialogError"
@confirm="handleFolderCreateConfirm"
/>
<RenameFolderDialog
v-if="renameFolderDialogService && renameFolderDialogFolder"
v-model="renameFolderDialogVisible"
:service="renameFolderDialogService"
:folder="renameFolderDialogFolder"
:parent-folder-label="mailUiStore.renameFolderDialogParentLabel"
:validate-name="validateRenameFolderName"
:loading="renameFolderDialogLoading"
:error-message="renameFolderDialogError"
@confirm="handleFolderEditConfirm"
/>
<DeleteFolderDialog
v-if="deleteFolderDialogService && deleteFolderDialogFolder"
v-model="deleteFolderDialogVisible"
:service="deleteFolderDialogService"
:folder="deleteFolderDialogFolder"
:loading="deleteFolderDialogLoading"
:error-message="deleteFolderDialogError"
@confirm="handleFolderDeleteConfirm"
/>
</div>
</template>
<style scoped lang="scss">
.mail-unavailable {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 48px;
text-align: center;
width: 100%;
}
.mail-unavailable-alert {
width: 100%;
text-align: left;
}
.mail-container {
display: flex;
flex-direction: column;
height: 100vh;
isolation: isolate;
}
.mail-toolbar {
flex-shrink: 0;
}
.mail-content {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
}
.mail-sidebar {
border-right: 1px solid rgb(var(--v-border-color));
overflow-y: auto;
}
.mail-main {
flex: 1;
display: flex;
overflow: hidden;
min-width: 0;
}
.mail-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.mail-list-panel {
width: 320px;
min-width: 280px;
max-width: 450px;
border-right: 1px solid rgb(var(--v-border-color));
overflow-y: auto;
display: flex;
flex-direction: column;
}
.mail-reader-panel {
flex: 1;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
}
/* Responsive adjustments */
@media (max-width: 960px) {
.mail-wrapper {
flex-direction: column;
}
.mail-list-panel {
width: 100%;
max-width: 100%;
border-right: none;
border-bottom: 1px solid rgb(var(--v-border-color));
max-height: 50%;
}
.mail-reader-panel {
width: 100%;
}
}
</style>