diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index b9c17e6a43..7a1ca000e5 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -1,89 +1,89 @@ [1-9]\d*)' => 'PhabricatorCalendarEventViewController', '/calendar/' => array( - '(?:query/(?P[^/]+)/)?' + '(?:query/(?P[^/]+)/(?:(?P\d+)/(?P\d+)/)?)?' => 'PhabricatorCalendarEventListController', 'event/' => array( 'create/' => 'PhabricatorCalendarEventEditController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventEditController', 'cancel/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventCancelController', '(?Pjoin|decline|accept)/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventJoinController', 'comment/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventCommentController', ), ), ); } public function getQuickCreateItems(PhabricatorUser $viewer) { $items = array(); $item = id(new PHUIListItemView()) ->setName(pht('Calendar Event')) ->setIcon('fa-calendar') ->setHref($this->getBaseURI().'event/create/'); $items[] = $item; return $items; } public function getMailCommandObjects() { return array( 'event' => array( 'name' => pht('Email Commands: Events'), 'header' => pht('Interacting with Calendar Events'), 'object' => new PhabricatorCalendarEvent(), 'summary' => pht( 'This page documents the commands you can use to interact with '. 'events in Calendar. These commands work when creating new tasks '. 'via email and when replying to existing tasks.'), ), ); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php index 1d82160a59..30a908fb8e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php @@ -1,39 +1,41 @@ queryKey = idx($data, 'queryKey'); - } + public function handleRequest(AphrontRequest $request) { + $year = $request->getURIData('year'); + $month = $request->getURIData('month'); + $engine = new PhabricatorCalendarEventSearchEngine(); + + if ($month && $year) { + $engine->setCalendarYearAndMonth($year, $month); + } - public function processRequest() { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) - ->setSearchEngine(new PhabricatorCalendarEventSearchEngine()) + ->setQueryKey($request->getURIData('queryKey')) + ->setSearchEngine($engine) ->setNavigation($this->buildSideNav()); return $this->delegateToController($controller); } public function buildSideNav() { $user = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhabricatorCalendarEventSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 9ad462577d..b65180ce66 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -1,317 +1,379 @@ setParameter( 'rangeStart', $this->readDateFromRequest($request, 'rangeStart')); $saved->setParameter( 'rangeEnd', $this->readDateFromRequest($request, 'rangeEnd')); $saved->setParameter( 'upcoming', $this->readBoolFromRequest($request, 'upcoming')); $saved->setParameter( 'invitedPHIDs', $this->readUsersFromRequest($request, 'invited')); $saved->setParameter( 'creatorPHIDs', $this->readUsersFromRequest($request, 'creators')); $saved->setParameter( 'isCancelled', $request->getStr('isCancelled')); $saved->setParameter( 'display', $request->getStr('display')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorCalendarEventQuery()); + $viewer = $this->requireViewer(); $min_range = null; $max_range = null; if ($saved->getParameter('rangeStart')) { $min_range = $saved->getParameter('rangeStart'); } if ($saved->getParameter('rangeEnd')) { $max_range = $saved->getParameter('rangeEnd'); } + if ($saved->getParameter('display') == 'month') { + list($start_month, $start_year) = $this->getDisplayMonthAndYear($saved); + $start_day = 1; + + $end_year = ($start_month == 12) ? $start_year + 1 : $start_year; + $end_month = ($start_month == 12) ? 1 : $start_month + 1; + $end_day = 1; + + $calendar_start = AphrontFormDateControlValue::newFromParts( + $viewer, + $start_year, + $start_month, + $start_day)->getEpoch(); + $calendar_end = AphrontFormDateControlValue::newFromParts( + $viewer, + $end_year, + $end_month, + $end_day)->getEpoch(); + + if (!$min_range || ($min_range < $calendar_start)) { + $min_range = $calendar_start; + } + if (!$max_range || ($max_range > $calendar_end)) { + $max_range = $calendar_end; + } + } + if ($saved->getParameter('upcoming')) { if ($min_range) { $min_range = max(time(), $min_range); } else { $min_range = time(); } } if ($min_range || $max_range) { $query->withDateRange($min_range, $max_range); } $invited_phids = $saved->getParameter('invitedPHIDs'); if ($invited_phids) { $query->withInvitedPHIDs($invited_phids); } $creator_phids = $saved->getParameter('creatorPHIDs'); if ($creator_phids) { $query->withCreatorPHIDs($creator_phids); } $is_cancelled = $saved->getParameter('isCancelled'); switch ($is_cancelled) { case 'active': $query->withIsCancelled(false); break; case 'cancelled': $query->withIsCancelled(true); break; } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $range_start = $saved->getParameter('rangeStart'); $range_end = $saved->getParameter('rangeEnd'); $upcoming = $saved->getParameter('upcoming'); $is_cancelled = $saved->getParameter('isCancelled', 'active'); $display = $saved->getParameter('display', 'month'); $invited_phids = $saved->getParameter('invitedPHIDs', array()); $creator_phids = $saved->getParameter('creatorPHIDs', array()); $resolution_types = array( 'active' => pht('Active Events Only'), 'cancelled' => pht('Cancelled Events Only'), 'both' => pht('Both Cancelled and Active Events'), ); $display_options = array( 'month' => pht('Month View'), 'list' => pht('List View'), ); $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('creators') ->setLabel(pht('Created By')) ->setValue($creator_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('invited') ->setLabel(pht('Invited')) ->setValue($invited_phids)) ->appendChild( id(new AphrontFormDateControl()) ->setLabel(pht('Occurs After')) ->setUser($this->requireViewer()) ->setName('rangeStart') ->setAllowNull(true) ->setValue($range_start)) ->appendChild( id(new AphrontFormDateControl()) ->setLabel(pht('Occurs Before')) ->setUser($this->requireViewer()) ->setName('rangeEnd') ->setAllowNull(true) ->setValue($range_end)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'upcoming', 1, pht('Show only upcoming events.'), $upcoming)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Cancelled Events')) ->setName('isCancelled') ->setValue($is_cancelled) ->setOptions($resolution_types)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Display Options')) ->setName('display') ->setValue($display) ->setOptions($display_options)); } protected function getURI($path) { return '/calendar/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'month' => pht('Month View'), 'upcoming' => pht('Upcoming Events'), 'all' => pht('All Events'), ); return $names; } + public function setCalendarYearAndMonth($year, $month) { + $this->calendarYear = $year; + $this->calendarMonth = $month; + + return $this; + } + public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'month': return $query->setParameter('display', 'month'); case 'upcoming': return $query->setParameter('upcoming', true); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { $phids = array(); foreach ($objects as $event) { $phids[$event->getUserPHID()] = 1; } return array_keys($phids); } protected function renderResultList( array $events, PhabricatorSavedQuery $query, array $handles) { if ($query->getParameter('display') == 'month') { return $this->buildCalendarView($events, $query, $handles); } assert_instances_of($events, 'PhabricatorCalendarEvent'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($events as $event) { $href = '/E'.$event->getID(); $from = phabricator_datetime($event->getDateFrom(), $viewer); $to = phabricator_datetime($event->getDateTo(), $viewer); $creator_handle = $handles[$event->getUserPHID()]; $name = (strlen($event->getName())) ? $event->getName() : $event->getTerseSummary($viewer); $color = ($event->getStatus() == PhabricatorCalendarEvent::STATUS_AWAY) ? 'red' : 'yellow'; $item = id(new PHUIObjectItemView()) ->setHeader($name) ->setHref($href) ->setBarColor($color) ->addByline(pht('Creator: %s', $creator_handle->renderLink())) ->addAttribute(pht('From %s to %s', $from, $to)) ->addAttribute(id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(64) ->truncateString($event->getDescription())); $list->addItem($item); } return $list; } private function buildCalendarView( array $statuses, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); $now = time(); - $epoch = $query->getParameter('rangeStart'); - if (!$epoch) { - $epoch = $query->getParameter('rangeEnd'); - if (!$epoch) { - $epoch = time(); - } - } - - $year = phabricator_format_local_time($epoch, $viewer, 'Y'); - $month = phabricator_format_local_time($epoch, $viewer, 'm'); + list($start_month, $start_year) = $this->getDisplayMonthAndYear($query); $now_year = phabricator_format_local_time($now, $viewer, 'Y'); $now_month = phabricator_format_local_time($now, $viewer, 'm'); $now_day = phabricator_format_local_time($now, $viewer, 'j'); - if ($month == $now_month && $year == $now_year) { - $month_view = new PHUICalendarMonthView($month, $year, $now_day); + if ($start_month == $now_month && $start_year == $now_year) { + $month_view = new PHUICalendarMonthView( + $start_month, + $start_year, + $now_day); } else { - $month_view = new PHUICalendarMonthView($month, $year); + $month_view = new PHUICalendarMonthView( + $start_month, + $start_year); } $month_view->setUser($viewer); $phids = mpull($statuses, 'getUserPHID'); /* Assign Colors */ $unique = array_unique($phids); $allblue = false; $calcolors = CalendarColors::getColors(); if (count($unique) > count($calcolors)) { $allblue = true; } $i = 0; $eventcolor = array(); foreach ($unique as $phid) { if ($allblue) { $eventcolor[$phid] = CalendarColors::COLOR_SKY; } else { $eventcolor[$phid] = $calcolors[$i]; } $i++; } foreach ($statuses as $status) { $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $name_text = $handles[$status->getUserPHID()]->getName(); $status_text = $status->getHumanStatus(); $event->setUserPHID($status->getUserPHID()); $event->setDescription(pht('%s (%s)', $name_text, $status_text)); $event->setName($status_text); $event->setEventID($status->getID()); $event->setColor($eventcolor[$status->getUserPHID()]); $month_view->addEvent($event); } + $month_view->setBrowseURI( + $this->getURI('query/'.$query->getQueryKey().'/')); + return $month_view; + } + + private function getDisplayMonthAndYear( + PhabricatorSavedQuery $query) { + $viewer = $this->requireViewer(); + // get month/year from url + if ($this->calendarYear && $this->calendarMonth) { + $start_year = $this->calendarYear; + $start_month = $this->calendarMonth; + } else { + $epoch = $query->getParameter('rangeStart'); + if (!$epoch) { + $epoch = $query->getParameter('rangeEnd'); + if (!$epoch) { + $epoch = time(); + } + } + $start_year = phabricator_format_local_time($epoch, $viewer, 'Y'); + $start_month = phabricator_format_local_time($epoch, $viewer, 'm'); + } + + return array($start_month, $start_year); + } + public function getPageSize(PhabricatorSavedQuery $saved) { + return $saved->getParameter('limit', 1000); } } diff --git a/src/view/form/control/AphrontFormDateControlValue.php b/src/view/form/control/AphrontFormDateControlValue.php index 5462e40a18..0874475884 100644 --- a/src/view/form/control/AphrontFormDateControlValue.php +++ b/src/view/form/control/AphrontFormDateControlValue.php @@ -1,97 +1,114 @@ valueDay; } public function getValueMonth() { return $this->valueMonth; } public function getValueYear() { return $this->valueYear; } public function getValueTime() { return $this->valueTime; } public function isValid() { return ($this->getEpoch() !== null); } + public static function newFromParts( + PhabricatorUser $viewer, + $year, + $month, + $day, + $time = '12:00 AM') { + + $value = new AphrontFormDateControlValue(); + $value->viewer = $viewer; + $value->valueYear = $year; + $value->valueMonth = $month; + $value->valueDay = $day; + $value->valueTime = $time; + + return $value; + } + public static function newFromRequest($request, $key) { $value = new AphrontFormDateControlValue(); $value->viewer = $request->getViewer(); $value->valueDay = $request->getInt($key.'_d'); $value->valueMonth = $request->getInt($key.'_m'); $value->valueYear = $request->getInt($key.'_y'); $value->valueTime = $request->getStr($key.'_t'); return $value; } public static function newFromEpoch(PhabricatorUser $viewer, $epoch) { $value = new AphrontFormDateControlValue(); $value->viewer = $viewer; $readable = $value->formatTime($epoch, 'Y!m!d!g:i A'); $readable = explode('!', $readable, 4); $value->valueYear = $readable[0]; $value->valueMonth = $readable[1]; $value->valueDay = $readable[2]; $value->valueTime = $readable[3]; return $value; } private function formatTime($epoch, $format) { return phabricator_format_local_time( $epoch, $this->viewer, $format); } public function getEpoch() { $year = $this->valueYear; $month = $this->valueMonth; $day = $this->valueDay; $time = $this->valueTime; $zone = $this->getTimezone(); if (!strlen($time)) { return null; } try { $date = new DateTime("{$year}-{$month}-{$day} {$time}", $zone); $value = $date->format('U'); } catch (Exception $ex) { $value = null; } return $value; } private function getTimezone() { if ($this->zone) { return $this->zone; } $viewer_zone = $this->viewer->getTimezoneIdentifier(); $this->zone = new DateTimeZone($viewer_zone); return $this->zone; } } diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php index b69128e5e8..c4a7624a3d 100644 --- a/src/view/phui/calendar/PHUICalendarMonthView.php +++ b/src/view/phui/calendar/PHUICalendarMonthView.php @@ -1,319 +1,316 @@ browseURI = $browse_uri; return $this; } private function getBrowseURI() { return $this->browseURI; } public function addEvent(AphrontCalendarEventView $event) { $this->events[] = $event; return $this; } public function setImage($uri) { $this->image = $uri; return $this; } public function setInfoView(PHUIInfoView $error) { $this->error = $error; return $this; } public function setHolidays(array $holidays) { assert_instances_of($holidays, 'PhabricatorCalendarHoliday'); $this->holidays = mpull($holidays, null, 'getDay'); return $this; } public function __construct($month, $year, $day = null) { $this->day = $day; $this->month = $month; $this->year = $year; } public function render() { if (empty($this->user)) { throw new Exception('Call setUser() before render()!'); } $events = msort($this->events, 'getEpochStart'); $days = $this->getDatesInMonth(); require_celerity_resource('phui-calendar-month-css'); $first = reset($days); $empty = $first->format('w'); $markup = array(); $empty_box = phutil_tag( 'div', array('class' => 'phui-calendar-day phui-calendar-empty'), ''); for ($ii = 0; $ii < $empty; $ii++) { $markup[] = $empty_box; } $show_events = array(); foreach ($days as $day) { $day_number = $day->format('j'); $holiday = idx($this->holidays, $day->format('Y-m-d')); $class = 'phui-calendar-day'; $weekday = $day->format('w'); if ($day_number == $this->day) { $class .= ' phui-calendar-today'; } if ($holiday || $weekday == 0 || $weekday == 6) { $class .= ' phui-calendar-not-work-day'; } $day->setTime(0, 0, 0); $epoch_start = $day->format('U'); $day->modify('+1 day'); $epoch_end = $day->format('U'); if ($weekday == 0) { $show_events = array(); } else { $show_events = array_fill_keys( array_keys($show_events), phutil_tag_div( 'phui-calendar-event phui-calendar-event-empty', "\xC2\xA0")); //   } $list_events = array(); foreach ($events as $event) { if ($event->getEpochStart() >= $epoch_end) { // This list is sorted, so we can stop looking. break; } if ($event->getEpochStart() < $epoch_end && $event->getEpochEnd() > $epoch_start) { $list_events[] = $event; } } $list = new PHUICalendarListView(); $list->setUser($this->user); foreach ($list_events as $item) { $list->addEvent($item); } $holiday_markup = null; if ($holiday) { $name = $holiday->getName(); $holiday_markup = phutil_tag( 'div', array( 'class' => 'phui-calendar-holiday', 'title' => $name, ), $name); } $markup[] = phutil_tag_div( $class, array( phutil_tag_div('phui-calendar-date-number', $day_number), $holiday_markup, $list, )); } $table = array(); $rows = array_chunk($markup, 7); foreach ($rows as $row) { $cells = array(); while (count($row) < 7) { $row[] = $empty_box; } $j = 0; foreach ($row as $cell) { if ($j == 0) { $cells[] = phutil_tag( 'td', array( 'class' => 'phui-calendar-month-weekstart', ), $cell); } else { $cells[] = phutil_tag('td', array(), $cell); } $j++; } $table[] = phutil_tag('tr', array(), $cells); } $header = phutil_tag( 'tr', array('class' => 'phui-calendar-day-of-week-header'), array( phutil_tag('th', array(), pht('Sun')), phutil_tag('th', array(), pht('Mon')), phutil_tag('th', array(), pht('Tue')), phutil_tag('th', array(), pht('Wed')), phutil_tag('th', array(), pht('Thu')), phutil_tag('th', array(), pht('Fri')), phutil_tag('th', array(), pht('Sat')), )); $table = phutil_tag( 'table', array('class' => 'phui-calendar-view'), array( $header, phutil_implode_html("\n", $table), )); $box = id(new PHUIObjectBoxView()) ->setHeader($this->renderCalendarHeader($first)) ->appendChild($table); if ($this->error) { $box->setInfoView($this->error); } return $box; } private function renderCalendarHeader(DateTime $date) { $button_bar = null; // check for a browseURI, which means we need "fancy" prev / next UI $uri = $this->getBrowseURI(); if ($uri) { - $uri = new PhutilURI($uri); list($prev_year, $prev_month) = $this->getPrevYearAndMonth(); - $query = array('year' => $prev_year, 'month' => $prev_month); - $prev_uri = (string) $uri->setQueryParams($query); + $prev_uri = $uri.$prev_year.'/'.$prev_month.'/'; list($next_year, $next_month) = $this->getNextYearAndMonth(); - $query = array('year' => $next_year, 'month' => $next_month); - $next_uri = (string) $uri->setQueryParams($query); + $next_uri = $uri.$next_year.'/'.$next_month.'/'; $button_bar = new PHUIButtonBarView(); $left_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-left bluegrey'); $left = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($prev_uri) ->setTitle(pht('Previous Month')) ->setIcon($left_icon); $right_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-right bluegrey'); $right = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($next_uri) ->setTitle(pht('Next Month')) ->setIcon($right_icon); $button_bar->addButton($left); $button_bar->addButton($right); } $header = id(new PHUIHeaderView()) ->setHeader($date->format('F Y')); if ($button_bar) { $header->setButtonBar($button_bar); } if ($this->image) { $header->setImage($this->image); } return $header; } private function getNextYearAndMonth() { $month = $this->month; $year = $this->year; $next_year = $year; $next_month = $month + 1; if ($next_month == 13) { $next_year = $year + 1; $next_month = 1; } return array($next_year, $next_month); } private function getPrevYearAndMonth() { $month = $this->month; $year = $this->year; $prev_year = $year; $prev_month = $month - 1; if ($prev_month == 0) { $prev_year = $year - 1; $prev_month = 12; } return array($prev_year, $prev_month); } /** * Return a DateTime object representing the first moment in each day in the * month, according to the user's locale. * * @return list List of DateTimes, one for each day. */ private function getDatesInMonth() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; // Get the year and month numbers of the following month, so we can // determine when this month ends. list($next_year, $next_month) = $this->getNextYearAndMonth(); $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); $end_epoch = $end_date->format('U'); $days = array(); for ($day = 1; $day <= 31; $day++) { $day_date = new DateTime("{$year}-{$month}-{$day}", $timezone); $day_epoch = $day_date->format('U'); if ($day_epoch >= $end_epoch) { break; } else { $days[] = $day_date; } } return $days; } }