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

Modularize workboard column orders

Summary:
Depends on D20267. Depends on D20268. Ref T10333. Currently, we support "Natural" and "Priority" orders, but a lot of the particulars are pretty hard-coded, including some logic in `ManiphestTask`.

Although it's not clear that we'll ever put other types of objects on workboards, it seems generally bad that you need to modify `ManiphestTask` to get a new ordering.

Pull the ordering logic out into a `ProjectColumnOrder` hierarchy instead, and let each ordering define the things it needs to work (name, icon, what headers look like, how different objects are sorted, and how to apply an edit when you drop an object under a header).

Then move the existing "Natural" and "Priority" orders into this new hierarchy.

This has a minor bug where using the "Edit" workflow to change a card's priority on a priority-ordered board doesn't fully refresh card/header order since the response isn't ordering-aware. I'll fix that in an upcoming change.

Test Plan: Grouped workboards by "Natural" and "Priority", dragged stuff around within and between columns, grepped for all touched symbols.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T10333

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

+637 -217
+51 -51
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' => '2181739b', 412 - 'rsrc/js/application/projects/WorkboardCard.js' => 'bc92741f', 413 - 'rsrc/js/application/projects/WorkboardCardTemplate.js' => 'b0b5f90a', 414 - 'rsrc/js/application/projects/WorkboardColumn.js' => '6461f58b', 411 + 'rsrc/js/application/projects/WorkboardBoard.js' => 'fc1664ff', 412 + 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 413 + 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 414 + 'rsrc/js/application/projects/WorkboardColumn.js' => '533dd592', 415 415 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 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' => 'cca3f5f8', 416 + 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 417 + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd', 418 + 'rsrc/js/application/projects/behavior-project-boards.js' => '285c337a', 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', ··· 656 656 'javelin-behavior-phuix-example' => 'c2c500a7', 657 657 'javelin-behavior-policy-control' => '0eaa33a9', 658 658 'javelin-behavior-policy-rule-editor' => '9347f172', 659 - 'javelin-behavior-project-boards' => 'cca3f5f8', 659 + 'javelin-behavior-project-boards' => '285c337a', 660 660 'javelin-behavior-project-create' => '34c53422', 661 661 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 662 662 'javelin-behavior-read-only-warning' => 'b9109f8f', ··· 728 728 'javelin-view-renderer' => '9aae2b66', 729 729 'javelin-view-visitor' => '308f9fe4', 730 730 'javelin-websocket' => 'fdc13e4e', 731 - 'javelin-workboard-board' => '2181739b', 732 - 'javelin-workboard-card' => 'bc92741f', 733 - 'javelin-workboard-card-template' => 'b0b5f90a', 734 - 'javelin-workboard-column' => '6461f58b', 731 + 'javelin-workboard-board' => 'fc1664ff', 732 + 'javelin-workboard-card' => '0392a5d8', 733 + 'javelin-workboard-card-template' => '2a61f8d4', 734 + 'javelin-workboard-column' => '533dd592', 735 735 'javelin-workboard-controller' => '42c7a5a7', 736 - 'javelin-workboard-header' => '6e75daea', 737 - 'javelin-workboard-header-template' => '2d641f7d', 736 + 'javelin-workboard-header' => '111bfd2d', 737 + 'javelin-workboard-header-template' => 'b65351bd', 738 738 'javelin-workflow' => '958e9045', 739 739 'maniphest-report-css' => '3d53188b', 740 740 'maniphest-task-edit-css' => '272daa84', ··· 909 909 'javelin-uri', 910 910 'javelin-util', 911 911 ), 912 + '0392a5d8' => array( 913 + 'javelin-install', 914 + ), 912 915 '04023d82' => array( 913 916 'javelin-install', 914 917 'phuix-button-view', ··· 993 996 'javelin-workflow', 994 997 'phuix-icon-view', 995 998 ), 999 + '111bfd2d' => array( 1000 + 'javelin-install', 1001 + ), 996 1002 '1325b731' => array( 997 1003 'javelin-behavior', 998 1004 'javelin-uri', ··· 1048 1054 'javelin-behavior', 1049 1055 'javelin-request', 1050 1056 ), 1051 - '2181739b' => array( 1052 - 'javelin-install', 1053 - 'javelin-dom', 1054 - 'javelin-util', 1055 - 'javelin-stratcom', 1056 - 'javelin-workflow', 1057 - 'phabricator-draggable-list', 1058 - 'javelin-workboard-column', 1059 - 'javelin-workboard-header-template', 1060 - 'javelin-workboard-card-template', 1061 - ), 1062 1057 '225bbb98' => array( 1063 1058 'javelin-install', 1064 1059 'javelin-reactor', ··· 1110 1105 'javelin-json', 1111 1106 'phabricator-prefab', 1112 1107 ), 1108 + '285c337a' => array( 1109 + 'javelin-behavior', 1110 + 'javelin-dom', 1111 + 'javelin-util', 1112 + 'javelin-vector', 1113 + 'javelin-stratcom', 1114 + 'javelin-workflow', 1115 + 'javelin-workboard-controller', 1116 + ), 1113 1117 '289bf236' => array( 1114 1118 'javelin-install', 1115 1119 'javelin-util', ··· 1118 1122 'phabricator-notification', 1119 1123 'javelin-stratcom', 1120 1124 'javelin-behavior', 1125 + ), 1126 + '2a61f8d4' => array( 1127 + 'javelin-install', 1121 1128 ), 1122 1129 '2a8b62d9' => array( 1123 1130 'multirow-row-manager', ··· 1141 1148 'javelin-json', 1142 1149 'javelin-dom', 1143 1150 'phabricator-keyboard-shortcut', 1144 - ), 1145 - '2d641f7d' => array( 1146 - 'javelin-install', 1147 1151 ), 1148 1152 '2e255291' => array( 1149 1153 'javelin-install', ··· 1343 1347 'javelin-dom', 1344 1348 'javelin-fx', 1345 1349 ), 1350 + '533dd592' => array( 1351 + 'javelin-install', 1352 + 'javelin-workboard-card', 1353 + 'javelin-workboard-header', 1354 + ), 1346 1355 '534f1757' => array( 1347 1356 'phui-oi-list-view-css', 1348 1357 ), ··· 1427 1436 '60cd9241' => array( 1428 1437 'javelin-behavior', 1429 1438 ), 1430 - '6461f58b' => array( 1431 - 'javelin-install', 1432 - 'javelin-workboard-card', 1433 - 'javelin-workboard-header', 1434 - ), 1435 1439 '65bb0011' => array( 1436 1440 'javelin-behavior', 1437 1441 'javelin-dom', ··· 1476 1480 'javelin-reactornode', 1477 1481 'javelin-install', 1478 1482 'javelin-util', 1479 - ), 1480 - '6e75daea' => array( 1481 - 'javelin-install', 1482 1483 ), 1483 1484 70245195 => array( 1484 1485 'javelin-behavior', ··· 1839 1840 'javelin-behavior-device', 1840 1841 'javelin-vector', 1841 1842 ), 1842 - 'b0b5f90a' => array( 1843 - 'javelin-install', 1844 - ), 1845 1843 'b105a3a6' => array( 1846 1844 'javelin-behavior', 1847 1845 'javelin-stratcom', ··· 1875 1873 'javelin-stratcom', 1876 1874 'javelin-dom', 1877 1875 ), 1876 + 'b65351bd' => array( 1877 + 'javelin-install', 1878 + ), 1878 1879 'b7b73831' => array( 1879 1880 'javelin-behavior', 1880 1881 'javelin-dom', ··· 1895 1896 ), 1896 1897 'bc16cf33' => array( 1897 1898 'phui-workcard-view-css', 1898 - ), 1899 - 'bc92741f' => array( 1900 - 'javelin-install', 1901 1899 ), 1902 1900 'bdce4d78' => array( 1903 1901 'javelin-install', ··· 1967 1965 'javelin-install', 1968 1966 'javelin-util', 1969 1967 'phabricator-keyboard-shortcut-manager', 1970 - ), 1971 - 'cca3f5f8' => array( 1972 - 'javelin-behavior', 1973 - 'javelin-dom', 1974 - 'javelin-util', 1975 - 'javelin-vector', 1976 - 'javelin-stratcom', 1977 - 'javelin-workflow', 1978 - 'javelin-workboard-controller', 1979 1968 ), 1980 1969 'cf32921f' => array( 1981 1970 'javelin-behavior', ··· 2133 2122 'javelin-quicksand', 2134 2123 'phabricator-keyboard-shortcut', 2135 2124 'conpherence-thread-manager', 2125 + ), 2126 + 'fc1664ff' => array( 2127 + 'javelin-install', 2128 + 'javelin-dom', 2129 + 'javelin-util', 2130 + 'javelin-stratcom', 2131 + 'javelin-workflow', 2132 + 'phabricator-draggable-list', 2133 + 'javelin-workboard-column', 2134 + 'javelin-workboard-header-template', 2135 + 'javelin-workboard-card-template', 2136 2136 ), 2137 2137 'fce5d170' => array( 2138 2138 'javelin-magical-init',
+8
src/__phutil_library_map__.php
··· 4052 4052 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 4053 4053 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 4054 4054 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', 4055 + 'PhabricatorProjectColumnHeader' => 'applications/project/order/PhabricatorProjectColumnHeader.php', 4055 4056 'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php', 4057 + 'PhabricatorProjectColumnNaturalOrder' => 'applications/project/order/PhabricatorProjectColumnNaturalOrder.php', 4058 + 'PhabricatorProjectColumnOrder' => 'applications/project/order/PhabricatorProjectColumnOrder.php', 4056 4059 'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php', 4057 4060 'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php', 4058 4061 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', 4062 + 'PhabricatorProjectColumnPriorityOrder' => 'applications/project/order/PhabricatorProjectColumnPriorityOrder.php', 4059 4063 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 4060 4064 'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php', 4061 4065 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', ··· 10132 10136 ), 10133 10137 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 10134 10138 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', 10139 + 'PhabricatorProjectColumnHeader' => 'Phobject', 10135 10140 'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController', 10141 + 'PhabricatorProjectColumnNaturalOrder' => 'PhabricatorProjectColumnOrder', 10142 + 'PhabricatorProjectColumnOrder' => 'Phobject', 10136 10143 'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType', 10137 10144 'PhabricatorProjectColumnPosition' => array( 10138 10145 'PhabricatorProjectDAO', 10139 10146 'PhabricatorPolicyInterface', 10140 10147 ), 10141 10148 'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 10149 + 'PhabricatorProjectColumnPriorityOrder' => 'PhabricatorProjectColumnOrder', 10142 10150 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 10143 10151 'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', 10144 10152 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction',
-16
src/applications/maniphest/storage/ManiphestTask.php
··· 248 248 return idx($this->properties, 'cover.thumbnailPHID'); 249 249 } 250 250 251 - public function getWorkboardOrderVectors() { 252 - return array( 253 - PhabricatorProjectColumn::ORDER_PRIORITY => array( 254 - (int)-$this->getPriority(), 255 - ), 256 - ); 257 - } 258 - 259 251 public function getPriorityKeyword() { 260 252 $priority = $this->getPriority(); 261 253 ··· 265 257 } 266 258 267 259 return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD; 268 - } 269 - 270 - public function getWorkboardProperties() { 271 - return array( 272 - 'status' => $this->getStatus(), 273 - 'points' => (double)$this->getPoints(), 274 - 'priority' => $this->getPriority(), 275 - ); 276 260 } 277 261 278 262
+38 -64
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 614 614 $board->addPanel($panel); 615 615 } 616 616 617 - // It's possible for tasks to have an invalid/unknown priority in the 618 - // database. We still want to generate a header for these tasks so we 619 - // don't break the workboard. 620 - $priorities = 621 - ManiphestTaskPriority::getTaskPriorityMap() + 622 - mpull($all_tasks, null, 'getPriority'); 623 - $priorities = array_keys($priorities); 617 + $order_key = $this->sortKey; 624 618 625 - $headers = array(); 626 - foreach ($priorities as $priority) { 627 - $header_key = sprintf('priority(%s)', $priority); 619 + $ordering_map = PhabricatorProjectColumnOrder::getAllOrders(); 620 + $ordering = id(clone $ordering_map[$order_key]) 621 + ->setViewer($viewer); 628 622 629 - $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); 630 - $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); 631 - $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); 623 + $headers = $ordering->getHeadersForObjects($all_tasks); 624 + $headers = mpull($headers, 'toDictionary'); 632 625 633 - $icon_view = id(new PHUIIconView()) 634 - ->setIcon("{$priority_icon} {$priority_color}"); 626 + $vectors = $ordering->getSortVectorsForObjects($all_tasks); 627 + $vector_map = array(); 628 + foreach ($vectors as $task_phid => $vector) { 629 + $vector_map[$task_phid][$order_key] = $vector; 630 + } 635 631 636 - $template = phutil_tag( 637 - 'li', 638 - array( 639 - 'class' => 'workboard-group-header', 640 - ), 641 - array( 642 - $icon_view, 643 - $priority_name, 644 - )); 632 + $header_keys = $ordering->getHeaderKeysForObjects($all_tasks); 645 633 646 - $headers[] = array( 647 - 'order' => PhabricatorProjectColumn::ORDER_PRIORITY, 648 - 'key' => $header_key, 649 - 'template' => hsprintf('%s', $template), 650 - 'vector' => array( 651 - (int)-$priority, 652 - PhabricatorProjectColumn::NODETYPE_HEADER, 653 - ), 654 - 'editProperties' => array( 655 - PhabricatorProjectColumn::ORDER_PRIORITY => (int)$priority, 656 - ), 657 - ); 658 - } 634 + $properties = array(); 659 635 660 636 $behavior_config = array( 661 637 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), ··· 667 643 'boardPHID' => $project->getPHID(), 668 644 'order' => $this->sortKey, 669 645 'headers' => $headers, 646 + 'headerKeys' => $header_keys, 670 647 'templateMap' => $templates, 671 648 'columnMaps' => $column_maps, 672 - 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), 673 - 'propertyMaps' => mpull($all_tasks, 'getWorkboardProperties'), 649 + 'orderMaps' => $vector_map, 650 + 'propertyMaps' => $properties, 674 651 675 652 'boardID' => $board_id, 676 653 'projectPHID' => $project->getPHID(), ··· 680 657 $sort_menu = $this->buildSortMenu( 681 658 $viewer, 682 659 $project, 683 - $this->sortKey); 660 + $this->sortKey, 661 + $ordering_map); 684 662 685 663 $filter_menu = $this->buildFilterMenu( 686 664 $viewer, ··· 775 753 return $default_sort; 776 754 } 777 755 778 - return PhabricatorProjectColumn::DEFAULT_ORDER; 756 + return PhabricatorProjectColumnNaturalOrder::ORDERKEY; 779 757 } 780 758 781 759 private function getDefaultFilter(PhabricatorProject $project) { ··· 789 767 } 790 768 791 769 private function isValidSort($sort) { 792 - switch ($sort) { 793 - case PhabricatorProjectColumn::ORDER_NATURAL: 794 - case PhabricatorProjectColumn::ORDER_PRIORITY: 795 - return true; 796 - } 797 - 798 - return false; 770 + $map = PhabricatorProjectColumnOrder::getAllOrders(); 771 + return isset($map[$sort]); 799 772 } 800 773 801 774 private function buildSortMenu( 802 775 PhabricatorUser $viewer, 803 776 PhabricatorProject $project, 804 - $sort_key) { 805 - 806 - $sort_icon = id(new PHUIIconView()) 807 - ->setIcon('fa-sort-amount-asc bluegrey'); 808 - 809 - $named = array( 810 - PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), 811 - PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'), 812 - ); 777 + $sort_key, 778 + array $ordering_map) { 813 779 814 780 $base_uri = $this->getURIWithState(); 815 781 816 782 $items = array(); 817 - foreach ($named as $key => $name) { 818 - $is_selected = ($key == $sort_key); 783 + foreach ($ordering_map as $key => $ordering) { 784 + // TODO: It would be desirable to build a real "PHUIIconView" here, but 785 + // the pathway for threading that through all the view classes ends up 786 + // being fairly complex, since some callers read the icon out of other 787 + // views. For now, just stick with a string. 788 + $ordering_icon = $ordering->getMenuIconIcon(); 789 + $ordering_name = $ordering->getDisplayName(); 790 + 791 + $is_selected = ($key === $sort_key); 819 792 if ($is_selected) { 820 - $active_order = $name; 793 + $active_name = $ordering_name; 794 + $active_icon = $ordering_icon; 821 795 } 822 796 823 797 $item = id(new PhabricatorActionView()) 824 - ->setIcon('fa-sort-amount-asc') 798 + ->setIcon($ordering_icon) 825 799 ->setSelected($is_selected) 826 - ->setName($name); 800 + ->setName($ordering_name); 827 801 828 802 $uri = $base_uri->alter('order', $key); 829 803 $item->setHref($uri); ··· 856 830 } 857 831 858 832 $sort_button = id(new PHUIListItemView()) 859 - ->setName($active_order) 860 - ->setIcon('fa-sort-amount-asc') 833 + ->setName($active_name) 834 + ->setIcon($active_icon) 861 835 ->setHref('#') 862 836 ->addSigil('boards-dropdown-menu') 863 837 ->setMetadata(
+13 -4
src/applications/project/controller/PhabricatorProjectController.php
··· 149 149 return $this; 150 150 } 151 151 152 - protected function newCardResponse($board_phid, $object_phid) { 152 + protected function newCardResponse( 153 + $board_phid, 154 + $object_phid, 155 + PhabricatorProjectColumnOrder $ordering = null) { 156 + 153 157 $viewer = $this->getViewer(); 154 158 155 159 $request = $this->getRequest(); ··· 158 162 $visible_phids = array(); 159 163 } 160 164 161 - return id(new PhabricatorBoardResponseEngine()) 165 + $engine = id(new PhabricatorBoardResponseEngine()) 162 166 ->setViewer($viewer) 163 167 ->setBoardPHID($board_phid) 164 168 ->setObjectPHID($object_phid) 165 - ->setVisiblePHIDs($visible_phids) 166 - ->buildResponse(); 169 + ->setVisiblePHIDs($visible_phids); 170 + 171 + if ($ordering) { 172 + $engine->setOrdering($ordering); 173 + } 174 + 175 + return $engine->buildResponse(); 167 176 } 168 177 169 178 public function renderHashtags(array $tags) {
+11 -30
src/applications/project/controller/PhabricatorProjectMoveController.php
··· 13 13 $object_phid = $request->getStr('objectPHID'); 14 14 $after_phid = $request->getStr('afterPHID'); 15 15 $before_phid = $request->getStr('beforePHID'); 16 - $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); 16 + 17 + $order = $request->getStr('order'); 18 + if (!strlen($order)) { 19 + $order = PhabricatorProjectColumnNaturalOrder::ORDERKEY; 20 + } 21 + 22 + $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order); 23 + $ordering = id(clone $ordering) 24 + ->setViewer($viewer); 17 25 18 26 $edit_header = null; 19 27 $raw_header = $request->getStr('header'); ··· 88 96 ) + $order_params, 89 97 )); 90 98 91 - $header_xactions = $this->newHeaderTransactions( 99 + $header_xactions = $ordering->getColumnTransactions( 92 100 $object, 93 - $order, 94 101 $edit_header); 95 102 foreach ($header_xactions as $header_xaction) { 96 103 $xactions[] = $header_xaction; ··· 104 111 105 112 $editor->applyTransactions($object, $xactions); 106 113 107 - return $this->newCardResponse($board_phid, $object_phid); 108 - } 109 - 110 - private function newHeaderTransactions( 111 - ManiphestTask $task, 112 - $order, 113 - array $header) { 114 - 115 - $xactions = array(); 116 - 117 - switch ($order) { 118 - case PhabricatorProjectColumn::ORDER_PRIORITY: 119 - $new_priority = idx($header, $order); 120 - 121 - if ($task->getPriority() !== $new_priority) { 122 - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); 123 - $keyword = head(idx($keyword_map, $new_priority)); 124 - 125 - $xactions[] = id(new ManiphestTransaction()) 126 - ->setTransactionType( 127 - ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) 128 - ->setNewValue($keyword); 129 - } 130 - break; 131 - } 132 - 133 - return $xactions; 114 + return $this->newCardResponse($board_phid, $object_phid, $ordering); 134 115 } 135 116 136 117 }
+62 -8
src/applications/project/engine/PhabricatorBoardResponseEngine.php
··· 6 6 private $boardPHID; 7 7 private $objectPHID; 8 8 private $visiblePHIDs; 9 + private $ordering; 9 10 10 11 public function setViewer(PhabricatorUser $viewer) { 11 12 $this->viewer = $viewer; ··· 43 44 return $this->visiblePHIDs; 44 45 } 45 46 47 + public function setOrdering(PhabricatorProjectColumnOrder $ordering) { 48 + $this->ordering = $ordering; 49 + return $this; 50 + } 51 + 52 + public function getOrdering() { 53 + return $this->ordering; 54 + } 55 + 46 56 public function buildResponse() { 47 57 $viewer = $this->getViewer(); 48 58 $object_phid = $this->getObjectPHID(); 49 59 $board_phid = $this->getBoardPHID(); 60 + $ordering = $this->getOrdering(); 50 61 51 62 // Load all the other tasks that are visible in the affected columns and 52 63 // perform layout for them. ··· 74 85 ->setViewer($viewer) 75 86 ->withPHIDs($visible_phids) 76 87 ->execute(); 88 + $all_visible = mpull($all_visible, null, 'getPHID'); 77 89 78 - $order_maps = array(); 79 - foreach ($all_visible as $visible) { 80 - $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors(); 90 + if ($ordering) { 91 + $vectors = $ordering->getSortVectorsForObjects($all_visible); 92 + $header_keys = $ordering->getHeaderKeysForObjects($all_visible); 93 + $headers = $ordering->getHeadersForObjects($all_visible); 94 + $headers = mpull($headers, 'toDictionary'); 95 + } else { 96 + $vectors = array(); 97 + $header_keys = array(); 98 + $headers = array(); 81 99 } 82 100 83 101 $object = id(new ManiphestTaskQuery()) ··· 91 109 92 110 $template = $this->buildTemplate($object); 93 111 112 + $cards = array(); 113 + foreach ($all_visible as $card_phid => $object) { 114 + $card = array( 115 + 'vectors' => array(), 116 + 'headers' => array(), 117 + 'properties' => array(), 118 + 'nodeHTMLTemplate' => null, 119 + ); 120 + 121 + if ($ordering) { 122 + $order_key = $ordering->getColumnOrderKey(); 123 + 124 + $vector = idx($vectors, $card_phid); 125 + if ($vector !== null) { 126 + $card['vectors'][$order_key] = $vector; 127 + } 128 + 129 + $header = idx($header_keys, $card_phid); 130 + if ($header !== null) { 131 + $card['headers'][$order_key] = $header; 132 + } 133 + 134 + $card['properties'] = array( 135 + 'points' => (double)$object->getPoints(), 136 + 'status' => $object->getStatus(), 137 + ); 138 + } 139 + 140 + if ($card_phid === $object_phid) { 141 + $card['nodeHTMLTemplate'] = hsprintf('%s', $template); 142 + } 143 + 144 + $card['vectors'] = (object)$card['vectors']; 145 + $card['headers'] = (object)$card['headers']; 146 + $card['properties'] = (object)$card['properties']; 147 + 148 + $cards[$card_phid] = $card; 149 + } 150 + 94 151 $payload = array( 95 152 'objectPHID' => $object_phid, 96 - 'cardHTML' => $template, 97 153 'columnMaps' => $natural, 98 - 'orderMaps' => $order_maps, 99 - 'propertyMaps' => array( 100 - $object_phid => $object->getWorkboardProperties(), 101 - ), 154 + 'cards' => $cards, 155 + 'headers' => $headers, 102 156 ); 103 157 104 158 return id(new AphrontAjaxResponse())
+94
src/applications/project/order/PhabricatorProjectColumnHeader.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectColumnHeader 4 + extends Phobject { 5 + 6 + private $orderKey; 7 + private $headerKey; 8 + private $sortVector; 9 + private $name; 10 + private $icon; 11 + private $editProperties; 12 + 13 + public function setOrderKey($order_key) { 14 + $this->orderKey = $order_key; 15 + return $this; 16 + } 17 + 18 + public function getOrderKey() { 19 + return $this->orderKey; 20 + } 21 + 22 + public function setHeaderKey($header_key) { 23 + $this->headerKey = $header_key; 24 + return $this; 25 + } 26 + 27 + public function getHeaderKey() { 28 + return $this->headerKey; 29 + } 30 + 31 + public function setSortVector(array $sort_vector) { 32 + $this->sortVector = $sort_vector; 33 + return $this; 34 + } 35 + 36 + public function getSortVector() { 37 + return $this->sortVector; 38 + } 39 + 40 + public function setName($name) { 41 + $this->name = $name; 42 + return $this; 43 + } 44 + 45 + public function getName() { 46 + return $this->name; 47 + } 48 + 49 + public function setIcon(PHUIIconView$icon) { 50 + $this->icon = $icon; 51 + return $this; 52 + } 53 + 54 + public function getIcon() { 55 + return $this->icon; 56 + } 57 + 58 + public function setEditProperties(array $edit_properties) { 59 + $this->editProperties = $edit_properties; 60 + return $this; 61 + } 62 + 63 + public function getEditProperties() { 64 + return $this->editProperties; 65 + } 66 + 67 + public function toDictionary() { 68 + return array( 69 + 'order' => $this->getOrderKey(), 70 + 'key' => $this->getHeaderKey(), 71 + 'template' => hsprintf('%s', $this->newView()), 72 + 'vector' => $this->getSortVector(), 73 + 'editProperties' => $this->getEditProperties(), 74 + ); 75 + } 76 + 77 + private function newView() { 78 + $icon_view = $this->getIcon(); 79 + $name = $this->getName(); 80 + 81 + $template = phutil_tag( 82 + 'li', 83 + array( 84 + 'class' => 'workboard-group-header', 85 + ), 86 + array( 87 + $icon_view, 88 + $name, 89 + )); 90 + 91 + return $template; 92 + } 93 + 94 + }
+12
src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectColumnNaturalOrder 4 + extends PhabricatorProjectColumnOrder { 5 + 6 + const ORDERKEY = 'natural'; 7 + 8 + public function getDisplayName() { 9 + return pht('Natural'); 10 + } 11 + 12 + }
+176
src/applications/project/order/PhabricatorProjectColumnOrder.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorProjectColumnOrder 4 + extends Phobject { 5 + 6 + private $viewer; 7 + 8 + final public function setViewer(PhabricatorUser $viewer) { 9 + $this->viewer = $viewer; 10 + return $this; 11 + } 12 + 13 + final public function getViewer() { 14 + return $this->viewer; 15 + } 16 + 17 + final public function getColumnOrderKey() { 18 + return $this->getPhobjectClassConstant('ORDERKEY'); 19 + } 20 + 21 + final public static function getAllOrders() { 22 + return id(new PhutilClassMapQuery()) 23 + ->setAncestorClass(__CLASS__) 24 + ->setUniqueMethod('getColumnOrderKey') 25 + ->execute(); 26 + } 27 + 28 + final public static function getOrderByKey($key) { 29 + $map = self::getAllOrders(); 30 + 31 + if (!isset($map[$key])) { 32 + throw new Exception( 33 + pht( 34 + 'No column ordering exists with key "%s".', 35 + $key)); 36 + } 37 + 38 + return $map[$key]; 39 + } 40 + 41 + final public function getColumnTransactions($object, array $header) { 42 + $result = $this->newColumnTransactions($object, $header); 43 + 44 + if (!is_array($result) && !is_null($result)) { 45 + throw new Exception( 46 + pht( 47 + 'Expected "newColumnTransactions()" on "%s" to return "null" or a '. 48 + 'list of transactions, but got "%s".', 49 + get_class($this), 50 + phutil_describe_type($result))); 51 + } 52 + 53 + if ($result === null) { 54 + $result = array(); 55 + } 56 + 57 + assert_instances_of($result, 'PhabricatorApplicationTransaction'); 58 + 59 + return $result; 60 + } 61 + 62 + final public function getMenuIconIcon() { 63 + return $this->newMenuIconIcon(); 64 + } 65 + 66 + protected function newMenuIconIcon() { 67 + return 'fa-sort-amount-asc'; 68 + } 69 + 70 + abstract public function getDisplayName(); 71 + 72 + protected function newColumnTransactions($object, array $header) { 73 + return array(); 74 + } 75 + 76 + final public function getHeadersForObjects(array $objects) { 77 + $headers = $this->newHeadersForObjects($objects); 78 + 79 + if (!is_array($headers)) { 80 + throw new Exception( 81 + pht( 82 + 'Expected "newHeadersForObjects()" on "%s" to return a list '. 83 + 'of headers, but got "%s".', 84 + get_class($this), 85 + phutil_describe_type($headers))); 86 + } 87 + 88 + assert_instances_of($headers, 'PhabricatorProjectColumnHeader'); 89 + 90 + // Add a "0" to the end of each header. This makes them sort above object 91 + // cards in the same group. 92 + foreach ($headers as $header) { 93 + $vector = $header->getSortVector(); 94 + $vector[] = 0; 95 + $header->setSortVector($vector); 96 + } 97 + 98 + return $headers; 99 + } 100 + 101 + protected function newHeadersForObjects(array $objects) { 102 + return array(); 103 + } 104 + 105 + final public function getSortVectorsForObjects(array $objects) { 106 + $vectors = $this->newSortVectorsForObjects($objects); 107 + 108 + if (!is_array($vectors)) { 109 + throw new Exception( 110 + pht( 111 + 'Expected "newSortVectorsForObjects()" on "%s" to return a '. 112 + 'map of vectors, but got "%s".', 113 + get_class($this), 114 + phutil_describe_type($vectors))); 115 + } 116 + 117 + assert_same_keys($objects, $vectors); 118 + 119 + return $vectors; 120 + } 121 + 122 + protected function newSortVectorsForObjects(array $objects) { 123 + $vectors = array(); 124 + 125 + foreach ($objects as $key => $object) { 126 + $vectors[$key] = $this->newSortVectorForObject($object); 127 + } 128 + 129 + return $vectors; 130 + } 131 + 132 + protected function newSortVectorForObject($object) { 133 + return array(); 134 + } 135 + 136 + final public function getHeaderKeysForObjects(array $objects) { 137 + $header_keys = $this->newHeaderKeysForObjects($objects); 138 + 139 + if (!is_array($header_keys)) { 140 + throw new Exception( 141 + pht( 142 + 'Expected "newHeaderKeysForObject()" on "%s" to return a '. 143 + 'map of header keys, but got "%s".', 144 + get_class($this), 145 + phutil_describe_type($header_keys))); 146 + } 147 + 148 + assert_same_keys($objects, $header_keys); 149 + 150 + return $header_keys; 151 + } 152 + 153 + protected function newHeaderKeysForObjects(array $objects) { 154 + $header_keys = array(); 155 + 156 + foreach ($objects as $key => $object) { 157 + $header_keys[$key] = $this->newHeaderKeyForObject($object); 158 + } 159 + 160 + return $header_keys; 161 + } 162 + 163 + protected function newHeaderKeyForObject($object) { 164 + return null; 165 + } 166 + 167 + final protected function newTransaction($object) { 168 + return $object->getApplicationTransactionTemplate(); 169 + } 170 + 171 + final protected function newHeader() { 172 + return id(new PhabricatorProjectColumnHeader()) 173 + ->setOrderKey($this->getColumnOrderKey()); 174 + } 175 + 176 + }
+91
src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectColumnPriorityOrder 4 + extends PhabricatorProjectColumnOrder { 5 + 6 + const ORDERKEY = 'priority'; 7 + 8 + public function getDisplayName() { 9 + return pht('Group by Priority'); 10 + } 11 + 12 + protected function newMenuIconIcon() { 13 + return 'fa-sort-numeric-asc'; 14 + } 15 + 16 + protected function newHeaderKeyForObject($object) { 17 + return $this->newHeaderKeyForPriority($object->getPriority()); 18 + } 19 + 20 + private function newHeaderKeyForPriority($priority) { 21 + return sprintf('priority(%d)', $priority); 22 + } 23 + 24 + protected function newSortVectorForObject($object) { 25 + return $this->newSortVectorForPriority($object->getPriority()); 26 + } 27 + 28 + private function newSortVectorForPriority($priority) { 29 + return array( 30 + (int)-$priority, 31 + ); 32 + } 33 + 34 + protected function newHeadersForObjects(array $objects) { 35 + $priorities = ManiphestTaskPriority::getTaskPriorityMap(); 36 + 37 + // It's possible for tasks to have an invalid/unknown priority in the 38 + // database. We still want to generate a header for these tasks so we 39 + // don't break the workboard. 40 + $priorities = $priorities + mpull($objects, null, 'getPriority'); 41 + 42 + $priorities = array_keys($priorities); 43 + 44 + $headers = array(); 45 + foreach ($priorities as $priority) { 46 + $header_key = $this->newHeaderKeyForPriority($priority); 47 + $sort_vector = $this->newSortVectorForPriority($priority); 48 + 49 + $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); 50 + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); 51 + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); 52 + 53 + $icon_view = id(new PHUIIconView()) 54 + ->setIcon($priority_icon, $priority_color); 55 + 56 + $header = $this->newHeader() 57 + ->setHeaderKey($header_key) 58 + ->setSortVector($sort_vector) 59 + ->setName($priority_name) 60 + ->setIcon($icon_view) 61 + ->setEditProperties( 62 + array( 63 + 'value' => (int)$priority, 64 + )); 65 + 66 + $headers[] = $header; 67 + } 68 + 69 + return $headers; 70 + } 71 + 72 + protected function newColumnTransactions($object, array $header) { 73 + $new_priority = idx($header, 'value'); 74 + 75 + if ($object->getPriority() === $new_priority) { 76 + return null; 77 + } 78 + 79 + $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); 80 + $keyword = head(idx($keyword_map, $new_priority)); 81 + 82 + $xactions = array(); 83 + $xactions[] = $this->newTransaction($object) 84 + ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) 85 + ->setNewValue($keyword); 86 + 87 + return $xactions; 88 + } 89 + 90 + 91 + }
-7
src/applications/project/storage/PhabricatorProjectColumn.php
··· 12 12 const STATUS_ACTIVE = 0; 13 13 const STATUS_HIDDEN = 1; 14 14 15 - const DEFAULT_ORDER = 'natural'; 16 - const ORDER_NATURAL = 'natural'; 17 - const ORDER_PRIORITY = 'priority'; 18 - 19 - const NODETYPE_HEADER = 0; 20 - const NODETYPE_CARD = 1; 21 - 22 15 protected $name; 23 16 protected $status; 24 17 protected $projectPHID;
+31 -17
webroot/rsrc/js/application/projects/WorkboardBoard.js
··· 289 289 var columns = this.getColumns(); 290 290 291 291 var phid = response.objectPHID; 292 - var card = this.getCardTemplate(phid); 293 292 294 293 for (var add_phid in response.columnMaps) { 295 294 var target_column = this.getColumn(add_phid); ··· 302 301 target_column.newCard(phid); 303 302 } 304 303 305 - card.setNodeHTMLTemplate(response.cardHTML); 306 - 307 - var order_maps = response.orderMaps; 308 - for (var order_phid in order_maps) { 309 - var card_template = this.getCardTemplate(order_phid); 310 - for (var order_key in order_maps[order_phid]) { 311 - card_template.setSortVector( 312 - order_key, 313 - order_maps[order_phid][order_key]); 314 - } 315 - } 316 - 317 304 var column_maps = response.columnMaps; 318 305 var natural_column; 319 306 for (var natural_phid in column_maps) { ··· 329 316 natural_column.setNaturalOrder(column_maps[natural_phid]); 330 317 } 331 318 332 - var property_maps = response.propertyMaps; 333 - for (var property_phid in property_maps) { 334 - this.getCardTemplate(property_phid) 335 - .setObjectProperties(property_maps[property_phid]); 319 + for (var card_phid in response.cards) { 320 + var card_data = response.cards[card_phid]; 321 + var card_template = this.getCardTemplate(card_phid); 322 + 323 + if (card_data.nodeHTMLTemplate) { 324 + card_template.setNodeHTMLTemplate(card_data.nodeHTMLTemplate); 325 + } 326 + 327 + var order; 328 + for (order in card_data.vectors) { 329 + card_template.setSortVector(order, card_data.vectors[order]); 330 + } 331 + 332 + for (order in card_data.headers) { 333 + card_template.setHeaderKey(order, card_data.headers[order]); 334 + } 335 + 336 + for (var key in card_data.properties) { 337 + card_template.setObjectProperty(key, card_data.properties[key]); 338 + } 339 + } 340 + 341 + var headers = response.headers; 342 + for (var jj = 0; jj < headers.length; jj++) { 343 + var header = headers[jj]; 344 + 345 + this.getHeaderTemplate(header.key) 346 + .setOrder(header.order) 347 + .setNodeHTMLTemplate(header.template) 348 + .setVector(header.vector) 349 + .setEditProperties(header.editProperties); 336 350 } 337 351 338 352 for (var column_phid in columns) {
+2 -5
webroot/rsrc/js/application/projects/WorkboardCard.js
··· 29 29 }, 30 30 31 31 getProperties: function() { 32 - return this.getColumn().getBoard().getCardTemplate(this.getPHID()) 32 + return this.getColumn().getBoard() 33 + .getCardTemplate(this.getPHID()) 33 34 .getObjectProperties(); 34 35 }, 35 36 ··· 39 40 40 41 getStatus: function() { 41 42 return this.getProperties().status; 42 - }, 43 - 44 - getPriority: function(order) { 45 - return this.getProperties().priority; 46 43 }, 47 44 48 45 getNode: function() {
+17
webroot/rsrc/js/application/projects/WorkboardCardTemplate.js
··· 9 9 construct: function(phid) { 10 10 this._phid = phid; 11 11 this._vectors = {}; 12 + this._headerKeys = {}; 12 13 13 14 this.setObjectProperties({}); 14 15 }, ··· 19 20 20 21 members: { 21 22 _phid: null, 23 + _html: null, 22 24 _vectors: null, 25 + _headerKeys: null, 23 26 24 27 getPHID: function() { 25 28 return this._phid; ··· 39 42 return this._vectors[order]; 40 43 }, 41 44 45 + setHeaderKey: function(order, key) { 46 + this._headerKeys[order] = key; 47 + return this; 48 + }, 49 + 50 + getHeaderKey: function(order) { 51 + return this._headerKeys[order]; 52 + }, 53 + 42 54 newNode: function() { 43 55 return JX.$H(this._html).getFragment().firstChild; 56 + }, 57 + 58 + setObjectProperty: function(key, value) { 59 + this.getObjectProperties()[key] = value; 60 + return this; 44 61 } 45 62 } 46 63
+6 -10
webroot/rsrc/js/application/projects/WorkboardColumn.js
··· 189 189 var board = this.getBoard(); 190 190 var order = board.getOrder(); 191 191 192 + // TODO: This should be modularized into "ProjectColumnOrder" classes, 193 + // but is currently hard-coded. 194 + 192 195 switch (order) { 193 196 case 'natural': 194 197 return false; 195 198 } 196 199 197 200 return true; 198 - }, 199 - 200 - _getCardHeaderKey: function(card, order) { 201 - switch (order) { 202 - case 'priority': 203 - return 'priority(' + card.getPriority() + ')'; 204 - default: 205 - return null; 206 - } 207 201 }, 208 202 209 203 redraw: function() { ··· 235 229 // cards in a column. 236 230 237 231 if (has_headers) { 238 - var header_key = this._getCardHeaderKey(card, order); 232 + var header_key = board.getCardTemplate(card.getPHID()) 233 + .getHeaderKey(order); 234 + 239 235 if (!seen_headers[header_key]) { 240 236 while (header_keys.length) { 241 237 var next = header_keys.pop();
+8 -4
webroot/rsrc/js/application/projects/WorkboardHeader.js
··· 27 27 getNode: function() { 28 28 if (!this._root) { 29 29 var header_key = this.getHeaderKey(); 30 - var board = this.getColumn().getBoard(); 31 - var template = board.getHeaderTemplate(header_key).getTemplate(); 32 - this._root = JX.$H(template).getFragment().firstChild; 33 30 34 - JX.Stratcom.getData(this._root).headerKey = header_key; 31 + var root = this.getColumn().getBoard() 32 + .getHeaderTemplate(header_key) 33 + .newNode(); 34 + 35 + JX.Stratcom.getData(root).headerKey = header_key; 36 + 37 + this._root = root; 35 38 } 39 + 36 40 return this._root; 37 41 }, 38 42
+10
webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js
··· 19 19 20 20 members: { 21 21 _headerKey: null, 22 + _html: null, 22 23 23 24 getHeaderKey: function() { 24 25 return this._headerKey; 26 + }, 27 + 28 + setNodeHTMLTemplate: function(html) { 29 + this._html = html; 30 + return this; 31 + }, 32 + 33 + newNode: function() { 34 + return JX.$H(this._html).getFragment().firstChild; 25 35 } 26 36 27 37 }
+7 -1
webroot/rsrc/js/application/projects/behavior-project-boards.js
··· 116 116 117 117 board.getHeaderTemplate(header.key) 118 118 .setOrder(header.order) 119 - .setTemplate(header.template) 119 + .setNodeHTMLTemplate(header.template) 120 120 .setVector(header.vector) 121 121 .setEditProperties(header.editProperties); 122 + } 123 + 124 + var header_keys = config.headerKeys; 125 + for (var header_phid in header_keys) { 126 + board.getCardTemplate(header_phid) 127 + .setHeaderKey(config.order, header_keys[header_phid]); 122 128 } 123 129 124 130 board.start();