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

Make drag-and-drop on workboards interact with priority column headers

Summary:
Ref T10333. Ref T8135. Depends on D20247. Allow users to drag-and-drop cards on a priority-sorted workboard under headers, even if the header has no other cards.

As of D20247, headers show up but they aren't really interactive. Now, you can drag cards directly underneath a header (instead of only between other cards). For example, if a column has only one "Wishlist" task, you may drag it under the "High", "Normal", or "Low" priority headers to select a specific priority.

(Some of this code still feels a little rough, but I think it will generalize once other types of sorting are available.)

Test Plan: Dragged cards within and between priority groups, saw appropriate priority edits applied in every case I could come up with.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T10333, T8135

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

+208 -79
+40 -40
resources/celerity/map.php
··· 409 409 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 410 410 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 411 411 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', 412 - 'rsrc/js/application/projects/WorkboardBoard.js' => 'e4e2d107', 413 - 'rsrc/js/application/projects/WorkboardCard.js' => 'c23ddfde', 414 - 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd9cb972', 412 + 'rsrc/js/application/projects/WorkboardBoard.js' => 'a4f1e85d', 413 + 'rsrc/js/application/projects/WorkboardCard.js' => '887ef74f', 414 + 'rsrc/js/application/projects/WorkboardColumn.js' => 'ca444dca', 415 415 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 416 - 'rsrc/js/application/projects/WorkboardHeader.js' => '354c5c0e', 417 - 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '9b86cd0d', 418 - 'rsrc/js/application/projects/behavior-project-boards.js' => 'a3f6b67f', 416 + 'rsrc/js/application/projects/WorkboardHeader.js' => '6e75daea', 417 + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '2d641f7d', 418 + 'rsrc/js/application/projects/behavior-project-boards.js' => 'e2730b90', 419 419 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 420 420 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 421 421 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', ··· 657 657 'javelin-behavior-phuix-example' => 'c2c500a7', 658 658 'javelin-behavior-policy-control' => '0eaa33a9', 659 659 'javelin-behavior-policy-rule-editor' => '9347f172', 660 - 'javelin-behavior-project-boards' => 'a3f6b67f', 660 + 'javelin-behavior-project-boards' => 'e2730b90', 661 661 'javelin-behavior-project-create' => '34c53422', 662 662 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 663 663 'javelin-behavior-read-only-warning' => 'b9109f8f', ··· 729 729 'javelin-view-renderer' => '9aae2b66', 730 730 'javelin-view-visitor' => '308f9fe4', 731 731 'javelin-websocket' => 'fdc13e4e', 732 - 'javelin-workboard-board' => 'e4e2d107', 733 - 'javelin-workboard-card' => 'c23ddfde', 734 - 'javelin-workboard-column' => 'fd9cb972', 732 + 'javelin-workboard-board' => 'a4f1e85d', 733 + 'javelin-workboard-card' => '887ef74f', 734 + 'javelin-workboard-column' => 'ca444dca', 735 735 'javelin-workboard-controller' => '42c7a5a7', 736 - 'javelin-workboard-header' => '354c5c0e', 737 - 'javelin-workboard-header-template' => '9b86cd0d', 736 + 'javelin-workboard-header' => '6e75daea', 737 + 'javelin-workboard-header-template' => '2d641f7d', 738 738 'javelin-workflow' => '958e9045', 739 739 'maniphest-report-css' => '3d53188b', 740 740 'maniphest-task-edit-css' => '272daa84', ··· 1125 1125 'javelin-dom', 1126 1126 'phabricator-keyboard-shortcut', 1127 1127 ), 1128 + '2d641f7d' => array( 1129 + 'javelin-install', 1130 + ), 1128 1131 '2e255291' => array( 1129 1132 'javelin-install', 1130 1133 'javelin-util', ··· 1162 1165 'javelin-dom', 1163 1166 'javelin-stratcom', 1164 1167 'javelin-workflow', 1165 - ), 1166 - '354c5c0e' => array( 1167 - 'javelin-install', 1168 1168 ), 1169 1169 '37b8a04a' => array( 1170 1170 'javelin-install', ··· 1458 1458 'javelin-install', 1459 1459 'javelin-util', 1460 1460 ), 1461 + '6e75daea' => array( 1462 + 'javelin-install', 1463 + ), 1461 1464 70245195 => array( 1462 1465 'javelin-behavior', 1463 1466 'javelin-stratcom', ··· 1566 1569 'javelin-install', 1567 1570 'javelin-dom', 1568 1571 ), 1572 + '887ef74f' => array( 1573 + 'javelin-install', 1574 + ), 1569 1575 '89a1ae3a' => array( 1570 1576 'javelin-dom', 1571 1577 'javelin-util', ··· 1701 1707 'javelin-dom', 1702 1708 'javelin-stratcom', 1703 1709 ), 1704 - '9b86cd0d' => array( 1705 - 'javelin-install', 1706 - ), 1707 1710 '9cec214e' => array( 1708 1711 'javelin-behavior', 1709 1712 'javelin-stratcom', ··· 1728 1731 'a241536a' => array( 1729 1732 'javelin-install', 1730 1733 ), 1731 - 'a3f6b67f' => array( 1732 - 'javelin-behavior', 1733 - 'javelin-dom', 1734 - 'javelin-util', 1735 - 'javelin-vector', 1736 - 'javelin-stratcom', 1737 - 'javelin-workflow', 1738 - 'javelin-workboard-controller', 1739 - ), 1740 1734 'a4356cde' => array( 1741 1735 'javelin-install', 1742 1736 'javelin-dom', ··· 1761 1755 'javelin-dom', 1762 1756 'javelin-request', 1763 1757 'javelin-util', 1758 + ), 1759 + 'a4f1e85d' => array( 1760 + 'javelin-install', 1761 + 'javelin-dom', 1762 + 'javelin-util', 1763 + 'javelin-stratcom', 1764 + 'javelin-workflow', 1765 + 'phabricator-draggable-list', 1766 + 'javelin-workboard-column', 1767 + 'javelin-workboard-header-template', 1764 1768 ), 1765 1769 'a5257c4e' => array( 1766 1770 'javelin-install', ··· 1906 1910 'javelin-stratcom', 1907 1911 'javelin-uri', 1908 1912 ), 1909 - 'c23ddfde' => array( 1910 - 'javelin-install', 1911 - ), 1912 1913 'c2c500a7' => array( 1913 1914 'javelin-install', 1914 1915 'javelin-dom', ··· 1958 1959 'javelin-install', 1959 1960 'javelin-util', 1960 1961 'phabricator-keyboard-shortcut-manager', 1962 + ), 1963 + 'ca444dca' => array( 1964 + 'javelin-install', 1965 + 'javelin-workboard-card', 1966 + 'javelin-workboard-header', 1961 1967 ), 1962 1968 'cf32921f' => array( 1963 1969 'javelin-behavior', ··· 2025 2031 'javelin-dom', 2026 2032 'javelin-history', 2027 2033 ), 2028 - 'e4e2d107' => array( 2029 - 'javelin-install', 2034 + 'e2730b90' => array( 2035 + 'javelin-behavior', 2030 2036 'javelin-dom', 2031 2037 'javelin-util', 2038 + 'javelin-vector', 2032 2039 'javelin-stratcom', 2033 2040 'javelin-workflow', 2034 - 'phabricator-draggable-list', 2035 - 'javelin-workboard-column', 2036 - 'javelin-workboard-header-template', 2041 + 'javelin-workboard-controller', 2037 2042 ), 2038 2043 'e562708c' => array( 2039 2044 'javelin-install', ··· 2135 2140 'fce5d170' => array( 2136 2141 'javelin-magical-init', 2137 2142 'javelin-util', 2138 - ), 2139 - 'fd9cb972' => array( 2140 - 'javelin-install', 2141 - 'javelin-workboard-card', 2142 - 'javelin-workboard-header', 2143 2143 ), 2144 2144 'fdc13e4e' => array( 2145 2145 'javelin-install',
+1
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, 255 256 (double)-$this->getSubpriority(), 256 257 (int)-$this->getID(), 257 258 ),
+5 -1
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 651 651 )); 652 652 653 653 $headers[] = array( 654 - 'order' => 'priority', 654 + 'order' => PhabricatorProjectColumn::ORDER_PRIORITY, 655 655 'key' => $header_key, 656 656 'template' => hsprintf('%s', $template), 657 657 'vector' => array( 658 658 (int)-$priority, 659 + PhabricatorProjectColumn::NODETYPE_HEADER, 660 + ), 661 + 'editProperties' => array( 662 + PhabricatorProjectColumn::ORDER_PRIORITY => (int)$priority, 659 663 ), 660 664 ); 661 665 }
+61 -16
src/applications/project/controller/PhabricatorProjectMoveController.php
··· 15 15 $before_phid = $request->getStr('beforePHID'); 16 16 $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); 17 17 18 + $edit_header = null; 19 + $raw_header = $request->getStr('header'); 20 + if (strlen($raw_header)) { 21 + $edit_header = phutil_json_decode($raw_header); 22 + } else { 23 + $edit_header = array(); 24 + } 25 + 18 26 $project = id(new PhabricatorProjectQuery()) 19 27 ->setViewer($viewer) 20 28 ->requireCapabilities( ··· 87 95 )); 88 96 89 97 if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) { 98 + $header_priority = idx( 99 + $edit_header, 100 + PhabricatorProjectColumn::ORDER_PRIORITY); 90 101 $priority_xactions = $this->getPriorityTransactions( 91 102 $object, 92 103 $after_phid, 93 - $before_phid); 104 + $before_phid, 105 + $header_priority); 94 106 foreach ($priority_xactions as $xaction) { 95 107 $xactions[] = $xaction; 96 108 } ··· 110 122 private function getPriorityTransactions( 111 123 ManiphestTask $task, 112 124 $after_phid, 113 - $before_phid) { 125 + $before_phid, 126 + $header_priority) { 127 + 128 + $xactions = array(); 129 + $must_move = false; 130 + 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 + } 114 147 115 148 list($after_task, $before_task) = $this->loadPriorityTasks( 116 149 $after_phid, 117 150 $before_phid); 118 151 119 - $must_move = false; 120 152 if ($after_task && !$task->isLowerPriorityThan($after_task)) { 121 153 $must_move = true; 122 154 } ··· 125 157 $must_move = true; 126 158 } 127 159 128 - // The move doesn't require a priority change to be valid, so don't 129 - // change the priority since we are not being forced to. 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. 130 162 if (!$must_move) { 131 - return array(); 163 + return $xactions; 132 164 } 133 165 134 166 $try = array( ··· 139 171 $pri = null; 140 172 $sub = null; 141 173 foreach ($try as $spec) { 142 - list($task, $is_after) = $spec; 174 + list($nearby_task, $is_after) = $spec; 143 175 144 - if (!$task) { 176 + if (!$nearby_task) { 145 177 continue; 146 178 } 147 179 148 180 list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( 149 - $task, 181 + $nearby_task, 150 182 $is_after); 151 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; 191 + } 192 + } 193 + 152 194 // If we find a priority on the first try, don't keep going. 153 195 break; 154 196 } 155 197 156 - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); 157 - $keyword = head(idx($keyword_map, $pri)); 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 + } 158 208 159 - $xactions = array(); 160 - if ($pri !== null) { 161 - $xactions[] = id(new ManiphestTransaction()) 162 - ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) 163 - ->setNewValue($keyword); 164 209 $xactions[] = id(new ManiphestTransaction()) 165 210 ->setTransactionType( 166 211 ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE)
+3
src/applications/project/storage/PhabricatorProjectColumn.php
··· 16 16 const ORDER_NATURAL = 'natural'; 17 17 const ORDER_PRIORITY = 'priority'; 18 18 19 + const NODETYPE_HEADER = 0; 20 + const NODETYPE_CARD = 1; 21 + 19 22 protected $name; 20 23 protected $status; 21 24 protected $projectPHID;
+35 -8
webroot/rsrc/js/application/projects/WorkboardBoard.js
··· 161 161 162 162 var list = new JX.DraggableList('project-card', column.getRoot()) 163 163 .setOuterContainer(this.getRoot()) 164 - .setFindItemsHandler(JX.bind(column, column.getCardNodes)) 164 + .setFindItemsHandler(JX.bind(column, column.getDropTargetNodes)) 165 165 .setCanDragX(true) 166 166 .setHasInfiniteHeight(true) 167 167 .setIsDropTargetHandler(JX.bind(column, column.setIsDropTarget)); 168 + 169 + var default_handler = list.getGhostHandler(); 170 + list.setGhostHandler( 171 + JX.bind(column, column.handleDragGhost, default_handler)); 168 172 169 173 if (this.getOrder() !== 'natural') { 170 174 list.setCompareHandler(JX.bind(column, column.compareHandler)); ··· 198 202 order: this.getOrder() 199 203 }; 200 204 201 - if (after_node) { 202 - data.afterPHID = JX.Stratcom.getData(after_node).objectPHID; 205 + var after_data; 206 + var after_card = after_node; 207 + while (after_card) { 208 + after_data = JX.Stratcom.getData(after_card); 209 + if (after_data.objectPHID) { 210 + break; 211 + } 212 + after_card = after_card.previousSibling; 203 213 } 204 214 205 - var before_node = item.nextSibling; 206 - if (before_node) { 207 - var before_phid = JX.Stratcom.getData(before_node).objectPHID; 208 - if (before_phid) { 209 - data.beforePHID = before_phid; 215 + if (after_data) { 216 + data.afterPHID = after_data.objectPHID; 217 + } 218 + 219 + var before_data; 220 + var before_card = item.nextSibling; 221 + while (before_card) { 222 + before_data = JX.Stratcom.getData(before_card); 223 + if (before_data.objectPHID) { 224 + break; 210 225 } 226 + before_card = before_card.nextSibling; 227 + } 228 + 229 + if (before_data) { 230 + data.beforePHID = before_data.objectPHID; 231 + } 232 + 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); 211 238 } 212 239 213 240 var visible_phids = [];
+4
webroot/rsrc/js/application/projects/WorkboardCard.js
··· 55 55 return this._root; 56 56 }, 57 57 58 + isWorkboardHeader: function() { 59 + return false; 60 + }, 61 + 58 62 redraw: function() { 59 63 var old_node = this._root; 60 64 this._root = null;
+49 -12
webroot/rsrc/js/application/projects/WorkboardColumn.js
··· 52 52 return this._cards; 53 53 }, 54 54 55 + _getObjects: function() { 56 + return this._objects; 57 + }, 58 + 55 59 getCard: function(phid) { 56 60 return this._cards[phid]; 57 61 }, ··· 126 130 return this; 127 131 }, 128 132 129 - getCardNodes: function() { 130 - var cards = this.getCards(); 133 + getDropTargetNodes: function() { 134 + var objects = this._getObjects(); 131 135 132 136 var nodes = []; 133 - for (var k in cards) { 134 - nodes.push(cards[k].getNode()); 137 + for (var ii = 0; ii < objects.length; ii++) { 138 + var object = objects[ii]; 139 + nodes.push(object.getNode()); 135 140 } 136 141 137 142 return nodes; ··· 160 165 return this._headers[key]; 161 166 }, 162 167 168 + handleDragGhost: function(default_handler, ghost, node) { 169 + // If the column has headers, don't let the user drag a card above 170 + // the topmost header: for example, you can't change a task to have 171 + // a priority higher than the highest possible priority. 172 + 173 + if (this._hasColumnHeaders()) { 174 + if (!node) { 175 + return false; 176 + } 177 + } 178 + 179 + return default_handler(ghost, node); 180 + }, 181 + 182 + _hasColumnHeaders: function() { 183 + var board = this.getBoard(); 184 + var order = board.getOrder(); 185 + 186 + switch (order) { 187 + case 'natural': 188 + return false; 189 + } 190 + 191 + return true; 192 + }, 193 + 163 194 _getCardHeaderKey: function(card, order) { 164 195 switch (order) { 165 196 case 'priority': ··· 174 205 var order = board.getOrder(); 175 206 176 207 var list; 177 - var has_headers; 178 208 if (order == 'natural') { 179 209 list = this._getCardsSortedNaturally(); 180 - has_headers = false; 181 210 } else { 182 211 list = this._getCardsSortedByKey(order); 183 - has_headers = true; 184 212 } 185 213 186 214 var ii; 187 215 var objects = []; 188 216 217 + var has_headers = this._hasColumnHeaders(); 189 218 var header_keys = []; 190 219 var seen_headers = {}; 191 220 if (has_headers) { ··· 245 274 var board = this.getBoard(); 246 275 var order = board.getOrder(); 247 276 248 - var src_phid = JX.Stratcom.getData(src_node).objectPHID; 249 - var dst_phid = JX.Stratcom.getData(dst_node).objectPHID; 277 + var u_vec = this._getNodeOrderVector(src_node, order); 278 + var v_vec = this._getNodeOrderVector(dst_node, order); 250 279 251 - var u_vec = board.getOrderVector(src_phid, order); 252 - var v_vec = board.getOrderVector(dst_phid, order); 280 + return board.compareVectors(u_vec, v_vec); 281 + }, 253 282 254 - return board.compareVectors(u_vec, v_vec); 283 + _getNodeOrderVector: function(node, order) { 284 + var board = this.getBoard(); 285 + var data = JX.Stratcom.getData(node); 286 + 287 + if (data.objectPHID) { 288 + return board.getOrderVector(data.objectPHID, order); 289 + } 290 + 291 + return board.getHeaderTemplate(data.headerKey).getVector(); 255 292 }, 256 293 257 294 setIsDropTarget: function(is_target) {
+6
webroot/rsrc/js/application/projects/WorkboardHeader.js
··· 30 30 var board = this.getColumn().getBoard(); 31 31 var template = board.getHeaderTemplate(header_key).getTemplate(); 32 32 this._root = JX.$H(template).getFragment().firstChild; 33 + 34 + JX.Stratcom.getData(this._root).headerKey = header_key; 33 35 } 34 36 return this._root; 37 + }, 38 + 39 + isWorkboardHeader: function() { 40 + return true; 35 41 } 36 42 } 37 43
+2 -1
webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js
··· 13 13 properties: { 14 14 template: null, 15 15 order: null, 16 - vector: null 16 + vector: null, 17 + editProperties: null 17 18 }, 18 19 19 20 members: {
+2 -1
webroot/rsrc/js/application/projects/behavior-project-boards.js
··· 112 112 board.getHeaderTemplate(header.key) 113 113 .setOrder(header.order) 114 114 .setTemplate(header.template) 115 - .setVector(header.vector); 115 + .setVector(header.vector) 116 + .setEditProperties(header.editProperties); 116 117 } 117 118 118 119 board.start();