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