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

Support natural ordering of workboards

Summary:
Ref T4807. This is probably a complete fix, but I'd be surprised if there isn't a little cleanup I missed.

When users drag tasks on a "natural"-ordered workboard, leave things where they put them.

This isn't //too// bad since a lot of the existing work is completely reusable (e.g., we don't need any new JS).

Test Plan:
- Dragged a bunch of stuff around, it stayed where I put it after dropped and when reloaded.
- Dragged stuff across priorities, no zany priority changes (in "natural" mode).
- Created new tasks, they show up at the top.
- Tagged new tasks, they show up at the top of backlog.
- Swapped to "priority" mode and got sorting and the old priority-altering reordering.
- Added tasks in priority mode.
- Viewed task transactions for correctness/sanity.

Reviewers: btrahan, chad

Reviewed By: chad

Subscribers: chad, epriestley

Maniphest Tasks: T4807

Differential Revision: https://secure.phabricator.com/D10182

+202 -14
+17 -4
src/applications/maniphest/controller/ManiphestTaskEditController.php
··· 387 387 ->withPHIDs($task_phids) 388 388 ->execute(); 389 389 390 - $sort_map = mpull( 391 - $column_tasks, 392 - 'getPrioritySortVector', 393 - 'getPHID'); 390 + if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { 391 + // TODO: This is a little bit awkward, because PHP and JS use 392 + // slightly different sort order parameters to achieve the same 393 + // effect. It would be unify this a bit at some point. 394 + $sort_map = array(); 395 + foreach ($positions as $position) { 396 + $sort_map[$position->getObjectPHID()] = array( 397 + -$position->getSequence(), 398 + $position->getID(), 399 + ); 400 + } 401 + } else { 402 + $sort_map = mpull( 403 + $column_tasks, 404 + 'getPrioritySortVector', 405 + 'getPHID'); 406 + } 394 407 395 408 $data = array( 396 409 'sortMap' => $sort_map,
+117 -7
src/applications/maniphest/editor/ManiphestTransactionEditor.php
··· 190 190 pht("Expected 'projectPHID' in column transaction.")); 191 191 } 192 192 193 + $old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array()); 193 194 $new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array()); 194 195 if (count($new_phids) !== 1) { 195 196 throw new Exception( 196 197 pht("Expected exactly one 'columnPHIDs' in column transaction.")); 197 198 } 198 199 200 + $columns = id(new PhabricatorProjectColumnQuery()) 201 + ->setViewer($this->requireActor()) 202 + ->withPHIDs($new_phids) 203 + ->execute(); 204 + $columns = mpull($columns, null, 'getPHID'); 205 + 199 206 $positions = id(new PhabricatorProjectColumnPositionQuery()) 200 207 ->setViewer($this->requireActor()) 201 208 ->withObjectPHIDs(array($object->getPHID())) 202 209 ->withBoardPHIDs(array($board_phid)) 203 210 ->execute(); 204 211 212 + $before_phid = idx($xaction->getNewValue(), 'beforePHID'); 213 + $after_phid = idx($xaction->getNewValue(), 'afterPHID'); 214 + 215 + if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) { 216 + // If we are not moving the object between columns and also not 217 + // reordering the position, this is a move on some other order 218 + // (like priority). We can leave the positions untouched and just 219 + // bail, there's no work to be done. 220 + return; 221 + } 222 + 223 + // Otherwise, we're either moving between columns or adjusting the 224 + // object's position in the "natural" ordering, so we do need to update 225 + // some rows. 226 + 205 227 // Remove all existing column positions on the board. 206 228 207 229 foreach ($positions as $position) { 208 230 $position->delete(); 209 231 } 210 232 211 - // Add the new column position. 233 + // Add the new column positions. 212 234 213 235 foreach ($new_phids as $phid) { 214 - id(new PhabricatorProjectColumnPosition()) 236 + $column = idx($columns, $phid); 237 + if (!$column) { 238 + throw new Exception( 239 + pht('No such column "%s" exists!', $phid)); 240 + } 241 + 242 + // Load the other object positions in the column. Note that we must 243 + // skip implicit column creation to avoid generating a new position 244 + // if the target column is a backlog column. 245 + 246 + $other_positions = id(new PhabricatorProjectColumnPositionQuery()) 247 + ->setViewer($this->requireActor()) 248 + ->withColumns(array($column)) 249 + ->withBoardPHIDs(array($board_phid)) 250 + ->setSkipImplicitCreate(true) 251 + ->execute(); 252 + $other_positions = msort($other_positions, 'getOrderingKey'); 253 + 254 + // Set up the new position object. We're going to figure out the 255 + // right sequence number and then persist this object with that 256 + // sequence number. 257 + $new_position = id(new PhabricatorProjectColumnPosition()) 215 258 ->setBoardPHID($board_phid) 216 - ->setColumnPHID($phid) 217 - ->setObjectPHID($object->getPHID()) 218 - // TODO: Do real sequence stuff. 219 - ->setSequence(0) 220 - ->save(); 259 + ->setColumnPHID($column->getPHID()) 260 + ->setObjectPHID($object->getPHID()); 261 + 262 + $updates = array(); 263 + $sequence = 0; 264 + 265 + // If we're just dropping this into the column without any specific 266 + // position information, put it at the top. 267 + if (!$before_phid && !$after_phid) { 268 + $new_position->setSequence($sequence)->save(); 269 + $sequence++; 270 + } 271 + 272 + foreach ($other_positions as $position) { 273 + $object_phid = $position->getObjectPHID(); 274 + 275 + // If this is the object we're moving before and we haven't 276 + // saved yet, insert here. 277 + if (($before_phid == $object_phid) && !$new_position->getID()) { 278 + $new_position->setSequence($sequence)->save(); 279 + $sequence++; 280 + } 281 + 282 + // This object goes here in the sequence; we might need to update 283 + // the row. 284 + if ($sequence != $position->getSequence()) { 285 + $updates[$position->getID()] = $sequence; 286 + } 287 + $sequence++; 288 + 289 + // If this is the object we're moving after and we haven't saved 290 + // yet, insert here. 291 + if (($after_phid == $object_phid) && !$new_position->getID()) { 292 + $new_position->setSequence($sequence)->save(); 293 + $sequence++; 294 + } 295 + } 296 + 297 + // We should have found a place to put it. 298 + if (!$new_position->getID()) { 299 + throw new Exception( 300 + pht('Unable to find a place to insert object on column!')); 301 + } 302 + 303 + // If we changed other objects' column positions, bulk reorder them. 304 + 305 + if ($updates) { 306 + $position = new PhabricatorProjectColumnPosition(); 307 + $conn_w = $position->establishConnection('w'); 308 + 309 + $pairs = array(); 310 + foreach ($updates as $id => $sequence) { 311 + // This is ugly because MySQL gets upset with us if it is 312 + // configured strictly and we attempt inserts which can't work. 313 + // We'll never actually do these inserts since they'll always 314 + // collide (triggering the ON DUPLICATE KEY logic), so we just 315 + // provide dummy values in order to get there. 316 + 317 + $pairs[] = qsprintf( 318 + $conn_w, 319 + '(%d, %d, "", "", "")', 320 + $id, 321 + $sequence); 322 + } 323 + 324 + queryfx( 325 + $conn_w, 326 + 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) 327 + VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', 328 + $position->getTableName(), 329 + implode(', ', $pairs)); 330 + } 221 331 } 222 332 break; 223 333 default:
+10
src/applications/maniphest/storage/ManiphestTransaction.php
··· 125 125 break; 126 126 case self::TYPE_SUBPRIORITY: 127 127 return true; 128 + case self::TYPE_PROJECT_COLUMN: 129 + $old_cols = idx($this->getOldValue(), 'columnPHIDs'); 130 + $new_cols = idx($this->getNewValue(), 'columnPHIDs'); 131 + 132 + $old_cols = array_values($old_cols); 133 + $new_cols = array_values($new_cols); 134 + sort($old_cols); 135 + sort($new_cols); 136 + 137 + return ($old_cols === $new_cols); 128 138 } 129 139 130 140 return parent::shouldHide();
+17
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 178 178 $task_map[$column_phid][] = $task_phid; 179 179 } 180 180 181 + // If we're showing the board in "natural" order, sort columns by their 182 + // column positions. 183 + if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) { 184 + foreach ($task_map as $column_phid => $task_phids) { 185 + $order = array(); 186 + foreach ($task_phids as $task_phid) { 187 + if (isset($positions[$task_phid])) { 188 + $order[$task_phid] = $positions[$task_phid]->getOrderingKey(); 189 + } else { 190 + $order[$task_phid] = 0; 191 + } 192 + } 193 + asort($order); 194 + $task_map[$column_phid] = array_keys($order); 195 + } 196 + } 197 + 181 198 $task_can_edit_map = id(new PhabricatorPolicyFilter()) 182 199 ->setViewer($viewer) 183 200 ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
+13 -2
src/applications/project/controller/PhabricatorProjectMoveController.php
··· 17 17 $object_phid = $request->getStr('objectPHID'); 18 18 $after_phid = $request->getStr('afterPHID'); 19 19 $before_phid = $request->getStr('beforePHID'); 20 + $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); 21 + 20 22 21 23 $project = id(new PhabricatorProjectQuery()) 22 24 ->setViewer($viewer) ··· 65 67 66 68 $xactions = array(); 67 69 70 + if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { 71 + $order_params = array( 72 + 'afterPHID' => $after_phid, 73 + 'beforePHID' => $before_phid, 74 + ); 75 + } else { 76 + $order_params = array(); 77 + } 78 + 68 79 $xactions[] = id(new ManiphestTransaction()) 69 80 ->setTransactionType(ManiphestTransaction::TYPE_PROJECT_COLUMN) 70 81 ->setNewValue( 71 82 array( 72 83 'columnPHIDs' => array($column->getPHID()), 73 84 'projectPHID' => $column->getProjectPHID(), 74 - )) 85 + ) + $order_params) 75 86 ->setOldValue( 76 87 array( 77 88 'columnPHIDs' => mpull($positions, 'getColumnPHID'), ··· 86 97 $task_phids[] = $before_phid; 87 98 } 88 99 89 - if ($task_phids) { 100 + if ($task_phids && ($order == PhabricatorProjectColumn::ORDER_PRIORITY)) { 90 101 $tasks = id(new ManiphestTaskQuery()) 91 102 ->setViewer($viewer) 92 103 ->withPHIDs($task_phids)
+17 -1
src/applications/project/query/PhabricatorProjectColumnPositionQuery.php
··· 9 9 private $columns; 10 10 11 11 private $needColumns; 12 + private $skipImplicitCreate; 12 13 13 14 public function withIDs(array $ids) { 14 15 $this->ids = $ids; ··· 46 47 return $this; 47 48 } 48 49 50 + 51 + /** 52 + * Skip implicit creation of column positions which are implied but do not 53 + * yet exist. 54 + * 55 + * This is primarily useful internally. 56 + * 57 + * @param bool True to skip implicit creation of column positions. 58 + * @return this 59 + */ 60 + public function setSkipImplicitCreate($skip) { 61 + $this->skipImplicitCreate = $skip; 62 + return $this; 63 + } 64 + 49 65 // NOTE: For now, boards are always attached to projects. However, they might 50 66 // not be in the future. This generalization just anticipates a future where 51 67 // we let other types of objects (like users) have boards, or let boards ··· 93 109 // column and put it in the default column. 94 110 95 111 $must_type_filter = false; 96 - if ($this->columns) { 112 + if ($this->columns && !$this->skipImplicitCreate) { 97 113 $default_map = array(); 98 114 foreach ($this->columns as $column) { 99 115 if ($column->isDefaultColumn()) {
+11
src/applications/project/storage/PhabricatorProjectColumnPosition.php
··· 25 25 return $this; 26 26 } 27 27 28 + public function getOrderingKey() { 29 + // Low sequence numbers go above high sequence numbers. 30 + // High position IDs go above low position IDs. 31 + // Broadly, this makes newly added stuff float to the top. 32 + 33 + return sprintf( 34 + '~%012d%012d', 35 + $this->getSequence(), 36 + ((1 << 31) - $this->getID())); 37 + } 38 + 28 39 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 29 40 30 41 public function getCapabilities() {