diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -415,8 +415,11 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '2ee659ce' => array( 'javelin-install', ), + '2fcefa17' => array( + 'javelin-install', + ), '327a00d1' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1096,17 +1104,6 @@ '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 @@ '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 @@ 'e6e25838' => array( 'javelin-install', ), + 'e8f303bb' => array( + 'javelin-install', + 'javelin-workboard-card', + ), 'e9581f08' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2088,6 +2098,16 @@ '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', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1820,6 +1820,7 @@ '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 @@ 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorBoardLayoutEngine' => 'Phobject', 'PhabricatorBoardRenderingEngine' => 'Phobject', + 'PhabricatorBoardResponseEngine' => 'Phobject', 'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -9,6 +9,7 @@ ->addContextParameter('responseType') ->addContextParameter('columnPHID') ->addContextParameter('order') + ->addContextParameter('visiblePHIDs') ->buildResponse(); } diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -289,7 +289,11 @@ $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 @@ 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()) - ->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()) + return id(new PhabricatorBoardResponseEngine()) ->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(); } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -194,14 +194,6 @@ 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 @@ 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 )----------------------------------- */ diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -238,20 +238,6 @@ '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 @@ ->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 @@ )); 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); diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -150,51 +150,18 @@ 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(); - } - - // Reload the object so it reflects edits which have been applied. - $object = id(new ManiphestTaskQuery()) - ->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(); - } + $request = $this->getRequest(); + $visible_phids = $request->getStrList('visiblePHIDs'); + if (!$visible_phids) { + $visible_phids = array(); } - $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + return id(new PhabricatorBoardResponseEngine()) ->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(); } } diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -0,0 +1,146 @@ +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; + } + +} diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -78,10 +78,6 @@ ->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 @@ $card->addAttribute($tag_list); } + $card->addClass('phui-workcard'); + return $card; } diff --git a/webroot/rsrc/js/application/projects/Workboard.js b/webroot/rsrc/js/application/projects/Workboard.js deleted file mode 100644 --- a/webroot/rsrc/js/application/projects/Workboard.js +++ /dev/null @@ -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); - } - - } - -}); diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -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(); + } + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -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; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -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; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardController.js @@ -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(); + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -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 @@ } } - - 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 @@ } 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 @@ } 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(); });