@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<?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}