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 + + + + + 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') 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"] }