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:
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user