diff --git a/appinfo/routes.php b/appinfo/routes.php index cd7665b2f0..6a494159de 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -34,6 +34,7 @@ ['name' => 'view#index', 'url' => '/{view}/{timeRange}', 'verb' => 'GET', 'requirements' => ['view' => 'timeGridDay|timeGridWeek|dayGridMonth|listMonth'], 'postfix' => 'view.timerange'], ['name' => 'view#index', 'url' => '/{view}/{timeRange}/new/{mode}/{isAllDay}/{dtStart}/{dtEnd}', 'verb' => 'GET', 'requirements' => ['view' => 'timeGridDay|timeGridWeek|dayGridMonth|listMonth'], 'postfix' => 'view.timerange.new'], ['name' => 'view#index', 'url' => '/{view}/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}', 'verb' => 'GET', 'requirements' => ['view' => 'timeGridDay|timeGridWeek|dayGridMonth|listMonth'], 'postfix' => 'view.timerange.edit'], + ['name' => 'view#getCalendarDotSvg', 'url' => '/public/getCalendarDotSvg/{color}.svg', 'verb' => 'GET'], // Appointments ['name' => 'appointment#index', 'url' => '/appointments/{userId}', 'verb' => 'GET'], ['name' => 'appointment#show', 'url' => '/appointment/{token}', 'verb' => 'GET'], diff --git a/css/dashboard.css b/css/dashboard.css index ceca5421de..5d95e425e7 100644 --- a/css/dashboard.css +++ b/css/dashboard.css @@ -1,8 +1,3 @@ .app-icon-calendar { background-image: url('../img/calendar-dark.svg'); } - -/* for NC <= 24 */ -body.theme--dark .app-icon-calendar { - background-image: url('../img/calendar.svg'); -} diff --git a/lib/Controller/ViewController.php b/lib/Controller/ViewController.php index 2cbf8335c4..521f2af59c 100644 --- a/lib/Controller/ViewController.php +++ b/lib/Controller/ViewController.php @@ -26,8 +26,12 @@ use OCA\Calendar\Service\Appointments\AppointmentConfigService; use OCP\App\IAppManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\IRequest; use function in_array; @@ -49,19 +53,23 @@ class ViewController extends Controller { /** @var string */ private $userId; + private IAppData $appData; + public function __construct(string $appName, IRequest $request, IConfig $config, AppointmentConfigService $appointmentConfigService, IInitialState $initialStateService, IAppManager $appManager, - ?string $userId) { + ?string $userId, + IAppData $appData) { parent::__construct($appName, $request); $this->config = $config; $this->appointmentConfigService = $appointmentConfigService; $this->initialStateService = $initialStateService; $this->appManager = $appManager; $this->userId = $userId; + $this->appData = $appData; } /** @@ -148,4 +156,36 @@ private function getView(string $view): string { return $view; } } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * This function makes the colour dots work for mobile widgets + * + * Returns an SVG with size32x32 if the hex colour is valid + * or a Nextcloud blue svg if the colour is not + * + * @param string $color - url encoded HEX colour + * @return FileDisplayResponse + * @throws NotFoundException + * @throws NotPermittedException + */ + public function getCalendarDotSvg(string $color = "#0082c9"): FileDisplayResponse { + $validColor = '#0082c9'; + $color = trim(urldecode($color), '#'); + if (preg_match('/^([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) { + $validColor = '#' . $color; + } + $svg = ''; + $folderName = implode('_', [ + 'calendar', + $this->userId + ]); + $folder = $this->appData->getFolder($folderName); + $file = $folder->newFile($color . '.svg', $svg); + $response = new FileDisplayResponse($file); + $response->cacheFor(24 * 3600); // 1 day + return $response; + } } diff --git a/lib/Dashboard/CalendarWidget.php b/lib/Dashboard/CalendarWidget.php index 802cbba4c5..b8c4461294 100644 --- a/lib/Dashboard/CalendarWidget.php +++ b/lib/Dashboard/CalendarWidget.php @@ -30,44 +30,52 @@ use DateTimeImmutable; use OCA\Calendar\AppInfo\Application; use OCA\Calendar\Service\JSDataService; -use OCA\DAV\CalDAV\CalDavBackend; use OCP\AppFramework\Services\IInitialState; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\IManager; use OCP\Dashboard\IAPIWidget; +use OCP\Dashboard\IButtonWidget; +use OCP\Dashboard\IIconWidget; +use OCP\Dashboard\Model\WidgetButton; use OCP\Dashboard\Model\WidgetItem; use OCP\IDateTimeFormatter; use OCP\IL10N; use OCP\IURLGenerator; use OCP\Util; -class CalendarWidget implements IAPIWidget { +class CalendarWidget implements IAPIWidget, IButtonWidget, IIconWidget { private IL10N $l10n; private IInitialState $initialStateService; private JSDataService $dataService; private IDateTimeFormatter $dateTimeFormatter; private IURLGenerator $urlGenerator; - private CalDavBackend $calDavBackend; + private IManager $calendarManager; + private ITimeFactory $timeFactory; /** * CalendarWidget constructor. + * * @param IL10N $l10n * @param IInitialState $initialStateService * @param JSDataService $dataService * @param IDateTimeFormatter $dateTimeFormatter * @param IURLGenerator $urlGenerator - * @param CalDavBackend $calDavBackend + * @param IManager $calendarManager */ public function __construct(IL10N $l10n, IInitialState $initialStateService, JSDataService $dataService, IDateTimeFormatter $dateTimeFormatter, IURLGenerator $urlGenerator, - CalDavBackend $calDavBackend) { + IManager $calendarManager, + ITimeFactory $timeFactory) { $this->l10n = $l10n; $this->initialStateService = $initialStateService; $this->dataService = $dataService; $this->dateTimeFormatter = $dateTimeFormatter; $this->urlGenerator = $urlGenerator; - $this->calDavBackend = $calDavBackend; + $this->calendarManager = $calendarManager; + $this->timeFactory = $timeFactory; } /** @@ -105,6 +113,15 @@ public function getUrl(): ?string { return null; } + /** + * @inheritDoc + */ + public function getIconUrl(): string { + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath(Application::APP_ID, 'calendar-dark.svg') + ); + } + /** * @inheritDoc */ @@ -119,44 +136,63 @@ public function load(): void { /** * @inheritDoc + * + * @param string|null $since Use any PHP DateTime allowed values to get future dates + * @param int $limit Max 14 items is the default */ - public function getItems(string $userId, ?string $since = null, int $limit = 7): array { - $calendars = $this->calDavBackend->getCalendarsForUser('principals/users/' . $userId); - $dateTimeNow = new DateTimeImmutable(); - $inTwoWeeks = $dateTimeNow->add(new DateInterval('P14D')); - $events = []; + public function getItems(string $userId, ?string $since = null, int $limit = 14): array { + $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId); + $count = count($calendars); + if ($count === 0) { + return []; + } + $limitPerCalendar = (int)round(($limit / $count)) ?? 1; + $dateTime = (new DateTimeImmutable())->setTimestamp($this->timeFactory->getTime()); + if ($since !== null) { + try { + $dateTime = new DateTimeImmutable($since); + } catch (\Exception $e) { + // silently drop the exception and proceed with "now" + } + } + $inTwoWeeks = $dateTime->add(new DateInterval('P14D')); $options = [ 'timerange' => [ - 'start' => $dateTimeNow, + 'start' => $dateTime, 'end' => $inTwoWeeks, ] ]; - $searchLimit = null; - foreach ($calendars as $calKey => $calendar) { - $searchResult = array_map(static function($event) use ($calendar, $dateTimeNow) { - $event['calendar_color'] = $calendar['{http://apple.com/ns/ical/}calendar-color']; - return $event; - }, $this->calDavBackend->search($calendar, '', [], $options, $searchLimit, 0)); - array_push($events, ...$searchResult); + $widgetItems = []; + foreach ($calendars as $calendar) { + $searchResult = $calendar->search('', [], $options, $limitPerCalendar); + foreach ($searchResult as $calendarEvent) { + /** @var DateTimeImmutable $startDate */ + $startDate = $calendarEvent['objects'][0]['DTSTART'][0]; + $widget = new WidgetItem( + $calendarEvent['objects'][0]['SUMMARY'][0] ?? 'New Event', + $this->dateTimeFormatter->formatTimeSpan(DateTime::createFromImmutable($startDate)), + $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.index', ['objectId' => $calendarEvent['uid']])), + $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.getCalendarDotSvg', ['color' => $calendar->getDisplayColor() ?? '#0082c9'])), // default NC blue fallback + (string) $startDate->getTimestamp(), + ); + $widgetItems[] = $widget; + } } - // for each event, is there a simple way to get the first occurrence in the future? + return $widgetItems; + } - return array_map(function(array $event) { - /** @var DateTimeImmutable $startDate */ - $startDate = $event['objects'][0]['DTSTART'][0]; - return new WidgetItem( - $event['objects'][0]['SUMMARY'][0] ?? '', - $this->dateTimeFormatter->formatTimeSpan(DateTime::createFromImmutable($startDate)), - // TODO fix this route and get the correct objectId + /** + * @inheritDoc + */ + public function getWidgetButtons(string $userId): array { + return [ + new WidgetButton( + WidgetButton::TYPE_MORE, $this->urlGenerator->getAbsoluteURL( - $this->urlGenerator->linkToRoute('calendar.view.index', ['objectId' => $event['uid']]) + $this->urlGenerator->linkToRoute(Application::APP_ID . '.view.index') ), - // TODO find or implement and endpoint providing colored dots images - // reminder: we can use the event color and the calendar color as a fallback - '', - // TODO this should be the next occurence date - (string) $startDate->getTimestamp(), - ); - }, $events); + $this->l10n->t('More events') + ), + ]; } } diff --git a/tests/php/unit/Controller/ViewControllerTest.php b/tests/php/unit/Controller/ViewControllerTest.php index 20a938329f..e866754e06 100755 --- a/tests/php/unit/Controller/ViewControllerTest.php +++ b/tests/php/unit/Controller/ViewControllerTest.php @@ -30,6 +30,7 @@ use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; +use OCP\Files\IAppData; use OCP\IConfig; use OCP\IRequest; use ChristophWurst\Nextcloud\Testing\TestCase; @@ -61,6 +62,9 @@ class ViewControllerTest extends TestCase { /** @var ViewController */ private $controller; + /** @var IAppData|MockObject */ + private $appData; + protected function setUp(): void { $this->appName = 'calendar'; $this->request = $this->createMock(IRequest::class); @@ -69,6 +73,7 @@ protected function setUp(): void { $this->appointmentContfigService = $this->createMock(AppointmentConfigService::class); $this->initialStateService = $this->createMock(IInitialState::class); $this->userId = 'user123'; + $this->appData = $this->createMock(IAppData::class); $this->controller = new ViewController( $this->appName, @@ -78,6 +83,7 @@ protected function setUp(): void { $this->initialStateService, $this->appManager, $this->userId, + $this->appData, ); } diff --git a/tests/php/unit/Dashbaord/CalendarWidgetTest.php b/tests/php/unit/Dashbaord/CalendarWidgetTest.php index 98a6a10221..58907b2c32 100644 --- a/tests/php/unit/Dashbaord/CalendarWidgetTest.php +++ b/tests/php/unit/Dashbaord/CalendarWidgetTest.php @@ -23,33 +23,69 @@ */ namespace OCA\Calendar\Dashboard; -use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Calendar\Service\JSDataService; -use OCP\IInitialStateService; +use OCA\DAV\CalDAV\CalendarImpl; +use OCP\AppFramework\Services\IInitialState; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\IManager; +use OCP\Dashboard\IButtonWidget; +use OCP\Dashboard\Model\WidgetItem; +use OCP\IDateTimeFormatter; use OCP\IL10N; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Safe\DateTimeImmutable; +use Test\TestCase; class CalendarWidgetTest extends TestCase { - /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IL10N|MockObject */ private $l10n; - /** @var IInitialStateService|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IInitialState|MockObject */ private $initialState; - /** @var JSDataService|\PHPUnit\Framework\MockObject\MockObject */ + /** @var JSDataService|MockObject */ private $service; - /** @var CalendarWidget */ - private $widget; + private CalendarWidget $widget; + + /** @var IDateTimeFormatter|MockObject */ + private $dateTimeFormatter; + + /** @var IURLGenerator|MockObject */ + private $urlGenerator; + + /** @var IManager|MockObject */ + private $calendarManager; + + /** @var ITimeFactory|MockObject */ + private $timeFactory; protected function setUp(): void { parent::setUp(); + if (!interface_exists(IButtonWidget::class)) { + self::markTestIncomplete(); + } + $this->l10n = $this->createMock(IL10N::class); - $this->initialState = $this->createMock(IInitialStateService::class); + $this->initialState = $this->createMock(IInitialState::class); $this->service = $this->createMock(JSDataService::class); - - $this->widget = new CalendarWidget($this->l10n, $this->initialState, $this->service); + $this->dateTimeFormatter = $this->createMock(IDateTimeFormatter::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->calendarManager = $this->createMock(IManager::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->widget = new CalendarWidget( + $this->l10n, + $this->initialState, + $this->service, + $this->dateTimeFormatter, + $this->urlGenerator, + $this->calendarManager, + $this->timeFactory, + ); } public function testGetId(): void { @@ -69,7 +105,7 @@ public function testGetOrder(): void { } public function testGetIconClass(): void { - $this->assertEquals('icon-calendar-dark', $this->widget->getIconClass()); + $this->assertEquals('app-icon-calendar', $this->widget->getIconClass()); } public function testGetUrl(): void { @@ -79,11 +115,76 @@ public function testGetUrl(): void { public function testLoad(): void { $this->initialState->expects($this->once()) ->method('provideLazyInitialState') - ->with('calendar', 'dashboard_data', $this->callback(function ($actual) { + ->with('dashboard_data', $this->callback(function ($actual) { $fnResult = $actual(); return $fnResult === $this->service; })); $this->widget->load(); } + + public function testGetItems() : void { + $userId = 'admin'; + $calendar = $this->createMock(CalendarImpl::class); + self::invokePrivate($calendar, 'calendarInfo', [['{http://apple.com/ns/ical/}calendar-color' => '#ffffff']]); + $calendars = [$calendar]; + $time = 1665550936; + $start = (new DateTimeImmutable())->setTimestamp($time); + $twoWeeks = $start->add(new \DateInterval('P14D')); + $options = [ + 'timerange' => [ + 'start' => $start, + 'end' => $twoWeeks, + ] + ]; + $limit = 14; + $result = [ + 'id' => '3599', + 'uid' => '59d30b6c-5a31-4d28-b1d6-c8f928180e96', + 'uri' => '60EE4FCB-2144-4811-BBD3-FFEA44739F40.ics', + 'objects' => [ + [ + 'DTSTART' => [ + $start + ], + 'SUMMARY' => [ + 'Test', + ] + ] + ] + ]; + + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with('principals/users/' . $userId) + ->willReturn($calendars); + $this->timeFactory->expects(self::once()) + ->method('getTime') + ->willReturn($time); + $calendar->expects(self::once()) + ->method('search') + ->with('', [], $options, $limit) + ->willReturn([$result]); + $calendar->expects(self::once()) + ->method('getDisplayColor') + ->willReturn('#ffffff'); + $this->dateTimeFormatter->expects(self::once()) + ->method('formatTimeSpan') + ->willReturn('12345678'); + $this->urlGenerator->expects(self::exactly(2)) + ->method('getAbsoluteURL') + ->willReturnOnConsecutiveCalls('59d30b6c-5a31-4d28-b1d6-c8f928180e96', '#ffffff'); + + $widget = new WidgetItem( + $result['objects'][0]['SUMMARY'][0], + '12345678', + '59d30b6c-5a31-4d28-b1d6-c8f928180e96', + '#ffffff', + (string) $start->getTimestamp(), + ); + + $widgets = $this->widget->getItems($userId); + $this->assertCount(1, $widgets); + $this->assertEquals($widgets[0], $widget); + } }