diff options
author | Hamza <40746210+hamza221@users.noreply.github.com> | 2024-04-08 20:36:23 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-08 20:36:23 +0200 |
commit | e1badcf48577f1246f61500d926ecf6ca04c101e (patch) | |
tree | 8fce6948306e2ff3f6cad6af04783ea5545b4fbe | |
parent | 5b94d9c6f255edcacb86ff2fd9e59138e61ae355 (diff) | |
parent | 321764990066edcf60691e31d3b51568b027034e (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.vue | 141 | ||||
-rw-r--r-- | src/components/ContactsList/ContactsListItem.vue | 82 | ||||
-rw-r--r-- | src/store/index.js | 2 |
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. |