diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5118dc741d..9bfb432f42 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -178,7 +178,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90', - 'rsrc/css/phui/workboards/phui-workpanel.css' => '7e12d43c', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bc16cf33', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -409,11 +409,13 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'fd96a6e8', - 'rsrc/js/application/projects/WorkboardCard.js' => '9a513421', - 'rsrc/js/application/projects/WorkboardColumn.js' => '1f71e559', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'e4e2d107', + 'rsrc/js/application/projects/WorkboardCard.js' => 'c23ddfde', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd9cb972', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', - 'rsrc/js/application/projects/behavior-project-boards.js' => '05c74d65', + 'rsrc/js/application/projects/WorkboardHeader.js' => '354c5c0e', + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '9b86cd0d', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'a3f6b67f', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -655,7 +657,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => '05c74d65', + 'javelin-behavior-project-boards' => 'a3f6b67f', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -727,10 +729,12 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'fd96a6e8', - 'javelin-workboard-card' => '9a513421', - 'javelin-workboard-column' => '1f71e559', + 'javelin-workboard-board' => 'e4e2d107', + 'javelin-workboard-card' => 'c23ddfde', + 'javelin-workboard-column' => 'fd9cb972', 'javelin-workboard-controller' => '42c7a5a7', + 'javelin-workboard-header' => '354c5c0e', + 'javelin-workboard-header-template' => '9b86cd0d', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', @@ -854,7 +858,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '8c536f90', - 'phui-workpanel-view-css' => '7e12d43c', + 'phui-workpanel-view-css' => 'bc16cf33', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -915,15 +919,6 @@ return array( 'javelin-dom', 'javelin-workflow', ), - '05c74d65' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), '05d290ef' => array( 'javelin-install', 'javelin-util', @@ -1034,10 +1029,6 @@ return array( 'javelin-behavior', 'javelin-dom', ), - '1f71e559' => array( - 'javelin-install', - 'javelin-workboard-card', - ), '1ff278aa' => array( 'phui-button-css', ), @@ -1172,6 +1163,9 @@ return array( 'javelin-stratcom', 'javelin-workflow', ), + '354c5c0e' => array( + 'javelin-install', + ), '37b8a04a' => array( 'javelin-install', 'javelin-util', @@ -1535,9 +1529,6 @@ return array( 'javelin-install', 'javelin-dom', ), - '7e12d43c' => array( - 'phui-workcard-view-css', - ), '80bff3af' => array( 'javelin-install', 'javelin-typeahead-source', @@ -1701,9 +1692,6 @@ return array( 'javelin-dom', 'javelin-router', ), - '9a513421' => array( - 'javelin-install', - ), '9aae2b66' => array( 'javelin-install', 'javelin-util', @@ -1713,6 +1701,9 @@ return array( 'javelin-dom', 'javelin-stratcom', ), + '9b86cd0d' => array( + 'javelin-install', + ), '9cec214e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1737,6 +1728,15 @@ return array( 'a241536a' => array( 'javelin-install', ), + 'a3f6b67f' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), 'a4356cde' => array( 'javelin-install', 'javelin-dom', @@ -1887,6 +1887,9 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'bc16cf33' => array( + 'phui-workcard-view-css', + ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1903,6 +1906,9 @@ return array( 'javelin-stratcom', 'javelin-uri', ), + 'c23ddfde' => array( + 'javelin-install', + ), 'c2c500a7' => array( 'javelin-install', 'javelin-dom', @@ -2019,6 +2025,16 @@ return array( 'javelin-dom', 'javelin-history', ), + 'e4e2d107' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + ), 'e562708c' => array( 'javelin-install', ), @@ -2120,14 +2136,10 @@ return array( 'javelin-magical-init', 'javelin-util', ), - 'fd96a6e8' => array( + 'fd9cb972' => array( 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', + 'javelin-workboard-card', + 'javelin-workboard-header', ), 'fdc13e4e' => array( 'javelin-install', diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 400bace650..88ade9c35a 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -306,6 +306,7 @@ final class ManiphestTask extends ManiphestDAO return array( 'status' => $this->getStatus(), 'points' => (double)$this->getPoints(), + 'priority' => $this->getPriority(), ); } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index f2965892d7..857004caf0 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -621,6 +621,45 @@ final class PhabricatorProjectBoardViewController $board->addPanel($panel); } + // It's possible for tasks to have an invalid/unknown priority in the + // database. We still want to generate a header for these tasks so we + // don't break the workboard. + $priorities = + ManiphestTaskPriority::getTaskPriorityMap() + + mpull($all_tasks, null, 'getPriority'); + $priorities = array_keys($priorities); + + $headers = array(); + foreach ($priorities as $priority) { + $header_key = sprintf('priority(%s)', $priority); + + $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); + + $icon_view = id(new PHUIIconView()) + ->setIcon("{$priority_icon} {$priority_color}"); + + $template = phutil_tag( + 'li', + array( + 'class' => 'workboard-group-header', + ), + array( + $icon_view, + $priority_name, + )); + + $headers[] = array( + 'order' => 'priority', + 'key' => $header_key, + 'template' => hsprintf('%s', $template), + 'vector' => array( + (int)-$priority, + ), + ); + } + $behavior_config = array( 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'uploadURI' => '/file/dropupload/', @@ -630,6 +669,7 @@ final class PhabricatorProjectBoardViewController 'boardPHID' => $project->getPHID(), 'order' => $this->sortKey, + 'headers' => $headers, 'templateMap' => $templates, 'columnMaps' => $column_maps, 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index fb7415ff20..2dac6b2233 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -145,3 +145,16 @@ .phui-workpanel-view.workboard-column-drop-target .phui-box-grey { border-color: {$lightblueborder}; } + +.workboard-group-header { + background: rgba({$alphablue}, 0.10); + padding: 4px 8px; + margin: 0 0 8px -8px; + border-bottom: 1px solid {$lightgreyborder}; + font-weight: bold; + color: {$darkgreytext}; +} + +.workboard-group-header .phui-icon-view { + margin-right: 8px; +} diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index b3f8e585d6..2ea38b07b2 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -7,6 +7,7 @@ * javelin-workflow * phabricator-draggable-list * javelin-workboard-column + * javelin-workboard-header-template * @javelin */ @@ -20,6 +21,7 @@ JX.install('WorkboardBoard', { this._templates = {}; this._orderMaps = {}; this._propertiesMap = {}; + this._headers = {}; this._buildColumns(); }, @@ -36,6 +38,7 @@ JX.install('WorkboardBoard', { _templates: null, _orderMaps: null, _propertiesMap: null, + _headers: null, getRoot: function() { return this._root; @@ -58,6 +61,36 @@ JX.install('WorkboardBoard', { return this; }, + getHeaderTemplate: function(header_key) { + if (!this._headers[header_key]) { + this._headers[header_key] = new JX.WorkboardHeaderTemplate(header_key); + } + + return this._headers[header_key]; + }, + + getHeaderTemplatesForOrder: function(order) { + var templates = []; + + for (var k in this._headers) { + var header = this._headers[k]; + + if (header.getOrder() !== order) { + continue; + } + + templates.push(header); + } + + templates.sort(JX.bind(this, this._sortHeaderTemplates)); + + return templates; + }, + + _sortHeaderTemplates: function(u, v) { + return this.compareVectors(u.getVector(), v.getVector()); + }, + setObjectProperties: function(phid, properties) { this._propertiesMap[phid] = properties; return this; @@ -84,6 +117,20 @@ JX.install('WorkboardBoard', { return this._orderMaps[phid][key]; }, + compareVectors: function(u_vec, v_vec) { + for (var ii = 0; ii < u_vec.length; ii++) { + if (u_vec[ii] > v_vec[ii]) { + return 1; + } + + if (u_vec[ii] < v_vec[ii]) { + return -1; + } + } + + return 0; + }, + start: function() { this._setupDragHandlers(); diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js index b506e655c1..753eca40f1 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCard.js +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -40,6 +40,10 @@ JX.install('WorkboardCard', { return this.getProperties().status; }, + getPriority: function(order) { + return this.getProperties().priority; + }, + getNode: function() { if (!this._root) { var phid = this.getPHID(); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index a94604a470..fdb165f589 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -2,6 +2,7 @@ * @provides javelin-workboard-column * @requires javelin-install * javelin-workboard-card + * javelin-workboard-header * @javelin */ @@ -21,6 +22,8 @@ JX.install('WorkboardColumn', { 'column-points-content'); this._cards = {}; + this._headers = {}; + this._objects = []; this._naturalOrder = []; }, @@ -29,11 +32,13 @@ JX.install('WorkboardColumn', { _root: null, _board: null, _cards: null, + _headers: null, _naturalOrder: null, _panel: null, _pointsNode: null, _pointsContentNode: null, _dirty: true, + _objects: null, getPHID: function() { return this._phid; @@ -148,24 +153,85 @@ JX.install('WorkboardColumn', { return this._dirty; }, + getHeader: function(key) { + if (!this._headers[key]) { + this._headers[key] = new JX.WorkboardHeader(this, key); + } + return this._headers[key]; + }, + + _getCardHeaderKey: function(card, order) { + switch (order) { + case 'priority': + return 'priority(' + card.getPriority() + ')'; + default: + return null; + } + }, + redraw: function() { var board = this.getBoard(); var order = board.getOrder(); var list; + var has_headers; if (order == 'natural') { list = this._getCardsSortedNaturally(); + has_headers = false; } else { list = this._getCardsSortedByKey(order); + has_headers = true; } - var content = []; - for (var ii = 0; ii < list.length; ii++) { + var ii; + var objects = []; + + var header_keys = []; + var seen_headers = {}; + if (has_headers) { + var header_templates = board.getHeaderTemplatesForOrder(order); + for (var k in header_templates) { + header_keys.push(header_templates[k].getHeaderKey()); + } + header_keys.reverse(); + } + + for (ii = 0; ii < list.length; ii++) { var card = list[ii]; - var node = card.getNode(); - content.push(node); + // If a column has a "High" priority card and a "Low" priority card, + // we need to add the "Normal" header in between them. This allows + // you to change priority to "Normal" even if there are no "Normal" + // cards in a column. + if (has_headers) { + var header_key = this._getCardHeaderKey(card, order); + if (!seen_headers[header_key]) { + while (header_keys.length) { + var next = header_keys.pop(); + + var header = this.getHeader(next); + objects.push(header); + seen_headers[header_key] = true; + + if (next === header_key) { + break; + } + } + } + } + + objects.push(card); + } + + this._objects = objects; + + var content = []; + for (ii = 0; ii < this._objects.length; ii++) { + var object = this._objects[ii]; + + var node = object.getNode(); + content.push(node); } JX.DOM.setContent(this.getRoot(), content); @@ -182,10 +248,10 @@ JX.install('WorkboardColumn', { var src_phid = JX.Stratcom.getData(src_node).objectPHID; var dst_phid = JX.Stratcom.getData(dst_node).objectPHID; - var u_vec = this.getBoard().getOrderVector(src_phid, order); - var v_vec = this.getBoard().getOrderVector(dst_phid, order); + var u_vec = board.getOrderVector(src_phid, order); + var v_vec = board.getOrderVector(dst_phid, order); - return this._compareVectors(u_vec, v_vec); + return board.compareVectors(u_vec, v_vec); }, setIsDropTarget: function(is_target) { @@ -218,24 +284,11 @@ JX.install('WorkboardColumn', { }, _sortCards: function(order, u, v) { - var u_vec = this.getBoard().getOrderVector(u.getPHID(), order); - var v_vec = this.getBoard().getOrderVector(v.getPHID(), order); + var board = this.getBoard(); + var u_vec = board.getOrderVector(u.getPHID(), order); + var v_vec = board.getOrderVector(v.getPHID(), order); - return this._compareVectors(u_vec, v_vec); - }, - - _compareVectors: function(u_vec, v_vec) { - for (var ii = 0; ii < u_vec.length; ii++) { - if (u_vec[ii] > v_vec[ii]) { - return 1; - } - - if (u_vec[ii] < v_vec[ii]) { - return -1; - } - } - - return 0; + return board.compareVectors(u_vec, v_vec); }, _redrawFrame: function() { diff --git a/webroot/rsrc/js/application/projects/WorkboardHeader.js b/webroot/rsrc/js/application/projects/WorkboardHeader.js new file mode 100644 index 0000000000..d6cfd137d0 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardHeader.js @@ -0,0 +1,38 @@ +/** + * @provides javelin-workboard-header + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardHeader', { + + construct: function(column, header_key) { + this._column = column; + this._headerKey = header_key; + }, + + members: { + _root: null, + _column: null, + _headerKey: null, + + getColumn: function() { + return this._column; + }, + + getHeaderKey: function() { + return this._headerKey; + }, + + getNode: function() { + if (!this._root) { + var header_key = this.getHeaderKey(); + var board = this.getColumn().getBoard(); + var template = board.getHeaderTemplate(header_key).getTemplate(); + this._root = JX.$H(template).getFragment().firstChild; + } + return this._root; + } + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js new file mode 100644 index 0000000000..c08652bed0 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js @@ -0,0 +1,28 @@ +/** + * @provides javelin-workboard-header-template + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardHeaderTemplate', { + + construct: function(header_key) { + this._headerKey = header_key; + }, + + properties: { + template: null, + order: null, + vector: null + }, + + members: { + _headerKey: null, + + getHeaderKey: function() { + return this._headerKey; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 83f41787ab..fd2c1a0fe6 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -105,6 +105,16 @@ JX.behavior('project-boards', function(config, statics) { board.setObjectProperties(property_phid, property_maps[property_phid]); } + var headers = config.headers; + for (var jj = 0; jj < headers.length; jj++) { + var header = headers[jj]; + + board.getHeaderTemplate(header.key) + .setOrder(header.order) + .setTemplate(header.template) + .setVector(header.vector); + } + board.start(); });