mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Added possibility to Drag&Drop message selection between folders.
fix #3775
This commit is contained in:
@@ -65,23 +65,31 @@ export default {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
deleteSelection(mailbox, selection) {
|
||||
moveSelection(source, destination, selection) {
|
||||
const body = {
|
||||
mailbox,
|
||||
source,
|
||||
destination,
|
||||
selection,
|
||||
}
|
||||
return repository.post('/webmail/emails/move/', body)
|
||||
},
|
||||
deleteSelection(source, selection) {
|
||||
const body = {
|
||||
source,
|
||||
selection,
|
||||
}
|
||||
return repository.post('/webmail/emails/delete/', body)
|
||||
},
|
||||
markSelectionAsJunk(mailbox, selection) {
|
||||
markSelectionAsJunk(source, selection) {
|
||||
const body = {
|
||||
mailbox,
|
||||
source,
|
||||
selection,
|
||||
}
|
||||
return repository.post('/webmail/emails/mark_as_junk/', body)
|
||||
},
|
||||
markSelectionAsNotJunk(mailbox, selection) {
|
||||
markSelectionAsNotJunk(source, selection) {
|
||||
const body = {
|
||||
mailbox,
|
||||
source,
|
||||
selection,
|
||||
}
|
||||
return repository.post('/webmail/emails/mark_as_not_junk/', body)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
v-model="selectAll"
|
||||
class="mr-4"
|
||||
hide-details
|
||||
color="primary"
|
||||
@update:model-value="toggleAllSelection"
|
||||
/>
|
||||
|
||||
@@ -88,7 +89,7 @@
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<div v-if="emails.results" class="d-flex align-center">
|
||||
<div v-if="emails" class="d-flex align-center">
|
||||
<div class="text-caption mr-2">
|
||||
{{ emails.first_index }}-{{ emails.last_index }} {{ $gettext('on') }}
|
||||
{{ emails.count }}
|
||||
@@ -119,14 +120,17 @@
|
||||
:key="email.imapid"
|
||||
density="compact"
|
||||
class="mb-2 mx-1"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(email)"
|
||||
>
|
||||
<v-card-text
|
||||
class="d-flex align-center"
|
||||
:class="{ 'font-weight-bold': email.style === 'unseen' }"
|
||||
>
|
||||
<v-checkbox
|
||||
v-model="selection"
|
||||
v-model="webmailStore.selection"
|
||||
:value="email.imapid"
|
||||
color="primary"
|
||||
hide-details
|
||||
/>
|
||||
<v-btn
|
||||
@@ -170,10 +174,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore } from '@/stores'
|
||||
import { useBusStore, useWebmailStore } from '@/stores'
|
||||
import api from '@/api/webmail'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -183,18 +187,20 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const { $gettext, $ngettext } = useGettext()
|
||||
const { displayNotification, reloadMailboxCounters } = useBusStore()
|
||||
const webmailStore = useWebmailStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const emails = ref({})
|
||||
const emails = ref([])
|
||||
const emailsPage = ref({})
|
||||
const page = ref(1)
|
||||
const search = ref('')
|
||||
const selectAll = ref(false)
|
||||
const selection = ref([])
|
||||
const working = ref(false)
|
||||
const emailList = ref(null)
|
||||
|
||||
let intervalId = null
|
||||
|
||||
@@ -233,18 +239,18 @@ const submitSearch = () => {
|
||||
|
||||
const toggleAllSelection = (value) => {
|
||||
if (!value) {
|
||||
selection.value = []
|
||||
webmailStore.selection = []
|
||||
} else {
|
||||
selection.value = emails.value.results.map((email) => email.imapid)
|
||||
webmailStore.selection = emails.value.results.map((email) => email.imapid)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSelection = () => {
|
||||
if (!selection.value.length) {
|
||||
if (!webmailStore.selection.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.deleteSelection(currentMailbox.value, selection.value).then(() => {
|
||||
api.deleteSelection(currentMailbox.value, webmailStore.selection).then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) deleted') })
|
||||
fetchEmails()
|
||||
@@ -253,41 +259,47 @@ const deleteSelection = () => {
|
||||
}
|
||||
|
||||
const markSelectionAsJunk = () => {
|
||||
if (!selection.value.length) {
|
||||
if (!webmailStore.selection.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.markSelectionAsJunk(currentMailbox.value, selection.value).then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) marked as junk') })
|
||||
autoRefreshContent()
|
||||
})
|
||||
api
|
||||
.markSelectionAsJunk(currentMailbox.value, webmailStore.selection)
|
||||
.then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) marked as junk') })
|
||||
autoRefreshContent()
|
||||
})
|
||||
}
|
||||
|
||||
const markSelectionAsNotJunk = () => {
|
||||
if (!selection.value.length) {
|
||||
if (!webmailStore.selection.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.markSelectionAsNotJunk(currentMailbox.value, selection.value).then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) marked as not junk') })
|
||||
autoRefreshContent()
|
||||
})
|
||||
api
|
||||
.markSelectionAsNotJunk(currentMailbox.value, webmailStore.selection)
|
||||
.then(() => {
|
||||
working.value = false
|
||||
displayNotification({ msg: $gettext('Message(s) marked as not junk') })
|
||||
autoRefreshContent()
|
||||
})
|
||||
}
|
||||
|
||||
const flagSelection = (status) => {
|
||||
if (!selection.value.length) {
|
||||
if (!webmailStore.selection.length) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
api.flagSelection(currentMailbox.value, selection.value, status).then(() => {
|
||||
working.value = false
|
||||
selection.value = []
|
||||
displayNotification({ msg: $gettext('Message(s) flagged') })
|
||||
fetchEmails()
|
||||
reloadMailboxCounters()
|
||||
})
|
||||
api
|
||||
.flagSelection(currentMailbox.value, webmailStore.selection, status)
|
||||
.then(() => {
|
||||
working.value = false
|
||||
webmailStore.selection = []
|
||||
displayNotification({ msg: $gettext('Message(s) flagged') })
|
||||
fetchEmails()
|
||||
reloadMailboxCounters()
|
||||
})
|
||||
}
|
||||
|
||||
const emptyMailbox = () => {
|
||||
@@ -305,6 +317,25 @@ const toggleFollowState = async (email) => {
|
||||
email.flagged = flag === 'flagged'
|
||||
}
|
||||
|
||||
const onDragStart = (email) => {
|
||||
if (!webmailStore.selection.includes(email.imapid)) {
|
||||
webmailStore.selection = [email.imapid]
|
||||
}
|
||||
const ghost = document.createElement('div')
|
||||
const count = webmailStore.selection.length || 0
|
||||
const msg = $ngettext('%{count} message', '%{count} messages', count, {
|
||||
count,
|
||||
})
|
||||
ghost.innerHTML = `
|
||||
<div class="pa-4 rounded-lg" style="background: #a9a9a9; opacity: 0.8; width: 300px">
|
||||
<div style="font-weight: 600">${msg}</div>
|
||||
</div>
|
||||
`
|
||||
document.body.appendChild(ghost)
|
||||
event.dataTransfer.setDragImage(ghost, -10, -10)
|
||||
setTimeout(() => ghost.remove(), 0)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
intervalId = setInterval(autoRefreshContent, 300 * 1000)
|
||||
})
|
||||
@@ -320,13 +351,22 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(selection, () => {
|
||||
if (!selection.value.length) {
|
||||
selectAll.value = false
|
||||
} else {
|
||||
selectAll.value = true
|
||||
watch(
|
||||
() => webmailStore.selection,
|
||||
() => {
|
||||
if (!webmailStore.selection.length) {
|
||||
selectAll.value = false
|
||||
} else {
|
||||
selectAll.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
watch(
|
||||
() => webmailStore.listingKey,
|
||||
() => {
|
||||
fetchEmails()
|
||||
}
|
||||
)
|
||||
watch(page, () => {
|
||||
fetchEmails()
|
||||
})
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
@click="updateSelection(mailbox.name)"
|
||||
@mouseover="setHover(mailbox, true)"
|
||||
@mouseleave="setHover(mailbox, false)"
|
||||
@dragover.prevent
|
||||
@dragenter="setHover(mailbox, true)"
|
||||
@dragleave="setHover(mailbox, false)"
|
||||
@drop="onDrop(mailbox)"
|
||||
>
|
||||
<v-icon :icon="iconByMailboxType[mailbox.type]" class="mr-4" />
|
||||
<template v-if="mailbox.name === route.query.mailbox">
|
||||
@@ -57,7 +61,8 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useBusStore } from '@/stores'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore, useWebmailStore } from '@/stores'
|
||||
import api from '@/api/webmail'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -86,7 +91,9 @@ const model = defineModel()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const route = useRoute()
|
||||
const { $gettext } = useGettext()
|
||||
const busStore = useBusStore()
|
||||
const webmailStore = useWebmailStore()
|
||||
|
||||
const iconByMailboxType = {
|
||||
inbox: 'mdi-inbox',
|
||||
@@ -152,6 +159,25 @@ function updateSelection(value) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onDrop(mailbox) {
|
||||
try {
|
||||
busStore.displayNotification({
|
||||
msg: $gettext('Moving selection...'),
|
||||
type: 'info',
|
||||
timeout: 0,
|
||||
})
|
||||
const resp = await api.moveSelection(
|
||||
route.query.mailbox,
|
||||
mailbox.name,
|
||||
webmailStore.selection
|
||||
)
|
||||
webmailStore.listingKey++
|
||||
webmailStore.selection = []
|
||||
} finally {
|
||||
busStore.hideNotification()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => busStore.mbCounterKey,
|
||||
() => {
|
||||
@@ -169,4 +195,8 @@ watch(
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailbox * {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -54,17 +54,21 @@ const props = defineProps({
|
||||
|
||||
const authUser = computed(() => authStore.authUser)
|
||||
const notificationColor = computed(() => busStore.notificationColor)
|
||||
const notificationTimeout = computed(() => busStore.notificationTimeout)
|
||||
const notification = computed(() => busStore.notification)
|
||||
const snackbar = ref(false)
|
||||
|
||||
const notificationTimeout = 2000
|
||||
|
||||
busStore.$onAction(({ name, after }) => {
|
||||
if (name === 'displayNotification') {
|
||||
after(() => {
|
||||
snackbar.value = true
|
||||
})
|
||||
}
|
||||
if (name === 'hideNotification') {
|
||||
after(() => {
|
||||
snackbar.value = false
|
||||
})
|
||||
}
|
||||
}, true)
|
||||
|
||||
if (props.loadTheme) {
|
||||
|
||||
@@ -3,14 +3,16 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useBusStore = defineStore('bus', () => {
|
||||
const notification = ref({})
|
||||
const notification = ref('')
|
||||
const notificationColor = ref('')
|
||||
const notificationTimeout = ref(2000)
|
||||
const dataKey = ref(1)
|
||||
const mbCounterKey = ref(1)
|
||||
|
||||
async function $reset() {
|
||||
notification.value = {}
|
||||
notification.value = ''
|
||||
notificationColor.value = ''
|
||||
notificationTimeout.value = 2000
|
||||
dataKey.value = 1
|
||||
mbCounterKey.value = 1
|
||||
}
|
||||
@@ -18,6 +20,13 @@ export const useBusStore = defineStore('bus', () => {
|
||||
function displayNotification(options) {
|
||||
notification.value = options.msg
|
||||
notificationColor.value = options.type ? options.type : 'success'
|
||||
if (options.timeout !== undefined) {
|
||||
notificationTimeout.value = options.timeout
|
||||
}
|
||||
}
|
||||
|
||||
function hideNotification() {
|
||||
notification.value = ''
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
@@ -32,6 +41,7 @@ export const useBusStore = defineStore('bus', () => {
|
||||
notification,
|
||||
notificationColor,
|
||||
displayNotification,
|
||||
hideNotification,
|
||||
reloadMailboxCounters,
|
||||
dataKey,
|
||||
mbCounterKey,
|
||||
|
||||
@@ -9,5 +9,6 @@ export * from './domains.store'
|
||||
export * from './parameters.store'
|
||||
export * from './providers.store'
|
||||
export * from './contacts.store'
|
||||
export * from './webmail.store'
|
||||
|
||||
export default createPinia()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useWebmailStore = defineStore('webmail', () => {
|
||||
const selection = ref([])
|
||||
const listingKey = ref(0)
|
||||
|
||||
return {
|
||||
selection,
|
||||
listingKey,
|
||||
}
|
||||
})
|
||||
@@ -245,6 +245,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||
|
||||
"@formkit/drag-and-drop@^0.5.3":
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@formkit/drag-and-drop/-/drag-and-drop-0.5.3.tgz#24408c0cf8749b26d3fba3b043503efba44b4c90"
|
||||
integrity sha512-VGQsKYc350OOMbz7wYt69YqDbn49x7RJ32eqiJieNke54nGZYmx18XEqTw0lBiJmH3RaiH6V45gB1u+tvI/TNw==
|
||||
|
||||
"@fullcalendar/core@^6.1.15":
|
||||
version "6.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.19.tgz#60379111ba0cfb9fd9bc6594fee47cf855fc77b2"
|
||||
|
||||
Reference in New Issue
Block a user