diff --git a/src/components/CreateFolderDialog.vue b/src/components/CreateFolderDialog.vue
index 386c0b0..80ac8f7 100644
--- a/src/components/CreateFolderDialog.vue
+++ b/src/components/CreateFolderDialog.vue
@@ -3,12 +3,12 @@ import { ref, computed, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { CollectionPropertiesObject } 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
interface Props {
modelValue: boolean
- service: ServiceInterface
+ service: ServiceObject
parentFolder?: CollectionObject | null
allFolders?: CollectionObject[]
}
diff --git a/src/components/FolderPageView.vue b/src/components/FolderPageView.vue
index 73b108d..7e64d9d 100644
--- a/src/components/FolderPageView.vue
+++ b/src/components/FolderPageView.vue
@@ -1,34 +1,38 @@
@@ -176,7 +177,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
variant="text"
size="small"
density="compact"
- @click.stop="emit('createFolder', group.service, getCurrentParentFolder(group.service, group.folders))"
+ @click.stop="emit('createFolder', group.service, getCurrentParentFolder(group.service))"
>
mdi-folder-plus
New Folder
@@ -187,12 +188,12 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
- {{ getCurrentBreadcrumb(group.service, group.folders) }}
+ {{ getCurrentBreadcrumb(group.service) }}
mdi-folder-plus
New Subfolder
@@ -210,7 +211,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
+
+
+
+
+
+ Loading folders
+
+
+
+
+
+
+ Folders unavailable
+ {{ group.error }}
+
+
+
+
+
+
+ No folders found
+
+
+
- {{ getCurrentPageLevel(group.service).length > 1 ? getCurrentBreadcrumb(group.service, group.folders) : 'FOLDERS' }}
+ {{ getCurrentPageLevel(group.service).length > 1 ? getCurrentBreadcrumb(group.service) : 'FOLDERS' }}
mdi-folder-plus
@@ -307,7 +361,7 @@ const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionOb
+
+
+
+
+
+ Loading folders
+
+
+
+
+
+
+ Folders unavailable
+ {{ group.error }}
+
+
+
+
+
+
+ No folders found
+
+
+
diff --git a/src/components/RenameFolderDialog.vue b/src/components/RenameFolderDialog.vue
index ec26b45..2628a75 100644
--- a/src/components/RenameFolderDialog.vue
+++ b/src/components/RenameFolderDialog.vue
@@ -3,11 +3,11 @@ import { ref, computed, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { CollectionPropertiesObject } 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 {
modelValue: boolean
- service: ServiceInterface
+ service: ServiceObject
folder: CollectionObject
allFolders?: CollectionObject[]
}
diff --git a/src/pages/MailPage.vue b/src/pages/MailPage.vue
index c1e0c62..188a244 100644
--- a/src/pages/MailPage.vue
+++ b/src/pages/MailPage.vue
@@ -11,6 +11,7 @@ import MessageComposer from '@/components/MessageComposer.vue'
import SettingsDialog from '@/components/settings/SettingsDialog.vue'
import type { EntityInterface } from '@MailManager/types/entity'
import type { MessageInterface } from '@MailManager/types/message'
+import type { CollectionObject } from '@MailManager/models'
// Vuetify display for responsive behavior
const display = useDisplay()
@@ -22,7 +23,7 @@ const isMailManagerAvailable = computed(() => {
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()
// storeToRefs preserves reactivity for state and computed properties
@@ -34,9 +35,6 @@ const {
selectedMessage,
composeMode,
composeReplyTo,
- snackbarVisible,
- snackbarMessage,
- snackbarColor,
currentMessages,
} = storeToRefs(mailStore)
@@ -50,31 +48,25 @@ onMounted(async () => {
})
// Handlers — thin wrappers that delegate to the store
-const handleFolderSelect = (folder: Parameters[0]) =>
- mailStore.selectFolder(folder)
+const handleFolderSelect = (folder: CollectionObject) => mailStore.selectFolder(folder)
-const handleMessageSelect = (message: EntityInterface) =>
- mailStore.selectMessage(message, isMobile.value)
+const handleMessageSelect = (message: EntityInterface) => mailStore.selectMessage(message, isMobile.value)
-const handleCompose = (replyTo?: EntityInterface) =>
- mailStore.openCompose(replyTo)
+const handleCompose = (replyTo?: EntityInterface) => mailStore.openCompose(replyTo)
const handleComposeClose = () => mailStore.closeCompose()
const handleComposeSent = () => mailStore.afterSent()
-const handleReply = (message: EntityInterface) =>
- mailStore.openCompose(message)
+const handleReply = (message: EntityInterface) => mailStore.openCompose(message)
-const handleDelete = (message: EntityInterface) =>
- mailStore.deleteMessage(message)
+const handleDelete = (message: EntityInterface) => mailStore.deleteMessage(message)
const toggleSidebar = () => mailStore.toggleSidebar()
const handleSettingsOpen = () => mailStore.openSettings()
-const handleFolderCreated = (folder: Parameters[0]) =>
- mailStore.onFolderCreated(folder)
+const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Folder "${folder.properties.label}" created`, 'success')
@@ -124,17 +116,17 @@ const handleFolderCreated = (folder: Parameters
mdi-refresh
- Refresh {{ mailSync.lastSync.value ? `(Last: ${new Date(mailSync.lastSync.value).toLocaleTimeString()})` : '' }}
+ Refresh {{ mailSync.lastSync ? `(Last: ${new Date(mailSync.lastSync).toLocaleTimeString()})` : '' }}
-
-
-
- {{ snackbarMessage }}
-
-
- Close
-
-
-
diff --git a/src/stores/mailStore.ts b/src/stores/mailStore.ts
index 7beec24..bd94464 100644
--- a/src/stores/mailStore.ts
+++ b/src/stores/mailStore.ts
@@ -4,52 +4,56 @@ import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useMailSync } from '@MailManager/composables/useMailSync'
+import { useSnackbar } from '@KTXC'
import type { CollectionObject } from '@MailManager/models/collection'
import type { EntityInterface } from '@MailManager/types/entity'
import type { MessageInterface } from '@MailManager/types/message'
+import type { ServiceObject } from '@MailManager/models'
export const useMailStore = defineStore('mailStore', () => {
const collectionsStore = useCollectionsStore()
const entitiesStore = useEntitiesStore()
const servicesStore = useServicesStore()
+ const { showSnackbar } = useSnackbar()
// Background mail sync
- const mailSync = useMailSync({
+ const mailSyncController = useMailSync({
interval: 30000,
autoStart: false,
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 settingsDialogVisible = ref(false)
const loading = ref(false)
+ const serviceFolderLoadingState = ref>({})
+ const serviceFolderLoadedState = ref>({})
+ const serviceFolderErrorState = ref>({})
// ── Selection State ───────────────────────────────────────────────────────
-
const selectedFolder = shallowRef(null)
const selectedMessage = shallowRef | null>(null)
// ── Compose State ─────────────────────────────────────────────────────────
-
const composeMode = ref(false)
const composeReplyTo = shallowRef | null>(null)
- // ── Notification State ────────────────────────────────────────────────────
-
- const snackbarVisible = ref(false)
- const snackbarMessage = ref('')
- const snackbarColor = ref<'success' | 'error' | 'info' | 'warning'>('success')
-
// ── Computed ──────────────────────────────────────────────────────────────
-
const currentMessages = computed(() => {
if (!selectedFolder.value) return []
const folder = selectedFolder.value
- // Access entitiesStore.entities (reactive computed array) so Vue tracks it
return entitiesStore.entities.filter(e =>
e.provider === folder.provider &&
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 ──────────────────────────────────────────────────────────
+ 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() {
- mailSync.clearSources()
+ mailSyncController.clearSources()
// Track the currently selected folder
if (selectedFolder.value) {
- mailSync.addSource({
+ mailSyncController.addSource({
provider: selectedFolder.value.provider,
service: selectedFolder.value.service,
collections: [selectedFolder.value.identifier],
@@ -73,15 +172,15 @@ export const useMailStore = defineStore('mailStore', () => {
// Always track inboxes for each account (for new-mail notifications)
servicesStore.services.forEach(service => {
- const inboxes = collectionsStore.collections.filter(
+ const inboxes = collectionsStore.collectionsForService(service.provider, service.identifier).filter(
c =>
- c.service === service.identifier &&
+ String(c.service) === String(service.identifier) &&
(c.properties.role === 'inbox' ||
String(c.identifier).toLowerCase() === 'inbox'),
)
if (inboxes.length > 0) {
- mailSync.addSource({
+ mailSyncController.addSource({
provider: service.provider,
service: service.identifier as string | number,
collections: inboxes.map(inbox => inbox.identifier),
@@ -89,11 +188,23 @@ export const useMailStore = defineStore('mailStore', () => {
}
})
- if (mailSync.sources.value.length > 0 && !mailSync.isRunning.value) {
- mailSync.start()
+ if (mailSyncController.sources.value.length > 0 && !mailSyncController.isRunning.value) {
+ 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 ───────────────────────────────────────────────────────────────
async function selectFolder(folder: CollectionObject) {
@@ -159,38 +270,8 @@ export const useMailStore = defineStore('mailStore', () => {
settingsDialogVisible.value = true
}
- function notify(message: string, color: typeof snackbarColor.value = 'success') {
- snackbarMessage.value = message
- 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
- }
+ function notify(message: string, color: 'success' | 'error' | 'info' | 'warning' = 'success') {
+ showSnackbar({ message, color })
}
// ── Exports ───────────────────────────────────────────────────────────────
@@ -210,9 +291,9 @@ export const useMailStore = defineStore('mailStore', () => {
selectedMessage,
composeMode,
composeReplyTo,
- snackbarVisible,
- snackbarMessage,
- snackbarColor,
+ serviceFolderLoadingState,
+ serviceFolderLoadedState,
+ serviceFolderErrorState,
// Computed
currentMessages,
@@ -227,7 +308,10 @@ export const useMailStore = defineStore('mailStore', () => {
toggleSidebar,
openSettings,
notify,
- onFolderCreated,
+ isServiceFolderLoading,
+ hasServiceFoldersLoaded,
+ getServiceFolderError,
+ loadFoldersForService,
initialize,
}
})
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 9f595f9..63fa08c 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -1,12 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
- "include": ["src/**/*", "src/**/*.vue"],
+ "include": ["src/**/*", "src/**/*.vue", "../../core/src/**/*.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
- "baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
+ "@KTXC": ["../../core/src/shared/index.ts"],
"@KTXC/*": ["../../core/src/*"],
"@MailManager/*": ["../mail_manager/src/*"]
}
diff --git a/tsconfig.json b/tsconfig.json
index 05abc69..1ffef60 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,13 +1,7 @@
{
- "extends": "@vue/tsconfig/tsconfig.dom.json",
- "include": ["src/**/*", "src/**/*.vue"],
- "exclude": ["src/**/__tests__/*"],
- "compilerOptions": {
- "composite": true,
- "paths": {
- "@/*": ["./src/*"],
- "@KTXC/*": ["../../core/src/*"],
- "@MailManager/*": ["../mail_manager/src/*"]
- }
- }
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
}
diff --git a/tsconfig.node.json b/tsconfig.node.json
index 42e92b5..b0b7275 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -1,10 +1,22 @@
{
- "extends": "@vue/tsconfig/tsconfig.node.json",
- "include": ["vite.config.*"],
"compilerOptions": {
- "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
"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"]
- }
+ },
+ "include": ["vite.config.ts"]
}