@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.

Smooth out milestone creation workflow

Summary:
Ref T10010.

- Default name to "Milestone X".
- Remove policy controls, which have no effect.
- Don't generate slugs for milestones since this is a big pain where they all generate as `#milestone_1` by default (you can add one if you want). I plan to add some kind of syntax like `#parent/32` to mean "Milestone 32 in Parent" later.
- Don't require projects to have unique names (again, 900 copies of "Milestone X"). I think we can trust users to sort this out for themselves since modern Phabricator has "Can Create Projects" permission, etc.

Test Plan: Created some milestones, had a less awful experience.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10010

Differential Revision: https://secure.phabricator.com/D14909

+96 -63
+3 -1
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 186 186 ->setName('#'.$slug->getSlug()); 187 187 } 188 188 189 - $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags)); 189 + if ($hashtags) { 190 + $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags)); 191 + } 190 192 191 193 $view->addProperty( 192 194 pht('Members'),
+37 -39
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 3 3 final class PhabricatorProjectTransactionEditor 4 4 extends PhabricatorApplicationTransactionEditor { 5 5 6 + private $isMilestone; 7 + 8 + private function setIsMilestone($is_milestone) { 9 + $this->isMilestone = $is_milestone; 10 + return $this; 11 + } 12 + 13 + private function getIsMilestone() { 14 + return $this->isMilestone; 15 + } 16 + 6 17 public function getEditorApplicationClass() { 7 18 return 'PhabricatorProjectApplication'; 8 19 } ··· 91 102 case PhabricatorProjectTransaction::TYPE_NAME: 92 103 $name = $xaction->getNewValue(); 93 104 $object->setName($name); 94 - $object->setPrimarySlug(PhabricatorSlug::normalizeProjectSlug($name)); 105 + if ($this->getIsMilestone()) { 106 + $object->setPrimarySlug(PhabricatorSlug::normalizeProjectSlug($name)); 107 + } 95 108 return; 96 109 case PhabricatorProjectTransaction::TYPE_SLUGS: 97 110 return; ··· 114 127 $object->setParentProjectPHID($xaction->getNewValue()); 115 128 return; 116 129 case PhabricatorProjectTransaction::TYPE_MILESTONE: 117 - $current = queryfx_one( 118 - $object->establishConnection('w'), 119 - 'SELECT MAX(milestoneNumber) n 120 - FROM %T 121 - WHERE parentProjectPHID = %s', 122 - $object->getTableName(), 123 - $object->getParentProject()->getPHID()); 124 - if (!$current) { 125 - $number = 1; 126 - } else { 127 - $number = (int)$current['n'] + 1; 128 - } 129 - 130 + $number = $object->getParentProject()->loadNextMilestoneNumber(); 130 131 $object->setMilestoneNumber($number); 131 132 $object->setParentProjectPHID($xaction->getNewValue()); 132 133 return; ··· 275 276 } 276 277 } 277 278 278 - $is_milestone = $object->isMilestone(); 279 - foreach ($xactions as $xaction) { 280 - switch ($xaction->getTransactionType()) { 281 - case PhabricatorProjectTransaction::TYPE_MILESTONE: 282 - if ($xaction->getNewValue() !== null) { 283 - $is_milestone = true; 284 - } 285 - break; 286 - } 287 - } 279 + $is_milestone = $this->getIsMilestone(); 288 280 289 281 $is_parent = $object->getHasSubprojects(); 290 282 ··· 346 338 break; 347 339 } 348 340 341 + if ($this->getIsMilestone()) { 342 + break; 343 + } 344 + 349 345 $name = last($xactions)->getNewValue(); 350 346 351 347 if (!PhabricatorSlug::isValidProjectSlug($name)) { ··· 358 354 break; 359 355 } 360 356 361 - $name_used_already = id(new PhabricatorProjectQuery()) 362 - ->setViewer($this->getActor()) 363 - ->withNames(array($name)) 364 - ->executeOne(); 365 - if ($name_used_already && 366 - ($name_used_already->getPHID() != $object->getPHID())) { 367 - $error = new PhabricatorApplicationTransactionValidationError( 368 - $type, 369 - pht('Duplicate'), 370 - pht('Project name is already used.'), 371 - nonempty(last($xactions), null)); 372 - $errors[] = $error; 373 - } 374 - 375 357 $slug = PhabricatorSlug::normalizeProjectSlug($name); 376 358 377 359 $slug_used_already = id(new PhabricatorProjectSlug()) ··· 381 363 $error = new PhabricatorApplicationTransactionValidationError( 382 364 $type, 383 365 pht('Duplicate'), 384 - pht('Project name can not be used due to hashtag collision.'), 366 + pht( 367 + 'Project name generates the same hashtag ("%s") as another '. 368 + 'existing project. Choose a unique name.', 369 + '#'.$slug), 385 370 nonempty(last($xactions), null)); 386 371 $errors[] = $error; 387 372 } ··· 882 867 )); 883 868 } 884 869 } 870 + 871 + $is_milestone = $object->isMilestone(); 872 + foreach ($xactions as $xaction) { 873 + switch ($xaction->getTransactionType()) { 874 + case PhabricatorProjectTransaction::TYPE_MILESTONE: 875 + if ($xaction->getNewValue() !== null) { 876 + $is_milestone = true; 877 + } 878 + break; 879 + } 880 + } 881 + 882 + $this->setIsMilestone($is_milestone); 885 883 886 884 return $results; 887 885 }
+33 -1
src/applications/project/engine/PhabricatorProjectEditEngine.php
··· 43 43 } 44 44 45 45 protected function newEditableObject() { 46 - return PhabricatorProject::initializeNewProject($this->getViewer()); 46 + $project = PhabricatorProject::initializeNewProject($this->getViewer()); 47 + 48 + $milestone = $this->getMilestoneProject(); 49 + if ($milestone) { 50 + $default_name = pht( 51 + 'Milestone %s', 52 + new PhutilNumber($milestone->loadNextMilestoneNumber())); 53 + $project->setName($default_name); 54 + } 55 + 56 + return $project; 47 57 } 48 58 49 59 protected function newObjectQuery() { ··· 90 100 protected function getCreateNewObjectPolicy() { 91 101 return $this->getApplication()->getPolicy( 92 102 ProjectCreateProjectsCapability::CAPABILITY); 103 + } 104 + 105 + protected function willConfigureFields($object, array $fields) { 106 + $is_milestone = ($this->getMilestoneProject() || $object->isMilestone()); 107 + 108 + $unavailable = array( 109 + PhabricatorTransactions::TYPE_VIEW_POLICY, 110 + PhabricatorTransactions::TYPE_EDIT_POLICY, 111 + PhabricatorTransactions::TYPE_JOIN_POLICY, 112 + ); 113 + $unavailable = array_fuse($unavailable); 114 + 115 + if ($is_milestone) { 116 + foreach ($fields as $key => $field) { 117 + $xaction_type = $field->getTransactionType(); 118 + if (isset($unavailable[$xaction_type])) { 119 + unset($fields[$key]); 120 + } 121 + } 122 + } 123 + 124 + return $fields; 93 125 } 94 126 95 127 protected function newBuiltinEngineConfigurations() {
+18 -9
src/applications/project/storage/PhabricatorProject.php
··· 211 211 'projectPathKey' => 'bytes4', 212 212 ), 213 213 self::CONFIG_KEY_SCHEMA => array( 214 - 'key_phid' => null, 215 - 'phid' => array( 216 - 'columns' => array('phid'), 217 - 'unique' => true, 218 - ), 219 214 'key_icon' => array( 220 215 'columns' => array('icon'), 221 216 ), 222 217 'key_color' => array( 223 218 'columns' => array('color'), 224 - ), 225 - 'name' => array( 226 - 'columns' => array('name'), 227 - 'unique' => true, 228 219 ), 229 220 'key_milestone' => array( 230 221 'columns' => array('parentProjectPHID', 'milestoneNumber'), ··· 473 464 } 474 465 475 466 return true; 467 + } 468 + 469 + public function loadNextMilestoneNumber() { 470 + $current = queryfx_one( 471 + $this->establishConnection('w'), 472 + 'SELECT MAX(milestoneNumber) n 473 + FROM %T 474 + WHERE parentProjectPHID = %s', 475 + $this->getTableName(), 476 + $this->getPHID()); 477 + 478 + if (!$current) { 479 + $number = 1; 480 + } else { 481 + $number = (int)$current['n'] + 1; 482 + } 483 + 484 + return $number; 476 485 } 477 486 478 487
+5 -6
src/applications/transactions/editengine/PhabricatorEditEngine.php
··· 64 64 abstract public function getEngineApplicationClass(); 65 65 abstract protected function buildCustomEditFields($object); 66 66 67 - protected function didBuildCustomEditFields($object, array $fields) { 68 - return; 69 - } 70 - 71 67 public function getFieldsForConfig( 72 68 PhabricatorEditEngineConfiguration $config) { 73 69 ··· 93 89 } 94 90 95 91 $fields = mpull($fields, null, 'getKey'); 96 - $this->didBuildCustomEditFields($object, $fields); 97 92 98 93 $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); 99 94 foreach ($extensions as $extension) { ··· 115 110 } 116 111 117 112 $extension_fields = mpull($extension_fields, null, 'getKey'); 118 - $extension->didBuildCustomEditFields($this, $object, $extension_fields); 119 113 120 114 foreach ($extension_fields as $key => $field) { 121 115 $fields[$key] = $field; ··· 123 117 } 124 118 125 119 $config = $this->getEditEngineConfiguration(); 120 + $fields = $this->willConfigureFields($object, $fields); 126 121 $fields = $config->applyConfigurationToFields($this, $object, $fields); 127 122 123 + return $fields; 124 + } 125 + 126 + protected function willConfigureFields($object, array $fields) { 128 127 return $fields; 129 128 } 130 129
-7
src/applications/transactions/engineextension/PhabricatorEditEngineExtension.php
··· 32 32 PhabricatorEditEngine $engine, 33 33 PhabricatorApplicationTransactionInterface $object); 34 34 35 - public function didBuildCustomEditFields( 36 - PhabricatorEditEngine $engine, 37 - PhabricatorApplicationTransactionInterface $object, 38 - array $fields) { 39 - return; 40 - } 41 - 42 35 final public static function getAllExtensions() { 43 36 return id(new PhutilClassMapQuery()) 44 37 ->setAncestorClass(__CLASS__)