mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-19 13:22:42 +01:00
Show user status on calendar
Summary: This is a rough cut, but gets some of the basics at least. Here's what it looks like: {F11690} Some things that would be nice for future diffs: - Different colors for different event types (tasks? MEETINGS?!) - When events span across multiple days, keep them in the same row. - Switch which month you're looking at. - Show specific users instead of all. - etc etc etc Test Plan: See screenshot. Reviewers: btrahan, vrana Reviewed By: btrahan CC: aran Differential Revision: https://secure.phabricator.com/D2514
This commit is contained in:
parent
c2d2f6ded1
commit
0548dc4c4c
10 changed files with 314 additions and 50 deletions
|
@ -389,7 +389,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'aphront-calendar-view-css' =>
|
||||
array(
|
||||
'uri' => '/res/57b017ec/rsrc/css/aphront/calendar-view.css',
|
||||
'uri' => '/res/4fd79240/rsrc/css/aphront/calendar-view.css',
|
||||
'type' => 'css',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -534,7 +534,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'aphront-tooltip-css' =>
|
||||
array(
|
||||
'uri' => '/res/6bbfd9dd/rsrc/css/aphront/tooltip.css',
|
||||
'uri' => '/res/b46170b3/rsrc/css/aphront/tooltip.css',
|
||||
'type' => 'css',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -2223,7 +2223,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'phabricator-standard-page-view' =>
|
||||
array(
|
||||
'uri' => '/res/1e786be6/rsrc/css/application/base/standard-page-view.css',
|
||||
'uri' => '/res/82608dbf/rsrc/css/application/base/standard-page-view.css',
|
||||
'type' => 'css',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -2478,7 +2478,7 @@ celerity_register_resource_map(array(
|
|||
), array(
|
||||
'packages' =>
|
||||
array(
|
||||
'c0b5efc6' =>
|
||||
'2eec2a11' =>
|
||||
array(
|
||||
'name' => 'core.pkg.css',
|
||||
'symbols' =>
|
||||
|
@ -2507,7 +2507,7 @@ celerity_register_resource_map(array(
|
|||
21 => 'phabricator-flag-css',
|
||||
22 => 'aphront-error-view-css',
|
||||
),
|
||||
'uri' => '/res/pkg/c0b5efc6/core.pkg.css',
|
||||
'uri' => '/res/pkg/2eec2a11/core.pkg.css',
|
||||
'type' => 'css',
|
||||
),
|
||||
'0c96375e' =>
|
||||
|
@ -2674,20 +2674,20 @@ celerity_register_resource_map(array(
|
|||
'reverse' =>
|
||||
array(
|
||||
'aphront-attached-file-view-css' => '7839ae2d',
|
||||
'aphront-crumbs-view-css' => 'c0b5efc6',
|
||||
'aphront-dialog-view-css' => 'c0b5efc6',
|
||||
'aphront-error-view-css' => 'c0b5efc6',
|
||||
'aphront-form-view-css' => 'c0b5efc6',
|
||||
'aphront-crumbs-view-css' => '2eec2a11',
|
||||
'aphront-dialog-view-css' => '2eec2a11',
|
||||
'aphront-error-view-css' => '2eec2a11',
|
||||
'aphront-form-view-css' => '2eec2a11',
|
||||
'aphront-headsup-action-list-view-css' => 'd9299c35',
|
||||
'aphront-headsup-view-css' => 'c0b5efc6',
|
||||
'aphront-list-filter-view-css' => 'c0b5efc6',
|
||||
'aphront-pager-view-css' => 'c0b5efc6',
|
||||
'aphront-panel-view-css' => 'c0b5efc6',
|
||||
'aphront-side-nav-view-css' => 'c0b5efc6',
|
||||
'aphront-table-view-css' => 'c0b5efc6',
|
||||
'aphront-tokenizer-control-css' => 'c0b5efc6',
|
||||
'aphront-tooltip-css' => 'c0b5efc6',
|
||||
'aphront-typeahead-control-css' => 'c0b5efc6',
|
||||
'aphront-headsup-view-css' => '2eec2a11',
|
||||
'aphront-list-filter-view-css' => '2eec2a11',
|
||||
'aphront-pager-view-css' => '2eec2a11',
|
||||
'aphront-panel-view-css' => '2eec2a11',
|
||||
'aphront-side-nav-view-css' => '2eec2a11',
|
||||
'aphront-table-view-css' => '2eec2a11',
|
||||
'aphront-tokenizer-control-css' => '2eec2a11',
|
||||
'aphront-tooltip-css' => '2eec2a11',
|
||||
'aphront-typeahead-control-css' => '2eec2a11',
|
||||
'differential-changeset-view-css' => 'd9299c35',
|
||||
'differential-core-view-css' => 'd9299c35',
|
||||
'differential-inline-comment-editor' => '5ef7da0b',
|
||||
|
@ -2753,15 +2753,15 @@ celerity_register_resource_map(array(
|
|||
'javelin-workflow' => '0c96375e',
|
||||
'maniphest-task-summary-css' => '7839ae2d',
|
||||
'maniphest-transaction-detail-css' => '7839ae2d',
|
||||
'phabricator-app-buttons-css' => 'c0b5efc6',
|
||||
'phabricator-app-buttons-css' => '2eec2a11',
|
||||
'phabricator-content-source-view-css' => 'd9299c35',
|
||||
'phabricator-core-buttons-css' => 'c0b5efc6',
|
||||
'phabricator-core-css' => 'c0b5efc6',
|
||||
'phabricator-directory-css' => 'c0b5efc6',
|
||||
'phabricator-core-buttons-css' => '2eec2a11',
|
||||
'phabricator-core-css' => '2eec2a11',
|
||||
'phabricator-directory-css' => '2eec2a11',
|
||||
'phabricator-drag-and-drop-file-upload' => '5ef7da0b',
|
||||
'phabricator-dropdown-menu' => '0c96375e',
|
||||
'phabricator-flag-css' => 'c0b5efc6',
|
||||
'phabricator-jump-nav' => 'c0b5efc6',
|
||||
'phabricator-flag-css' => '2eec2a11',
|
||||
'phabricator-jump-nav' => '2eec2a11',
|
||||
'phabricator-keyboard-shortcut' => '0c96375e',
|
||||
'phabricator-keyboard-shortcut-manager' => '0c96375e',
|
||||
'phabricator-menu-item' => '0c96375e',
|
||||
|
@ -2769,11 +2769,11 @@ celerity_register_resource_map(array(
|
|||
'phabricator-paste-file-upload' => '0c96375e',
|
||||
'phabricator-prefab' => '0c96375e',
|
||||
'phabricator-project-tag-css' => '7839ae2d',
|
||||
'phabricator-remarkup-css' => 'c0b5efc6',
|
||||
'phabricator-remarkup-css' => '2eec2a11',
|
||||
'phabricator-shaped-request' => '5ef7da0b',
|
||||
'phabricator-standard-page-view' => 'c0b5efc6',
|
||||
'phabricator-standard-page-view' => '2eec2a11',
|
||||
'phabricator-tooltip' => '0c96375e',
|
||||
'phabricator-transaction-view-css' => 'c0b5efc6',
|
||||
'syntax-highlighting-css' => 'c0b5efc6',
|
||||
'phabricator-transaction-view-css' => '2eec2a11',
|
||||
'syntax-highlighting-css' => '2eec2a11',
|
||||
),
|
||||
));
|
||||
|
|
|
@ -16,6 +16,7 @@ phutil_register_library_map(array(
|
|||
'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration',
|
||||
'AphrontAttachedFileView' => 'view/control/attachedfile',
|
||||
'AphrontCSRFException' => 'aphront/exception/csrf',
|
||||
'AphrontCalendarEventView' => 'applications/calendar/view/event',
|
||||
'AphrontCalendarMonthView' => 'applications/calendar/view/month',
|
||||
'AphrontContextBarView' => 'view/layout/contextbar',
|
||||
'AphrontController' => 'aphront/controller',
|
||||
|
@ -1069,6 +1070,7 @@ phutil_register_library_map(array(
|
|||
'AphrontAjaxResponse' => 'AphrontResponse',
|
||||
'AphrontAttachedFileView' => 'AphrontView',
|
||||
'AphrontCSRFException' => 'AphrontException',
|
||||
'AphrontCalendarEventView' => 'AphrontView',
|
||||
'AphrontCalendarMonthView' => 'AphrontView',
|
||||
'AphrontContextBarView' => 'AphrontView',
|
||||
'AphrontCrumbsView' => 'AphrontView',
|
||||
|
|
|
@ -22,25 +22,46 @@ final class PhabricatorCalendarBrowseController
|
|||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
// TODO: These should be user-based and navigable in the interface.
|
||||
$year = idate('Y');
|
||||
$month = idate('m');
|
||||
|
||||
$holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere(
|
||||
'day BETWEEN %s AND %s',
|
||||
"{$year}-01-01",
|
||||
"{$year}-12-31");
|
||||
"{$year}-{$month}-01",
|
||||
"{$year}-{$month}-31");
|
||||
|
||||
$months = array();
|
||||
for ($ii = 1; $ii <= 12; $ii++) {
|
||||
$month_view = new AphrontCalendarMonthView($ii, $year);
|
||||
$month_view->setUser($user);
|
||||
$month_view->setHolidays($holidays);
|
||||
$months[] = '<div style="padding: 2em;">';
|
||||
$months[] = $month_view;
|
||||
$months[] = '</div>';
|
||||
$statuses = id(new PhabricatorUserStatus())
|
||||
->loadAllWhere(
|
||||
'dateTo >= %d AND dateFrom <= %d',
|
||||
strtotime("{$year}-{$month}-01"),
|
||||
strtotime("{$year}-{$month}-01 next month"));
|
||||
|
||||
$month_view = new AphrontCalendarMonthView($month, $year);
|
||||
$month_view->setUser($user);
|
||||
$month_view->setHolidays($holidays);
|
||||
|
||||
$phids = mpull($statuses, 'getUserPHID');
|
||||
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
|
||||
|
||||
foreach ($statuses as $status) {
|
||||
$event = new AphrontCalendarEventView();
|
||||
$event->setEpochRange($status->getDateFrom(), $status->getDateTo());
|
||||
|
||||
$name_text = $handles[$status->getUserPHID()]->getName();
|
||||
$status_text = $status->getTextStatus();
|
||||
$event->setName("{$name_text} ({$status_text})");
|
||||
$event->setDescription($status->getStatusDescription($user));
|
||||
$month_view->addEvent($event);
|
||||
}
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$months,
|
||||
array(
|
||||
'<div style="padding: 2em;">',
|
||||
$month_view,
|
||||
'</div>',
|
||||
),
|
||||
array(
|
||||
'title' => 'Calendar',
|
||||
));
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
|
||||
phutil_require_module('phabricator', 'applications/calendar/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/calendar/storage/holiday');
|
||||
phutil_require_module('phabricator', 'applications/calendar/view/event');
|
||||
phutil_require_module('phabricator', 'applications/calendar/view/month');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/userstatus');
|
||||
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
final class AphrontCalendarEventView extends AphrontView {
|
||||
|
||||
private $name;
|
||||
private $epochStart;
|
||||
private $epochEnd;
|
||||
private $description;
|
||||
|
||||
public function setName($name) {
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEpochRange($start, $end) {
|
||||
$this->epochStart = $start;
|
||||
$this->epochEnd = $end;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEpochStart() {
|
||||
return $this->epochStart;
|
||||
}
|
||||
|
||||
public function getEpochEnd() {
|
||||
return $this->epochEnd;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setDescription($description) {
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription() {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function render() {
|
||||
throw new Exception("Events are only rendered indirectly.");
|
||||
}
|
||||
|
||||
}
|
12
src/applications/calendar/view/event/__init__.php
Normal file
12
src/applications/calendar/view/event/__init__.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'view/base');
|
||||
|
||||
|
||||
phutil_require_source('AphrontCalendarEventView.php');
|
|
@ -22,12 +22,18 @@ final class AphrontCalendarMonthView extends AphrontView {
|
|||
private $month;
|
||||
private $year;
|
||||
private $holidays = array();
|
||||
private $events = array();
|
||||
|
||||
public function setUser(PhabricatorUser $user) {
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addEvent(AphrontCalendarEventView $event) {
|
||||
$this->events[] = $event;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setHolidays(array $holidays) {
|
||||
assert_instances_of($holidays, 'PhabricatorCalendarHoliday');
|
||||
$this->holidays = mpull($holidays, null, 'getDay');
|
||||
|
@ -44,6 +50,8 @@ final class AphrontCalendarMonthView extends AphrontView {
|
|||
throw new Exception("Call setUser() before render()!");
|
||||
}
|
||||
|
||||
$events = msort($this->events, 'getEpochStart');
|
||||
|
||||
$days = $this->getDatesInMonth();
|
||||
|
||||
require_celerity_resource('aphront-calendar-view-css');
|
||||
|
@ -53,21 +61,59 @@ final class AphrontCalendarMonthView extends AphrontView {
|
|||
|
||||
$markup = array();
|
||||
|
||||
$empty_box =
|
||||
'<div class="aphront-calendar-day aphront-calendar-empty">'.
|
||||
'</div>';
|
||||
|
||||
for ($ii = 0; $ii < $empty; $ii++) {
|
||||
$markup[] = null;
|
||||
$markup[] = $empty_box;
|
||||
}
|
||||
|
||||
foreach ($days as $day) {
|
||||
$holiday = idx($this->holidays, $day->format('Y-m-d'));
|
||||
$class = 'aphront-calendar-day-of-month';
|
||||
$class = 'aphront-calendar-day';
|
||||
$weekday = $day->format('w');
|
||||
if ($holiday || $weekday == 0 || $weekday == 6) {
|
||||
$class .= ' aphront-calendar-not-work-day';
|
||||
}
|
||||
|
||||
$day->setTime(0, 0, 0);
|
||||
$epoch_start = $day->format('U');
|
||||
|
||||
$day->setTime(23, 59, 59);
|
||||
$epoch_end = $day->format('U');
|
||||
|
||||
$show_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) {
|
||||
$show_events[] = $this->renderEvent(
|
||||
$event,
|
||||
$day,
|
||||
$epoch_start,
|
||||
$epoch_end);
|
||||
}
|
||||
}
|
||||
|
||||
$holiday_markup = null;
|
||||
if ($holiday) {
|
||||
$holiday_markup =
|
||||
'<div class="aphront-calendar-holiday">'.
|
||||
phutil_escape_html($holiday->getName()).
|
||||
'</div>';
|
||||
}
|
||||
|
||||
$markup[] =
|
||||
'<div class="'.$class.'">'.
|
||||
$day->format('j').
|
||||
($holiday ? '<br />'.phutil_escape_html($holiday->getName()) : '').
|
||||
$holiday_markup.
|
||||
'<div class="aphront-calendar-date-number">'.
|
||||
$day->format('j').
|
||||
'</div>'.
|
||||
implode("\n", $show_events).
|
||||
'</div>';
|
||||
}
|
||||
|
||||
|
@ -76,7 +122,7 @@ final class AphrontCalendarMonthView extends AphrontView {
|
|||
foreach ($rows as $row) {
|
||||
$table[] = '<tr>';
|
||||
while (count($row) < 7) {
|
||||
$row[] = null;
|
||||
$row[] = $empty_box;
|
||||
}
|
||||
foreach ($row as $cell) {
|
||||
$table[] = '<td>'.$cell.'</td>';
|
||||
|
@ -142,4 +188,61 @@ final class AphrontCalendarMonthView extends AphrontView {
|
|||
|
||||
return $days;
|
||||
}
|
||||
|
||||
private function renderEvent(
|
||||
AphrontCalendarEventView $event,
|
||||
DateTime $day,
|
||||
$epoch_start,
|
||||
$epoch_end) {
|
||||
|
||||
$user = $this->user;
|
||||
|
||||
$event_start = $event->getEpochStart();
|
||||
$event_end = $event->getEpochEnd();
|
||||
|
||||
$classes = array();
|
||||
$when = array();
|
||||
|
||||
$classes[] = 'aphront-calendar-event';
|
||||
if ($event_start < $epoch_start) {
|
||||
$classes[] = 'aphront-calendar-event-continues-before';
|
||||
$when[] = 'Started '.phabricator_datetime($event_start, $user);
|
||||
} else {
|
||||
$when[] = 'Starts at '.phabricator_time($event_start, $user);
|
||||
}
|
||||
|
||||
if ($event_end > $epoch_end) {
|
||||
$classes[] = 'aphront-calendar-event-continues-after';
|
||||
$when[] = 'Ends '.phabricator_datetime($event_start, $user);
|
||||
} else {
|
||||
$when[] = 'Ends at '.phabricator_time($event_end, $user);
|
||||
}
|
||||
|
||||
Javelin::initBehavior('phabricator-tooltips');
|
||||
|
||||
$info = $event->getName();
|
||||
if ($event->getDescription()) {
|
||||
$info .= "\n\n".$event->getDescription();
|
||||
}
|
||||
|
||||
$text_div = javelin_render_tag(
|
||||
'div',
|
||||
array(
|
||||
'sigil' => 'has-tooltip',
|
||||
'meta' => array(
|
||||
'tip' => $info."\n\n".implode("\n", $when),
|
||||
'size' => 240,
|
||||
),
|
||||
'class' => 'aphront-calendar-event-text',
|
||||
),
|
||||
phutil_escape_html(phutil_utf8_shorten($event->getName(), 32)));
|
||||
|
||||
return javelin_render_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => implode(' ', $classes),
|
||||
),
|
||||
$text_div);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
|
||||
|
||||
phutil_require_module('phabricator', 'infrastructure/celerity/api');
|
||||
phutil_require_module('phabricator', 'infrastructure/javelin/api');
|
||||
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
|
||||
phutil_require_module('phabricator', 'view/base');
|
||||
phutil_require_module('phabricator', 'view/utils');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
|
|
@ -28,18 +28,75 @@ tr.aphront-calendar-day-of-week-header th {
|
|||
|
||||
table.aphront-calendar-view td {
|
||||
border: 1px solid #dfdfdf;
|
||||
width: 14.2857%; /* This is one seventh, approximately. */
|
||||
}
|
||||
|
||||
table.aphront-calendar-view td div.aphront-calendar-day {
|
||||
min-height: 110px;
|
||||
}
|
||||
|
||||
.aphront-calendar-holiday {
|
||||
float: left;
|
||||
color: #666666;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
table.aphront-calendar-view td div {
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.aphront-calendar-day-of-month {
|
||||
text-align: right;
|
||||
.aphront-calendar-date-number {
|
||||
font-weight: bold;
|
||||
color: #666666;
|
||||
padding: .5em;
|
||||
border-color: #dfdfdf;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 1px;
|
||||
float: right;
|
||||
background: #ffffff;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
text-align: center;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.aphront-calendar-not-work-day {
|
||||
background-color: #fdfae7;
|
||||
}
|
||||
|
||||
.aphront-calendar-empty {
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.aphront-calendar-event {
|
||||
clear: both;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #6666ff;
|
||||
|
||||
background: #ddddff;
|
||||
font-size: 11px;
|
||||
margin: 2px 0;
|
||||
|
||||
border-radius: 8px;
|
||||
-moz-border-radius: 8px;
|
||||
padding: 3px 5%;
|
||||
color: #222266;
|
||||
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aphront-calendar-event-text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.aphront-calendar-event-continues-before {
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
.aphront-calendar-event-continues-after {
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
font-size: 11px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.jx-tooltip-align-E {
|
||||
|
|
Loading…
Reference in a new issue