1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-22 05:20:56 +01:00

Rewrite workboards to have way more bugs

Summary:
Ref T4900. Briefly:

  - Much more layout and rendering is now done in Javascript.
  - This should otherwise be identical to the behavior at HEAD, except that:
    - editing a task and removing the current board from it no longer removes the task; and
    - points still don't work.

However, this can now plausibly support realtime workboard updates and other complex state-based behaviors like points calculations in a future change.

Test Plan:
  - Changed card covers.
  - Moved cards.
  - Sorted board by priority and natural.
  - Added new cards.
  - Edited cards in place.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T4900

Differential Revision: https://secure.phabricator.com/D15234
This commit is contained in:
epriestley 2016-02-10 05:59:46 -08:00
parent 01084bfe22
commit 0bf3519045
15 changed files with 936 additions and 542 deletions

View file

@ -415,8 +415,11 @@ return array(
'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
'rsrc/js/application/projects/Workboard.js' => '088b2495',
'rsrc/js/application/projects/behavior-project-boards.js' => '37eb99e4',
'rsrc/js/application/projects/WorkboardBoard.js' => '069d6dd3',
'rsrc/js/application/projects/WorkboardCard.js' => '2fcefa17',
'rsrc/js/application/projects/WorkboardColumn.js' => 'e8f303bb',
'rsrc/js/application/projects/WorkboardController.js' => 'fa1378c3',
'rsrc/js/application/projects/behavior-project-boards.js' => 'e1b56d72',
'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
@ -656,7 +659,7 @@ return array(
'javelin-behavior-phui-profile-menu' => '12884df9',
'javelin-behavior-policy-control' => 'd0c516d5',
'javelin-behavior-policy-rule-editor' => '5e9f347c',
'javelin-behavior-project-boards' => '37eb99e4',
'javelin-behavior-project-boards' => 'e1b56d72',
'javelin-behavior-project-create' => '065227cc',
'javelin-behavior-quicksand-blacklist' => '7927a7d3',
'javelin-behavior-recurring-edit' => '5f1c4d5f',
@ -723,7 +726,10 @@ return array(
'javelin-view-renderer' => '6c2b09a2',
'javelin-view-visitor' => 'efe49472',
'javelin-websocket' => 'e292eaf4',
'javelin-workboard' => '088b2495',
'javelin-workboard-board' => '069d6dd3',
'javelin-workboard-card' => '2fcefa17',
'javelin-workboard-column' => 'e8f303bb',
'javelin-workboard-controller' => 'fa1378c3',
'javelin-workflow' => '5b2e3e2b',
'lightbox-attachment-css' => '7acac05d',
'maniphest-batch-editor' => 'b0f0b6d5',
@ -913,6 +919,15 @@ return array(
'javelin-stratcom',
'javelin-workflow',
),
'069d6dd3' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
'javelin-workboard-column',
),
'06c32383' => array(
'javelin-behavior',
'javelin-typeahead-ondemand-source',
@ -930,16 +945,6 @@ return array(
'javelin-stratcom',
'javelin-vector',
),
'088b2495' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
'phabricator-drag-and-drop-file-upload',
),
'0a3f3021' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1077,6 +1082,9 @@ return array(
'2ee659ce' => array(
'javelin-install',
),
'2fcefa17' => array(
'javelin-install',
),
'327a00d1' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1096,17 +1104,6 @@ return array(
'javelin-vector',
'phuix-autocomplete',
),
'37eb99e4' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
'phabricator-drag-and-drop-file-upload',
'javelin-workboard',
),
'3ab51e2c' => array(
'javelin-behavior',
'javelin-behavior-device',
@ -1937,6 +1934,15 @@ return array(
'javelin-dom',
'phabricator-prefab',
),
'e1b56d72' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'javelin-workboard-controller',
),
'e1d25dfb' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2004,6 +2010,10 @@ return array(
'e6e25838' => array(
'javelin-install',
),
'e8f303bb' => array(
'javelin-install',
'javelin-workboard-card',
),
'e9581f08' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2088,6 +2098,16 @@ return array(
'javelin-vector',
'javelin-magical-init',
),
'fa1378c3' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'phabricator-drag-and-drop-file-upload',
'javelin-workboard-board',
),
'fb20ac8d' => array(
'javelin-behavior',
'javelin-aphlict',

View file

@ -1820,6 +1820,7 @@ phutil_register_library_map(array(
'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php',
'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php',
'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php',
'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php',
'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
@ -6058,6 +6059,7 @@ phutil_register_library_map(array(
'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorBoardLayoutEngine' => 'Phobject',
'PhabricatorBoardRenderingEngine' => 'Phobject',
'PhabricatorBoardResponseEngine' => 'Phobject',
'PhabricatorBot' => 'PhabricatorDaemon',
'PhabricatorBotChannel' => 'PhabricatorBotTarget',
'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',

View file

@ -9,6 +9,7 @@ final class ManiphestTaskEditController extends ManiphestController {
->addContextParameter('responseType')
->addContextParameter('columnPHID')
->addContextParameter('order')
->addContextParameter('visiblePHIDs')
->buildResponse();
}

View file

@ -289,7 +289,11 @@ final class ManiphestEditEngine
$viewer = $request->getViewer();
$column_phid = $request->getStr('columnPHID');
$order = $request->getStr('order');
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
@ -299,98 +303,15 @@ final class ManiphestEditEngine
return new Aphront404Response();
}
// If the workboard's project and all descendant projects have been removed
// from the card's project list, we are going to remove it from the board
// completely.
// TODO: If the user did something sneaky and changed a subproject, we'll
// currently leave the card where it was but should really move it to the
// proper new column.
$board_phid = $column->getProjectPHID();
$object_phid = $task->getPHID();
$descendant_projects = id(new PhabricatorProjectQuery())
return id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->withAncestorProjectPHIDs(array($column->getProjectPHID()))
->execute();
$board_phids = mpull($descendant_projects, 'getPHID', 'getPHID');
$board_phids[$board_phid] = $board_phid;
$project_map = array_fuse($task->getProjectPHIDs());
$remove_card = !array_intersect_key($board_phids, $project_map);
// TODO: Maybe the caller should pass a list of visible task PHIDs so we
// know which ones we need to reorder? This is a HUGE overfetch.
$objects = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($board_phids))
->setViewer($viewer)
->execute();
$objects = mpull($objects, null, 'getPHID');
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs(array_keys($objects))
->executeLayout();
$positions = $layout_engine->getColumnObjectPositions(
$board_phid,
$column_phid);
$column_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$column_phid);
$column_tasks = array_select_keys($objects, $column_phids);
if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
// TODO: This is a little bit awkward, because PHP and JS use
// slightly different sort order parameters to achieve the same
// effect. It would be good to unify this a bit at some point.
$sort_map = array();
foreach ($positions as $position) {
$sort_map[$position->getObjectPHID()] = array(
-$position->getSequence(),
$position->getID(),
);
}
} else {
$sort_map = mpull(
$column_tasks,
'getPrioritySortVector',
'getPHID');
}
$data = array(
'removeFromBoard' => $remove_card,
'sortMap' => $sort_map,
);
$rendering_engine = id(new PhabricatorBoardRenderingEngine())
->setViewer($viewer)
->setObjects(array($task))
->setExcludedProjectPHIDs($board_phids);
$card = $rendering_engine->renderCard($task->getPHID());
$item = $card->getItem();
$item->addClass('phui-workcard');
$payload = array(
'tasks' => $item,
'data' => $data,
);
return id(new AphrontAjaxResponse())
->setContent(
array(
'tasks' => $item,
'data' => $data,
));
->setBoardPHID($board_phid)
->setObjectPHID($object_phid)
->setVisiblePHIDs($visible_phids)
->buildResponse();
}

View file

@ -194,14 +194,6 @@ final class ManiphestTask extends ManiphestDAO
return ManiphestTaskStatus::isClosedStatus($this->getStatus());
}
public function getPrioritySortVector() {
return array(
$this->getPriority(),
-$this->getSubpriority(),
$this->getID(),
);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
@ -219,6 +211,16 @@ final class ManiphestTask extends ManiphestDAO
return idx($this->properties, 'cover.thumbnailPHID');
}
public function getWorkboardOrderVectors() {
return array(
PhabricatorProjectColumn::ORDER_PRIORITY => array(
(int)-$this->getPriority(),
(double)-$this->getSubpriority(),
(int)-$this->getID(),
),
);
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */

View file

@ -238,20 +238,6 @@ final class PhabricatorProjectBoardViewController
'boardPHID' => $project->getPHID(),
));
$behavior_config = array(
'boardID' => $board_id,
'projectPHID' => $project->getPHID(),
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
'createURI' => $this->getCreateURI(),
'uploadURI' => '/file/dropupload/',
'coverURI' => $this->getApplicationURI('cover/'),
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
'order' => $this->sortKey,
);
$this->initBehavior(
'project-boards',
$behavior_config);
$visible_columns = array();
$column_phids = array();
$visible_phids = array();
@ -297,6 +283,9 @@ final class PhabricatorProjectBoardViewController
->setEditMap($task_can_edit_map)
->setExcludedProjectPHIDs($select_phids);
$templates = array();
$column_maps = array();
$all_tasks = array();
foreach ($visible_columns as $column_phid => $column) {
$column_tasks = $column_phids[$column_phid];
@ -356,14 +345,35 @@ final class PhabricatorProjectBoardViewController
));
foreach ($column_tasks as $task) {
$card = $rendering_engine->renderCard($task->getPHID());
$cards->addItem($card->getItem());
$object_phid = $task->getPHID();
$card = $rendering_engine->renderCard($object_phid);
$templates[$object_phid] = hsprintf('%s', $card->getItem());
$column_maps[$column_phid][] = $object_phid;
$all_tasks[$object_phid] = $task;
}
$panel->setCards($cards);
$board->addPanel($panel);
}
$behavior_config = array(
'boardID' => $board_id,
'projectPHID' => $project->getPHID(),
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
'createURI' => $this->getCreateURI(),
'uploadURI' => '/file/dropupload/',
'coverURI' => $this->getApplicationURI('cover/'),
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
'order' => $this->sortKey,
'templateMap' => $templates,
'columnMaps' => $column_maps,
'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'),
);
$this->initBehavior('project-boards', $behavior_config);
$sort_menu = $this->buildSortMenu(
$viewer,
$this->sortKey);

View file

@ -150,51 +150,18 @@ abstract class PhabricatorProjectController extends PhabricatorController {
protected function newCardResponse($board_phid, $object_phid) {
$viewer = $this->getViewer();
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs(array($board_phid))
->executeOne();
if (!$project) {
return new Aphront404Response();
$request = $this->getRequest();
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
// Reload the object so it reflects edits which have been applied.
$object = id(new ManiphestTaskQuery())
return id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->needProjectPHIDs(true)
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$except_phids = array($board_phid);
if ($project->getHasSubprojects() || $project->getHasMilestones()) {
$descendants = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withAncestorProjectPHIDs($except_phids)
->execute();
foreach ($descendants as $descendant) {
$except_phids[] = $descendant->getPHID();
}
}
$rendering_engine = id(new PhabricatorBoardRenderingEngine())
->setViewer($viewer)
->setObjects(array($object))
->setExcludedProjectPHIDs($except_phids);
$card = $rendering_engine->renderCard($object->getPHID());
$item = $card->getItem();
$item->addClass('phui-workcard');
return id(new AphrontAjaxResponse())
->setContent(
array(
'objectPHID' => $object->getPHID(),
'cardHTML' => $item,
));
->setBoardPHID($board_phid)
->setObjectPHID($object_phid)
->setVisiblePHIDs($visible_phids)
->buildResponse();
}
}

View file

@ -0,0 +1,146 @@
<?php
final class PhabricatorBoardResponseEngine extends Phobject {
private $viewer;
private $boardPHID;
private $objectPHID;
private $visiblePHIDs;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setBoardPHID($board_phid) {
$this->boardPHID = $board_phid;
return $this;
}
public function getBoardPHID() {
return $this->boardPHID;
}
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setVisiblePHIDs(array $visible_phids) {
$this->visiblePHIDs = $visible_phids;
return $this;
}
public function getVisiblePHIDs() {
return $this->visiblePHIDs;
}
public function buildResponse() {
$viewer = $this->getViewer();
$object_phid = $this->getObjectPHID();
$board_phid = $this->getBoardPHID();
// Load all the other tasks that are visible in the affected columns and
// perform layout for them.
$visible_phids = $this->getAllVisiblePHIDs();
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs($visible_phids)
->executeLayout();
$object_columns = $layout_engine->getObjectColumns(
$board_phid,
$object_phid);
$natural = array();
foreach ($object_columns as $column_phid => $column) {
$column_object_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$column_phid);
$natural[$column_phid] = array_values($column_object_phids);
}
$all_visible = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs($visible_phids)
->execute();
$order_maps = array();
foreach ($all_visible as $visible) {
$order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors();
}
$template = $this->buildTemplate();
$payload = array(
'objectPHID' => $object_phid,
'cardHTML' => $template,
'columnMaps' => $natural,
'orderMaps' => $order_maps,
);
return id(new AphrontAjaxResponse())
->setContent($payload);
}
private function buildTemplate() {
$viewer = $this->getViewer();
$object_phid = $this->getObjectPHID();
$excluded_phids = $this->loadExcludedProjectPHIDs();
$object = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->needProjectPHIDs(true)
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$rendering_engine = id(new PhabricatorBoardRenderingEngine())
->setViewer($viewer)
->setObjects(array($object))
->setExcludedProjectPHIDs($excluded_phids);
$card = $rendering_engine->renderCard($object_phid);
return hsprintf('%s', $card->getItem());
}
private function loadExcludedProjectPHIDs() {
$viewer = $this->getViewer();
$board_phid = $this->getBoardPHID();
$exclude_phids = array($board_phid);
$descendants = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withAncestorProjectPHIDs($exclude_phids)
->execute();
foreach ($descendants as $descendant) {
$exclude_phids[] = $descendant->getPHID();
}
return array_fuse($exclude_phids);
}
private function getAllVisiblePHIDs() {
$visible_phids = $this->getVisiblePHIDs();
$visible_phids[] = $this->getObjectPHID();
$visible_phids = array_fuse($visible_phids);
return $visible_phids;
}
}

View file

@ -78,10 +78,6 @@ final class ProjectBoardTaskCard extends Phobject {
->setHref('/T'.$task->getID())
->addSigil('project-card')
->setDisabled($task->isClosed())
->setMetadata(
array(
'objectPHID' => $task->getPHID(),
))
->addAction(
id(new PHUIListItemView())
->setName(pht('Edit'))
@ -115,6 +111,8 @@ final class ProjectBoardTaskCard extends Phobject {
$card->addAttribute($tag_list);
}
$card->addClass('phui-workcard');
return $card;
}

View file

@ -1,242 +0,0 @@
/**
* @provides javelin-workboard
* @requires javelin-install
* javelin-dom
* javelin-util
* javelin-vector
* javelin-stratcom
* javelin-workflow
* phabricator-draggable-list
* phabricator-drag-and-drop-file-upload
* @javelin
*/
JX.install('Workboard', {
construct: function(config) {
this._config = config;
this._boardNodes = {};
this._columnMap = {};
},
properties: {
uploadURI: null,
coverURI: null,
moveURI: null,
chunkThreshold: null
},
members: {
_config: null,
_boardNodes: null,
_currentBoard: null,
_panOrigin: null,
_panNode: null,
_panX: null,
_columnMap: null,
start: function() {
this._setupCoverImageHandlers();
this._setupPanHandlers();
return this;
},
addBoard: function(board_phid, board_node) {
this._currentBoard = board_phid;
this._boardNodes[board_phid] = board_node;
this._setupDragHandlers(board_node);
},
_getConfig: function() {
return this._config;
},
_setupCoverImageHandlers: function() {
if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) {
return;
}
var drop = new JX.PhabricatorDragAndDropFileUpload('project-card')
.setURI(this.getUploadURI())
.setChunkThreshold(this.getChunkThreshold());
drop.listen('didBeginDrag', function(node) {
JX.DOM.alterClass(node, 'phui-workcard-upload-target', true);
});
drop.listen('didEndDrag', function(node) {
JX.DOM.alterClass(node, 'phui-workcard-upload-target', false);
});
drop.listen('didUpload', JX.bind(this, this._oncoverupload));
drop.start();
},
_oncoverupload: function(file) {
var node = file.getTargetNode();
var board = JX.DOM.findAbove(node, 'div', 'jx-workboard');
var data = {
boardPHID: JX.Stratcom.getData(board).boardPHID,
objectPHID: JX.Stratcom.getData(node).objectPHID,
filePHID: file.getPHID()
};
new JX.Workflow(this.getCoverURI(), data)
.setHandler(JX.bind(this, this._queueCardUpdate))
.start();
},
_setupPanHandlers: function() {
var mousedown = JX.bind(this, this._onpanmousedown);
var mousemove = JX.bind(this, this._onpanmousemove);
var mouseup = JX.bind(this, this._onpanmouseup);
JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown);
JX.Stratcom.listen('mousemove', null, mousemove);
JX.Stratcom.listen('mouseup', null, mouseup);
},
_onpanmousedown: function(e) {
if (!JX.Device.isDesktop()) {
return;
}
if (e.getNode('workpanel')) {
return;
}
if (JX.Stratcom.pass()) {
return;
}
e.kill();
this._panOrigin = JX.$V(e);
this._panNode = e.getNode('workboard-shadow');
this._panX = this._panNode.scrollLeft;
},
_onpanmousemove: function(e) {
if (!this._panOrigin) {
return;
}
var cursor = JX.$V(e);
this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x);
},
_onpanmouseup: function() {
this._panOrigin = null;
},
_setupDragHandlers: function(board_node) {
var columns = this._findBoardColumns(board_node);
var column;
var ii;
var lists = [];
for (ii = 0; ii < columns.length; ii++) {
column = columns[ii];
var list = new JX.DraggableList('project-card', column)
.setOuterContainer(board_node)
.setFindItemsHandler(JX.bind(this, this._findCardsInColumn, column))
.setCanDragX(true)
.setHasInfiniteHeight(true);
// TODO: Restore these behaviors.
// list.listen('didSend', JX.bind(list, onupdate, cols[ii]));
// list.listen('didReceive', JX.bind(list, onupdate, cols[ii]));
// onupdate(cols[ii]);
list.listen('didDrop', JX.bind(this, this._onmovecard, list));
lists.push(list);
}
for (ii = 0; ii < lists.length; ii++) {
lists[ii].setGroup(lists);
}
},
_findBoardColumns: function(board_node) {
return JX.DOM.scry(board_node, 'ul', 'project-column');
},
_findCardsInColumn: function(column_node) {
return JX.DOM.scry(column_node, 'li', 'project-card');
},
_onmovecard: function(list, item, after_node) {
list.lock();
JX.DOM.alterClass(item, 'drag-sending', true);
var item_phid = JX.Stratcom.getData(item).objectPHID;
var data = {
objectPHID: item_phid,
columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID
};
if (after_node) {
data.afterPHID = JX.Stratcom.getData(after_node).objectPHID;
}
var before_node = item.nextSibling;
if (before_node) {
var before_phid = JX.Stratcom.getData(before_node).objectPHID;
if (before_phid) {
data.beforePHID = before_phid;
}
}
// TODO: This should be managed per-board.
var config = this._getConfig();
data.order = config.order;
new JX.Workflow(this.getMoveURI(), data)
.setHandler(JX.bind(this, this._oncardupdate, item, list))
.start();
},
_oncardupdate: function(item, list, response) {
list.unlock();
JX.DOM.alterClass(item, 'drag-sending', false);
this._queueCardUpdate(response);
},
_queueCardUpdate: function(response) {
var board_node = this._boardNodes[this._currentBoard];
var columns = this._findBoardColumns(board_node);
var cards;
var ii;
var jj;
var data;
for (ii = 0; ii < columns.length; ii++) {
cards = this._findCardsInColumn(columns[ii]);
for (jj = 0; jj < cards.length; jj++) {
data = JX.Stratcom.getData(cards[jj]);
if (data.objectPHID == response.objectPHID) {
this._replaceCard(cards[jj], JX.$H(response.cardHTML));
}
}
}
},
_replaceCard: function(old_node, new_node) {
JX.DOM.replace(old_node, new_node);
}
}
});

View file

@ -0,0 +1,221 @@
/**
* @provides javelin-workboard-board
* @requires javelin-install
* javelin-dom
* javelin-util
* javelin-stratcom
* javelin-workflow
* phabricator-draggable-list
* javelin-workboard-column
* @javelin
*/
JX.install('WorkboardBoard', {
construct: function(controller, phid, root) {
this._controller = controller;
this._phid = phid;
this._root = root;
this._templates = {};
this._orderMaps = {};
this._buildColumns();
},
properties: {
order: null,
},
members: {
_controller: null,
_phid: null,
_root: null,
_columns: null,
_templates: null,
_orderMaps: null,
getRoot: function() {
return this._root;
},
getColumns: function() {
return this._columns;
},
getColumn: function(k) {
return this._columns[k];
},
getPHID: function() {
return this._phid;
},
setCardTemplate: function(phid, template) {
this._templates[phid] = template;
return this;
},
getCardTemplate: function(phid) {
return this._templates[phid];
},
getController: function() {
return this._controller;
},
setOrderMap: function(phid, map) {
this._orderMaps[phid] = map;
return this;
},
getOrderVector: function(phid, key) {
return this._orderMaps[phid][key];
},
start: function() {
this._setupDragHandlers();
for (var k in this._columns) {
this._columns[k].redraw();
}
},
_buildColumns: function() {
var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column');
this._columns = {};
for (var ii = 0; ii < nodes.length; ii++) {
var node = nodes[ii];
var data = JX.Stratcom.getData(node);
var phid = data.columnPHID;
this._columns[phid] = new JX.WorkboardColumn(this, phid, node);
}
},
_setupDragHandlers: function() {
var columns = this.getColumns();
var lists = [];
for (var k in columns) {
var column = columns[k];
var list = new JX.DraggableList('project-card', column.getRoot())
.setOuterContainer(this.getRoot())
.setFindItemsHandler(JX.bind(column, column.getCardNodes))
.setCanDragX(true)
.setHasInfiniteHeight(true);
list.listen('didDrop', JX.bind(this, this._onmovecard, list));
lists.push(list);
}
for (var ii = 0; ii < lists.length; ii++) {
lists[ii].setGroup(lists);
}
},
_findCardsInColumn: function(column_node) {
return JX.DOM.scry(column_node, 'li', 'project-card');
},
_onmovecard: function(list, item, after_node, src_list) {
list.lock();
JX.DOM.alterClass(item, 'drag-sending', true);
var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;
var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID;
var item_phid = JX.Stratcom.getData(item).objectPHID;
var data = {
objectPHID: item_phid,
columnPHID: dst_phid,
order: this.getOrder()
};
if (after_node) {
data.afterPHID = JX.Stratcom.getData(after_node).objectPHID;
}
var before_node = item.nextSibling;
if (before_node) {
var before_phid = JX.Stratcom.getData(before_node).objectPHID;
if (before_phid) {
data.beforePHID = before_phid;
}
}
var visible_phids = [];
var column = this.getColumn(dst_phid);
for (var object_phid in column.getCards()) {
visible_phids.push(object_phid);
}
data.visiblePHIDs = visible_phids.join(',');
var onupdate = JX.bind(
this,
this._oncardupdate,
list,
src_phid,
dst_phid,
data.afterPHID);
new JX.Workflow(this.getController().getMoveURI(), data)
.setHandler(onupdate)
.start();
},
_oncardupdate: function(list, src_phid, dst_phid, after_phid, response) {
var src_column = this.getColumn(src_phid);
var dst_column = this.getColumn(dst_phid);
var card = src_column.removeCard(response.objectPHID);
dst_column.addCard(card, after_phid);
this.updateCard(response);
list.unlock();
},
updateCard: function(response) {
var columns = this.getColumns();
var phid = response.objectPHID;
if (!this._templates[phid]) {
for (var add_phid in response.columnMaps) {
this.getColumn(add_phid).newCard(phid);
}
}
this.setCardTemplate(phid, response.cardHTML);
var order_maps = response.orderMaps;
for (var order_phid in order_maps) {
this.setOrderMap(order_phid, order_maps[order_phid]);
}
var column_maps = response.columnMaps;
for (var natural_phid in column_maps) {
this.getColumn(natural_phid).setNaturalOrder(column_maps[natural_phid]);
}
for (var column_phid in columns) {
var cards = columns[column_phid].getCards();
for (var object_phid in cards) {
if (object_phid !== phid) {
continue;
}
var card = cards[object_phid];
card.redraw();
}
columns[column_phid].redraw();
}
}
}
});

View file

@ -0,0 +1,56 @@
/**
* @provides javelin-workboard-card
* @requires javelin-install
* @javelin
*/
JX.install('WorkboardCard', {
construct: function(column, phid) {
this._column = column;
this._phid = phid;
},
members: {
_column: null,
_phid: null,
_root: null,
getPHID: function() {
return this._phid;
},
getColumn: function() {
return this._column;
},
setColumn: function(column) {
this._column = column;
},
getNode: function() {
if (!this._root) {
var phid = this.getPHID();
var template = this.getColumn().getBoard().getCardTemplate(phid);
this._root = JX.$H(template).getFragment().firstChild;
JX.Stratcom.getData(this._root).objectPHID = this.getPHID();
}
return this._root;
},
redraw: function() {
var old_node = this._root;
this._root = null;
var new_node = this.getNode();
if (old_node && old_node.parentNode) {
JX.DOM.replace(old_node, new_node);
}
return this;
}
}
});

View file

@ -0,0 +1,177 @@
/**
* @provides javelin-workboard-column
* @requires javelin-install
* javelin-workboard-card
* @javelin
*/
JX.install('WorkboardColumn', {
construct: function(board, phid, root) {
this._board = board;
this._phid = phid;
this._root = root;
this._cards = {};
this._naturalOrder = [];
},
members: {
_phid: null,
_root: null,
_board: null,
_cards: null,
_naturalOrder: null,
getPHID: function() {
return this._phid;
},
getRoot: function() {
return this._root;
},
getCards: function() {
return this._cards;
},
getCard: function(phid) {
return this._cards[phid];
},
getBoard: function() {
return this._board;
},
setNaturalOrder: function(order) {
this._naturalOrder = order;
return this;
},
newCard: function(phid) {
var card = new JX.WorkboardCard(this, phid);
this._cards[phid] = card;
this._naturalOrder.push(phid);
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);
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);
}
return this;
},
getCardNodes: function() {
var cards = this.getCards();
var nodes = [];
for (var k in cards) {
nodes.push(cards[k].getNode());
}
return nodes;
},
getCardPHIDs: function() {
return JX.keys(this.getCards());
},
redraw: function() {
var order = this.getBoard().getOrder();
var list;
if (order == 'natural') {
list = this._getCardsSortedNaturally();
} else {
list = this._getCardsSortedByKey(order);
}
var content = [];
for (var ii = 0; ii < list.length; ii++) {
var node = list[ii].getNode();
content.push(node);
}
JX.DOM.setContent(this.getRoot(), content);
},
_getCardsSortedNaturally: function() {
var list = [];
for (var ii = 0; ii < this._naturalOrder.length; ii++) {
var phid = this._naturalOrder[ii];
list.push(this.getCard(phid));
}
return list;
},
_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 ud = this.getBoard().getOrderVector(u.getPHID(), order);
var vd = this.getBoard().getOrderVector(v.getPHID(), order);
for (var ii = 0; ii < ud.length; ii++) {
if (ud[ii] > vd[ii]) {
return 1;
}
if (ud[ii] < vd[ii]) {
return -1;
}
}
return 0;
}
}
});

View file

@ -0,0 +1,201 @@
/**
* @provides javelin-workboard-controller
* @requires javelin-install
* javelin-dom
* javelin-util
* javelin-vector
* javelin-stratcom
* javelin-workflow
* phabricator-drag-and-drop-file-upload
* javelin-workboard-board
* @javelin
*/
JX.install('WorkboardController', {
construct: function() {
this._boards = {};
},
properties: {
uploadURI: null,
coverURI: null,
moveURI: null,
createURI: null,
chunkThreshold: null
},
members: {
_boards: null,
_panOrigin: null,
_panNode: null,
_panX: null,
start: function() {
this._setupCoverImageHandlers();
this._setupPanHandlers();
this._setupEditHandlers();
return this;
},
newBoard: function(phid, node) {
var board = new JX.WorkboardBoard(this, phid, node);
this._boards[phid] = board;
return board;
},
_getBoard: function(board_phid) {
return this._boards[board_phid];
},
_setupCoverImageHandlers: function() {
if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) {
return;
}
var drop = new JX.PhabricatorDragAndDropFileUpload('project-card')
.setURI(this.getUploadURI())
.setChunkThreshold(this.getChunkThreshold());
drop.listen('didBeginDrag', function(node) {
JX.DOM.alterClass(node, 'phui-workcard-upload-target', true);
});
drop.listen('didEndDrag', function(node) {
JX.DOM.alterClass(node, 'phui-workcard-upload-target', false);
});
drop.listen('didUpload', JX.bind(this, this._oncoverupload));
drop.start();
},
_oncoverupload: function(file) {
var node = file.getTargetNode();
var board = this._getBoardFromNode(node);
var column_node = JX.DOM.findAbove(node, 'ul', 'project-column');
var column_phid = JX.Stratcom.getData(column_node).columnPHID;
var column = board.getColumn(column_phid);
var data = {
boardPHID: board.getPHID(),
objectPHID: JX.Stratcom.getData(node).objectPHID,
filePHID: file.getPHID(),
visiblePHIDs: column.getCardPHIDs()
};
new JX.Workflow(this.getCoverURI(), data)
.setHandler(JX.bind(board, board.updateCard))
.start();
},
_getBoardFromNode: function(node) {
var board_node = JX.DOM.findAbove(node, 'div', 'jx-workboard');
var board_phid = JX.Stratcom.getData(board_node).boardPHID;
return this._getBoard(board_phid);
},
_setupPanHandlers: function() {
var mousedown = JX.bind(this, this._onpanmousedown);
var mousemove = JX.bind(this, this._onpanmousemove);
var mouseup = JX.bind(this, this._onpanmouseup);
JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown);
JX.Stratcom.listen('mousemove', null, mousemove);
JX.Stratcom.listen('mouseup', null, mouseup);
},
_onpanmousedown: function(e) {
if (!JX.Device.isDesktop()) {
return;
}
if (e.getNode('workpanel')) {
return;
}
if (JX.Stratcom.pass()) {
return;
}
e.kill();
this._panOrigin = JX.$V(e);
this._panNode = e.getNode('workboard-shadow');
this._panX = this._panNode.scrollLeft;
},
_onpanmousemove: function(e) {
if (!this._panOrigin) {
return;
}
var cursor = JX.$V(e);
this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x);
},
_onpanmouseup: function() {
this._panOrigin = null;
},
_setupEditHandlers: function() {
var onadd = JX.bind(this, this._onaddcard);
var onedit = JX.bind(this, this._oneditcard);
JX.Stratcom.listen('click', 'column-add-task', onadd);
JX.Stratcom.listen('click', 'edit-project-card', onedit);
},
_onaddcard: function(e) {
// We want the 'boards-dropdown-menu' behavior to see this event and
// close the dropdown, but don't want to follow the link.
e.prevent();
var column_data = e.getNodeData('column-add-task');
var column_phid = column_data.columnPHID;
var board_phid = column_data.projectPHID;
var board = this._getBoard(board_phid);
var column = board.getColumn(column_phid);
var request_data = {
responseType: 'card',
columnPHID: column.getPHID(),
projects: board.getPHID(),
visiblePHIDs: column.getCardPHIDs(),
order: board.getOrder()
};
new JX.Workflow(this.getCreateURI(), request_data)
.setHandler(JX.bind(board, board.updateCard))
.start();
},
_oneditcard: function(e) {
e.kill();
var column_node = e.getNode('project-column');
var column_phid = JX.Stratcom.getData(column_node).columnPHID;
var board = this._getBoardFromNode(column_node);
var column = board.getColumn(column_phid);
var request_data = {
responseType: 'card',
columnPHID: column.getPHID(),
visiblePHIDs: column.getCardPHIDs(),
order: board.getOrder()
};
new JX.Workflow(e.getNode('tag:a').href, request_data)
.setHandler(JX.bind(board, board.updateCard))
.start();
}
}
});

View file

@ -6,9 +6,7 @@
* javelin-vector
* javelin-stratcom
* javelin-workflow
* phabricator-draggable-list
* phabricator-drag-and-drop-file-upload
* javelin-workboard
* javelin-workboard-controller
*/
JX.behavior('project-boards', function(config, statics) {
@ -61,66 +59,6 @@ JX.behavior('project-boards', function(config, statics) {
}
}
function colsort(u, v) {
var ud = JX.Stratcom.getData(u).sort || [];
var vd = JX.Stratcom.getData(v).sort || [];
for (var ii = 0; ii < ud.length; ii++) {
if (parseInt(ud[ii]) < parseInt(vd[ii])) {
return 1;
}
if (parseInt(ud[ii]) > parseInt(vd[ii])) {
return -1;
}
}
return 0;
}
function onedit(column, r) {
var new_card = JX.$H(r.tasks).getNode();
var new_data = JX.Stratcom.getData(new_card);
var items = finditems(column);
var edited = false;
var remove_index = null;
for (var ii = 0; ii < items.length; ii++) {
var item = items[ii];
var data = JX.Stratcom.getData(item);
var phid = data.objectPHID;
if (phid == new_data.objectPHID) {
if (r.data.removeFromBoard) {
remove_index = ii;
}
items[ii] = new_card;
data = new_data;
edited = true;
}
data.sort = r.data.sortMap[data.objectPHID] || data.sort;
}
// this is an add then...!
if (!edited) {
items[items.length + 1] = new_card;
new_data.sort = r.data.sortMap[new_data.objectPHID] || new_data.sort;
}
if (remove_index !== null) {
items.splice(remove_index, 1);
}
items.sort(colsort);
JX.DOM.setContent(column, items);
onupdate(column);
};
function update_statics(update_config) {
statics.boardID = update_config.boardID;
statics.projectPHID = update_config.projectPHID;
@ -130,56 +68,6 @@ JX.behavior('project-boards', function(config, statics) {
}
function setup() {
JX.Stratcom.listen(
'click',
['edit-project-card'],
function(e) {
e.kill();
var column = e.getNode('project-column');
var request_data = {
responseType: 'card',
columnPHID: JX.Stratcom.getData(column).columnPHID,
order: statics.order
};
new JX.Workflow(e.getNode('tag:a').href, request_data)
.setHandler(JX.bind(null, onedit, column))
.start();
});
JX.Stratcom.listen(
'click',
['column-add-task'],
function (e) {
// We want the 'boards-dropdown-menu' behavior to see this event and
// close the dropdown, but don't want to follow the link.
e.prevent();
var column_data = e.getNodeData('column-add-task');
var column_phid = column_data.columnPHID;
var request_data = {
responseType: 'card',
columnPHID: column_phid,
projects: column_data.projectPHID,
order: statics.order
};
var cols = getcolumns();
var ii;
var column;
for (ii = 0; ii < cols.length; ii++) {
if (JX.Stratcom.getData(cols[ii]).columnPHID == column_phid) {
column = cols[ii];
break;
}
}
new JX.Workflow(statics.createURI, request_data)
.setHandler(JX.bind(null, onedit, column))
.start();
});
JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) {
var data = e.getNodeData('boards-dropdown-menu');
if (data.menu) {
@ -234,14 +122,40 @@ JX.behavior('project-boards', function(config, statics) {
}
if (!statics.workboard) {
statics.workboard = new JX.Workboard(config)
statics.workboard = new JX.WorkboardController()
.setUploadURI(config.uploadURI)
.setCoverURI(config.coverURI)
.setMoveURI(config.moveURI)
.setCreateURI(config.createURI)
.setChunkThreshold(config.chunkThreshold)
.start();
}
statics.workboard.addBoard(config.projectPHID, JX.$(config.boardID));
var board_phid = config.projectPHID;
var board_node = JX.$(config.boardID);
var board = statics.workboard.newBoard(board_phid, board_node)
.setOrder(config.order);
var templates = config.templateMap;
for (var k in templates) {
board.setCardTemplate(k, templates[k]);
}
var column_maps = config.columnMaps;
for (var column_phid in column_maps) {
var column = board.getColumn(column_phid);
var column_map = column_maps[column_phid];
for (var ii = 0; ii < column_map.length; ii++) {
column.newCard(column_map[ii]);
}
}
var order_maps = config.orderMaps;
for (var object_phid in order_maps) {
board.setOrderMap(object_phid, order_maps[object_phid]);
}
board.start();
});