1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-02 19:52:44 +01:00
phorge-phorge/webroot/rsrc/js/application/projects/WorkboardColumn.js
epriestley c020f027bb Add an "Sort by Creation Date" filter to workboards and modularize remaining order behaviors
Summary:
Depends on D20274. Ref T10578. This is en route to an ordering by points, it's just a simpler half-step on the way there.

Allow columns to be sorted by creation date, so the newest tasks rise to the top.

In this ordering you can never reposition cards, since editing a creation date by dragging makes no sense. This will be true of the "points" ordering too (although we could imagine doing something like prompting the user, some day).

Test Plan: Viewed boards by "natural" (allows reordering both when dragging within and between columns), "priority" (reorder only within columns), and "creation date" (reorder never). Dragged cards around between and within columns, got apparently sensible behavior.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T10578

Differential Revision: https://secure.phabricator.com/D20275
2019-03-12 13:46:49 -07:00

471 lines
12 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();
return board.getOrderTemplate(order).getHasHeaders();
},
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();
}
var header_key;
var next;
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) {
header_key = board.getCardTemplate(card.getPHID())
.getHeaderKey(order);
if (!seen_headers[header_key]) {
while (header_keys.length) {
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);
}
// Add any leftover headers at the bottom of the column which don't have
// any cards in them. In particular, empty columns don't have any cards
// but should still have headers.
while (header_keys.length) {
next = header_keys.pop();
if (seen_headers[next]) {
continue;
}
objects.push(this.getHeader(next));
}
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);
// Only put the "empty" style on the column (which just adds some empty
// space so it's easier to drop cards into an empty column) if it has no
// cards and no headers.
var is_empty =
(!this.getCardPHIDs().length) &&
(!this._hasColumnHeaders());
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);
}
}
});