@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorCalendarEventSearchEngine
4 extends PhabricatorApplicationSearchEngine {
5
6 private $calendarYear;
7 private $calendarMonth;
8 private $calendarDay;
9
10 public function getResultTypeDescription() {
11 return pht('Calendar Events');
12 }
13
14 public function getApplicationClassName() {
15 return PhabricatorCalendarApplication::class;
16 }
17
18 /**
19 * @return PhabricatorCalendarEventQuery
20 */
21 public function newQuery() {
22 $viewer = $this->requireViewer();
23
24 return id(new PhabricatorCalendarEventQuery())
25 ->needRSVPs(array($viewer->getPHID()));
26 }
27
28 protected function shouldShowOrderField() {
29 return false;
30 }
31
32 protected function buildCustomSearchFields() {
33 return array(
34 id(new PhabricatorSearchDatasourceField())
35 ->setLabel(pht('Hosts'))
36 ->setKey('hostPHIDs')
37 ->setAliases(array('host', 'hostPHID', 'hosts'))
38 ->setDescription(
39 pht('Search for events created by specific hosts.'))
40 ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()),
41 id(new PhabricatorSearchDatasourceField())
42 ->setLabel(pht('Invited'))
43 ->setKey('invitedPHIDs')
44 ->setDescription(
45 pht('Search for events with specific invited users.'))
46 ->setDatasource(new PhabricatorCalendarInviteeDatasource()),
47 id(new PhabricatorSearchDateControlField())
48 ->setLabel(pht('Occurs After'))
49 ->setKey('rangeStart'),
50 id(new PhabricatorSearchDateControlField())
51 ->setLabel(pht('Occurs Before'))
52 ->setKey('rangeEnd')
53 ->setAliases(array('rangeEnd')),
54 id(new PhabricatorSearchCheckboxesField())
55 ->setKey('upcoming')
56 ->setOptions(array(
57 'upcoming' => pht('Show only upcoming events.'),
58 )),
59 id(new PhabricatorSearchSelectField())
60 ->setLabel(pht('Cancelled Events'))
61 ->setKey('isCancelled')
62 ->setDescription(pht('Search for active or cancelled events.'))
63 ->setOptions($this->getCancelledOptions())
64 ->setDefault('active'),
65 id(new PhabricatorPHIDsSearchField())
66 ->setLabel(pht('Import Sources'))
67 ->setKey('importSourcePHIDs')
68 ->setDescription(
69 pht('Search for events with specific import sources.'))
70 ->setAliases(array('importSourcePHID')),
71 id(new PhabricatorSearchSelectField())
72 ->setLabel(pht('Display Options'))
73 ->setKey('display')
74 ->setDescription(
75 pht('Display events in a certain display format.'))
76 ->setOptions($this->getViewOptions())
77 ->setDefault('month'),
78 );
79 }
80
81 private function getCancelledOptions() {
82 return array(
83 'active' => pht('Active Events Only'),
84 'cancelled' => pht('Cancelled Events Only'),
85 'both' => pht('Both Cancelled and Active Events'),
86 );
87 }
88
89 private function getViewOptions() {
90 return array(
91 'month' => pht('Month View'),
92 'day' => pht('Day View'),
93 'list' => pht('List View'),
94 );
95 }
96
97 public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
98 $query = parent::buildQueryFromSavedQuery($saved);
99
100 // If this is an export query for generating an ".ics" file, don't
101 // build ghost events.
102 if ($saved->getParameter('export')) {
103 $query->setGenerateGhosts(false);
104 }
105
106 return $query;
107 }
108
109 /**
110 * @return PhabricatorCalendarEventQuery
111 */
112 protected function buildQueryFromParameters(array $map) {
113 $query = $this->newQuery();
114 $viewer = $this->requireViewer();
115
116 if ($map['hostPHIDs']) {
117 $query->withHostPHIDs($map['hostPHIDs']);
118 }
119
120 if ($map['invitedPHIDs']) {
121 $query->withInvitedPHIDs($map['invitedPHIDs']);
122 }
123
124 $range_start = $map['rangeStart'];
125 $range_end = $map['rangeEnd'];
126 $display = $map['display'];
127
128 if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') {
129 $upcoming = true;
130 } else {
131 $upcoming = false;
132 }
133
134 list($range_start, $range_end) = $this->getQueryDateRange(
135 $range_start,
136 $range_end,
137 $display,
138 $upcoming);
139
140 $query->withDateRange($range_start, $range_end);
141
142 switch ($map['isCancelled']) {
143 case 'active':
144 $query->withIsCancelled(false);
145 break;
146 case 'cancelled':
147 $query->withIsCancelled(true);
148 break;
149 }
150
151 if ($map['importSourcePHIDs']) {
152 $query->withImportSourcePHIDs($map['importSourcePHIDs']);
153 }
154
155 if (empty($map['ids']) && empty($map['phids'])) {
156 $query
157 ->withIsStub(false)
158 ->setGenerateGhosts(true);
159 }
160
161 return $query;
162 }
163
164 private function getQueryDateRange(
165 $start_date_wild,
166 $end_date_wild,
167 $display,
168 $upcoming) {
169
170 $start_date_value = $this->getSafeDate($start_date_wild);
171 $end_date_value = $this->getSafeDate($end_date_wild);
172
173 $viewer = $this->requireViewer();
174 $timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
175 $min_range = null;
176 $max_range = null;
177
178 $min_range = $start_date_value->getEpoch();
179 $max_range = $end_date_value->getEpoch();
180
181 if ($display == 'month' || $display == 'day') {
182 list($start_year, $start_month, $start_day) =
183 $this->getDisplayYearAndMonthAndDay($min_range, $max_range, $display);
184
185 $start_day = new DateTime(
186 "{$start_year}-{$start_month}-{$start_day}",
187 $timezone);
188 $next = clone $start_day;
189
190 if ($display == 'month') {
191 $next->modify('+1 month');
192 } else if ($display == 'day') {
193 $next->modify('+7 day');
194 }
195
196 $display_start = $start_day->format('U');
197 $display_end = $next->format('U');
198
199 $start_of_week = $viewer->getUserSetting(
200 PhabricatorWeekStartDaySetting::SETTINGKEY);
201
202 $end_of_week = ($start_of_week + 6) % 7;
203
204 $first_of_month = $start_day->format('w');
205 $last_of_month = id(clone $next)->modify('-1 day')->format('w');
206
207 if (!$min_range || ($min_range < $display_start)) {
208 $min_range = $display_start;
209
210 if ($display == 'month' &&
211 $first_of_month !== $start_of_week) {
212 $interim_day_num = ($first_of_month + 7 - $start_of_week) % 7;
213 $min_range = id(clone $start_day)
214 ->modify('-'.$interim_day_num.' days')
215 ->format('U');
216 }
217 }
218 if (!$max_range || ($max_range > $display_end)) {
219 $max_range = $display_end;
220
221 if ($display == 'month' &&
222 $last_of_month !== $end_of_week) {
223 $interim_day_num = ($end_of_week + 7 - $last_of_month) % 7;
224 $max_range = id(clone $next)
225 ->modify('+'.$interim_day_num.' days')
226 ->format('U');
227 }
228 }
229 }
230
231 if ($upcoming) {
232 $now = PhabricatorTime::getNow();
233 if ($min_range) {
234 $min_range = max($now, $min_range);
235 } else {
236 $min_range = $now;
237 }
238 }
239
240 return array($min_range, $max_range);
241 }
242
243 protected function getURI($path) {
244 return '/calendar/'.$path;
245 }
246
247 protected function getBuiltinQueryNames() {
248 $names = array(
249 'month' => pht('Month View'),
250 'day' => pht('Day View'),
251 'upcoming' => pht('Upcoming Events'),
252 'all' => pht('All Events'),
253 );
254
255 return $names;
256 }
257
258 public function setCalendarYearAndMonthAndDay($year, $month, $day = null) {
259 $this->calendarYear = $year;
260 $this->calendarMonth = $month;
261 $this->calendarDay = $day;
262
263 return $this;
264 }
265
266 public function buildSavedQueryFromBuiltin($query_key) {
267 $query = $this->newSavedQuery();
268 $query->setQueryKey($query_key);
269
270 switch ($query_key) {
271 case 'month':
272 return $query->setParameter('display', 'month');
273 case 'day':
274 return $query->setParameter('display', 'day');
275 case 'upcoming':
276 return $query
277 ->setParameter('display', 'list')
278 ->setParameter('upcoming', array(
279 0 => 'upcoming',
280 ));
281 case 'all':
282 return $query;
283 }
284
285 return parent::buildSavedQueryFromBuiltin($query_key);
286 }
287
288 /**
289 * @param array<PhabricatorCalendarEvent> $events
290 * @param PhabricatorSavedQuery $query
291 * @param array<PhabricatorObjectHandle> $handles
292 * @return PhabricatorApplicationSearchResultView
293 */
294 protected function renderResultList(
295 array $events,
296 PhabricatorSavedQuery $query,
297 array $handles) {
298
299 if ($this->isMonthView($query)) {
300 $result = $this->buildCalendarMonthView($events, $query);
301 } else if ($this->isDayView($query)) {
302 $result = $this->buildCalendarDayView($events, $query);
303 } else {
304 $result = $this->buildCalendarListView($events, $query);
305 }
306
307 return $result;
308 }
309
310 /**
311 * @param array<PhabricatorCalendarEvent> $events
312 * @param PhabricatorSavedQuery $query
313 * @return PhabricatorApplicationSearchResultView
314 */
315 private function buildCalendarListView(
316 array $events,
317 PhabricatorSavedQuery $query) {
318
319 assert_instances_of($events, PhabricatorCalendarEvent::class);
320 $viewer = $this->requireViewer();
321 $list = new PHUIObjectItemListView();
322
323 foreach ($events as $event) {
324 if ($event->getIsGhostEvent()) {
325 $monogram = $event->getParentEvent()->getMonogram();
326 $index = $event->getSequenceIndex();
327 $monogram = "{$monogram}/{$index}";
328 } else {
329 $monogram = $event->getMonogram();
330 }
331
332 $item = id(new PHUIObjectItemView())
333 ->setUser($viewer)
334 ->setObject($event)
335 ->setObjectName($monogram)
336 ->setHeader($event->getName())
337 ->setHref($event->getURI());
338
339 $item->addAttribute($event->renderEventDate($viewer, false));
340
341 if ($event->getIsCancelled()) {
342 $item->setDisabled(true);
343 }
344
345 $status_icon = $event->getDisplayIcon($viewer);
346 $status_color = $event->getDisplayIconColor($viewer);
347 $status_label = $event->getDisplayIconLabel($viewer);
348
349 $item->setStatusIcon("{$status_icon} {$status_color}", $status_label);
350
351 $host = pht(
352 'Hosted by %s',
353 $viewer->renderHandle($event->getHostPHID()));
354 $item->addByline($host);
355
356 $list->addItem($item);
357 }
358
359 return $this->newResultView()
360 ->setObjectList($list)
361 ->setNoDataString(pht('No events found.'));
362 }
363
364 /**
365 * @param array<PhabricatorCalendarEvent> $events
366 * @param PhabricatorSavedQuery $query
367 * @return PhabricatorApplicationSearchResultView
368 */
369 private function buildCalendarMonthView(
370 array $events,
371 PhabricatorSavedQuery $query) {
372 assert_instances_of($events, PhabricatorCalendarEvent::class);
373
374 $viewer = $this->requireViewer();
375 $now = PhabricatorTime::getNow();
376
377 list($start_year, $start_month) =
378 $this->getDisplayYearAndMonthAndDay(
379 $this->getQueryDateFrom($query)->getEpoch(),
380 $this->getQueryDateTo($query)->getEpoch(),
381 $query->getParameter('display'));
382
383 $now_year = phabricator_format_local_time($now, $viewer, 'Y');
384 $now_month = phabricator_format_local_time($now, $viewer, 'm');
385 $now_day = phabricator_format_local_time($now, $viewer, 'j');
386
387 if ($start_month == $now_month && $start_year == $now_year) {
388 $month_view = new PHUICalendarMonthView(
389 $this->getQueryDateFrom($query),
390 $this->getQueryDateTo($query),
391 $start_month,
392 $start_year,
393 $now_day);
394 } else {
395 $month_view = new PHUICalendarMonthView(
396 $this->getQueryDateFrom($query),
397 $this->getQueryDateTo($query),
398 $start_month,
399 $start_year);
400 }
401
402 $month_view->setViewer($viewer);
403
404 $viewer_phid = $viewer->getPHID();
405 foreach ($events as $event) {
406 $epoch_min = $event->getStartDateTimeEpoch();
407 $epoch_max = $event->getEndDateTimeEpoch();
408
409 $is_invited = $event->isRSVPInvited($viewer_phid);
410 $is_attending = $event->getIsUserAttending($viewer_phid);
411
412 $event_view = id(new AphrontCalendarEventView())
413 ->setHostPHID($event->getHostPHID())
414 ->setEpochRange($epoch_min, $epoch_max)
415 ->setIsCancelled($event->getIsCancelled())
416 ->setName($event->getName())
417 ->setURI($event->getURI())
418 ->setIsAllDay($event->getIsAllDay())
419 ->setIcon($event->getDisplayIcon($viewer))
420 ->setViewerIsInvited($is_invited || $is_attending)
421 ->setDatetimeSummary($event->renderEventDate($viewer, true))
422 ->setIconColor($event->getDisplayIconColor($viewer));
423
424 $month_view->addEvent($event_view);
425 }
426
427 $month_view->setBrowseURI(
428 $this->getURI('query/'.$query->getQueryKey().'/'));
429
430 $from = $this->getQueryDateFrom($query)->getDateTime();
431
432 $crumbs = array();
433 $crumbs[] = id(new PHUICrumbView())
434 ->setName($from->format('F Y'));
435
436 $header = id(new PHUIHeaderView())
437 ->setProfileHeader(true)
438 ->setHeader($from->format('F Y'));
439
440 return $this->newResultView($month_view)
441 ->setCrumbs($crumbs)
442 ->setHeader($header);
443 }
444
445 /**
446 * @param array<PhabricatorCalendarEvent> $events
447 * @param PhabricatorSavedQuery $query
448 * @return PhabricatorApplicationSearchResultView
449 */
450 private function buildCalendarDayView(
451 array $events,
452 PhabricatorSavedQuery $query) {
453
454 $viewer = $this->requireViewer();
455
456 list($start_year, $start_month, $start_day) =
457 $this->getDisplayYearAndMonthAndDay(
458 $this->getQueryDateFrom($query)->getEpoch(),
459 $this->getQueryDateTo($query)->getEpoch(),
460 $query->getParameter('display'));
461
462 $day_view = id(new PHUICalendarDayView(
463 $this->getQueryDateFrom($query),
464 $this->getQueryDateTo($query),
465 $start_year,
466 $start_month,
467 $start_day))
468 ->setQuery($query->getQueryKey());
469
470 $day_view->setUser($viewer);
471
472 $phids = mpull($events, 'getHostPHID');
473
474 foreach ($events as $event) {
475 $can_edit = PhabricatorPolicyFilter::hasCapability(
476 $viewer,
477 $event,
478 PhabricatorPolicyCapability::CAN_EDIT);
479
480 $epoch_min = $event->getStartDateTimeEpoch();
481 $epoch_max = $event->getEndDateTimeEpoch();
482
483 $status_icon = $event->getDisplayIcon($viewer);
484 $status_color = $event->getDisplayIconColor($viewer);
485
486 $event_view = id(new AphrontCalendarEventView())
487 ->setCanEdit($can_edit)
488 ->setEventID($event->getID())
489 ->setEpochRange($epoch_min, $epoch_max)
490 ->setIsAllDay($event->getIsAllDay())
491 ->setIcon($status_icon)
492 ->setIconColor($status_color)
493 ->setName($event->getName())
494 ->setURI($event->getURI())
495 ->setDatetimeSummary($event->renderEventDate($viewer, true))
496 ->setIsCancelled($event->getIsCancelled());
497
498 $day_view->addEvent($event_view);
499 }
500
501 $browse_uri = $this->getURI('query/'.$query->getQueryKey().'/');
502 $day_view->setBrowseURI($browse_uri);
503
504 $from = $this->getQueryDateFrom($query)->getDateTime();
505 $month_uri = $browse_uri.$from->format('Y/m/');
506
507 $crumbs = array(
508 id(new PHUICrumbView())
509 ->setName($from->format('F Y'))
510 ->setHref($month_uri),
511 id(new PHUICrumbView())
512 ->setName($from->format('D jS')),
513 );
514
515 $header = id(new PHUIHeaderView())
516 ->setProfileHeader(true)
517 ->setHeader($from->format('D, F jS'));
518
519 return $this->newResultView($day_view)
520 ->setCrumbs($crumbs)
521 ->setHeader($header);
522 }
523
524 /**
525 * @param string|null $range_start Epoch
526 * @param string|null $range_end Epoch
527 * @param string $display View, such as "month" or "day"
528 * @return array<string|int, string|int, string|int> YYYY, M, D
529 */
530 private function getDisplayYearAndMonthAndDay(
531 $range_start,
532 $range_end,
533 $display) {
534
535 $viewer = $this->requireViewer();
536 $epoch = null;
537
538 if ($this->calendarYear && $this->calendarMonth) {
539 $start_year = $this->calendarYear;
540 $start_month = $this->calendarMonth;
541 $start_day = $this->calendarDay ? $this->calendarDay : 1;
542 } else {
543 if ($range_start) {
544 $epoch = $range_start;
545 } else if ($range_end) {
546 $epoch = $range_end;
547 } else {
548 $epoch = time();
549 }
550 if ($display == 'month') {
551 $day = 1;
552 } else {
553 $day = phabricator_format_local_time($epoch, $viewer, 'd');
554 }
555 $start_year = phabricator_format_local_time($epoch, $viewer, 'Y');
556 $start_month = phabricator_format_local_time($epoch, $viewer, 'm');
557 $start_day = $day;
558 }
559 return array($start_year, $start_month, $start_day);
560 }
561
562 public function getPageSize(PhabricatorSavedQuery $saved) {
563 if ($this->isMonthView($saved) || $this->isDayView($saved)) {
564 return $saved->getParameter('limit', 1000);
565 } else {
566 return $saved->getParameter('limit', 100);
567 }
568 }
569
570 /**
571 * @param PhabricatorSavedQuery $saved
572 * @return AphrontFormDateControlValue Query date range start
573 */
574 private function getQueryDateFrom(PhabricatorSavedQuery $saved) {
575 if ($this->calendarYear && $this->calendarMonth) {
576 $viewer = $this->requireViewer();
577
578 $start_year = $this->calendarYear;
579 $start_month = $this->calendarMonth;
580 $start_day = $this->calendarDay ? $this->calendarDay : 1;
581
582 return AphrontFormDateControlValue::newFromDictionary(
583 $viewer,
584 array(
585 'd' => "{$start_year}-{$start_month}-{$start_day}",
586 ));
587 }
588
589 $date = $this->getQueryDate($saved, 'rangeStart');
590 $this->validateDate($date);
591
592 return $date;
593 }
594
595 /**
596 * @param PhabricatorSavedQuery $saved
597 * @return AphrontFormDateControlValue Query date range end
598 */
599 private function getQueryDateTo(PhabricatorSavedQuery $saved) {
600 $date = $this->getQueryDate($saved, 'rangeEnd');
601 $this->validateDate($date);
602 return $date;
603 }
604
605 /**
606 * Validate the user provided date and time value(s) by calling
607 * @{class:AphrontFormDateControlValue}::isValid().
608 * Throw an Exception if invalid.
609 *
610 * @param AphrontFormDateControlValue $date
611 * @return void
612 */
613 private function validateDate(AphrontFormDateControlValue $date) {
614 if (!$date->isValid()) {
615 // TODO: Use DateMalformedStringException once we require PHP 8.3.0
616 throw new Exception(
617 pht('Invalid date or time value set as query value.'));
618 }
619 }
620
621 private function getQueryDate(PhabricatorSavedQuery $saved, $key) {
622 $viewer = $this->requireViewer();
623
624 $wild = $saved->getParameter($key);
625 return $this->getSafeDate($wild);
626 }
627
628 private function getSafeDate($value) {
629 $viewer = $this->requireViewer();
630 if ($value) {
631 // ideally this would be consistent and always pass in the same type
632 if ($value instanceof AphrontFormDateControlValue) {
633 return $value;
634 } else {
635 $value = AphrontFormDateControlValue::newFromWild($viewer, $value);
636 }
637 } else {
638 $value = AphrontFormDateControlValue::newFromEpoch(
639 $viewer,
640 PhabricatorTime::getTodayMidnightDateTime($viewer)->format('U'));
641 $value->setEnabled(false);
642 }
643
644 $value->setOptional(true);
645
646 return $value;
647 }
648
649 private function isMonthView(PhabricatorSavedQuery $query) {
650 if ($this->isDayView($query)) {
651 return false;
652 }
653 if ($query->getParameter('display') == 'month') {
654 return true;
655 }
656 }
657
658 private function isDayView(PhabricatorSavedQuery $query) {
659 if ($query->getParameter('display') == 'day') {
660 return true;
661 }
662 if ($this->calendarDay) {
663 return true;
664 }
665
666 return false;
667 }
668
669 public function newUseResultsActions(PhabricatorSavedQuery $saved) {
670 $viewer = $this->requireViewer();
671 $can_export = $viewer->isLoggedIn();
672
673 return array(
674 id(new PhabricatorActionView())
675 ->setIcon('fa-download')
676 ->setName(pht('Export Query as .ics'))
677 ->setDisabled(!$can_export)
678 ->setHref('/calendar/export/edit/?queryKey='.$saved->getQueryKey()),
679 );
680 }
681
682 /**
683 * @return PhabricatorApplicationSearchResultView
684 */
685 private function newResultView($content = null) {
686 // If we aren't rendering a dashboard panel, activate global drag-and-drop
687 // so you can import ".ics" files by dropping them directly onto the
688 // calendar.
689 if (!$this->isPanelContext()) {
690 $drop_upload = id(new PhabricatorGlobalUploadTargetView())
691 ->setViewer($this->requireViewer())
692 ->setHintText("\xE2\x87\xAA ".pht('Drop .ics Files to Import'))
693 ->setSubmitURI('/calendar/import/drop/')
694 ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
695
696 $content = array(
697 $drop_upload,
698 $content,
699 );
700 }
701
702 return id(new PhabricatorApplicationSearchResultView())
703 ->setContent($content);
704 }
705
706}