@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 ManiphestEditEngine
4 extends PhabricatorEditEngine {
5
6 const ENGINECONST = 'maniphest.task';
7
8 public function getEngineName() {
9 return pht('Maniphest Tasks');
10 }
11
12 public function getSummaryHeader() {
13 return pht('Configure Maniphest Task Forms');
14 }
15
16 public function getSummaryText() {
17 return pht('Configure how users create and edit tasks.');
18 }
19
20 public function getEngineApplicationClass() {
21 return PhabricatorManiphestApplication::class;
22 }
23
24 public function isDefaultQuickCreateEngine() {
25 return true;
26 }
27
28 public function getQuickCreateOrderVector() {
29 return id(new PhutilSortVector())->addInt(100);
30 }
31
32 protected function newEditableObject() {
33 return ManiphestTask::initializeNewTask($this->getViewer());
34 }
35
36 protected function newObjectQuery() {
37 return id(new ManiphestTaskQuery());
38 }
39
40 protected function getObjectCreateTitleText($object) {
41 return pht('Create New Task');
42 }
43
44 protected function getObjectEditTitleText($object) {
45 return pht('Edit Task: %s', $object->getTitle());
46 }
47
48 protected function getObjectEditShortText($object) {
49 return $object->getMonogram();
50 }
51
52 protected function getObjectCreateShortText() {
53 return pht('Create Task');
54 }
55
56 protected function getObjectName() {
57 return pht('Task');
58 }
59
60 protected function getEditorURI() {
61 return $this->getApplication()->getApplicationURI('task/edit/');
62 }
63
64 protected function getCommentViewHeaderText($object) {
65 return pht('Weigh In');
66 }
67
68 protected function getCommentViewButtonText($object) {
69 return pht('Set Sail for Adventure');
70 }
71
72 public function getCommentFieldPlaceholderText($object) {
73 if ($object->getStatus() === ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) {
74 return pht('This task is closed as a duplicate. '.
75 'Only comment if you think that this task is not a duplicate.');
76 }
77 return '';
78 }
79
80 protected function getObjectViewURI($object) {
81 return '/'.$object->getMonogram();
82 }
83
84 protected function buildCustomEditFields($object) {
85 $status_map = $this->getTaskStatusMap($object);
86 $priority_map = $this->getTaskPriorityMap($object);
87
88 $alias_map = ManiphestTaskPriority::getTaskPriorityAliasMap();
89
90 if ($object->isClosed()) {
91 $default_status = ManiphestTaskStatus::getDefaultStatus();
92 } else {
93 $default_status = ManiphestTaskStatus::getDefaultClosedStatus();
94 }
95
96 if ($object->getOwnerPHID()) {
97 $owner_value = array($object->getOwnerPHID());
98 } else {
99 $owner_value = array($this->getViewer()->getPHID());
100 }
101
102 $column_documentation = pht(<<<EODOCS
103You can use this transaction type to create a task into a particular workboard
104column, or move an existing task between columns.
105
106The transaction value can be specified in several forms. Some are simpler but
107less powerful, while others are more complex and more powerful.
108
109The simplest valid value is a single column PHID:
110
111```lang=json
112"PHID-PCOL-1111"
113```
114
115This will move the task into that column, or create the task into that column
116if you are creating a new task. If the task is currently on the board, it will
117be moved out of any exclusive columns. If the task is not currently on the
118board, it will be added to the board.
119
120You can also perform multiple moves at the same time by passing a list of
121PHIDs:
122
123```lang=json
124["PHID-PCOL-2222", "PHID-PCOL-3333"]
125```
126
127This is equivalent to performing each move individually.
128
129The most complex and most powerful form uses a dictionary to provide additional
130information about the move, including an optional specific position within the
131column.
132
133The target column should be identified as `columnPHID`, and you may select a
134position by passing either `beforePHIDs` or `afterPHIDs`, specifying the PHIDs
135of tasks currently in the column that you want to move this task before or
136after:
137
138```lang=json
139[
140 {
141 "columnPHID": "PHID-PCOL-4444",
142 "beforePHIDs": ["PHID-TASK-5555"]
143 }
144]
145```
146
147When you specify multiple PHIDs, the task will be moved adjacent to the first
148valid PHID found in either of the lists. This allows positional moves to
149generally work as users expect even if the client view of the board has fallen
150out of date and some of the nearby tasks have moved elsewhere.
151EODOCS
152 );
153
154 $column_map = $this->getColumnMap($object);
155
156 $fields = array(
157 id(new PhabricatorHandlesEditField())
158 ->setKey('parent')
159 ->setLabel(pht('Parent Task'))
160 ->setDescription(pht('Task to make this a subtask of.'))
161 ->setConduitDescription(pht('Create as a subtask of another task.'))
162 ->setConduitTypeDescription(pht('PHID of the parent task.'))
163 ->setAliases(array('parentPHID'))
164 ->setTransactionType(ManiphestTaskParentTransaction::TRANSACTIONTYPE)
165 ->setHandleParameterType(new ManiphestTaskListHTTPParameterType())
166 ->setSingleValue(null)
167 ->setIsReorderable(false)
168 ->setIsDefaultable(false)
169 ->setIsLockable(false),
170 id(new PhabricatorColumnsEditField())
171 ->setKey('column')
172 ->setLabel(pht('Column'))
173 ->setDescription(pht('Create a task in a workboard column.'))
174 ->setConduitDescription(
175 pht('Move a task to one or more workboard columns.'))
176 ->setConduitTypeDescription(
177 pht('List of columns to move the task to.'))
178 ->setConduitDocumentation($column_documentation)
179 ->setAliases(array('columnPHID', 'columns', 'columnPHIDs'))
180 ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
181 ->setIsReorderable(false)
182 ->setIsDefaultable(false)
183 ->setIsLockable(false)
184 ->setCommentActionLabel(pht('Move on Workboard'))
185 ->setCommentActionOrder(2000)
186 ->setColumnMap($column_map),
187 id(new PhabricatorTextEditField())
188 ->setKey('title')
189 ->setLabel(pht('Title'))
190 ->setBulkEditLabel(pht('Set title to'))
191 ->setDescription(pht('Name of the task.'))
192 ->setConduitDescription(pht('Rename the task.'))
193 ->setConduitTypeDescription(pht('New task name.'))
194 ->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
195 ->setIsRequired(true)
196 ->setValue($object->getTitle()),
197 id(new PhabricatorUsersEditField())
198 ->setKey('owner')
199 ->setAliases(array('ownerPHID', 'assign', 'assigned'))
200 ->setLabel(pht('Assigned To'))
201 ->setBulkEditLabel(pht('Assign to'))
202 ->setDescription(pht('User who is responsible for the task.'))
203 ->setConduitDescription(pht('Reassign the task.'))
204 ->setConduitTypeDescription(
205 pht('New task assignee, or `null` to unassign.'))
206 ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
207 ->setIsCopyable(true)
208 ->setIsNullable(true)
209 ->setSingleValue($object->getOwnerPHID())
210 ->setCommentActionLabel(pht('Assign / Claim'))
211 ->setCommentActionValue($owner_value),
212 id(new PhabricatorSelectEditField())
213 ->setKey('status')
214 ->setLabel(pht('Status'))
215 ->setBulkEditLabel(pht('Set status to'))
216 ->setDescription(pht('Status of the task.'))
217 ->setConduitDescription(pht('Change the task status.'))
218 ->setConduitTypeDescription(pht('New task status constant.'))
219 ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
220 ->setIsCopyable(true)
221 ->setValue($object->getStatus())
222 ->setOptions($status_map)
223 ->setCommentActionLabel(pht('Change Status'))
224 ->setCommentActionValue($default_status),
225 id(new PhabricatorSelectEditField())
226 ->setKey('priority')
227 ->setLabel(pht('Priority'))
228 ->setBulkEditLabel(pht('Set priority to'))
229 ->setDescription(pht('Priority of the task.'))
230 ->setConduitDescription(pht('Change the priority of the task.'))
231 ->setConduitTypeDescription(pht('New task priority constant.'))
232 ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
233 ->setIsCopyable(true)
234 ->setValue($object->getPriorityKeyword())
235 ->setOptions($priority_map)
236 ->setOptionAliases($alias_map)
237 ->setCommentActionLabel(pht('Change Priority')),
238 );
239
240 if (ManiphestTaskPoints::getIsEnabled()) {
241 $points_label = ManiphestTaskPoints::getPointsLabel();
242 $action_label = ManiphestTaskPoints::getPointsActionLabel();
243
244 $fields[] = id(new PhabricatorPointsEditField())
245 ->setKey('points')
246 ->setLabel($points_label)
247 ->setBulkEditLabel($action_label)
248 ->setDescription(pht('Point value of the task.'))
249 ->setConduitDescription(pht('Change the task point value.'))
250 ->setConduitTypeDescription(pht('New task point value.'))
251 ->setTransactionType(ManiphestTaskPointsTransaction::TRANSACTIONTYPE)
252 ->setIsCopyable(true)
253 ->setValue($object->getPoints())
254 ->setCommentActionLabel($action_label);
255 }
256
257 $fields[] = id(new PhabricatorRemarkupEditField())
258 ->setKey('description')
259 ->setLabel(pht('Description'))
260 ->setBulkEditLabel(pht('Set description to'))
261 ->setDescription(pht('Task description.'))
262 ->setConduitDescription(pht('Update the task description.'))
263 ->setConduitTypeDescription(pht('New task description.'))
264 ->setTransactionType(ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE)
265 ->setValue($object->getDescription())
266 ->setPreviewPanel(
267 id(new PHUIRemarkupPreviewPanel())
268 ->setHeader(pht('Description Preview')));
269
270 $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
271 $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
272 $commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST;
273
274 $src_phid = $object->getPHID();
275 if ($src_phid) {
276 $edge_query = id(new PhabricatorEdgeQuery())
277 ->withSourcePHIDs(array($src_phid))
278 ->withEdgeTypes(
279 array(
280 $parent_type,
281 $subtask_type,
282 $commit_type,
283 ));
284 $edge_query->execute();
285
286 $parent_phids = $edge_query->getDestinationPHIDs(
287 array($src_phid),
288 array($parent_type));
289
290 $subtask_phids = $edge_query->getDestinationPHIDs(
291 array($src_phid),
292 array($subtask_type));
293
294 $commit_phids = $edge_query->getDestinationPHIDs(
295 array($src_phid),
296 array($commit_type));
297 } else {
298 $parent_phids = array();
299 $subtask_phids = array();
300 $commit_phids = array();
301 }
302
303 $fields[] = id(new PhabricatorHandlesEditField())
304 ->setKey('parents')
305 ->setLabel(pht('Parents'))
306 ->setDescription(pht('Parent tasks.'))
307 ->setConduitDescription(pht('Change the parents of this task.'))
308 ->setConduitTypeDescription(pht('List of parent task PHIDs.'))
309 ->setUseEdgeTransactions(true)
310 ->setIsFormField(false)
311 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
312 ->setMetadataValue('edge:type', $parent_type)
313 ->setValue($parent_phids);
314
315 $fields[] = id(new PhabricatorHandlesEditField())
316 ->setKey('subtasks')
317 ->setLabel(pht('Subtasks'))
318 ->setDescription(pht('Subtasks.'))
319 ->setConduitDescription(pht('Change the subtasks of this task.'))
320 ->setConduitTypeDescription(pht('List of subtask PHIDs.'))
321 ->setUseEdgeTransactions(true)
322 ->setIsFormField(false)
323 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
324 ->setMetadataValue('edge:type', $subtask_type)
325 ->setValue($subtask_phids);
326
327 $fields[] = id(new PhabricatorHandlesEditField())
328 ->setKey('commits')
329 ->setLabel(pht('Commits'))
330 ->setDescription(pht('Related commits.'))
331 ->setConduitDescription(pht('Change the related commits for this task.'))
332 ->setConduitTypeDescription(pht('List of related commit PHIDs.'))
333 ->setUseEdgeTransactions(true)
334 ->setIsFormField(false)
335 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
336 ->setMetadataValue('edge:type', $commit_type)
337 ->setValue($commit_phids);
338
339 return $fields;
340 }
341
342 private function getTaskStatusMap(ManiphestTask $task) {
343 $status_map = ManiphestTaskStatus::getTaskStatusMap();
344
345 $current_status = $task->getStatus();
346
347 // If the current status is something we don't recognize (maybe an older
348 // status which was deleted), put a dummy entry in the status map so that
349 // saving the form doesn't destroy any data by accident.
350 if (idx($status_map, $current_status) === null) {
351 $status_map[$current_status] = pht('<Unknown: %s>', $current_status);
352 }
353
354 $dup_status = ManiphestTaskStatus::getDuplicateStatus();
355 foreach ($status_map as $status => $status_name) {
356 // Always keep the task's current status.
357 if ($status == $current_status) {
358 continue;
359 }
360
361 // Don't allow tasks to be changed directly into "Closed, Duplicate"
362 // status. Instead, you have to merge them. See T4819.
363 if ($status == $dup_status) {
364 unset($status_map[$status]);
365 continue;
366 }
367
368 // Don't let new or existing tasks be moved into a disabled status.
369 if (ManiphestTaskStatus::isDisabledStatus($status)) {
370 unset($status_map[$status]);
371 continue;
372 }
373 }
374
375 return $status_map;
376 }
377
378 private function getTaskPriorityMap(ManiphestTask $task) {
379 $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
380 $priority_keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
381 $current_priority = $task->getPriority();
382 $results = array();
383
384 foreach ($priority_map as $priority => $priority_name) {
385 $disabled = ManiphestTaskPriority::isDisabledPriority($priority);
386 if ($disabled && !($priority == $current_priority)) {
387 continue;
388 }
389
390 $keyword = head(idx($priority_keywords, $priority));
391 $results[$keyword] = $priority_name;
392 }
393
394 // If the current value isn't a legitimate one, put it in the dropdown
395 // anyway so saving the form doesn't cause any side effects.
396 if (idx($priority_map, $current_priority) === null) {
397 $results[ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD] = pht(
398 '<Unknown: %s>',
399 $current_priority);
400 }
401
402 return $results;
403 }
404
405 protected function newEditResponse(
406 AphrontRequest $request,
407 $object,
408 array $xactions) {
409
410 $response_type = $request->getStr('responseType');
411 $is_card = ($response_type === 'card');
412
413 if ($is_card) {
414 // Reload the task to make sure we pick up the final task state.
415 $viewer = $this->getViewer();
416 $task = id(new ManiphestTaskQuery())
417 ->setViewer($viewer)
418 ->withIDs(array($object->getID()))
419 ->needSubscriberPHIDs(true)
420 ->needProjectPHIDs(true)
421 ->executeOne();
422
423 return $this->buildCardResponse($task);
424 }
425
426 return parent::newEditResponse($request, $object, $xactions);
427 }
428
429 private function buildCardResponse(ManiphestTask $task) {
430 $controller = $this->getController();
431 $request = $controller->getRequest();
432 $viewer = $request->getViewer();
433
434 $column_phid = $request->getStr('columnPHID');
435
436 $visible_phids = $request->getStrList('visiblePHIDs');
437 if (!$visible_phids) {
438 $visible_phids = array();
439 }
440
441 $column = id(new PhabricatorProjectColumnQuery())
442 ->setViewer($viewer)
443 ->withPHIDs(array($column_phid))
444 ->executeOne();
445 if (!$column) {
446 return new Aphront404Response();
447 }
448
449 $board_phid = $column->getProjectPHID();
450 $object_phid = $task->getPHID();
451
452 $order = $request->getStr('order');
453 if ($order) {
454 $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order);
455 $ordering = id(clone $ordering)
456 ->setViewer($viewer);
457 } else {
458 $ordering = null;
459 }
460
461 $engine = id(new PhabricatorBoardResponseEngine())
462 ->setViewer($viewer)
463 ->setBoardPHID($board_phid)
464 ->setUpdatePHIDs(array($object_phid))
465 ->setVisiblePHIDs($visible_phids);
466
467 if ($ordering) {
468 $engine->setOrdering($ordering);
469 }
470
471 return $engine->buildResponse();
472 }
473
474 private function getColumnMap(ManiphestTask $task) {
475 $phid = $task->getPHID();
476 if (!$phid) {
477 return array();
478 }
479
480 $board_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
481 $phid,
482 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
483 if (!$board_phids) {
484 return array();
485 }
486
487 $viewer = $this->getViewer();
488
489 $layout_engine = id(new PhabricatorBoardLayoutEngine())
490 ->setViewer($viewer)
491 ->setBoardPHIDs($board_phids)
492 ->setObjectPHIDs(array($task->getPHID()))
493 ->executeLayout();
494
495 $map = array();
496 foreach ($board_phids as $board_phid) {
497 $in_columns = $layout_engine->getObjectColumns($board_phid, $phid);
498 $in_columns = mpull($in_columns, null, 'getPHID');
499
500 $all_columns = $layout_engine->getColumns($board_phid);
501 if (!$all_columns) {
502 // This could be a project with no workboard, or a project the viewer
503 // does not have permission to see.
504 continue;
505 }
506
507 $board = head($all_columns)->getProject();
508
509 $options = array();
510 foreach ($all_columns as $column) {
511 $name = $column->getDisplayName();
512
513 $is_hidden = $column->isHidden();
514 $is_selected = isset($in_columns[$column->getPHID()]);
515
516 // Don't show hidden, subproject or milestone columns in this map
517 // unless the object is currently in the column.
518 $skip_column = ($is_hidden || $column->getProxyPHID());
519 if ($skip_column) {
520 if (!$is_selected) {
521 continue;
522 }
523 }
524
525 if ($is_hidden) {
526 $name = pht('(%s)', $name);
527 }
528
529 if ($is_selected) {
530 $name = pht("\xE2\x97\x8F %s", $name);
531 } else {
532 $name = pht("\xE2\x97\x8B %s", $name);
533 }
534
535 $option = array(
536 'key' => $column->getPHID(),
537 'label' => $name,
538 'selected' => (bool)$is_selected,
539 );
540
541 $options[] = $option;
542 }
543
544 $map[] = array(
545 'label' => $board->getDisplayName(),
546 'options' => $options,
547 );
548 }
549
550 $map = isort($map, 'label');
551 $map = array_values($map);
552
553 return $map;
554 }
555
556
557}