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();
}
$result = $this->contactsManager->search($search, ['FN', 'EMAIL']);
$contactsResult = $this->contactsManager->search($search, ['FN', 'EMAIL']);
$groupsContactsResult = $this->contactsManager->search($search, ['CATEGORIES']);
$contacts = [];
foreach ($result as $r) {
foreach ($contactsResult as $r) {
// Information about system users is fetched via DAV nowadays
if (isset($r['isLocalSystemBook']) && $r['isLocalSystemBook']) {
continue;
@ -135,43 +137,30 @@ class ContactController extends Controller {
continue;
}
$name = $this->getNameFromContact($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,
];
$contacts[] = $this->processContact($r);
}
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;
}
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">
<template #option="{ option }">
<div class="invitees-search-list-item">
<!-- We need to specify a unique key here for the avatar to be reactive. -->
<Avatar v-if="option.isUser"
<Avatar v-if="option.type === 'group'">
<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"
:user="option.avatar"
:display-name="option.dropdownName" />
@ -52,9 +56,12 @@
<div>
{{ option.dropdownName }}
</div>
<div v-if="option.email !== option.dropdownName">
<div v-if="option.email !== option.dropdownName && option.type !== 'group'">
{{ option.email }}
</div>
<div v-if="option.type === 'group'">
{{ option.subtitle }}
</div>
</div>
</div>
</template>
@ -69,12 +76,14 @@ import HttpClient from '@nextcloud/axios'
import debounce from 'debounce'
import { linkTo } from '@nextcloud/router'
import { randomId } from '../../../utils/randomId.js'
import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
export default {
name: 'InviteesListSearch',
components: {
Avatar,
Multiselect,
AccountMultiple,
},
props: {
alreadyInvitedEmails: {
@ -147,7 +156,13 @@ export default {
this.matches = matches
}, 500),
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) {
let response
@ -161,8 +176,24 @@ export default {
return []
}
const data = response.data
return data.reduce((arr, result) => {
const contacts = []
/** 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
result.emails.forEach((email) => {
@ -174,12 +205,11 @@ export default {
} else {
name = email
}
if (this.alreadyInvitedEmails.includes(email)) {
return
}
arr.push({
type: 'contact',
calendarUserType: 'INDIVIDUAL',
commonName: result.name,
email,
@ -191,7 +221,6 @@ export default {
dropdownName: name,
})
})
return arr
}, [])
},

View File

@ -167,10 +167,10 @@ class ContactControllerTest extends TestCase {
->with()
->willReturn(true);
$this->manager->expects(self::once())
$this->manager->expects(self::exactly(2))
->method('search')
->with('search 123', ['FN', 'EMAIL'])
->willReturn([
->withConsecutive(['search 123', ['FN', 'EMAIL']], ['search 123', ['CATEGORIES']])
->willReturnOnConsecutiveCalls([
[
'FN' => 'Person 1',
'ADR' => [
@ -213,29 +213,68 @@ class ContactControllerTest extends TestCase {
'TZ' => 'Australia/Adelaide',
'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');
$this->assertInstanceOf(JSONResponse::class, $response);
$this->assertEquals([
[
'name' => 'Person 1',
'emails' => [
'foo1@example.com',
'foo2@example.com',
],
'lang' => 'de',
'tzid' => 'Europe/Berlin',
'photo' => 'http://foo.bar',
], [
'name' => 'Person 2',
'emails' => [
'foo3@example.com'
],
'lang' => null,
'tzid' => null,
'photo' => null,
'contacts' => [
[
'name' => 'Person 1',
'emails' => [
'foo1@example.com',
'foo2@example.com',
],
'lang' => 'de',
'tzid' => 'Europe/Berlin',
'photo' => 'http://foo.bar',
], [
'name' => 'Person 2',
'emails' => [
'foo3@example.com'
],
'lang' => 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());
$this->assertEquals(200, $response->getStatus());