From 130ecdd585ff06d88ed0590d0abb21834ab00649 Mon Sep 17 00:00:00 2001 From: Valerio Bozzolan Date: Tue, 13 Jan 2026 11:54:48 +0100 Subject: [PATCH] Milestone workboard creation: Propose previous milestone's columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: When you have a milestone and you are creating its workboard, very often (always?) you just want to inherit the same columns from the previous milestone. So, when you use this feature on a milestone without a workboard: {nav Project > Workboard > Import Columns} Then, enjoy a good default: the previous milestone. | Before | After | |-----------|------------| |{F6677478} | {F6677483} | Note that the previous milestone will not be suggested if it has no columns, or if it has just the "Backlog" default column (which is not really a column). This is desired. We only show a default value if it does not cause errors when proceeding. If you don't like this default, click on the “x". In the future, clicking that "x" angrily may automatically send a perfumed letter of complaint to Valerio. Additionally, now the user interface does not say anymore 'Choose a project', but 'Choose a project or a milestone' because this feature has always worked with both projects and milestones. It's true that milestones are technically projects... but extra clarity does not hurt here. Closes T16380 Test Plan: Check that the out-of-discussion cases do not provide this new default: Create a new Project named "MY PROJ". Click on Workboard > Import Columns. No default. As usual. From the project "MY PROJ", create a milestone named "FIRST MILESTONE". From "FIRST MILESTONE": Click on Workboard > Import Columns. No default. As usual. From "FIRST MILESTONE", create a simple workboard without any extra column. Create another milestone named "SECOND MILESTONE". From there: Click on Workboard > Import Columns. No default. As usual. Since the "FIRST MILESTONE" has no columns. From the workboard of "FIRST MILESTONE", create some columns. From "SECOND MILESTONE", finally: Click on Workboard > Import Columns. Enjoy the default set to "FIRST MILESTONE". YEEEH! Finally, click on "Import": The "SECOND MILESTONE" imported columns from "FIRST MILESTONE". Do the very same but try to ignore the default, and manually select another project: your manually-selected project is used instead, successfully. Reviewers: O1 Blessed Committers, aklapper Reviewed By: O1 Blessed Committers, aklapper Subscribers: aklapper, tobiaswiese, Matthew, Cigaryno Maniphest Tasks: T16380 Differential Revision: https://we.phorge.it/D26550 --- ...habricatorProjectBoardImportController.php | 135 +++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) 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(); + } + } -- 2.51.2