Add calendar sharing

This commit is contained in:
tchapi 2020-12-17 15:16:35 +01:00
parent b84a9590ea
commit 363e014e6c
7 changed files with 235 additions and 24 deletions

View File

@ -17,12 +17,52 @@ $(document).ready(function() {
$('#delete-' + modalFlavour).modal('show');
})
// "Sharing settings" modals
$('a.share-modal').click(function() {
// Grab calendar shares url and add url
let shareesUrl = $(this).attr('data-sharees-href');
let targetUrl = $(this).attr('data-href');
// Put it into the modal's OK button
$('#share .add-sharee').attr('data-href', targetUrl);
// Get calendar shares
$.get(shareesUrl, function(data) {
// Catch error TODO
$('#shares').empty()
if (data.length === 0) {
$('.none').removeClass('d-none')
} else {
$('.none').addClass('d-none')
data.forEach(element => {
const newShare = $($('#template-share').html())
newShare.find('span.name').text(element.displayName)
newShare.find('span.badge').text(element.accessText)
console.log(element.isWriteAccess)
if (element.isWriteAccess) {
newShare.find('span.badge').addClass('badge-success').removeClass('badge-info')
}
newShare.appendTo($('#shares'));
});
}
})
// Show the modal
$('#share').modal('show');
})
// Color swatch : update it live (not working in IE ¯\_(ツ)_/¯ but it's just a nice to have)
$('#calendar_instance_calendarColor').keyup(function() {
document.body.style.setProperty('--calendar-color', $(this).val());
})
document.body.style.setProperty('--calendar-color', $('#calendar_instance_calendarColor').val());
// Modal to add a sharee on a calendar, catch the click to add the query parameter
$('a.add-sharee').click(function(e) {
e.preventDefault()
window.location = $(this).attr('data-href') + "?principalId=" + $("#member").val() + "&write=" + ($("#write").is(':checked') ? 'true' : 'false')
})
// Modal to add delegate, catch the click to add the query parameter
$('a.add-delegate').click(function(e) {
e.preventDefault()

View File

@ -16,6 +16,7 @@ use App\Form\CalendarInstanceType;
use App\Form\UserType;
use App\Services\Utils;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -334,18 +335,23 @@ class AdminController extends AbstractController
// Separate shared calendars
$calendars = [];
$shared = [];
foreach($allCalendars as $calendar) {
if ($calendar->getAccess() === CalendarInstance::ACCESS_OWNER){
foreach ($allCalendars as $calendar) {
if (CalendarInstance::ACCESS_OWNER === $calendar->getAccess()) {
$calendars[] = $calendar;
} else {
$shared[] = $calendar;
}
}
// We need all the other users so we can propose to share calendars with them
$allPrincipalsExcept = $this->get('doctrine')->getRepository(Principal::class)->findAllExceptPrincipal(Principal::PREFIX.$username);
return $this->render('calendars/index.html.twig', [
'calendars' => $calendars,
'shared' => $shared,
'principal' => $principal,
'username' => $username,
'allPrincipals' => $allPrincipalsExcept,
]);
}
@ -374,7 +380,7 @@ class AdminController extends AbstractController
$form = $this->createForm(CalendarInstanceType::class, $calendarInstance, [
'new' => !$id,
'shared' => $calendarInstance->getAccess() !== CalendarInstance::ACCESS_OWNER
'shared' => CalendarInstance::ACCESS_OWNER !== $calendarInstance->getAccess(),
]);
$components = explode(',', $calendarInstance->getCalendar()->getComponents());
@ -418,6 +424,71 @@ class AdminController extends AbstractController
]);
}
/**
* @Route("/calendars/{username}/shares/{calendarid}", name="calendar_shares", requirements={"calendarid":"\d+"})
*/
public function calendarShares(string $username, string $calendarid, TranslatorInterface $trans)
{
$instances = $this->get('doctrine')->getRepository(CalendarInstance::class)->findSharedInstancesOfInstance($calendarid);
$response = [];
foreach ($instances as $instance) {
$response[] = [
'principalUri' => stream_get_contents($instance[0]['principalUri']),
'displayName' => $instance['displayName'],
'email' => stream_get_contents($instance['email']),
'accessText' => $trans->trans('calendar.share_access.'.$instance[0]['access']),
'isWriteAccess' => CalendarInstance::ACCESS_READWRITE === $instance[0]['access'],
];
}
return new JsonResponse($response);
}
/**
* @Route("/calendars/{username}/share/{instanceid}", name="calendar_share_add", requirements={"instanceid":"\d+"})
*/
public function calendarShareAdd(Request $request, string $username, string $instanceid, TranslatorInterface $trans)
{
$instance = $this->get('doctrine')->getRepository(CalendarInstance::class)->findOneById($instanceid);
if (!$instance) {
throw $this->createNotFoundException('Calendar not found');
}
$newShareeToAdd = $this->get('doctrine')->getRepository(Principal::class)->findOneById($request->get('principalId'));
if (!$newShareeToAdd) {
throw $this->createNotFoundException('Member not found');
}
// Let's check that there wasn't another instance
// already existing first, so we can update it:
$existingSharedInstance = $this->get('doctrine')->getRepository(CalendarInstance::class)->findSharedInstanceOfInstanceFor($instance->getCalendar()->getId(), $newShareeToAdd->getUri());
$writeAccess = ('true' === $request->get('write') ? CalendarInstance::ACCESS_READWRITE : CalendarInstance::ACCESS_READ);
$entityManager = $this->get('doctrine')->getManager();
if ($existingSharedInstance) {
$existingSharedInstance->setAccess($writeAccess);
} else {
$sharedInstance = new CalendarInstance();
$sharedInstance->setTransparent(1)
->setCalendar($instance->getCalendar())
->setShareHref('mailto:'.$newShareeToAdd->getEmail())
->setDescription($instance->getDescription())
->setDisplayName($instance->getDisplayName())
->setUri(\Sabre\DAV\UUIDUtil::getUUID())
->setPrincipalUri($newShareeToAdd->getUri())
->setAccess($writeAccess);
$entityManager->persist($sharedInstance);
}
$entityManager->flush();
$this->addFlash('success', $trans->trans('calendar.shared'));
return $this->redirectToRoute('calendars', ['username' => $username]);
}
/**
* @Route("/calendars/{username}/delete/{id}", name="calendar_delete", requirements={"id":"\d+"})
*/

View File

@ -3,6 +3,7 @@
namespace App\Repository;
use App\Entity\CalendarInstance;
use App\Entity\Principal;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -19,32 +20,35 @@ class CalendarInstanceRepository extends ServiceEntityRepository
parent::__construct($registry, CalendarInstance::class);
}
// /**
// * @return CalendarInstance[] Returns an array of CalendarInstance objects
// */
/*
public function findByExampleField($value)
/**
* @return CalendarInstance[] Returns an array of CalendarInstance objects
*/
public function findSharedInstancesOfInstance(int $calendarId)
{
return $this->createQueryBuilder('c')
->andWhere('c.exampleField = :val')
->setParameter('val', $value)
->orderBy('c.id', 'ASC')
->setMaxResults(10)
->leftJoin(Principal::class, 'p', \Doctrine\ORM\Query\Expr\Join::WITH, 'c.principalUri = p.uri')
->addSelect('p.displayName', 'p.email')
->where('c.calendar = :id')
->setParameter('id', $calendarId)
->andWhere('c.access != :ownerAccess')
->setParameter('ownerAccess', CalendarInstance::ACCESS_OWNER)
->getQuery()
->getResult()
;
->getArrayResult();
}
*/
/*
public function findOneBySomeField($value): ?CalendarInstance
/**
* @return CalendarInstance Returns a CalendarInstance object
*/
public function findSharedInstanceOfInstanceFor(int $calendarId, string $principalUri)
{
return $this->createQueryBuilder('c')
->andWhere('c.exampleField = :val')
->setParameter('val', $value)
->where('c.calendar = :id')
->setParameter('id', $calendarId)
->andWhere('c.access != :ownerAccess')
->setParameter('ownerAccess', CalendarInstance::ACCESS_OWNER)
->andWhere('c.principalUri = :principalUri')
->setParameter('principalUri', $principalUri)
->getQuery()
->getOneOrNullResult()
;
->getOneOrNullResult();
}
*/
}

View File

@ -16,6 +16,7 @@
<option value="{{ principal.id}}">{{ principal.displayName }} ({{ principal.email }})</option>
{% endfor %}
</select>
<small class="form-text text-muted">{{ "delegates.member.help"|trans }}</small>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="write">

View File

@ -0,0 +1,48 @@
<div class="modal fade" id="share" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ "calendars.delegates.new"|trans }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h4>{{ "calendars.delegates.existing"|trans }}</h4>
<template id="template-share">
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<span class="name"></span>
<span class="badge badge-info badge-pill ml-2"></span>
</div>
<a href="#" class="btn btn-sm btn-danger">{{ "revoke"|trans }}</a>
</div>
</template>
<span class="d-none none"><em>{{ "calendars.delegates.none"|trans }}</em></span>
<ul class="list-group" id="shares">
</ul>
<form class="mt-2">
<div class="form-group">
<label for="member" class="col-form-label">{{ "calendars.delegates.member.add"|trans }}</label>
<select class="form-control" id="member">
{% for principal in principals %}
<option value="{{ principal.id}}">{{ principal.displayName }} ({{ principal.email }})</option>
{% endfor %}
</select>
<small class="form-text text-muted">{{ "calendars.delegates.member.help"|trans }}</small>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="write">
<label class="form-check-label" for="write">
{{ "calendars.delegates.write.give"|trans }}
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ "cancel"|trans }}</button>
<a href="#" class="btn btn-primary add-sharee">{{ "add"|trans }}</a>
</div>
</div>
</div>
</div>

View File

@ -13,6 +13,7 @@
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 mr-auto">{{ calendar.displayName }} <span class="badge badge-pill" style="background-color: {{ calendar.calendarColor }}">&nbsp;</span></h5>
<div class="mr-0 text-right d-md-block d-none">
<a href="#" data-sharees-href="{{ path('calendar_shares',{username: username, calendarid: calendar.calendar.id})}}" data-href="{{ path('calendar_share_add', {username: principal.username, instanceid: calendar.id}) }}" class="btn btn-sm btn-outline-info ml-1 share-modal">🔗 {{ "sharing"|trans }}</a>
<a href="{{ path('calendar_edit',{username: username, id: calendar.id})}}" class="btn btn-sm btn-outline-primary ml-1">✎ {{ "edit"|trans }}</a>
<a href="#" data-href="{{ path('calendar_delete',{username: username, id: calendar.id})}}" data-flavour="calendars" class="btn btn-sm btn-outline-danger ml-1 delete-modal">⚠ {{ "delete"|trans }}</a>
</div>
@ -27,6 +28,7 @@
{{ "calendars.entries"|trans({'%count%': calendar.calendar.objects|length}) }}
</small>
<div class="btn-group btn-group-sm mt-3 d-flex d-md-none" role="group">
<a href="#" data-sharees-href="{{ path('calendar_shares',{username: username, calendarid: calendar.calendar.id})}}" data-href="{{ path('calendar_share_add', {username: principal.username, instanceid: calendar.id}) }}" class="btn btn-outline-info share-modal">🔗 {{ "sharing"|trans }}</a>
<a href="{{ path('calendar_edit',{username: username, id: calendar.id})}}" class="btn btn-outline-primary">✎ {{ "edit"|trans }}</a>
<a href="#" data-href="{{ path('calendar_delete',{username: username, id: calendar.id})}}" data-flavour="calendars" class="btn btn-outline-danger delete-modal">⚠ {{ "delete"|trans }}</a>
</div>
@ -41,7 +43,15 @@
{% for calendar in shared %}
<div class="list-group-item list-group-item-action p-3">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 mr-auto">{{ calendar.displayName }} <span class="badge badge-pill" style="background-color: {{ calendar.calendarColor }}">&nbsp;</span></h5>
<h5 class="mb-1 mr-auto">
{{ calendar.displayName }}
{% if calendar.access == constant('\\App\\Entity\\CalendarInstance::ACCESS_READWRITE') %}
<span class="badge badge-success ml-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>
{% else %}
<span class="badge badge-info ml-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>
{% endif %}
<span class="badge badge-pill" style="background-color: {{ calendar.calendarColor }}">&nbsp;</span>
</h5>
<div class="mr-0 text-right d-md-block d-none">
<a href="{{ path('calendar_edit',{username: username, id: calendar.id})}}" class="btn btn-sm btn-outline-primary ml-1">✎ {{ "edit"|trans }}</a>
<a href="#" data-href="{{ path('calendar_revoke',{username: username, id: calendar.id})}}" data-flavour="revoke" class="btn btn-sm btn-outline-danger ml-1 delete-modal">🚫 {{ "revoke"|trans }}</a>
@ -67,6 +77,7 @@
{% include '_partials/delete_modal.html.twig' with {flavour: 'revoke'} %}
{% endif %}
{% include '_partials/share_modal.html.twig' with {principals: allPrincipals} %}
{% include '_partials/delete_modal.html.twig' with {flavour: 'calendars'} %}
{% endblock %}

View File

@ -245,10 +245,22 @@
<source>calendar.deleted</source>
<target>Calendar deleted successfully</target>
</trans-unit>
<trans-unit id="qCIo2jM" resname="calendar.shared">
<source>calendar.shared</source>
<target>Calendar shares modified successfully</target>
</trans-unit>
<trans-unit id="fxgFe6c" resname="calendar.revoked">
<source>calendar.revoked</source>
<target>Calendar revoked successfully</target>
</trans-unit>
<trans-unit id="grS2E58" resname="calendars.delegates.member.add">
<source>calendars.delegates.member.add</source>
<target>Share this calendar with another user:</target>
</trans-unit>
<trans-unit id="B95edtg" resname="calendars.delegates.member.help">
<source>calendars.delegates.member.help</source>
<target>Adding a user who already has a shared access to this calendar will only affect its access right</target>
</trans-unit>
<trans-unit id="Iq2tnfk" resname="addressbooks.saved">
<source>addressbooks.saved</source>
<target>Address Book saved successfully</target>
@ -399,7 +411,11 @@
</trans-unit>
<trans-unit id="QhzQj3K" resname="delegates.modal.text">
<source>delegates.modal.text</source>
<target>Are you sure you want to remove this delegate ? This user will no longer have access to the calendars.</target>
<target>Are you sure you want to remove this delegate ? This user will no longer have access to the calendars, contacts, etc.</target>
</trans-unit>
<trans-unit id="bsFHDY4" resname="delegates.member.help">
<source>delegates.member.help</source>
<target>Adding a user who already is a delegate will only affect its access right</target>
</trans-unit>
<trans-unit id="I3TZF5S" resname="cancel">
<source>cancel</source>
@ -445,6 +461,10 @@
<source>revoke</source>
<target>Revoke</target>
</trans-unit>
<trans-unit id="efo4jbl" resname="sharing">
<source>sharing</source>
<target>Sharing</target>
</trans-unit>
<trans-unit id="fWFSd1u" resname="calendars.delegates.add">
<source>calendars.delegates.add</source>
<target>Add a delegate</target>
@ -457,6 +477,14 @@
<source>calendars.delegates.new</source>
<target>New delegate</target>
</trans-unit>
<trans-unit id="MLydEYK" resname="calendars.delegates.existing">
<source>calendars.delegates.existing</source>
<target>This calendar is shared with:</target>
</trans-unit>
<trans-unit id="BROOTDT" resname="calendars.delegates.none">
<source>calendars.delegates.none</source>
<target>none</target>
</trans-unit>
<trans-unit id="wPVq0sd" resname="calendars.delegates.member">
<source>calendars.delegates.member</source>
<target>Member:</target>
@ -497,6 +525,14 @@
<source>delegates.readonly</source>
<target>has readonly access</target>
</trans-unit>
<trans-unit id="pnQau7c" resname="calendar.share_access.2">
<source>calendar.share_access.2</source>
<target>readonly</target>
</trans-unit>
<trans-unit id="BuDoAdU" resname="calendar.share_access.3">
<source>calendar.share_access.3</source>
<target>read / write</target>
</trans-unit>
</body>
</file>
</xliff>