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

On Workboards, sort groups by "natural order", not subpriority

Summary:
Depends on D20263. Ref T10333. I want to add groups like "Assignee" to workboards. This means you may have several tasks grouped under, say, "Alice".

When you drag the bottom-most task under "Alice" to the top, what does that mean?

Today, the only grouping is "Priority", and it means "change the task's secret/hidden global subpriority". However, this seems to generally be a somewhat-bad answer, and is quite complex. It also doesn't make much sense for an author grouping, since one task can't really be "more assigned" to Alice than another task.

Users likely intend this operation to mean "move it, visually, with no other effects" -- that is, user intent is to shuffle sticky notes around on a board, not edit anything substantive. The meaning is probably something like "this is similar to other nearby tasks" or "maybe this is a good place to start", which we can't really capture with any top-level attribute.

We could extend "subpriority" and give tasks a secret/hidden "sub-assignment strength" and so on, but this seems like a bad road to walk down. We'll also run into trouble later when subproject columns may appear on the board, and a user could want to put a task in different positions on different subprojects, conceivably.

In the "Natural" order view, we already have what is probably a generally better approach for this: a task display order particular to the column, that just remembers where you put the sticky notes.

Move away from "subpriority", and toward a world where we mostly keep sticky notes where you stuck them and move them around only when we have to. With no grouping, we still sort by "natural" order, as before. With priority grouping, we now sort by `<priority, natural>`. When you drag stuff around inside a priority group, we update the natural order.

This means that moving cards around on a "priority" board will also move them around on a "natural" board, at least somewhat. I think this is okay. If it's not intuitive, we could give every ordering its own separate "natural" view, so we remember where you stuck stuff on the "priority" board but that doesn't affect the "Natural" board. But I suspect we won't need to.

Test Plan:
- Viewed and dragged a natural board.
- Viewed and dragged a priority board.
- Dragged within and between groups of 0, 1, and multiple items.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T10333

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

+141 -201
+19 -19
resources/celerity/map.php
··· 408 408 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 409 409 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 410 410 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', 411 - 'rsrc/js/application/projects/WorkboardBoard.js' => 'a4f1e85d', 411 + 'rsrc/js/application/projects/WorkboardBoard.js' => '902a1551', 412 412 'rsrc/js/application/projects/WorkboardCard.js' => '887ef74f', 413 - 'rsrc/js/application/projects/WorkboardColumn.js' => 'ca444dca', 413 + 'rsrc/js/application/projects/WorkboardColumn.js' => '01ea93b3', 414 414 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 415 415 'rsrc/js/application/projects/WorkboardHeader.js' => '6e75daea', 416 416 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '2d641f7d', ··· 727 727 'javelin-view-renderer' => '9aae2b66', 728 728 'javelin-view-visitor' => '308f9fe4', 729 729 'javelin-websocket' => 'fdc13e4e', 730 - 'javelin-workboard-board' => 'a4f1e85d', 730 + 'javelin-workboard-board' => '902a1551', 731 731 'javelin-workboard-card' => '887ef74f', 732 - 'javelin-workboard-column' => 'ca444dca', 732 + 'javelin-workboard-column' => '01ea93b3', 733 733 'javelin-workboard-controller' => '42c7a5a7', 734 734 'javelin-workboard-header' => '6e75daea', 735 735 'javelin-workboard-header-template' => '2d641f7d', ··· 888 888 'javelin-behavior', 889 889 'javelin-uri', 890 890 'phabricator-notification', 891 + ), 892 + '01ea93b3' => array( 893 + 'javelin-install', 894 + 'javelin-workboard-card', 895 + 'javelin-workboard-header', 891 896 ), 892 897 '022516b4' => array( 893 898 'javelin-install', ··· 1615 1620 'javelin-workflow', 1616 1621 'javelin-stratcom', 1617 1622 ), 1623 + '902a1551' => array( 1624 + 'javelin-install', 1625 + 'javelin-dom', 1626 + 'javelin-util', 1627 + 'javelin-stratcom', 1628 + 'javelin-workflow', 1629 + 'phabricator-draggable-list', 1630 + 'javelin-workboard-column', 1631 + 'javelin-workboard-header-template', 1632 + ), 1618 1633 91863989 => array( 1619 1634 'javelin-install', 1620 1635 'javelin-stratcom', ··· 1749 1764 'javelin-dom', 1750 1765 'javelin-request', 1751 1766 'javelin-util', 1752 - ), 1753 - 'a4f1e85d' => array( 1754 - 'javelin-install', 1755 - 'javelin-dom', 1756 - 'javelin-util', 1757 - 'javelin-stratcom', 1758 - 'javelin-workflow', 1759 - 'phabricator-draggable-list', 1760 - 'javelin-workboard-column', 1761 - 'javelin-workboard-header-template', 1762 1767 ), 1763 1768 'a5257c4e' => array( 1764 1769 'javelin-install', ··· 1956 1961 'javelin-install', 1957 1962 'javelin-util', 1958 1963 'phabricator-keyboard-shortcut-manager', 1959 - ), 1960 - 'ca444dca' => array( 1961 - 'javelin-install', 1962 - 'javelin-workboard-card', 1963 - 'javelin-workboard-header', 1964 1964 ), 1965 1965 'cf32921f' => array( 1966 1966 'javelin-behavior',
-3
src/applications/maniphest/storage/ManiphestTask.php
··· 252 252 return array( 253 253 PhabricatorProjectColumn::ORDER_PRIORITY => array( 254 254 (int)-$this->getPriority(), 255 - PhabricatorProjectColumn::NODETYPE_CARD, 256 - (double)-$this->getSubpriority(), 257 - (int)-$this->getID(), 258 255 ), 259 256 ); 260 257 }
-8
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 522 522 $column->getPHID()); 523 523 524 524 $column_tasks = array_select_keys($tasks, $task_phids); 525 - 526 - // If we aren't using "natural" order, reorder the column by the original 527 - // query order. 528 - if ($this->sortKey != PhabricatorProjectColumn::ORDER_NATURAL) { 529 - $column_tasks = array_select_keys($column_tasks, array_keys($tasks)); 530 - } 531 - 532 525 $column_phid = $column->getPHID(); 533 526 534 527 $visible_columns[$column_phid] = $column; ··· 683 676 'projectPHID' => $project->getPHID(), 684 677 ); 685 678 $this->initBehavior('project-boards', $behavior_config); 686 - 687 679 688 680 $sort_menu = $this->buildSortMenu( 689 681 $viewer,
+25 -144
src/applications/project/controller/PhabricatorProjectMoveController.php
··· 71 71 ->setObjectPHIDs(array($object_phid)) 72 72 ->executeLayout(); 73 73 74 - $columns = $engine->getObjectColumns($board_phid, $object_phid); 75 - $old_column_phids = mpull($columns, 'getPHID'); 76 - 77 - $xactions = array(); 78 - 79 74 $order_params = array(); 80 - if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { 81 - if ($after_phid) { 82 - $order_params['afterPHID'] = $after_phid; 83 - } else if ($before_phid) { 84 - $order_params['beforePHID'] = $before_phid; 85 - } 75 + if ($after_phid) { 76 + $order_params['afterPHID'] = $after_phid; 77 + } else if ($before_phid) { 78 + $order_params['beforePHID'] = $before_phid; 86 79 } 87 80 81 + $xactions = array(); 88 82 $xactions[] = id(new ManiphestTransaction()) 89 83 ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) 90 84 ->setNewValue( ··· 94 88 ) + $order_params, 95 89 )); 96 90 97 - if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) { 98 - $header_priority = idx( 99 - $edit_header, 100 - PhabricatorProjectColumn::ORDER_PRIORITY); 101 - $priority_xactions = $this->getPriorityTransactions( 102 - $object, 103 - $after_phid, 104 - $before_phid, 105 - $header_priority); 106 - foreach ($priority_xactions as $xaction) { 107 - $xactions[] = $xaction; 108 - } 91 + $header_xactions = $this->newHeaderTransactions( 92 + $object, 93 + $order, 94 + $edit_header); 95 + foreach ($header_xactions as $header_xaction) { 96 + $xactions[] = $header_xaction; 109 97 } 110 98 111 99 $editor = id(new ManiphestTransactionEditor()) ··· 119 107 return $this->newCardResponse($board_phid, $object_phid); 120 108 } 121 109 122 - private function getPriorityTransactions( 110 + private function newHeaderTransactions( 123 111 ManiphestTask $task, 124 - $after_phid, 125 - $before_phid, 126 - $header_priority) { 112 + $order, 113 + array $header) { 127 114 128 115 $xactions = array(); 129 - $must_move = false; 130 116 131 - if ($header_priority !== null) { 132 - if ($task->getPriority() !== $header_priority) { 133 - $task = id(clone $task) 134 - ->setPriority($header_priority); 135 - 136 - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); 137 - $keyword = head(idx($keyword_map, $header_priority)); 138 - 139 - $xactions[] = id(new ManiphestTransaction()) 140 - ->setTransactionType( 141 - ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) 142 - ->setNewValue($keyword); 143 - 144 - $must_move = true; 145 - } 146 - } 147 - 148 - list($after_task, $before_task) = $this->loadPriorityTasks( 149 - $after_phid, 150 - $before_phid); 151 - 152 - if ($after_task && !$task->isLowerPriorityThan($after_task)) { 153 - $must_move = true; 154 - } 117 + switch ($order) { 118 + case PhabricatorProjectColumn::ORDER_PRIORITY: 119 + $new_priority = idx($header, $order); 155 120 156 - if ($before_task && !$task->isHigherPriorityThan($before_task)) { 157 - $must_move = true; 158 - } 121 + if ($task->getPriority() !== $new_priority) { 122 + $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); 123 + $keyword = head(idx($keyword_map, $new_priority)); 159 124 160 - // The move doesn't require a subpriority change to be valid, so don't 161 - // change the subpriority since we are not being forced to. 162 - if (!$must_move) { 163 - return $xactions; 164 - } 165 - 166 - $try = array( 167 - array($after_task, true), 168 - array($before_task, false), 169 - ); 170 - 171 - $pri = null; 172 - $sub = null; 173 - foreach ($try as $spec) { 174 - list($nearby_task, $is_after) = $spec; 175 - 176 - if (!$nearby_task) { 177 - continue; 178 - } 179 - 180 - list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( 181 - $nearby_task, 182 - $is_after); 183 - 184 - // If we drag under a "Low" header between a "Normal" task and a "Low" 185 - // task, we don't want to accept a subpriority assignment which changes 186 - // our priority to "Normal". Only accept a subpriority that keeps us in 187 - // the right primary priority. 188 - if ($header_priority !== null) { 189 - if ($pri !== $header_priority) { 190 - continue; 125 + $xactions[] = id(new ManiphestTransaction()) 126 + ->setTransactionType( 127 + ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) 128 + ->setNewValue($keyword); 191 129 } 192 - } 193 - 194 - // If we find a priority on the first try, don't keep going. 195 - break; 196 - } 197 - 198 - if ($pri !== null) { 199 - if ($header_priority === null) { 200 - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); 201 - $keyword = head(idx($keyword_map, $pri)); 202 - 203 - $xactions[] = id(new ManiphestTransaction()) 204 - ->setTransactionType( 205 - ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) 206 - ->setNewValue($keyword); 207 - } 208 - 209 - $xactions[] = id(new ManiphestTransaction()) 210 - ->setTransactionType( 211 - ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE) 212 - ->setNewValue($sub); 130 + break; 213 131 } 214 132 215 133 return $xactions; 216 - } 217 - 218 - private function loadPriorityTasks($after_phid, $before_phid) { 219 - $viewer = $this->getViewer(); 220 - 221 - $task_phids = array(); 222 - 223 - if ($after_phid) { 224 - $task_phids[] = $after_phid; 225 - } 226 - if ($before_phid) { 227 - $task_phids[] = $before_phid; 228 - } 229 - 230 - if (!$task_phids) { 231 - return array(null, null); 232 - } 233 - 234 - $tasks = id(new ManiphestTaskQuery()) 235 - ->setViewer($viewer) 236 - ->withPHIDs($task_phids) 237 - ->execute(); 238 - $tasks = mpull($tasks, null, 'getPHID'); 239 - 240 - if ($after_phid) { 241 - $after_task = idx($tasks, $after_phid); 242 - } else { 243 - $after_task = null; 244 - } 245 - 246 - if ($before_phid) { 247 - $before_task = idx($tasks, $before_phid); 248 - } else { 249 - $before_task = null; 250 - } 251 - 252 - return array($after_task, $before_task); 253 134 } 254 135 255 136 }
+37 -7
webroot/rsrc/js/application/projects/WorkboardBoard.js
··· 202 202 order: this.getOrder() 203 203 }; 204 204 205 + // We're going to send an "afterPHID" and a "beforePHID" if the card 206 + // was dropped immediately adjacent to another card. If a card was 207 + // dropped before or after a header, we don't send a PHID for the card 208 + // on the other side of the header. 209 + 210 + // If the view has headers, we always send the header the card was 211 + // dropped under. 212 + 205 213 var after_data; 206 214 var after_card = after_node; 207 215 while (after_card) { 208 216 after_data = JX.Stratcom.getData(after_card); 209 217 if (after_data.objectPHID) { 218 + break; 219 + } 220 + if (after_data.headerKey) { 210 221 break; 211 222 } 212 223 after_card = after_card.previousSibling; 213 224 } 214 225 215 226 if (after_data) { 216 - data.afterPHID = after_data.objectPHID; 227 + if (after_data.objectPHID) { 228 + data.afterPHID = after_data.objectPHID; 229 + } 217 230 } 218 231 219 232 var before_data; ··· 223 236 if (before_data.objectPHID) { 224 237 break; 225 238 } 239 + if (before_data.headerKey) { 240 + break; 241 + } 226 242 before_card = before_card.nextSibling; 227 243 } 228 244 229 245 if (before_data) { 230 - data.beforePHID = before_data.objectPHID; 246 + if (before_data.objectPHID) { 247 + data.beforePHID = before_data.objectPHID; 248 + } 231 249 } 232 250 233 - var header_key = JX.Stratcom.getData(after_node).headerKey; 234 - if (header_key) { 235 - var properties = this.getHeaderTemplate(header_key) 236 - .getEditProperties(); 237 - data.header = JX.JSON.stringify(properties); 251 + var header_data; 252 + var header_node = after_node; 253 + while (header_node) { 254 + header_data = JX.Stratcom.getData(header_node); 255 + if (header_data.headerKey) { 256 + break; 257 + } 258 + header_node = header_node.previousSibling; 259 + } 260 + 261 + if (header_data) { 262 + var header_key = header_data.headerKey; 263 + if (header_key) { 264 + var properties = this.getHeaderTemplate(header_key) 265 + .getEditProperties(); 266 + data.header = JX.JSON.stringify(properties); 267 + } 238 268 } 239 269 240 270 var visible_phids = [];
+60 -20
webroot/rsrc/js/application/projects/WorkboardColumn.js
··· 34 34 _cards: null, 35 35 _headers: null, 36 36 _naturalOrder: null, 37 + _orderVectors: null, 37 38 _panel: null, 38 39 _pointsNode: null, 39 40 _pointsContentNode: null, ··· 66 67 67 68 setNaturalOrder: function(order) { 68 69 this._naturalOrder = order; 70 + this._orderVectors = null; 69 71 return this; 70 72 }, 71 73 ··· 86 88 87 89 this._cards[phid] = card; 88 90 this._naturalOrder.push(phid); 91 + this._orderVectors = null; 89 92 90 93 return card; 91 94 }, ··· 97 100 for (var ii = 0; ii < this._naturalOrder.length; ii++) { 98 101 if (this._naturalOrder[ii] == phid) { 99 102 this._naturalOrder.splice(ii, 1); 103 + this._orderVectors = null; 100 104 break; 101 105 } 102 106 } ··· 127 131 this._naturalOrder.splice(index, 0, phid); 128 132 } 129 133 134 + this._orderVectors = null; 135 + 130 136 return this; 131 137 }, 132 138 ··· 204 210 var board = this.getBoard(); 205 211 var order = board.getOrder(); 206 212 207 - var list; 208 - if (order == 'natural') { 209 - list = this._getCardsSortedNaturally(); 210 - } else { 211 - list = this._getCardsSortedByKey(order); 212 - } 213 + var list = this._getCardsSortedByKey(order); 213 214 214 215 var ii; 215 216 var objects = []; ··· 285 286 var data = JX.Stratcom.getData(node); 286 287 287 288 if (data.objectPHID) { 288 - return board.getOrderVector(data.objectPHID, order); 289 + return this._getOrderVector(data.objectPHID, order); 289 290 } 290 291 291 292 return board.getHeaderTemplate(data.headerKey).getVector(); ··· 296 297 JX.DOM.alterClass(node, 'workboard-column-drop-target', is_target); 297 298 }, 298 299 299 - _getCardsSortedNaturally: function() { 300 - var list = []; 301 - 302 - for (var ii = 0; ii < this._naturalOrder.length; ii++) { 303 - var phid = this._naturalOrder[ii]; 304 - list.push(this.getCard(phid)); 305 - } 306 - 307 - return list; 308 - }, 309 - 310 300 _getCardsSortedByKey: function(order) { 311 301 var cards = this.getCards(); 312 302 ··· 322 312 323 313 _sortCards: function(order, u, v) { 324 314 var board = this.getBoard(); 325 - var u_vec = board.getOrderVector(u.getPHID(), order); 326 - var v_vec = board.getOrderVector(v.getPHID(), order); 315 + var u_vec = this._getOrderVector(u.getPHID(), order); 316 + var v_vec = this._getOrderVector(v.getPHID(), order); 327 317 328 318 return board.compareVectors(u_vec, v_vec); 319 + }, 320 + 321 + _getOrderVector: function(phid, order) { 322 + if (!this._orderVectors) { 323 + this._orderVectors = {}; 324 + } 325 + 326 + if (!this._orderVectors[order]) { 327 + var board = this.getBoard(); 328 + var cards = this.getCards(); 329 + var vectors = {}; 330 + 331 + for (var k in cards) { 332 + var card_phid = cards[k].getPHID(); 333 + var vector = board.getOrderVector(card_phid, order); 334 + vectors[card_phid] = [].concat(vector); 335 + 336 + // Push a "card" type, so cards always sort after headers; headers 337 + // have a "0" in this position. 338 + vectors[card_phid].push(1); 339 + } 340 + 341 + for (var ii = 0; ii < this._naturalOrder.length; ii++) { 342 + var natural_phid = this._naturalOrder[ii]; 343 + if (vectors[natural_phid]) { 344 + vectors[natural_phid].push(ii); 345 + } 346 + } 347 + 348 + this._orderVectors[order] = vectors; 349 + } 350 + 351 + if (!this._orderVectors[order][phid]) { 352 + // In this case, we're comparing a card being dragged in from another 353 + // column to the cards already in this column. We're just going to 354 + // build a temporary vector for it. 355 + var incoming_vector = this.getBoard().getOrderVector(phid, order); 356 + incoming_vector = [].concat(incoming_vector); 357 + 358 + // Add a "card" type to sort this after headers. 359 + incoming_vector.push(1); 360 + 361 + // Add a "0" for the natural ordering to put this on top. A new card 362 + // has no natural ordering on a column it isn't part of yet. 363 + incoming_vector.push(0); 364 + 365 + return incoming_vector; 366 + } 367 + 368 + return this._orderVectors[order][phid]; 329 369 }, 330 370 331 371 _redrawFrame: function() {