diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '36142bff', + 'core.pkg.css' => '439658b5', 'core.pkg.js' => '328799d0', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'bb338e4b', @@ -112,7 +112,7 @@ 'rsrc/css/core/core.css' => 'aaea7a7a', 'rsrc/css/core/remarkup.css' => '07b7dc54', 'rsrc/css/core/syntax.css' => '6b7b24d9', - 'rsrc/css/core/z-index.css' => '8414a09b', + 'rsrc/css/core/z-index.css' => 'c4732d32', 'rsrc/css/diviner/diviner-shared.css' => '38813222', 'rsrc/css/font/font-awesome.css' => 'e2e712fe', 'rsrc/css/font/font-source-sans-pro.css' => '8906c07b', @@ -121,7 +121,7 @@ 'rsrc/css/layout/phabricator-hovercard-view.css' => 'dd9121a9', 'rsrc/css/layout/phabricator-side-menu-view.css' => 'c1db9e9c', 'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894', - 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'c0cf782a', + 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'feba82c5', 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338', 'rsrc/css/phui/calendar/phui-calendar-month.css' => '476be7e0', 'rsrc/css/phui/calendar/phui-calendar.css' => 'ccabe893', @@ -331,7 +331,7 @@ 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', - 'rsrc/js/application/calendar/behavior-day-view.js' => 'f4f4ad80', + 'rsrc/js/application/calendar/behavior-day-view.js' => 'dc0065ab', 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', 'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '10246726', @@ -554,7 +554,7 @@ 'javelin-behavior-dashboard-move-panels' => '82439934', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', - 'javelin-behavior-day-view' => 'f4f4ad80', + 'javelin-behavior-day-view' => 'dc0065ab', 'javelin-behavior-device' => 'a205cf28', 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 'javelin-behavior-differential-comment-jump' => '4fdb476d', @@ -752,7 +752,7 @@ 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', - 'phabricator-zindex-css' => '8414a09b', + 'phabricator-zindex-css' => 'c4732d32', 'phame-css' => '88bd4705', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', @@ -767,7 +767,7 @@ 'phui-box-css' => '7b3a2eed', 'phui-button-css' => 'de610129', 'phui-calendar-css' => 'ccabe893', - 'phui-calendar-day-css' => 'c0cf782a', + 'phui-calendar-day-css' => 'feba82c5', 'phui-calendar-list-css' => 'c1c7f338', 'phui-calendar-month-css' => '476be7e0', 'phui-crumbs-view-css' => '594d719e', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1498,6 +1498,7 @@ 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', 'PhabricatorCalendarEventCommentController' => 'applications/calendar/controller/PhabricatorCalendarEventCommentController.php', + 'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php', 'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php', 'PhabricatorCalendarEventEditIconController' => 'applications/calendar/controller/PhabricatorCalendarEventEditIconController.php', 'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php', @@ -4853,6 +4854,7 @@ ), 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditIconController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -54,6 +54,8 @@ => 'PhabricatorCalendarEventEditController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventEditController', + 'drag/(?P[1-9]\d*)/' + => 'PhabricatorCalendarEventDragController', 'cancel/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventCancelController', '(?Pjoin|decline|accept)/(?P[1-9]\d*)/' diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventDragController.php b/src/applications/calendar/controller/PhabricatorCalendarEventDragController.php new file mode 100644 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarEventDragController.php @@ -0,0 +1,66 @@ +getViewer(); + + $event = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$event) { + return new Aphront404Response(); + } + + if (!$request->validateCSRF()) { + return new Aphront400Response(); + } + + if ($event->getIsAllDay()) { + return new Aphront400Response(); + } + + $xactions = array(); + + $duration = $event->getDateTo() - $event->getDateFrom(); + + $start = $request->getInt('start'); + $start_value = id(AphrontFormDateControlValue::newFromEpoch( + $viewer, + $start)); + + $end = $start + $duration; + $end_value = id(AphrontFormDateControlValue::newFromEpoch( + $viewer, + $end)); + + + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_START_DATE) + ->setNewValue($start_value); + + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_END_DATE) + ->setNewValue($end_value); + + + $editor = id(new PhabricatorCalendarEventEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $xactions = $editor->applyTransactions($event, $xactions); + + return id(new AphrontReloadResponse()); + } +} diff --git a/src/view/phui/calendar/PHUICalendarDayView.php b/src/view/phui/calendar/PHUICalendarDayView.php --- a/src/view/phui/calendar/PHUICalendarDayView.php +++ b/src/view/phui/calendar/PHUICalendarDayView.php @@ -9,7 +9,6 @@ private $year; private $browseURI; private $events = array(); - private $jsTodayEvents = array(); private $allDayEvents = array(); @@ -44,8 +43,11 @@ public function render() { require_celerity_resource('phui-calendar-day-css'); + $viewer = $this->getUser(); + $hours = $this->getHoursOfDay(); $js_hours = array(); + $js_today_events = array(); foreach ($hours as $hour) { $js_hours[] = array( @@ -55,10 +57,7 @@ } $first_event_hour = null; - - $js_hourly_events = array(); $js_today_all_day_events = array(); - $all_day_events = $this->getAllDayEvents(); $day_start = $this->getDateTime(); @@ -81,60 +80,54 @@ } } - foreach ($hours as $hour) { - $current_hour_events = array(); - $hour_start = $hour->format('U'); - $hour_end = id(clone $hour)->modify('+1 hour')->format('U'); + $this->events = msort($this->events, 'getEpochStart'); - foreach ($this->events as $event) { - if ($event->getIsAllDay()) { - continue; - } - if (($hour == $day_start && - $event->getEpochStart() <= $hour_start && - $event->getEpochEnd() > $day_start_epoch) || - ($event->getEpochStart() >= $hour_start - && $event->getEpochStart() < $hour_end)) { - $current_hour_events[] = $event; - $this->jsTodayEvents[] = array( - 'eventStartEpoch' => $event->getEpochStart(), - 'eventEndEpoch' => $event->getEpochEnd(), - 'eventName' => $event->getName(), - 'eventID' => $event->getEventID(), - 'viewerIsInvited' => $event->getViewerIsInvited(), - 'uri' => $event->getURI(), - ); - } + if (!$this->events) { + $first_event_hour = $this->getDateTime()->setTime(8, 0, 0); + } + + foreach ($this->events as $event) { + if ($event->getIsAllDay()) { + continue; } - foreach ($current_hour_events as $event) { - $day_start_epoch = $this->getDateTime()->format('U'); + if ($event->getEpochStart() <= $day_end_epoch && + $event->getEpochEnd() > $day_start_epoch) { + + if ($first_event_hour === null) { + $first_event_hour = new DateTime('@'.$event->getEpochStart()); + $first_event_hour->setTimeZone($viewer->getTimeZone()); + $eight_am = $this->getDateTime()->setTime(8, 0, 0); + if ($eight_am->format('U') < $first_event_hour->format('U')) { + $first_event_hour = clone $eight_am; + } + } + $event_start = max($event->getEpochStart(), $day_start_epoch); $event_end = min($event->getEpochEnd(), $day_end_epoch); - $top = (($event_start - $hour_start) / ($hour_end - $hour_start)) - * 100; - $top = max(0, $top); + $day_duration = ($day_end_epoch - $first_event_hour->format('U')) / 60; - $height = (($event_end - $event_start) / ($hour_end - $hour_start)) - * 100; - $height = min(2400, $height); + $top = (($event_start - $first_event_hour->format('U')) + / ($day_end_epoch - $first_event_hour->format('U'))) + * $day_duration; + $top = max(0, $top); - if ($first_event_hour === null) { - $first_event_hour = $hour; - } + $height = (($event_end - $event_start) + / ($day_end_epoch - $first_event_hour->format('U'))) + * $day_duration; + $height = min($day_duration, $height); - $js_hourly_events[] = array( + $js_today_events[] = array( 'eventStartEpoch' => $event->getEpochStart(), 'eventEndEpoch' => $event->getEpochEnd(), 'eventName' => $event->getName(), 'eventID' => $event->getEventID(), 'viewerIsInvited' => $event->getViewerIsInvited(), 'uri' => $event->getURI(), - 'hour' => $hour->format('G'), 'offset' => '0', 'width' => '100%', - 'top' => $top.'%', - 'height' => $height.'%', + 'top' => $top.'px', + 'height' => $height.'px', ); } } @@ -156,10 +149,10 @@ 'day-view', array( 'allDayEvents' => $js_today_all_day_events, - 'todayEvents' => $this->jsTodayEvents, - 'hourlyEvents' => $js_hourly_events, + 'todayEvents' => $js_today_events, 'hours' => $js_hours, 'firstEventHour' => $first_event_hour->format('G'), + 'firstEventHourEpoch' => $first_event_hour->format('U'), 'tableID' => $table_id, )); diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -36,6 +36,10 @@ z-index: 2; } +div.phui-calendar-day-event { + z-index: 2; +} + .slowvote-above-the-bar { z-index: 3; } diff --git a/webroot/rsrc/css/phui/calendar/phui-calendar-day.css b/webroot/rsrc/css/phui/calendar/phui-calendar-day.css --- a/webroot/rsrc/css/phui/calendar/phui-calendar-day.css +++ b/webroot/rsrc/css/phui/calendar/phui-calendar-day.css @@ -35,7 +35,11 @@ border-top: 1px solid {$lightgreyborder}; } -.phui-calendar-day-view td div.phui-calendar-day-event { +.phui-drag { + opacity: .25; +} + +div.phui-calendar-day-event { width: 100%; position: absolute; top: 0; @@ -43,11 +47,15 @@ min-height: 30px; } +div.phui-calendar-day-event.all-day { + position: relative; +} + .phui-calendar-day-event-link { padding: 8px; border: 1px solid {$greyborder}; background-color: {$darkgreybackground}; - margin: 0 4px; + margin: 0 1px; position: absolute; left: 0; right: 0; diff --git a/webroot/rsrc/js/application/calendar/behavior-day-view.js b/webroot/rsrc/js/application/calendar/behavior-day-view.js --- a/webroot/rsrc/js/application/calendar/behavior-day-view.js +++ b/webroot/rsrc/js/application/calendar/behavior-day-view.js @@ -4,13 +4,6 @@ JX.behavior('day-view', function(config) { - var hours = config.hours; - var first_event_hour = config.firstEventHour; - var hourly_events = config.hourlyEvents; - var today_events = config.todayEvents; - var today_all_day_events = config.allDayEvents; - var table_wrapper = JX.$(config.tableID); - function findTodayClusters() { var events = today_events.sort(function(x, y){ @@ -23,8 +16,8 @@ var today_event = events[i]; var destination_cluster_index = null; - var event_start = today_event.eventStartEpoch - (30*60); - var event_end = today_event.eventEndEpoch + (30*60); + var event_start = today_event.eventStartEpoch - (60); + var event_end = today_event.eventEndEpoch + (60); for (var j=0; j < clusters.length; j++) { var cluster = clusters[j]; @@ -59,7 +52,7 @@ return clusters; } - function updateEventsFromCluster(cluster, hourly_events) { + function updateEventsFromCluster(cluster) { var cluster_size = cluster.length; var n = 0; for(var i=0; i < cluster.length; i++) { @@ -69,28 +62,30 @@ var offset = ((n / cluster_size) * 100) + '%'; var width = ((1 / cluster_size) * 100) + '%'; - for (var j=0; j < hourly_events.length; j++) { - if (hourly_events[j].eventID == event_id) { + for (var j=0; j < today_events.length; j++) { + if (today_events[j].eventID == event_id) { - hourly_events[j]['offset'] = offset; - hourly_events[j]['width'] = width; + today_events[j]['offset'] = offset; + today_events[j]['width'] = width; } } n++; } - return hourly_events; + return today_events; } - function drawEvent(hourly_event) { - var name = hourly_event['eventName']; - var viewerIsInvited = hourly_event['viewerIsInvited']; - var offset = hourly_event['offset']; - var width = hourly_event['width']; - var top = hourly_event['top']; - var height = hourly_event['height']; - var uri = hourly_events['uri']; - + function drawEvent(e) { + var name = e['eventName']; + var eventID = e['eventID']; + var viewerIsInvited = e['viewerIsInvited']; + var offset = e['offset']; + var width = e['width']; + var top = e['top']; + var height = e['height']; + var uri = e['uri']; + + var sigil = 'phui-calendar-day-event'; var link_class = 'phui-calendar-day-event-link'; if (viewerIsInvited) { @@ -109,6 +104,8 @@ 'div', { className: 'phui-calendar-day-event', + sigil: sigil, + meta: {eventID: eventID, record: e, uri: uri}, style: { left: offset, width: width, @@ -145,7 +142,7 @@ var div_all_day = JX.$N( 'div', - {className: 'phui-calendar-day-event'}, + {className: 'phui-calendar-day-event all-day'}, [all_day_label, name]); return div_all_day; @@ -164,24 +161,17 @@ if (hours[i]['hour'] < min_early_hour) { continue; } - var drawn_hourly_events = []; var cell_time = JX.$N( 'td', {className: 'phui-calendar-day-hour'}, hours[i]['hour_meridian']); - for (var j=0; j < hourly_events.length; j++) { - if (hourly_events[j]['hour'] == hours[i]['hour']) { - drawn_hourly_events.push(drawEvent(hourly_events[j])); - } - } - var cell_event = JX.$N( 'td', { className: 'phui-calendar-day-events' - }, - drawn_hourly_events); + }); + var row = JX.$N( 'tr', {}, @@ -191,10 +181,26 @@ return rows; } - var today_clusters = findTodayClusters(); - for(var i=0; i < today_clusters.length; i++) { - hourly_events = updateEventsFromCluster(today_clusters[i], hourly_events); + function clusterAndDrawEvents() { + var today_clusters = findTodayClusters(); + for(var i=0; i < today_clusters.length; i++) { + today_events = updateEventsFromCluster(today_clusters[i]); + } + var drawn_hourly_events = []; + for (i=0; i < today_events.length; i++) { + drawn_hourly_events.push(drawEvent(today_events[i])); + } + + JX.DOM.setContent(hourly_events_wrapper, drawn_hourly_events); + } + + var hours = config.hours; + var first_event_hour = config.firstEventHour; + var first_event_hour_epoch = parseInt(config.firstEventHourEpoch, 10); + var today_events = config.todayEvents; + var today_all_day_events = config.allDayEvents; + var table_wrapper = JX.$(config.tableID); var rows = drawRows(); var all_day_events = []; @@ -211,5 +217,108 @@ {className: 'phui-calendar-day-view'}, rows); - JX.DOM.setContent(table_wrapper, [all_day_events, table]); + var dragging = false; + var origin = null; + + var offset_top = null; + var new_top = null; + + var click_time = null; + + JX.DOM.listen( + table_wrapper, + 'mousedown', + 'phui-calendar-day-event', + function(e){ + + if (!e.isNormalMouseEvent()) { + return; + } + e.kill(); + dragging = e.getNode('phui-calendar-day-event'); + JX.DOM.alterClass(dragging, 'phui-drag', true); + + click_time = new Date(); + + origin = JX.$V(e); + + var outer = JX.Vector.getPos(table); + var inner = JX.Vector.getPos(dragging); + + offset_top = inner.y - outer.y; + new_top = offset_top; + + dragging.style.top = offset_top + 'px'; + }); + JX.Stratcom.listen('mousemove', null, function(e){ + if (!dragging) { + return; + } + var cursor = JX.$V(e); + + new_top = cursor.y - origin.y + offset_top; + new_top = Math.min(new_top, 1320); + new_top = Math.max(new_top, 0); + new_top = Math.floor(new_top/15) * 15; + + dragging.style.top = new_top + 'px'; + }); + JX.Stratcom.listen('mouseup', null, function(){ + var data = JX.Stratcom.getData(dragging); + var record = data.record; + + if (!dragging) { + return; + } + if (new_top == offset_top) { + var now = new Date(); + if (now.getTime() - click_time.getTime() < 250) { + JX.$U(record.uri).go(); + } + + JX.DOM.alterClass(dragging, 'phui-drag', false); + dragging = false; + return; + } + var new_time = first_event_hour_epoch + (new_top * 60); + var id = data.eventID; + var duration = record.eventEndEpoch - record.eventStartEpoch; + record.eventStartEpoch = new_time; + record.eventEndEpoch = new_time + duration; + record.top = new_top + 'px'; + + new JX.Workflow( + '/calendar/event/drag/' + id + '/', + {start: new_time}) + .start(); + + JX.DOM.alterClass(dragging, 'phui-drag', false); + dragging = false; + + clusterAndDrawEvents(); + }); + + JX.DOM.listen(table_wrapper, 'click', 'phui-calendar-day-event', function(e){ + if (e.isNormalClick()) { + e.kill(); + } + }); + + var hourly_events_wrapper = JX.$N( + 'div', + {style: { + position: 'absolute', + left: '69px', + right: 0 + }}); + + clusterAndDrawEvents(); + + var daily_wrapper = JX.$N( + 'div', + {style: {position: 'relative'}}, + [hourly_events_wrapper, table]); + + JX.DOM.setContent(table_wrapper, [all_day_events, daily_wrapper]); + });