summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHamza <40746210+hamza221@users.noreply.github.com>2024-04-08 20:36:23 +0200
committerGitHub <noreply@github.com>2024-04-08 20:36:23 +0200
commite1badcf48577f1246f61500d926ecf6ca04c101e (patch)
tree8fce6948306e2ff3f6cad6af04783ea5545b4fbe
parent5b94d9c6f255edcacb86ff2fd9e59138e61ae355 (diff)
parent321764990066edcf60691e31d3b51568b027034e (diff)
Merge pull request #2466 from SteKoe/feature/drag-drop
Implement Drag and Drop feature for adding contacts to group
-rw-r--r--src/components/AppNavigation/GroupNavigationItem.vue141
-rw-r--r--src/components/ContactsList/ContactsListItem.vue82
-rw-r--r--src/store/index.js2
3 files changed, 163 insertions, 62 deletions
diff --git a/src/components/AppNavigation/GroupNavigationItem.vue b/src/components/AppNavigation/GroupNavigationItem.vue
index 273395ad..4ced3543 100644
--- a/src/components/AppNavigation/GroupNavigationItem.vue
+++ b/src/components/AppNavigation/GroupNavigationItem.vue
@@ -20,47 +20,54 @@
-
-->
<template>
- <AppNavigationItem :key="group.key"
- :to="group.router"
- :name="group.name">
- <template #icon>
- <IconContact :size="20" />
- </template>
- <template #actions>
- <ActionButton :close-after-click="true"
- @click="addContactsToGroup(group)">
- <template #icon>
- <IconAdd :size="20" />
- </template>
- {{ t('contacts', 'Add contacts') }}
- </ActionButton>
- <ActionButton :close-after-click="true"
- @click="downloadGroup(group)">
- <template #icon>
- <IconDownload :size="20" />
- </template>
- {{ t('contacts', 'Export') }}
- </ActionButton>
- <ActionButton @click="emailGroup(group)">
- <template #icon>
- <IconEmail :size="20" />
- </template>
- {{ t('contacts', 'Send email') }}
- </ActionButton>
- <ActionButton @click="emailGroup(group, 'bcc')">
- <template #icon>
- <IconEmail :size="20" />
- </template>
- {{ t('contacts', 'Send email as BCC') }}
- </ActionButton>
- </template>
+ <div class="group-drop-area"
+ data-testid="group-drop-area"
+ @drop="onDrop($event, group)"
+ @dragenter.prevent
+ @dragover="onDragOver($event)"
+ @dragleave="onDragLeave($event)">
+ <AppNavigationItem :key="group.key"
+ :to="group.router"
+ :name="group.name">
+ <template #icon>
+ <IconContact :size="20" />
+ </template>
+ <template #actions>
+ <ActionButton :close-after-click="true"
+ @click="addContactsToGroup(group)">
+ <template #icon>
+ <IconAdd :size="20" />
+ </template>
+ {{ t('contacts', 'Add contacts') }}
+ </ActionButton>
+ <ActionButton :close-after-click="true"
+ @click="downloadGroup(group)">
+ <template #icon>
+ <IconDownload :size="20" />
+ </template>
+ {{ t('contacts', 'Export') }}
+ </ActionButton>
+ <ActionButton @click="emailGroup(group)">
+ <template #icon>
+ <IconEmail :size="20" />
+ </template>
+ {{ t('contacts', 'Send email') }}
+ </ActionButton>
+ <ActionButton @click="emailGroup(group, 'bcc')">
+ <template #icon>
+ <IconEmail :size="20" />
+ </template>
+ {{ t('contacts', 'Send email as BCC') }}
+ </ActionButton>
+ </template>
- <template #counter>
- <NcCounterBubble v-if="group.contacts.length > 0">
- {{ group.contacts.length }}
- </NcCounterBubble>
- </template>
- </AppNavigationItem>
+ <template #counter>
+ <NcCounterBubble v-if="group.contacts.length > 0">
+ {{ group.contacts.length }}
+ </NcCounterBubble>
+ </template>
+ </AppNavigationItem>
+ </div>
</template>
<script>
@@ -77,6 +84,7 @@ import IconContact from 'vue-material-design-icons/AccountMultiple.vue'
import IconAdd from 'vue-material-design-icons/Plus.vue'
import IconDownload from 'vue-material-design-icons/Download.vue'
import IconEmail from 'vue-material-design-icons/Email.vue'
+import { showError } from '@nextcloud/dialogs'
export default {
name: 'GroupNavigationItem',
@@ -105,6 +113,53 @@ export default {
},
methods: {
+ /**
+ * @param groups
+ * @param groupId
+ */
+ isInGroup(groups, groupId) {
+ return groups.includes(groupId)
+ },
+ /**
+ * Drop contact on group handler.
+ *
+ * @param {object} event drop event
+ * @param {object} group to add to dropped contact
+ * @return {Promise<void>}
+ */
+ async onDrop(event, group) {
+ try {
+ const contactFromDropData = JSON.parse(event.dataTransfer.getData('item'))
+ const contactFromStore = this.$store.getters.getContact(`${contactFromDropData.uid}~${contactFromDropData.addressbookId}`)
+ if (contactFromStore && !this.isInGroup(contactFromStore.groups, group.id)) {
+ const contact = this.$store.getters.getContact(`${contactFromDropData.uid}~${contactFromDropData.addressbookId}`)
+ await this.$store.dispatch('updateContactGroups', {
+ groupNames: [...contactFromStore.groups, group.id],
+ contact,
+ })
+ const localContact = Object.assign(
+ Object.create(Object.getPrototypeOf(contact)),
+ contact,
+ )
+ localContact.groups = [...contactFromStore.groups, group.id]
+ await this.$store.dispatch('updateContact', localContact)
+ }
+ } catch (e) {
+ console.error(e)
+ showError('Tried to drop an invalid contact!')
+ } finally {
+ event.target.closest('.group-drop-area').removeAttribute('drop-active')
+ }
+ },
+ // Add marker for drop area
+ onDragOver(event) {
+ event.preventDefault()
+ event.target.closest('.group-drop-area').setAttribute('drop-active', true)
+ },
+ // Remove marker from drop area
+ onDragLeave(event) {
+ event.target.closest('.group-drop-area').removeAttribute('drop-active')
+ },
// Trigger the entity picker view
addContactsToGroup() {
emit('contacts:group:append', this.group.name)
@@ -184,3 +239,9 @@ export default {
},
}
</script>
+
+<style lang="scss" scoped>
+.group-drop-area[drop-active=true] {
+ background-color: var(--color-primary-light);
+}
+</style>
diff --git a/src/components/ContactsList/ContactsListItem.vue b/src/components/ContactsList/ContactsListItem.vue
index d46888a3..2439a117 100644
--- a/src/components/ContactsList/ContactsListItem.vue
+++ b/src/components/ContactsList/ContactsListItem.vue
@@ -1,24 +1,28 @@
<template>
- <ListItem :id="id"
- :key="source.key"
- class="list-item-style envelope"
- :name="source.displayName"
- :to="{ name: 'contact', params: { selectedGroup: selectedGroup, selectedContact: source.key } }">
- <!-- @slot Icon slot -->
-
- <template #icon>
- <div class="app-content-list-item-icon">
- <BaseAvatar :display-name="source.displayName" :url="avatarUrl" :size="40" />
- </div>
- </template>
- <template #subtitle>
- <div class="envelope__subtitle">
- <span class="envelope__subtitle__subject">
- {{ source.email }}
- </span>
- </div>
- </template>
- </ListItem>
+ <div class="contacts-list__item-wrapper"
+ :draggable="isDraggable"
+ @dragstart="startDrag($event, source)">
+ <ListItem :id="id"
+ :key="source.key"
+ class="list-item-style envelope"
+ :name="source.displayName"
+ :to="{ name: 'contact', params: { selectedGroup: selectedGroup, selectedContact: source.key } }">
+ <!-- @slot Icon slot -->
+
+ <template #icon>
+ <div class="app-content-list-item-icon">
+ <BaseAvatar :display-name="source.displayName" :url="avatarUrl" :size="40" />
+ </div>
+ </template>
+ <template #subtitle>
+ <div class="envelope__subtitle">
+ <span class="envelope__subtitle__subject">
+ {{ source.email }}
+ </span>
+ </div>
+ </template>
+ </ListItem>
+ </div>
</template>
<script>
@@ -62,7 +66,10 @@ export default {
selectedContact() {
return this.$route.params.selectedContact
},
-
+ // contact is not draggable when it has not been saved on server as it can't be added to groups/circles before
+ isDraggable() {
+ return !!this.source.dav && this.source.addressbook.id !== 'z-server-generated--system'
+ },
// usable and valid html id for scrollTo
id() {
return window.btoa(this.source.key).slice(0, -2)
@@ -81,6 +88,17 @@ export default {
await this.loadAvatarUrl()
},
methods: {
+ startDrag(evt, item) {
+ evt.dataTransfer.dropEffect = 'move'
+ evt.dataTransfer.effectAllowed = 'move'
+ evt.dataTransfer.setData('item', JSON.stringify({
+ addressbookId: item.addressbook.id,
+ displayName: item.displayName,
+ groups: item.groups,
+ url: item.url,
+ uid: item.uid,
+ }))
+ },
/**
* Is called on save in ContactDetails to reload Avatar,
@@ -120,6 +138,17 @@ export default {
this.avatarUrl = `${this.source.url}?photo`
}
},
+
+ /**
+ * Select this contact within the list
+ */
+ selectContact() {
+ // change url with router
+ this.$router.push({
+ name: 'contact',
+ params: { selectedGroup: this.selectedGroup, selectedContact: this.source.key },
+ })
+ },
},
}
</script>
@@ -148,3 +177,14 @@ export default {
}
</style>
+<style lang="scss">
+.contacts-list__item-wrapper {
+ &[draggable='true'] .avatardiv * {
+ cursor: move !important;
+ }
+
+ &[draggable='false'] .avatardiv * {
+ cursor: not-allowed !important;
+ }
+}
+</style>
diff --git a/src/store/index.js b/src/store/index.js
index aa0240ce..8bf112bf 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -55,7 +55,7 @@ export default new Store({
* the contat ical update itself on property getters
* this is causing issues with the strict mode.
* Since we're only getting the data for the contacts list
- * and considering we're initiating an independant contact
+ * and considering we're initiating an independent contact
* class for the details which replace itself into the
* store by mutations we can ignore this and say that
* the risk of losing track of changes is expandable.