@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 706 lines 21 kB view raw
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}