diff --git a/src/applications/project/controller/PhabricatorProjectBoardImportController.php b/src/applications/project/controller/PhabricatorProjectBoardImportController.php index 67bddaaa52..89a01e8db1 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardImportController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardImportController.php @@ -94,9 +94,21 @@ final class PhabricatorProjectBoardImportController return id(new AphrontRedirectResponse())->setURI($board_uri); } + // Default value. The Tokenizer wants an array of phids. + $tokenizer_value = array(); + + // Default to the previous milestone, if available. + $value_candidate = $this->getPreviousMilestoneIfHasImportableColumns( + $viewer, $project); + + if ($value_candidate) { + $tokenizer_value[] = $value_candidate->getPHID(); + } + $proj_selector = id(new AphrontFormTokenizerControl()) ->setName('importProjectPHID') ->setUser($viewer) + ->setValue($tokenizer_value) ->setDatasource(id(new PhabricatorProjectDatasource()) ->setParameters(array('mustHaveColumns' => true)) ->setLimit(1)); @@ -104,10 +116,131 @@ final class PhabricatorProjectBoardImportController return $this->newDialog() ->setTitle(pht('Import Columns')) ->setWidth(AphrontDialogView::WIDTH_FORM) - ->appendParagraph(pht('Choose a project to import columns from:')) + ->appendParagraph(pht('Choose a project or a milestone to import '. + 'columns from:')) ->appendChild($proj_selector) ->addCancelButton($board_uri) ->addSubmitButton(pht('Import')); } + /** + * Starting from a milestone, get the previous milestone, + * but only if it has at least one column that could be imported. + * @param PhabricatorUser $viewer Current user + * @param PhabricatorProject $project Current milestone with no + * workboard yet. + * Technically, this parameter + * could also be a project, + * and projects are silently + * rejected if passed here. + * @return PhabricatorProject|null The milestone preceding + * your specified milestone, + * but only if it has at least + * one importable column; + * null in any other case. + */ + private function getPreviousMilestoneIfHasImportableColumns( + PhabricatorUser $viewer, + PhabricatorProject $milestone): ?PhabricatorProject { + + // We can suggest something, only if you are creating workboard + // on a milestone. + if (!$milestone->isMilestone()) { + return null; + } + + // Possible design choices: + // 1. Suggest the most recent milestone (excluding this specific one). + // CONS: Suggesting the most recent milestone doesn't make much sense, + // when I want to create a workboard from a milestone from + // a year ago. + // Side note: to suggest the 'most recent', we need at least one + // more query to find that number. It needs a "SELECT MAX(n)". + // 2. Suggest the precedent milestone. + // PRO: convenient when you already worked on a workboard, and you just + // create an additional milestone; convenient also when you create + // a workboard on a old milestone, so as to maintain the + // workboard structure of that time. + // Side note: finding the previous milestone is also extremely + // easy. We avoid the extra query "SELECT MAX(n)". + // + // So we go with: 2. Suggest the precedent milestone. + $previous_milestone_num = (int)$milestone->getMilestoneNumber(); + $previous_milestone_num--; + if ($previous_milestone_num < 1) { + return null; + } + + $previous_milestone = $this->getProjectMilestoneFromNumber( + $viewer, + $milestone->getParentProjectPHID(), + $previous_milestone_num); + + if (!$previous_milestone) { + return null; + } + + // Micro-optimization to avoid querying columns. + if (!$previous_milestone->getHasWorkboard()) { + return null; + } + + // Check if this milestone has at least one + // existing column which could be imported, or we can cause the error + // 'Source Workboard Has No Columns', + // and it would be more confusing than useful. + $example_column = $this->getOneImportableProjectColumn( + $viewer, + $previous_milestone->getPHID()); + + if (!$example_column) { + return null; + } + + return $previous_milestone; + } + + /** + * Get the milestone of a project from its milestone number. + * @param PhabricatorUser $viewer Current user + * @param string $proj_phid Project PHID + * @param int $number Milestone number + */ + private function getProjectMilestoneFromNumber( + PhabricatorUser $viewer, + string $proj_phid, int $number): ?PhabricatorProject { + + $query_proj = new PhabricatorProjectQuery(); + return $query_proj + ->setViewer($viewer) + ->withParentProjectPHIDs(array($proj_phid)) + ->withIsMilestone(true) + ->withMilestoneNumberBetween($number, $number) + ->executeOne(); + } + + /** + * Get whatever non-proxy column on the workboard, + * if existing, except for the default "Backlog" column. + * @param PhabricatorUser $viewer Current user + * @param string $proj_phid Project PHID + * @return PhabricatorProjectColumn|null Column, or null + * if there are none. + */ + private function getOneImportableProjectColumn( + PhabricatorUser $viewer, + string $proj_phid): ?PhabricatorProjectColumn { + + // Query one (whatever) project column suitable for the import. + // This code is inspired from PhabricatorProjectDatasource, + // looking at its parameter 'mustHaveColumns'. + $query_columns = new PhabricatorProjectColumnQuery(); + return $query_columns + ->setViewer($viewer) + ->withProjectPHIDs(array($proj_phid)) + ->withIsProxyColumn(false) + ->setLimit(1) + ->executeOne(); + } + }