mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-23 06:38:40 +02:00
Improvements about mailbox listing and email display
This commit is contained in:
@@ -55,4 +55,12 @@ export default {
|
||||
}
|
||||
return repository.post('/webmail/emails/mark_as_not_junk/', body)
|
||||
},
|
||||
flagSelection(mailbox, selection, status) {
|
||||
const body = {
|
||||
mailbox,
|
||||
selection,
|
||||
status,
|
||||
}
|
||||
return repository.post('/webmail/emails/flag/', body)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<v-card class="mt-6 mb-2">
|
||||
<v-card class="mt-6 mb-2 mx-1">
|
||||
<v-toolbar color="white" flat>
|
||||
<v-checkbox class="mr-4" hide-details />
|
||||
<v-checkbox
|
||||
v-model="selectAll"
|
||||
class="mr-4"
|
||||
hide-details
|
||||
@update:model-value="toggleAllSelection"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
@@ -14,54 +19,138 @@
|
||||
density="compact"
|
||||
class="flex-grow-0 w-33 mr-4"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
icon="mdi-trash-can"
|
||||
size="small"
|
||||
:loading="working"
|
||||
@click="deleteSelection"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="$route.params.mailbox === 'Junk'"
|
||||
class="ml-2"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
icon="mdi-fire"
|
||||
size="small"
|
||||
:loading="working"
|
||||
@click="markSelectionAsJunk"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
class="ml-2"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
icon="mdi-thumb-up"
|
||||
size="small"
|
||||
:loading="working"
|
||||
@click="markSelectionAsNotJunk"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn class="ml-2" variant="tonal" icon size="small">
|
||||
<v-icon icon="mdi-cog" />
|
||||
<v-menu activator="parent">
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as read')"
|
||||
prepend-icon="mdi-eye"
|
||||
@click="() => flagSelection('read')"
|
||||
/>
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as unread')"
|
||||
prepend-icon="mdi-eye-outline"
|
||||
@click="() => flagSelection('unread')"
|
||||
/>
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as followed')"
|
||||
prepend-icon="mdi-star"
|
||||
@click="() => flagSelection('flagged')"
|
||||
/>
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as unfollowed')"
|
||||
prepend-icon="mdi-star-outline"
|
||||
@click="() => flagSelection('unflagged')"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<div v-if="emails.results" class="d-flex align-center">
|
||||
<div class="text-caption mr-2">
|
||||
{{ emails.first_index }}-{{ emails.last_index }} {{ $gettext('on') }}
|
||||
{{ emails.count }}
|
||||
</div>
|
||||
<div>
|
||||
<v-btn
|
||||
icon="mdi-chevron-left"
|
||||
size="x-small"
|
||||
:disabled="emails.prev_page === null"
|
||||
@click="page = emails.prev_page"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-chevron-right"
|
||||
size="x-small"
|
||||
:disabled="emails.next_page === null"
|
||||
@click="page = emails.next_page"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-toolbar>
|
||||
</v-card>
|
||||
<v-skeleton-loader v-if="loading" type="card@2"></v-skeleton-loader>
|
||||
<template v-else>
|
||||
<v-card
|
||||
v-for="email in emails"
|
||||
:key="email.imapid"
|
||||
density="compact"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-card-text
|
||||
class="d-flex align-center"
|
||||
:class="{ 'font-weight-bold': email.style === 'unseen' }"
|
||||
<div class="emails position-absolute top-0 bottom-0 w-100 overflow-y-auto">
|
||||
<v-card
|
||||
v-for="email in emails.results"
|
||||
:key="email.imapid"
|
||||
density="compact"
|
||||
class="mb-2 mx-1"
|
||||
>
|
||||
<v-checkbox :value="email.imapid" hide-details />
|
||||
<v-btn
|
||||
:icon="email.flagged ? 'mdiu-star' : 'mdi-star-outline'"
|
||||
variant="flat"
|
||||
/>
|
||||
<div class="ml-4 clickable" @click="openEmail(email.imapid)">
|
||||
<div>{{ email.subject }}</div>
|
||||
<div class="mt-1 text-grey">
|
||||
<span
|
||||
v-if="email.from_address.name"
|
||||
:title="email.from_address.address"
|
||||
>{{ email.from_address.name }}</span
|
||||
>
|
||||
<span v-else>{{ email.from_address.address }}</span>
|
||||
<v-card-text
|
||||
class="d-flex align-center"
|
||||
:class="{ 'font-weight-bold': email.style === 'unseen' }"
|
||||
>
|
||||
<v-checkbox v-model="selection" :value="email.imapid" hide-details />
|
||||
<v-btn
|
||||
:icon="email.flagged ? 'mdiu-star' : 'mdi-star-outline'"
|
||||
variant="flat"
|
||||
/>
|
||||
<div class="ml-4 clickable" @click="openEmail(email.imapid)">
|
||||
<div>{{ email.subject }}</div>
|
||||
<div class="mt-1 text-grey">
|
||||
<span
|
||||
v-if="email.from_address.name"
|
||||
:title="email.from_address.address"
|
||||
>{{ email.from_address.name }}</span
|
||||
>
|
||||
<span v-else>{{ email.from_address.address }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<div class="text-right">
|
||||
<div>{{ email.date }}</div>
|
||||
<div class="mt-1">
|
||||
<v-icon v-if="email.answered" icon="mdi-reply-outline" />
|
||||
<v-icon v-if="email.forwarded" icon="mdi-share-outline" />
|
||||
<v-icon v-if="email.attachments" icon="mdi-paperclip" />
|
||||
<span class="text-grey">{{ $filesize(email.size) }}</span>
|
||||
<v-spacer />
|
||||
<div class="text-right">
|
||||
<div>{{ email.date }}</div>
|
||||
<div class="mt-1">
|
||||
<v-icon v-if="email.answered" icon="mdi-reply-outline" />
|
||||
<v-icon v-if="email.forwarded" icon="mdi-share-outline" />
|
||||
<v-icon v-if="email.attachments" icon="mdi-paperclip" />
|
||||
<span class="text-grey">{{ $filesize(email.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore } from '@/stores'
|
||||
import api from '@/api/webmail'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -71,11 +160,18 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const { displayNotification } = useBusStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const emails = ref([])
|
||||
const emails = ref({})
|
||||
const page = ref(1)
|
||||
const search = ref('')
|
||||
const selectAll = ref(false)
|
||||
const selection = ref([])
|
||||
const working = ref(false)
|
||||
|
||||
const openEmail = (emailid) => {
|
||||
router.push({
|
||||
@@ -84,29 +180,103 @@ const openEmail = (emailid) => {
|
||||
})
|
||||
}
|
||||
|
||||
const fetchEmails = () => {
|
||||
emails.value = {}
|
||||
loading.value = true
|
||||
api
|
||||
.getMailboxEmails(props.mailbox, { page: page.value })
|
||||
.then((resp) => {
|
||||
emails.value = resp.data
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const toggleAllSelection = (value) => {
|
||||
if (!value) {
|
||||
selection.value = []
|
||||
} else {
|
||||
selection.value = emails.value.results.map((email) => email.imapid)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSelection = () => {
|
||||
if (!selection.value.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.deleteSelection(route.query.mailbox, selection.value).then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) deleted') })
|
||||
fetchEmails()
|
||||
})
|
||||
}
|
||||
|
||||
const markSelectionAsJunk = () => {
|
||||
if (!selection.value.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.markSelectionAsJunk(route.query.mailbox, selection.value).then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) marked as junk') })
|
||||
fetchEmails()
|
||||
})
|
||||
}
|
||||
|
||||
const markSelectionAsNotJunk = () => {
|
||||
if (!selection.value.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.markSelectionAsNotJunk(route.query.mailbox, selection.value).then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) marked as not junk') })
|
||||
fetchEmails()
|
||||
})
|
||||
}
|
||||
|
||||
const flagSelection = (status) => {
|
||||
if (!selection.value.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.flagSelection(route.query.mailbox, selection.value, status).then(() => {
|
||||
working.value = false
|
||||
selection.value = []
|
||||
displayNotification({ msg: $gettext('Message(s) flagged') })
|
||||
fetchEmails()
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.mailbox,
|
||||
(value) => {
|
||||
emails.value = []
|
||||
loading.value = true
|
||||
api
|
||||
.getMailboxEmails(value, {})
|
||||
.then((resp) => {
|
||||
emails.value = resp.data
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
() => {
|
||||
fetchEmails()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(selection, () => {
|
||||
if (!selection.value.length) {
|
||||
selectAll.value = false
|
||||
} else {
|
||||
selectAll.value = true
|
||||
}
|
||||
})
|
||||
watch(page, () => {
|
||||
fetchEmails()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-card-text {
|
||||
padding: 0;
|
||||
}
|
||||
.emails {
|
||||
margin-top: 150px;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,14 @@
|
||||
@mouseleave="setHover(mailbox, false)"
|
||||
>
|
||||
<v-icon :icon="iconByMailboxType[mailbox.type]" class="mr-4" />
|
||||
{{ getMailboxLabel(mailbox) }}
|
||||
<template v-if="mailbox.unseen > 0">
|
||||
<span class="font-weight-bold">
|
||||
{{ getMailboxLabel(mailbox) }} ({{ mailbox.unseen }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ getMailboxLabel(mailbox) }}
|
||||
</template>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="mailbox.sub"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-show="loaded" class="bg-white rounded-lg pa-4 mt-12">
|
||||
<div v-show="loaded" class="bg-white rounded-lg pa-4 position-relative h-100">
|
||||
<v-toolbar color="white">
|
||||
<v-btn icon="mdi-arrow-left" size="small" variant="flat" @click="close" />
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<div v-if="email" class="bg-white pa-4">
|
||||
<div v-if="email" ref="headers" class="bg-white pa-4">
|
||||
<h2>{{ email.subject }}</h2>
|
||||
<div class="d-flex mt-2">
|
||||
<h3 v-if="email.from_address.fulladdress">
|
||||
@@ -97,13 +97,13 @@
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
<iframe class="email-frame" />
|
||||
</div>
|
||||
<iframe class="email-frame" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore } from '@/stores'
|
||||
@@ -116,6 +116,7 @@ const router = useRouter()
|
||||
|
||||
const enableLinks = ref(false)
|
||||
const email = ref(null)
|
||||
const headers = ref(null)
|
||||
const loaded = ref(false)
|
||||
const working = ref(false)
|
||||
|
||||
@@ -129,9 +130,14 @@ const recipients = computed(() => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', resizeEmailIframe)
|
||||
fetchMailContent()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resizeEmailIframe)
|
||||
})
|
||||
|
||||
watch(enableLinks, () => {
|
||||
fetchMailContent()
|
||||
})
|
||||
@@ -143,6 +149,14 @@ const close = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const resizeEmailIframe = () => {
|
||||
const iframe = document.querySelector('iframe')
|
||||
const rect = headers.value.getBoundingClientRect()
|
||||
iframe.style.top = `${rect.bottom}px`
|
||||
iframe.style.width = `${rect.width - 24}px`
|
||||
iframe.style.height = `${window.innerHeight - rect.bottom - 32}px`
|
||||
}
|
||||
|
||||
const fetchMailContent = () => {
|
||||
const options = {
|
||||
dformat: 'html',
|
||||
@@ -160,6 +174,7 @@ const fetchMailContent = () => {
|
||||
iframeDoc.write(email.value.body)
|
||||
iframeDoc.close()
|
||||
loaded.value = true
|
||||
nextTick(resizeEmailIframe)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -220,10 +235,10 @@ const markEmailAsNotJunk = () => {
|
||||
|
||||
<style>
|
||||
.email-frame {
|
||||
width: 100%;
|
||||
min-height: 1000px;
|
||||
position: absolute !important;
|
||||
left: 24px;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
margin-top: 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div class="text-h5 ml-4">
|
||||
{{ $gettext('Webmail') }}
|
||||
<div class="position-relative h-100">
|
||||
<div class="text-h5 ml-4">
|
||||
{{ $gettext('Webmail') }}
|
||||
</div>
|
||||
<EmailList :mailbox="$route.query.mailbox || 'INBOX'" />
|
||||
</div>
|
||||
<EmailList :mailbox="$route.query.mailbox || 'INBOX'" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
Reference in New Issue
Block a user