From 963485a3da25c97d0ba480f7179f6fe51ea153ee Mon Sep 17 00:00:00 2001 From: lkassianik Date: Sat, 23 May 2015 19:47:23 -0700 Subject: [PATCH] Rescheduling events by dragging them in day view Summary: Ref T8300, Rescheduling events by dragging them in day view Test Plan: Open day view, drag events, observe them reschedule. Reviewers: chad, epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, epriestley Maniphest Tasks: T8300 Differential Revision: https://secure.phabricator.com/D12988 --- resources/celerity/map.php | 14 +- src/__phutil_library_map__.php | 2 + .../PhabricatorCalendarApplication.php | 2 + ...PhabricatorCalendarEventDragController.php | 66 +++++++ .../phui/calendar/PHUICalendarDayView.php | 81 ++++---- webroot/rsrc/css/core/z-index.css | 4 + .../css/phui/calendar/phui-calendar-day.css | 12 +- .../application/calendar/behavior-day-view.js | 183 ++++++++++++++---- 8 files changed, 274 insertions(+), 90 deletions(-) create mode 100644 src/applications/calendar/controller/PhabricatorCalendarEventDragController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 422fee0b9d..1a237ab2d5 100644 --- 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 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 @@ return array( '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 index cbc2e27de1..bb4a613cae 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1498,6 +1498,7 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( ), '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 index 534b11d2e8..d41577a3af 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -54,6 +54,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { => '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 index 0000000000..7b813cdc44 --- /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 index 30d35e93fd..9901ad81b0 100644 --- a/src/view/phui/calendar/PHUICalendarDayView.php +++ b/src/view/phui/calendar/PHUICalendarDayView.php @@ -9,7 +9,6 @@ final class PHUICalendarDayView extends AphrontView { private $year; private $browseURI; private $events = array(); - private $jsTodayEvents = array(); private $allDayEvents = array(); @@ -44,8 +43,11 @@ final class PHUICalendarDayView extends AphrontView { 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 @@ final class PHUICalendarDayView extends AphrontView { } $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 @@ final class PHUICalendarDayView extends AphrontView { } } - 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; + $day_duration = ($day_end_epoch - $first_event_hour->format('U')) / 60; + + $top = (($event_start - $first_event_hour->format('U')) + / ($day_end_epoch - $first_event_hour->format('U'))) + * $day_duration; $top = max(0, $top); - $height = (($event_end - $event_start) / ($hour_end - $hour_start)) - * 100; - $height = min(2400, $height); + $height = (($event_end - $event_start) + / ($day_end_epoch - $first_event_hour->format('U'))) + * $day_duration; + $height = min($day_duration, $height); - if ($first_event_hour === null) { - $first_event_hour = $hour; - } - - $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 @@ final class PHUICalendarDayView extends AphrontView { '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 index 11cb76de27..e22187dcda 100644 --- 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 index 715bd6fd3b..e90e2b12aa 100644 --- 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 index 9c42b7f719..ae4aaf6b6b 100644 --- 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 @@ JX.behavior('day-view', function(config) { 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 @@ JX.behavior('day-view', function(config) { 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 @@ JX.behavior('day-view', function(config) { 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 @@ JX.behavior('day-view', function(config) { 'div', { className: 'phui-calendar-day-event', + sigil: sigil, + meta: {eventID: eventID, record: e, uri: uri}, style: { left: offset, width: width, @@ -145,7 +142,7 @@ JX.behavior('day-view', function(config) { 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 @@ JX.behavior('day-view', function(config) { 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 @@ JX.behavior('day-view', function(config) { 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 @@ JX.behavior('day-view', function(config) { {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]); + });