566 lines
18 KiB
Vue
566 lines
18 KiB
Vue
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
|
|
import type { CollectionObject } from '@MailManager/models/collection'
|
|
import type { ServiceObject } from '@MailManager/models'
|
|
|
|
// Props
|
|
interface Props {
|
|
selectedFolder?: CollectionObject | null
|
|
serviceGroups: Array<{
|
|
service: ServiceObject
|
|
loading: boolean
|
|
loaded: boolean
|
|
error: string | null
|
|
}>
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const collectionsStore = useCollectionsStore()
|
|
|
|
// Emits
|
|
const emit = defineEmits<{
|
|
select: [folder: CollectionObject]
|
|
createFolder: [service: ServiceObject, parentFolder: CollectionObject | null]
|
|
editFolder: [folder: CollectionObject]
|
|
moveFolder: [folder: CollectionObject]
|
|
deleteFolder: [folder: CollectionObject]
|
|
}>()
|
|
|
|
// Page-based navigation state per service account
|
|
const pageLevels = ref<Record<string, (string | number | null)[]>>({})
|
|
|
|
const getServiceKey = (service: ServiceObject): string => {
|
|
return `${service.provider}-${service.identifier}`
|
|
}
|
|
|
|
const getCurrentPageLevel = (service: ServiceObject): (string | number | null)[] => {
|
|
const key = getServiceKey(service)
|
|
if (!pageLevels.value[key]) {
|
|
pageLevels.value[key] = [null]
|
|
}
|
|
return pageLevels.value[key]
|
|
}
|
|
|
|
// Get folders for current page level
|
|
const getCurrentPageFolders = (service: ServiceObject): CollectionObject[] => {
|
|
if (service.identifier === null) {
|
|
return []
|
|
}
|
|
|
|
const level = getCurrentPageLevel(service)
|
|
const currentParent = level[level.length - 1]
|
|
return collectionsStore.collectionsInCollection(service.provider, service.identifier, currentParent)
|
|
}
|
|
|
|
// Check if folder has children
|
|
const hasChildren = (folder: CollectionObject): boolean => {
|
|
return collectionsStore.hasChildrenInCollection(folder.provider, folder.service, folder.identifier)
|
|
}
|
|
|
|
const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
|
|
if (service.identifier === null) {
|
|
return []
|
|
}
|
|
|
|
return collectionsStore.collectionsForService(service.provider, service.identifier)
|
|
}
|
|
|
|
const canDeleteFolder = (folder: CollectionObject): boolean => {
|
|
return !folder.properties.role
|
|
}
|
|
|
|
// Get icon for folder based on role
|
|
const getFolderIcon = (folder: CollectionObject): string => {
|
|
switch (folder.properties.role) {
|
|
case 'inbox':
|
|
return 'mdi-inbox'
|
|
case 'sent':
|
|
return 'mdi-send'
|
|
case 'drafts':
|
|
return 'mdi-file-document'
|
|
case 'trash':
|
|
return 'mdi-delete'
|
|
case 'junk':
|
|
return 'mdi-alert-octagon'
|
|
case 'archive':
|
|
return 'mdi-archive'
|
|
case 'outbox':
|
|
return 'mdi-tray-arrow-up'
|
|
default:
|
|
return 'mdi-folder'
|
|
}
|
|
}
|
|
|
|
// Get color for folder based on role
|
|
const getFolderColor = (folder: CollectionObject): string | undefined => {
|
|
switch (folder.properties.role) {
|
|
case 'inbox':
|
|
return 'primary'
|
|
case 'sent':
|
|
return 'success'
|
|
case 'drafts':
|
|
return 'warning'
|
|
case 'trash':
|
|
return 'error'
|
|
case 'junk':
|
|
return 'orange'
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
// Check if folder is selected
|
|
const isSelected = (folder: CollectionObject): boolean => {
|
|
if (!props.selectedFolder) return false
|
|
return (
|
|
folder.provider === props.selectedFolder.provider &&
|
|
String(folder.service) === String(props.selectedFolder.service) &&
|
|
String(folder.identifier) === String(props.selectedFolder.identifier)
|
|
)
|
|
}
|
|
|
|
// Handle folder click - just select it
|
|
const handleFolderClick = (folder: CollectionObject) => {
|
|
emit('select', folder)
|
|
}
|
|
|
|
// Navigate into a folder to show its children
|
|
const handleNavigateInto = (service: ServiceObject, folderId: string | number) => {
|
|
const level = getCurrentPageLevel(service)
|
|
level.push(folderId)
|
|
}
|
|
|
|
// Navigate back in page-based view
|
|
const navigateBack = (service: ServiceObject) => {
|
|
const level = getCurrentPageLevel(service)
|
|
if (level.length > 1) {
|
|
level.pop()
|
|
}
|
|
}
|
|
|
|
// Get breadcrumb label for current page
|
|
const getCurrentBreadcrumb = (service: ServiceObject): string => {
|
|
if (service.identifier === null) {
|
|
return 'Folders'
|
|
}
|
|
|
|
const level = getCurrentPageLevel(service)
|
|
const currentParent = level[level.length - 1]
|
|
if (currentParent === null) return 'All Folders'
|
|
|
|
const labels = level
|
|
.filter((id): id is string | number => id !== null)
|
|
.map(id => collectionsStore.collection(service.provider, service.identifier, id)?.properties.label)
|
|
.filter((label): label is string => !!label)
|
|
|
|
if (labels.length === 0) return 'Folders'
|
|
if (labels.length <= 2) return labels.join('/')
|
|
return `../${labels.slice(-2).join('/')}`
|
|
}
|
|
|
|
// Get current parent folder for dialog context
|
|
const getCurrentParentFolder = (service: ServiceObject): CollectionObject | null => {
|
|
if (service.identifier === null) {
|
|
return null
|
|
}
|
|
|
|
const level = getCurrentPageLevel(service)
|
|
const currentParent = level[level.length - 1]
|
|
if (currentParent === null) return null
|
|
|
|
return collectionsStore.collection(service.provider, service.identifier, currentParent)
|
|
}
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<template v-for="(group, index) in serviceGroups" :key="`${group.service.provider}-${group.service.identifier}`">
|
|
<!-- Service account group -->
|
|
<v-list-group
|
|
v-if="serviceGroups.length > 1"
|
|
:class="['no-indent', { 'account-group-spaced': index < serviceGroups.length - 1 }]"
|
|
>
|
|
<template v-slot:activator="{ props: activatorProps }">
|
|
<v-list-item
|
|
v-bind="activatorProps"
|
|
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>
|
|
</template>
|
|
|
|
<!-- Breadcrumb with New Folder button -->
|
|
<v-list-subheader v-if="getCurrentPageLevel(group.service).length > 1" class="d-flex align-center">
|
|
<span class="flex-grow-1">{{ getCurrentBreadcrumb(group.service) }}</span>
|
|
<v-btn
|
|
icon="mdi-folder-plus"
|
|
variant="text"
|
|
size="x-small"
|
|
@click="emit('createFolder', group.service, getCurrentParentFolder(group.service))"
|
|
>
|
|
<v-icon size="small">mdi-folder-plus</v-icon>
|
|
<v-tooltip activator="parent" location="bottom">New Subfolder</v-tooltip>
|
|
</v-btn>
|
|
</v-list-subheader>
|
|
|
|
<!-- Back button if not at root -->
|
|
<v-list-item
|
|
v-if="getCurrentPageLevel(group.service).length > 1"
|
|
class="back-row-item"
|
|
@click="navigateBack(group.service)"
|
|
prepend-icon="mdi-arrow-left"
|
|
title="Back"
|
|
/>
|
|
|
|
<!-- Current level folders -->
|
|
<v-list-item
|
|
v-for="folder in getCurrentPageFolders(group.service)"
|
|
:key="`${folder.provider}-${folder.service}-${folder.identifier}`"
|
|
class="folder-page-item folder-row-item"
|
|
:title="folder.properties.label"
|
|
:active="isSelected(folder)"
|
|
@click="handleFolderClick(folder)"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon
|
|
:icon="getFolderIcon(folder)"
|
|
:color="getFolderColor(folder)"
|
|
/>
|
|
</template>
|
|
|
|
<template v-slot:append>
|
|
<!-- Unread badge -->
|
|
<v-badge
|
|
v-if="folder.properties.unread && folder.properties.unread > 0"
|
|
:content="folder.properties.unread"
|
|
color="primary"
|
|
inline
|
|
class="mr-2"
|
|
/>
|
|
|
|
<!-- Chevron for folders with children -->
|
|
<v-btn
|
|
v-if="hasChildren(folder)"
|
|
icon="mdi-chevron-right"
|
|
variant="text"
|
|
size="small"
|
|
density="compact"
|
|
@click.stop="handleNavigateInto(group.service, folder.identifier)"
|
|
/>
|
|
|
|
<!-- Menu for folder actions -->
|
|
<v-menu>
|
|
<template v-slot:activator="{ props: menuProps }">
|
|
<v-btn
|
|
v-bind="menuProps"
|
|
icon="mdi-dots-vertical"
|
|
variant="text"
|
|
size="small"
|
|
density="compact"
|
|
@click.stop
|
|
/>
|
|
</template>
|
|
|
|
<v-list density="compact">
|
|
<v-list-item
|
|
prepend-icon="mdi-pencil"
|
|
@click="emit('editFolder', folder)"
|
|
>
|
|
<v-list-item-title>Edit Folder Name</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item
|
|
prepend-icon="mdi-folder-plus"
|
|
@click="emit('createFolder', group.service, folder)"
|
|
>
|
|
<v-list-item-title>New Subfolder</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item
|
|
prepend-icon="mdi-folder-move"
|
|
@click="emit('moveFolder', folder)"
|
|
>
|
|
<v-list-item-title>Move Folder</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item
|
|
v-if="canDeleteFolder(folder)"
|
|
prepend-icon="mdi-delete"
|
|
base-color="error"
|
|
@click="emit('deleteFolder', folder)"
|
|
>
|
|
<v-list-item-title>Delete Folder</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</template>
|
|
</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>
|
|
|
|
<!-- Single service - show folders directly -->
|
|
<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 -->
|
|
<v-list-subheader class="d-flex align-center">
|
|
<span class="flex-grow-1">
|
|
{{ getCurrentPageLevel(group.service).length > 1 ? getCurrentBreadcrumb(group.service) : 'FOLDERS' }}
|
|
</span>
|
|
<v-btn
|
|
icon="mdi-folder-plus"
|
|
variant="text"
|
|
size="x-small"
|
|
@click="emit('createFolder', group.service, getCurrentParentFolder(group.service))"
|
|
>
|
|
<v-icon size="small">mdi-folder-plus</v-icon>
|
|
<v-tooltip activator="parent" location="bottom">
|
|
{{ getCurrentPageLevel(group.service).length > 1 ? 'New Subfolder' : 'New Folder' }}
|
|
</v-tooltip>
|
|
</v-btn>
|
|
</v-list-subheader>
|
|
|
|
<!-- Back button if not at root -->
|
|
<v-list-item
|
|
v-if="getCurrentPageLevel(group.service).length > 1"
|
|
class="back-row-item"
|
|
@click="navigateBack(group.service)"
|
|
prepend-icon="mdi-arrow-left"
|
|
title="Back"
|
|
/>
|
|
|
|
<!-- Current level folders -->
|
|
<v-list-item
|
|
v-for="folder in getCurrentPageFolders(group.service)"
|
|
:key="`${folder.provider}-${folder.service}-${folder.identifier}`"
|
|
class="folder-row-item"
|
|
:title="folder.properties.label"
|
|
:active="isSelected(folder)"
|
|
@click="handleFolderClick(folder)"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon
|
|
:icon="getFolderIcon(folder)"
|
|
:color="getFolderColor(folder)"
|
|
/>
|
|
</template>
|
|
|
|
<template v-slot:append>
|
|
<!-- Unread badge -->
|
|
<v-badge
|
|
v-if="folder.properties.unread && folder.properties.unread > 0"
|
|
:content="folder.properties.unread"
|
|
color="primary"
|
|
inline
|
|
class="mr-2"
|
|
/>
|
|
|
|
<!-- Chevron for folders with children or Menu for actions -->
|
|
<v-btn
|
|
v-if="hasChildren(folder)"
|
|
icon="mdi-chevron-right"
|
|
variant="text"
|
|
size="small"
|
|
density="compact"
|
|
@click.stop="handleNavigateInto(group.service, folder.identifier)"
|
|
/>
|
|
|
|
<v-menu>
|
|
<template v-slot:activator="{ props: menuProps }">
|
|
<v-btn
|
|
v-bind="menuProps"
|
|
icon="mdi-dots-vertical"
|
|
variant="text"
|
|
size="small"
|
|
density="compact"
|
|
@click.stop
|
|
/>
|
|
</template>
|
|
|
|
<v-list density="compact">
|
|
<v-list-item
|
|
prepend-icon="mdi-pencil"
|
|
@click="emit('editFolder', folder)"
|
|
>
|
|
<v-list-item-title>Edit Folder Name</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item
|
|
prepend-icon="mdi-folder-plus"
|
|
@click="emit('createFolder', group.service, folder)"
|
|
>
|
|
<v-list-item-title>New Subfolder</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item
|
|
prepend-icon="mdi-folder-move"
|
|
@click="emit('moveFolder', folder)"
|
|
>
|
|
<v-list-item-title>Move Folder</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item
|
|
v-if="canDeleteFolder(folder)"
|
|
prepend-icon="mdi-delete"
|
|
base-color="error"
|
|
@click="emit('deleteFolder', folder)"
|
|
>
|
|
<v-list-item-title>Delete Folder</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</template>
|
|
</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>
|
|
</div>
|
|
</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>
|
|
.v-list-item--active {
|
|
background-color: rgba(var(--v-theme-primary), 0.12);
|
|
}
|
|
|
|
/* Keep folder rows left-aligned inside multi-account groups */
|
|
.folder-page-item {
|
|
--indent-padding: 0 !important;
|
|
}
|
|
|
|
.no-indent :deep(.v-list-group__items) {
|
|
--indent-padding: 0;
|
|
}
|
|
|
|
.account-group-spaced {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.account-header-item {
|
|
background-color: rgba(var(--v-theme-primary), 0.1);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.folder-row-item,
|
|
.back-row-item,
|
|
.account-header-item {
|
|
--v-list-item-prepend-size: 22px;
|
|
}
|
|
|
|
.folder-row-item :deep(.v-list-item__prepend),
|
|
.back-row-item :deep(.v-list-item__prepend),
|
|
.account-header-item :deep(.v-list-item__prepend) {
|
|
padding-inline-start: 4px;
|
|
margin-inline-end: 2px;
|
|
}
|
|
</style>
|