mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Continuation of email scheduling feature
This commit is contained in:
@@ -128,4 +128,13 @@ export default {
|
||||
`/webmail/compose-sessions/${sessionUid}/attachments/${name}/`
|
||||
)
|
||||
},
|
||||
getScheduledMessage(messageId) {
|
||||
return repository.get(`/webmail/scheduled-messages/${messageId}/`)
|
||||
},
|
||||
updateScheduledMessage(messageId, data) {
|
||||
return repository.patch(`/webmail/scheduled-messages/${messageId}/`, data)
|
||||
},
|
||||
deleteScheduledMessage(messageId) {
|
||||
return repository.delete(`/webmail/scheduled-messages/${messageId}/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,16 +10,29 @@
|
||||
variant="flat"
|
||||
@click="close"
|
||||
/>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-send"
|
||||
:loading="working"
|
||||
:text="$gettext('Send')"
|
||||
@click="submit"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn-group color="primary" rounded="lg" density="compact" divided>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
prepend-icon="mdi-send"
|
||||
:loading="working"
|
||||
:text="$gettext('Send')"
|
||||
@click="submit"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn size="small" icon>
|
||||
<v-icon icon="mdi-chevron-down" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-send-clock-outline"
|
||||
:title="$gettext('Schedule sending')"
|
||||
@click="openSchedulingForm"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
variant="tonal"
|
||||
@@ -167,6 +180,12 @@
|
||||
@close="closeAttachmentDialog"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showSchedulingForm" max-width="800">
|
||||
<EmailSchedulingForm
|
||||
@schedule="scheduleAndSubmit"
|
||||
@close="closeSchedulingForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -178,6 +197,7 @@ import { useAuthStore, useBusStore } from '@/stores'
|
||||
import debounce from 'debounce'
|
||||
import AttachmentsDialog from '@/components/webmail/AttachmentsDialog'
|
||||
import BodyEditor from '@/components/webmail/BodyEditor'
|
||||
import EmailSchedulingForm from '@/components/webmail/EmailSchedulingForm'
|
||||
import rules from '@/plugins/rules'
|
||||
import api from '@/api/webmail'
|
||||
import contactsApi from '@/api/contacts'
|
||||
@@ -209,6 +229,7 @@ const formRef = ref()
|
||||
const showAttachmentsDialog = ref(false)
|
||||
const showCcField = ref(false)
|
||||
const showBccField = ref(false)
|
||||
const showSchedulingForm = ref(false)
|
||||
const working = ref(false)
|
||||
|
||||
const close = () => {
|
||||
@@ -275,7 +296,10 @@ const submit = async () => {
|
||||
try {
|
||||
await api.sendEmailFromComposeSession(route.query.uid, body)
|
||||
router.push({ name: 'MailboxView' })
|
||||
displayNotification({ msg: $gettext('Email sent') })
|
||||
const msg = body.scheduled_datetime
|
||||
? $gettext('Email scheduled')
|
||||
: $gettext('Email seng')
|
||||
displayNotification({ msg })
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
@@ -283,6 +307,11 @@ const submit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleAndSubmit = async (datetime) => {
|
||||
form.value.scheduled_datetime = datetime
|
||||
await submit()
|
||||
}
|
||||
|
||||
const openAttachmentsDialog = () => {
|
||||
showAttachmentsDialog.value = true
|
||||
}
|
||||
@@ -316,6 +345,21 @@ const initialize = (body) => {
|
||||
editorMode.value = body.editor_format
|
||||
}
|
||||
|
||||
const openSchedulingForm = () => {
|
||||
if (!form.value.to?.length) {
|
||||
displayNotification({
|
||||
msg: $gettext('You must provide one recipient at least'),
|
||||
type: 'info',
|
||||
})
|
||||
return
|
||||
}
|
||||
showSchedulingForm.value = true
|
||||
}
|
||||
|
||||
const closeSchedulingForm = () => {
|
||||
showSchedulingForm.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.originalEmail,
|
||||
() => {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<template v-for="(address, index) in addressesSample" :key="address">
|
||||
<span v-if="address.name" :title="address.address">{{ address.name }}</span>
|
||||
<span v-else>{{ address.address }}</span>
|
||||
<span v-if="index < addressesSample.length - 1">, </span>
|
||||
<span v-else-if="addressesSample.length < props.addresses.length"
|
||||
>, ...</span
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
addresses: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const addressesSample = computed(() =>
|
||||
props.addresses.length > 2 ? props.addresses.slice(0, 2) : props.addresses
|
||||
)
|
||||
</script>
|
||||
@@ -24,6 +24,7 @@
|
||||
@keyup.enter="submitSearch"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
v-if="!inScheduledView"
|
||||
class="ml-2"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@@ -33,28 +34,30 @@
|
||||
@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>
|
||||
<template v-if="!inScheduledView">
|
||||
<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>
|
||||
</template>
|
||||
<v-btn class="ml-2" variant="tonal" icon size="small">
|
||||
<v-icon icon="mdi-cog" />
|
||||
<v-menu activator="parent">
|
||||
@@ -80,7 +83,7 @@
|
||||
@click="() => flagSelection('unflagged')"
|
||||
/>
|
||||
<v-list-item
|
||||
v-if="$route.params.mailbox !== 'Trash'"
|
||||
v-if="$route.query.mailbox === 'Trash'"
|
||||
:title="$gettext('Empty mailbox')"
|
||||
prepend-icon="mdi-trash-can"
|
||||
@click="emptyMailbox"
|
||||
@@ -113,7 +116,23 @@
|
||||
</v-card>
|
||||
<v-skeleton-loader v-if="loading" type="card@2"></v-skeleton-loader>
|
||||
<template v-else>
|
||||
<div class="emails position-absolute top-0 bottom-0 w-100 overflow-y-auto">
|
||||
<v-alert
|
||||
v-if="inScheduledView"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mx-1"
|
||||
>
|
||||
{{
|
||||
$gettext(
|
||||
'Scheduled messages will be sent at the specified date and time. (visible on the right)'
|
||||
)
|
||||
}}
|
||||
</v-alert>
|
||||
<div
|
||||
class="emails position-absolute bottom-0 w-100 overflow-y-auto"
|
||||
:class="{ 'top-0': !inScheduledView, 'scheduling-top': inScheduledView }"
|
||||
>
|
||||
<template v-if="emails.results?.length">
|
||||
<v-card
|
||||
v-for="email in emails.results"
|
||||
@@ -138,20 +157,38 @@
|
||||
variant="flat"
|
||||
@click="toggleFollowState(email)"
|
||||
/>
|
||||
<v-menu v-if="inScheduledView" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
v-bind="props"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
<MenuItems :items="getScheduledMessageActions()" :obj="email" />
|
||||
</v-menu>
|
||||
|
||||
<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>
|
||||
<EmailAddressList :addresses="getEmailAddresses(email)" />
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<div class="text-right">
|
||||
<div>{{ email.date }}</div>
|
||||
<div v-if="!isScheduledDateOver(email)">
|
||||
{{ getEmailDate(email) }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-error font-weight-bold"
|
||||
style="cursor: pointer"
|
||||
@click="displaySchedulingError(email)"
|
||||
>
|
||||
{{ getEmailDate(email) }}
|
||||
</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" />
|
||||
@@ -170,6 +207,28 @@
|
||||
variant="tonal"
|
||||
/>
|
||||
</div>
|
||||
<v-dialog v-model="showSchedulingForm" max-width="800">
|
||||
<EmailSchedulingForm
|
||||
:initial-date="selectedScheduledEmail.scheduled_datetime_raw"
|
||||
@schedule="updateScheduledEmail"
|
||||
@close="closeSchedulingForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showSchedulingError" max-width="400">
|
||||
<v-card
|
||||
max-width="400"
|
||||
:text="schedulingError"
|
||||
:title="$gettext('Sending failure')"
|
||||
>
|
||||
<template #:actions>
|
||||
<v-btn
|
||||
class="ms-auto"
|
||||
:text="$gettext('Close')"
|
||||
@click="showSchedulingError = false"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -178,6 +237,10 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore, useWebmailStore } from '@/stores'
|
||||
import { DateTime } from 'luxon'
|
||||
import EmailAddressList from './EmailAddressList.vue'
|
||||
import EmailSchedulingForm from './EmailSchedulingForm.vue'
|
||||
import MenuItems from '@/components/tools/MenuItems.vue'
|
||||
import api from '@/api/webmail'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -193,11 +256,15 @@ const webmailStore = useWebmailStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const selectedScheduledEmail = ref(null)
|
||||
const loading = ref(false)
|
||||
const emails = ref({})
|
||||
const page = ref(1)
|
||||
const schedulingError = ref('')
|
||||
const search = ref('')
|
||||
const selectAll = ref(false)
|
||||
const showSchedulingError = ref(false)
|
||||
const showSchedulingForm = ref(false)
|
||||
const working = ref(false)
|
||||
|
||||
let intervalId = null
|
||||
@@ -206,6 +273,79 @@ const currentMailbox = computed(() => {
|
||||
return route.query.mailbox || 'INBOX'
|
||||
})
|
||||
|
||||
const inScheduledView = computed(() => props.mailbox === 'Scheduled')
|
||||
|
||||
const getScheduledMessageActions = () => {
|
||||
return [
|
||||
{
|
||||
label: $gettext('Reschedule'),
|
||||
icon: 'mdi-send-clock-outline',
|
||||
onClick: reScheduleMessage,
|
||||
},
|
||||
{
|
||||
label: $gettext('Delete'),
|
||||
icon: 'mdi-delete-outline',
|
||||
onClick: deleteScheduledMessage,
|
||||
color: 'red',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getEmailAddresses = (email) => {
|
||||
if (inScheduledView.value) {
|
||||
return email.recipients
|
||||
}
|
||||
return [email.from_address]
|
||||
}
|
||||
|
||||
const getEmailDate = (email) => {
|
||||
if (inScheduledView.value) {
|
||||
return email.scheduled_datetime
|
||||
}
|
||||
return email.date
|
||||
}
|
||||
|
||||
const isScheduledDateOver = (email) => {
|
||||
if (!inScheduledView.value || !email.scheduled_datetime_raw) return false
|
||||
const date = DateTime.fromISO(email.scheduled_datetime_raw)
|
||||
return date < DateTime.now()
|
||||
}
|
||||
|
||||
const reScheduleMessage = (email) => {
|
||||
selectedScheduledEmail.value = email
|
||||
showSchedulingForm.value = true
|
||||
}
|
||||
|
||||
const updateScheduledEmail = async (datetime) => {
|
||||
const data = {
|
||||
scheduled_datetime: datetime,
|
||||
}
|
||||
await api.updateScheduledMessage(
|
||||
selectedScheduledEmail.value.scheduled_id,
|
||||
data
|
||||
)
|
||||
fetchEmails()
|
||||
displayNotification({ msg: $gettext('Scheduling updated') })
|
||||
}
|
||||
|
||||
const closeSchedulingForm = () => {
|
||||
showSchedulingForm.value = false
|
||||
}
|
||||
|
||||
const deleteScheduledMessage = async (email) => {
|
||||
await api.deleteScheduledMessage(email.scheduled_id)
|
||||
fetchEmails()
|
||||
displayNotification({
|
||||
msg: $gettext('Scheduled canceled and message move to trash folder'),
|
||||
})
|
||||
}
|
||||
|
||||
const displaySchedulingError = async (email) => {
|
||||
const resp = await api.getScheduledMessage(email.scheduled_id)
|
||||
schedulingError.value = resp.data.error
|
||||
showSchedulingError.value = true
|
||||
}
|
||||
|
||||
const openEmail = (emailid) => {
|
||||
router.push({
|
||||
name: 'EmailView',
|
||||
@@ -380,4 +520,7 @@ watch(page, () => {
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.scheduling-top {
|
||||
top: 45px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<v-card :title="$gettext('Schedule your sending')">
|
||||
<v-card-text>
|
||||
<v-form ref="formRef">
|
||||
<div class="d-flex">
|
||||
<v-date-picker
|
||||
v-model="date"
|
||||
class="w-50"
|
||||
:allowed-dates="allowedDates"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<div
|
||||
class="w-33 pt-2 text-uppercase text-overline font-weight-regular"
|
||||
>
|
||||
<label>{{ $gettext('Select time') }}</label>
|
||||
<v-text-field
|
||||
v-model="time"
|
||||
variant="outlined"
|
||||
class="mt-6"
|
||||
:rules="[rules.required, rules.time]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn :text="$gettext('Close')" :loading="loading" @click="close" />
|
||||
<v-btn
|
||||
:text="$gettext('Schedule')"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="submit"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { DateTime } from 'luxon'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore } from '@/stores'
|
||||
import rules from '@/plugins/rules.js'
|
||||
|
||||
const props = defineProps({
|
||||
initialDate: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['close', 'schedule'])
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const { displayNotification } = useBusStore()
|
||||
|
||||
const close = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const allowedDates = (value) => {
|
||||
const now = new Date()
|
||||
return value > now
|
||||
}
|
||||
|
||||
const validateDatetime = (value) => {
|
||||
const now = new Date()
|
||||
const msg = $gettext('Provided time must be one minute from now at least')
|
||||
if (value <= now) {
|
||||
displayNotification({ msg, type: 'error' })
|
||||
return false
|
||||
}
|
||||
const delta = value - now
|
||||
if (delta / 1000 < 60) {
|
||||
displayNotification({ msg, type: 'error' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const date = ref('')
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const time = ref('')
|
||||
|
||||
const now = DateTime.now()
|
||||
date.value = now.toJSDate()
|
||||
time.value = now.toFormat('HH:mm')
|
||||
|
||||
const submit = async () => {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
const datetime = date.value
|
||||
const parts = time.value.split(':')
|
||||
datetime.setHours(parseInt(parts[0]), parseInt(parts[1]))
|
||||
if (!validateDatetime(datetime)) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
emit('schedule', datetime.toISOString())
|
||||
emit('close')
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.initialDate,
|
||||
(value) => {
|
||||
if (value) {
|
||||
const initialDate = DateTime.fromISO(value)
|
||||
date.value = initialDate.toJSDate()
|
||||
time.value = initialDate.toFormat('HH:mm')
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -106,6 +106,7 @@ const iconByMailboxType = {
|
||||
draft: 'mdi-file',
|
||||
junk: 'mdi-fire',
|
||||
sent: 'mdi-email-fast-outline',
|
||||
scheduled: 'mdi-send-clock-outline',
|
||||
trash: 'mdi-trash-can-outline',
|
||||
normal: 'mdi-folder-outline',
|
||||
}
|
||||
|
||||
@@ -22,6 +22,13 @@ function samePassword(value, confirmation) {
|
||||
return value === confirmation
|
||||
}
|
||||
|
||||
function validateTime(value) {
|
||||
if (!/^([01]\d|2[0-3]):?([0-5]\d)$/.test(value)) {
|
||||
return $gettext('Not a valid time')
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
required: (value) =>
|
||||
(value !== '' &&
|
||||
@@ -43,4 +50,5 @@ export default {
|
||||
samePassword(value, confirmation) || $gettext('Passwords mismatch'),
|
||||
portNumber: (value) =>
|
||||
(value > 0 && value < 65536) || $gettext('Invalid port number'),
|
||||
time: (value) => validateTime(value),
|
||||
}
|
||||
|
||||
@@ -3,63 +3,57 @@
|
||||
<v-toolbar color="white">
|
||||
<v-btn icon="mdi-arrow-left" size="small" variant="flat" @click="close" />
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-reply"
|
||||
@click="() => replyToEmail()"
|
||||
>
|
||||
{{ $gettext('Reply') }}
|
||||
<template #append>
|
||||
<v-btn size="x-small" variant="plain">
|
||||
<v-icon icon="mdi-chevron-down" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
:title="$gettext('Reply all')"
|
||||
@click="() => replyToEmail(true)"
|
||||
/>
|
||||
<v-list-item
|
||||
:title="$gettext('Forward')"
|
||||
@click="forwardEmail"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
icon="mdi-trash-can"
|
||||
size="small"
|
||||
:loading="working"
|
||||
@click="deleteEmail"
|
||||
>
|
||||
</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="markEmailAsJunk"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
class="ml-2"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
icon="mdi-thumb-up"
|
||||
size="small"
|
||||
:loading="working"
|
||||
@click="markEmailAsNotJunk"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn-group color="primary" rounded="lg" density="compact" divided>
|
||||
<v-btn prepend-icon="mdi-reply" @click="() => replyToEmail()">
|
||||
{{ $gettext('Reply') }}
|
||||
</v-btn>
|
||||
<v-btn size="small" icon>
|
||||
<v-icon icon="mdi-chevron-down" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
:title="$gettext('Reply all')"
|
||||
@click="() => replyToEmail(true)"
|
||||
/>
|
||||
<v-list-item :title="$gettext('Forward')" @click="forwardEmail" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
<template v-if="$route.query.mailbox !== 'Scheduled'">
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
icon="mdi-trash-can"
|
||||
size="small"
|
||||
:loading="working"
|
||||
@click="deleteEmail"
|
||||
>
|
||||
</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="markEmailAsJunk"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
class="ml-2"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
icon="mdi-thumb-up"
|
||||
size="small"
|
||||
:loading="working"
|
||||
@click="markEmailAsNotJunk"
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-btn class="ml-2" variant="tonal" icon size="small">
|
||||
<v-icon icon="mdi-cog" />
|
||||
<v-menu activator="parent">
|
||||
@@ -140,6 +134,17 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<v-alert
|
||||
v-if="email?.scheduled_datetime"
|
||||
ref="schedulingInfo"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mx-1"
|
||||
>
|
||||
{{ $gettext('Message scheduled at:') }}
|
||||
{{ $date(email.scheduled_datetime) }}
|
||||
</v-alert>
|
||||
<iframe class="email-frame" />
|
||||
</div>
|
||||
<v-dialog v-model="showEmailSource" max-width="1200">
|
||||
@@ -176,6 +181,7 @@ const email = ref(null)
|
||||
const emailSource = ref(null)
|
||||
const headers = ref(null)
|
||||
const loaded = ref(false)
|
||||
const schedulingInfo = ref()
|
||||
const showEmailSource = ref(false)
|
||||
const working = ref(false)
|
||||
|
||||
@@ -201,7 +207,12 @@ const close = () => {
|
||||
|
||||
const resizeEmailIframe = () => {
|
||||
const iframe = document.querySelector('iframe')
|
||||
const rect = headers.value.getBoundingClientRect()
|
||||
let rect
|
||||
if (schedulingInfo.value) {
|
||||
rect = schedulingInfo.value.$el.getBoundingClientRect()
|
||||
} else {
|
||||
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`
|
||||
@@ -221,9 +232,11 @@ const fetchMailContent = () => {
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.classList.add('email-frame')
|
||||
document.querySelector('iframe').replaceWith(iframe)
|
||||
const iframeDoc = iframe.contentDocument
|
||||
iframeDoc.write(email.value.body)
|
||||
iframeDoc.close()
|
||||
if (email.value.body) {
|
||||
const iframeDoc = iframe.contentDocument
|
||||
iframeDoc.write(email.value.body)
|
||||
iframeDoc.close()
|
||||
}
|
||||
loaded.value = true
|
||||
nextTick(resizeEmailIframe)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user