From f5c474a264ed6b73cc6892d05bae3afdddf409af Mon Sep 17 00:00:00 2001 From: Jonas Date: Sat, 7 Mar 2026 00:58:39 +0100 Subject: [PATCH] fix: add event reference provider and widget Fixes: #7104 Signed-off-by: Jonas --- lib/AppInfo/Application.php | 3 + lib/Reference/EventReferenceProvider.php | 188 +++++++++++++++++++++++ psalm.xml | 2 + src/reference.js | 13 ++ src/views/EventReferenceWidget.vue | 163 ++++++++++++++++++++ 5 files changed, 369 insertions(+) create mode 100644 lib/Reference/EventReferenceProvider.php create mode 100644 src/views/EventReferenceWidget.vue diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b0409bc4c7..082a84949d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -15,6 +15,7 @@ use OCA\Calendar\Listener\UserDeletedListener; use OCA\Calendar\Notification\Notifier; use OCA\Calendar\Profile\AppointmentsAction; +use OCA\Calendar\Reference\EventReferenceProvider; use OCA\Calendar\Reference\ReferenceProvider; use OCA\Calendar\UserMigration\Migrator; use OCP\AppFramework\App; @@ -52,6 +53,8 @@ public function register(IRegistrationContext $context): void { $context->registerProfileLinkAction(AppointmentsAction::class); + $context->registerReferenceProvider(EventReferenceProvider::class); + $context->registerReferenceProvider(ReferenceProvider::class); $context->registerEventListener(BeforeAppointmentBookedEvent::class, AppointmentBookedListener::class); diff --git a/lib/Reference/EventReferenceProvider.php b/lib/Reference/EventReferenceProvider.php new file mode 100644 index 0000000000..6c93f70cfb --- /dev/null +++ b/lib/Reference/EventReferenceProvider.php @@ -0,0 +1,188 @@ +l10n->t('Calendar event'); + } + + #[\Override] + public function getOrder(): int { + return 21; + } + + #[\Override] + public function getIconUrl(): string { + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath(Application::APP_ID, 'calendar-dark.svg') + ); + } + + #[\Override] + public function matchReference(string $referenceText): bool { + $start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID); + $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID); + + foreach ([$start, $startIndex] as $base) { + $quoted = preg_quote($base, '/'); + + // URL pattern: .../apps/calendar/object/{uid}[/{recurrenceId}] + if (preg_match('/^' . $quoted . '\/object\/[^\/?#]+(?:\/[^\/?#]+)?\/?$/i', $referenceText) === 1) { + return true; + } + } + + return false; + } + + #[\Override] + public function resolveReference(string $referenceText): ?IReference { + if ($this->userId === null || !$this->matchReference($referenceText)) { + return null; + } + + $uid = $this->getUidFromUrl($referenceText); + if ($uid === null) { + return null; + } + + $resolved = $this->objectResolverService->findByUid($this->userId, $uid); + if ($resolved === null) { + return null; + } + + $calendar = $this->getCalendar($resolved['calendarUri']); + if ($calendar === null || $calendar->isDeleted()) { + return null; + } + + $eventData = $this->getEventData($calendar, $resolved['objectUri']); + if ($eventData === null) { + return null; + } + + $reference = new Reference($referenceText); + $reference->setTitle($eventData['title']); + $reference->setDescription($eventData['date'] ?? $calendar->getDisplayName()); + $reference->setRichObject( + 'calendar_event', + [ + 'title' => $eventData['title'], + 'calendarName' => $calendar->getDisplayName(), + 'calendarColor' => $calendar->getDisplayColor(), + 'date' => $eventData['date'], + 'startTimestamp' => $eventData['startTimestamp'], + 'endTimestamp' => $eventData['endTimestamp'], + 'url' => $referenceText, + ] + ); + return $reference; + } + + private function getUidFromUrl(string $url): ?string { + // URL pattern: .../apps/calendar/object/{uid}[/{recurrenceId}] + if (preg_match('/\/object\/([^\/?#]+)/i', $url, $matches) === 1) { + return $matches[1]; + } + return null; + } + + private function getCalendar(string $calendarUri): ?ICalendar { + $principalUri = 'principals/users/' . $this->userId; + $calendars = $this->calendarManager->getCalendarsForPrincipal($principalUri, [$calendarUri]); + return $calendars[0] ?? null; + } + + private function getEventData(ICalendar $calendar, string $eventFile): ?array { + $event = null; + foreach ($calendar->search('') as $result) { + if (($result['uri'] ?? null) === $eventFile) { + $event = $result; + break; + } + } + if ($event === null) { + return null; + } + + $object = $event['objects'][0] ?? null; + if ($object === null) { + return null; + + } + + $date = null; + $startTimestamp = null; + /** @var \DateTimeInterface|null $dtStart */ + $dtStart = $object['DTSTART'][0] ?? null; + if ($dtStart instanceof \DateTimeInterface) { + $date = $this->dateTimeFormatter->formatTimeSpan(\DateTime::createFromInterface($dtStart)); + $startTimestamp = $dtStart->getTimestamp(); + } + + $endTimestamp = null; + /** @var \DateTimeInterface|null $dtEnd */ + $dtEnd = $object['DTEND'][0] ?? null; + if ($dtEnd instanceof \DateTimeInterface) { + $endTimestamp = $dtEnd->getTimestamp(); + } elseif ($startTimestamp !== null) { + $duration = $object['DURATION'][0] ?? null; + if ($duration instanceof \DateInterval) { + $endTimestamp = (new \DateTime())->setTimestamp($startTimestamp)->add($duration)->getTimestamp(); + } + } + + return [ + 'title' => $object['SUMMARY'][0] ?? $this->l10n->t('Untitled event'), + 'date' => $date, + 'startTimestamp' => $startTimestamp, + 'endTimestamp' => $endTimestamp, + 'location' => $object['LOCATION'][0] ?? null, + ]; + } + + #[\Override] + public function getCachePrefix(string $referenceId): string { + return $this->userId ?? ''; + } + + #[\Override] + public function getCacheKey(string $referenceId): string { + return $referenceId; + } +} diff --git a/psalm.xml b/psalm.xml index 3fa04ae5dc..507071dfc8 100644 --- a/psalm.xml +++ b/psalm.xml @@ -37,6 +37,7 @@ + @@ -45,6 +46,7 @@ + diff --git a/src/reference.js b/src/reference.js index dfb01581d3..f674de7fb8 100644 --- a/src/reference.js +++ b/src/reference.js @@ -43,3 +43,16 @@ registerWidget('calendar_widget', async (el, { richObjectType, richObject, acces }, (el, renderResult) => { renderResult.object.$destroy() }, true) + +registerWidget('calendar_event', async (el, { richObject }) => { + const { createApp } = await import('vue') + const { default: EventReferenceWidget } = await import('./views/EventReferenceWidget.vue') + + const app = createApp(EventReferenceWidget, { + richObject, + }) + app.mount(el) + return new NcCustomPickerRenderResult(el, app) +}, (el, renderResult) => { + renderResult.object.$destroy() +}) diff --git a/src/views/EventReferenceWidget.vue b/src/views/EventReferenceWidget.vue new file mode 100644 index 0000000000..f4876185ae --- /dev/null +++ b/src/views/EventReferenceWidget.vue @@ -0,0 +1,163 @@ + + + + + + +