diff --git a/lib/Controller/ContactController.php b/lib/Controller/ContactController.php index a6b3bad07..1cc73d4df 100644 --- a/lib/Controller/ContactController.php +++ b/lib/Controller/ContactController.php @@ -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, + ]; + } } diff --git a/src/components/Editor/Invitees/InviteesListSearch.vue b/src/components/Editor/Invitees/InviteesListSearch.vue index 0f7898751..f0e92e721 100644 --- a/src/components/Editor/Invitees/InviteesListSearch.vue +++ b/src/components/Editor/Invitees/InviteesListSearch.vue @@ -38,8 +38,12 @@ @select="addAttendee"> @@ -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 }, []) }, diff --git a/tests/php/unit/Controller/ContactControllerTest.php b/tests/php/unit/Controller/ContactControllerTest.php index 88726fff5..ac6814aa9 100644 --- a/tests/php/unit/Controller/ContactControllerTest.php +++ b/tests/php/unit/Controller/ContactControllerTest.php @@ -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());