mirror of https://github.com/nextcloud/calendar
feat: Ability to invite circles
This commit adds support for adding circles as attendees to a calendar event. The relationship between the imported group and members will be compliant with the iCal specification. A circle with the title "testcircle" will be added as an attendee with iCal attributes "CUTYPE=GROUP` and uri "mailto:circle+CIRCLEID@CIRCLE_INSTANCE". Members of the circle will be imported as standard attendees. Each member gets assigned to the circle group entry by assigning them to the group uri using the iCal member property: "MEMBER='mailto:circle+CIRCLEID@CIRCLE_INSTANCE'". Searching for circles is only enabled if the circles app is activated. Circles added to the list of attendees get imported only once and are not synced yet. While adding a circle, a notice about this is shown to the user. Only members of local circles which are local users get imported. Rendering groups in the frontend is done in a separate PR https://github.com/nextcloud/calendar/pull/5396 Signed-off-by: Jonas Heinrich <heinrich@synyx.de>
This commit is contained in:
parent
0cd8b2570e
commit
df2d51b666
|
@ -5,9 +5,12 @@ declare(strict_types=1);
|
|||
* Calendar App
|
||||
*
|
||||
* @author Georg Ehrke
|
||||
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Thomas Müller
|
||||
* @author Jonas Heinrich
|
||||
*
|
||||
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @copyright 2016 Thomas Müller <thomas.mueller@tmit.eu>
|
||||
* @copyright 2023 Jonas Heinrich <heinrich@synyx.net>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
|
@ -54,6 +57,8 @@ return [
|
|||
['name' => 'contact#searchAttendee', 'url' => '/v1/autocompletion/attendee', 'verb' => 'POST'],
|
||||
['name' => 'contact#searchLocation', 'url' => '/v1/autocompletion/location', 'verb' => 'POST'],
|
||||
['name' => 'contact#searchPhoto', 'url' => '/v1/autocompletion/photo', 'verb' => 'POST'],
|
||||
// Circles
|
||||
['name' => 'contact#getCircleMembers', 'url' => '/v1/circles/getmembers', 'verb' => 'GET'],
|
||||
// Settings
|
||||
['name' => 'settings#setConfig', 'url' => '/v1/config/{key}', 'verb' => 'POST'],
|
||||
// Tools
|
||||
|
|
|
@ -7,10 +7,12 @@ declare(strict_types=1);
|
|||
* @author Georg Ehrke
|
||||
* @author Jakob Röhrl
|
||||
* @author Christoph Wurst
|
||||
* @author Jonas Heinrich
|
||||
*
|
||||
* @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @copyright 2019 Jakob Röhrl <jakob.roehrl@web.de>
|
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @copyright 2023 Jonas Heinrich <heinrich@synyx.net>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
|
@ -28,11 +30,16 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\Calendar\Controller;
|
||||
|
||||
use OCA\Calendar\Service\ServiceException;
|
||||
use OCA\Circles\Exceptions\CircleNotFoundException;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\QueryException;
|
||||
use OCP\Contacts\IManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserManager;
|
||||
|
||||
/**
|
||||
* Class ContactController
|
||||
|
@ -43,6 +50,12 @@ class ContactController extends Controller {
|
|||
/** @var IManager */
|
||||
private $contactsManager;
|
||||
|
||||
/** @var IAppManager */
|
||||
private $appManager;
|
||||
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
|
||||
/**
|
||||
* ContactController constructor.
|
||||
*
|
||||
|
@ -52,9 +65,13 @@ class ContactController extends Controller {
|
|||
*/
|
||||
public function __construct(string $appName,
|
||||
IRequest $request,
|
||||
IManager $contacts) {
|
||||
IManager $contacts,
|
||||
IAppManager $appManager,
|
||||
IUserManager $userManager) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->contactsManager = $contacts;
|
||||
$this->appManager = $appManager;
|
||||
$this->userManager = $userManager;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,6 +190,66 @@ class ContactController extends Controller {
|
|||
return new JSONResponse($contacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query members of a circle by circleId
|
||||
*
|
||||
* @param string $circleId CircleId to query for members
|
||||
* @return JSONResponse
|
||||
* @throws Exception
|
||||
* @throws \OCP\AppFramework\QueryException
|
||||
*
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function getCircleMembers(string $circleId):JSONResponse {
|
||||
if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
|
||||
return new JSONResponse();
|
||||
}
|
||||
if (!$this->contactsManager->isEnabled()) {
|
||||
return new JSONResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleId, true);
|
||||
} catch (QueryException $ex) {
|
||||
return new JSONResponse();
|
||||
} catch (CircleNotFoundException $ex) {
|
||||
return new JSONResponse();
|
||||
}
|
||||
|
||||
if (!$circle) {
|
||||
return new JSONResponse();
|
||||
}
|
||||
|
||||
$circleMembers = $circle->getInheritedMembers();
|
||||
|
||||
foreach ($circleMembers as $circleMember) {
|
||||
if ($circleMember->isLocal()) {
|
||||
|
||||
$circleMemberUserId = $circleMember->getUserId();
|
||||
|
||||
$user = $this->userManager->get($circleMemberUserId);
|
||||
|
||||
if ($user === null) {
|
||||
throw new ServiceException('Could not find organizer');
|
||||
}
|
||||
|
||||
$contacts[] = [
|
||||
'commonName' => $circleMember->getDisplayName(),
|
||||
'calendarUserType' => 'INDIVIDUAL',
|
||||
'email' => $user->getEMailAddress(),
|
||||
'isUser' => true,
|
||||
'avatar' => $circleMemberUserId,
|
||||
'hasMultipleEMails' => false,
|
||||
'dropdownName' => $circleMember->getDisplayName(),
|
||||
'member' => 'mailto:circle+' . $circleId . '@' . $circleMember->getInstance(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return new JSONResponse($contacts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a contact's photo based on their email-address
|
||||
*
|
||||
|
|
|
@ -6,8 +6,10 @@ declare(strict_types=1);
|
|||
*
|
||||
* @author Georg Ehrke
|
||||
* @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
* @author Jonas Heinrich <heinrich@synyx.net>
|
||||
* @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @copyright Copyright (c) 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
|
||||
* @copyright 2023 Jonas Heinrich <heinrich@synyx.net>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
|
@ -25,6 +27,7 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\Calendar\Controller;
|
||||
|
||||
use OC\App\CompareVersion;
|
||||
use OCA\Calendar\Service\Appointments\AppointmentConfigService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
|
@ -51,6 +54,9 @@ class ViewController extends Controller {
|
|||
/** @var IAppManager */
|
||||
private $appManager;
|
||||
|
||||
/** @var CompareVersion */
|
||||
private $compareVersion;
|
||||
|
||||
/** @var string */
|
||||
private $userId;
|
||||
|
||||
|
@ -62,6 +68,7 @@ class ViewController extends Controller {
|
|||
AppointmentConfigService $appointmentConfigService,
|
||||
IInitialState $initialStateService,
|
||||
IAppManager $appManager,
|
||||
CompareVersion $compareVersion,
|
||||
?string $userId,
|
||||
IAppData $appData) {
|
||||
parent::__construct($appName, $request);
|
||||
|
@ -69,6 +76,7 @@ class ViewController extends Controller {
|
|||
$this->appointmentConfigService = $appointmentConfigService;
|
||||
$this->initialStateService = $initialStateService;
|
||||
$this->appManager = $appManager;
|
||||
$this->compareVersion = $compareVersion;
|
||||
$this->userId = $userId;
|
||||
$this->appData = $appData;
|
||||
}
|
||||
|
@ -117,6 +125,11 @@ class ViewController extends Controller {
|
|||
$talkApiVersion = version_compare($this->appManager->getAppVersion('spreed'), '12.0.0', '>=') ? 'v4' : 'v1';
|
||||
$tasksEnabled = $this->appManager->isEnabledForUser('tasks');
|
||||
|
||||
$circleVersion = $this->appManager->getAppVersion('circles');
|
||||
$isCirclesEnabled = $this->appManager->isEnabledForUser('circles') === true;
|
||||
// if circles is not installed, we use 0.0.0
|
||||
$isCircleVersionCompatible = $this->compareVersion->isCompatible($circleVersion ? $circleVersion : '0.0.0', '22');
|
||||
|
||||
$this->initialStateService->provideInitialState('app_version', $appVersion);
|
||||
$this->initialStateService->provideInitialState('event_limit', $eventLimit);
|
||||
$this->initialStateService->provideInitialState('first_run', $firstRun);
|
||||
|
@ -138,6 +151,7 @@ class ViewController extends Controller {
|
|||
$this->initialStateService->provideInitialState('disable_appointments', $disableAppointments);
|
||||
$this->initialStateService->provideInitialState('can_subscribe_link', $canSubscribeLink);
|
||||
$this->initialStateService->provideInitialState('show_resources', $showResources);
|
||||
$this->initialStateService->provideInitialState('isCirclesEnabled', $isCirclesEnabled && $isCircleVersionCompatible);
|
||||
|
||||
return new TemplateResponse($this->appName, 'main');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Calendar\Service;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ServiceException extends Exception {
|
||||
}
|
|
@ -27,6 +27,10 @@
|
|||
<referencedClass name="Doctrine\DBAL\Types\Type" />
|
||||
<referencedClass name="Doctrine\DBAL\Types\Types" />
|
||||
<referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" />
|
||||
<referencedClass name="OC\App\CompareVersion" />
|
||||
<referencedClass name="OCA\Circles\Api\v1\Circles" />
|
||||
<referencedClass name="OCA\Circles\Exceptions\CircleNotFoundException" />
|
||||
<referencedClass name="OCA\Calendar\Controller\Exception" />
|
||||
<referencedClass name="Psr\Http\Client\ClientExceptionInterface" />
|
||||
<referencedClass name="Sabre\VObject\Component\VCalendar" />
|
||||
<referencedClass name="Sabre\VObject\Component\VTimezone" />
|
||||
|
@ -48,6 +52,9 @@
|
|||
<referencedClass name="Doctrine\DBAL\Schema\Table" />
|
||||
<referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" />
|
||||
<referencedClass name="Sabre\VObject\Component\VTimezone" />
|
||||
<referencedClass name="OC\App\CompareVersion" />
|
||||
<referencedClass name="OCA\Calendar\Controller\Exception" />
|
||||
<referencedClass name="OCA\Circles\Api\v1\Circles" />
|
||||
<referencedClass name="Symfony\Component\Console\Output\OutputInterface" />
|
||||
</errorLevel>
|
||||
</UndefinedDocblockClass>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<div>
|
||||
<InviteesListSearch v-if="!isReadOnly && !isSharedWithMe && hasUserEmailAddress"
|
||||
:already-invited-emails="alreadyInvitedEmails"
|
||||
:organizer="calendarObjectInstance.organizer"
|
||||
@add-attendee="addAttendee" />
|
||||
<OrganizerListItem v-if="hasOrganizer"
|
||||
:is-read-only="isReadOnly || isSharedWithMe"
|
||||
|
@ -133,8 +134,16 @@ export default {
|
|||
})
|
||||
},
|
||||
groups() {
|
||||
return this.calendarObjectInstance.attendees.filter(attendee => {
|
||||
return attendee.attendeeProperty.userType === 'GROUP'
|
||||
return this.invitees.filter(attendee => {
|
||||
if (attendee.attendeeProperty.userType === 'GROUP') {
|
||||
attendee.members = this.invitees.filter(invitee => {
|
||||
return invitee.attendeeProperty.member
|
||||
&& invitee.attendeeProperty.member.includes(attendee.uri)
|
||||
&& attendee.attendeeProperty.userType === 'GROUP'
|
||||
})
|
||||
return attendee.members.length > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
},
|
||||
inviteesWithoutOrganizer() {
|
||||
|
@ -146,36 +155,20 @@ export default {
|
|||
return this.invitees
|
||||
.filter(attendee => {
|
||||
// Filter attendees which are part of an invited group
|
||||
let isMemberOfGroup = false
|
||||
if (attendee.attendeeProperty.member) {
|
||||
isMemberOfGroup = this.groups.some(function(group) {
|
||||
return attendee.attendeeProperty.member.includes(group.uri)
|
||||
&& attendee.attendeeProperty.userType === 'INDIVIDUAL'
|
||||
})
|
||||
if (this.groups.some(function(group) {
|
||||
return attendee.attendeeProperty.member
|
||||
&& attendee.attendeeProperty.member.includes(group.uri)
|
||||
&& attendee.attendeeProperty.userType === 'INDIVIDUAL'
|
||||
})) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add attendee to group member list
|
||||
if (isMemberOfGroup) {
|
||||
this.groups.forEach(group => {
|
||||
if (attendee.member.includes(group.uri)) {
|
||||
if (!group.members) {
|
||||
group.members = []
|
||||
}
|
||||
group.members.push(attendee)
|
||||
}
|
||||
})
|
||||
// Filter empty groups
|
||||
if (attendee.attendeeProperty.userType === 'GROUP') {
|
||||
return attendee.members.length > 0
|
||||
}
|
||||
|
||||
// Check if attendee is an empty group
|
||||
let isEmptyGroup = attendee.attendeeProperty.userType === 'GROUP'
|
||||
this.invitees.forEach(invitee => {
|
||||
if (invitee.member && invitee.member.includes(attendee.uri)) {
|
||||
isEmptyGroup = false
|
||||
}
|
||||
})
|
||||
|
||||
return attendee.uri !== this.calendarObjectInstance.organizer.uri
|
||||
&& !isMemberOfGroup && !isEmptyGroup
|
||||
})
|
||||
},
|
||||
isOrganizer() {
|
||||
|
@ -230,7 +223,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
addAttendee({ commonName, email, calendarUserType, language, timezoneId }) {
|
||||
addAttendee({ commonName, email, calendarUserType, language, timezoneId, member }) {
|
||||
this.$store.commit('addAttendee', {
|
||||
calendarObjectInstance: this.calendarObjectInstance,
|
||||
commonName,
|
||||
|
@ -242,6 +235,7 @@ export default {
|
|||
language,
|
||||
timezoneId,
|
||||
organizer: this.$store.getters.getCurrentUserPrincipal,
|
||||
member,
|
||||
})
|
||||
},
|
||||
removeAttendee(attendee) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
|
||||
- @copyright Copyright (c) 2023 Jonas Heinrich <heinrich@synyx.net>
|
||||
-
|
||||
- @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
- @author Jonas Heinrich <heinrich@synyx.net>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
|
@ -43,7 +45,12 @@
|
|||
:key="option.uid"
|
||||
:user="option.avatar"
|
||||
:display-name="option.dropdownName" />
|
||||
<Avatar v-else
|
||||
<Avatar v-else-if="option.type === 'circle'">
|
||||
<template #icon>
|
||||
<GoogleCirclesCommunitiesIcon :size="20" />
|
||||
</template>
|
||||
</Avatar>
|
||||
<Avatar v-if="!option.isUser && option.type !== 'circle'"
|
||||
:key="option.uid"
|
||||
:url="option.avatar"
|
||||
:display-name="option.dropdownName" />
|
||||
|
@ -52,9 +59,12 @@
|
|||
<div>
|
||||
{{ option.dropdownName }}
|
||||
</div>
|
||||
<div v-if="option.email !== option.dropdownName">
|
||||
<div v-if="option.email !== option.dropdownName && option.type !== 'circle'">
|
||||
{{ option.email }}
|
||||
</div>
|
||||
<div v-if="option.type === 'circle'">
|
||||
{{ option.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -67,33 +77,46 @@ import {
|
|||
NcMultiselect as Multiselect,
|
||||
} from '@nextcloud/vue'
|
||||
import { principalPropertySearchByDisplaynameOrEmail } from '../../../services/caldavService.js'
|
||||
import isCirclesEnabled from '../../../services/isCirclesEnabled.js'
|
||||
import {
|
||||
circleSearchByName,
|
||||
circleGetMembers,
|
||||
} from '../../../services/circleService.js'
|
||||
import HttpClient from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import { linkTo } from '@nextcloud/router'
|
||||
import { randomId } from '../../../utils/randomId.js'
|
||||
import GoogleCirclesCommunitiesIcon from 'vue-material-design-icons/GoogleCirclesCommunities.vue'
|
||||
import { showInfo } from '@nextcloud/dialogs'
|
||||
|
||||
export default {
|
||||
name: 'InviteesListSearch',
|
||||
components: {
|
||||
Avatar,
|
||||
Multiselect,
|
||||
GoogleCirclesCommunitiesIcon,
|
||||
},
|
||||
props: {
|
||||
alreadyInvitedEmails: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
organizer: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
inputGiven: false,
|
||||
matches: [],
|
||||
isCirclesEnabled,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
placeholder() {
|
||||
return this.$t('calendar', 'Search for emails, users or contacts')
|
||||
return this.$t('calendar', 'Search for emails, users, contacts or groups')
|
||||
},
|
||||
noResult() {
|
||||
return this.$t('calendar', 'No match found')
|
||||
|
@ -109,10 +132,16 @@ export default {
|
|||
this.findAttendeesFromContactsAPI(query),
|
||||
this.findAttendeesFromDAV(query),
|
||||
]
|
||||
if (isCirclesEnabled) {
|
||||
promises.push(this.findAttendeesFromCircles(query))
|
||||
}
|
||||
|
||||
const [contactsResults, davResults] = await Promise.all(promises)
|
||||
const [contactsResults, davResults, circleResults] = await Promise.all(promises)
|
||||
matches.push(...contactsResults)
|
||||
matches.push(...davResults)
|
||||
if (isCirclesEnabled) {
|
||||
matches.push(...circleResults)
|
||||
}
|
||||
|
||||
// Source of the Regex: https://stackoverflow.com/a/46181
|
||||
// eslint-disable-next-line
|
||||
|
@ -149,8 +178,30 @@ export default {
|
|||
this.matches = matches
|
||||
}, 500),
|
||||
addAttendee(selectedValue) {
|
||||
|
||||
if (selectedValue.type === 'circle') {
|
||||
showInfo(this.$t('calendar', 'Note that members of circles get invited but are not synced yet.'))
|
||||
this.resolveCircleMembers(selectedValue.id, selectedValue.email)
|
||||
}
|
||||
this.$emit('add-attendee', selectedValue)
|
||||
},
|
||||
async resolveCircleMembers(circleId, groupId) {
|
||||
let results
|
||||
try {
|
||||
// Going to query custom backend to fetch Circle members since we're going to use
|
||||
// mail addresses of local circle members. The Circles API doesn't expose member
|
||||
// emails yet. Change approved by @miaulalala and @ChristophWurst.
|
||||
results = await circleGetMembers(circleId)
|
||||
} catch (error) {
|
||||
console.debug(error)
|
||||
return []
|
||||
}
|
||||
results.data.forEach((member) => {
|
||||
if (!this.organizer || member.email !== this.organizer.uri) {
|
||||
this.$emit('add-attendee', member)
|
||||
}
|
||||
})
|
||||
},
|
||||
async findAttendeesFromContactsAPI(query) {
|
||||
let response
|
||||
|
||||
|
@ -239,6 +290,30 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
async findAttendeesFromCircles(query) {
|
||||
let results
|
||||
try {
|
||||
results = await circleSearchByName(query)
|
||||
} catch (error) {
|
||||
console.debug(error)
|
||||
return []
|
||||
}
|
||||
|
||||
return results.filter((circle) => {
|
||||
return true
|
||||
}).map((circle) => {
|
||||
return {
|
||||
commonName: circle.displayname,
|
||||
calendarUserType: 'GROUP',
|
||||
email: 'circle+' + circle.id + '@' + circle.instance,
|
||||
isUser: false,
|
||||
dropdownName: circle.displayname,
|
||||
type: 'circle',
|
||||
id: circle.id,
|
||||
subtitle: this.$n('calendar', '%n member', '%n members', circle.population),
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 Jonas Heinrich
|
||||
*
|
||||
* @author Jonas Heinrich <heinrich@synyx.net>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import HttpClient from '@nextcloud/axios'
|
||||
import {
|
||||
generateOcsUrl,
|
||||
linkTo,
|
||||
} from '@nextcloud/router'
|
||||
|
||||
/**
|
||||
* Finds circles by displayname
|
||||
*
|
||||
* @param {string} query The search-term
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
const circleSearchByName = async (query) => {
|
||||
let results
|
||||
try {
|
||||
results = await HttpClient.get(generateOcsUrl('apps/files_sharing/api/v1/') + 'sharees', {
|
||||
params: {
|
||||
format: 'json',
|
||||
search: query,
|
||||
perPage: 200,
|
||||
itemType: 'pringroucipals',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (results.data.ocs.meta.status === 'failure') {
|
||||
return []
|
||||
}
|
||||
|
||||
let circles = []
|
||||
if (Array.isArray(results.data.ocs.data.circles)) {
|
||||
circles = circles.concat(results.data.ocs.data.circles)
|
||||
}
|
||||
if (Array.isArray(results.data.ocs.data.exact.circles)) {
|
||||
circles = circles.concat(results.data.ocs.data.exact.circles)
|
||||
}
|
||||
|
||||
if (circles.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return circles.filter((circle) => {
|
||||
return true
|
||||
}).map(circle => ({
|
||||
displayname: circle.label,
|
||||
population: circle.value.circle.population,
|
||||
id: circle.value.circle.id,
|
||||
instance: circle.value.circle.owner.instance,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get members of circle by id
|
||||
*
|
||||
* @param {string} circleId The circle id to query
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
const circleGetMembers = async (circleId) => {
|
||||
let results
|
||||
try {
|
||||
results = await HttpClient.get(linkTo('calendar', 'index.php') + '/v1/circles/getmembers', {
|
||||
params: {
|
||||
format: 'json',
|
||||
circleId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.debug(error)
|
||||
return []
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export {
|
||||
circleSearchByName,
|
||||
circleGetMembers,
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @copyright Copyright (c) 2023 Jonas Heinrich <heinrich@synyx.net>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Jonas Heinrich <heinrich@synyx.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
const isCirclesEnabled = loadState('calendar', 'isCirclesEnabled', false)
|
||||
export default isCirclesEnabled
|
|
@ -431,7 +431,7 @@ const mutations = {
|
|||
* @param {string=} data.timezoneId Preferred timezone of the attendee
|
||||
* @param {object=} data.organizer Principal of the organizer to be set if not present
|
||||
*/
|
||||
addAttendee(state, { calendarObjectInstance, commonName, uri, calendarUserType = null, participationStatus = null, role = null, rsvp = null, language = null, timezoneId = null, organizer = null }) {
|
||||
addAttendee(state, { calendarObjectInstance, commonName, uri, calendarUserType = null, participationStatus = null, role = null, rsvp = null, language = null, timezoneId = null, organizer = null, member = null }) {
|
||||
const attendee = AttendeeProperty.fromNameAndEMail(commonName, uri)
|
||||
|
||||
if (calendarUserType !== null) {
|
||||
|
@ -452,6 +452,9 @@ const mutations = {
|
|||
if (timezoneId !== null) {
|
||||
attendee.updateParameterIfExist('TZID', timezoneId)
|
||||
}
|
||||
if (member !== null) {
|
||||
attendee.updateParameterIfExist('MEMBER', member)
|
||||
}
|
||||
|
||||
// TODO - use real addAttendeeFrom method
|
||||
calendarObjectInstance.eventComponent.addProperty(attendee)
|
||||
|
|
|
@ -24,9 +24,11 @@ declare(strict_types=1);
|
|||
namespace OCA\Calendar\Controller;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\Contacts\IManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class ContactControllerTest extends TestCase {
|
||||
|
@ -39,6 +41,12 @@ class ContactControllerTest extends TestCase {
|
|||
/** @var IManager|MockObject */
|
||||
protected $manager;
|
||||
|
||||
/** @var IAppManager|MockObject */
|
||||
private $appManager;
|
||||
|
||||
/** @var IUserManager|MockObject */
|
||||
private $userManager;
|
||||
|
||||
/** @var ContactController */
|
||||
protected $controller;
|
||||
|
||||
|
@ -48,8 +56,10 @@ class ContactControllerTest extends TestCase {
|
|||
$this->appName = 'calendar';
|
||||
$this->request = $this->createMock(IRequest::class);
|
||||
$this->manager = $this->createMock(IManager::class);
|
||||
$this->appManager = $this->createMock(IAppManager::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->controller = new ContactController($this->appName,
|
||||
$this->request, $this->manager);
|
||||
$this->request, $this->manager, $this->appManager, $this->userManager);
|
||||
}
|
||||
|
||||
public function testSearchLocationDisabled():void {
|
||||
|
|
|
@ -27,6 +27,7 @@ declare(strict_types=1);
|
|||
namespace OCA\Calendar\Controller;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use OC\App\CompareVersion;
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
use OCA\Calendar\Service\Appointments\AppointmentConfigService;
|
||||
use OCP\App\IAppManager;
|
||||
|
@ -65,6 +66,9 @@ class ViewControllerTest extends TestCase {
|
|||
/** @var IAppData|MockObject */
|
||||
private $appData;
|
||||
|
||||
/** @var CompareVersion|MockObject*/
|
||||
private $compareVersion;
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->appName = 'calendar';
|
||||
$this->request = $this->createMock(IRequest::class);
|
||||
|
@ -72,6 +76,7 @@ class ViewControllerTest extends TestCase {
|
|||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->appointmentContfigService = $this->createMock(AppointmentConfigService::class);
|
||||
$this->initialStateService = $this->createMock(IInitialState::class);
|
||||
$this->compareVersion = $this->createMock(CompareVersion::class);
|
||||
$this->userId = 'user123';
|
||||
$this->appData = $this->createMock(IAppData::class);
|
||||
|
||||
|
@ -82,6 +87,7 @@ class ViewControllerTest extends TestCase {
|
|||
$this->appointmentContfigService,
|
||||
$this->initialStateService,
|
||||
$this->appManager,
|
||||
$this->compareVersion,
|
||||
$this->userId,
|
||||
$this->appData,
|
||||
);
|
||||
|
@ -122,22 +128,24 @@ class ViewControllerTest extends TestCase {
|
|||
['user123', 'calendar', 'defaultReminder', 'defaultDefaultReminder', '00:10:00'],
|
||||
['user123', 'calendar', 'showTasks', 'defaultShowTasks', '00:15:00'],
|
||||
]);
|
||||
$this->appManager->expects(self::exactly(2))
|
||||
$this->appManager->expects(self::exactly(3))
|
||||
->method('isEnabledForUser')
|
||||
->willReturnMap([
|
||||
['spreed', null, true],
|
||||
['tasks', null, true]
|
||||
['tasks', null, true],
|
||||
['circles', null, false],
|
||||
]);
|
||||
$this->appManager->expects(self::once())
|
||||
$this->appManager->expects(self::exactly(2))
|
||||
->method('getAppVersion')
|
||||
->willReturnMap([
|
||||
['spreed', true, '12.0.0'],
|
||||
['circles', true, '22.0.0'],
|
||||
]);
|
||||
$this->appointmentContfigService->expects(self::once())
|
||||
->method('getAllAppointmentConfigurations')
|
||||
->with($this->userId)
|
||||
->willReturn([new AppointmentConfig()]);
|
||||
$this->initialStateService->expects(self::exactly(21))
|
||||
$this->initialStateService->expects(self::exactly(22))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
['app_version', '1.0.0'],
|
||||
|
@ -161,6 +169,7 @@ class ViewControllerTest extends TestCase {
|
|||
['disable_appointments', false],
|
||||
['can_subscribe_link', false],
|
||||
['show_resources', true],
|
||||
['isCirclesEnabled', false],
|
||||
);
|
||||
|
||||
$response = $this->controller->index();
|
||||
|
@ -212,22 +221,24 @@ class ViewControllerTest extends TestCase {
|
|||
['user123', 'calendar', 'defaultReminder', 'defaultDefaultReminder', '00:10:00'],
|
||||
['user123', 'calendar', 'showTasks', 'defaultShowTasks', '00:15:00'],
|
||||
]);
|
||||
$this->appManager->expects(self::exactly(2))
|
||||
$this->appManager->expects(self::exactly(3))
|
||||
->method('isEnabledForUser')
|
||||
->willReturnMap([
|
||||
['spreed', null, false],
|
||||
['tasks', null, false]
|
||||
['tasks', null, false],
|
||||
['circles', null, false],
|
||||
]);
|
||||
$this->appManager->expects(self::once())
|
||||
$this->appManager->expects(self::exactly(2))
|
||||
->method('getAppVersion')
|
||||
->willReturnMap([
|
||||
['spreed', true, '11.3.0'],
|
||||
['circles', true, '22.0.0'],
|
||||
]);
|
||||
$this->appointmentContfigService->expects(self::once())
|
||||
->method('getAllAppointmentConfigurations')
|
||||
->with($this->userId)
|
||||
->willReturn([new AppointmentConfig()]);
|
||||
$this->initialStateService->expects(self::exactly(21))
|
||||
$this->initialStateService->expects(self::exactly(22))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
['app_version', '1.0.0'],
|
||||
|
@ -251,6 +262,7 @@ class ViewControllerTest extends TestCase {
|
|||
['disable_appointments', false],
|
||||
['can_subscribe_link', false],
|
||||
['show_resources', true],
|
||||
['isCirclesEnabled', false],
|
||||
);
|
||||
|
||||
$response = $this->controller->index();
|
||||
|
|
Loading…
Reference in New Issue