1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-10-24 01:28:52 +02:00
phorge-phorge/webroot/rsrc/js/application/projects/WorkboardColumn.js
epriestley 9a8019d4a9 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
2019-03-12 13:07:50 -07:00

454 lines
11 KiB
JavaScript

/**
* @provides javelin-workboard-column
* @requires javelin-install
* javelin-workboard-card
* javelin-workboard-header
* @javelin
*/
JX.install('WorkboardColumn', {
construct: function(board, phid, root) {
this._board = board;
this._phid = phid;
this._root = root;
this._panel = JX.DOM.findAbove(root, 'div', 'workpanel');
this._pointsNode = JX.DOM.find(this._panel, 'span', 'column-points');
this._pointsContentNode = JX.DOM.find(
this._panel,
'span',
'column-points-content');
this._cards = {};
this._headers = {};
this._objects = [];
this._naturalOrder = [];
},
members: {
_phid: null,
_root: null,
_board: null,
_cards: null,
_headers: null,
_naturalOrder: null,
_orderVectors: null,
_panel: null,
_pointsNode: null,
_pointsContentNode: null,
_dirty: true,
_objects: null,
getPHID: function() {
return this._phid;
},
getRoot: function() {
return this._root;
},
getCards: function() {
return this._cards;
},
_getObjects: function() {
return this._objects;
},
getCard: function(phid) {
return this._cards[phid];
},
getBoard: function() {
return this._board;
},
setNaturalOrder: function(order) {
this._naturalOrder = order;
this._orderVectors = null;
return this;
},
getPointsNode: function() {
return this._pointsNode;
},
getPointsContentNode: function() {
return this._pointsContentNode;
},
getWorkpanelNode: function() {
return this._panel;
},
newCard: function(phid) {
var card = new JX.WorkboardCard(this, phid);
this._cards[phid] = card;
this._naturalOrder.push(phid);
this._orderVectors = null;
return card;
},
removeCard: function(phid) {
var card = this._cards[phid];
delete this._cards[phid];
for (var ii = 0; ii < this._naturalOrder.length; ii++) {
if (this._naturalOrder[ii] == phid) {
this._naturalOrder.splice(ii, 1);
this._orderVectors = null;
break;
}
}
return card;
},
addCard: function(card, after) {
var phid = card.getPHID();
card.setColumn(this);
this._cards[phid] = card;
var index = 0;
if (after) {
for (var ii = 0; ii < this._naturalOrder.length; ii++) {
if (this._naturalOrder[ii] == after) {
index = ii + 1;
break;
}
}
}
if (index > this._naturalOrder.length) {
this._naturalOrder.push(phid);
} else {
this._naturalOrder.splice(index, 0, phid);
}
this._orderVectors = null;
return this;
},
getDropTargetNodes: function() {
var objects = this._getObjects();
var nodes = [];
for (var ii = 0; ii < objects.length; ii++) {
var object = objects[ii];
nodes.push(object.getNode());
}
return nodes;
},
getCardPHIDs: function() {
return JX.keys(this.getCards());
},
getPointLimit: function() {
return JX.Stratcom.getData(this.getRoot()).pointLimit;
},
markForRedraw: function() {
this._dirty = true;
},
isMarkedForRedraw: function() {
return this._dirty;
},
getHeader: function(key) {
if (!this._headers[key]) {
this._headers[key] = new JX.WorkboardHeader(this, key);
}
return this._headers[key];
},
handleDragGhost: function(default_handler, ghost, node) {
// If the column has headers, don't let the user drag a card above
// the topmost header: for example, you can't change a task to have
// a priority higher than the highest possible priority.
if (this._hasColumnHeaders()) {
if (!node) {
return false;
}
}
return default_handler(ghost, node);
},
_hasColumnHeaders: function() {
var board = this.getBoard();
var order = board.getOrder();
// TODO: This should be modularized into "ProjectColumnOrder" classes,
// but is currently hard-coded.
switch (order) {
case 'natural':
return false;
}
return true;
},
redraw: function() {
var board = this.getBoard();
var order = board.getOrder();
var list = this._getCardsSortedByKey(order);
var ii;
var objects = [];
var has_headers = this._hasColumnHeaders();
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];
// 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 = board.getCardTemplate(card.getPHID())
.getHeaderKey(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);
this._redrawFrame();
this._dirty = false;
},
compareHandler: function(src_list, src_node, dst_list, dst_node) {
var board = this.getBoard();
var order = board.getOrder();
var u_vec = this._getNodeOrderVector(src_node, order);
var v_vec = this._getNodeOrderVector(dst_node, order);
return board.compareVectors(u_vec, v_vec);
},
_getNodeOrderVector: function(node, order) {
var board = this.getBoard();
var data = JX.Stratcom.getData(node);
if (data.objectPHID) {
return this._getOrderVector(data.objectPHID, order);
}
return board.getHeaderTemplate(data.headerKey).getVector();
},
setIsDropTarget: function(is_target) {
var node = this.getWorkpanelNode();
JX.DOM.alterClass(node, 'workboard-column-drop-target', is_target);
},
_getCardsSortedByKey: function(order) {
var cards = this.getCards();
var list = [];
for (var k in cards) {
list.push(cards[k]);
}
list.sort(JX.bind(this, this._sortCards, order));
return list;
},
_sortCards: function(order, u, v) {
var board = this.getBoard();
var u_vec = this._getOrderVector(u.getPHID(), order);
var v_vec = this._getOrderVector(v.getPHID(), order);
return board.compareVectors(u_vec, v_vec);
},
_getOrderVector: function(phid, order) {
var board = this.getBoard();
if (!this._orderVectors) {
this._orderVectors = {};
}
if (!this._orderVectors[order]) {
var cards = this.getCards();
var vectors = {};
for (var k in cards) {
var card_phid = cards[k].getPHID();
var vector = board.getCardTemplate(card_phid)
.getSortVector(order);
vectors[card_phid] = [].concat(vector);
// Push a "card" type, so cards always sort after headers; headers
// have a "0" in this position.
vectors[card_phid].push(1);
}
for (var ii = 0; ii < this._naturalOrder.length; ii++) {
var natural_phid = this._naturalOrder[ii];
if (vectors[natural_phid]) {
vectors[natural_phid].push(ii);
}
}
this._orderVectors[order] = vectors;
}
if (!this._orderVectors[order][phid]) {
// In this case, we're comparing a card being dragged in from another
// column to the cards already in this column. We're just going to
// build a temporary vector for it.
var incoming_vector = board.getCardTemplate(phid)
.getSortVector(order);
incoming_vector = [].concat(incoming_vector);
// Add a "card" type to sort this after headers.
incoming_vector.push(1);
// Add a "0" for the natural ordering to put this on top. A new card
// has no natural ordering on a column it isn't part of yet.
incoming_vector.push(0);
return incoming_vector;
}
return this._orderVectors[order][phid];
},
_redrawFrame: function() {
var cards = this.getCards();
var board = this.getBoard();
var points = {};
var count = 0;
var decimal_places = 0;
for (var phid in cards) {
var card = cards[phid];
var card_points;
if (board.getPointsEnabled()) {
card_points = card.getPoints();
} else {
card_points = 1;
}
if (card_points !== null) {
var status = card.getStatus();
if (!points[status]) {
points[status] = 0;
}
points[status] += card_points;
// Count the number of decimal places in the point value with the
// most decimal digits. We'll use the same precision when rendering
// the point sum. This avoids rounding errors and makes the display
// a little more consistent.
var parts = card_points.toString().split('.');
if (parts[1]) {
decimal_places = Math.max(decimal_places, parts[1].length);
}
}
count++;
}
var total_points = 0;
for (var k in points) {
total_points += points[k];
}
total_points = total_points.toFixed(decimal_places);
var limit = this.getPointLimit();
var display_value;
if (limit !== null && limit !== 0) {
display_value = total_points + ' / ' + limit;
} else {
display_value = total_points;
}
if (board.getPointsEnabled()) {
display_value = count + ' | ' + display_value;
}
var over_limit = ((limit !== null) && (total_points > limit));
var content_node = this.getPointsContentNode();
var points_node = this.getPointsNode();
JX.DOM.setContent(content_node, display_value);
var is_empty = !this.getCardPHIDs().length;
var panel = JX.DOM.findAbove(this.getRoot(), 'div', 'workpanel');
JX.DOM.alterClass(panel, 'project-panel-empty', is_empty);
JX.DOM.alterClass(panel, 'project-panel-over-limit', over_limit);
var color_map = {
'phui-tag-disabled': (total_points === 0),
'phui-tag-blue': (total_points > 0 && !over_limit),
'phui-tag-red': (over_limit)
};
for (var c in color_map) {
JX.DOM.alterClass(points_node, c, !!color_map[c]);
}
JX.DOM.show(points_node);
}
}
});