diff --git a/rsrc/behavior-sprint-boards.js b/rsrc/behavior-sprint-boards.js index 13432f19d55c7f766da0cfc77acda27bbb2da153..505fb3e9e6fdf9c270c296a9dd4f597d7074683e 100644 --- a/rsrc/behavior-sprint-boards.js +++ b/rsrc/behavior-sprint-boards.js @@ -160,196 +160,200 @@ JX.behavior('sprint-boards', function(config, statics) { data.beforePHID = before_phid; } - data.order = statics.order; + data.order = statics.order; - var workflow = new JX.Workflow(statics.moveURI, data) - .setHandler(function(response) { - onresponse(response, item, list); - }); + var workflow = new JX.Workflow(statics.moveURI, data) + .setHandler(function(response) { + onresponse(response, item, list); + }); - workflow.start(); - } + workflow.start(); + } - 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; - } + 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; - data.sort = r.data.sortMap[data.objectPHID] || data.sort; - } + for (var ii = 0; ii < items.length; ii++) { + var item = items[ii]; - // 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; - } + var data = JX.Stratcom.getData(item); + var phid = data.objectPHID; - if (remove_index !== null) { - items.splice(remove_index, 1); + if (phid == new_data.objectPHID) { + if (r.data.removeFromBoard) { + remove_index = ii; } + items[ii] = new_card; + data = new_data; + edited = true; + } - items.sort(colsort); - - JX.DOM.setContent(column, items); + data.sort = r.data.sortMap[data.objectPHID] || data.sort; + } - onupdate(column); + // 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; } - function update_statics(update_config) { - statics.boardID = update_config.boardID; - statics.projectPHID = update_config.projectPHID; - statics.order = update_config.order; - statics.moveURI = update_config.moveURI; - statics.createURI = update_config.createURI; + if (remove_index !== null) { + items.splice(remove_index, 1); } - function init_board() { - var lists = []; - var ii; - var cols = getcolumns(); + items.sort(colsort); - for (ii = 0; ii < cols.length; ii++) { - var list = new JX.DraggableList('project-card', cols[ii]) - .setFindItemsHandler(JX.bind(null, finditems, cols[ii])) - .setOuterContainer(JX.$(config.boardID)) - .setCanDragX(true); + JX.DOM.setContent(column, items); - list.listen('didSend', JX.bind(list, onupdate, cols[ii])); - list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); + onupdate(column); + }; - list.listen('didDrop', JX.bind(null, ondrop, list)); + function update_statics(update_config) { + statics.boardID = update_config.boardID; + statics.projectPHID = update_config.projectPHID; + statics.order = update_config.order; + statics.moveURI = update_config.moveURI; + statics.createURI = update_config.createURI; + } - list.listen('didBeginDrag', JX.bind(null, onbegindrag)); - list.listen('didEndDrag', JX.bind(null, onenddrag)); + function init_board() { + var lists = []; + var ii; + var cols = getcolumns(); - lists.push(list); + for (ii = 0; ii < cols.length; ii++) { + var list = new JX.DraggableList('project-card', cols[ii]) + .setFindItemsHandler(JX.bind(null, finditems, cols[ii])) + .setOuterContainer(JX.$(config.boardID)) + .setCanDragX(true); - onupdate(cols[ii]); - } + list.listen('didSend', JX.bind(list, onupdate, cols[ii])); + list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); - for (ii = 0; ii < lists.length; ii++) { - lists[ii].setGroup(lists); - } - } + list.listen('didDrop', JX.bind(null, ondrop, list)); - 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_phid = e.getNodeData('column-add-task').columnPHID; - var request_data = { - responseType: 'card', - columnPHID: column_phid, - projects: statics.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) { - return; - } + list.listen('didBeginDrag', JX.bind(null, onbegindrag)); + list.listen('didEndDrag', JX.bind(null, onenddrag)); + + lists.push(list); - e.kill(); - - var list = JX.$H(data.items).getFragment().firstChild; - - var button = e.getNode('boards-dropdown-menu'); - data.menu = new JX.PHUIXDropdownMenu(button); - data.menu.setContent(list); - data.menu.open(); - - JX.DOM.listen(list, 'click', 'tag:a', function(e) { - if (!e.isNormalClick()) { - return; - } - data.menu.close(); - }); - }); - - JX.Stratcom.listen( - 'quicksand-redraw', - null, - function (e) { - var data = e.getData(); - if (!data.newResponse.boardConfig) { - return; - } - var new_config; - if (data.fromServer) { - new_config = data.newResponse.boardConfig; - statics.boardConfigCache[data.newResponseID] = new_config; - } else { - new_config = statics.boardConfigCache[data.newResponseID]; - statics.boardID = new_config.boardID; - } - update_statics(new_config); - if (data.fromServer) { - init_board(); - } - }); - return true; + onupdate(cols[ii]); } - if (!statics.setup) { - update_statics(config); - var current_page_id = JX.Quicksand.getCurrentPageID(); - statics.boardConfigCache = {}; - statics.boardConfigCache[current_page_id] = config; - statics.setup = init_board(); + for (ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); } + } + + 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) { + return; + } + + e.kill(); + + var list = JX.$H(data.items).getFragment().firstChild; + + var button = e.getNode('boards-dropdown-menu'); + data.menu = new JX.PHUIXDropdownMenu(button); + data.menu.setContent(list); + data.menu.open(); + + JX.DOM.listen(list, 'click', 'tag:a', function(e) { + if (!e.isNormalClick()) { + return; + } + data.menu.close(); + }); + }); + + JX.Stratcom.listen( + 'quicksand-redraw', + null, + function (e) { + var data = e.getData(); + if (!data.newResponse.boardConfig) { + return; + } + var new_config; + if (data.fromServer) { + new_config = data.newResponse.boardConfig; + statics.boardConfigCache[data.newResponseID] = new_config; + } else { + new_config = statics.boardConfigCache[data.newResponseID]; + statics.boardID = new_config.boardID; + } + update_statics(new_config); + if (data.fromServer) { + init_board(); + } + }); + return true; + } + + if (!statics.setup) { + update_statics(config); + var current_page_id = JX.Quicksand.getCurrentPageID(); + statics.boardConfigCache = {}; + statics.boardConfigCache[current_page_id] = config; + init_board(); + statics.setup = setup(); + } }); diff --git a/src/controller/board/SprintBoardColumnDetailController.php b/src/controller/board/SprintBoardColumnDetailController.php index 9e59ecfae2fbe0105ec981ffa236bf4195b70e53..663e307e07c3c6a86b1132da8e91f5b662039a3b 100644 --- a/src/controller/board/SprintBoardColumnDetailController.php +++ b/src/controller/board/SprintBoardColumnDetailController.php @@ -23,6 +23,8 @@ final class SprintBoardColumnDetailController } $this->setProject($project); + $project_id = $project->getID(); + $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withIDs(array($id)) @@ -46,6 +48,10 @@ final class SprintBoardColumnDetailController $actions = $this->buildActionView($column); $properties = $this->buildPropertyView($column, $actions); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$project_id}/"); + $crumbs->addTextCrumb(pht('Column: %s', $title)); + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); @@ -53,13 +59,14 @@ final class SprintBoardColumnDetailController $nav = $this->getProfileMenu(); return $this->newPage() - ->setTitle($title) - ->setNavigation($nav) - ->appendChild( - array( - $box, - $timeline, - )); + ->setTitle($title) + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->appendChild( + array( + $box, + $timeline, + )); } private function buildHeaderView(PhabricatorProjectColumn $column) { diff --git a/src/controller/board/SprintBoardColumnEditController.php b/src/controller/board/SprintBoardColumnEditController.php index 3c79119f41be4de79bb7526dc8f2ee624238ad43..3c2ad54cb9fc74fb976203b8e4ae7c78ab7c49c4 100644 --- a/src/controller/board/SprintBoardColumnEditController.php +++ b/src/controller/board/SprintBoardColumnEditController.php @@ -81,10 +81,12 @@ final class SprintBoardColumnEditController $xactions = array(); - $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; - $xactions[] = id(new PhabricatorProjectColumnTransaction()) - ->setTransactionType($type_name) - ->setNewValue($v_name); + if (!$column->getProxy()) { + $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; + $xactions[] = id(new PhabricatorProjectColumnTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + } $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; $xactions[] = id(new PhabricatorProjectColumnTransaction()) @@ -105,25 +107,26 @@ final class SprintBoardColumnEditController } } - $form = new AphrontFormView(); - $form - ->setUser($request->getUser()) - ->appendChild( + $form = id(new AphrontFormView()) + ->setUser($request->getUser()); + + if (!$column->getProxy()) { + $form->appendChild( id(new AphrontFormTextControl()) ->setValue($v_name) ->setLabel(pht('Name')) ->setName('name') - ->setError($e_name) - ->setCaption( - pht('This will be displayed as the header of the column.'))) - ->appendChild( - id(new AphrontFormTextControl()) - ->setValue($v_limit) - ->setLabel(pht('Point Limit')) - ->setName('limit') - ->setError($e_limit) - ->setCaption( - pht('Maximum number of points of tasks allowed in the column.'))); + ->setError($e_name)); + } + + $form->appendChild( + id(new AphrontFormTextControl()) + ->setValue($v_limit) + ->setLabel(pht('Point Limit')) + ->setName('limit') + ->setError($e_limit) + ->setCaption( + pht('Maximum number of points of tasks allowed in the column.'))); if ($is_new) { @@ -147,8 +150,8 @@ final class SprintBoardColumnEditController $nav = $this->getProfileMenu(); return $this->newPage() - ->setTitle($title) - ->setNavigation($nav) - ->appendChild($form_box); + ->setTitle($title) + ->setNavigation($nav) + ->appendChild($form_box); } } diff --git a/src/controller/board/SprintBoardMoveController.php b/src/controller/board/SprintBoardMoveController.php index f7d2f1fd6590c63b418aafad2271194666a9ce96..49eb609b414f11618d1c202d60d39146a0d1ea34 100644 --- a/src/controller/board/SprintBoardMoveController.php +++ b/src/controller/board/SprintBoardMoveController.php @@ -26,6 +26,7 @@ final class SprintBoardMoveController return new Aphront404Response(); } $is_sprint = $this->isSprint($project); + $board_phid = $project->getPHID(); $object = id(new ManiphestTaskQuery()) ->setViewer($viewer) @@ -55,11 +56,14 @@ final class SprintBoardMoveController return new Aphront404Response(); } - $positions = id(new PhabricatorProjectColumnPositionQuery()) + $engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) - ->withColumns($columns) - ->withObjectPHIDs(array($object_phid)) - ->execute(); + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs(array($object_phid)) + ->executeLayout(); + + $columns = $engine->getObjectColumns($board_phid, $object_phid); + $old_column_phids = mpull($columns, 'getPHID'); $xactions = array(); @@ -81,7 +85,7 @@ final class SprintBoardMoveController ) + $order_params) ->setOldValue( array( - 'columnPHIDs' => mpull($positions, 'getColumnPHID'), + 'columnPHIDs' => $old_column_phids, 'projectPHID' => $column->getProjectPHID(), )); @@ -135,7 +139,33 @@ final class SprintBoardMoveController ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) ->setNewValue($sub); } - } + } + + $proxy = $column->getProxy(); + if ($proxy) { + // We're moving the task into a subproject or milestone column, so add + // the subproject or milestone. + $add_projects = array($proxy->getPHID()); + } else if ($project->getHasSubprojects() || $project->getHasMilestones()) { + // We're moving the task into the "Backlog" column on the parent project, + // so add the parent explicitly. This gets rid of any subproject or + // milestone tags. + $add_projects = array($project->getPHID()); + } else { + $add_projects = array(); + } + + if ($add_projects) { + $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $project_type) + ->setNewValue( + array( + '+' => array_fuse($add_projects), + )); + } $editor = id(new ManiphestTransactionEditor()) ->setActor($viewer) @@ -152,9 +182,41 @@ final class SprintBoardMoveController ->withPHIDs(array($object->getOwnerPHID())) ->executeOne(); } + + // Reload the object so it reflects edits which have been applied. + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->needProjectPHIDs(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + $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(); + } + } + + $except_phids = array_fuse($except_phids); + $handle_phids = array_fuse($object->getProjectPHIDs()); + $handle_phids = array_diff_key($handle_phids, $except_phids); + + $project_handles = $viewer->loadHandles($handle_phids); + $project_handles = iterator_to_array($project_handles); + if ($is_sprint == true) { $card = id(new SprintBoardTaskCard()) ->setProject($project) + ->setProjectHandles($project_handles) ->setViewer($viewer) ->setTask($object) ->setOwner($owner) @@ -164,6 +226,7 @@ final class SprintBoardMoveController $card = id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setTask($object) + ->setProjectHandles($project_handles) ->setOwner($owner) ->setCanEdit(true) ->setProject($project) @@ -173,6 +236,6 @@ final class SprintBoardMoveController return id(new AphrontAjaxResponse())->setContent( array('task' => $card)); - } + } } diff --git a/src/controller/board/SprintBoardViewController.php b/src/controller/board/SprintBoardViewController.php index 3ce36c65b7e30829a02b3df816d5af905a582349..ed8dd231510410a4e264dcab1e645e1d2b941cfb 100755 --- a/src/controller/board/SprintBoardViewController.php +++ b/src/controller/board/SprintBoardViewController.php @@ -28,68 +28,34 @@ final class SprintBoardViewController $project = $this->getProject(); $this->readRequestState(); - $columns = $this->loadColumns($project); - // TODO: Expand the checks here if we add the ability - // to hide the Backlog column - if (!$columns) { - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $project, - PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - $content = $this->buildNoAccessContent($project); - } else { - $content = $this->buildInitializeContent($project); - } - - if ($content instanceof AphrontResponse) { - return $content; - } - - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::PANEL_WORKBOARD); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Workboard')); - - return $this->newPage() - ->setTitle( - array( - pht('Workboard'), - $project->getName(), - )) - ->setNavigation($nav) - ->setCrumbs($crumbs) - ->appendChild($content); - } $is_sprint = $this->isSprint($project); $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); - $engine = id(new ManiphestTaskSearchEngine()) + $search_engine = id(new ManiphestTaskSearchEngine()) ->setViewer($viewer) ->setBaseURI($board_uri) ->setIsBoardView(true); - if ($request->isFormPost()) { - $saved = $engine->buildSavedQueryFromRequest($request); - $engine->saveQuery($saved); + if ($request->isFormPost() && !$request->getBool('initialize')) { + $saved = $search_engine->buildSavedQueryFromRequest($request); + $search_engine->saveQuery($saved); $filter_form = id(new AphrontFormView()) ->setUser($viewer); - $engine->buildSearchForm($filter_form, $saved); - if ($engine->getErrors()) { + $search_engine->buildSearchForm($filter_form, $saved); + if ($search_engine->getErrors()) { return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('Advanced Filter')) ->appendChild($filter_form->buildLayoutView()) - ->setErrors($engine->getErrors()) + ->setErrors($search_engine->getErrors()) ->setSubmitURI($board_uri) ->addSubmitButton(pht('Apply Filter')) ->addCancelButton($board_uri); } return id(new AphrontRedirectResponse())->setURI( $this->getURIWithState( - $engine->getQueryResultsPageURI($saved->getQueryKey()))); + $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); } $query_key = $request->getURIData('queryKey'); @@ -103,8 +69,8 @@ final class SprintBoardViewController $this->queryKey = $query_key; $custom_query = null; - if ($engine->isBuiltinQuery($query_key)) { - $saved = $engine->buildSavedQueryFromBuiltin($query_key); + if ($search_engine->isBuiltinQuery($query_key)) { + $saved = $search_engine->buildSavedQueryFromBuiltin($query_key); } else { $saved = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) @@ -121,7 +87,7 @@ final class SprintBoardViewController if ($request->getURIData('filter')) { $filter_form = id(new AphrontFormView()) ->setUser($viewer); - $engine->buildSearchForm($filter_form, $saved); + $search_engine->buildSearchForm($filter_form, $saved); return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) @@ -132,58 +98,68 @@ final class SprintBoardViewController ->addCancelButton($board_uri); } - $task_query = $engine->buildQueryFromSavedQuery($saved); + $task_query = $search_engine->buildQueryFromSavedQuery($saved); + + $select_phids = array($project->getPHID()); + if ($project->getHasSubprojects() || $project->getHasMilestones()) { + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($select_phids) + ->execute(); + foreach ($descendants as $descendant) { + $select_phids[] = $descendant->getPHID(); + } + } $tasks = $task_query ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - PhabricatorQueryConstraint::OPERATOR_AND, - array($project->getPHID())) + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($select_phids)) ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) ->setViewer($viewer) ->execute(); $tasks = mpull($tasks, null, 'getPHID'); - if ($tasks) { - $positions = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($viewer) - ->withObjectPHIDs(mpull($tasks, 'getPHID')) - ->withColumns($columns) - ->execute(); - $positions = mpull($positions, null, 'getObjectPHID'); - } else { - $positions = array(); - } + $board_phid = $project->getPHID(); - $task_map = array(); - foreach ($tasks as $task) { - $task_phid = $task->getPHID(); - if (empty($positions[$task_phid])) { - // This shouldn't normally be possible because we create positions on - // demand, but we might have raced as an object was removed from the - // board. Just drop the task if we don't have a position for it. - continue; - } + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs(array_keys($tasks)) + ->executeLayout(); - $position = $positions[$task_phid]; - $task_map[$position->getColumnPHID()][] = $task_phid; - } + $columns = $layout_engine->getColumns($board_phid); + if (!$columns) { + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + $content = $this->buildNoAccessContent($project); + } else { + $content = $this->buildInitializeContent($project); + } - // If we're showing the board in "natural" order, sort columns by their - // column positions. - if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) { - foreach ($task_map as $column_phid => $task_phids) { - $order = array(); - foreach ($task_phids as $task_phid) { - if (isset($positions[$task_phid])) { - $order[$task_phid] = $positions[$task_phid]->getOrderingKey(); - } else { - $order[$task_phid] = 0; - } - } - asort($order); - $task_map[$column_phid] = array_keys($order); + if ($content instanceof AphrontResponse) { + return $content; } + + $nav = $this->getProfileMenu(); + $nav->selectFilter(PhabricatorProject::PANEL_WORKBOARD); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Workboard')); + + return $this->newPage() + ->setTitle( + array( + pht('Workboard'), + $project->getName(), + )) + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->appendChild($content); } $task_can_edit_map = id(new PhabricatorPolicyFilter()) @@ -202,7 +178,10 @@ final class SprintBoardViewController return new Aphront404Response(); } - $batch_task_phids = idx($task_map, $batch_column->getPHID(), array()); + $batch_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $batch_column->getPHID()); + foreach ($batch_task_phids as $key => $batch_task_phid) { if (empty($task_can_edit_map[$batch_task_phid])) { unset($batch_task_phids[$key]); @@ -270,10 +249,46 @@ final class SprintBoardViewController $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); + $all_project_phids = array(); + foreach ($tasks as $task) { + foreach ($task->getProjectPHIDs() as $project_phid) { + $all_project_phids[$project_phid] = $project_phid; + } + } + + foreach ($select_phids as $phid) { + unset($all_project_phids[$phid]); + } + + $all_handles = $viewer->loadHandles($all_project_phids); + $all_handles = iterator_to_array($all_handles); + foreach ($columns as $column) { - $task_phids = idx($task_map, $column->getPHID(), array()); + if (!$this->showHidden) { + if ($column->isHidden()) { + continue; + } + } + + $proxy = $column->getProxy(); + if ($proxy && !$proxy->isMilestone()) { + // TODO: For now, don't show subproject columns because we can't + // handle tasks with multiple positions yet. + continue; + } + + $task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column->getPHID()); + $column_tasks = array_select_keys($tasks, $task_phids); + // If we aren't using "natural" order, reorder the column by the original + // query order. + if ($this->sortKey != PhabricatorProjectColumn::ORDER_NATURAL) { + $column_tasks = array_select_keys($column_tasks, array_keys($tasks)); + } + $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->setSubHeader($column->getDisplayType()) @@ -284,6 +299,11 @@ final class SprintBoardViewController $panel->setHeaderIcon($header_icon); } + $display_class = $column->getDisplayClass(); + if ($display_class) { + $panel->addClass($display_class); + } + if ($column->isHidden()) { $panel->addClass('project-panel-hidden'); } @@ -323,9 +343,11 @@ final class SprintBoardViewController $owner = $this->handles[$task->getOwnerPHID()]; } $can_edit = idx($task_can_edit_map, $task->getPHID(), false); + $handles = array_select_keys($all_handles, $task->getProjectPHIDs()); if ($is_sprint == true) { $cards->addItem(id(new SprintBoardTaskCard()) ->setProject($project) + ->setProjectHandles($handles) ->setViewer($viewer) ->setTask($task) ->setOwner($owner) @@ -335,6 +357,7 @@ final class SprintBoardViewController $cards->addItem(id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setProject($project) + ->setProjectHandles($handles) ->setTask($task) ->setOwner($owner) ->setCanEdit($can_edit) @@ -352,7 +375,7 @@ final class SprintBoardViewController $filter_menu = $this->buildFilterMenu( $viewer, $custom_query, - $engine, + $search_engine, $query_key); $manage_menu = $this->buildManageMenu($project, $this->showHidden); @@ -379,19 +402,19 @@ final class SprintBoardViewController $crumbs->addAction($manage_menu); return $this->newPage() - ->setTitle(pht('%s Board', $project->getName())) - ->setPageObjectPHIDs(array($project->getPHID())) - ->setShowFooter(false) - ->setNavigation($nav) - ->setCrumbs($crumbs) - ->addQuicksandConfig( - array( - 'boardConfig' => $behavior_config, - )) - ->appendChild( - array( - $board_box, - )); + ->setTitle(pht('%s Board', $project->getName())) + ->setPageObjectPHIDs(array($project->getPHID())) + ->setShowFooter(false) + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->addQuicksandConfig( + array( + 'boardConfig' => $behavior_config, + )) + ->appendChild( + array( + $board_box, + )); } private function readRequestState() { @@ -413,31 +436,12 @@ final class SprintBoardViewController $this->sortKey = $sort_key; } - private function loadColumns(PhabricatorProject $project) { - $viewer = $this->getViewer(); - - $column_query = id(new PhabricatorProjectColumnQuery()) - ->setViewer($viewer) - ->withProjectPHIDs(array($project->getPHID())); - - if (!$this->showHidden) { - $column_query->withStatuses( - array(PhabricatorProjectColumn::STATUS_ACTIVE)); - } - - $columns = $column_query->execute(); - $columns = mpull($columns, null, 'getSequence'); - ksort($columns); - - return $columns; - } - private function buildSortMenu( PhabricatorUser $viewer, $sort_key) { $sort_icon = id(new PHUIIconView()) - ->setIcon('fa-sort-amount-asc bluegrey'); + ->setIcon('fa-sort-amount-asc bluegrey'); $named = array( PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), @@ -471,14 +475,14 @@ final class SprintBoardViewController } $sort_button = id(new PHUIListItemView()) - ->setName(pht('Sort: %s', $active_order)) - ->setIcon('fa-sort-amount-asc') - ->setHref('#') - ->addSigil('boards-dropdown-menu') - ->setMetadata( - array( - 'items' => hsprintf('%s', $sort_menu), - )); + ->setName(pht('Sort: %s', $active_order)) + ->setIcon('fa-sort-amount-asc') + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->setMetadata( + array( + 'items' => hsprintf('%s', $sort_menu), + )); return $sort_button; } @@ -545,14 +549,14 @@ final class SprintBoardViewController } $filter_button = id(new PHUIListItemView()) - ->setName(pht('Filter: %s', $active_filter)) - ->setIcon('fa-search') - ->setHref('#') - ->addSigil('boards-dropdown-menu') - ->setMetadata( - array( - 'items' => hsprintf('%s', $filter_menu), - )); + ->setName(pht('Filter: %s', $active_filter)) + ->setIcon('fa-search') + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->setMetadata( + array( + 'items' => hsprintf('%s', $filter_menu), + )); return $filter_button; } @@ -562,7 +566,7 @@ final class SprintBoardViewController $show_hidden) { $request = $this->getRequest(); - $viewer = $request->getViewer(); + $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -622,14 +626,14 @@ final class SprintBoardViewController } $manage_button = id(new PHUIListItemView()) - ->setName(pht('Manage Board')) - ->setIcon('fa-cog') - ->setHref('#') - ->addSigil('boards-dropdown-menu') - ->setMetadata( - array( - 'items' => hsprintf('%s', $manage_menu), - )); + ->setName(pht('Manage Board')) + ->setIcon('fa-cog') + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->setMetadata( + array( + 'items' => hsprintf('%s', $manage_menu), + )); return $manage_button; } @@ -648,6 +652,12 @@ final class SprintBoardViewController $column_items = array(); + if ($column->getProxyPHID()) { + $default_phid = $column->getProxyPHID(); + } else { + $default_phid = $column->getProjectPHID(); + } + $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Create Task...')) @@ -656,6 +666,7 @@ final class SprintBoardViewController ->setMetadata( array( 'columnPHID' => $column->getPHID(), + 'projectPHID' => $default_phid, )); $batch_edit_uri = $request->getRequestURI(); @@ -672,12 +683,12 @@ final class SprintBoardViewController ->setDisabled(!$can_batch_edit); $detail_uri = $this->getApplicationURI( - 'board/'.$this->id.'/column/'.$column->getID().'/'); + 'board/'.$this->id.'/column/'.$column->getID().'/'); $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-columns') - ->setName(pht('Column Details')) - ->setHref($detail_uri); + ->setIcon('fa-columns') + ->setName(pht('Column Details')) + ->setHref($detail_uri); $can_hide = ($can_edit && !$column->isDefaultColumn()); $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/'; @@ -718,7 +729,8 @@ final class SprintBoardViewController return $column_button; } - /** + + /** * Add current state parameters (like order and the visibility of hidden * columns) to a URI. * @@ -760,8 +772,8 @@ final class SprintBoardViewController // TODO: This should be cleaned up, but maybe we're going to make options // for each column or board? $edit_config = id(new ManiphestEditEngine()) - ->setViewer($viewer) - ->loadDefaultEditConfiguration(); + ->setViewer($viewer) + ->loadDefaultEditConfiguration(); if ($edit_config) { $form_key = $edit_config->getIdentifier(); $create_uri = "/maniphest/task/edit/form/{$form_key}/"; @@ -788,63 +800,64 @@ final class SprintBoardViewController $set_default = $request->getBool('default'); if ($set_default) { $this - ->getProfilePanelEngine() - ->adjustDefault(PhabricatorProject::PANEL_WORKBOARD); + ->getProfilePanelEngine() + ->adjustDefault(PhabricatorProject::PANEL_WORKBOARD); } if ($request->isFormPost()) { if ($type == 'backlog-only') { $column = PhabricatorProjectColumn::initializeNewColumn($viewer) - ->setSequence(0) - ->setProperty('isDefault', true) - ->setProjectPHID($project->getPHID()) - ->save(); + ->setSequence(0) + ->setProperty('isDefault', true) + ->setProjectPHID($project->getPHID()) + ->save(); $project->setHasWorkboard(1)->save(); return id(new AphrontRedirectResponse()) - ->setURI($board_uri); + ->setURI($board_uri); } else { return id(new AphrontRedirectResponse()) - ->setURI($import_uri); + ->setURI($import_uri); } } $new_selector = id(new AphrontFormRadioButtonControl()) - ->setLabel(pht('Columns')) - ->setName('initialize-type') - ->setValue('backlog-only') - ->addButton( - 'backlog-only', - pht('New Empty Board'), - pht('Create a new board with just a backlog column.')) - ->addButton( - 'import', - pht('Import Columns'), - pht('Import board columns from another project.')); + ->setLabel(pht('Columns')) + ->setName('initialize-type') + ->setValue('backlog-only') + ->addButton( + 'backlog-only', + pht('New Empty Board'), + pht('Create a new board with just a backlog column.')) + ->addButton( + 'import', + pht('Import Columns'), + pht('Import board columns from another project.')); $default_checkbox = id(new AphrontFormCheckboxControl()) - ->setLabel(pht('Make Default')) - ->addCheckbox( - 'default', - 1, - pht('Make the workboard the default view for this project.'), - true); + ->setLabel(pht('Make Default')) + ->addCheckbox( + 'default', + 1, + pht('Make the workboard the default view for this project.'), + true); $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendRemarkupInstructions( - pht('The workboard for this project has not been created yet.')) - ->appendControl($new_selector) - ->appendControl($default_checkbox) - ->appendControl( - id(new AphrontFormSubmitControl()) - ->addCancelButton($profile_uri) - ->setValue(pht('Create Workboard'))); + ->setUser($viewer) + ->addHiddenInput('initialize', 1) + ->appendRemarkupInstructions( + pht('The workboard for this project has not been created yet.')) + ->appendControl($new_selector) + ->appendControl($default_checkbox) + ->appendControl( + id(new AphrontFormSubmitControl()) + ->addCancelButton($profile_uri) + ->setValue(pht('Create Workboard'))); $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Create Workboard')) - ->setForm($form); + ->setHeaderText(pht('Create Workboard')) + ->setForm($form); return $box; } @@ -857,13 +870,13 @@ final class SprintBoardViewController $profile_uri = $this->getApplicationURI("profile/{$id}/"); return $this->newDialog() - ->setTitle(pht('Unable to Create Workboard')) - ->appendParagraph( - pht( - 'The workboard for this project has not been created yet, '. - 'but you do not have permission to create it. Only users '. - 'who can edit this project can create a workboard for it.')) - ->addCancelButton($profile_uri); + ->setTitle(pht('Unable to Create Workboard')) + ->appendParagraph( + pht( + 'The workboard for this project has not been created yet, '. + 'but you do not have permission to create it. Only users '. + 'who can edit this project can create a workboard for it.')) + ->addCancelButton($profile_uri); } } diff --git a/src/controller/board/SprintManiphestEditEngine.php b/src/controller/board/SprintManiphestEditEngine.php index cce13c934db9e57b3a14647a1994b37d08d2d70a..2b348cb722b99aac08a71d78c694f1101b97325b 100644 --- a/src/controller/board/SprintManiphestEditEngine.php +++ b/src/controller/board/SprintManiphestEditEngine.php @@ -280,21 +280,36 @@ final class SprintManiphestEditEngine return new Aphront404Response(); } - // If the workboard's project has been removed from the card's project - // list, we are going to remove it from the board completely. + // 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. + + $descendant_projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs(array($column->getProjectPHID())) + ->execute(); + $board_phids = mpull($descendant_projects, 'getPHID', 'getPHID'); + $board_phids[$column->getProjectPHID()] = $column->getProjectPHID(); + $project_map = array_fuse($task->getProjectPHIDs()); - $remove_card = empty($project_map[$column->getProjectPHID()]); + $remove_card = !array_intersect_key($board_phids, $project_map); $positions = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($viewer) - ->withColumns(array($column)) - ->execute(); + ->setViewer($viewer) + ->withBoardPHIDs(array($column->getProjectPHID())) + ->withColumnPHIDs(array($column->getPHID())) + ->execute(); $task_phids = mpull($positions, 'getObjectPHID'); $column_tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($task_phids) - ->execute(); + ->setViewer($viewer) + ->withPHIDs($task_phids) + ->needProjectPHIDs(true) + ->execute(); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { // TODO: This is a little bit awkward, because PHP and JS use @@ -303,20 +318,20 @@ final class SprintManiphestEditEngine $sort_map = array(); foreach ($positions as $position) { $sort_map[$position->getObjectPHID()] = array( - -$position->getSequence(), - $position->getID(), + -$position->getSequence(), + $position->getID(), ); } } else { $sort_map = mpull( - $column_tasks, - 'getPrioritySortVector', - 'getPHID'); + $column_tasks, + 'getPrioritySortVector', + 'getPHID'); } $data = array( - 'removeFromBoard' => $remove_card, - 'sortMap' => $sort_map, + 'removeFromBoard' => $remove_card, + 'sortMap' => $sort_map, ); // TODO: This should just use HandlePool once we get through the EditEngine @@ -324,22 +339,32 @@ final class SprintManiphestEditEngine $owner = null; if ($task->getOwnerPHID()) { $owner = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($task->getOwnerPHID())) - ->executeOne(); + ->setViewer($viewer) + ->withPHIDs(array($task->getOwnerPHID())) + ->executeOne(); } + $handle_phids = $task->getProjectPHIDs(); + $handle_phids = array_fuse($handle_phids); + $handle_phids = array_diff_key($handle_phids, $board_phids); + + $project_handles = $viewer->loadHandles($handle_phids); + $project_handles = iterator_to_array($project_handles); + $projects = $request->getArr('projectPHIDs'); $project = $this->getSprintProjectforTask($viewer, $projects); $tasks = id(new SprintBoardTaskCard()) ->setViewer($viewer) ->setProject($project) + ->setProjectHandles($project_handles) ->setTask($task) ->setOwner($owner) ->setCanEdit(true) ->getItem(); + $tasks->addClass('phui-workcard'); + $payload = array( 'tasks' => $tasks, 'data' => $data, diff --git a/src/query/SprintQuery.php b/src/query/SprintQuery.php index 424807c43652380cbf41f961607454ac0a2e3a11..bb94457cf525504104a42d046dc63d521792ab25 100644 --- a/src/query/SprintQuery.php +++ b/src/query/SprintQuery.php @@ -304,8 +304,7 @@ final class SprintQuery extends SprintDAO { ->setViewer($this->viewer) ->withBoardPHIDs(array($this->projectPHID)) ->withObjectPHIDs(mpull($tasks, 'getPHID')) - ->withColumns($columns) - ->needColumns(true) + ->withColumnPHIDs(mpull($columns, 'getPHID')) ->execute(); $positions = mpull($positions, null, 'getObjectPHID'); } else { diff --git a/src/view/SprintBoardTaskCard.php b/src/view/SprintBoardTaskCard.php index e40266fcefc832544f7d992fbe9eb483d5850229..b64ff7e38dfc9db3fec4dfa1b5553c01da82df72 100644 --- a/src/view/SprintBoardTaskCard.php +++ b/src/view/SprintBoardTaskCard.php @@ -4,6 +4,7 @@ final class SprintBoardTaskCard extends Phobject { private $project; private $viewer; + private $projectHandles; private $task; private $owner; private $canEdit; @@ -28,6 +29,15 @@ final class SprintBoardTaskCard extends Phobject { return $this->viewer; } + public function setProjectHandles(array $handles) { + $this->projectHandles = $handles; + return $this; + } + + public function getProjectHandles() { + return $this->projectHandles; + } + public function setTask(ManiphestTask $task) { $this->task = $task; return $this; @@ -128,14 +138,12 @@ final class SprintBoardTaskCard extends Phobject { $card->addHandleIcon($owner, $owner->getName()); } $card->addAttribute($this->getCardAttributes()); - $project_phids = array_fuse($task->getProjectPHIDs()); - unset($project_phids[$this->project->getPHID()]); - if ($project_phids) { - $handle_list = $viewer->loadHandles($project_phids); + $project_handles = $this->getProjectHandles(); + if ($project_handles) { $tag_list = id(new PHUIHandleTagListView()) - ->setSlim(true) - ->setHandles($handle_list); + ->setSlim(true) + ->setHandles($project_handles); $card->addAttribute($tag_list); } return $card;