1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

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
This commit is contained in:
epriestley 2019-03-05 06:00:12 -08:00
parent be1e3b2cc0
commit 14a433c773
10 changed files with 306 additions and 60 deletions

View file

@ -178,7 +178,7 @@ return array(
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308',
'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98',
'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90', '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-login.css' => '18b368a6',
'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/sprite-tokens.css' => 'f1896dc5',
'rsrc/css/syntax/syntax-default.css' => '055fc231', '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/phortune/phortune-credit-card-form.js' => 'd12d214f',
'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172',
'rsrc/js/application/projects/WorkboardBoard.js' => 'fd96a6e8', 'rsrc/js/application/projects/WorkboardBoard.js' => 'e4e2d107',
'rsrc/js/application/projects/WorkboardCard.js' => '9a513421', 'rsrc/js/application/projects/WorkboardCard.js' => 'c23ddfde',
'rsrc/js/application/projects/WorkboardColumn.js' => '1f71e559', 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd9cb972',
'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', '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-project-create.js' => '34c53422',
'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9',
'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68',
@ -655,7 +657,7 @@ return array(
'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-phuix-example' => 'c2c500a7',
'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-control' => '0eaa33a9',
'javelin-behavior-policy-rule-editor' => '9347f172', 'javelin-behavior-policy-rule-editor' => '9347f172',
'javelin-behavior-project-boards' => '05c74d65', 'javelin-behavior-project-boards' => 'a3f6b67f',
'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-project-create' => '34c53422',
'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06',
'javelin-behavior-read-only-warning' => 'b9109f8f', 'javelin-behavior-read-only-warning' => 'b9109f8f',
@ -727,10 +729,12 @@ return array(
'javelin-view-renderer' => '9aae2b66', 'javelin-view-renderer' => '9aae2b66',
'javelin-view-visitor' => '308f9fe4', 'javelin-view-visitor' => '308f9fe4',
'javelin-websocket' => 'fdc13e4e', 'javelin-websocket' => 'fdc13e4e',
'javelin-workboard-board' => 'fd96a6e8', 'javelin-workboard-board' => 'e4e2d107',
'javelin-workboard-card' => '9a513421', 'javelin-workboard-card' => 'c23ddfde',
'javelin-workboard-column' => '1f71e559', 'javelin-workboard-column' => 'fd9cb972',
'javelin-workboard-controller' => '42c7a5a7', 'javelin-workboard-controller' => '42c7a5a7',
'javelin-workboard-header' => '354c5c0e',
'javelin-workboard-header-template' => '9b86cd0d',
'javelin-workflow' => '958e9045', 'javelin-workflow' => '958e9045',
'maniphest-report-css' => '3d53188b', 'maniphest-report-css' => '3d53188b',
'maniphest-task-edit-css' => '272daa84', 'maniphest-task-edit-css' => '272daa84',
@ -854,7 +858,7 @@ return array(
'phui-workboard-color-css' => 'e86de308', 'phui-workboard-color-css' => 'e86de308',
'phui-workboard-view-css' => '74fc9d98', 'phui-workboard-view-css' => '74fc9d98',
'phui-workcard-view-css' => '8c536f90', 'phui-workcard-view-css' => '8c536f90',
'phui-workpanel-view-css' => '7e12d43c', 'phui-workpanel-view-css' => 'bc16cf33',
'phuix-action-list-view' => 'c68f183f', 'phuix-action-list-view' => 'c68f183f',
'phuix-action-view' => 'aaa08f3b', 'phuix-action-view' => 'aaa08f3b',
'phuix-autocomplete' => '8f139ef0', 'phuix-autocomplete' => '8f139ef0',
@ -915,15 +919,6 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-workflow', 'javelin-workflow',
), ),
'05c74d65' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'javelin-workboard-controller',
),
'05d290ef' => array( '05d290ef' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1034,10 +1029,6 @@ return array(
'javelin-behavior', 'javelin-behavior',
'javelin-dom', 'javelin-dom',
), ),
'1f71e559' => array(
'javelin-install',
'javelin-workboard-card',
),
'1ff278aa' => array( '1ff278aa' => array(
'phui-button-css', 'phui-button-css',
), ),
@ -1172,6 +1163,9 @@ return array(
'javelin-stratcom', 'javelin-stratcom',
'javelin-workflow', 'javelin-workflow',
), ),
'354c5c0e' => array(
'javelin-install',
),
'37b8a04a' => array( '37b8a04a' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1535,9 +1529,6 @@ return array(
'javelin-install', 'javelin-install',
'javelin-dom', 'javelin-dom',
), ),
'7e12d43c' => array(
'phui-workcard-view-css',
),
'80bff3af' => array( '80bff3af' => array(
'javelin-install', 'javelin-install',
'javelin-typeahead-source', 'javelin-typeahead-source',
@ -1701,9 +1692,6 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-router', 'javelin-router',
), ),
'9a513421' => array(
'javelin-install',
),
'9aae2b66' => array( '9aae2b66' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1713,6 +1701,9 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-stratcom', 'javelin-stratcom',
), ),
'9b86cd0d' => array(
'javelin-install',
),
'9cec214e' => array( '9cec214e' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', 'javelin-stratcom',
@ -1737,6 +1728,15 @@ return array(
'a241536a' => array( 'a241536a' => array(
'javelin-install', 'javelin-install',
), ),
'a3f6b67f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'javelin-workboard-controller',
),
'a4356cde' => array( 'a4356cde' => array(
'javelin-install', 'javelin-install',
'javelin-dom', 'javelin-dom',
@ -1887,6 +1887,9 @@ return array(
'javelin-uri', 'javelin-uri',
'phabricator-notification', 'phabricator-notification',
), ),
'bc16cf33' => array(
'phui-workcard-view-css',
),
'bdce4d78' => array( 'bdce4d78' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1903,6 +1906,9 @@ return array(
'javelin-stratcom', 'javelin-stratcom',
'javelin-uri', 'javelin-uri',
), ),
'c23ddfde' => array(
'javelin-install',
),
'c2c500a7' => array( 'c2c500a7' => array(
'javelin-install', 'javelin-install',
'javelin-dom', 'javelin-dom',
@ -2019,6 +2025,16 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-history', '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( 'e562708c' => array(
'javelin-install', 'javelin-install',
), ),
@ -2120,14 +2136,10 @@ return array(
'javelin-magical-init', 'javelin-magical-init',
'javelin-util', 'javelin-util',
), ),
'fd96a6e8' => array( 'fd9cb972' => array(
'javelin-install', 'javelin-install',
'javelin-dom', 'javelin-workboard-card',
'javelin-util', 'javelin-workboard-header',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
'javelin-workboard-column',
), ),
'fdc13e4e' => array( 'fdc13e4e' => array(
'javelin-install', 'javelin-install',

View file

@ -306,6 +306,7 @@ final class ManiphestTask extends ManiphestDAO
return array( return array(
'status' => $this->getStatus(), 'status' => $this->getStatus(),
'points' => (double)$this->getPoints(), 'points' => (double)$this->getPoints(),
'priority' => $this->getPriority(),
); );
} }

View file

@ -621,6 +621,45 @@ final class PhabricatorProjectBoardViewController
$board->addPanel($panel); $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( $behavior_config = array(
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
'uploadURI' => '/file/dropupload/', 'uploadURI' => '/file/dropupload/',
@ -630,6 +669,7 @@ final class PhabricatorProjectBoardViewController
'boardPHID' => $project->getPHID(), 'boardPHID' => $project->getPHID(),
'order' => $this->sortKey, 'order' => $this->sortKey,
'headers' => $headers,
'templateMap' => $templates, 'templateMap' => $templates,
'columnMaps' => $column_maps, 'columnMaps' => $column_maps,
'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'),

View file

@ -145,3 +145,16 @@
.phui-workpanel-view.workboard-column-drop-target .phui-box-grey { .phui-workpanel-view.workboard-column-drop-target .phui-box-grey {
border-color: {$lightblueborder}; 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;
}

View file

@ -7,6 +7,7 @@
* javelin-workflow * javelin-workflow
* phabricator-draggable-list * phabricator-draggable-list
* javelin-workboard-column * javelin-workboard-column
* javelin-workboard-header-template
* @javelin * @javelin
*/ */
@ -20,6 +21,7 @@ JX.install('WorkboardBoard', {
this._templates = {}; this._templates = {};
this._orderMaps = {}; this._orderMaps = {};
this._propertiesMap = {}; this._propertiesMap = {};
this._headers = {};
this._buildColumns(); this._buildColumns();
}, },
@ -36,6 +38,7 @@ JX.install('WorkboardBoard', {
_templates: null, _templates: null,
_orderMaps: null, _orderMaps: null,
_propertiesMap: null, _propertiesMap: null,
_headers: null,
getRoot: function() { getRoot: function() {
return this._root; return this._root;
@ -58,6 +61,36 @@ JX.install('WorkboardBoard', {
return this; 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) { setObjectProperties: function(phid, properties) {
this._propertiesMap[phid] = properties; this._propertiesMap[phid] = properties;
return this; return this;
@ -84,6 +117,20 @@ JX.install('WorkboardBoard', {
return this._orderMaps[phid][key]; 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() { start: function() {
this._setupDragHandlers(); this._setupDragHandlers();

View file

@ -40,6 +40,10 @@ JX.install('WorkboardCard', {
return this.getProperties().status; return this.getProperties().status;
}, },
getPriority: function(order) {
return this.getProperties().priority;
},
getNode: function() { getNode: function() {
if (!this._root) { if (!this._root) {
var phid = this.getPHID(); var phid = this.getPHID();

View file

@ -2,6 +2,7 @@
* @provides javelin-workboard-column * @provides javelin-workboard-column
* @requires javelin-install * @requires javelin-install
* javelin-workboard-card * javelin-workboard-card
* javelin-workboard-header
* @javelin * @javelin
*/ */
@ -21,6 +22,8 @@ JX.install('WorkboardColumn', {
'column-points-content'); 'column-points-content');
this._cards = {}; this._cards = {};
this._headers = {};
this._objects = [];
this._naturalOrder = []; this._naturalOrder = [];
}, },
@ -29,11 +32,13 @@ JX.install('WorkboardColumn', {
_root: null, _root: null,
_board: null, _board: null,
_cards: null, _cards: null,
_headers: null,
_naturalOrder: null, _naturalOrder: null,
_panel: null, _panel: null,
_pointsNode: null, _pointsNode: null,
_pointsContentNode: null, _pointsContentNode: null,
_dirty: true, _dirty: true,
_objects: null,
getPHID: function() { getPHID: function() {
return this._phid; return this._phid;
@ -148,24 +153,85 @@ JX.install('WorkboardColumn', {
return this._dirty; 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() { redraw: function() {
var board = this.getBoard(); var board = this.getBoard();
var order = board.getOrder(); var order = board.getOrder();
var list; var list;
var has_headers;
if (order == 'natural') { if (order == 'natural') {
list = this._getCardsSortedNaturally(); list = this._getCardsSortedNaturally();
has_headers = false;
} else { } else {
list = this._getCardsSortedByKey(order); list = this._getCardsSortedByKey(order);
has_headers = true;
} }
var content = []; var ii;
for (var ii = 0; ii < list.length; 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 card = list[ii];
var node = card.getNode(); // If a column has a "High" priority card and a "Low" priority card,
content.push(node); // 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); JX.DOM.setContent(this.getRoot(), content);
@ -182,10 +248,10 @@ JX.install('WorkboardColumn', {
var src_phid = JX.Stratcom.getData(src_node).objectPHID; var src_phid = JX.Stratcom.getData(src_node).objectPHID;
var dst_phid = JX.Stratcom.getData(dst_node).objectPHID; var dst_phid = JX.Stratcom.getData(dst_node).objectPHID;
var u_vec = this.getBoard().getOrderVector(src_phid, order); var u_vec = board.getOrderVector(src_phid, order);
var v_vec = this.getBoard().getOrderVector(dst_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) { setIsDropTarget: function(is_target) {
@ -218,24 +284,11 @@ JX.install('WorkboardColumn', {
}, },
_sortCards: function(order, u, v) { _sortCards: function(order, u, v) {
var u_vec = this.getBoard().getOrderVector(u.getPHID(), order); var board = this.getBoard();
var v_vec = this.getBoard().getOrderVector(v.getPHID(), order); var u_vec = board.getOrderVector(u.getPHID(), order);
var v_vec = board.getOrderVector(v.getPHID(), order);
return this._compareVectors(u_vec, v_vec); return board.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;
}, },
_redrawFrame: function() { _redrawFrame: function() {

View file

@ -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;
}
}
});

View file

@ -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;
}
}
});

View file

@ -105,6 +105,16 @@ JX.behavior('project-boards', function(config, statics) {
board.setObjectProperties(property_phid, property_maps[property_phid]); 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(); board.start();
}); });