Files
mail/src/components/FolderSelectionDialog.vue
2026-03-29 17:02:00 -04:00

284 lines
7.1 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useMailStore } from '@/stores/mailStore'
import type { ServiceObject, CollectionObject } from '@MailManager/models'
import FolderSelectionTreeNode from './FolderSelectionTreeNode.vue'
interface Props {
modelValue: boolean
loading?: boolean
title?: string
confirmText?: string
emptyText?: string
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
title: 'Move To',
confirmText: 'Move',
emptyText: 'No folders are available.',
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
select: [folder: CollectionObject]
cancel: []
}>()
const collectionsStore = useCollectionsStore()
const servicesStore = useServicesStore()
const mailStore = useMailStore()
const selectedFolderKey = ref<string | null>(null)
const dialogValue = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value),
})
const folderKeyFor = (folder: CollectionObject): string => {
return `${folder.provider}:${String(folder.service)}:${String(folder.identifier)}`
}
interface ServiceGroup {
service: ServiceObject
loading: boolean
loaded: boolean
error: string | null
}
const serviceGroups = computed<ServiceGroup[]>(() => {
const moveCandidate = mailStore.moveMessageCandidates[0]
if (!moveCandidate) {
return []
}
const service = servicesStore.services.find(entry =>
entry.provider === moveCandidate.provider &&
String(entry.identifier) === String(moveCandidate.service),
)
if (!service) {
return []
}
if (service.identifier === null) {
return []
}
return [{
service,
loading: mailStore.isServiceFolderLoading(service.provider, service.identifier),
loaded: mailStore.hasServiceFoldersLoaded(service.provider, service.identifier),
error: mailStore.getServiceFolderError(service.provider, service.identifier),
}]
})
const getRootFolders = (service: ServiceObject): CollectionObject[] => {
if (service.identifier === null) {
return []
}
return collectionsStore.collectionsInCollection(service.provider, service.identifier, null)
}
const getServiceFolders = (service: ServiceObject): CollectionObject[] => {
if (service.identifier === null) {
return []
}
return collectionsStore.collectionsForService(service.provider, service.identifier)
}
const selectedFolder = computed(() => {
if (!selectedFolderKey.value) {
return null
}
const group = serviceGroups.value[0]
if (!group) {
return null
}
return getServiceFolders(group.service).find(folder => folderKeyFor(folder) === selectedFolderKey.value) ?? null
})
const canConfirm = computed(() => {
return selectedFolder.value !== null && !props.loading
})
watch(
() => [props.modelValue, mailStore.moveMessageCandidates],
([isOpen]) => {
if (!isOpen) {
return
}
selectedFolderKey.value = null
},
)
const handleSelect = (folder: CollectionObject) => {
selectedFolderKey.value = folderKeyFor(folder)
}
const handleCancel = () => {
emit('cancel')
dialogValue.value = false
}
const handleConfirm = () => {
if (!selectedFolder.value) {
return
}
emit('select', selectedFolder.value)
}
</script>
<template>
<v-dialog
v-model="dialogValue"
max-width="680"
>
<v-card>
<v-card-title class="text-h6">
{{ title }}
</v-card-title>
<v-card-text>
<div class="folder-tree-card">
<v-list
density="compact"
nav
class="folder-tree-list"
>
<template
v-for="group in serviceGroups"
:key="`${group.service.provider}-${group.service.identifier}`"
>
<v-list-item
class="account-header-item account-header-static"
:title="group.service.label || 'Mail Account'"
:subtitle="group.service.primaryAddress || undefined"
>
<template #prepend>
<v-icon icon="mdi-email-outline" />
</template>
</v-list-item>
<FolderSelectionTreeNode
v-for="folder in getRootFolders(group.service)"
:key="folderKeyFor(folder)"
:folder="folder"
:service="group.service"
:selected-folder-key="selectedFolderKey"
@select="handleSelect"
/>
<v-list-item
v-if="group.loading && getServiceFolders(group.service).length === 0"
disabled
class="folder-status-item"
>
<template #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 #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"
:title="emptyText"
disabled
class="folder-status-item"
>
<template #prepend>
<v-icon icon="mdi-folder-off-outline" />
</template>
</v-list-item>
</template>
<v-list-item
v-if="serviceGroups.length === 0"
:title="emptyText"
disabled
>
<template #prepend>
<v-icon icon="mdi-folder-off-outline" />
</template>
</v-list-item>
</v-list>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
:disabled="loading"
@click="handleCancel"
>
Cancel
</v-btn>
<v-btn
color="primary"
variant="elevated"
:disabled="!canConfirm"
:loading="loading"
@click="handleConfirm"
>
{{ confirmText }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.folder-tree-card {
min-height: 280px;
}
.folder-tree-list {
max-height: 380px;
overflow-y: auto;
}
.account-header-item {
--v-list-item-prepend-size: 22px;
background-color: rgba(var(--v-theme-primary), 0.1);
border-radius: 6px;
}
.account-header-static {
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>