Merge pull request 'refactor: improvemets' (#9) from refactor/improvements into main

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2026-03-24 23:14:07 +00:00
11 changed files with 499 additions and 270 deletions

View File

@@ -3,12 +3,12 @@ import { ref, computed, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore' import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { CollectionPropertiesObject } from '@MailManager/models/collection' import { CollectionPropertiesObject } from '@MailManager/models/collection'
import type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service' import type { ServiceObject } from '@MailManager/models'
// Props // Props
interface Props { interface Props {
modelValue: boolean modelValue: boolean
service: ServiceInterface service: ServiceObject
parentFolder?: CollectionObject | null parentFolder?: CollectionObject | null
allFolders?: CollectionObject[] allFolders?: CollectionObject[]
} }

View File

@@ -1,34 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service' import type { ServiceObject } from '@MailManager/models'
// Props // Props
interface Props { interface Props {
selectedFolder?: CollectionObject | null selectedFolder?: CollectionObject | null
serviceGroups: Array<{ serviceGroups: Array<{
service: ServiceInterface service: ServiceObject
folders: CollectionObject[] loading: boolean
loaded: boolean
error: string | null
}> }>
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const collectionsStore = useCollectionsStore()
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
select: [folder: CollectionObject] select: [folder: CollectionObject]
createFolder: [service: ServiceInterface, parentFolder: CollectionObject | null] createFolder: [service: ServiceObject, parentFolder: CollectionObject | null]
editFolder: [service: ServiceInterface, folder: CollectionObject] editFolder: [service: ServiceObject, folder: CollectionObject]
}>() }>()
// Page-based navigation state per service account // Page-based navigation state per service account
const pageLevels = ref<Record<string, (string | number | null)[]>>({}) const pageLevels = ref<Record<string, (string | number | null)[]>>({})
const getServiceKey = (service: ServiceInterface): string => { const getServiceKey = (service: ServiceObject): string => {
return `${service.provider}-${service.identifier}` return `${service.provider}-${service.identifier}`
} }
const getCurrentPageLevel = (service: ServiceInterface): (string | number | null)[] => { const getCurrentPageLevel = (service: ServiceObject): (string | number | null)[] => {
const key = getServiceKey(service) const key = getServiceKey(service)
if (!pageLevels.value[key]) { if (!pageLevels.value[key]) {
pageLevels.value[key] = [null] pageLevels.value[key] = [null]
@@ -37,20 +41,19 @@ const getCurrentPageLevel = (service: ServiceInterface): (string | number | null
} }
// Get folders for current page level // Get folders for current page level
const getCurrentPageFolders = (service: ServiceInterface, folders: CollectionObject[]): CollectionObject[] => { const getCurrentPageFolders = (service: ServiceObject): CollectionObject[] => {
const level = getCurrentPageLevel(service) const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1] const currentParent = level[level.length - 1]
return folders.filter(f => { return collectionsStore.collectionsInCollection(service.provider, service.identifier, currentParent)
if (currentParent === null) {
return f.collection === null || f.collection === undefined
}
return String(f.collection) === String(currentParent)
})
} }
// Check if folder has children // Check if folder has children
const hasChildren = (folder: CollectionObject, allFolders: CollectionObject[]): boolean => { const hasChildren = (folder: CollectionObject): boolean => {
return allFolders.some(f => String(f.collection) === String(folder.identifier)) return collectionsStore.hasChildrenInCollection(folder.provider, folder.service, folder.identifier)
}
const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
return collectionsStore.collectionsForService(service.provider, service.identifier)
} }
// Get icon for folder based on role // Get icon for folder based on role
@@ -109,13 +112,13 @@ const handleFolderClick = (folder: CollectionObject) => {
} }
// Navigate into a folder to show its children // Navigate into a folder to show its children
const handleNavigateInto = (service: ServiceInterface, folderId: string | number) => { const handleNavigateInto = (service: ServiceObject, folderId: string | number) => {
const level = getCurrentPageLevel(service) const level = getCurrentPageLevel(service)
level.push(folderId) level.push(folderId)
} }
// Navigate back in page-based view // Navigate back in page-based view
const navigateBack = (service: ServiceInterface) => { const navigateBack = (service: ServiceObject) => {
const level = getCurrentPageLevel(service) const level = getCurrentPageLevel(service)
if (level.length > 1) { if (level.length > 1) {
level.pop() level.pop()
@@ -123,14 +126,14 @@ const navigateBack = (service: ServiceInterface) => {
} }
// Get breadcrumb label for current page // Get breadcrumb label for current page
const getCurrentBreadcrumb = (service: ServiceInterface, folders: CollectionObject[]): string => { const getCurrentBreadcrumb = (service: ServiceObject): string => {
const level = getCurrentPageLevel(service) const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1] const currentParent = level[level.length - 1]
if (currentParent === null) return 'All Folders' if (currentParent === null) return 'All Folders'
const labels = level const labels = level
.filter((id): id is string | number => id !== null) .filter((id): id is string | number => id !== null)
.map(id => folders.find(f => String(f.identifier) === String(id))?.properties.label) .map(id => collectionsStore.collection(service.provider, service.identifier, id)?.properties.label)
.filter((label): label is string => !!label) .filter((label): label is string => !!label)
if (labels.length === 0) return 'Folders' if (labels.length === 0) return 'Folders'
@@ -139,14 +142,12 @@ const getCurrentBreadcrumb = (service: ServiceInterface, folders: CollectionObje
} }
// Get current parent folder for dialog context // Get current parent folder for dialog context
const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionObject[]): CollectionObject | null => { const getCurrentParentFolder = (service: ServiceObject): CollectionObject | null => {
const level = getCurrentPageLevel(service) const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1] const currentParent = level[level.length - 1]
if (currentParent === null) return null if (currentParent === null) return null
// Search through all folders in the array return collectionsStore.collection(service.provider, service.identifier, currentParent)
const found = folders.find(f => String(f.identifier) === String(currentParent))
return found || null
} }
</script> </script>
@@ -176,7 +177,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
variant="text" variant="text"
size="small" size="small"
density="compact" density="compact"
@click.stop="emit('createFolder', group.service, getCurrentParentFolder(group.service, group.folders))" @click.stop="emit('createFolder', group.service, getCurrentParentFolder(group.service))"
> >
<v-icon>mdi-folder-plus</v-icon> <v-icon>mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom">New Folder</v-tooltip> <v-tooltip activator="parent" location="bottom">New Folder</v-tooltip>
@@ -187,12 +188,12 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
<!-- Breadcrumb with New Folder button --> <!-- Breadcrumb with New Folder button -->
<v-list-subheader v-if="getCurrentPageLevel(group.service).length > 1" class="d-flex align-center"> <v-list-subheader v-if="getCurrentPageLevel(group.service).length > 1" class="d-flex align-center">
<span class="flex-grow-1">{{ getCurrentBreadcrumb(group.service, group.folders) }}</span> <span class="flex-grow-1">{{ getCurrentBreadcrumb(group.service) }}</span>
<v-btn <v-btn
icon="mdi-folder-plus" icon="mdi-folder-plus"
variant="text" variant="text"
size="x-small" size="x-small"
@click="emit('createFolder', group.service, getCurrentParentFolder(group.service, group.folders))" @click="emit('createFolder', group.service, getCurrentParentFolder(group.service))"
> >
<v-icon size="small">mdi-folder-plus</v-icon> <v-icon size="small">mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom">New Subfolder</v-tooltip> <v-tooltip activator="parent" location="bottom">New Subfolder</v-tooltip>
@@ -210,7 +211,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
<!-- Current level folders --> <!-- Current level folders -->
<v-list-item <v-list-item
v-for="folder in getCurrentPageFolders(group.service, group.folders)" v-for="folder in getCurrentPageFolders(group.service)"
:key="`${folder.provider}-${folder.service}-${folder.identifier}`" :key="`${folder.provider}-${folder.service}-${folder.identifier}`"
class="folder-page-item folder-row-item" class="folder-page-item folder-row-item"
:title="folder.properties.label" :title="folder.properties.label"
@@ -236,7 +237,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
<!-- Chevron for folders with children --> <!-- Chevron for folders with children -->
<v-btn <v-btn
v-if="hasChildren(folder, group.folders)" v-if="hasChildren(folder)"
icon="mdi-chevron-right" icon="mdi-chevron-right"
variant="text" variant="text"
size="small" size="small"
@@ -274,20 +275,73 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
</v-menu> </v-menu>
</template> </template>
</v-list-item> </v-list-item>
<v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item">
<template v-slot: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 v-slot: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"
disabled
class="folder-status-item"
>
<template v-slot:prepend>
<v-icon icon="mdi-folder-off-outline" />
</template>
<v-list-item-title>No folders found</v-list-item-title>
</v-list-item>
</v-list-group> </v-list-group>
<!-- Single service - show folders directly --> <!-- Single service - show folders directly -->
<template v-else> <template v-else>
<v-list-item
class="account-header-item"
:title="group.service.label || 'Mail Account'"
:subtitle="group.service.primaryAddress || undefined"
>
<template v-slot:prepend>
<v-icon icon="mdi-email-outline" />
</template>
<template v-slot:append>
<v-btn
icon="mdi-folder-plus"
variant="text"
size="small"
density="compact"
@click.stop="emit('createFolder', group.service, getCurrentParentFolder(group.service))"
>
<v-icon>mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom">New Folder</v-tooltip>
</v-btn>
</template>
</v-list-item>
<!-- Header with New Folder button --> <!-- Header with New Folder button -->
<v-list-subheader class="d-flex align-center"> <v-list-subheader class="d-flex align-center">
<span class="flex-grow-1"> <span class="flex-grow-1">
{{ getCurrentPageLevel(group.service).length > 1 ? getCurrentBreadcrumb(group.service, group.folders) : 'FOLDERS' }} {{ getCurrentPageLevel(group.service).length > 1 ? getCurrentBreadcrumb(group.service) : 'FOLDERS' }}
</span> </span>
<v-btn <v-btn
icon="mdi-folder-plus" icon="mdi-folder-plus"
variant="text" variant="text"
size="x-small" size="x-small"
@click="emit('createFolder', group.service, getCurrentParentFolder(group.service, group.folders))" @click="emit('createFolder', group.service, getCurrentParentFolder(group.service))"
> >
<v-icon size="small">mdi-folder-plus</v-icon> <v-icon size="small">mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom"> <v-tooltip activator="parent" location="bottom">
@@ -307,7 +361,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
<!-- Current level folders --> <!-- Current level folders -->
<v-list-item <v-list-item
v-for="folder in getCurrentPageFolders(group.service, group.folders)" v-for="folder in getCurrentPageFolders(group.service)"
:key="`${folder.provider}-${folder.service}-${folder.identifier}`" :key="`${folder.provider}-${folder.service}-${folder.identifier}`"
class="folder-row-item" class="folder-row-item"
:title="folder.properties.label" :title="folder.properties.label"
@@ -333,7 +387,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
<!-- Chevron for folders with children or Menu for actions --> <!-- Chevron for folders with children or Menu for actions -->
<v-btn <v-btn
v-if="hasChildren(folder, group.folders)" v-if="hasChildren(folder)"
icon="mdi-chevron-right" icon="mdi-chevron-right"
variant="text" variant="text"
size="small" size="small"
@@ -370,11 +424,59 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
</v-menu> </v-menu>
</template> </template>
</v-list-item> </v-list-item>
<v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item">
<template v-slot: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 v-slot: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"
disabled
class="folder-status-item"
>
<template v-slot:prepend>
<v-icon icon="mdi-folder-off-outline" />
</template>
<v-list-item-title>No folders found</v-list-item-title>
</v-list-item>
</template> </template>
</template> </template>
</div> </div>
</template> </template>
<style scoped>
.account-header-item {
--v-list-item-prepend-size: 22px;
background-color: rgba(var(--v-theme-primary), 0.1);
border-radius: 6px;
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>
<style scoped> <style scoped>
.v-list-item--active { .v-list-item--active {
background-color: rgba(var(--v-theme-primary), 0.12); background-color: rgba(var(--v-theme-primary), 0.12);

View File

@@ -2,22 +2,20 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore' import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useServicesStore } from '@MailManager/stores/servicesStore' import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useMailStore } from '@/stores/mailStore'
import { useUser } from '@KTXC' import { useUser } from '@KTXC'
import FolderTreeView from './FolderTreeView.vue' import FolderTreeView from './FolderTreeView.vue'
import FolderPageView from './FolderPageView.vue' import FolderPageView from './FolderPageView.vue'
import CreateFolderDialog from './CreateFolderDialog.vue' import CreateFolderDialog from './CreateFolderDialog.vue'
import RenameFolderDialog from './RenameFolderDialog.vue' import RenameFolderDialog from './RenameFolderDialog.vue'
import type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service' import type { ServiceObject } from '@MailManager/models'
type FolderViewMode = 'tree' | 'page' type FolderViewMode = 'tree' | 'page'
// Props const props = defineProps<{
interface Props {
selectedFolder?: CollectionObject | null selectedFolder?: CollectionObject | null
} }>()
const props = defineProps<Props>()
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
@@ -28,6 +26,7 @@ const emit = defineEmits<{
// Stores // Stores
const collectionsStore = useCollectionsStore() const collectionsStore = useCollectionsStore()
const servicesStore = useServicesStore() const servicesStore = useServicesStore()
const mailStore = useMailStore()
// User settings // User settings
const { settings } = useUser() const { settings } = useUser()
@@ -39,14 +38,14 @@ const folderViewMode = computed(() => {
// Create folder dialog state // Create folder dialog state
const createDialogVisible = ref(false) const createDialogVisible = ref(false)
const createDialogService = ref<ServiceInterface | null>(null) const createDialogService = ref<ServiceObject | null>(null)
const createDialogParent = ref<CollectionObject | null>(null) const createDialogParent = ref<CollectionObject | null>(null)
const renameDialogVisible = ref(false) const renameDialogVisible = ref(false)
const renameDialogService = ref<ServiceInterface | null>(null) const renameDialogService = ref<ServiceObject | null>(null)
const renameDialogFolder = ref<CollectionObject | null>(null) const renameDialogFolder = ref<CollectionObject | null>(null)
// Handle create folder event from child components // Handle create folder event from child components
const handleCreateFolder = (service: ServiceInterface, parentFolder: CollectionObject | null = null) => { const handleCreateFolder = (service: ServiceObject, parentFolder: CollectionObject | null = null) => {
createDialogService.value = service createDialogService.value = service
createDialogParent.value = parentFolder createDialogParent.value = parentFolder
createDialogVisible.value = true createDialogVisible.value = true
@@ -58,7 +57,7 @@ const handleFolderCreated = (newFolder: CollectionObject) => {
emit('select', newFolder) emit('select', newFolder)
} }
const handleEditFolder = (service: ServiceInterface, folder: CollectionObject) => { const handleEditFolder = (service: ServiceObject, folder: CollectionObject) => {
renameDialogService.value = service renameDialogService.value = service
renameDialogFolder.value = folder renameDialogFolder.value = folder
renameDialogVisible.value = true renameDialogVisible.value = true
@@ -70,71 +69,27 @@ const handleFolderRenamed = (updatedFolder: CollectionObject) => {
// Computed: all folders for validation // Computed: all folders for validation
const allFolders = computed(() => const allFolders = computed(() =>
serviceGroups.value.flatMap(g => g.folders) servicesStore.services.flatMap(service =>
collectionsStore.collectionsForService(service.provider, service.identifier),
)
) )
// Folder hierarchy helper type interface ServiceGroup {
interface FolderNode { service: ServiceObject
folder: CollectionObject loading: boolean
children: FolderNode[] loaded: boolean
error: string | null
} }
// Build hierarchical tree structure
const buildFolderTree = (folders: CollectionObject[]): FolderNode[] => {
const nodeMap = new Map<string | number, FolderNode>()
const roots: FolderNode[] = []
// Create nodes for all folders
folders.forEach(folder => {
nodeMap.set(folder.identifier, {
folder,
children: []
})
})
// Build parent-child relationships
folders.forEach(folder => {
const node = nodeMap.get(folder.identifier)
if (!node) return
if (folder.collection === null || folder.collection === undefined) {
// Root level folder
roots.push(node)
} else {
// Child folder - add to parent
const parent = nodeMap.get(folder.collection)
if (parent) {
parent.children.push(node)
} else {
// Parent not found, treat as root
roots.push(node)
}
}
})
return roots
}
// Group collections by service
const serviceGroups = computed(() => { const serviceGroups = computed(() => {
const groups: Array<{ const groups: ServiceGroup[] = []
service: ServiceInterface
folders: CollectionObject[]
folderTree: FolderNode[]
}> = []
servicesStore.services.forEach(service => { servicesStore.services.forEach(service => {
const folders = collectionsStore.collections.filter(
c => c.provider === service.provider && String(c.service) === String(service.identifier)
)
if (folders.length > 0) {
groups.push({ groups.push({
service, service,
folders, loading: mailStore.isServiceFolderLoading(service.provider, service.identifier),
folderTree: buildFolderTree(folders) loaded: mailStore.hasServiceFoldersLoaded(service.provider, service.identifier),
error: mailStore.getServiceFolderError(service.provider, service.identifier),
}) })
}
}) })
return groups return groups
@@ -164,7 +119,7 @@ const serviceGroups = computed(() => {
/> />
<!-- Empty state --> <!-- Empty state -->
<v-list-item v-if="serviceGroups.length === 0"> <v-list-item v-if="servicesStore.services.length === 0">
<v-list-item-title class="text-center text-medium-emphasis"> <v-list-item-title class="text-center text-medium-emphasis">
No mail accounts configured No mail accounts configured
</v-list-item-title> </v-list-item-title>

View File

@@ -1,26 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service' import type { ServiceObject } from '@MailManager/models'
export interface FolderNode {
folder: CollectionObject
children: FolderNode[]
}
export interface Props { export interface Props {
node: FolderNode folder: CollectionObject
service: ServiceInterface service: ServiceObject
selectedFolder?: CollectionObject | null selectedFolder?: CollectionObject | null
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const collectionsStore = useCollectionsStore()
const expanded = ref(false)
const emit = defineEmits<{ const emit = defineEmits<{
select: [folder: CollectionObject] select: [folder: CollectionObject]
createSubfolder: [service: ServiceInterface, parentFolder: CollectionObject] createSubfolder: [service: ServiceObject, parentFolder: CollectionObject]
editFolder: [service: ServiceInterface, folder: CollectionObject] editFolder: [service: ServiceObject, folder: CollectionObject]
}>() }>()
const childFolders = computed(() => {
return collectionsStore.collectionsInCollection(props.service.provider, props.service.identifier, props.folder.identifier)
})
const hasChildren = computed(() => {
return collectionsStore.hasChildrenInCollection(props.service.provider, props.service.identifier, props.folder.identifier)
})
const handleExpandableFolderClick = () => {
expanded.value = !expanded.value
emit('select', props.folder)
}
// Get icon for folder based on role // Get icon for folder based on role
const getFolderIcon = (folder: CollectionObject): string => { const getFolderIcon = (folder: CollectionObject): string => {
switch (folder.properties.role) { switch (folder.properties.role) {
@@ -73,26 +85,26 @@ const isSelected = (folder: CollectionObject): boolean => {
</script> </script>
<template> <template>
<v-list-group v-if="node.children.length > 0" class="folder-tree-group"> <v-list-group v-if="hasChildren" v-model="expanded" class="folder-tree-group">
<template v-slot:activator="{ props: activatorProps }"> <template v-slot:activator="{ props: activatorProps }">
<v-list-item <v-list-item
v-bind="activatorProps" v-bind="activatorProps"
class="folder-row-item" class="folder-row-item"
:title="node.folder.properties.label" :title="folder.properties.label"
:active="isSelected(node.folder)" :active="isSelected(folder)"
@click.stop="emit('select', node.folder)" @click.stop="handleExpandableFolderClick()"
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-icon <v-icon
:icon="getFolderIcon(node.folder)" :icon="getFolderIcon(folder)"
:color="getFolderColor(node.folder)" :color="getFolderColor(folder)"
/> />
</template> </template>
<template v-slot:append> <template v-slot:append>
<v-badge <v-badge
v-if="node.folder.properties.unread && node.folder.properties.unread > 0" v-if="folder.properties.unread && folder.properties.unread > 0"
:content="node.folder.properties.unread" :content="folder.properties.unread"
color="primary" color="primary"
inline inline
class="mr-2" class="mr-2"
@@ -116,13 +128,13 @@ const isSelected = (folder: CollectionObject): boolean => {
<v-list density="compact"> <v-list density="compact">
<v-list-item <v-list-item
prepend-icon="mdi-pencil" prepend-icon="mdi-pencil"
@click="emit('editFolder', service, node.folder)" @click="emit('editFolder', service, folder)"
> >
<v-list-item-title>Edit Folder Name</v-list-item-title> <v-list-item-title>Edit Folder Name</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
prepend-icon="mdi-folder-plus" prepend-icon="mdi-folder-plus"
@click="emit('createSubfolder', service, node.folder)" @click="emit('createSubfolder', service, folder)"
> >
<v-list-item-title>New Subfolder</v-list-item-title> <v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item> </v-list-item>
@@ -132,11 +144,11 @@ const isSelected = (folder: CollectionObject): boolean => {
</v-list-item> </v-list-item>
</template> </template>
<div class="folder-tree-children"> <div v-if="expanded" class="folder-tree-children">
<FolderTreeNode <FolderTreeNode
v-for="child in node.children" v-for="childFolder in childFolders"
:key="child.folder.identifier" :key="childFolder.identifier"
:node="child" :folder="childFolder"
:service="service" :service="service"
:selected-folder="selectedFolder" :selected-folder="selectedFolder"
@select="emit('select', $event)" @select="emit('select', $event)"
@@ -149,21 +161,21 @@ const isSelected = (folder: CollectionObject): boolean => {
<v-list-item <v-list-item
v-else v-else
class="folder-tree-item folder-row-item" class="folder-tree-item folder-row-item"
:title="node.folder.properties.label" :title="folder.properties.label"
:active="isSelected(node.folder)" :active="isSelected(folder)"
@click="emit('select', node.folder)" @click="emit('select', folder)"
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-icon <v-icon
:icon="getFolderIcon(node.folder)" :icon="getFolderIcon(folder)"
:color="getFolderColor(node.folder)" :color="getFolderColor(folder)"
/> />
</template> </template>
<template v-slot:append> <template v-slot:append>
<v-badge <v-badge
v-if="node.folder.properties.unread && node.folder.properties.unread > 0" v-if="folder.properties.unread && folder.properties.unread > 0"
:content="node.folder.properties.unread" :content="folder.properties.unread"
color="primary" color="primary"
inline inline
class="mr-2" class="mr-2"
@@ -187,13 +199,13 @@ const isSelected = (folder: CollectionObject): boolean => {
<v-list density="compact"> <v-list density="compact">
<v-list-item <v-list-item
prepend-icon="mdi-pencil" prepend-icon="mdi-pencil"
@click="emit('editFolder', service, node.folder)" @click="emit('editFolder', service, folder)"
> >
<v-list-item-title>Edit Folder Name</v-list-item-title> <v-list-item-title>Edit Folder Name</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
prepend-icon="mdi-folder-plus" prepend-icon="mdi-folder-plus"
@click="emit('createSubfolder', service, node.folder)" @click="emit('createSubfolder', service, folder)"
> >
<v-list-item-title>New Subfolder</v-list-item-title> <v-list-item-title>New Subfolder</v-list-item-title>
</v-list-item> </v-list-item>

View File

@@ -1,32 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service' import type { ServiceObject } from '@MailManager/models'
import FolderTreeNode from './FolderTreeNode.vue' import FolderTreeNode from './FolderTreeNode.vue'
// Folder hierarchy helper type
export interface FolderNode {
folder: CollectionObject
children: FolderNode[]
}
// Props // Props
interface Props { interface Props {
selectedFolder?: CollectionObject | null selectedFolder?: CollectionObject | null
serviceGroups: Array<{ serviceGroups: Array<{
service: ServiceInterface service: ServiceObject
folders: CollectionObject[] loading: boolean
folderTree: FolderNode[] loaded: boolean
error: string | null
}> }>
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const collectionsStore = useCollectionsStore()
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
select: [folder: CollectionObject] select: [folder: CollectionObject]
createFolder: [service: ServiceInterface, parentFolder: CollectionObject | null] createFolder: [service: ServiceObject, parentFolder: CollectionObject | null]
editFolder: [service: ServiceInterface, folder: CollectionObject] editFolder: [service: ServiceObject, folder: CollectionObject]
}>() }>()
const getRootFolders = (service: ServiceObject): CollectionObject[] => {
return collectionsStore.collectionsInCollection(service.provider, service.identifier, null)
}
const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
return collectionsStore.collectionsForService(service.provider, service.identifier)
}
</script> </script>
<template> <template>
@@ -61,19 +66,72 @@ const emit = defineEmits<{
</template> </template>
<FolderTreeNode <FolderTreeNode
v-for="node in group.folderTree" v-for="folder in getRootFolders(group.service)"
:key="`${node.folder.provider}-${node.folder.service}-${node.folder.identifier}`" :key="`${folder.provider}-${folder.service}-${folder.identifier}`"
:node="node" :folder="folder"
:service="group.service" :service="group.service"
:selected-folder="selectedFolder" :selected-folder="selectedFolder"
@select="emit('select', $event)" @select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)" @create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)" @edit-folder="(service, folder) => emit('editFolder', service, folder)"
/> />
<v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item">
<template v-slot: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 v-slot: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"
disabled
class="folder-status-item"
>
<template v-slot:prepend>
<v-icon icon="mdi-folder-off-outline" />
</template>
<v-list-item-title>No folders found</v-list-item-title>
</v-list-item>
</v-list-group> </v-list-group>
<!-- Single service - show folders directly --> <!-- Single service - show folders directly -->
<template v-else> <template v-else>
<v-list-item
class="account-header-item account-header-static"
:title="group.service.label || 'Mail Account'"
:subtitle="group.service.primaryAddress || undefined"
>
<template v-slot:prepend>
<v-icon icon="mdi-email-outline" />
</template>
<template v-slot:append>
<v-btn
icon="mdi-folder-plus"
variant="text"
size="small"
density="compact"
@click.stop="emit('createFolder', group.service, null)"
>
<v-icon>mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom">New Folder</v-tooltip>
</v-btn>
</template>
</v-list-item>
<!-- Header with New Folder button --> <!-- Header with New Folder button -->
<v-list-subheader class="d-flex align-center"> <v-list-subheader class="d-flex align-center">
<span class="flex-grow-1">FOLDERS</span> <span class="flex-grow-1">FOLDERS</span>
@@ -89,15 +147,45 @@ const emit = defineEmits<{
</v-list-subheader> </v-list-subheader>
<FolderTreeNode <FolderTreeNode
v-for="node in group.folderTree" v-for="folder in getRootFolders(group.service)"
:key="`${node.folder.provider}-${node.folder.service}-${node.folder.identifier}`" :key="`${folder.provider}-${folder.service}-${folder.identifier}`"
:node="node" :folder="folder"
:service="group.service" :service="group.service"
:selected-folder="selectedFolder" :selected-folder="selectedFolder"
@select="emit('select', $event)" @select="emit('select', $event)"
@create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)" @create-subfolder="(service, parentFolder) => emit('createFolder', service, parentFolder)"
@edit-folder="(service, folder) => emit('editFolder', service, folder)" @edit-folder="(service, folder) => emit('editFolder', service, folder)"
/> />
<v-list-item v-if="group.loading && getServiceFolders(group.service).length === 0" disabled class="folder-status-item">
<template v-slot: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 v-slot: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"
disabled
class="folder-status-item"
>
<template v-slot:prepend>
<v-icon icon="mdi-folder-off-outline" />
</template>
<v-list-item-title>No folders found</v-list-item-title>
</v-list-item>
</template> </template>
</template> </template>
</div> </div>
@@ -114,8 +202,16 @@ const emit = defineEmits<{
border-radius: 6px; border-radius: 6px;
} }
.account-header-static {
margin-bottom: 4px;
}
.account-header-item :deep(.v-list-item__prepend) { .account-header-item :deep(.v-list-item__prepend) {
padding-inline-start: 4px; padding-inline-start: 4px;
margin-inline-end: 2px; margin-inline-end: 2px;
} }
.folder-status-item {
padding-inline-start: 16px;
}
</style> </style>

View File

@@ -3,11 +3,11 @@ import { ref, computed, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore' import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { CollectionPropertiesObject } from '@MailManager/models/collection' import { CollectionPropertiesObject } from '@MailManager/models/collection'
import type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service' import type { ServiceObject } from '@MailManager/models'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
service: ServiceInterface service: ServiceObject
folder: CollectionObject folder: CollectionObject
allFolders?: CollectionObject[] allFolders?: CollectionObject[]
} }

View File

@@ -11,6 +11,7 @@ import MessageComposer from '@/components/MessageComposer.vue'
import SettingsDialog from '@/components/settings/SettingsDialog.vue' import SettingsDialog from '@/components/settings/SettingsDialog.vue'
import type { EntityInterface } from '@MailManager/types/entity' import type { EntityInterface } from '@MailManager/types/entity'
import type { MessageInterface } from '@MailManager/types/message' 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()
@@ -22,7 +23,7 @@ const isMailManagerAvailable = computed(() => {
return moduleStore.has('mail_manager') || moduleStore.has('MailManager') return moduleStore.has('mail_manager') || moduleStore.has('MailManager')
}) })
// Mail store — single source of truth for all mail UI state // Mail module store
const mailStore = useMailStore() const mailStore = useMailStore()
// storeToRefs preserves reactivity for state and computed properties // storeToRefs preserves reactivity for state and computed properties
@@ -34,9 +35,6 @@ const {
selectedMessage, selectedMessage,
composeMode, composeMode,
composeReplyTo, composeReplyTo,
snackbarVisible,
snackbarMessage,
snackbarColor,
currentMessages, currentMessages,
} = storeToRefs(mailStore) } = storeToRefs(mailStore)
@@ -50,31 +48,25 @@ onMounted(async () => {
}) })
// Handlers — thin wrappers that delegate to the store // Handlers — thin wrappers that delegate to the store
const handleFolderSelect = (folder: Parameters<typeof mailStore.selectFolder>[0]) => const handleFolderSelect = (folder: CollectionObject) => mailStore.selectFolder(folder)
mailStore.selectFolder(folder)
const handleMessageSelect = (message: EntityInterface<MessageInterface>) => const handleMessageSelect = (message: EntityInterface<MessageInterface>) => mailStore.selectMessage(message, isMobile.value)
mailStore.selectMessage(message, isMobile.value)
const handleCompose = (replyTo?: EntityInterface<MessageInterface>) => const handleCompose = (replyTo?: EntityInterface<MessageInterface>) => mailStore.openCompose(replyTo)
mailStore.openCompose(replyTo)
const handleComposeClose = () => mailStore.closeCompose() const handleComposeClose = () => mailStore.closeCompose()
const handleComposeSent = () => mailStore.afterSent() const handleComposeSent = () => mailStore.afterSent()
const handleReply = (message: EntityInterface<MessageInterface>) => const handleReply = (message: EntityInterface<MessageInterface>) => mailStore.openCompose(message)
mailStore.openCompose(message)
const handleDelete = (message: EntityInterface<MessageInterface>) => const handleDelete = (message: EntityInterface<MessageInterface>) => mailStore.deleteMessage(message)
mailStore.deleteMessage(message)
const toggleSidebar = () => mailStore.toggleSidebar() const toggleSidebar = () => mailStore.toggleSidebar()
const handleSettingsOpen = () => mailStore.openSettings() const handleSettingsOpen = () => mailStore.openSettings()
const handleFolderCreated = (folder: Parameters<typeof mailStore.onFolderCreated>[0]) => const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Folder "${folder.properties.label}" created`, 'success')
mailStore.onFolderCreated(folder)
</script> </script>
<template> <template>
@@ -124,17 +116,17 @@ const handleFolderCreated = (folder: Parameters<typeof mailStore.onFolderCreated
<v-btn <v-btn
icon="mdi-refresh" icon="mdi-refresh"
@click="mailSync.sync()" @click="mailSync.sync()"
:loading="mailSync.isRunning.value && entitiesStore.transceiving" :loading="mailSync.isRunning && entitiesStore.transceiving"
variant="text" variant="text"
> >
<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.value ? `(Last: ${new Date(mailSync.lastSync.value).toLocaleTimeString()})` : '' }} Refresh {{ mailSync.lastSync ? `(Last: ${new Date(mailSync.lastSync).toLocaleTimeString()})` : '' }}
</v-tooltip> </v-tooltip>
</v-btn> </v-btn>
<v-icon <v-icon
v-if="mailSync.isRunning.value" v-if="mailSync.isRunning"
color="success" color="success"
size="small" size="small"
class="ml-2" class="ml-2"
@@ -211,24 +203,6 @@ const handleFolderCreated = (folder: Parameters<typeof mailStore.onFolderCreated
<!-- Settings Dialog --> <!-- Settings Dialog -->
<SettingsDialog v-model="settingsDialogVisible" /> <SettingsDialog v-model="settingsDialogVisible" />
<!-- Success Snackbar -->
<v-snackbar
v-model="snackbarVisible"
:color="snackbarColor"
:timeout="3000"
location="bottom right"
>
{{ snackbarMessage }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbarVisible = false"
>
Close
</v-btn>
</template>
</v-snackbar>
</div> </div>
</template> </template>

View File

@@ -4,52 +4,56 @@ import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useEntitiesStore } from '@MailManager/stores/entitiesStore' 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 type { CollectionObject } from '@MailManager/models/collection' import type { CollectionObject } from '@MailManager/models/collection'
import type { EntityInterface } from '@MailManager/types/entity' 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'
export const useMailStore = defineStore('mailStore', () => { export const useMailStore = defineStore('mailStore', () => {
const collectionsStore = useCollectionsStore() const collectionsStore = useCollectionsStore()
const entitiesStore = useEntitiesStore() const entitiesStore = useEntitiesStore()
const servicesStore = useServicesStore() const servicesStore = useServicesStore()
const { showSnackbar } = useSnackbar()
// Background mail sync // Background mail sync
const mailSync = useMailSync({ const mailSyncController = useMailSync({
interval: 30000, interval: 30000,
autoStart: false, autoStart: false,
fetchDetails: true, fetchDetails: true,
}) })
const mailSync = {
isRunning: mailSyncController.isRunning,
lastSync: mailSyncController.lastSync,
error: mailSyncController.error,
sync: mailSyncController.sync,
start: mailSyncController.start,
stop: mailSyncController.stop,
restart: mailSyncController.restart,
}
// ── UI State ────────────────────────────────────────────────────────────── // ── General State ─────────────────-───────────────────────────────────────
const sidebarVisible = ref(true) const sidebarVisible = ref(true)
const settingsDialogVisible = ref(false) const settingsDialogVisible = ref(false)
const loading = ref(false) const loading = ref(false)
const serviceFolderLoadingState = ref<Record<string, boolean>>({})
const serviceFolderLoadedState = ref<Record<string, boolean>>({})
const serviceFolderErrorState = ref<Record<string, string | null>>({})
// ── 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<EntityInterface<MessageInterface> | null>(null)
// ── Compose State ───────────────────────────────────────────────────────── // ── Compose State ─────────────────────────────────────────────────────────
const composeMode = ref(false) const composeMode = ref(false)
const composeReplyTo = shallowRef<EntityInterface<MessageInterface> | null>(null) const composeReplyTo = shallowRef<EntityInterface<MessageInterface> | null>(null)
// ── Notification State ────────────────────────────────────────────────────
const snackbarVisible = ref(false)
const snackbarMessage = ref('')
const snackbarColor = ref<'success' | 'error' | 'info' | 'warning'>('success')
// ── Computed ────────────────────────────────────────────────────────────── // ── Computed ──────────────────────────────────────────────────────────────
const currentMessages = computed(() => { const currentMessages = computed(() => {
if (!selectedFolder.value) return [] if (!selectedFolder.value) return []
const folder = selectedFolder.value const folder = selectedFolder.value
// Access entitiesStore.entities (reactive computed array) so Vue tracks it
return entitiesStore.entities.filter(e => return entitiesStore.entities.filter(e =>
e.provider === folder.provider && e.provider === folder.provider &&
String(e.service) === String(folder.service) && String(e.service) === String(folder.service) &&
@@ -57,14 +61,109 @@ export const useMailStore = defineStore('mailStore', () => {
) )
}) })
// ── Initialization ────────────────────────────────────────────────────────
async function initialize() {
loading.value = true
try {
await servicesStore.list()
const services = [...servicesStore.services]
services.forEach(service => {
void loadFoldersForService(service,{ selectInbox: true })
})
} catch (error) {
console.error('[Mail] Failed to initialize:', error)
} finally {
loading.value = false
}
}
async function loadFoldersForService(
service: ServiceObject,
options: { selectInbox?: boolean } = {},
) {
if (service.identifier === null) {
return
}
_setServiceFolderLoading(service.provider, service.identifier, true)
_setServiceFolderError(service.provider, service.identifier, null)
try {
// retrieve folders for service
const collections = await collectionsStore.list({
[service.provider]: {
[String(service.identifier)]: true,
},
})
_setServiceFolderLoaded(service.provider, service.identifier, true)
if (options.selectInbox && !selectedFolder.value) {
const inbox = Object.values(collections).find(
folder =>
folder.provider === service.provider &&
String(folder.service) === String(service.identifier) &&
(folder.properties.role === 'inbox' ||
String(folder.identifier).toLowerCase() === 'inbox'),
)
if (inbox) {
await selectFolder(inbox)
}
}
_updateSyncSources()
return collections
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to load folders'
_setServiceFolderError(service.provider, service.identifier, message)
console.error(
`[Mail] Failed to load folders for ${service.provider}:${String(service.identifier)}:`,
error,
)
_updateSyncSources()
return {}
} finally {
_setServiceFolderLoading(service.provider, service.identifier, false)
}
}
// ── Sync Helpers ────────────────────────────────────────────────────────── // ── Sync Helpers ──────────────────────────────────────────────────────────
function _serviceKey(provider: string, service: string | number) {
return `${provider}:${String(service)}`
}
function _setServiceFolderLoading(provider: string, service: string | number, loadingState: boolean) {
serviceFolderLoadingState.value = {
...serviceFolderLoadingState.value,
[_serviceKey(provider, service)]: loadingState,
}
}
function _setServiceFolderLoaded(provider: string, service: string | number, loaded: boolean) {
serviceFolderLoadedState.value = {
...serviceFolderLoadedState.value,
[_serviceKey(provider, service)]: loaded,
}
}
function _setServiceFolderError(provider: string, service: string | number, error: string | null) {
serviceFolderErrorState.value = {
...serviceFolderErrorState.value,
[_serviceKey(provider, service)]: error,
}
}
function _updateSyncSources() { function _updateSyncSources() {
mailSync.clearSources() mailSyncController.clearSources()
// Track the currently selected folder // Track the currently selected folder
if (selectedFolder.value) { if (selectedFolder.value) {
mailSync.addSource({ mailSyncController.addSource({
provider: selectedFolder.value.provider, provider: selectedFolder.value.provider,
service: selectedFolder.value.service, service: selectedFolder.value.service,
collections: [selectedFolder.value.identifier], collections: [selectedFolder.value.identifier],
@@ -73,15 +172,15 @@ export const useMailStore = defineStore('mailStore', () => {
// Always track inboxes for each account (for new-mail notifications) // Always track inboxes for each account (for new-mail notifications)
servicesStore.services.forEach(service => { servicesStore.services.forEach(service => {
const inboxes = collectionsStore.collections.filter( const inboxes = collectionsStore.collectionsForService(service.provider, service.identifier).filter(
c => c =>
c.service === service.identifier && String(c.service) === String(service.identifier) &&
(c.properties.role === 'inbox' || (c.properties.role === 'inbox' ||
String(c.identifier).toLowerCase() === 'inbox'), String(c.identifier).toLowerCase() === 'inbox'),
) )
if (inboxes.length > 0) { if (inboxes.length > 0) {
mailSync.addSource({ mailSyncController.addSource({
provider: service.provider, provider: service.provider,
service: service.identifier as string | number, service: service.identifier as string | number,
collections: inboxes.map(inbox => inbox.identifier), collections: inboxes.map(inbox => inbox.identifier),
@@ -89,11 +188,23 @@ export const useMailStore = defineStore('mailStore', () => {
} }
}) })
if (mailSync.sources.value.length > 0 && !mailSync.isRunning.value) { if (mailSyncController.sources.value.length > 0 && !mailSyncController.isRunning.value) {
mailSync.start() mailSyncController.start()
} }
} }
function isServiceFolderLoading(provider: string, service: string | number) {
return serviceFolderLoadingState.value[_serviceKey(provider, service)] === true
}
function hasServiceFoldersLoaded(provider: string, service: string | number) {
return serviceFolderLoadedState.value[_serviceKey(provider, service)] === true
}
function getServiceFolderError(provider: string, service: string | number) {
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
}
// ── Actions ─────────────────────────────────────────────────────────────── // ── Actions ───────────────────────────────────────────────────────────────
async function selectFolder(folder: CollectionObject) { async function selectFolder(folder: CollectionObject) {
@@ -159,38 +270,8 @@ export const useMailStore = defineStore('mailStore', () => {
settingsDialogVisible.value = true settingsDialogVisible.value = true
} }
function notify(message: string, color: typeof snackbarColor.value = 'success') { function notify(message: string, color: 'success' | 'error' | 'info' | 'warning' = 'success') {
snackbarMessage.value = message showSnackbar({ message, color })
snackbarColor.value = color
snackbarVisible.value = true
}
async function onFolderCreated(folder: CollectionObject) {
notify(`Folder "${folder.properties.label}" created successfully`)
// Reload collections so the sidebar reflects the new folder
await collectionsStore.list()
}
// ── Initialization ────────────────────────────────────────────────────────
async function initialize() {
loading.value = true
try {
await servicesStore.list()
await collectionsStore.list()
// Select inbox by default
const inbox = collectionsStore.collections.find(c => c.properties.role === 'inbox')
if (inbox) {
await selectFolder(inbox)
}
mailSync.start()
} catch (error) {
console.error('[Mail] Failed to initialize:', error)
} finally {
loading.value = false
}
} }
// ── Exports ─────────────────────────────────────────────────────────────── // ── Exports ───────────────────────────────────────────────────────────────
@@ -210,9 +291,9 @@ export const useMailStore = defineStore('mailStore', () => {
selectedMessage, selectedMessage,
composeMode, composeMode,
composeReplyTo, composeReplyTo,
snackbarVisible, serviceFolderLoadingState,
snackbarMessage, serviceFolderLoadedState,
snackbarColor, serviceFolderErrorState,
// Computed // Computed
currentMessages, currentMessages,
@@ -227,7 +308,10 @@ export const useMailStore = defineStore('mailStore', () => {
toggleSidebar, toggleSidebar,
openSettings, openSettings,
notify, notify,
onFolderCreated, isServiceFolderLoading,
hasServiceFoldersLoaded,
getServiceFolderError,
loadFoldersForService,
initialize, initialize,
} }
}) })

View File

@@ -1,12 +1,12 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["src/**/*", "src/**/*.vue"], "include": ["src/**/*", "src/**/*.vue", "../../core/src/**/*.ts"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"@KTXC": ["../../core/src/shared/index.ts"],
"@KTXC/*": ["../../core/src/*"], "@KTXC/*": ["../../core/src/*"],
"@MailManager/*": ["../mail_manager/src/*"] "@MailManager/*": ["../mail_manager/src/*"]
} }

View File

@@ -1,13 +1,7 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "files": [],
"include": ["src/**/*", "src/**/*.vue"], "references": [
"exclude": ["src/**/__tests__/*"], { "path": "./tsconfig.app.json" },
"compilerOptions": { { "path": "./tsconfig.node.json" }
"composite": true, ]
"paths": {
"@/*": ["./src/*"],
"@KTXC/*": ["../../core/src/*"],
"@MailManager/*": ["../mail_manager/src/*"]
}
}
} }

View File

@@ -1,10 +1,22 @@
{ {
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"types": ["node"] "types": ["node"]
} },
"include": ["vite.config.ts"]
} }