Allow to select contact groups

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2022-11-17 17:56:59 +01:00
parent cdce2f6fd8
commit 1ef769ea3b
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
3 changed files with 160 additions and 66 deletions

View File

@ -122,10 +122,12 @@ class ContactController extends Controller {
return new JSONResponse(); return new JSONResponse();
} }
$result = $this->contactsManager->search($search, ['FN', 'EMAIL']); $contactsResult = $this->contactsManager->search($search, ['FN', 'EMAIL']);
$groupsContactsResult = $this->contactsManager->search($search, ['CATEGORIES']);
$contacts = []; $contacts = [];
foreach ($result as $r) { foreach ($contactsResult as $r) {
// Information about system users is fetched via DAV nowadays // Information about system users is fetched via DAV nowadays
if (isset($r['isLocalSystemBook']) && $r['isLocalSystemBook']) { if (isset($r['isLocalSystemBook']) && $r['isLocalSystemBook']) {
continue; continue;
@ -135,43 +137,30 @@ class ContactController extends Controller {
continue; continue;
} }
$name = $this->getNameFromContact($r); $contacts[] = $this->processContact($r);
if (\is_string($r['EMAIL'])) {
$r['EMAIL'] = [$r['EMAIL']];
}
$photo = isset($r['PHOTO'])
? $this->getPhotoUri($r['PHOTO'])
: null;
$lang = null;
if (isset($r['LANG'])) {
if (\is_array($r['LANG'])) {
$lang = $r['LANG'][0];
} else {
$lang = $r['LANG'];
}
}
$timezoneId = null;
if (isset($r['TZ'])) {
if (\is_array($r['TZ'])) {
$timezoneId = $r['TZ'][0];
} else {
$timezoneId = $r['TZ'];
}
}
$contacts[] = [
'name' => $name,
'emails' => $r['EMAIL'],
'lang' => $lang,
'tzid' => $timezoneId,
'photo' => $photo,
];
} }
return new JSONResponse($contacts); $groupsContacts = array_reduce($groupsContactsResult, function (array $acc, array $groupContact) use ($search) {
// Information about system users is fetched via DAV nowadays
if (isset($groupContact['isLocalSystemBook']) && $groupContact['isLocalSystemBook']) {
return $acc;
}
if (!isset($groupContact['EMAIL'])) {
return $acc;
}
$categories = array_filter(explode(',', $groupContact['CATEGORIES']), function (string $category) use ($search) {
return str_contains(mb_strtolower($category), mb_strtolower($search));
});
foreach ($categories as $category) {
$acc[$category][] = $this->processContact($groupContact);
}
return $acc;
}, []);
return new JSONResponse(['contacts' => $contacts, 'groups' => $groupsContacts]);
} }
/** /**
@ -252,4 +241,41 @@ class ContactController extends Controller {
return null; return null;
} }
private function processContact(array $contactData): array {
$name = $this->getNameFromContact($contactData);
if (\is_string($contactData['EMAIL'])) {
$contactData['EMAIL'] = [$contactData['EMAIL']];
}
$photo = isset($contactData['PHOTO'])
? $this->getPhotoUri($contactData['PHOTO'])
: null;
$lang = null;
if (isset($contactData['LANG'])) {
if (\is_array($contactData['LANG'])) {
$lang = $contactData['LANG'][0];
} else {
$lang = $contactData['LANG'];
}
}
$timezoneId = null;
if (isset($contactData['TZ'])) {
if (\is_array($contactData['TZ'])) {
$timezoneId = $contactData['TZ'][0];
} else {
$timezoneId = $contactData['TZ'];
}
}
return [
'name' => $name,
'emails' => $contactData['EMAIL'],
'lang' => $lang,
'tzid' => $timezoneId,
'photo' => $photo,
];
}
} }

View File

@ -38,8 +38,12 @@
@select="addAttendee"> @select="addAttendee">
<template #option="{ option }"> <template #option="{ option }">
<div class="invitees-search-list-item"> <div class="invitees-search-list-item">
<!-- We need to specify a unique key here for the avatar to be reactive. --> <Avatar v-if="option.type === 'group'">
<Avatar v-if="option.isUser" <template #icon>
<AccountMultiple :size="20" />
</template>
</Avatar><!-- We need to specify a unique key here for the avatar to be reactive. -->
<Avatar v-else-if="option.isUser"
:key="option.uid" :key="option.uid"
:user="option.avatar" :user="option.avatar"
:display-name="option.dropdownName" /> :display-name="option.dropdownName" />
@ -52,9 +56,12 @@
<div> <div>
{{ option.dropdownName }} {{ option.dropdownName }}
</div> </div>
<div v-if="option.email !== option.dropdownName"> <div v-if="option.email !== option.dropdownName && option.type !== 'group'">
{{ option.email }} {{ option.email }}
</div> </div>
<div v-if="option.type === 'group'">
{{ option.subtitle }}
</div>
</div> </div>
</div> </div>
</template> </template>
@ -69,12 +76,14 @@ import HttpClient from '@nextcloud/axios'
import debounce from 'debounce' import debounce from 'debounce'
import { linkTo } from '@nextcloud/router' import { linkTo } from '@nextcloud/router'
import { randomId } from '../../../utils/randomId.js' import { randomId } from '../../../utils/randomId.js'
import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
export default { export default {
name: 'InviteesListSearch', name: 'InviteesListSearch',
components: { components: {
Avatar, Avatar,
Multiselect, Multiselect,
AccountMultiple,
}, },
props: { props: {
alreadyInvitedEmails: { alreadyInvitedEmails: {
@ -147,7 +156,13 @@ export default {
this.matches = matches this.matches = matches
}, 500), }, 500),
addAttendee(selectedValue) { addAttendee(selectedValue) {
this.$emit('add-attendee', selectedValue) if (selectedValue.type === 'group') {
selectedValue.contacts.forEach((contact) => {
this.$emit('add-attendee', contact)
})
} else {
this.$emit('add-attendee', selectedValue)
}
}, },
async findAttendeesFromContactsAPI(query) { async findAttendeesFromContactsAPI(query) {
let response let response
@ -161,8 +176,24 @@ export default {
return [] return []
} }
const data = response.data const contacts = []
return data.reduce((arr, result) => { /** Groups are shown before contacts */
for (const [groupName, groupContacts] of Object.entries(response.data.groups)) {
const processedGroupContacts = this.buildEmailsFromContactData(groupContacts)
if (processedGroupContacts.length > 0) {
contacts.push({
type: 'group',
dropdownName: groupName,
subtitle: this.$n('calendar', 'Contains %n contact', 'Contains %n contacts', processedGroupContacts.length),
contacts: processedGroupContacts,
})
}
}
return [...contacts, ...this.buildEmailsFromContactData(response.data.contacts)]
},
buildEmailsFromContactData(contactsData) {
return contactsData.reduce((arr, result) => {
const hasMultipleEMails = result.emails.length > 1 const hasMultipleEMails = result.emails.length > 1
result.emails.forEach((email) => { result.emails.forEach((email) => {
@ -174,12 +205,11 @@ export default {
} else { } else {
name = email name = email
} }
if (this.alreadyInvitedEmails.includes(email)) { if (this.alreadyInvitedEmails.includes(email)) {
return return
} }
arr.push({ arr.push({
type: 'contact',
calendarUserType: 'INDIVIDUAL', calendarUserType: 'INDIVIDUAL',
commonName: result.name, commonName: result.name,
email, email,
@ -191,7 +221,6 @@ export default {
dropdownName: name, dropdownName: name,
}) })
}) })
return arr return arr
}, []) }, [])
}, },

View File

@ -167,10 +167,10 @@ class ContactControllerTest extends TestCase {
->with() ->with()
->willReturn(true); ->willReturn(true);
$this->manager->expects(self::once()) $this->manager->expects(self::exactly(2))
->method('search') ->method('search')
->with('search 123', ['FN', 'EMAIL']) ->withConsecutive(['search 123', ['FN', 'EMAIL']], ['search 123', ['CATEGORIES']])
->willReturn([ ->willReturnOnConsecutiveCalls([
[ [
'FN' => 'Person 1', 'FN' => 'Person 1',
'ADR' => [ 'ADR' => [
@ -213,29 +213,68 @@ class ContactControllerTest extends TestCase {
'TZ' => 'Australia/Adelaide', 'TZ' => 'Australia/Adelaide',
'PHOTO' => 'VALUE:BINARY:4242424242' 'PHOTO' => 'VALUE:BINARY:4242424242'
], ],
], [
[
'FN' => 'Person 4',
'EMAIL' => 'foo4@example.com',
'CATEGORIES' => 'search 123,other'
],
[
'FN' => 'Person 5',
'CATEGORIES' => 'search 123'
],
[
'FN' => 'Person 6',
'EMAIL' => 'foo6@example.com',
'CATEGORIES' => 'search 123'
],
]); ]);
$response = $this->controller->searchAttendee('search 123'); $response = $this->controller->searchAttendee('search 123');
$this->assertInstanceOf(JSONResponse::class, $response); $this->assertInstanceOf(JSONResponse::class, $response);
$this->assertEquals([ $this->assertEquals([
[ 'contacts' => [
'name' => 'Person 1', [
'emails' => [ 'name' => 'Person 1',
'foo1@example.com', 'emails' => [
'foo2@example.com', 'foo1@example.com',
], 'foo2@example.com',
'lang' => 'de', ],
'tzid' => 'Europe/Berlin', 'lang' => 'de',
'photo' => 'http://foo.bar', 'tzid' => 'Europe/Berlin',
], [ 'photo' => 'http://foo.bar',
'name' => 'Person 2', ], [
'emails' => [ 'name' => 'Person 2',
'foo3@example.com' 'emails' => [
], 'foo3@example.com'
'lang' => null, ],
'tzid' => null, 'lang' => null,
'photo' => null, 'tzid' => null,
'photo' => null,
]
],
'groups' => [
'search 123' => [
[
'name' => 'Person 4',
'emails' => [
'foo4@example.com'
],
'lang' => null,
'tzid' => null,
'photo' => null,
],
[
'name' => 'Person 6',
'emails' => [
'foo6@example.com'
],
'lang' => null,
'tzid' => null,
'photo' => null,
]
]
] ]
], $response->getData()); ], $response->getData());
$this->assertEquals(200, $response->getStatus()); $this->assertEquals(200, $response->getStatus());