diff --git a/code_samples/collaboration/CollaborationController.php b/code_samples/collaboration/CollaborationController.php new file mode 100644 index 0000000000..a60c357746 --- /dev/null +++ b/code_samples/collaboration/CollaborationController.php @@ -0,0 +1,261 @@ +contentService->loadContent($contentId); + + $requestData = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); + + $sessionStruct = new SessionCreateStruct(); + $sessionStruct->contentId = $contentId; + $sessionStruct->name = $requestData['name'] ?? 'Collaboration Session'; + + if (isset($requestData['expires_at'])) { + $sessionStruct->expiresAt = new \DateTime($requestData['expires_at']); + } + + $session = $this->collaborationService->createSession($sessionStruct); + + return $this->json([ + 'success' => true, + 'session' => [ + 'id' => $session->id, + 'name' => $session->name, + 'status' => $session->status, + 'content_id' => $session->contentId, + 'created_at' => $session->createdAt->format('c'), + ], + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => $e->getMessage(), + ], Response::HTTP_BAD_REQUEST); + } + } + + #[Route('/collaboration/session/{sessionId}/invite', name: 'collaboration_invite_user', methods: ['POST'])] + public function inviteUser(int $sessionId, Request $request): JsonResponse + { + try { + $session = $this->collaborationService->getSession($sessionId); + $requestData = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); + + $invitationStruct = new InvitationCreateStruct(); + $invitationStruct->sessionId = $sessionId; + $invitationStruct->email = $requestData['email']; + $invitationStruct->role = $requestData['role'] ?? 'editor'; + $invitationStruct->message = $requestData['message'] ?? ''; + + if (isset($requestData['expires_at'])) { + $invitationStruct->expiresAt = new \DateTime($requestData['expires_at']); + } + + $invitation = $this->invitationService->createInvitation($invitationStruct); + + return $this->json([ + 'success' => true, + 'invitation' => [ + 'id' => $invitation->id, + 'email' => $invitation->email, + 'role' => $invitation->role, + 'status' => $invitation->status, + 'created_at' => $invitation->createdAt->format('c'), + ], + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => $e->getMessage(), + ], Response::HTTP_BAD_REQUEST); + } + } + + #[Route('/collaboration/session/{sessionId}/participants', name: 'collaboration_get_participants', methods: ['GET'])] + public function getParticipants(int $sessionId): JsonResponse + { + try { + $participants = $this->participantService->getParticipants($sessionId); + + $participantData = []; + foreach ($participants as $participant) { + $user = $this->userService->loadUser($participant->userId); + + $participantData[] = [ + 'id' => $participant->id, + 'user_id' => $participant->userId, + 'user_name' => $user->getName(), + 'role' => $participant->role, + 'permissions' => $participant->permissions, + 'joined_at' => $participant->joinedAt->format('c'), + 'last_activity' => $participant->lastActivity?->format('c'), + ]; + } + + return $this->json([ + 'success' => true, + 'participants' => $participantData, + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => $e->getMessage(), + ], Response::HTTP_BAD_REQUEST); + } + } + + #[Route('/collaboration/session/{sessionId}/join', name: 'collaboration_join_session', methods: ['POST'])] + public function joinSession(int $sessionId): JsonResponse + { + try { + $session = $this->collaborationService->getSession($sessionId); + $currentUser = $this->userService->loadUser($this->getUser()->getUserIdentifier()); + + $participantStruct = new ParticipantCreateStruct(); + $participantStruct->userId = $currentUser->id; + $participantStruct->role = 'editor'; + $participantStruct->permissions = [ + 'edit' => true, + 'comment' => true, + 'invite' => false, + ]; + + $participant = $this->participantService->addParticipant($session, $participantStruct); + + return $this->json([ + 'success' => true, + 'participant' => [ + 'id' => $participant->id, + 'role' => $participant->role, + 'permissions' => $participant->permissions, + 'joined_at' => $participant->joinedAt->format('c'), + ], + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => $e->getMessage(), + ], Response::HTTP_BAD_REQUEST); + } + } + + #[Route('/collaboration/session/{sessionId}/end', name: 'collaboration_end_session', methods: ['POST'])] + public function endSession(int $sessionId): JsonResponse + { + try { + $this->collaborationService->endSession($sessionId); + + return $this->json([ + 'success' => true, + 'message' => 'Session ended successfully', + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => $e->getMessage(), + ], Response::HTTP_BAD_REQUEST); + } + } + + #[Route('/collaboration/my-sessions', name: 'collaboration_my_sessions', methods: ['GET'])] + public function getMySessions(): JsonResponse + { + try { + $currentUser = $this->userService->loadUser($this->getUser()->getUserIdentifier()); + + // Get sessions where user is owner + $query = new \Ibexa\Contracts\Collaboration\Values\Session\Query\SessionQuery(); + $query->filter = new \Ibexa\Contracts\Collaboration\Values\Session\Query\Criterion\OwnerId($currentUser->id); + $query->sortClauses = [ + new \Ibexa\Contracts\Collaboration\Values\Session\Query\SortClause\UpdatedAt(\Ibexa\Contracts\Core\Repository\Values\Content\Query::SORT_DESC), + ]; + + $ownedSessions = $this->collaborationService->findSessions($query); + + // Get sessions where user is participant + $participantQuery = new \Ibexa\Contracts\Collaboration\Values\Session\Query\SessionQuery(); + $participantQuery->filter = new \Ibexa\Contracts\Collaboration\Values\Session\Query\Criterion\ParticipantId($currentUser->id); + + $participantSessions = $this->collaborationService->findSessions($participantQuery); + + return $this->json([ + 'success' => true, + 'owned_sessions' => $this->formatSessionsData($ownedSessions->sessions), + 'participant_sessions' => $this->formatSessionsData($participantSessions->sessions), + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'error' => $e->getMessage(), + ], Response::HTTP_BAD_REQUEST); + } + } + + private function formatSessionsData(array $sessions): array + { + $data = []; + + foreach ($sessions as $session) { + try { + $content = $this->contentService->loadContent($session->contentId); + + $data[] = [ + 'id' => $session->id, + 'name' => $session->name, + 'status' => $session->status, + 'content' => [ + 'id' => $content->id, + 'name' => $content->getName(), + 'content_type' => $content->getContentType()->getName(), + ], + 'created_at' => $session->createdAt->format('c'), + 'expires_at' => $session->expiresAt?->format('c'), + 'participant_count' => count($this->participantService->getParticipants($session->id)), + ]; + } catch (\Exception $e) { + // Skip sessions with inaccessible content + continue; + } + } + + return $data; + } +} \ No newline at end of file diff --git a/code_samples/collaboration/CollaborationEventSubscriber.php b/code_samples/collaboration/CollaborationEventSubscriber.php new file mode 100644 index 0000000000..8ac37c6ba4 --- /dev/null +++ b/code_samples/collaboration/CollaborationEventSubscriber.php @@ -0,0 +1,306 @@ + ['onSessionCreated', 10], + SessionEndedEvent::class => ['onSessionEnded', 10], + ParticipantAddedEvent::class => ['onParticipantAdded', 10], + ParticipantRemovedEvent::class => ['onParticipantRemoved', 10], + InvitationCreatedEvent::class => ['onInvitationCreated', 10], + InvitationAcceptedEvent::class => ['onInvitationAccepted', 10], + ]; + } + + public function onSessionCreated(SessionCreatedEvent $event): void + { + $session = $event->getSession(); + + try { + $content = $this->contentService->loadContent($session->contentId); + $owner = $this->userService->loadUser($session->ownerId); + + $this->logger->info('Collaboration session created', [ + 'session_id' => $session->id, + 'content_id' => $session->contentId, + 'content_name' => $content->getName(), + 'owner_id' => $session->ownerId, + 'owner_name' => $owner->getName(), + ]); + + // Send notification email to owner + $this->sendSessionCreatedEmail($session, $owner, $content); + + } catch (\Exception $e) { + $this->logger->error('Error handling session created event', [ + 'session_id' => $session->id, + 'error' => $e->getMessage(), + ]); + } + } + + public function onSessionEnded(SessionEndedEvent $event): void + { + $session = $event->getSession(); + + try { + $this->logger->info('Collaboration session ended', [ + 'session_id' => $session->id, + 'content_id' => $session->contentId, + 'duration' => $session->createdAt->diff(new \DateTime())->format('%d days %h hours'), + ]); + + // Clean up session data if needed + $this->cleanupSessionData($session); + + } catch (\Exception $e) { + $this->logger->error('Error handling session ended event', [ + 'session_id' => $session->id, + 'error' => $e->getMessage(), + ]); + } + } + + public function onParticipantAdded(ParticipantAddedEvent $event): void + { + $participant = $event->getParticipant(); + $session = $event->getSession(); + + try { + $user = $this->userService->loadUser($participant->userId); + $content = $this->contentService->loadContent($session->contentId); + + $this->logger->info('Participant added to collaboration session', [ + 'session_id' => $session->id, + 'participant_id' => $participant->id, + 'user_id' => $participant->userId, + 'user_name' => $user->getName(), + 'role' => $participant->role, + ]); + + // Send welcome email to new participant + $this->sendParticipantWelcomeEmail($participant, $session, $user, $content); + + } catch (\Exception $e) { + $this->logger->error('Error handling participant added event', [ + 'session_id' => $session->id, + 'participant_id' => $participant->id, + 'error' => $e->getMessage(), + ]); + } + } + + public function onParticipantRemoved(ParticipantRemovedEvent $event): void + { + $participant = $event->getParticipant(); + $session = $event->getSession(); + + try { + $this->logger->info('Participant removed from collaboration session', [ + 'session_id' => $session->id, + 'participant_id' => $participant->id, + 'user_id' => $participant->userId, + 'role' => $participant->role, + ]); + + } catch (\Exception $e) { + $this->logger->error('Error handling participant removed event', [ + 'session_id' => $session->id, + 'participant_id' => $participant->id, + 'error' => $e->getMessage(), + ]); + } + } + + public function onInvitationCreated(InvitationCreatedEvent $event): void + { + $invitation = $event->getInvitation(); + + try { + $session = $event->getSession(); + $content = $this->contentService->loadContent($session->contentId); + $owner = $this->userService->loadUser($session->ownerId); + + $this->logger->info('Collaboration invitation created', [ + 'invitation_id' => $invitation->id, + 'session_id' => $invitation->sessionId, + 'email' => $invitation->email, + 'role' => $invitation->role, + ]); + + // Send invitation email + $this->sendInvitationEmail($invitation, $session, $owner, $content); + + } catch (\Exception $e) { + $this->logger->error('Error handling invitation created event', [ + 'invitation_id' => $invitation->id, + 'error' => $e->getMessage(), + ]); + } + } + + public function onInvitationAccepted(InvitationAcceptedEvent $event): void + { + $invitation = $event->getInvitation(); + $participant = $event->getParticipant(); + + try { + $session = $event->getSession(); + $user = $this->userService->loadUser($participant->userId); + + $this->logger->info('Collaboration invitation accepted', [ + 'invitation_id' => $invitation->id, + 'session_id' => $invitation->sessionId, + 'participant_id' => $participant->id, + 'user_name' => $user->getName(), + ]); + + // Notify session owner about accepted invitation + $this->sendInvitationAcceptedEmail($invitation, $session, $user); + + } catch (\Exception $e) { + $this->logger->error('Error handling invitation accepted event', [ + 'invitation_id' => $invitation->id, + 'error' => $e->getMessage(), + ]); + } + } + + private function sendSessionCreatedEmail($session, $owner, $content): void + { + try { + $email = (new Email()) + ->from($this->fromEmail) + ->to($owner->email) + ->subject('Collaboration Session Created') + ->html($this->twig->render('emails/collaboration/session_created.html.twig', [ + 'session' => $session, + 'owner' => $owner, + 'content' => $content, + ])); + + $this->mailer->send($email); + } catch (\Exception $e) { + $this->logger->error('Failed to send session created email', [ + 'session_id' => $session->id, + 'error' => $e->getMessage(), + ]); + } + } + + private function sendParticipantWelcomeEmail($participant, $session, $user, $content): void + { + try { + $email = (new Email()) + ->from($this->fromEmail) + ->to($user->email) + ->subject('Welcome to Collaboration Session') + ->html($this->twig->render('emails/collaboration/participant_welcome.html.twig', [ + 'participant' => $participant, + 'session' => $session, + 'user' => $user, + 'content' => $content, + ])); + + $this->mailer->send($email); + } catch (\Exception $e) { + $this->logger->error('Failed to send participant welcome email', [ + 'participant_id' => $participant->id, + 'error' => $e->getMessage(), + ]); + } + } + + private function sendInvitationEmail($invitation, $session, $owner, $content): void + { + try { + $invitationUrl = sprintf( + 'https://example.com/collaboration/invitation/accept/%s', + $invitation->token + ); + + $email = (new Email()) + ->from($this->fromEmail) + ->to($invitation->email) + ->subject(sprintf('Invitation to Collaborate on "%s"', $content->getName())) + ->html($this->twig->render('emails/collaboration/invitation.html.twig', [ + 'invitation' => $invitation, + 'session' => $session, + 'owner' => $owner, + 'content' => $content, + 'invitation_url' => $invitationUrl, + ])); + + $this->mailer->send($email); + } catch (\Exception $e) { + $this->logger->error('Failed to send invitation email', [ + 'invitation_id' => $invitation->id, + 'error' => $e->getMessage(), + ]); + } + } + + private function sendInvitationAcceptedEmail($invitation, $session, $user): void + { + try { + $owner = $this->userService->loadUser($session->ownerId); + + $email = (new Email()) + ->from($this->fromEmail) + ->to($owner->email) + ->subject('Collaboration Invitation Accepted') + ->html($this->twig->render('emails/collaboration/invitation_accepted.html.twig', [ + 'invitation' => $invitation, + 'session' => $session, + 'user' => $user, + 'owner' => $owner, + ])); + + $this->mailer->send($email); + } catch (\Exception $e) { + $this->logger->error('Failed to send invitation accepted email', [ + 'invitation_id' => $invitation->id, + 'error' => $e->getMessage(), + ]); + } + } + + private function cleanupSessionData($session): void + { + // Implement session cleanup logic here + // For example: remove temporary files, clear cache, etc. + $this->logger->info('Cleaning up session data', [ + 'session_id' => $session->id, + ]); + } +} \ No newline at end of file diff --git a/docs/api/event_reference/collaboration_events.md b/docs/api/event_reference/collaboration_events.md new file mode 100644 index 0000000000..dd9306d245 --- /dev/null +++ b/docs/api/event_reference/collaboration_events.md @@ -0,0 +1,340 @@ +--- +description: Events related to collaborative editing features, including sessions, participants, and invitations. +--- + +# Collaboration events + +Collaboration events are dispatched during various collaboration activities, allowing you to customize behavior and integrate with external systems. + +## Session events + +### Session lifecycle + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionCreateEvent` | Before creating a collaboration session | +| `Ibexa\Contracts\Collaboration\Event\Session\SessionCreatedEvent` | After a session is created | +| `Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionUpdateEvent` | Before updating session properties | +| `Ibexa\Contracts\Collaboration\Event\Session\SessionUpdatedEvent` | After session properties are updated | +| `Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionEndEvent` | Before ending a session | +| `Ibexa\Contracts\Collaboration\Event\Session\SessionEndedEvent` | After a session is ended | + +### Session activity + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Session\SessionExpiredEvent` | When a session expires automatically | +| `Ibexa\Contracts\Collaboration\Event\Session\SessionSuspendedEvent` | When a session is suspended | +| `Ibexa\Contracts\Collaboration\Event\Session\SessionResumedEvent` | When a suspended session is resumed | + +## Participant events + +### Participant management + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Participant\BeforeParticipantAddEvent` | Before adding a participant | +| `Ibexa\Contracts\Collaboration\Event\Participant\ParticipantAddedEvent` | After a participant is added | +| `Ibexa\Contracts\Collaboration\Event\Participant\BeforeParticipantRemoveEvent` | Before removing a participant | +| `Ibexa\Contracts\Collaboration\Event\Participant\ParticipantRemovedEvent` | After a participant is removed | +| `Ibexa\Contracts\Collaboration\Event\Participant\ParticipantUpdatedEvent` | When participant role or permissions change | + +### Participant activity + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Participant\ParticipantJoinedEvent` | When a participant joins an active session | +| `Ibexa\Contracts\Collaboration\Event\Participant\ParticipantLeftEvent` | When a participant leaves a session | +| `Ibexa\Contracts\Collaboration\Event\Participant\ParticipantActivityEvent` | When a participant performs an action | + +## Invitation events + +### Invitation lifecycle + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Invitation\InvitationCreatedEvent` | After an invitation is sent | +| `Ibexa\Contracts\Collaboration\Event\Invitation\InvitationAcceptedEvent` | When an invitation is accepted | +| `Ibexa\Contracts\Collaboration\Event\Invitation\InvitationDeclinedEvent` | When an invitation is declined | +| `Ibexa\Contracts\Collaboration\Event\Invitation\InvitationExpiredEvent` | When an invitation expires | +| `Ibexa\Contracts\Collaboration\Event\Invitation\InvitationCancelledEvent` | When an invitation is cancelled | + +### Invitation reminders + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Invitation\InvitationReminderEvent` | When sending invitation reminders | +| `Ibexa\Contracts\Collaboration\Event\Invitation\InvitationFollowUpEvent` | For follow-up actions on invitations | + +## Real-time editing events + +### Content synchronization + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\RealTime\ContentChangedEvent` | When content is modified during collaboration | +| `Ibexa\Contracts\Collaboration\Event\RealTime\ConflictDetectedEvent` | When conflicting changes are detected | +| `Ibexa\Contracts\Collaboration\Event\RealTime\ConflictResolvedEvent` | After conflicts are resolved | +| `Ibexa\Contracts\Collaboration\Event\RealTime\SyncCompletedEvent` | After content synchronization | + +### User presence + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\RealTime\ParticipantPresenceEvent` | When participant presence changes | +| `Ibexa\Contracts\Collaboration\Event\RealTime\CursorMovedEvent` | When a participant's cursor moves | +| `Ibexa\Contracts\Collaboration\Event\RealTime\UserConnectedEvent` | When a user connects to real-time session | +| `Ibexa\Contracts\Collaboration\Event\RealTime\UserDisconnectedEvent` | When a user disconnects from session | + +## Notification events + +### Email notifications + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Notification\EmailNotificationEvent` | Before sending email notifications | +| `Ibexa\Contracts\Collaboration\Event\Notification\EmailSentEvent` | After an email is sent | +| `Ibexa\Contracts\Collaboration\Event\Notification\EmailFailedEvent` | When email delivery fails | + +### System notifications + +| Event | Dispatched when | +|-------|----------------| +| `Ibexa\Contracts\Collaboration\Event\Notification\SystemNotificationEvent` | For in-app notifications | +| `Ibexa\Contracts\Collaboration\Event\Notification\NotificationReadEvent` | When a notification is read | +| `Ibexa\Contracts\Collaboration\Event\Notification\NotificationDismissedEvent` | When a notification is dismissed | + +## Usage examples + +### Basic event listener + +```php +use Ibexa\Contracts\Collaboration\Event\Session\SessionCreatedEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class CollaborationEventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + SessionCreatedEvent::class => 'onSessionCreated', + ]; + } + + public function onSessionCreated(SessionCreatedEvent $event): void + { + $session = $event->getSession(); + // Your custom logic here + } +} +``` + +### Event listener with attributes + +```php +use Ibexa\Contracts\Collaboration\Event\Participant\ParticipantAddedEvent; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + +class NotificationService +{ + #[AsEventListener(event: ParticipantAddedEvent::class)] + public function onParticipantAdded(ParticipantAddedEvent $event): void + { + $participant = $event->getParticipant(); + $session = $event->getSession(); + + // Send welcome notification + $this->sendWelcomeNotification($participant, $session); + } +} +``` + +### Preventable events + +```php +use Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionEndEvent; + +#[AsEventListener(event: BeforeSessionEndEvent::class)] +public function onBeforeSessionEnd(BeforeSessionEndEvent $event): void +{ + $session = $event->getSession(); + + // Check if session can be ended + if ($this->hasUnsavedChanges($session)) { + // Prevent the session from ending + $event->stopPropagation(); + + // Optionally set a reason + $event->setErrorMessage('Session has unsaved changes'); + } +} +``` + +## Event data access + +### Session events + +```php +public function onSessionCreated(SessionCreatedEvent $event): void +{ + $session = $event->getSession(); // CollaborationSession + $sessionStruct = $event->getSessionCreateStruct(); // SessionCreateStruct (in BeforeSessionCreateEvent) + + // Access session data + $sessionId = $session->id; + $contentId = $session->contentId; + $ownerId = $session->ownerId; + $status = $session->status; +} +``` + +### Participant events + +```php +public function onParticipantAdded(ParticipantAddedEvent $event): void +{ + $participant = $event->getParticipant(); // Participant + $session = $event->getSession(); // CollaborationSession + + // Access participant data + $userId = $participant->userId; + $role = $participant->role; + $permissions = $participant->permissions; +} +``` + +### Invitation events + +```php +public function onInvitationCreated(InvitationCreatedEvent $event): void +{ + $invitation = $event->getInvitation(); // Invitation + $session = $event->getSession(); // CollaborationSession + + // Access invitation data + $email = $invitation->email; + $role = $invitation->role; + $token = $invitation->token; + $expiresAt = $invitation->expiresAt; +} +``` + +## Integration with other systems + +### Workflow integration + +```php +use Ibexa\Contracts\Workflow\Event\Action\ActionExecutedEvent; +use Ibexa\Contracts\Collaboration\Event\Session\SessionCreatedEvent; + +#[AsEventListener(event: ActionExecutedEvent::class)] +public function onWorkflowAction(ActionExecutedEvent $event): void +{ + if ($event->getActionName() === 'collaboration_review') { + // Create collaboration session from workflow + $this->createCollaborationFromWorkflow($event->getWorkflowItem()); + } +} + +#[AsEventListener(event: SessionCreatedEvent::class)] +public function onSessionCreated(SessionCreatedEvent $event): void +{ + $session = $event->getSession(); + + // Update workflow if session was created from workflow + if (isset($session->metadata['workflow_item_id'])) { + $this->updateWorkflowProgress($session); + } +} +``` + +### Search integration + +```php +use Ibexa\Contracts\Collaboration\Event\Session\SessionEndedEvent; + +#[AsEventListener(event: SessionEndedEvent::class)] +public function onSessionEnded(SessionEndedEvent $event): void +{ + $session = $event->getSession(); + + // Update search index with collaboration metadata + $this->updateContentSearchIndex($session); +} +``` + +## Custom events + +You can create custom events for specific collaboration scenarios: + +```php +use Ibexa\Contracts\Collaboration\Event\AbstractCollaborationEvent; + +class CustomCollaborationEvent extends AbstractCollaborationEvent +{ + public function __construct( + private CollaborationSession $session, + private array $customData + ) { + } + + public function getSession(): CollaborationSession + { + return $this->session; + } + + public function getCustomData(): array + { + return $this->customData; + } +} + +// Dispatch custom event +$this->eventDispatcher->dispatch( + new CustomCollaborationEvent($session, $customData) +); +``` + +## Performance considerations + +### Async event processing + +For heavy operations, consider using message queues: + +```php +use Symfony\Component\Messenger\MessageBusInterface; + +#[AsEventListener(event: SessionCreatedEvent::class)] +public function onSessionCreated(SessionCreatedEvent $event): void +{ + // Queue heavy operations + $this->messageBus->dispatch(new ProcessSessionCreated($event->getSession()->id)); +} +``` + +### Event batching + +For high-frequency events, implement batching: + +```php +class BatchedEventProcessor +{ + private array $eventQueue = []; + + #[AsEventListener(event: ParticipantActivityEvent::class)] + public function queueParticipantActivity(ParticipantActivityEvent $event): void + { + $this->eventQueue[] = $event; + + if (count($this->eventQueue) >= 10) { + $this->processBatch(); + } + } +} +``` + +## See also + +- [Collaborative editing guide](../../content_management/collaborative_editing/collaborative_editing_guide.md) +- [Collaborative editing API](../../content_management/collaborative_editing/collaborative_editing_api.md) +- [Event reference](event_reference.md) \ No newline at end of file diff --git a/docs/content_management/collaborative_editing/collaborative_editing_api.md b/docs/content_management/collaborative_editing/collaborative_editing_api.md new file mode 100644 index 0000000000..5b5f2205c2 --- /dev/null +++ b/docs/content_management/collaborative_editing/collaborative_editing_api.md @@ -0,0 +1,627 @@ +--- +description: Use the collaborative editing PHP API to integrate collaboration features into your applications. +--- + +# Collaborative editing PHP API + +The collaborative editing feature provides a comprehensive PHP API for managing collaboration sessions, participants, invitations, and real-time synchronization. + +## Core services + +### CollaborationService + +The main service for managing collaboration sessions. + +```php +use Ibexa\Contracts\Collaboration\CollaborationServiceInterface; + +class MyCollaborationController +{ + public function __construct( + private CollaborationServiceInterface $collaborationService + ) {} + + public function createSession(Content $content): CollaborationSession + { + $sessionCreateStruct = new SessionCreateStruct(); + $sessionCreateStruct->contentId = $content->id; + $sessionCreateStruct->name = 'Review Session'; + $sessionCreateStruct->expiresAt = new \DateTime('+1 day'); + + return $this->collaborationService->createSession($sessionCreateStruct); + } +} +``` + +#### Key methods + +| Method | Description | +|--------|-------------| +| `createSession(SessionCreateStruct $struct)` | Create a new collaboration session | +| `getSession(int $sessionId)` | Retrieve session by ID | +| `updateSession(CollaborationSession $session, SessionUpdateStruct $struct)` | Update session properties | +| `endSession(int $sessionId)` | End an active session | +| `findSessions(SessionQuery $query)` | Search for sessions | + +### ParticipantService + +Manages session participants and their permissions. + +```php +use Ibexa\Contracts\Collaboration\ParticipantServiceInterface; + +public function addParticipant( + CollaborationSession $session, + User $user, + string $role = 'editor' +): Participant { + $participantStruct = new ParticipantCreateStruct(); + $participantStruct->userId = $user->id; + $participantStruct->role = $role; + $participantStruct->permissions = [ + 'edit' => true, + 'comment' => true, + 'invite' => false, + ]; + + return $this->participantService->addParticipant($session, $participantStruct); +} +``` + +#### Participant management methods + +| Method | Description | +|--------|-------------| +| `addParticipant(CollaborationSession $session, ParticipantCreateStruct $struct)` | Add user to session | +| `removeParticipant(int $sessionId, int $userId)` | Remove participant | +| `updateParticipant(Participant $participant, ParticipantUpdateStruct $struct)` | Update participant role/permissions | +| `getParticipants(int $sessionId)` | List session participants | +| `isParticipant(int $sessionId, int $userId)` | Check if user is participant | + +### InvitationService + +Handles invitation creation and management. + +```php +use Ibexa\Contracts\Collaboration\InvitationServiceInterface; + +public function inviteUser(CollaborationSession $session, string $email): Invitation +{ + $invitationStruct = new InvitationCreateStruct(); + $invitationStruct->sessionId = $session->id; + $invitationStruct->email = $email; + $invitationStruct->role = 'reviewer'; + $invitationStruct->message = 'Please review this content'; + $invitationStruct->expiresAt = new \DateTime('+7 days'); + + return $this->invitationService->createInvitation($invitationStruct); +} +``` + +#### Invitation methods + +| Method | Description | +|--------|-------------| +| `createInvitation(InvitationCreateStruct $struct)` | Send invitation | +| `acceptInvitation(string $token)` | Accept invitation by token | +| `declineInvitation(string $token)` | Decline invitation | +| `cancelInvitation(int $invitationId)` | Cancel pending invitation | +| `findInvitations(InvitationQuery $query)` | Search invitations | + +## Value objects + +### CollaborationSession + +Represents a collaboration session. + +```php +use Ibexa\Contracts\Collaboration\Values\Session\CollaborationSession; + +class CollaborationSession extends ValueObject +{ + public readonly int $id; + public readonly int $contentId; + public readonly int $ownerId; + public readonly string $name; + public readonly string $status; + public readonly \DateTimeInterface $createdAt; + public readonly ?\DateTimeInterface $expiresAt; + public readonly array $metadata; +} +``` + +#### Session status values + +- `active` - Session is currently active +- `expired` - Session has expired +- `ended` - Session was manually ended +- `suspended` - Session is temporarily suspended + +### Participant + +Represents a session participant. + +```php +use Ibexa\Contracts\Collaboration\Values\Participant\Participant; + +class Participant extends ValueObject +{ + public readonly int $id; + public readonly int $sessionId; + public readonly int $userId; + public readonly string $role; + public readonly array $permissions; + public readonly \DateTimeInterface $joinedAt; + public readonly ?\DateTimeInterface $lastActivity; +} +``` + +#### Participant roles + +- `owner` - Session owner with full control +- `admin` - Can manage session and participants +- `editor` - Can edit content and comment +- `reviewer` - Can review, comment, and approve +- `viewer` - Read-only access with commenting + +### Invitation + +Represents a collaboration invitation. + +```php +use Ibexa\Contracts\Collaboration\Values\Invitation\Invitation; + +class Invitation extends ValueObject +{ + public readonly int $id; + public readonly int $sessionId; + public readonly string $email; + public readonly ?int $userId; + public readonly string $token; + public readonly string $status; + public readonly string $role; + public readonly \DateTimeInterface $createdAt; + public readonly ?\DateTimeInterface $expiresAt; +} +``` + +## Query and search + +### SessionQuery + +Search for collaboration sessions. + +```php +use Ibexa\Contracts\Collaboration\Values\Session\Query\SessionQuery; +use Ibexa\Contracts\Collaboration\Values\Session\Query\Criterion; +use Ibexa\Contracts\Collaboration\Values\Session\Query\SortClause; + +$query = new SessionQuery(); +$query->filter = new Criterion\LogicalAnd([ + new Criterion\Status(['active']), + new Criterion\OwnerId($currentUserId), + new Criterion\ContentType(['article', 'blog_post']), +]); + +$query->sortClauses = [ + new SortClause\UpdatedAt(Query::SORT_DESC), +]; + +$query->limit = 20; +$query->offset = 0; + +$sessions = $this->collaborationService->findSessions($query); +``` + +#### Available criteria + +| Criterion | Description | +|-----------|-------------| +| `Status` | Filter by session status | +| `OwnerId` | Filter by session owner | +| `ParticipantId` | Sessions where user is participant | +| `ContentId` | Filter by content ID | +| `ContentType` | Filter by content type | +| `DateRange` | Filter by date range | + +#### Sort clauses + +| Sort Clause | Description | +|-------------|-------------| +| `CreatedAt` | Sort by creation date | +| `UpdatedAt` | Sort by last update | +| `ExpiresAt` | Sort by expiration date | +| `Name` | Sort alphabetically by name | + +### InvitationQuery + +Search for invitations. + +```php +use Ibexa\Contracts\Collaboration\Values\Invitation\Query\InvitationQuery; +use Ibexa\Contracts\Collaboration\Values\Invitation\Query\Criterion as InvitationCriterion; + +$query = new InvitationQuery(); +$query->filter = new InvitationCriterion\LogicalAnd([ + new InvitationCriterion\Status(['pending']), + new InvitationCriterion\Email($userEmail), +]); + +$invitations = $this->invitationService->findInvitations($query); +``` + +## Real-time editing API + +### RealTimeEditingService + +Manages real-time collaboration features. + +```php +use Ibexa\Contracts\Collaboration\RealTimeEditingServiceInterface; + +public function enableRealTimeEditing(CollaborationSession $session): void +{ + $this->realTimeService->enableRealTime($session->id, [ + 'sync_interval' => 1000, // milliseconds + 'conflict_resolution' => 'last_write_wins', + 'cursor_tracking' => true, + ]); +} + +public function syncChanges(int $sessionId, array $changes): array +{ + return $this->realTimeService->syncChanges($sessionId, $changes); +} +``` + +### WebSocket integration + +```php +use Ibexa\Contracts\Collaboration\WebSocket\CollaborationWebSocketInterface; + +public function broadcastChange(int $sessionId, array $change): void +{ + $this->webSocketService->broadcast($sessionId, [ + 'type' => 'content_change', + 'user_id' => $this->currentUser->id, + 'change' => $change, + 'timestamp' => time(), + ]); +} +``` + +## Events and listeners + +### Collaboration events + +The system dispatches various events during collaboration: + +```php +use Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionCreateEvent; +use Ibexa\Contracts\Collaboration\Event\Session\SessionCreatedEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class CollaborationEventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + BeforeSessionCreateEvent::class => 'onBeforeSessionCreate', + SessionCreatedEvent::class => 'onSessionCreated', + ]; + } + + public function onBeforeSessionCreate(BeforeSessionCreateEvent $event): void + { + $struct = $event->getSessionCreateStruct(); + + // Add custom validation + if (empty($struct->name)) { + $struct->name = 'Collaboration Session ' . date('Y-m-d H:i'); + } + } + + public function onSessionCreated(SessionCreatedEvent $event): void + { + $session = $event->getSession(); + + // Send notification to content owner + $this->notificationService->notify( + $session->ownerId, + 'collaboration_session_created', + ['session' => $session] + ); + } +} +``` + +#### Available events + +**Session events**: +- `BeforeSessionCreateEvent` / `SessionCreatedEvent` +- `BeforeSessionUpdateEvent` / `SessionUpdatedEvent` +- `BeforeSessionEndEvent` / `SessionEndedEvent` + +**Participant events**: +- `BeforeParticipantAddEvent` / `ParticipantAddedEvent` +- `BeforeParticipantRemoveEvent` / `ParticipantRemovedEvent` +- `ParticipantUpdatedEvent` + +**Invitation events**: +- `InvitationCreatedEvent` +- `InvitationAcceptedEvent` / `InvitationDeclinedEvent` +- `InvitationExpiredEvent` + +## Custom integrations + +### Custom session types + +Create custom session types for specific workflows: + +```php +use Ibexa\Contracts\Collaboration\Values\Session\SessionCreateStruct; + +class ReviewSessionCreateStruct extends SessionCreateStruct +{ + public array $reviewers = []; + public ?\DateTime $reviewDeadline = null; + public bool $requireAllApprovals = false; +} + +class ReviewCollaborationService +{ + public function createReviewSession( + Content $content, + array $reviewers, + \DateTime $deadline + ): CollaborationSession { + $struct = new ReviewSessionCreateStruct(); + $struct->contentId = $content->id; + $struct->reviewers = $reviewers; + $struct->reviewDeadline = $deadline; + $struct->requireAllApprovals = true; + + $session = $this->collaborationService->createSession($struct); + + // Auto-invite reviewers + foreach ($reviewers as $reviewerId) { + $this->addReviewer($session, $reviewerId); + } + + return $session; + } +} +``` + +### Integration with workflow + +Connect collaboration with content workflows: + +```php +use Ibexa\Contracts\Workflow\Event\Action\ActionExecutedEvent; + +class WorkflowCollaborationIntegration +{ + public function onWorkflowAction(ActionExecutedEvent $event): void + { + $workflowItem = $event->getWorkflowItem(); + + if ($event->getActionName() === 'collaboration_review') { + $content = $this->contentService->loadContent( + $workflowItem->getSubject()->id + ); + + // Create collaboration session for review stage + $session = $this->createCollaborationFromWorkflow($content, $workflowItem); + + // Auto-assign reviewers based on workflow metadata + $this->assignWorkflowReviewers($session, $workflowItem); + } + } +} +``` + +### REST API extension + +Extend the REST API for collaboration features: + +```php +use Ibexa\Contracts\Rest\Output\ValueObjectVisitor; +use Ibexa\Contracts\Rest\Output\Generator; +use Ibexa\Contracts\Rest\Output\Visitor; + +class CollaborationSessionValueObjectVisitor extends ValueObjectVisitor +{ + public function visit(Visitor $visitor, Generator $generator, $data) + { + $generator->startObjectElement('CollaborationSession'); + + $generator->startValueElement('id', $data->id); + $generator->endValueElement('id'); + + $generator->startValueElement('contentId', $data->contentId); + $generator->endValueElement('contentId'); + + // Add participants + $generator->startObjectElement('participants'); + foreach ($data->participants as $participant) { + $visitor->visitValueObject($participant); + } + $generator->endObjectElement('participants'); + + $generator->endObjectElement('CollaborationSession'); + } +} +``` + +## Performance optimization + +### Caching strategies + +```php +use Symfony\Contracts\Cache\CacheInterface; + +class CachedCollaborationService +{ + public function __construct( + private CollaborationServiceInterface $collaborationService, + private CacheInterface $cache + ) {} + + public function getSession(int $sessionId): CollaborationSession + { + return $this->cache->get( + "collaboration_session_{$sessionId}", + function () use ($sessionId) { + return $this->collaborationService->getSession($sessionId); + } + ); + } + + public function invalidateSessionCache(int $sessionId): void + { + $this->cache->delete("collaboration_session_{$sessionId}"); + } +} +``` + +### Batch operations + +```php +public function createBulkInvitations( + CollaborationSession $session, + array $emails, + string $role = 'reviewer' +): array { + $invitations = []; + + // Process in batches to avoid memory issues + $batches = array_chunk($emails, 50); + + foreach ($batches as $batch) { + $batchInvitations = $this->invitationService->createBulkInvitations( + $session->id, + $batch, + $role + ); + + $invitations = array_merge($invitations, $batchInvitations); + } + + return $invitations; +} +``` + +## Error handling + +### Custom exceptions + +```php +use Ibexa\Contracts\Collaboration\Exception\CollaborationException; + +class SessionExpiredException extends CollaborationException +{ + public function __construct(CollaborationSession $session) + { + parent::__construct( + "Collaboration session {$session->id} has expired" + ); + } +} + +class InsufficientPermissionsException extends CollaborationException +{ + public function __construct(string $permission, int $userId) + { + parent::__construct( + "User {$userId} does not have '{$permission}' permission" + ); + } +} +``` + +### Exception handling in controllers + +```php +public function joinSessionAction(int $sessionId): Response +{ + try { + $session = $this->collaborationService->getSession($sessionId); + + if (!$session->isActive()) { + throw new SessionExpiredException($session); + } + + $participant = $this->participantService->addCurrentUserToSession($session); + + return $this->json(['success' => true, 'participant' => $participant]); + + } catch (SessionExpiredException $e) { + return $this->json(['error' => 'Session has expired'], 410); + } catch (InsufficientPermissionsException $e) { + return $this->json(['error' => 'Access denied'], 403); + } catch (CollaborationException $e) { + return $this->json(['error' => $e->getMessage()], 400); + } +} +``` + +## Testing collaboration features + +### Unit testing + +```php +use PHPUnit\Framework\TestCase; +use Ibexa\Contracts\Collaboration\CollaborationServiceInterface; + +class CollaborationServiceTest extends TestCase +{ + public function testCreateSession(): void + { + $content = $this->createMockContent(); + + $struct = new SessionCreateStruct(); + $struct->contentId = $content->id; + $struct->name = 'Test Session'; + + $session = $this->collaborationService->createSession($struct); + + $this->assertEquals($content->id, $session->contentId); + $this->assertEquals('Test Session', $session->name); + $this->assertEquals('active', $session->status); + } +} +``` + +### Integration testing + +```php +use Ibexa\Tests\Integration\Core\Repository\BaseTest; + +class CollaborationIntegrationTest extends BaseTest +{ + public function testFullCollaborationWorkflow(): void + { + // Create content + $content = $this->createTestContent(); + + // Create session + $session = $this->createTestSession($content); + + // Invite participant + $invitation = $this->inviteTestUser($session); + + // Accept invitation + $participant = $this->acceptInvitation($invitation); + + // Test collaboration + $this->assertTrue($session->isParticipant($participant->userId)); + $this->assertEquals('editor', $participant->role); + } +} +``` + +## Next steps + +- [Explore collaboration events](collaborative_editing_events.md) +- [View PHP API reference](../../api/php_api/php_api_reference/namespaces/ibexa-contracts-collaboration.html) \ No newline at end of file diff --git a/docs/content_management/collaborative_editing/collaborative_editing_events.md b/docs/content_management/collaborative_editing/collaborative_editing_events.md new file mode 100644 index 0000000000..cdad6f4fe9 --- /dev/null +++ b/docs/content_management/collaborative_editing/collaborative_editing_events.md @@ -0,0 +1,657 @@ +--- +description: Learn about collaboration events and how to customize collaborative editing behavior. +--- + +# Collaborative editing events + +The collaborative editing system dispatches various events that allow you to customize behavior, add custom logic, and integrate with external systems. + +## Session events + +### Session lifecycle events + +#### BeforeSessionCreateEvent + +Dispatched before creating a collaboration session. + +```php +use Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionCreateEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class SessionEventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + BeforeSessionCreateEvent::class => ['onBeforeSessionCreate', 10], + ]; + } + + public function onBeforeSessionCreate(BeforeSessionCreateEvent $event): void + { + $struct = $event->getSessionCreateStruct(); + + // Auto-generate session name if not provided + if (empty($struct->name)) { + $content = $this->contentService->loadContent($struct->contentId); + $struct->name = "Collaboration on: " . $content->getName(); + } + + // Set default expiration + if (!$struct->expiresAt) { + $struct->expiresAt = new \DateTime('+7 days'); + } + + // Add metadata + $struct->metadata['creator_ip'] = $this->request->getClientIp(); + $struct->metadata['created_via'] = 'web_interface'; + } +} +``` + +#### SessionCreatedEvent + +Dispatched after a session is successfully created. + +```php +use Ibexa\Contracts\Collaboration\Event\Session\SessionCreatedEvent; + +public function onSessionCreated(SessionCreatedEvent $event): void +{ + $session = $event->getSession(); + + // Send notification to content stakeholders + $content = $this->contentService->loadContent($session->contentId); + $stakeholders = $this->getContentStakeholders($content); + + foreach ($stakeholders as $stakeholder) { + $this->notificationService->send($stakeholder, 'session_created', [ + 'session' => $session, + 'content' => $content, + ]); + } + + // Log session creation for audit + $this->logger->info('Collaboration session created', [ + 'session_id' => $session->id, + 'content_id' => $session->contentId, + 'owner_id' => $session->ownerId, + ]); +} +``` + +#### BeforeSessionUpdateEvent / SessionUpdatedEvent + +```php +use Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionUpdateEvent; +use Ibexa\Contracts\Collaboration\Event\Session\SessionUpdatedEvent; + +public function onBeforeSessionUpdate(BeforeSessionUpdateEvent $event): void +{ + $session = $event->getSession(); + $struct = $event->getSessionUpdateStruct(); + + // Validate session updates + if ($struct->expiresAt && $struct->expiresAt < new \DateTime()) { + throw new \InvalidArgumentException('Cannot set expiration date in the past'); + } + + // Track what's being changed + $changes = []; + if (isset($struct->name) && $struct->name !== $session->name) { + $changes['name'] = ['old' => $session->name, 'new' => $struct->name]; + } + + $event->setMetadata('changes', $changes); +} + +public function onSessionUpdated(SessionUpdatedEvent $event): void +{ + $session = $event->getSession(); + $changes = $event->getMetadata('changes', []); + + if (!empty($changes)) { + // Notify participants of changes + $this->notifyParticipantsOfUpdate($session, $changes); + + // Log the update + $this->auditLogger->info('Session updated', [ + 'session_id' => $session->id, + 'changes' => $changes, + ]); + } +} +``` + +#### BeforeSessionEndEvent / SessionEndedEvent + +```php +use Ibexa\Contracts\Collaboration\Event\Session\BeforeSessionEndEvent; +use Ibexa\Contracts\Collaboration\Event\Session\SessionEndedEvent; + +public function onBeforeSessionEnd(BeforeSessionEndEvent $event): void +{ + $session = $event->getSession(); + + // Archive session data before ending + $this->archiveService->archiveSession($session); + + // Check for unsaved changes + $unsavedChanges = $this->checkForUnsavedChanges($session); + if ($unsavedChanges) { + // Optionally prevent session ending + // $event->stopPropagation(); + + // Or warn the user + $event->setMetadata('unsaved_changes', $unsavedChanges); + } +} + +public function onSessionEnded(SessionEndedEvent $event): void +{ + $session = $event->getSession(); + + // Notify all participants + $participants = $this->participantService->getParticipants($session->id); + foreach ($participants as $participant) { + $this->notificationService->send($participant->userId, 'session_ended', [ + 'session' => $session, + ]); + } + + // Generate session report + $report = $this->reportService->generateSessionReport($session); + $this->emailService->sendSessionReport($session->ownerId, $report); +} +``` + +## Participant events + +### ParticipantAddedEvent + +Dispatched when a user joins a collaboration session. + +```php +use Ibexa\Contracts\Collaboration\Event\Participant\ParticipantAddedEvent; + +public function onParticipantAdded(ParticipantAddedEvent $event): void +{ + $participant = $event->getParticipant(); + $session = $event->getSession(); + + // Welcome new participant + $this->notificationService->send($participant->userId, 'welcome_to_session', [ + 'session' => $session, + 'role' => $participant->role, + ]); + + // Notify other participants + $otherParticipants = $this->participantService->getOtherParticipants( + $session->id, + $participant->userId + ); + + foreach ($otherParticipants as $other) { + $this->notificationService->send($other->userId, 'participant_joined', [ + 'session' => $session, + 'new_participant' => $participant, + ]); + } + + // Initialize real-time presence if enabled + if ($this->isRealTimeEnabled($session)) { + $this->realTimeService->addParticipantPresence($participant); + } +} +``` + +### ParticipantRemovedEvent + +```php +use Ibexa\Contracts\Collaboration\Event\Participant\ParticipantRemovedEvent; + +public function onParticipantRemoved(ParticipantRemovedEvent $event): void +{ + $participant = $event->getParticipant(); + $session = $event->getSession(); + + // Clean up participant data + $this->cleanupParticipantData($participant); + + // Remove from real-time presence + if ($this->isRealTimeEnabled($session)) { + $this->realTimeService->removeParticipantPresence($participant); + } + + // Notify remaining participants + $remainingParticipants = $this->participantService->getParticipants($session->id); + foreach ($remainingParticipants as $other) { + $this->notificationService->send($other->userId, 'participant_left', [ + 'session' => $session, + 'removed_participant' => $participant, + ]); + } +} +``` + +## Invitation events + +### InvitationCreatedEvent + +```php +use Ibexa\Contracts\Collaboration\Event\Invitation\InvitationCreatedEvent; + +public function onInvitationCreated(InvitationCreatedEvent $event): void +{ + $invitation = $event->getInvitation(); + $session = $this->collaborationService->getSession($invitation->sessionId); + + // Send invitation email + $this->emailService->sendInvitationEmail($invitation, [ + 'session' => $session, + 'inviter' => $this->userService->loadUser($session->ownerId), + 'invitation_url' => $this->generateInvitationUrl($invitation), + ]); + + // Track invitation metrics + $this->metricsService->incrementCounter('collaboration.invitations.sent', [ + 'session_id' => $session->id, + 'role' => $invitation->role, + 'user_type' => $invitation->userId ? 'internal' : 'external', + ]); +} +``` + +### InvitationAcceptedEvent / InvitationDeclinedEvent + +```php +use Ibexa\Contracts\Collaboration\Event\Invitation\InvitationAcceptedEvent; +use Ibexa\Contracts\Collaboration\Event\Invitation\InvitationDeclinedEvent; + +public function onInvitationAccepted(InvitationAcceptedEvent $event): void +{ + $invitation = $event->getInvitation(); + $participant = $event->getParticipant(); + $session = $this->collaborationService->getSession($invitation->sessionId); + + // Track acceptance + $this->metricsService->incrementCounter('collaboration.invitations.accepted'); + + // Send welcome message + $this->notificationService->send($participant->userId, 'invitation_accepted', [ + 'session' => $session, + 'participant' => $participant, + ]); + + // Update session activity + $this->updateSessionActivity($session); +} + +public function onInvitationDeclined(InvitationDeclinedEvent $event): void +{ + $invitation = $event->getInvitation(); + $session = $this->collaborationService->getSession($invitation->sessionId); + + // Track declination + $this->metricsService->incrementCounter('collaboration.invitations.declined'); + + // Notify session owner + $this->notificationService->send($session->ownerId, 'invitation_declined', [ + 'invitation' => $invitation, + 'session' => $session, + ]); +} +``` + +## Real-time editing events + +### ContentChangedEvent + +```php +use Ibexa\Contracts\Collaboration\Event\RealTime\ContentChangedEvent; + +public function onContentChanged(ContentChangedEvent $event): void +{ + $change = $event->getChange(); + $session = $event->getSession(); + $userId = $event->getUserId(); + + // Broadcast change to other participants via WebSocket + $this->webSocketService->broadcast($session->id, [ + 'type' => 'content_change', + 'user_id' => $userId, + 'change' => $change, + 'timestamp' => time(), + ], [$userId]); // Exclude the user who made the change + + // Store change history + $this->changeHistoryService->recordChange($session, $change, $userId); + + // Check for conflicts + if ($this->hasConflict($change, $session)) { + $this->conflictResolutionService->handleConflict($session, $change); + } +} +``` + +### ParticipantPresenceEvent + +```php +use Ibexa\Contracts\Collaboration\Event\RealTime\ParticipantPresenceEvent; + +public function onParticipantPresence(ParticipantPresenceEvent $event): void +{ + $presence = $event->getPresence(); + $session = $event->getSession(); + + // Update participant last activity + $this->participantService->updateLastActivity($presence->participantId); + + // Broadcast presence to other participants + $this->webSocketService->broadcast($session->id, [ + 'type' => 'participant_presence', + 'participant_id' => $presence->participantId, + 'cursor_position' => $presence->cursorPosition, + 'is_active' => $presence->isActive, + ], [$presence->participantId]); +} +``` + +## Content workflow integration events + +### WorkflowActionExecutedEvent integration + +```php +use Ibexa\Contracts\Workflow\Event\Action\ActionExecutedEvent; +use Ibexa\Contracts\Collaboration\Event\Session\SessionCreatedEvent; + +public function onWorkflowActionExecuted(ActionExecutedEvent $event): void +{ + if ($event->getActionName() === 'request_collaboration_review') { + $workflowItem = $event->getWorkflowItem(); + $content = $this->contentService->loadContent($workflowItem->getSubject()->id); + + // Create collaboration session for workflow review + $sessionStruct = new SessionCreateStruct(); + $sessionStruct->contentId = $content->id; + $sessionStruct->name = "Workflow Review: " . $content->getName(); + $sessionStruct->metadata = [ + 'workflow_item_id' => $workflowItem->getId(), + 'workflow_name' => $workflowItem->getWorkflowName(), + 'created_from' => 'workflow_action', + ]; + + $session = $this->collaborationService->createSession($sessionStruct); + + // Auto-invite reviewers based on workflow metadata + $reviewers = $workflowItem->getMetadata('reviewers', []); + foreach ($reviewers as $reviewerId) { + $this->inviteUserToSession($session, $reviewerId, 'reviewer'); + } + } +} +``` + +## Custom event examples + +### SessionActivityEvent + +Create custom events for specific use cases: + +```php +use Ibexa\Contracts\Collaboration\Event\AbstractCollaborationEvent; + +class SessionActivityEvent extends AbstractCollaborationEvent +{ + public function __construct( + private CollaborationSession $session, + private string $activityType, + private array $activityData = [] + ) {} + + public function getSession(): CollaborationSession + { + return $this->session; + } + + public function getActivityType(): string + { + return $this->activityType; + } + + public function getActivityData(): array + { + return $this->activityData; + } +} + +// Usage +$this->eventDispatcher->dispatch( + new SessionActivityEvent($session, 'content_updated', [ + 'field' => 'title', + 'old_value' => 'Old Title', + 'new_value' => 'New Title', + ]) +); +``` + +### NotificationEvent integration + +```php +use Ibexa\Contracts\Notifications\Event\NotificationEvent; + +public function onSessionActivity(SessionActivityEvent $event): void +{ + $session = $event->getSession(); + $activity = $event->getActivityType(); + + // Create notification for session activity + $notificationEvent = new NotificationEvent( + 'collaboration_activity', + $session->ownerId, + [ + 'session_id' => $session->id, + 'activity_type' => $activity, + 'activity_data' => $event->getActivityData(), + ] + ); + + $this->eventDispatcher->dispatch($notificationEvent); +} +``` + +## Event listener registration + +### Via service configuration + +```yaml +# config/services.yaml +services: + App\EventSubscriber\CollaborationEventSubscriber: + tags: + - name: kernel.event_subscriber + + App\EventListener\SessionNotificationListener: + tags: + - name: kernel.event_listener + event: Ibexa\Contracts\Collaboration\Event\Session\SessionCreatedEvent + method: onSessionCreated + priority: 100 +``` + +### Via PHP attributes + +```php +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + +class CollaborationNotificationService +{ + #[AsEventListener(event: SessionCreatedEvent::class, priority: 100)] + public function onSessionCreated(SessionCreatedEvent $event): void + { + // Handle session created + } + + #[AsEventListener(event: ParticipantAddedEvent::class)] + public function onParticipantAdded(ParticipantAddedEvent $event): void + { + // Handle participant added + } +} +``` + +## Event debugging and monitoring + +### Debug event listeners + +```bash +# List all collaboration event listeners +php bin/console debug:event-dispatcher | grep -i collaboration + +# Debug specific event +php bin/console debug:event-dispatcher SessionCreatedEvent +``` + +### Event monitoring + +```php +use Psr\Log\LoggerInterface; + +class CollaborationEventMonitor implements EventSubscriberInterface +{ + public function __construct( + private LoggerInterface $logger + ) {} + + public static function getSubscribedEvents(): array + { + return [ + // Monitor all collaboration events with low priority + SessionCreatedEvent::class => ['logEvent', -100], + ParticipantAddedEvent::class => ['logEvent', -100], + InvitationCreatedEvent::class => ['logEvent', -100], + ]; + } + + public function logEvent($event): void + { + $eventName = get_class($event); + $data = $this->extractEventData($event); + + $this->logger->info("Collaboration event: {$eventName}", $data); + } + + private function extractEventData($event): array + { + $data = []; + + if (method_exists($event, 'getSession')) { + $data['session_id'] = $event->getSession()->id; + } + + if (method_exists($event, 'getParticipant')) { + $data['participant_id'] = $event->getParticipant()->id; + } + + return $data; + } +} +``` + +## Performance considerations + +### Async event processing + +```php +use Symfony\Component\Messenger\MessageBusInterface; + +class AsyncCollaborationEventHandler +{ + public function __construct( + private MessageBusInterface $messageBus + ) {} + + public function onSessionCreated(SessionCreatedEvent $event): void + { + // Process heavy operations asynchronously + $this->messageBus->dispatch(new SessionCreatedMessage( + $event->getSession()->id + )); + } +} + +// Message handler +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +#[AsMessageHandler] +class SessionCreatedMessageHandler +{ + public function __invoke(SessionCreatedMessage $message): void + { + $session = $this->collaborationService->getSession($message->sessionId); + + // Perform heavy operations + $this->generateSessionAnalytics($session); + $this->updateSearchIndex($session); + $this->syncWithExternalSystem($session); + } +} +``` + +### Event batching + +```php +class BatchCollaborationEventProcessor +{ + private array $eventQueue = []; + + public function queueEvent($event): void + { + $this->eventQueue[] = $event; + + if (count($this->eventQueue) >= 10) { + $this->processBatch(); + } + } + + private function processBatch(): void + { + // Process events in batches for better performance + $notifications = []; + + foreach ($this->eventQueue as $event) { + if ($event instanceof SessionCreatedEvent) { + $notifications[] = $this->createSessionNotification($event); + } + } + + // Send all notifications at once + $this->notificationService->sendBatch($notifications); + + $this->eventQueue = []; + } +} +``` + +## Best practices + +### Event handling best practices + +- **Keep listeners lightweight**: Avoid heavy operations in event listeners +- **Use appropriate priority**: Set listener priority based on importance +- **Handle exceptions**: Prevent one listener from breaking others +- **Use async processing**: Move heavy operations to message queues +- **Log important events**: Maintain audit trail for collaboration activities + +### Security considerations + +- **Validate event data**: Don't trust event data without validation +- **Check permissions**: Verify user permissions in event listeners +- **Sanitize input**: Clean any user-provided data in events +- **Rate limiting**: Implement rate limiting for event-triggered actions +- **Audit logging**: Log security-relevant collaboration events + +## Next steps + +- [Explore the PHP API](collaborative_editing_api.md) +- [View event reference documentation](../../api/event_reference/collaboration_events.md) \ No newline at end of file diff --git a/docs/content_management/collaborative_editing/collaborative_editing_guide.md b/docs/content_management/collaborative_editing/collaborative_editing_guide.md new file mode 100644 index 0000000000..74ecced69b --- /dev/null +++ b/docs/content_management/collaborative_editing/collaborative_editing_guide.md @@ -0,0 +1,123 @@ +--- +description: Enable multiple users to collaborate on content creation, editing, and review through shared drafts, real-time editing, and invitation systems. +edition: experience,commerce +month_change: true +--- + +# Collaborative editing + +Collaborative editing allows multiple users to work together on content creation and editing in [[= product_name =]]. +This feature enables teams to streamline content workflows by sharing drafts, collaborating in real-time, and managing review processes efficiently. + +## Key features + +### Shared drafts + +Content creators can share their draft content with internal team members or external collaborators. +Shared drafts maintain version control while allowing multiple users to contribute to the same content item. + +Key benefits: + +- **Version control**: All changes are tracked and versioned +- **Access control**: Fine-grained permissions for different user types +- **Dashboard integration**: Shared drafts appear in dedicated dashboard tabs +- **Workflow integration**: Seamless integration with existing content workflows + +### Real-time editing + +The real-time editing feature enables users to see each other's changes as they happen, providing immediate visual feedback and preventing conflicts. + +Features include: + +- **Live cursors**: See where other users are editing +- **Instant synchronization**: Changes appear immediately for all participants +- **Conflict prevention**: Smart conflict resolution when multiple users edit the same area +- **Asynchronous support**: Users can also work offline and sync changes later + +### Invitation system + +Collaborate with both internal users and external stakeholders through a flexible invitation system. + +Invitation options: + +- **Internal invitations**: Invite existing [[= product_name =]] users +- **External invitations**: Invite users outside your organization via email +- **Time-limited access**: Set expiration dates for collaboration sessions +- **Role-based permissions**: Control what invited users can do (view, edit, comment) + +## Dashboard integration + +Collaborative editing extends the back office dashboard with two new tabs: + +- **My shared drafts**: Content you've shared with others +- **Drafts shared with me**: Content others have shared with you + +These tabs provide quick access to active collaboration sessions and help users stay organized. + +## Getting started + +To start using collaborative editing: + +1. [Install and configure](collaborative_editing_installation.md) the collaboration feature +2. [Set up user permissions](collaborative_editing_permissions.md) for collaboration +3. [Create your first collaboration session](collaborative_editing_usage.md) +4. Explore the [PHP API](collaborative_editing_api.md) for custom integrations + +## Use cases + +### Content review process + +**Scenario**: Marketing team creates a blog post that needs review from legal and technical teams. + +**Solution**: +1. Marketing creates the initial draft +2. Share the draft with legal and technical reviewers +3. Reviewers can comment and suggest changes in real-time +4. Marketing incorporates feedback and publishes + +### External contributor workflow + +**Scenario**: Work with external consultants on product documentation. + +**Solution**: +1. Create content structure internally +2. Invite external experts via email with time-limited access +3. Collaborate on content creation with real-time feedback +4. Review and publish internally after collaboration ends + +### Multi-language content coordination + +**Scenario**: Coordinate content creation across different language teams. + +**Solution**: +1. Create master content in primary language +2. Share drafts with regional language teams +3. Parallel translation and localization work +4. Synchronized publication across all languages + +## Technical overview + +The collaborative editing system is built on several core components: + +- **Session management**: Handles collaboration sessions and participant management +- **Real-time synchronization**: WebSocket-based real-time updates +- **Permission system**: Integration with [[= product_name =]] role and policy system +- **Notification system**: Email and in-app notifications for collaboration events +- **REST API**: Full API access for custom integrations + +## Security considerations + +Collaborative editing includes several security features: + +- **Permission inheritance**: Respects existing content permissions +- **Audit logging**: All collaboration activities are logged +- **Session expiration**: Automatic cleanup of expired sessions +- **External user limitations**: Configurable restrictions for external collaborators + +## Next steps + +- [Installation and setup](collaborative_editing_installation.md) +- [User permissions configuration](collaborative_editing_permissions.md) +- [Using collaborative editing](collaborative_editing_usage.md) +- [PHP API reference](collaborative_editing_api.md) +- [Events and customization](collaborative_editing_events.md) \ No newline at end of file diff --git a/docs/content_management/collaborative_editing/collaborative_editing_installation.md b/docs/content_management/collaborative_editing/collaborative_editing_installation.md new file mode 100644 index 0000000000..84901e4ed7 --- /dev/null +++ b/docs/content_management/collaborative_editing/collaborative_editing_installation.md @@ -0,0 +1,324 @@ +--- +description: Install and configure the collaborative editing feature in Ibexa DXP. +--- + +# Collaborative editing installation + +The collaborative editing feature is available as part of [[= product_name =]] v5.0+ and requires the `ibexa/collaboration` package. + +## Requirements + +- [[= product_name =]] v5.0 or higher +- PHP 8.2 or higher +- Symfony 6.4+ or 7.0+ +- Database: MySQL 8.0+ or PostgreSQL 12+ +- Redis (recommended for real-time features) + +## Installation + +### 1. Install the collaboration package + +The `ibexa/collaboration` package is included by default in [[= product_name =]] v5.0+ Experience and Commerce editions. + +For manual installation: + +```bash +composer require ibexa/collaboration +``` + +### 2. Enable the bundle + +Add the collaboration bundle to your `config/bundles.php`: + +```php + ['all' => true], +]; +``` + +### 3. Configure the database + +Run the database migration to create the necessary tables: + +```bash +php bin/console doctrine:migrations:migrate --configuration=vendor/ibexa/collaboration/src/bundle/Resources/config/migrations.yml +``` + +This creates the following tables: +- `ibexa_collaboration_sessions` - Collaboration session data +- `ibexa_collaboration_participants` - Session participants +- `ibexa_collaboration_invitations` - Invitation management + +### 4. Configure Redis (optional but recommended) + +For real-time editing features, configure Redis: + +```yaml +# config/packages/redis.yaml +framework: + cache: + app: cache.adapter.redis + default_redis_provider: 'redis://localhost:6379' + +ibexa: + system: + default: + collaboration: + real_time: + enabled: true + redis_dsn: 'redis://localhost:6379' +``` + +### 5. Configure WebSocket support (for real-time editing) + +Install and configure a WebSocket server for real-time features: + +```bash +composer require reactphp/socket ratchet/pawl +``` + +Add WebSocket configuration: + +```yaml +# config/packages/collaboration.yaml +ibexa: + system: + default: + collaboration: + websocket: + enabled: true + host: 'localhost' + port: 8080 + path: '/collaboration' +``` + +### 6. Configure email notifications + +Set up email templates and sender configuration: + +```yaml +# config/packages/collaboration.yaml +ibexa: + system: + default: + collaboration: + notifications: + email: + enabled: true + from_email: 'noreply@example.com' + from_name: 'Your Site Name' + templates: + invitation: '@IbexaCollaboration/emails/invitation.html.twig' + session_started: '@IbexaCollaboration/emails/session_started.html.twig' +``` + +## Configuration options + +### Basic configuration + +```yaml +# config/packages/collaboration.yaml +ibexa: + system: + default: + collaboration: + enabled: true + + # Session management + sessions: + max_duration: P1D # 1 day + cleanup_interval: PT1H # 1 hour + max_participants: 10 + + # Invitation settings + invitations: + expiration_time: P7D # 7 days + external_users_enabled: true + require_confirmation: true + + # Dashboard integration + dashboard: + enabled: true + items_per_page: 20 +``` + +### Real-time editing configuration + +```yaml +ibexa: + system: + default: + collaboration: + real_time: + enabled: true + sync_interval: 1000 # milliseconds + conflict_resolution: 'last_write_wins' + cursor_timeout: 30000 # milliseconds +``` + +### Security settings + +```yaml +ibexa: + system: + default: + collaboration: + security: + # External user restrictions + external_users: + max_session_duration: PT4H # 4 hours + allowed_content_types: ['article', 'blog_post'] + restricted_fields: ['internal_notes'] + + # Session security + require_https: true + session_token_expiry: PT2H # 2 hours +``` + +## Permissions setup + +### Grant collaboration permissions + +Add the necessary policies to user roles: + +```yaml +# In your role configuration +policies: + - module: collaboration + function: create_session + - module: collaboration + function: participate + - module: collaboration + function: invite + limitations: + - identifier: User_Group + values: [4, 5] # Restrict to specific user groups + - module: collaboration + function: manage_sessions +``` + +Available collaboration policies: +- `collaboration/create_session` - Create new collaboration sessions +- `collaboration/participate` - Participate in sessions +- `collaboration/invite` - Invite other users +- `collaboration/manage_sessions` - Manage any collaboration session +- `collaboration/view_shared` - View shared drafts + +### Content-specific permissions + +Collaborative editing respects existing content permissions. Users need: +- `content/read` for the content being collaborated on +- `content/edit` to make changes (if editing is allowed) +- `content/publish` to publish collaborative changes + +## Testing the installation + +### 1. Verify bundle installation + +Check that the bundle is properly loaded: + +```bash +php bin/console debug:container ibexa.collaboration +``` + +### 2. Test database setup + +Verify tables were created: + +```sql +SHOW TABLES LIKE 'ibexa_collaboration_%'; +``` + +### 3. Check permissions + +Log into the back office and verify: +- New dashboard tabs appear: "My shared drafts" and "Drafts shared with me" +- Collaboration options appear in content editing interface +- Invitation options are available + +### 4. Test basic functionality + +1. Create or edit a content item +2. Look for "Share" or "Collaborate" buttons +3. Try inviting another user +4. Verify email notifications are sent + +## Troubleshooting + +### Common issues + +**Bundle not found error** +``` +Bundle "IbexaCollaborationBundle" not found +``` +Solution: Ensure the bundle is properly added to `config/bundles.php` + +**Database migration failures** +``` +Migration failed: Table already exists +``` +Solution: Check if tables already exist and run migrations individually + +**WebSocket connection issues** +``` +WebSocket connection failed +``` +Solution: Verify Redis is running and WebSocket server is configured correctly + +**Permission denied errors** +``` +Access denied to collaboration feature +``` +Solution: Check user roles have the necessary collaboration policies + +### Debug commands + +Enable debug mode for collaboration: + +```bash +php bin/console debug:config ibexa collaboration +``` + +View collaboration-related logs: + +```bash +tail -f var/log/collaboration.log +``` + +Test Redis connection: + +```bash +redis-cli ping +``` + +## Performance considerations + +### Database optimization + +Add indexes for better performance: + +```sql +CREATE INDEX idx_collaboration_session_status ON ibexa_collaboration_sessions(status); +CREATE INDEX idx_collaboration_participant_user ON ibexa_collaboration_participants(user_id); +``` + +### Caching configuration + +Enable proper caching for collaboration data: + +```yaml +doctrine: + orm: + result_cache_driver: redis + metadata_cache_driver: redis + query_cache_driver: redis +``` + +## Next steps + +- [Configure user permissions](collaborative_editing_permissions.md) +- [Learn how to use collaborative editing](collaborative_editing_usage.md) +- [Explore the PHP API](collaborative_editing_api.md) \ No newline at end of file diff --git a/docs/content_management/collaborative_editing/collaborative_editing_permissions.md b/docs/content_management/collaborative_editing/collaborative_editing_permissions.md new file mode 100644 index 0000000000..688a602e9d --- /dev/null +++ b/docs/content_management/collaborative_editing/collaborative_editing_permissions.md @@ -0,0 +1,459 @@ +--- +description: Configure user permissions and access control for collaborative editing features. +--- + +# Collaborative editing permissions + +Collaborative editing integrates with [[= product_name =]]'s role-based permission system to provide fine-grained access control over collaboration features. + +## Overview + +Collaborative editing permissions work on two levels: + +1. **Feature permissions**: Control who can use collaboration features +2. **Content permissions**: Respect existing content access controls + +All collaboration activities respect the underlying content permissions, ensuring users can only collaborate on content they already have access to. + +## Collaboration policies + +### Core policies + +The collaboration module introduces several new policies: + +| Policy | Function | Description | +|--------|----------|-------------| +| `collaboration` | `create_session` | Create new collaboration sessions | +| `collaboration` | `participate` | Join and participate in sessions | +| `collaboration` | `invite` | Invite other users to sessions | +| `collaboration` | `manage_sessions` | Manage any collaboration session | +| `collaboration` | `view_shared` | View shared drafts dashboard | +| `collaboration` | `real_time_edit` | Use real-time editing features | + +### Invitation-specific policies + +| Policy | Function | Description | +|--------|----------|-------------| +| `collaboration` | `invite_internal` | Invite internal users | +| `collaboration` | `invite_external` | Invite external users via email | +| `collaboration` | `invite_groups` | Invite entire user groups | + +### Session management policies + +| Policy | Function | Description | +|--------|----------|-------------| +| `collaboration` | `end_session` | End collaboration sessions | +| `collaboration` | `modify_permissions` | Change participant permissions | +| `collaboration` | `extend_session` | Extend session duration | +| `collaboration` | `export_data` | Export collaboration data | + +## Policy configuration examples + +### Basic collaboration user + +A typical content editor who can participate in collaboration: + +```yaml +# config/packages/security.yaml +role_editor_collaboration: + policies: + - module: collaboration + function: create_session + - module: collaboration + function: participate + - module: collaboration + function: invite_internal + - module: collaboration + function: view_shared + - module: content + function: read + - module: content + function: edit +``` + +### Advanced collaboration manager + +A user who can manage collaboration sessions for their team: + +```yaml +role_collaboration_manager: + policies: + - module: collaboration + function: manage_sessions + limitations: + - identifier: User_Group + values: [12, 13] # Specific user groups + - module: collaboration + function: invite_external + - module: collaboration + function: modify_permissions + - module: collaboration + function: end_session + - module: collaboration + function: export_data +``` + +### External collaboration user (limited) + +Restricted permissions for external collaborators: + +```yaml +role_external_collaborator: + policies: + - module: collaboration + function: participate + limitations: + - identifier: Content_Type + values: ['article', 'blog_post'] + - module: collaboration + function: view_shared + - module: content + function: read + limitations: + - identifier: Section + values: [2] # Public section only +``` + +## Limitations + +### User Group limitation + +Restrict collaboration to specific user groups: + +```yaml +policies: + - module: collaboration + function: invite + limitations: + - identifier: User_Group + values: [4, 5, 6] # Marketing, Editorial, Design teams +``` + +### Content Type limitation + +Limit collaboration to specific content types: + +```yaml +policies: + - module: collaboration + function: create_session + limitations: + - identifier: Content_Type + values: ['article', 'blog_post', 'landing_page'] +``` + +### Section limitation + +Restrict collaboration to content in specific sections: + +```yaml +policies: + - module: collaboration + function: participate + limitations: + - identifier: Section + values: [1, 2] # Standard and Media sections +``` + +### Time-based limitations + +Configure time-based restrictions (configured at system level): + +```yaml +# config/packages/collaboration.yaml +ibexa: + system: + default: + collaboration: + limitations: + session_duration: + default: P1D # 1 day + external_users: PT4H # 4 hours for external users + daily_sessions: + max_per_user: 5 + max_per_content: 3 +``` + +## Permission inheritance + +### Content permissions + +Collaborative editing respects existing content permissions: + +- **Read access**: Users must have `content/read` for the content being collaborated on +- **Edit access**: To make changes, users need `content/edit` permissions +- **Publish access**: Only users with `content/publish` can finalize collaborative changes + +### Location-based permissions + +Content location permissions apply to collaboration: + +```yaml +policies: + - module: content + function: read + limitations: + - identifier: Subtree + values: ['/1/2/'] # Media subtree + - module: collaboration + function: participate # Can only collaborate on content in Media subtree +``` + +### Language permissions + +Multi-language content collaboration respects language limitations: + +```yaml +policies: + - module: content + function: edit + limitations: + - identifier: Language + values: ['eng-US', 'fre-FR'] + - module: collaboration + function: create_session # Can collaborate on English and French content +``` + +## External user permissions + +### Configuration + +Configure external user access in system settings: + +```yaml +# config/packages/collaboration.yaml +ibexa: + system: + default: + collaboration: + external_users: + enabled: true + max_session_duration: PT4H + allowed_content_types: ['article', 'blog_post'] + restricted_fields: ['internal_notes', 'seo_metadata'] + require_terms_acceptance: true +``` + +### External user role template + +Create a template role for external users: + +```yaml +role_external_template: + policies: + - module: collaboration + function: participate + limitations: + - identifier: Content_Type + values: ['article'] + - module: content + function: read + limitations: + - identifier: Section + values: [2] # Public section only +``` + +### Email domain restrictions + +Restrict external invitations to specific email domains: + +```yaml +ibexa: + system: + default: + collaboration: + external_users: + allowed_domains: ['partner.com', 'contractor.org'] + blocked_domains: ['competitor.com'] +``` + +## Session-level permissions + +### Participant roles + +Each collaboration session can have different participant roles: + +| Role | Permissions | Use Case | +|------|-------------|----------| +| **Owner** | Full control over session | Session creator | +| **Admin** | Manage participants, end session | Team leads | +| **Editor** | Edit content, comment | Content creators | +| **Reviewer** | View, comment, approve/reject | Reviewers | +| **Viewer** | View only, comment | Stakeholders | + +### Dynamic permission assignment + +Assign permissions when inviting users: + +```php +// In your invitation code +$invitation = $collaborationService->createInvitation($session, [ + 'user_id' => $userId, + 'role' => 'editor', + 'permissions' => [ + 'edit' => true, + 'comment' => true, + 'invite_others' => false, + 'end_session' => false, + ], + 'expiration' => new \DateTime('+7 days'), +]); +``` + +## Permission checking examples + +### Check if user can create sessions + +```php +use Ibexa\Contracts\Core\Repository\PermissionResolver; + +class CollaborationPermissionChecker +{ + public function canCreateSession(User $user, Content $content): bool + { + return $this->permissionResolver->hasAccess('collaboration', 'create_session') && + $this->permissionResolver->canUser('content', 'edit', $content); + } +} +``` + +### Check collaboration permissions in templates + +```twig +{% if ibexa_is_granted('collaboration', 'create_session') %} + +{% endif %} + +{% if ibexa_is_granted('collaboration', 'invite_external') %} + +{% endif %} +``` + +### Verify session access + +```php +public function canAccessSession(User $user, CollaborationSession $session): bool +{ + // Check if user is participant + if ($session->isParticipant($user)) { + return true; + } + + // Check if user has manage_sessions permission + if ($this->permissionResolver->hasAccess('collaboration', 'manage_sessions')) { + return true; + } + + // Check if user has access to the content + return $this->permissionResolver->canUser('content', 'read', $session->getContent()); +} +``` + +## Security considerations + +### Session security + +- **Token-based access**: Each session uses unique tokens for participant authentication +- **IP restrictions**: Optionally restrict sessions to specific IP addresses +- **Device limits**: Limit number of concurrent devices per user +- **Audit logging**: All collaboration activities are logged for security review + +### Data protection + +- **Content isolation**: Users can only access content they have permissions for +- **Change attribution**: All modifications are attributed to specific users +- **Export restrictions**: Control who can export collaboration data +- **Cleanup policies**: Automatic cleanup of expired sessions and data + +### External user security + +- **Limited access**: External users have restricted permissions by default +- **Time limits**: Enforce shorter session durations for external users +- **Content restrictions**: Limit which content types external users can access +- **Email verification**: Require email verification for external participants + +## Troubleshooting permissions + +### Common permission issues + +**Cannot start collaboration** +``` +Check: collaboration/create_session policy +Check: content/edit permission on target content +Check: User is not exceeding session limits +``` + +**Cannot invite users** +``` +Check: collaboration/invite_internal or collaboration/invite_external policy +Check: Target users have necessary content permissions +Check: Email domain restrictions for external users +``` + +**Session access denied** +``` +Check: Session is still active (not expired) +Check: User is valid participant or has manage_sessions policy +Check: Content permissions still valid +``` + +### Debug permission issues + +Use the debug commands to troubleshoot: + +```bash +# Check user permissions +php bin/console debug:permission collaboration create_session --user=john + +# List all collaboration policies +php bin/console debug:permission --module=collaboration + +# Check content permissions +php bin/console debug:permission content edit --content-id=123 +``` + +### Enable permission logging + +Add detailed logging for permission checks: + +```yaml +# config/packages/monolog.yaml +monolog: + handlers: + collaboration_permissions: + type: stream + path: '%kernel.logs_dir%/collaboration_permissions.log' + channels: ['collaboration_permissions'] + level: debug +``` + +## Best practices + +### Role design + +- **Principle of least privilege**: Grant minimum necessary permissions +- **Clear role hierarchy**: Define clear relationships between roles +- **Regular review**: Periodically review and update permission assignments +- **Documentation**: Document custom roles and their intended use cases + +### External user management + +- **Time limits**: Use shorter session durations for external users +- **Content restrictions**: Limit external access to public or shared content +- **Review requirements**: Require internal review of external user contributions +- **Terms and conditions**: Require acceptance of collaboration terms + +### Monitoring and auditing + +- **Regular audits**: Review collaboration activity logs regularly +- **Permission changes**: Monitor changes to collaboration policies +- **Session analytics**: Track session usage and participation patterns +- **Security alerts**: Set up alerts for suspicious collaboration activity + +## Next steps + +- [Learn how to use collaborative editing](collaborative_editing_usage.md) +- [Explore the PHP API](collaborative_editing_api.md) +- [Set up custom events and notifications](collaborative_editing_events.md) \ No newline at end of file diff --git a/docs/content_management/collaborative_editing/collaborative_editing_usage.md b/docs/content_management/collaborative_editing/collaborative_editing_usage.md new file mode 100644 index 0000000000..bb674ab5a1 --- /dev/null +++ b/docs/content_management/collaborative_editing/collaborative_editing_usage.md @@ -0,0 +1,266 @@ +--- +description: Learn how to use collaborative editing to work with team members on content creation and editing. +--- + +# Using collaborative editing + +This guide walks you through the collaborative editing features, from creating your first collaboration session to managing shared drafts and working with real-time editing. + +## Starting a collaboration session + +### From content editing + +1. **Open content for editing**: Navigate to any content item and click "Edit" +2. **Start collaboration**: Look for the "Collaborate" or "Share" button in the toolbar +3. **Configure session**: + - Set session name (optional) + - Choose collaboration type (view-only, edit, or full access) + - Set expiration date +4. **Invite participants**: Add users by username, email, or user group + +![Starting collaboration from content edit](img/start_collaboration.png "Starting collaboration from content edit") + +### From draft management + +1. **Navigate to drafts**: Go to your dashboard and select "My drafts" +2. **Share existing draft**: Click the share icon next to any draft +3. **Configure sharing options**: Set permissions and expiration +4. **Send invitations**: Choose recipients and send invitations + +## Managing collaboration sessions + +### Session dashboard + +Access your collaboration sessions through the dedicated dashboard tabs: + +- **My shared drafts**: Content you've shared with others +- **Drafts shared with me**: Content others have shared with you + +![Collaboration dashboard](img/collaboration_dashboard.png "Collaboration dashboard") + +The dashboard shows: +- Session status (active, expired, completed) +- Number of participants +- Last activity timestamp +- Quick actions (edit, manage, end session) + +### Session details + +Click on any session to view detailed information: + +- **Participants list**: Who's currently in the session +- **Activity log**: Recent changes and comments +- **Session settings**: Permissions and expiration details +- **Actions**: End session, modify permissions, export changes + +## Working with invitations + +### Sending invitations + +When starting a collaboration session, you can invite: + +**Internal users**: +- Search by username or display name +- Select from user group membership +- Assign specific roles (viewer, editor, reviewer) + +**External users**: +- Enter email addresses +- Set limited access permissions +- Configure session duration (typically shorter than internal users) + +![Invitation interface](img/send_invitations.png "Invitation interface") + +### Receiving invitations + +When someone invites you to collaborate: + +1. **Email notification**: You'll receive an invitation email with session details +2. **Dashboard notification**: New sessions appear in "Drafts shared with me" +3. **Join session**: Click the invitation link or join from the dashboard +4. **Accept terms**: Review and accept collaboration terms (if configured) + +### Managing received invitations + +From the "Drafts shared with me" tab: + +- **Join active sessions**: Click "Join" to enter collaboration mode +- **View session info**: See who else is participating and session details +- **Leave sessions**: Exit collaboration if no longer needed +- **Notifications**: Configure how you want to be notified of session activity + +## Real-time collaboration features + +### Live editing + +When real-time editing is enabled, you'll see: + +**User cursors**: Live cursors showing where other users are currently editing + +**Instant updates**: Changes appear immediately as other users type + +**Conflict indicators**: Visual warnings when multiple users edit the same area + +**User presence**: List of currently active users in the session + +![Real-time editing interface](img/realtime_editing.png "Real-time editing interface") + +### Working asynchronously + +Even without real-time editing, you can collaborate effectively: + +**Change tracking**: All modifications are tracked with user attribution + +**Comment system**: Leave comments on specific content sections + +**Version comparison**: See what changed between your last visit + +**Merge assistance**: Guided merge process when conflicts occur + +## Collaboration workflow examples + +### Content review workflow + +**Scenario**: Blog post requiring legal and technical review + +1. **Author creates draft**: Marketing team writes initial blog post +2. **Share for review**: Author shares draft with legal and technical teams as "reviewers" +3. **Parallel review**: Both teams can review simultaneously, leaving comments +4. **Author incorporates feedback**: Marketing team addresses comments and updates content +5. **Final approval**: Reviewers confirm changes and approve for publication +6. **Publish**: Author publishes the final version + +### External collaboration + +**Scenario**: Working with external consultants on documentation + +1. **Create collaboration session**: Internal team creates draft and shares with consultants +2. **Time-limited access**: External users get 4-hour access windows +3. **Guided collaboration**: Internal users facilitate sessions and provide context +4. **Knowledge transfer**: External experts contribute specialized content +5. **Internal review**: Internal team reviews and finalizes content before publication + +### Multi-team content creation + +**Scenario**: Product announcement requiring input from multiple departments + +1. **Initial structure**: Product marketing creates content outline +2. **Parallel contribution**: Technical writers, designers, and developers contribute simultaneously +3. **Real-time coordination**: Teams see each other's work and coordinate in real-time +4. **Review and approval**: Management reviews consolidated content +5. **Launch coordination**: All teams finalize their sections before coordinated publication + +## Comments and feedback + +### Leaving comments + +Add comments to specific content sections: + +1. **Select text**: Highlight the content you want to comment on +2. **Add comment**: Click the comment button or use keyboard shortcut +3. **Write feedback**: Provide constructive feedback or ask questions +4. **Tag participants**: Use @mentions to notify specific users +5. **Submit**: Comment is immediately visible to all participants + +### Managing comments + +**Reply to comments**: Continue the conversation by replying to existing comments + +**Resolve comments**: Mark comments as resolved once addressed + +**Filter comments**: View only your comments, unresolved issues, or all feedback + +**Export comments**: Download comment summary for external review + +![Comment system](img/collaboration_comments.png "Comment system interface") + +## Session management + +### Ending sessions + +**Manual termination**: End active sessions when collaboration is complete + +**Automatic expiration**: Sessions end automatically based on configured timeouts + +**Graceful shutdown**: All participants receive notifications before session ends + +**Data preservation**: Comments and change history are preserved after session ends + +### Permissions during collaboration + +Participants can have different permission levels: + +**Viewer**: Can see content and leave comments but cannot edit + +**Editor**: Can modify content and participate fully in collaboration + +**Reviewer**: Can see changes, comment, and approve/reject modifications + +**Administrator**: Full control over session, including ending and managing participants + +### Monitoring activity + +Track collaboration progress through: + +**Activity feeds**: Real-time updates on participant actions + +**Change summaries**: Consolidated view of all modifications made during session + +**Participation metrics**: See who's contributing most actively + +**Time tracking**: Monitor how long users spend in sessions + +## Best practices + +### Before starting collaboration + +- **Define objectives**: Clearly communicate what you want to achieve +- **Set expectations**: Let participants know their roles and responsibilities +- **Prepare content**: Have a solid foundation before inviting others +- **Check permissions**: Ensure all participants have necessary access rights + +### During collaboration + +- **Stay focused**: Keep discussions relevant to the content being developed +- **Use comments effectively**: Provide specific, actionable feedback +- **Communicate changes**: Explain significant modifications you're making +- **Respect others' work**: Don't overwrite others' contributions without discussion + +### After collaboration + +- **Review changes**: Check all modifications before finalizing content +- **Archive sessions**: Properly close sessions when work is complete +- **Document decisions**: Preserve important discussion points and decisions +- **Follow up**: Ensure all feedback has been addressed + +## Troubleshooting common issues + +### Connection problems + +**Real-time sync issues**: Refresh the page or rejoin the session + +**Missing updates**: Check your internet connection and session status + +**Performance slowdown**: Reduce number of active participants or use asynchronous mode + +### Permission problems + +**Cannot edit content**: Verify you have edit permissions for the underlying content + +**Cannot invite users**: Check if you have collaboration/invite policy permissions + +**Access denied errors**: Ensure your user role includes necessary collaboration policies + +### Session management issues + +**Session expired**: Check expiration settings and renew if necessary + +**Cannot join session**: Verify invitation is still valid and you're logged in + +**Missing shared drafts**: Check if content has been moved or permissions changed + +## Next steps + +- [Configure advanced permissions](collaborative_editing_permissions.md) +- [Explore the PHP API](collaborative_editing_api.md) +- [Set up custom notifications](collaborative_editing_events.md) \ No newline at end of file diff --git a/docs/content_management/collaborative_editing/img/collaboration_comments.png b/docs/content_management/collaborative_editing/img/collaboration_comments.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/content_management/collaborative_editing/img/collaboration_comments.png.txt b/docs/content_management/collaborative_editing/img/collaboration_comments.png.txt new file mode 100644 index 0000000000..f5a4d522bd --- /dev/null +++ b/docs/content_management/collaborative_editing/img/collaboration_comments.png.txt @@ -0,0 +1 @@ +Placeholder for collaboration_comments.png diff --git a/docs/content_management/collaborative_editing/img/collaboration_dashboard.png b/docs/content_management/collaborative_editing/img/collaboration_dashboard.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/content_management/collaborative_editing/img/collaboration_dashboard.png.txt b/docs/content_management/collaborative_editing/img/collaboration_dashboard.png.txt new file mode 100644 index 0000000000..46fd678084 --- /dev/null +++ b/docs/content_management/collaborative_editing/img/collaboration_dashboard.png.txt @@ -0,0 +1 @@ +Placeholder for collaboration_dashboard.png diff --git a/docs/content_management/collaborative_editing/img/realtime_editing.png b/docs/content_management/collaborative_editing/img/realtime_editing.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/content_management/collaborative_editing/img/realtime_editing.png.txt b/docs/content_management/collaborative_editing/img/realtime_editing.png.txt new file mode 100644 index 0000000000..1cc2fd1005 --- /dev/null +++ b/docs/content_management/collaborative_editing/img/realtime_editing.png.txt @@ -0,0 +1 @@ +Placeholder for realtime_editing.png diff --git a/docs/content_management/collaborative_editing/img/send_invitations.png b/docs/content_management/collaborative_editing/img/send_invitations.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/content_management/collaborative_editing/img/send_invitations.png.txt b/docs/content_management/collaborative_editing/img/send_invitations.png.txt new file mode 100644 index 0000000000..d22f995d52 --- /dev/null +++ b/docs/content_management/collaborative_editing/img/send_invitations.png.txt @@ -0,0 +1 @@ +Placeholder for send_invitations.png diff --git a/docs/content_management/collaborative_editing/img/start_collaboration.png b/docs/content_management/collaborative_editing/img/start_collaboration.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/content_management/collaborative_editing/img/start_collaboration.png.txt b/docs/content_management/collaborative_editing/img/start_collaboration.png.txt new file mode 100644 index 0000000000..925031bd03 --- /dev/null +++ b/docs/content_management/collaborative_editing/img/start_collaboration.png.txt @@ -0,0 +1 @@ +Placeholder for start_collaboration.png diff --git a/docs/content_management/content_management.md b/docs/content_management/content_management.md index d83dda3271..d2849dbad7 100644 --- a/docs/content_management/content_management.md +++ b/docs/content_management/content_management.md @@ -14,5 +14,6 @@ page_type: landing_page "content_management/forms/forms", "content_management/taxonomy/taxonomy", "content_management/workflow/workflow", + "content_management/collaborative_editing/collaborative_editing_guide", "content_management/data_migration/data_migration", ], columns=4) =]] diff --git a/mkdocs.yml b/mkdocs.yml index ca209a9202..26df3fd747 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,6 +101,7 @@ nav: - Trash events: api/event_reference/trash_events.md - Twig Components: api/event_reference/twig_component_events.md - AI Action events: api/event_reference/ai_action_events.md + - Collaboration events: api/event_reference/collaboration_events.md - Discounts events: api/event_reference/discounts_events.md - Other events: api/event_reference/other_events.md - Administration: @@ -280,7 +281,12 @@ nav: - URL field type: content_management/field_types/field_type_reference/urlfield.md - User field type: content_management/field_types/field_type_reference/userfield.md - Collaborative editing: - - Collaborative editing product guide: content_management/collaborative_editing/collaborative_editing_guide.md + - Collaborative editing guide: content_management/collaborative_editing/collaborative_editing_guide.md + - Installation: content_management/collaborative_editing/collaborative_editing_installation.md + - Usage: content_management/collaborative_editing/collaborative_editing_usage.md + - Permissions: content_management/collaborative_editing/collaborative_editing_permissions.md + - PHP API: content_management/collaborative_editing/collaborative_editing_api.md + - Events and customization: content_management/collaborative_editing/collaborative_editing_events.md - Templating: - Templating: templating/templating.md - Render content: