283
src/components/FolderSelectionDialog.vue
Normal file
283
src/components/FolderSelectionDialog.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<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.moveMessageCandidate
|
||||
|
||||
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.moveMessageCandidate],
|
||||
([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>
|
||||
Reference in New Issue
Block a user