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 @@ -1814,6 +1814,7 @@ 'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php', 'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php', 'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php', + 'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php', 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', @@ -6038,6 +6039,7 @@ 'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', + 'PhabricatorBoardLayoutEngine' => 'Phobject', 'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -26,6 +26,8 @@ return new Aphront404Response(); } + $board_phid = $project->getPHID(); + $object = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) @@ -54,11 +56,14 @@ 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(); @@ -80,7 +85,7 @@ ) + $order_params) ->setOldValue( array( - 'columnPHIDs' => mpull($positions, 'getColumnPHID'), + 'columnPHIDs' => $old_column_phids, 'projectPHID' => $column->getProjectPHID(), )); diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -0,0 +1,187 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setBoardPHIDs(array $board_phids) { + $this->boardPHIDs = $board_phids; + return $this; + } + + public function getBoardPHIDs() { + return $this->boardPHIDs; + } + + public function setObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function getObjectPHIDs() { + return $this->objectPHIDs; + } + + public function executeLayout() { + $viewer = $this->getViewer(); + + $boards = $this->loadBoards(); + if (!$boards) { + return $this; + } + + $columns = $this->loadColumns($boards); + $positions = $this->loadPositions($boards); + + foreach ($boards as $board_phid => $board) { + $board_columns = idx($columns, $board_phid); + + // Don't layout boards with no columns. These boards need to be formally + // created first. + if (!$columns) { + continue; + } + + $board_positions = idx($positions, $board_phid, array()); + + $this->layoutBoard($board, $board_columns, $board_positions); + } + + return $this; + } + + public function getObjectColumns($board_phid, $object_phid) { + $board_map = idx($this->objectColumnMap, $board_phid, array()); + + $column_phids = idx($board_map, $object_phid); + if (!$column_phids) { + return array(); + } + + return array_select_keys($this->columnMap, $column_phids); + } + + private function loadBoards() { + $viewer = $this->getViewer(); + $board_phids = $this->getBoardPHIDs(); + + $boards = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs($board_phids) + ->execute(); + $boards = mpull($boards, null, 'getPHID'); + + foreach ($boards as $key => $board) { + if (!$board->getHasWorkboard()) { + unset($boards[$key]); + } + } + + return $boards; + } + + private function loadColumns(array $boards) { + $viewer = $this->getViewer(); + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array_keys($boards)) + ->execute(); + $columns = msort($columns, 'getSequence'); + $columns = mpull($columns, null, 'getPHID'); + + $this->columnMap = $columns; + $columns = mgroup($columns, 'getProjectPHID'); + + return $columns; + } + + private function loadPositions(array $boards) { + $viewer = $this->getViewer(); + + $positions = id(new PhabricatorProjectColumnPositionQuery()) + ->setViewer($viewer) + ->withBoardPHIDs(array_keys($boards)) + ->withObjectPHIDs($this->getObjectPHIDs()) + ->execute(); + $positions = msort($positions, 'getOrderingKey'); + $positions = mgroup($positions, 'getBoardPHID'); + + return $positions; + } + + private function layoutBoard( + $board, + array $columns, + array $positions) { + + $board_phid = $board->getPHID(); + $position_groups = mgroup($positions, 'getObjectPHID'); + + foreach ($columns as $column) { + if ($column->isDefaultColumn()) { + $default_phid = $column->getPHID(); + break; + } + } + + $layout = array(); + + $object_phids = $this->getObjectPHIDs(); + foreach ($object_phids as $object_phid) { + $positions = idx($position_groups, $object_phid, array()); + + // Remove any positions in columns which no longer exist. + foreach ($positions as $key => $position) { + $column_phid = $position->getColumnPHID(); + if (empty($columns[$column_phid])) { + unset($positions[$key]); + } + } + + // If the object has no position, put it on the default column. + if (!$positions) { + $new_position = id(new PhabricatorProjectColumnPosition()) + ->setBoardPHID($board_phid) + ->setColumnPHID($default_phid) + ->setObjectPHID($object_phid) + ->setSequence(0); + $positions = array( + $new_position, + ); + } + + foreach ($positions as $position) { + $column_phid = $position->getColumnPHID(); + $layout[$column_phid][$object_phid] = $position; + } + } + + foreach ($layout as $column_phid => $map) { + $map = msort($map, 'getOrderingKey'); + $layout[$column_phid] = $map; + + foreach ($map as $object_phid => $position) { + $this->objectColumnMap[$board_phid][$object_phid][] = $column_phid; + } + } + + $this->boardLayout[$board_phid] = $layout; + } + +} diff --git a/src/applications/project/events/PhabricatorProjectUIEventListener.php b/src/applications/project/events/PhabricatorProjectUIEventListener.php --- a/src/applications/project/events/PhabricatorProjectUIEventListener.php +++ b/src/applications/project/events/PhabricatorProjectUIEventListener.php @@ -49,37 +49,24 @@ $annotations = array(); if ($handles && $can_appear_on_boards) { + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($user) + ->setBoardPHIDs($project_phids) + ->setObjectPHIDs(array($object->getPHID())) + ->executeLayout(); // TDOO: Generalize this UI and move it out of Maniphest. - require_celerity_resource('maniphest-task-summary-css'); - $positions_query = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($user) - ->withBoardPHIDs($project_phids) - ->withObjectPHIDs(array($object->getPHID())) - ->needColumns(true); - - // This is important because positions will be created "on demand" - // based on the set of columns. If we don't specify it, positions - // won't be created. - $columns = id(new PhabricatorProjectColumnQuery()) - ->setViewer($user) - ->withProjectPHIDs($project_phids) - ->execute(); - if ($columns) { - $positions_query->withColumns($columns); - } - $positions = $positions_query->execute(); - $positions = mpull($positions, null, 'getBoardPHID'); - foreach ($project_phids as $project_phid) { $handle = $handles[$project_phid]; - $position = idx($positions, $project_phid); - if ($position) { - $column = $position->getColumn(); + $columns = $engine->getObjectColumns( + $project_phid, + $object->getPHID()); + $annotation = array(); + foreach ($columns as $column) { $column_name = pht('(%s)', $column->getDisplayName()); $column_link = phutil_tag( 'a', @@ -89,9 +76,13 @@ ), $column_name); + $annotation[] = $column_link; + } + + if ($annotation) { $annotations[$project_phid] = array( ' ', - $column_link, + phutil_implode_html(', ', $annotation), ); } } diff --git a/src/applications/project/storage/PhabricatorProjectColumnPosition.php b/src/applications/project/storage/PhabricatorProjectColumnPosition.php --- a/src/applications/project/storage/PhabricatorProjectColumnPosition.php +++ b/src/applications/project/storage/PhabricatorProjectColumnPosition.php @@ -41,6 +41,10 @@ } public function getOrderingKey() { + if (!$this->getID()) { + return 0; + } + // Low sequence numbers go above high sequence numbers. // High position IDs go above low position IDs. // Broadly, this makes newly added stuff float to the top.