Skip to content

Commit 111b779

Browse files
committed
feat: add batch moving contacts between addressbooks
Signed-off-by: Grigory Vodyanov <[email protected]>
1 parent a9d2899 commit 111b779

File tree

2 files changed

+140
-27
lines changed

2 files changed

+140
-27
lines changed

src/components/ContactsList.vue

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,16 @@
3434

3535
<NcModal v-if="isGrouping"
3636
:name="t('contacts', 'Add contacts to group')"
37-
size="large"
38-
@close="isGrouping = false">
39-
<Batch :contacts="Array.from(multiSelectedContacts.values())" @submit="isGrouping = false" />
37+
size="large"
38+
@close="isGrouping = false">
39+
<Batch :contacts="Array.from(multiSelectedContacts.values())" mode="grouping" @submit="isGrouping = false" />
40+
</NcModal>
41+
42+
<NcModal v-if="isMovingAddressbook"
43+
:name="t('contacts', 'Move contacts to addressbook')"
44+
size="large"
45+
@close="isMovingAddressbook = false">
46+
<Batch :contacts="Array.from(multiSelectedContacts.values())" mode="ab" @submit="isMovingAddressbook = false" />
4047
</NcModal>
4148

4249
<div class="contacts-list__header">
@@ -60,20 +67,30 @@
6067
<IconDelete :size="16" />
6168
</NcButton>
6269
<NcButton v-if="!isMergingLoading"
63-
type="tertiary"
70+
variant="tertiary"
6471
:disabled="!areTwoEditable"
6572
:title="mergeActionTitle"
6673
:close-after-click="true"
6774
@click.prevent="initiateContactMerging">
6875
<IconSetMerge :size="20" />
6976
</NcButton>
7077
<NcLoadingIcon v-else :size="20" />
71-
<NcButton type="tertiary"
78+
<NcButton variant="tertiary"
7279
:title="groupActionTitle"
80+
:disabled="!isAtLeastOneEditable"
7381
:close-after-click="true"
82+
@submit="finishBatch"
7483
@click.prevent="isGrouping = true">
7584
<IconAccountMultiple :size="20" />
7685
</NcButton>
86+
<NcButton variant="tertiary"
87+
:title="addressbookActionTitle"
88+
:disabled="!isAtLeastOneEditable"
89+
:close-after-click="true"
90+
@submit="finishBatch"
91+
@click.prevent="isMovingAddressbook = true">
92+
<IconBookAccount :size="20" />
93+
</NcButton>
7794
</div>
7895
</transition>
7996

@@ -98,6 +115,7 @@ import IconSelect from 'vue-material-design-icons/CloseThick.vue'
98115
import IconDelete from 'vue-material-design-icons/TrashCanOutline.vue'
99116
import IconSetMerge from 'vue-material-design-icons/SetMerge.vue'
100117
import IconAccountMultiple from 'vue-material-design-icons/AccountMultipleOutline.vue'
118+
import IconBookAccount from 'vue-material-design-icons/BookAccountOutline.vue'
101119
import Merging from './ContactsList/Merging.vue'
102120
import Batch from './ContactsList/Batch.vue'
103121
@@ -119,6 +137,7 @@ export default {
119137
IconDelete,
120138
IconSetMerge,
121139
IconAccountMultiple,
140+
IconBookAccount,
122141
NcDialog,
123142
NcModal,
124143
Merging,
@@ -173,6 +192,7 @@ export default {
173192
isMerging: false,
174193
isMergingLoading: false,
175194
isGrouping: false,
195+
isMovingAddressbook: false,
176196
}
177197
},
178198
@@ -227,6 +247,11 @@ export default {
227247
? n('contacts', 'Add {number} contact to group', 'Add {number} contacts to group', this.multiSelectedContacts.size, { number: this.multiSelectedContacts.size })
228248
: t('contacts', 'Please select at least one editable contact to add to a group')
229249
},
250+
addressbookActionTitle() {
251+
return this.isAtLeastOneEditable
252+
? n('contacts', 'Move {number} contact to addressbook', 'Move {number} contacts to addressbook', this.multiSelectedContacts.size, { number: this.multiSelectedContacts.size })
253+
: t('contacts', 'Please select at least one editable contact to move to an addressbook')
254+
},
230255
},
231256
232257
watch: {
@@ -396,6 +421,17 @@ export default {
396421
name: 'root',
397422
})
398423
},
424+
425+
async finishBatch() {
426+
this.isGrouping = false
427+
this.isMovingAddressbook = false
428+
429+
for (const contact of this.multiSelectedContacts.values()) {
430+
await this.$store.dispatch('fetchFullContact', { contact, forceReFetch: true })
431+
}
432+
433+
this.unselectAllMultiSelected()
434+
},
399435
},
400436
}
401437
</script>

src/components/ContactsList/Batch.vue

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,29 @@
66
<template>
77
<div class="batch">
88
<div class="batch__title">
9-
<h3>{{ t('contacts', 'Add contacts to groups') }}</h3>
9+
<h3 v-if="mode === 'grouping'">
10+
{{ t('contacts', 'Add contacts to groups') }}
11+
</h3>
12+
<h3 v-if="mode === 'ab'">
13+
{{ t('contacts', 'Move contacts to addressbook') }}
14+
</h3>
1015
</div>
1116

12-
<NcSelect v-model="selectedGroups"
17+
<NcSelect v-if="mode === 'grouping'"
18+
v-model="selectedGroups"
1319
:input-label="t('contacts', 'Select groups')"
1420
:multiple="true"
1521
:options="groupOptions" />
1622

23+
<!-- Addressbook selector for move mode -->
24+
<NcSelect v-if="mode === 'ab'"
25+
v-model="selectedAddressesBook"
26+
:input-label="t('contacts', 'Select addressbook')"
27+
:options="addressbookOptions" />
28+
1729
<h6>{{ t('contacts', 'Selected contacts') }}</h6>
1830
<NcNoteCard v-if="amountOfReadOnlyContacts > 0" type="info">
19-
{{ t('contacts', 'Please note that {count} contact{p} readonly and will not be added to groups. If you want to include them all you can create a Team instead.', { count: amountOfReadOnlyContacts, p: amountOfReadOnlyContacts === 1 ? ' is' : 's are' }) }}
31+
{{ t('contacts', 'Please note that {count} contact{p} readonly and will not be modified.', { count: amountOfReadOnlyContacts, p: amountOfReadOnlyContacts === 1 ? ' is' : 's are' }) }}
2032
</NcNoteCard>
2133

2234
<div class="contacts-list">
@@ -26,25 +38,39 @@
2638
:index="index"
2739
:source="contact"
2840
:reload-bus="reloadBus"
29-
:title="contact.addressbook.canModifyCard ? '' : t('contacts', 'This contact is read-only and cannot be added to groups. Try creating a Team instead.')"
41+
:title="contact.addressbook.canModifyCard ? '' : t('contacts', 'This contact is read-only and cannot be modified.')"
3042
:is-static="true" />
3143
</div>
3244
</div>
3345

34-
<NcButton v-if="contacts.length > 9" variant="secondary" @click="showAllContacts = !showAllContacts">
35-
<template #icon>
36-
<IconPlus :size="20" />
37-
</template>
38-
{{ t('contacts', showAllContacts ? 'Show less' : 'Show all') }}
39-
</NcButton>
40-
41-
<div class="batch__footer">
42-
<NcButton variant="primary" :disabled="selectedGroups.length === 0" @click="submit">
43-
<template #icon>
44-
<IconAccountPlus :size="20" />
45-
</template>
46-
{{ t('contacts', 'Add') }}
47-
</NcButton>
46+
<NcButton v-if="contacts.length > 9"
47+
variant="secondary"
48+
@click="showAllContacts = !showAllContacts">
49+
<template #icon>
50+
<IconPlus :size="20" />
51+
</template>
52+
{{ t('contacts', showAllContacts ? 'Show less' : 'Show all') }}
53+
</NcButton>
54+
55+
<div class="batch__footer">
56+
<NcButton v-if="mode === 'grouping'"
57+
variant="primary"
58+
:disabled="selectedGroups.length === 0"
59+
@click="submit">
60+
<template #icon>
61+
<IconAccountPlus :size="20" />
62+
</template>
63+
{{ t('contacts', 'Add') }}
64+
</NcButton>
65+
<NcButton v-if="mode === 'ab'"
66+
variant="primary"
67+
:disabled="!selectedAddressesBook"
68+
@click="submit">
69+
<template #icon>
70+
<IconBookArrow :size="20" />
71+
</template>
72+
{{ t('contacts', 'Move') }}
73+
</NcButton>
4874
</div>
4975
</div>
5076
</template>
@@ -54,6 +80,7 @@ import ContactsListItem from './ContactsListItem.vue'
5480
import { NcButton, NcSelect, NcNoteCard } from '@nextcloud/vue'
5581
import IconPlus from 'vue-material-design-icons/Plus.vue'
5682
import IconAccountPlus from 'vue-material-design-icons/AccountMultiplePlusOutline.vue'
83+
import IconBookArrow from 'vue-material-design-icons/BookArrowRightOutline.vue'
5784
import appendContactToGroup from '../../services/appendContactToGroup.js'
5885
5986
export default {
@@ -65,6 +92,7 @@ export default {
6592
NcSelect,
6693
IconPlus,
6794
IconAccountPlus,
95+
IconBookArrow,
6896
NcNoteCard,
6997
},
7098
@@ -73,13 +101,21 @@ export default {
73101
type: Array,
74102
required: true,
75103
},
104+
mode: {
105+
type: String,
106+
required: false,
107+
default: 'grouping',
108+
},
76109
},
77110
111+
emits: ['submit'],
112+
78113
data() {
79114
return {
80115
reloadBus: null,
81116
showAllContacts: false,
82117
selectedGroups: [],
118+
selectedAddressesBook: null,
83119
}
84120
},
85121
@@ -99,17 +135,33 @@ export default {
99135
amountOfReadOnlyContacts() {
100136
return this.contacts.filter(contact => !contact.addressbook.canModifyCard).length
101137
},
138+
addressbookOptions() {
139+
// Provide only enabled, writable addressbooks to move to
140+
return this.$store.getters.getAddressbooks
141+
.filter(ab => !ab.readOnly && ab.enabled)
142+
.map(ab => ({ label: ab.displayName || ab.label || ab.addressbook, value: ab.id || ab.addressbook }))
143+
},
102144
},
103145
104146
methods: {
105-
async submit() {
147+
submit() {
148+
if (this.mode === 'grouping') {
149+
this.group()
150+
}
151+
152+
if (this.mode === 'ab') {
153+
this.moveToAddressbook()
154+
}
155+
},
156+
157+
async group() {
106158
const allGroups = this.$store.getters.getGroups
107159
108160
// Add to groups
109-
this.selectedGroups.forEach(groupName => {
110-
const group = allGroups.find(g => g.name === groupName)
161+
this.selectedGroups.forEach(selectedGroup => {
162+
const group = allGroups.find(g => g.name === selectedGroup.value)
111163
if (!group) {
112-
console.error('Cannot add contact to an undefined group', groupName)
164+
console.error('Cannot add contact to an undefined group', selectedGroup)
113165
return
114166
}
115167
this.contacts.forEach(contact => {
@@ -127,6 +179,31 @@ export default {
127179
128180
this.$emit('submit')
129181
},
182+
183+
async moveToAddressbook() {
184+
if (!this.selectedAddressesBook) return
185+
const addressbook = this.$store.getters.getAddressbooks.find(ab => ab.id === this.selectedAddressesBook.value)
186+
if (!addressbook) {
187+
console.error('Selected addressbook not found', this.selectedAddressesBook)
188+
return
189+
}
190+
191+
const movePromises = this.contacts.map(async (contact) => {
192+
if (!contact.addressbook.canModifyCard || contact.addressbook.id === addressbook.id) {
193+
return null
194+
}
195+
try {
196+
await this.$store.dispatch('moveContactToAddressbook', { contact, addressbook })
197+
return contact
198+
} catch (error) {
199+
console.error('Failed to move contact', contact, error)
200+
return null
201+
}
202+
})
203+
204+
await Promise.all(movePromises)
205+
this.$emit('submit')
206+
},
130207
},
131208
}
132209
</script>

0 commit comments

Comments
 (0)