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

Add priority group headers to workboard columns (display only)

Summary:
Ref T10333. When workboards are ordered (for example, by priority), add headers to the various groups. Major goals are:

- Allow users to drag-and-drop to set values that no cards currently have: for example, you can change a card priority to "normal" by dragging it under the "normal" header, even if no other cards in the column are currently "Normal".
- Make future orderings more useful, particularly "order by assignee". We don't really have room to put the username on every card and it would create a fair amount of clutter, but we can put usernames in these headers and then reference them with just the profile picture. This also allows you to assign to users who are not currently assigned anything in a given column.
- Make the drag-and-drop behavior more obvious by showing what it will do more clearly (see T8135).
- Make things a little easier to scan in general: because space on cards is limited, some information isn't conveyed very clearly (for example, priority information is currently conveyed //only// through color, which can be hard to pick out visually and is probably not functional for users who need vision accommodations).
- Maybe do "swimlanes": this is pretty much a "swimlanes" UI if we add whitespace at the bottom of each group so that the headers line up across all the columns (e.g., "Normal" is at the same y-axis position in every column as you scroll down the page). Not sold on this being useful, but it's just a UI adjustment if we do want to try it.

NOTE: This only makes these headers work for display.

They aren't yet recognized as targets by the drag list UI, so you can't drag cards into an empty group. I'll tackle that in a followup.

Test Plan: {F6257686}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T10333

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

+306 -60
+48 -36
resources/celerity/map.php
··· 178 178 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 179 179 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 180 180 'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90', 181 - 'rsrc/css/phui/workboards/phui-workpanel.css' => '7e12d43c', 181 + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bc16cf33', 182 182 'rsrc/css/sprite-login.css' => '18b368a6', 183 183 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 184 184 'rsrc/css/syntax/syntax-default.css' => '055fc231', ··· 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' => 'fd96a6e8', 413 - 'rsrc/js/application/projects/WorkboardCard.js' => '9a513421', 414 - 'rsrc/js/application/projects/WorkboardColumn.js' => '1f71e559', 412 + 'rsrc/js/application/projects/WorkboardBoard.js' => 'e4e2d107', 413 + 'rsrc/js/application/projects/WorkboardCard.js' => 'c23ddfde', 414 + 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd9cb972', 415 415 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 416 - 'rsrc/js/application/projects/behavior-project-boards.js' => '05c74d65', 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', 417 419 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 418 420 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 419 421 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', ··· 655 657 'javelin-behavior-phuix-example' => 'c2c500a7', 656 658 'javelin-behavior-policy-control' => '0eaa33a9', 657 659 'javelin-behavior-policy-rule-editor' => '9347f172', 658 - 'javelin-behavior-project-boards' => '05c74d65', 660 + 'javelin-behavior-project-boards' => 'a3f6b67f', 659 661 'javelin-behavior-project-create' => '34c53422', 660 662 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 661 663 'javelin-behavior-read-only-warning' => 'b9109f8f', ··· 727 729 'javelin-view-renderer' => '9aae2b66', 728 730 'javelin-view-visitor' => '308f9fe4', 729 731 'javelin-websocket' => 'fdc13e4e', 730 - 'javelin-workboard-board' => 'fd96a6e8', 731 - 'javelin-workboard-card' => '9a513421', 732 - 'javelin-workboard-column' => '1f71e559', 732 + 'javelin-workboard-board' => 'e4e2d107', 733 + 'javelin-workboard-card' => 'c23ddfde', 734 + 'javelin-workboard-column' => 'fd9cb972', 733 735 'javelin-workboard-controller' => '42c7a5a7', 736 + 'javelin-workboard-header' => '354c5c0e', 737 + 'javelin-workboard-header-template' => '9b86cd0d', 734 738 'javelin-workflow' => '958e9045', 735 739 'maniphest-report-css' => '3d53188b', 736 740 'maniphest-task-edit-css' => '272daa84', ··· 854 858 'phui-workboard-color-css' => 'e86de308', 855 859 'phui-workboard-view-css' => '74fc9d98', 856 860 'phui-workcard-view-css' => '8c536f90', 857 - 'phui-workpanel-view-css' => '7e12d43c', 861 + 'phui-workpanel-view-css' => 'bc16cf33', 858 862 'phuix-action-list-view' => 'c68f183f', 859 863 'phuix-action-view' => 'aaa08f3b', 860 864 'phuix-autocomplete' => '8f139ef0', ··· 915 919 'javelin-dom', 916 920 'javelin-workflow', 917 921 ), 918 - '05c74d65' => array( 919 - 'javelin-behavior', 920 - 'javelin-dom', 921 - 'javelin-util', 922 - 'javelin-vector', 923 - 'javelin-stratcom', 924 - 'javelin-workflow', 925 - 'javelin-workboard-controller', 926 - ), 927 922 '05d290ef' => array( 928 923 'javelin-install', 929 924 'javelin-util', ··· 1033 1028 '1e413dc9' => array( 1034 1029 'javelin-behavior', 1035 1030 'javelin-dom', 1036 - ), 1037 - '1f71e559' => array( 1038 - 'javelin-install', 1039 - 'javelin-workboard-card', 1040 1031 ), 1041 1032 '1ff278aa' => array( 1042 1033 'phui-button-css', ··· 1171 1162 'javelin-dom', 1172 1163 'javelin-stratcom', 1173 1164 'javelin-workflow', 1165 + ), 1166 + '354c5c0e' => array( 1167 + 'javelin-install', 1174 1168 ), 1175 1169 '37b8a04a' => array( 1176 1170 'javelin-install', ··· 1535 1529 'javelin-install', 1536 1530 'javelin-dom', 1537 1531 ), 1538 - '7e12d43c' => array( 1539 - 'phui-workcard-view-css', 1540 - ), 1541 1532 '80bff3af' => array( 1542 1533 'javelin-install', 1543 1534 'javelin-typeahead-source', ··· 1701 1692 'javelin-dom', 1702 1693 'javelin-router', 1703 1694 ), 1704 - '9a513421' => array( 1705 - 'javelin-install', 1706 - ), 1707 1695 '9aae2b66' => array( 1708 1696 'javelin-install', 1709 1697 'javelin-util', ··· 1712 1700 'javelin-behavior', 1713 1701 'javelin-dom', 1714 1702 'javelin-stratcom', 1703 + ), 1704 + '9b86cd0d' => array( 1705 + 'javelin-install', 1715 1706 ), 1716 1707 '9cec214e' => array( 1717 1708 'javelin-behavior', ··· 1737 1728 'a241536a' => array( 1738 1729 'javelin-install', 1739 1730 ), 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 1740 'a4356cde' => array( 1741 1741 'javelin-install', 1742 1742 'javelin-dom', ··· 1887 1887 'javelin-uri', 1888 1888 'phabricator-notification', 1889 1889 ), 1890 + 'bc16cf33' => array( 1891 + 'phui-workcard-view-css', 1892 + ), 1890 1893 'bdce4d78' => array( 1891 1894 'javelin-install', 1892 1895 'javelin-util', ··· 1903 1906 'javelin-stratcom', 1904 1907 'javelin-uri', 1905 1908 ), 1909 + 'c23ddfde' => array( 1910 + 'javelin-install', 1911 + ), 1906 1912 'c2c500a7' => array( 1907 1913 'javelin-install', 1908 1914 'javelin-dom', ··· 2019 2025 'javelin-dom', 2020 2026 'javelin-history', 2021 2027 ), 2028 + 'e4e2d107' => array( 2029 + 'javelin-install', 2030 + 'javelin-dom', 2031 + 'javelin-util', 2032 + 'javelin-stratcom', 2033 + 'javelin-workflow', 2034 + 'phabricator-draggable-list', 2035 + 'javelin-workboard-column', 2036 + 'javelin-workboard-header-template', 2037 + ), 2022 2038 'e562708c' => array( 2023 2039 'javelin-install', 2024 2040 ), ··· 2120 2136 'javelin-magical-init', 2121 2137 'javelin-util', 2122 2138 ), 2123 - 'fd96a6e8' => array( 2139 + 'fd9cb972' => array( 2124 2140 'javelin-install', 2125 - 'javelin-dom', 2126 - 'javelin-util', 2127 - 'javelin-stratcom', 2128 - 'javelin-workflow', 2129 - 'phabricator-draggable-list', 2130 - 'javelin-workboard-column', 2141 + 'javelin-workboard-card', 2142 + 'javelin-workboard-header', 2131 2143 ), 2132 2144 'fdc13e4e' => array( 2133 2145 'javelin-install',
+1
src/applications/maniphest/storage/ManiphestTask.php
··· 306 306 return array( 307 307 'status' => $this->getStatus(), 308 308 'points' => (double)$this->getPoints(), 309 + 'priority' => $this->getPriority(), 309 310 ); 310 311 } 311 312
+40
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 621 621 $board->addPanel($panel); 622 622 } 623 623 624 + // It's possible for tasks to have an invalid/unknown priority in the 625 + // database. We still want to generate a header for these tasks so we 626 + // don't break the workboard. 627 + $priorities = 628 + ManiphestTaskPriority::getTaskPriorityMap() + 629 + mpull($all_tasks, null, 'getPriority'); 630 + $priorities = array_keys($priorities); 631 + 632 + $headers = array(); 633 + foreach ($priorities as $priority) { 634 + $header_key = sprintf('priority(%s)', $priority); 635 + 636 + $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); 637 + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); 638 + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); 639 + 640 + $icon_view = id(new PHUIIconView()) 641 + ->setIcon("{$priority_icon} {$priority_color}"); 642 + 643 + $template = phutil_tag( 644 + 'li', 645 + array( 646 + 'class' => 'workboard-group-header', 647 + ), 648 + array( 649 + $icon_view, 650 + $priority_name, 651 + )); 652 + 653 + $headers[] = array( 654 + 'order' => 'priority', 655 + 'key' => $header_key, 656 + 'template' => hsprintf('%s', $template), 657 + 'vector' => array( 658 + (int)-$priority, 659 + ), 660 + ); 661 + } 662 + 624 663 $behavior_config = array( 625 664 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 626 665 'uploadURI' => '/file/dropupload/', ··· 630 669 631 670 'boardPHID' => $project->getPHID(), 632 671 'order' => $this->sortKey, 672 + 'headers' => $headers, 633 673 'templateMap' => $templates, 634 674 'columnMaps' => $column_maps, 635 675 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'),
+13
webroot/rsrc/css/phui/workboards/phui-workpanel.css
··· 145 145 .phui-workpanel-view.workboard-column-drop-target .phui-box-grey { 146 146 border-color: {$lightblueborder}; 147 147 } 148 + 149 + .workboard-group-header { 150 + background: rgba({$alphablue}, 0.10); 151 + padding: 4px 8px; 152 + margin: 0 0 8px -8px; 153 + border-bottom: 1px solid {$lightgreyborder}; 154 + font-weight: bold; 155 + color: {$darkgreytext}; 156 + } 157 + 158 + .workboard-group-header .phui-icon-view { 159 + margin-right: 8px; 160 + }
+47
webroot/rsrc/js/application/projects/WorkboardBoard.js
··· 7 7 * javelin-workflow 8 8 * phabricator-draggable-list 9 9 * javelin-workboard-column 10 + * javelin-workboard-header-template 10 11 * @javelin 11 12 */ 12 13 ··· 20 21 this._templates = {}; 21 22 this._orderMaps = {}; 22 23 this._propertiesMap = {}; 24 + this._headers = {}; 23 25 this._buildColumns(); 24 26 }, 25 27 ··· 36 38 _templates: null, 37 39 _orderMaps: null, 38 40 _propertiesMap: null, 41 + _headers: null, 39 42 40 43 getRoot: function() { 41 44 return this._root; ··· 58 61 return this; 59 62 }, 60 63 64 + getHeaderTemplate: function(header_key) { 65 + if (!this._headers[header_key]) { 66 + this._headers[header_key] = new JX.WorkboardHeaderTemplate(header_key); 67 + } 68 + 69 + return this._headers[header_key]; 70 + }, 71 + 72 + getHeaderTemplatesForOrder: function(order) { 73 + var templates = []; 74 + 75 + for (var k in this._headers) { 76 + var header = this._headers[k]; 77 + 78 + if (header.getOrder() !== order) { 79 + continue; 80 + } 81 + 82 + templates.push(header); 83 + } 84 + 85 + templates.sort(JX.bind(this, this._sortHeaderTemplates)); 86 + 87 + return templates; 88 + }, 89 + 90 + _sortHeaderTemplates: function(u, v) { 91 + return this.compareVectors(u.getVector(), v.getVector()); 92 + }, 93 + 61 94 setObjectProperties: function(phid, properties) { 62 95 this._propertiesMap[phid] = properties; 63 96 return this; ··· 82 115 83 116 getOrderVector: function(phid, key) { 84 117 return this._orderMaps[phid][key]; 118 + }, 119 + 120 + compareVectors: function(u_vec, v_vec) { 121 + for (var ii = 0; ii < u_vec.length; ii++) { 122 + if (u_vec[ii] > v_vec[ii]) { 123 + return 1; 124 + } 125 + 126 + if (u_vec[ii] < v_vec[ii]) { 127 + return -1; 128 + } 129 + } 130 + 131 + return 0; 85 132 }, 86 133 87 134 start: function() {
+4
webroot/rsrc/js/application/projects/WorkboardCard.js
··· 40 40 return this.getProperties().status; 41 41 }, 42 42 43 + getPriority: function(order) { 44 + return this.getProperties().priority; 45 + }, 46 + 43 47 getNode: function() { 44 48 if (!this._root) { 45 49 var phid = this.getPHID();
+77 -24
webroot/rsrc/js/application/projects/WorkboardColumn.js
··· 2 2 * @provides javelin-workboard-column 3 3 * @requires javelin-install 4 4 * javelin-workboard-card 5 + * javelin-workboard-header 5 6 * @javelin 6 7 */ 7 8 ··· 21 22 'column-points-content'); 22 23 23 24 this._cards = {}; 25 + this._headers = {}; 26 + this._objects = []; 24 27 this._naturalOrder = []; 25 28 }, 26 29 ··· 29 32 _root: null, 30 33 _board: null, 31 34 _cards: null, 35 + _headers: null, 32 36 _naturalOrder: null, 33 37 _panel: null, 34 38 _pointsNode: null, 35 39 _pointsContentNode: null, 36 40 _dirty: true, 41 + _objects: null, 37 42 38 43 getPHID: function() { 39 44 return this._phid; ··· 148 153 return this._dirty; 149 154 }, 150 155 156 + getHeader: function(key) { 157 + if (!this._headers[key]) { 158 + this._headers[key] = new JX.WorkboardHeader(this, key); 159 + } 160 + return this._headers[key]; 161 + }, 162 + 163 + _getCardHeaderKey: function(card, order) { 164 + switch (order) { 165 + case 'priority': 166 + return 'priority(' + card.getPriority() + ')'; 167 + default: 168 + return null; 169 + } 170 + }, 171 + 151 172 redraw: function() { 152 173 var board = this.getBoard(); 153 174 var order = board.getOrder(); 154 175 155 176 var list; 177 + var has_headers; 156 178 if (order == 'natural') { 157 179 list = this._getCardsSortedNaturally(); 180 + has_headers = false; 158 181 } else { 159 182 list = this._getCardsSortedByKey(order); 183 + has_headers = true; 160 184 } 161 185 186 + var ii; 187 + var objects = []; 188 + 189 + var header_keys = []; 190 + var seen_headers = {}; 191 + if (has_headers) { 192 + var header_templates = board.getHeaderTemplatesForOrder(order); 193 + for (var k in header_templates) { 194 + header_keys.push(header_templates[k].getHeaderKey()); 195 + } 196 + header_keys.reverse(); 197 + } 198 + 199 + for (ii = 0; ii < list.length; ii++) { 200 + var card = list[ii]; 201 + 202 + // If a column has a "High" priority card and a "Low" priority card, 203 + // we need to add the "Normal" header in between them. This allows 204 + // you to change priority to "Normal" even if there are no "Normal" 205 + // cards in a column. 206 + 207 + if (has_headers) { 208 + var header_key = this._getCardHeaderKey(card, order); 209 + if (!seen_headers[header_key]) { 210 + while (header_keys.length) { 211 + var next = header_keys.pop(); 212 + 213 + var header = this.getHeader(next); 214 + objects.push(header); 215 + seen_headers[header_key] = true; 216 + 217 + if (next === header_key) { 218 + break; 219 + } 220 + } 221 + } 222 + } 223 + 224 + objects.push(card); 225 + } 226 + 227 + this._objects = objects; 228 + 162 229 var content = []; 163 - for (var ii = 0; ii < list.length; ii++) { 164 - var card = list[ii]; 230 + for (ii = 0; ii < this._objects.length; ii++) { 231 + var object = this._objects[ii]; 165 232 166 - var node = card.getNode(); 233 + var node = object.getNode(); 167 234 content.push(node); 168 - 169 235 } 170 236 171 237 JX.DOM.setContent(this.getRoot(), content); ··· 182 248 var src_phid = JX.Stratcom.getData(src_node).objectPHID; 183 249 var dst_phid = JX.Stratcom.getData(dst_node).objectPHID; 184 250 185 - var u_vec = this.getBoard().getOrderVector(src_phid, order); 186 - var v_vec = this.getBoard().getOrderVector(dst_phid, order); 251 + var u_vec = board.getOrderVector(src_phid, order); 252 + var v_vec = board.getOrderVector(dst_phid, order); 187 253 188 - return this._compareVectors(u_vec, v_vec); 254 + return board.compareVectors(u_vec, v_vec); 189 255 }, 190 256 191 257 setIsDropTarget: function(is_target) { ··· 218 284 }, 219 285 220 286 _sortCards: function(order, u, v) { 221 - var u_vec = this.getBoard().getOrderVector(u.getPHID(), order); 222 - var v_vec = this.getBoard().getOrderVector(v.getPHID(), order); 287 + var board = this.getBoard(); 288 + var u_vec = board.getOrderVector(u.getPHID(), order); 289 + var v_vec = board.getOrderVector(v.getPHID(), order); 223 290 224 - return this._compareVectors(u_vec, v_vec); 225 - }, 226 - 227 - _compareVectors: function(u_vec, v_vec) { 228 - for (var ii = 0; ii < u_vec.length; ii++) { 229 - if (u_vec[ii] > v_vec[ii]) { 230 - return 1; 231 - } 232 - 233 - if (u_vec[ii] < v_vec[ii]) { 234 - return -1; 235 - } 236 - } 237 - 238 - return 0; 291 + return board.compareVectors(u_vec, v_vec); 239 292 }, 240 293 241 294 _redrawFrame: function() {
+38
webroot/rsrc/js/application/projects/WorkboardHeader.js
··· 1 + /** 2 + * @provides javelin-workboard-header 3 + * @requires javelin-install 4 + * @javelin 5 + */ 6 + 7 + JX.install('WorkboardHeader', { 8 + 9 + construct: function(column, header_key) { 10 + this._column = column; 11 + this._headerKey = header_key; 12 + }, 13 + 14 + members: { 15 + _root: null, 16 + _column: null, 17 + _headerKey: null, 18 + 19 + getColumn: function() { 20 + return this._column; 21 + }, 22 + 23 + getHeaderKey: function() { 24 + return this._headerKey; 25 + }, 26 + 27 + getNode: function() { 28 + if (!this._root) { 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 + } 34 + return this._root; 35 + } 36 + } 37 + 38 + });
+28
webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js
··· 1 + /** 2 + * @provides javelin-workboard-header-template 3 + * @requires javelin-install 4 + * @javelin 5 + */ 6 + 7 + JX.install('WorkboardHeaderTemplate', { 8 + 9 + construct: function(header_key) { 10 + this._headerKey = header_key; 11 + }, 12 + 13 + properties: { 14 + template: null, 15 + order: null, 16 + vector: null 17 + }, 18 + 19 + members: { 20 + _headerKey: null, 21 + 22 + getHeaderKey: function() { 23 + return this._headerKey; 24 + } 25 + 26 + } 27 + 28 + });
+10
webroot/rsrc/js/application/projects/behavior-project-boards.js
··· 105 105 board.setObjectProperties(property_phid, property_maps[property_phid]); 106 106 } 107 107 108 + var headers = config.headers; 109 + for (var jj = 0; jj < headers.length; jj++) { 110 + var header = headers[jj]; 111 + 112 + board.getHeaderTemplate(header.key) 113 + .setOrder(header.order) 114 + .setTemplate(header.template) 115 + .setVector(header.vector); 116 + } 117 + 108 118 board.start(); 109 119 110 120 });