@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
at upstream/main 370 lines 13 kB view raw
1<?php 2 3final class PhabricatorProjectEditEngine 4 extends PhabricatorEditEngine { 5 6 const ENGINECONST = 'projects.project'; 7 8 private $parentProject; 9 private $milestoneProject; 10 11 public function setParentProject(PhabricatorProject $parent_project) { 12 $this->parentProject = $parent_project; 13 return $this; 14 } 15 16 public function getParentProject() { 17 return $this->parentProject; 18 } 19 20 public function setMilestoneProject(PhabricatorProject $milestone_project) { 21 $this->milestoneProject = $milestone_project; 22 return $this; 23 } 24 25 public function getMilestoneProject() { 26 return $this->milestoneProject; 27 } 28 29 public function isDefaultQuickCreateEngine() { 30 return true; 31 } 32 33 public function getQuickCreateOrderVector() { 34 return id(new PhutilSortVector())->addInt(200); 35 } 36 37 public function getEngineName() { 38 return pht('Projects'); 39 } 40 41 public function getSummaryHeader() { 42 return pht('Configure Project Forms'); 43 } 44 45 public function getSummaryText() { 46 return pht('Configure forms for creating projects.'); 47 } 48 49 public function getEngineApplicationClass() { 50 return PhabricatorProjectApplication::class; 51 } 52 53 protected function newEditableObject() { 54 $parent = nonempty($this->parentProject, $this->milestoneProject); 55 56 return PhabricatorProject::initializeNewProject( 57 $this->getViewer(), 58 $parent); 59 } 60 61 protected function newObjectQuery() { 62 return id(new PhabricatorProjectQuery()) 63 ->needSlugs(true); 64 } 65 66 protected function getObjectCreateTitleText($object) { 67 return pht('Create New Project'); 68 } 69 70 protected function getObjectEditTitleText($object) { 71 return pht('Edit Project: %s', $object->getName()); 72 } 73 74 protected function getObjectEditShortText($object) { 75 return $object->getName(); 76 } 77 78 protected function getObjectCreateShortText() { 79 return pht('Create Project'); 80 } 81 82 protected function getObjectName() { 83 return pht('Project'); 84 } 85 86 protected function getObjectViewURI($object) { 87 if ($this->getIsCreate()) { 88 return $object->getURI(); 89 } else { 90 $id = $object->getID(); 91 return "/project/manage/{$id}/"; 92 } 93 } 94 95 protected function getObjectCreateCancelURI($object) { 96 $parent = $this->getParentProject(); 97 $milestone = $this->getMilestoneProject(); 98 99 if ($parent || $milestone) { 100 $id = nonempty($parent, $milestone)->getID(); 101 return "/project/subprojects/{$id}/"; 102 } 103 104 return parent::getObjectCreateCancelURI($object); 105 } 106 107 protected function getCreateNewObjectPolicy() { 108 return $this->getApplication()->getPolicy( 109 ProjectCreateProjectsCapability::CAPABILITY); 110 } 111 112 protected function willConfigureFields($object, array $fields) { 113 $is_milestone = ($this->getMilestoneProject() || $object->isMilestone()); 114 115 $unavailable = array( 116 PhabricatorTransactions::TYPE_VIEW_POLICY, 117 PhabricatorTransactions::TYPE_EDIT_POLICY, 118 PhabricatorTransactions::TYPE_JOIN_POLICY, 119 PhabricatorTransactions::TYPE_SPACE, 120 PhabricatorProjectIconTransaction::TRANSACTIONTYPE, 121 PhabricatorProjectColorTransaction::TRANSACTIONTYPE, 122 ); 123 $unavailable = array_fuse($unavailable); 124 125 if ($is_milestone) { 126 foreach ($fields as $key => $field) { 127 $xaction_type = $field->getTransactionType(); 128 if ($xaction_type !== null && isset($unavailable[$xaction_type])) { 129 unset($fields[$key]); 130 } 131 } 132 } 133 134 return $fields; 135 } 136 137 protected function newBuiltinEngineConfigurations() { 138 $configuration = head(parent::newBuiltinEngineConfigurations()); 139 140 // TODO: This whole method is clumsy, and the ordering for the custom 141 // field is especially clumsy. Maybe try to make this more natural to 142 // express. 143 144 $configuration 145 ->setFieldOrder( 146 array( 147 'parent', 148 'milestone', 149 'milestone.previous', 150 'name', 151 'std:project:internal:description', 152 'icon', 153 'color', 154 'slugs', 155 )); 156 157 return array( 158 $configuration, 159 ); 160 } 161 162 protected function buildCustomEditFields($object) { 163 $slugs = mpull($object->getSlugs(), 'getSlug'); 164 $slugs = array_fuse($slugs); 165 unset($slugs[$object->getPrimarySlug()]); 166 $slugs = array_values($slugs); 167 168 $milestone = $this->getMilestoneProject(); 169 $parent = $this->getParentProject(); 170 171 if ($parent) { 172 $parent_phid = $parent->getPHID(); 173 } else { 174 $parent_phid = null; 175 } 176 177 $previous_milestone_phid = null; 178 if ($milestone) { 179 $milestone_phid = $milestone->getPHID(); 180 181 // Load the current milestone so we can show the user a hint about what 182 // it was called, so they don't have to remember if the next one should 183 // be "Sprint 287" or "Sprint 278". 184 185 $number = ($milestone->loadNextMilestoneNumber() - 1); 186 if ($number > 0) { 187 $previous_milestone = id(new PhabricatorProjectQuery()) 188 ->setViewer($this->getViewer()) 189 ->withParentProjectPHIDs(array($milestone->getPHID())) 190 ->withIsMilestone(true) 191 ->withMilestoneNumberBetween($number, $number) 192 ->executeOne(); 193 if ($previous_milestone) { 194 $previous_milestone_phid = $previous_milestone->getPHID(); 195 } 196 } 197 } else { 198 $milestone_phid = null; 199 } 200 201 // 202 // Load the colors available for selection. 203 // 204 // Goals: 205 // 1. Allow to pick the colors available from the option 206 // 'projects.colors' ('getColorMap'). 207 // 2. Allow to show what is the **current** color. 208 // 2.1 If the current value is 'orange' but if omit 209 // any fruit from 'projects.colors', 210 // then show the truth: show 'Orange' (not 'Red'). 211 // So, you can easily inspect this legacy color and replace it. 212 // 2.2 If the current value is an internal color, like 'disabled', 213 // which is an hardcoded color used for archived projects, 214 // then show the truth: show 'Disabled' (not 'Red'). 215 // 3.3. If the current color is anything else esoteric which is still 216 // supported for rendering (available in 'getShadeMap'), 217 // show it, and do not fallback on the first color. 218 // In short, do not fallback on 'Red'. 219 // 220 // Elsewhere, if the current value is not a color, and cannot be 221 // rendered (not in 'getShadeMap') then don't propose it. 222 // So the UX forces you to select a "clean" one on the next edit. 223 // 224 // https://we.phorge.it/T16236 225 $colors_for_select = PhabricatorProjectIconSet::getColorMap(); 226 $color_current = $object->getColor(); 227 if ($color_current) { 228 $colors_supported = PHUITagView::getShadeMapCached(); 229 if (isset($colors_supported[$color_current])) { 230 $colors_for_select[$color_current] = $colors_supported[$color_current]; 231 } 232 } 233 234 $fields = array( 235 id(new PhabricatorHandlesEditField()) 236 ->setKey('parent') 237 ->setLabel(pht('Parent')) 238 ->setDescription(pht('Create a subproject of an existing project.')) 239 ->setConduitDescription( 240 pht('Choose a parent project to create a subproject beneath.')) 241 ->setConduitTypeDescription(pht('PHID of the parent project.')) 242 ->setAliases(array('parentPHID')) 243 ->setTransactionType( 244 PhabricatorProjectParentTransaction::TRANSACTIONTYPE) 245 ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) 246 ->setSingleValue($parent_phid) 247 ->setIsReorderable(false) 248 ->setIsDefaultable(false) 249 ->setIsLockable(false) 250 ->setIsLocked(true), 251 id(new PhabricatorHandlesEditField()) 252 ->setKey('milestone') 253 ->setLabel(pht('Milestone Of')) 254 ->setDescription(pht('Parent project to create a milestone for.')) 255 ->setConduitDescription( 256 pht('Choose a parent project to create a new milestone for.')) 257 ->setConduitTypeDescription(pht('PHID of the parent project.')) 258 ->setAliases(array('milestonePHID')) 259 ->setTransactionType( 260 PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE) 261 ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) 262 ->setSingleValue($milestone_phid) 263 ->setIsReorderable(false) 264 ->setIsDefaultable(false) 265 ->setIsLockable(false) 266 ->setIsLocked(true), 267 id(new PhabricatorHandlesEditField()) 268 ->setKey('milestone.previous') 269 ->setLabel(pht('Previous Milestone')) 270 ->setSingleValue($previous_milestone_phid) 271 ->setIsReorderable(false) 272 ->setIsDefaultable(false) 273 ->setIsLockable(false) 274 ->setIsLocked(true), 275 id(new PhabricatorTextEditField()) 276 ->setKey('name') 277 ->setLabel(pht('Name')) 278 ->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE) 279 ->setIsRequired(true) 280 ->setDescription(pht('Project name.')) 281 ->setConduitDescription(pht('Rename the project')) 282 ->setConduitTypeDescription(pht('New project name.')) 283 ->setValue($object->getName()), 284 id(new PhabricatorIconSetEditField()) 285 ->setKey('icon') 286 ->setLabel(pht('Icon')) 287 ->setTransactionType( 288 PhabricatorProjectIconTransaction::TRANSACTIONTYPE) 289 ->setIconSet(new PhabricatorProjectIconSet()) 290 ->setDescription(pht('Project icon.')) 291 ->setConduitDescription(pht('Change the project icon.')) 292 ->setConduitTypeDescription(pht('New project icon.')) 293 ->setValue($object->getIcon()), 294 id(new PhabricatorSelectEditField()) 295 ->setKey('color') 296 ->setLabel(pht('Color')) 297 ->setTransactionType( 298 PhabricatorProjectColorTransaction::TRANSACTIONTYPE) 299 ->setOptions($colors_for_select) 300 ->setDescription(pht('Project tag color.')) 301 ->setConduitDescription(pht('Change the project tag color.')) 302 ->setConduitTypeDescription(pht('New project tag color.')) 303 // When a project is archived, whatever color is set in the 304 // storage does not really matter, since the 'getColor()' has 305 // always been hardcoded to return a specific different color 306 // (the 'disabled' color). 307 // So, for archived projects, do not allow to change color. 308 // https://we.phorge.it/T15236 309 // 310 // Incidentally, this means that recently archived projects will 311 // keep their original color in their storage, so, when you 312 // re-activate a project, its original color is successfully shown. 313 ->setIsLocked($object->isArchived()) 314 ->setValue($object->getColor()), 315 id(new PhabricatorStringListEditField()) 316 ->setKey('slugs') 317 ->setLabel(pht('Additional Hashtags')) 318 ->setTransactionType( 319 PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE) 320 ->setDescription(pht('Additional project tags.')) 321 ->setConduitDescription(pht('Change project tags.')) 322 ->setConduitTypeDescription(pht('New list of hashtags.')) 323 ->setValue($slugs), 324 ); 325 326 $can_edit_members = (!$milestone) && 327 (!$object->isMilestone()) && 328 (!$object->getHasSubprojects()); 329 330 if ($can_edit_members) { 331 332 // Show this on the web UI when creating a project, but not when editing 333 // one. It is always available via Conduit. 334 $show_field = (bool)$this->getIsCreate(); 335 336 $members_field = id(new PhabricatorUsersEditField()) 337 ->setKey('members') 338 ->setAliases(array('memberPHIDs')) 339 ->setLabel(pht('Initial Members')) 340 ->setIsFormField($show_field) 341 ->setUseEdgeTransactions(true) 342 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 343 ->setMetadataValue( 344 'edge:type', 345 PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) 346 ->setDescription(pht('Initial project members.')) 347 ->setConduitDescription(pht('Set project members.')) 348 ->setConduitTypeDescription(pht('New list of members.')) 349 ->setValue(array()); 350 351 $members_field->setViewer($this->getViewer()); 352 353 $edit_add = $members_field->getConduitEditType('members.add') 354 ->setConduitDescription(pht('Add members.')); 355 356 $edit_set = $members_field->getConduitEditType('members.set') 357 ->setConduitDescription( 358 pht('Set members, overwriting the current value.')); 359 360 $edit_rem = $members_field->getConduitEditType('members.remove') 361 ->setConduitDescription(pht('Remove members.')); 362 363 $fields[] = $members_field; 364 } 365 366 return $fields; 367 368 } 369 370}