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

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

+274 -90
+7 -7
resources/celerity/map.php
··· 7 7 */ 8 8 return array( 9 9 'names' => array( 10 - 'core.pkg.css' => '36142bff', 10 + 'core.pkg.css' => '439658b5', 11 11 'core.pkg.js' => '328799d0', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => 'bb338e4b', ··· 112 112 'rsrc/css/core/core.css' => 'aaea7a7a', 113 113 'rsrc/css/core/remarkup.css' => '07b7dc54', 114 114 'rsrc/css/core/syntax.css' => '6b7b24d9', 115 - 'rsrc/css/core/z-index.css' => '8414a09b', 115 + 'rsrc/css/core/z-index.css' => 'c4732d32', 116 116 'rsrc/css/diviner/diviner-shared.css' => '38813222', 117 117 'rsrc/css/font/font-awesome.css' => 'e2e712fe', 118 118 'rsrc/css/font/font-source-sans-pro.css' => '8906c07b', ··· 121 121 'rsrc/css/layout/phabricator-hovercard-view.css' => 'dd9121a9', 122 122 'rsrc/css/layout/phabricator-side-menu-view.css' => 'c1db9e9c', 123 123 'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894', 124 - 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'c0cf782a', 124 + 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'feba82c5', 125 125 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338', 126 126 'rsrc/css/phui/calendar/phui-calendar-month.css' => '476be7e0', 127 127 'rsrc/css/phui/calendar/phui-calendar.css' => 'ccabe893', ··· 331 331 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', 332 332 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 333 333 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 334 - 'rsrc/js/application/calendar/behavior-day-view.js' => 'f4f4ad80', 334 + 'rsrc/js/application/calendar/behavior-day-view.js' => 'dc0065ab', 335 335 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', 336 336 'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de', 337 337 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '10246726', ··· 554 554 'javelin-behavior-dashboard-move-panels' => '82439934', 555 555 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 556 556 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 557 - 'javelin-behavior-day-view' => 'f4f4ad80', 557 + 'javelin-behavior-day-view' => 'dc0065ab', 558 558 'javelin-behavior-device' => 'a205cf28', 559 559 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 560 560 'javelin-behavior-differential-comment-jump' => '4fdb476d', ··· 752 752 'phabricator-uiexample-reactor-select' => 'a155550f', 753 753 'phabricator-uiexample-reactor-sendclass' => '1def2711', 754 754 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 755 - 'phabricator-zindex-css' => '8414a09b', 755 + 'phabricator-zindex-css' => 'c4732d32', 756 756 'phame-css' => '88bd4705', 757 757 'pholio-css' => '95174bdd', 758 758 'pholio-edit-css' => '3ad9d1ee', ··· 767 767 'phui-box-css' => '7b3a2eed', 768 768 'phui-button-css' => 'de610129', 769 769 'phui-calendar-css' => 'ccabe893', 770 - 'phui-calendar-day-css' => 'c0cf782a', 770 + 'phui-calendar-day-css' => 'feba82c5', 771 771 'phui-calendar-list-css' => 'c1c7f338', 772 772 'phui-calendar-month-css' => '476be7e0', 773 773 'phui-crumbs-view-css' => '594d719e',
+2
src/__phutil_library_map__.php
··· 1498 1498 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', 1499 1499 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', 1500 1500 'PhabricatorCalendarEventCommentController' => 'applications/calendar/controller/PhabricatorCalendarEventCommentController.php', 1501 + 'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php', 1501 1502 'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php', 1502 1503 'PhabricatorCalendarEventEditIconController' => 'applications/calendar/controller/PhabricatorCalendarEventEditIconController.php', 1503 1504 'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php', ··· 4853 4854 ), 4854 4855 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', 4855 4856 'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController', 4857 + 'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController', 4856 4858 'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController', 4857 4859 'PhabricatorCalendarEventEditIconController' => 'PhabricatorCalendarController', 4858 4860 'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor',
+2
src/applications/calendar/application/PhabricatorCalendarApplication.php
··· 54 54 => 'PhabricatorCalendarEventEditController', 55 55 'edit/(?P<id>[1-9]\d*)/' 56 56 => 'PhabricatorCalendarEventEditController', 57 + 'drag/(?P<id>[1-9]\d*)/' 58 + => 'PhabricatorCalendarEventDragController', 57 59 'cancel/(?P<id>[1-9]\d*)/' 58 60 => 'PhabricatorCalendarEventCancelController', 59 61 '(?P<action>join|decline|accept)/(?P<id>[1-9]\d*)/'
+66
src/applications/calendar/controller/PhabricatorCalendarEventDragController.php
··· 1 + <?php 2 + 3 + final class PhabricatorCalendarEventDragController 4 + extends PhabricatorCalendarController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + 9 + $event = id(new PhabricatorCalendarEventQuery()) 10 + ->setViewer($viewer) 11 + ->withIDs(array($request->getURIData('id'))) 12 + ->requireCapabilities( 13 + array( 14 + PhabricatorPolicyCapability::CAN_VIEW, 15 + PhabricatorPolicyCapability::CAN_EDIT, 16 + )) 17 + ->executeOne(); 18 + if (!$event) { 19 + return new Aphront404Response(); 20 + } 21 + 22 + if (!$request->validateCSRF()) { 23 + return new Aphront400Response(); 24 + } 25 + 26 + if ($event->getIsAllDay()) { 27 + return new Aphront400Response(); 28 + } 29 + 30 + $xactions = array(); 31 + 32 + $duration = $event->getDateTo() - $event->getDateFrom(); 33 + 34 + $start = $request->getInt('start'); 35 + $start_value = id(AphrontFormDateControlValue::newFromEpoch( 36 + $viewer, 37 + $start)); 38 + 39 + $end = $start + $duration; 40 + $end_value = id(AphrontFormDateControlValue::newFromEpoch( 41 + $viewer, 42 + $end)); 43 + 44 + 45 + $xactions[] = id(new PhabricatorCalendarEventTransaction()) 46 + ->setTransactionType( 47 + PhabricatorCalendarEventTransaction::TYPE_START_DATE) 48 + ->setNewValue($start_value); 49 + 50 + $xactions[] = id(new PhabricatorCalendarEventTransaction()) 51 + ->setTransactionType( 52 + PhabricatorCalendarEventTransaction::TYPE_END_DATE) 53 + ->setNewValue($end_value); 54 + 55 + 56 + $editor = id(new PhabricatorCalendarEventEditor()) 57 + ->setActor($viewer) 58 + ->setContinueOnMissingFields(true) 59 + ->setContentSourceFromRequest($request) 60 + ->setContinueOnNoEffect(true); 61 + 62 + $xactions = $editor->applyTransactions($event, $xactions); 63 + 64 + return id(new AphrontReloadResponse()); 65 + } 66 + }
+37 -44
src/view/phui/calendar/PHUICalendarDayView.php
··· 9 9 private $year; 10 10 private $browseURI; 11 11 private $events = array(); 12 - private $jsTodayEvents = array(); 13 12 14 13 private $allDayEvents = array(); 15 14 ··· 44 43 public function render() { 45 44 require_celerity_resource('phui-calendar-day-css'); 46 45 46 + $viewer = $this->getUser(); 47 + 47 48 $hours = $this->getHoursOfDay(); 48 49 $js_hours = array(); 50 + $js_today_events = array(); 49 51 50 52 foreach ($hours as $hour) { 51 53 $js_hours[] = array( ··· 55 57 } 56 58 57 59 $first_event_hour = null; 58 - 59 - $js_hourly_events = array(); 60 60 $js_today_all_day_events = array(); 61 - 62 61 $all_day_events = $this->getAllDayEvents(); 63 62 64 63 $day_start = $this->getDateTime(); ··· 81 80 } 82 81 } 83 82 84 - foreach ($hours as $hour) { 85 - $current_hour_events = array(); 86 - $hour_start = $hour->format('U'); 87 - $hour_end = id(clone $hour)->modify('+1 hour')->format('U'); 83 + $this->events = msort($this->events, 'getEpochStart'); 84 + 85 + if (!$this->events) { 86 + $first_event_hour = $this->getDateTime()->setTime(8, 0, 0); 87 + } 88 + 89 + foreach ($this->events as $event) { 90 + if ($event->getIsAllDay()) { 91 + continue; 92 + } 93 + if ($event->getEpochStart() <= $day_end_epoch && 94 + $event->getEpochEnd() > $day_start_epoch) { 88 95 89 - foreach ($this->events as $event) { 90 - if ($event->getIsAllDay()) { 91 - continue; 96 + if ($first_event_hour === null) { 97 + $first_event_hour = new DateTime('@'.$event->getEpochStart()); 98 + $first_event_hour->setTimeZone($viewer->getTimeZone()); 99 + $eight_am = $this->getDateTime()->setTime(8, 0, 0); 100 + if ($eight_am->format('U') < $first_event_hour->format('U')) { 101 + $first_event_hour = clone $eight_am; 102 + } 92 103 } 93 - if (($hour == $day_start && 94 - $event->getEpochStart() <= $hour_start && 95 - $event->getEpochEnd() > $day_start_epoch) || 96 - ($event->getEpochStart() >= $hour_start 97 - && $event->getEpochStart() < $hour_end)) { 98 - $current_hour_events[] = $event; 99 - $this->jsTodayEvents[] = array( 100 - 'eventStartEpoch' => $event->getEpochStart(), 101 - 'eventEndEpoch' => $event->getEpochEnd(), 102 - 'eventName' => $event->getName(), 103 - 'eventID' => $event->getEventID(), 104 - 'viewerIsInvited' => $event->getViewerIsInvited(), 105 - 'uri' => $event->getURI(), 106 - ); 107 - } 108 - } 109 - foreach ($current_hour_events as $event) { 110 - $day_start_epoch = $this->getDateTime()->format('U'); 104 + 111 105 $event_start = max($event->getEpochStart(), $day_start_epoch); 112 106 $event_end = min($event->getEpochEnd(), $day_end_epoch); 113 107 114 - $top = (($event_start - $hour_start) / ($hour_end - $hour_start)) 115 - * 100; 116 - $top = max(0, $top); 108 + $day_duration = ($day_end_epoch - $first_event_hour->format('U')) / 60; 117 109 118 - $height = (($event_end - $event_start) / ($hour_end - $hour_start)) 119 - * 100; 120 - $height = min(2400, $height); 110 + $top = (($event_start - $first_event_hour->format('U')) 111 + / ($day_end_epoch - $first_event_hour->format('U'))) 112 + * $day_duration; 113 + $top = max(0, $top); 121 114 122 - if ($first_event_hour === null) { 123 - $first_event_hour = $hour; 124 - } 115 + $height = (($event_end - $event_start) 116 + / ($day_end_epoch - $first_event_hour->format('U'))) 117 + * $day_duration; 118 + $height = min($day_duration, $height); 125 119 126 - $js_hourly_events[] = array( 120 + $js_today_events[] = array( 127 121 'eventStartEpoch' => $event->getEpochStart(), 128 122 'eventEndEpoch' => $event->getEpochEnd(), 129 123 'eventName' => $event->getName(), 130 124 'eventID' => $event->getEventID(), 131 125 'viewerIsInvited' => $event->getViewerIsInvited(), 132 126 'uri' => $event->getURI(), 133 - 'hour' => $hour->format('G'), 134 127 'offset' => '0', 135 128 'width' => '100%', 136 - 'top' => $top.'%', 137 - 'height' => $height.'%', 129 + 'top' => $top.'px', 130 + 'height' => $height.'px', 138 131 ); 139 132 } 140 133 } ··· 156 149 'day-view', 157 150 array( 158 151 'allDayEvents' => $js_today_all_day_events, 159 - 'todayEvents' => $this->jsTodayEvents, 160 - 'hourlyEvents' => $js_hourly_events, 152 + 'todayEvents' => $js_today_events, 161 153 'hours' => $js_hours, 162 154 'firstEventHour' => $first_event_hour->format('G'), 155 + 'firstEventHourEpoch' => $first_event_hour->format('U'), 163 156 'tableID' => $table_id, 164 157 )); 165 158
+4
webroot/rsrc/css/core/z-index.css
··· 36 36 z-index: 2; 37 37 } 38 38 39 + div.phui-calendar-day-event { 40 + z-index: 2; 41 + } 42 + 39 43 .slowvote-above-the-bar { 40 44 z-index: 3; 41 45 }
+10 -2
webroot/rsrc/css/phui/calendar/phui-calendar-day.css
··· 35 35 border-top: 1px solid {$lightgreyborder}; 36 36 } 37 37 38 - .phui-calendar-day-view td div.phui-calendar-day-event { 38 + .phui-drag { 39 + opacity: .25; 40 + } 41 + 42 + div.phui-calendar-day-event { 39 43 width: 100%; 40 44 position: absolute; 41 45 top: 0; ··· 43 47 min-height: 30px; 44 48 } 45 49 50 + div.phui-calendar-day-event.all-day { 51 + position: relative; 52 + } 53 + 46 54 .phui-calendar-day-event-link { 47 55 padding: 8px; 48 56 border: 1px solid {$greyborder}; 49 57 background-color: {$darkgreybackground}; 50 - margin: 0 4px; 58 + margin: 0 1px; 51 59 position: absolute; 52 60 left: 0; 53 61 right: 0;
+146 -37
webroot/rsrc/js/application/calendar/behavior-day-view.js
··· 4 4 5 5 6 6 JX.behavior('day-view', function(config) { 7 - var hours = config.hours; 8 - var first_event_hour = config.firstEventHour; 9 - var hourly_events = config.hourlyEvents; 10 - var today_events = config.todayEvents; 11 - var today_all_day_events = config.allDayEvents; 12 - var table_wrapper = JX.$(config.tableID); 13 - 14 7 15 8 function findTodayClusters() { 16 9 var events = today_events.sort(function(x, y){ ··· 23 16 var today_event = events[i]; 24 17 25 18 var destination_cluster_index = null; 26 - var event_start = today_event.eventStartEpoch - (30*60); 27 - var event_end = today_event.eventEndEpoch + (30*60); 19 + var event_start = today_event.eventStartEpoch - (60); 20 + var event_end = today_event.eventEndEpoch + (60); 28 21 29 22 for (var j=0; j < clusters.length; j++) { 30 23 var cluster = clusters[j]; ··· 59 52 return clusters; 60 53 } 61 54 62 - function updateEventsFromCluster(cluster, hourly_events) { 55 + function updateEventsFromCluster(cluster) { 63 56 var cluster_size = cluster.length; 64 57 var n = 0; 65 58 for(var i=0; i < cluster.length; i++) { ··· 69 62 var offset = ((n / cluster_size) * 100) + '%'; 70 63 var width = ((1 / cluster_size) * 100) + '%'; 71 64 72 - for (var j=0; j < hourly_events.length; j++) { 73 - if (hourly_events[j].eventID == event_id) { 65 + for (var j=0; j < today_events.length; j++) { 66 + if (today_events[j].eventID == event_id) { 74 67 75 - hourly_events[j]['offset'] = offset; 76 - hourly_events[j]['width'] = width; 68 + today_events[j]['offset'] = offset; 69 + today_events[j]['width'] = width; 77 70 } 78 71 } 79 72 n++; 80 73 } 81 74 82 - return hourly_events; 75 + return today_events; 83 76 } 84 77 85 - function drawEvent(hourly_event) { 86 - var name = hourly_event['eventName']; 87 - var viewerIsInvited = hourly_event['viewerIsInvited']; 88 - var offset = hourly_event['offset']; 89 - var width = hourly_event['width']; 90 - var top = hourly_event['top']; 91 - var height = hourly_event['height']; 92 - var uri = hourly_events['uri']; 78 + function drawEvent(e) { 79 + var name = e['eventName']; 80 + var eventID = e['eventID']; 81 + var viewerIsInvited = e['viewerIsInvited']; 82 + var offset = e['offset']; 83 + var width = e['width']; 84 + var top = e['top']; 85 + var height = e['height']; 86 + var uri = e['uri']; 93 87 88 + var sigil = 'phui-calendar-day-event'; 94 89 var link_class = 'phui-calendar-day-event-link'; 95 90 96 91 if (viewerIsInvited) { ··· 109 104 'div', 110 105 { 111 106 className: 'phui-calendar-day-event', 107 + sigil: sigil, 108 + meta: {eventID: eventID, record: e, uri: uri}, 112 109 style: { 113 110 left: offset, 114 111 width: width, ··· 145 142 146 143 var div_all_day = JX.$N( 147 144 'div', 148 - {className: 'phui-calendar-day-event'}, 145 + {className: 'phui-calendar-day-event all-day'}, 149 146 [all_day_label, name]); 150 147 151 148 return div_all_day; ··· 164 161 if (hours[i]['hour'] < min_early_hour) { 165 162 continue; 166 163 } 167 - var drawn_hourly_events = []; 168 164 var cell_time = JX.$N( 169 165 'td', 170 166 {className: 'phui-calendar-day-hour'}, 171 167 hours[i]['hour_meridian']); 172 168 173 - for (var j=0; j < hourly_events.length; j++) { 174 - if (hourly_events[j]['hour'] == hours[i]['hour']) { 175 - drawn_hourly_events.push(drawEvent(hourly_events[j])); 176 - } 177 - } 178 - 179 169 var cell_event = JX.$N( 180 170 'td', 181 171 { 182 172 className: 'phui-calendar-day-events' 183 - }, 184 - drawn_hourly_events); 173 + }); 174 + 185 175 var row = JX.$N( 186 176 'tr', 187 177 {}, ··· 191 181 return rows; 192 182 } 193 183 194 - var today_clusters = findTodayClusters(); 195 - for(var i=0; i < today_clusters.length; i++) { 196 - hourly_events = updateEventsFromCluster(today_clusters[i], hourly_events); 184 + function clusterAndDrawEvents() { 185 + var today_clusters = findTodayClusters(); 186 + for(var i=0; i < today_clusters.length; i++) { 187 + today_events = updateEventsFromCluster(today_clusters[i]); 188 + } 189 + var drawn_hourly_events = []; 190 + for (i=0; i < today_events.length; i++) { 191 + drawn_hourly_events.push(drawEvent(today_events[i])); 192 + } 193 + 194 + JX.DOM.setContent(hourly_events_wrapper, drawn_hourly_events); 195 + 197 196 } 197 + 198 + var hours = config.hours; 199 + var first_event_hour = config.firstEventHour; 200 + var first_event_hour_epoch = parseInt(config.firstEventHourEpoch, 10); 201 + var today_events = config.todayEvents; 202 + var today_all_day_events = config.allDayEvents; 203 + var table_wrapper = JX.$(config.tableID); 198 204 var rows = drawRows(); 199 205 200 206 var all_day_events = []; ··· 211 217 {className: 'phui-calendar-day-view'}, 212 218 rows); 213 219 214 - JX.DOM.setContent(table_wrapper, [all_day_events, table]); 220 + var dragging = false; 221 + var origin = null; 222 + 223 + var offset_top = null; 224 + var new_top = null; 225 + 226 + var click_time = null; 227 + 228 + JX.DOM.listen( 229 + table_wrapper, 230 + 'mousedown', 231 + 'phui-calendar-day-event', 232 + function(e){ 233 + 234 + if (!e.isNormalMouseEvent()) { 235 + return; 236 + } 237 + e.kill(); 238 + dragging = e.getNode('phui-calendar-day-event'); 239 + JX.DOM.alterClass(dragging, 'phui-drag', true); 240 + 241 + click_time = new Date(); 242 + 243 + origin = JX.$V(e); 244 + 245 + var outer = JX.Vector.getPos(table); 246 + var inner = JX.Vector.getPos(dragging); 247 + 248 + offset_top = inner.y - outer.y; 249 + new_top = offset_top; 250 + 251 + dragging.style.top = offset_top + 'px'; 252 + }); 253 + JX.Stratcom.listen('mousemove', null, function(e){ 254 + if (!dragging) { 255 + return; 256 + } 257 + var cursor = JX.$V(e); 258 + 259 + new_top = cursor.y - origin.y + offset_top; 260 + new_top = Math.min(new_top, 1320); 261 + new_top = Math.max(new_top, 0); 262 + new_top = Math.floor(new_top/15) * 15; 263 + 264 + dragging.style.top = new_top + 'px'; 265 + }); 266 + JX.Stratcom.listen('mouseup', null, function(){ 267 + var data = JX.Stratcom.getData(dragging); 268 + var record = data.record; 269 + 270 + if (!dragging) { 271 + return; 272 + } 273 + if (new_top == offset_top) { 274 + var now = new Date(); 275 + if (now.getTime() - click_time.getTime() < 250) { 276 + JX.$U(record.uri).go(); 277 + } 278 + 279 + JX.DOM.alterClass(dragging, 'phui-drag', false); 280 + dragging = false; 281 + return; 282 + } 283 + var new_time = first_event_hour_epoch + (new_top * 60); 284 + var id = data.eventID; 285 + var duration = record.eventEndEpoch - record.eventStartEpoch; 286 + record.eventStartEpoch = new_time; 287 + record.eventEndEpoch = new_time + duration; 288 + record.top = new_top + 'px'; 289 + 290 + new JX.Workflow( 291 + '/calendar/event/drag/' + id + '/', 292 + {start: new_time}) 293 + .start(); 294 + 295 + JX.DOM.alterClass(dragging, 'phui-drag', false); 296 + dragging = false; 297 + 298 + clusterAndDrawEvents(); 299 + }); 300 + 301 + JX.DOM.listen(table_wrapper, 'click', 'phui-calendar-day-event', function(e){ 302 + if (e.isNormalClick()) { 303 + e.kill(); 304 + } 305 + }); 306 + 307 + var hourly_events_wrapper = JX.$N( 308 + 'div', 309 + {style: { 310 + position: 'absolute', 311 + left: '69px', 312 + right: 0 313 + }}); 314 + 315 + clusterAndDrawEvents(); 316 + 317 + var daily_wrapper = JX.$N( 318 + 'div', 319 + {style: {position: 'relative'}}, 320 + [hourly_events_wrapper, table]); 321 + 322 + JX.DOM.setContent(table_wrapper, [all_day_events, daily_wrapper]); 323 + 215 324 });