@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Milestone workboard creation: Propose previous milestone's columns

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

+134 -1
+134 -1
src/applications/project/controller/PhabricatorProjectBoardImportController.php
··· 94 94 return id(new AphrontRedirectResponse())->setURI($board_uri); 95 95 } 96 96 97 + // Default value. The Tokenizer wants an array of phids. 98 + $tokenizer_value = array(); 99 + 100 + // Default to the previous milestone, if available. 101 + $value_candidate = $this->getPreviousMilestoneIfHasImportableColumns( 102 + $viewer, $project); 103 + 104 + if ($value_candidate) { 105 + $tokenizer_value[] = $value_candidate->getPHID(); 106 + } 107 + 97 108 $proj_selector = id(new AphrontFormTokenizerControl()) 98 109 ->setName('importProjectPHID') 99 110 ->setUser($viewer) 111 + ->setValue($tokenizer_value) 100 112 ->setDatasource(id(new PhabricatorProjectDatasource()) 101 113 ->setParameters(array('mustHaveColumns' => true)) 102 114 ->setLimit(1)); ··· 104 116 return $this->newDialog() 105 117 ->setTitle(pht('Import Columns')) 106 118 ->setWidth(AphrontDialogView::WIDTH_FORM) 107 - ->appendParagraph(pht('Choose a project to import columns from:')) 119 + ->appendParagraph(pht('Choose a project or a milestone to import '. 120 + 'columns from:')) 108 121 ->appendChild($proj_selector) 109 122 ->addCancelButton($board_uri) 110 123 ->addSubmitButton(pht('Import')); 124 + } 125 + 126 + /** 127 + * Starting from a milestone, get the previous milestone, 128 + * but only if it has at least one column that could be imported. 129 + * @param PhabricatorUser $viewer Current user 130 + * @param PhabricatorProject $project Current milestone with no 131 + * workboard yet. 132 + * Technically, this parameter 133 + * could also be a project, 134 + * and projects are silently 135 + * rejected if passed here. 136 + * @return PhabricatorProject|null The milestone preceding 137 + * your specified milestone, 138 + * but only if it has at least 139 + * one importable column; 140 + * null in any other case. 141 + */ 142 + private function getPreviousMilestoneIfHasImportableColumns( 143 + PhabricatorUser $viewer, 144 + PhabricatorProject $milestone): ?PhabricatorProject { 145 + 146 + // We can suggest something, only if you are creating workboard 147 + // on a milestone. 148 + if (!$milestone->isMilestone()) { 149 + return null; 150 + } 151 + 152 + // Possible design choices: 153 + // 1. Suggest the most recent milestone (excluding this specific one). 154 + // CONS: Suggesting the most recent milestone doesn't make much sense, 155 + // when I want to create a workboard from a milestone from 156 + // a year ago. 157 + // Side note: to suggest the 'most recent', we need at least one 158 + // more query to find that number. It needs a "SELECT MAX(n)". 159 + // 2. Suggest the precedent milestone. 160 + // PRO: convenient when you already worked on a workboard, and you just 161 + // create an additional milestone; convenient also when you create 162 + // a workboard on a old milestone, so as to maintain the 163 + // workboard structure of that time. 164 + // Side note: finding the previous milestone is also extremely 165 + // easy. We avoid the extra query "SELECT MAX(n)". 166 + // 167 + // So we go with: 2. Suggest the precedent milestone. 168 + $previous_milestone_num = (int)$milestone->getMilestoneNumber(); 169 + $previous_milestone_num--; 170 + if ($previous_milestone_num < 1) { 171 + return null; 172 + } 173 + 174 + $previous_milestone = $this->getProjectMilestoneFromNumber( 175 + $viewer, 176 + $milestone->getParentProjectPHID(), 177 + $previous_milestone_num); 178 + 179 + if (!$previous_milestone) { 180 + return null; 181 + } 182 + 183 + // Micro-optimization to avoid querying columns. 184 + if (!$previous_milestone->getHasWorkboard()) { 185 + return null; 186 + } 187 + 188 + // Check if this milestone has at least one 189 + // existing column which could be imported, or we can cause the error 190 + // 'Source Workboard Has No Columns', 191 + // and it would be more confusing than useful. 192 + $example_column = $this->getOneImportableProjectColumn( 193 + $viewer, 194 + $previous_milestone->getPHID()); 195 + 196 + if (!$example_column) { 197 + return null; 198 + } 199 + 200 + return $previous_milestone; 201 + } 202 + 203 + /** 204 + * Get the milestone of a project from its milestone number. 205 + * @param PhabricatorUser $viewer Current user 206 + * @param string $proj_phid Project PHID 207 + * @param int $number Milestone number 208 + */ 209 + private function getProjectMilestoneFromNumber( 210 + PhabricatorUser $viewer, 211 + string $proj_phid, int $number): ?PhabricatorProject { 212 + 213 + $query_proj = new PhabricatorProjectQuery(); 214 + return $query_proj 215 + ->setViewer($viewer) 216 + ->withParentProjectPHIDs(array($proj_phid)) 217 + ->withIsMilestone(true) 218 + ->withMilestoneNumberBetween($number, $number) 219 + ->executeOne(); 220 + } 221 + 222 + /** 223 + * Get whatever non-proxy column on the workboard, 224 + * if existing, except for the default "Backlog" column. 225 + * @param PhabricatorUser $viewer Current user 226 + * @param string $proj_phid Project PHID 227 + * @return PhabricatorProjectColumn|null Column, or null 228 + * if there are none. 229 + */ 230 + private function getOneImportableProjectColumn( 231 + PhabricatorUser $viewer, 232 + string $proj_phid): ?PhabricatorProjectColumn { 233 + 234 + // Query one (whatever) project column suitable for the import. 235 + // This code is inspired from PhabricatorProjectDatasource, 236 + // looking at its parameter 'mustHaveColumns'. 237 + $query_columns = new PhabricatorProjectColumnQuery(); 238 + return $query_columns 239 + ->setViewer($viewer) 240 + ->withProjectPHIDs(array($proj_phid)) 241 + ->withIsProxyColumn(false) 242 + ->setLimit(1) 243 + ->executeOne(); 111 244 } 112 245 113 246 }