diff --git a/resources/celerity/map.php b/resources/celerity/map.php index cb8c9c1e8b..7b9e355b68 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -413,6 +413,7 @@ return array( 'rsrc/js/application/projects/behavior-boards-dropdown.js' => '0ec56e1d', 'rsrc/js/application/projects/behavior-project-boards.js' => 'c6b95cbd', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', + 'rsrc/js/application/projects/behavior-reorder-columns.js' => '09eee344', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'ab836011', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', @@ -645,6 +646,7 @@ return array( 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 'javelin-behavior-remarkup-preview' => 'f7379f45', 'javelin-behavior-reorder-applications' => '76b9fc3e', + 'javelin-behavior-reorder-columns' => '09eee344', 'javelin-behavior-repository-crossreference' => 'f9539603', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-on-click' => '4e3e79a6', @@ -872,6 +874,14 @@ return array( 4 => 'javelin-util', 5 => 'phabricator-busy', ), + '09eee344' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-workflow', + 3 => 'javelin-dom', + 4 => 'phabricator-draggable-list', + ), '0a3f3021' => array( 0 => 'javelin-behavior', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2216f70dc8..9811c0d1ef 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1971,6 +1971,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', 'PhabricatorProjectBoardDeleteController' => 'applications/project/controller/PhabricatorProjectBoardDeleteController.php', 'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php', + 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', @@ -4842,6 +4843,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardDeleteController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardEditController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumn' => array( diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php index de193b5837..cd33f9cabb 100644 --- a/src/applications/project/application/PhabricatorApplicationProject.php +++ b/src/applications/project/application/PhabricatorApplicationProject.php @@ -64,12 +64,16 @@ final class PhabricatorApplicationProject extends PhabricatorApplication { '(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', - 'board/(?P[1-9]\d*)/edit/(?:(?P\d+)/)?' - => 'PhabricatorProjectBoardEditController', - 'board/(?P[1-9]\d*)/delete/(?:(?P\d+)/)?' - => 'PhabricatorProjectBoardDeleteController', - 'board/(?P[1-9]\d*)/column/(?:(?P\d+)/)?' - => 'PhabricatorProjectColumnDetailController', + 'board/(?P[1-9]\d*)/' => array( + 'edit/(?:(?P\d+)/)?' + => 'PhabricatorProjectBoardEditController', + 'delete/(?:(?P\d+)/)?' + => 'PhabricatorProjectBoardDeleteController', + 'column/(?:(?P\d+)/)?' + => 'PhabricatorProjectColumnDetailController', + 'reorder/' + => 'PhabricatorProjectBoardReorderController', + ), 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', 'history/(?P[1-9]\d*)/' => 'PhabricatorProjectHistoryController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardReorderController.php b/src/applications/project/controller/PhabricatorProjectBoardReorderController.php new file mode 100644 index 0000000000..0660f8428a --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardReorderController.php @@ -0,0 +1,157 @@ +projectID = $data['projectID']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($this->projectID)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + $this->setProject($project); + + + $project_id = $project->getID(); + + $board_uri = $this->getApplicationURI("board/{$project_id}/"); + $reorder_uri = $this->getApplicationURI("board/{$project_id}/reorder/"); + + if ($request->isFormPost()) { + // User clicked "Done", make sure the page reloads to show the new + // column order. + return id(new AphrontRedirectResponse())->setURI($board_uri); + } + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array($project->getPHID())) + ->execute(); + $columns = msort($columns, 'getSequence'); + + $column_phid = $request->getStr('columnPHID'); + if ($column_phid && $request->validateCSRF()) { + + $columns = mpull($columns, null, 'getPHID'); + if (empty($columns[$column_phid])) { + return new Aphront404Response(); + } + + // TODO: We could let you move the backlog column around if you really + // want, but for now we use sequence position 0 as magic. + $target_column = $columns[$column_phid]; + $new_sequence = $request->getInt('sequence'); + if ($target_column->isDefaultColumn() || $new_sequence < 1) { + return new Aphront404Response(); + } + + // TODO: For now, we're not recording any transactions here. We probably + // should, but this sort of edit is extremely trivial. + + // Resequence the columns so that the moved column has the correct + // sequence number. Move columns after it up one place in the sequence. + $new_map = array(); + foreach ($columns as $phid => $column) { + $value = $column->getSequence(); + if ($column->getPHID() == $column_phid) { + $value = $new_sequence; + } else if ($column->getSequence() >= $new_sequence) { + $value = $value + 1; + } + $new_map[$phid] = $value; + } + + // Sort the columns into their new ordering. + asort($new_map); + + // Now, compact the ordering and adjust any columns that need changes. + $project->openTransaction(); + $sequence = 0; + foreach ($new_map as $phid => $ignored) { + $new_value = $sequence++; + $cur_value = $columns[$phid]->getSequence(); + if ($new_value != $cur_value) { + $columns[$phid]->setSequence($new_value)->save(); + } + } + $project->saveTransaction(); + + return id(new AphrontAjaxResponse())->setContent( + array( + 'sequenceMap' => mpull($columns, 'getSequence', 'getPHID'), + )); + } + + $list_id = celerity_generate_unique_node_id(); + + $static_list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setFlush(true) + ->setStackable(true); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setID($list_id) + ->setFlush(true) + ->setStackable(true); + + foreach ($columns as $column) { + $item = id(new PHUIObjectItemView()) + ->setHeader($column->getDisplayName()); + + if ($column->isHidden()) { + $item->setDisabled(true); + } + + if ($column->isDefaultColumn()) { + $item->setDisabled(true); + $static_list->addItem($item); + } else { + $item->setGrippable(true); + $item->addSigil('board-column'); + $item->setMetadata( + array( + 'columnPHID' => $column->getPHID(), + 'columnSequence' => $column->getSequence(), + )); + + $list->addItem($item); + } + + } + + Javelin::initBehavior( + 'reorder-columns', + array( + 'listID' => $list_id, + 'reorderURI' => $reorder_uri, + )); + + return $this->newDialog() + ->setTitle(pht('Reorder Columns')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendParagraph(pht('This column can not be moved:')) + ->appendChild($static_list) + ->appendParagraph(pht('Drag and drop these columns to reorder them:')) + ->appendChild($list) + ->addSubmitButton(pht('Done')); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 87b0fd7bcf..006fd3563e 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -357,7 +357,16 @@ final class PhabricatorProjectBoardViewController $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Add Column')) - ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')); + ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-exchange') + ->setName(pht('Reorder Columns')) + ->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/')) + ->setDisabled(!$can_edit) + ->setWorkflow(true); if ($show_hidden) { $hidden_uri = $request->getRequestURI() diff --git a/webroot/rsrc/js/application/projects/behavior-reorder-columns.js b/webroot/rsrc/js/application/projects/behavior-reorder-columns.js new file mode 100644 index 0000000000..8b0a2c33fa --- /dev/null +++ b/webroot/rsrc/js/application/projects/behavior-reorder-columns.js @@ -0,0 +1,58 @@ +/** + * @provides javelin-behavior-reorder-columns + * @requires javelin-behavior + * javelin-stratcom + * javelin-workflow + * javelin-dom + * phabricator-draggable-list + */ + +JX.behavior('reorder-columns', function(config) { + + var root = JX.$(config.listID); + + var list = new JX.DraggableList('board-column', root) + .setFindItemsHandler(function() { + return JX.DOM.scry(root, 'li', 'board-column'); + }); + + list.listen('didDrop', function(node) { + var nodes = list.findItems(); + + var node_data = JX.Stratcom.getData(node); + + // Find the column sequence of the previous node. + var sequence = null; + var data; + for (var ii = 0; ii < nodes.length; ii++) { + data = JX.Stratcom.getData(nodes[ii]); + if (data.columnPHID === node_data.columnPHID) { + break; + } + sequence = data.columnSequence; + } + + list.lock(); + JX.DOM.alterClass(node, 'drag-sending', true); + + var parameters = { + columnPHID: node_data.columnPHID, + sequence: (sequence === null) ? 1 : (parseInt(sequence, 10) + 1) + }; + + new JX.Workflow(config.reorderURI, parameters) + .setHandler(function(r) { + + // Adjust metadata for the new sequence numbers. + for (var ii = 0; ii < nodes.length; ii++) { + var data = JX.Stratcom.getData(nodes[ii]); + data.columnSequence = r.sequenceMap[data.columnPHID]; + } + + list.unlock(); + JX.DOM.alterClass(node, 'drag-sending', false); + }) + .start(); + }); + +});