diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index e338450aa9..6133838975 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -1,352 +1,353 @@ getViewer()); } protected function newObjectQuery() { return id(new ManiphestTaskQuery()); } protected function getObjectCreateTitleText($object) { return pht('Create New Task'); } protected function getObjectEditTitleText($object) { return pht('Edit %s %s', $object->getMonogram(), $object->getTitle()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Task'); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('task/edit/'); } protected function getCommentViewHeaderText($object) { return pht('Weigh In'); } protected function getCommentViewButtonText($object) { return pht('Set Sail for Adventure'); } protected function getObjectViewURI($object) { return '/'.$object->getMonogram(); } protected function buildCustomEditFields($object) { $status_map = $this->getTaskStatusMap($object); $priority_map = $this->getTaskPriorityMap($object); if ($object->isClosed()) { $default_status = ManiphestTaskStatus::getDefaultStatus(); } else { $default_status = ManiphestTaskStatus::getDefaultClosedStatus(); } if ($object->getOwnerPHID()) { $owner_value = array($object->getOwnerPHID()); } else { $owner_value = array($this->getViewer()->getPHID()); } return array( id(new PhabricatorHandlesEditField()) ->setKey('parent') ->setLabel(pht('Parent Task')) ->setDescription(pht('Task to make this a subtask of.')) ->setConduitDescription(pht('Create as a subtask of another task.')) ->setConduitTypeDescription(pht('PHID of the parent task.')) ->setAliases(array('parentPHID')) ->setTransactionType(ManiphestTransaction::TYPE_PARENT) ->setHandleParameterType(new ManiphestTaskListHTTPParameterType()) ->setSingleValue(null) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false), id(new PhabricatorHandlesEditField()) ->setKey('column') ->setLabel(pht('Column')) ->setDescription(pht('Workboard column to create this task into.')) ->setConduitDescription(pht('Create into a workboard column.')) ->setConduitTypeDescription(pht('PHID of workboard column.')) ->setAliases(array('columnPHID')) ->setTransactionType(ManiphestTransaction::TYPE_COLUMN) ->setSingleValue(null) ->setIsInvisible(true) ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false), id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) ->setDescription(pht('Name of the task.')) ->setConduitDescription(pht('Rename the task.')) ->setConduitTypeDescription(pht('New task name.')) ->setTransactionType(ManiphestTransaction::TYPE_TITLE) ->setIsRequired(true) ->setValue($object->getTitle()), id(new PhabricatorUsersEditField()) ->setKey('owner') ->setAliases(array('ownerPHID', 'assign', 'assigned')) ->setLabel(pht('Assigned To')) ->setDescription(pht('User who is responsible for the task.')) ->setConduitDescription(pht('Reassign the task.')) ->setConduitTypeDescription( pht('New task owner, or `null` to unassign.')) ->setTransactionType(ManiphestTransaction::TYPE_OWNER) ->setIsCopyable(true) ->setSingleValue($object->getOwnerPHID()) ->setCommentActionLabel(pht('Assign / Claim')) ->setCommentActionValue($owner_value), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setDescription(pht('Status of the task.')) ->setConduitDescription(pht('Change the task status.')) ->setConduitTypeDescription(pht('New task status constant.')) ->setTransactionType(ManiphestTransaction::TYPE_STATUS) ->setIsCopyable(true) ->setValue($object->getStatus()) ->setOptions($status_map) ->setCommentActionLabel(pht('Change Status')) ->setCommentActionValue($default_status), id(new PhabricatorSelectEditField()) ->setKey('priority') ->setLabel(pht('Priority')) ->setDescription(pht('Priority of the task.')) ->setConduitDescription(pht('Change the priority of the task.')) ->setConduitTypeDescription(pht('New task priority constant.')) ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) ->setIsCopyable(true) ->setValue($object->getPriority()) ->setOptions($priority_map) ->setCommentActionLabel(pht('Change Priority')), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Task description.')) ->setConduitDescription(pht('Update the task description.')) ->setConduitTypeDescription(pht('New task description.')) ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) ->setValue($object->getDescription()) ->setPreviewPanel( id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Description Preview'))), ); } private function getTaskStatusMap(ManiphestTask $task) { $status_map = ManiphestTaskStatus::getTaskStatusMap(); $current_status = $task->getStatus(); // If the current status is something we don't recognize (maybe an older // status which was deleted), put a dummy entry in the status map so that // saving the form doesn't destroy any data by accident. if (idx($status_map, $current_status) === null) { $status_map[$current_status] = pht('', $current_status); } $dup_status = ManiphestTaskStatus::getDuplicateStatus(); foreach ($status_map as $status => $status_name) { // Always keep the task's current status. if ($status == $current_status) { continue; } // Don't allow tasks to be changed directly into "Closed, Duplicate" // status. Instead, you have to merge them. See T4819. if ($status == $dup_status) { unset($status_map[$status]); continue; } // Don't let new or existing tasks be moved into a disabled status. if (ManiphestTaskStatus::isDisabledStatus($status)) { unset($status_map[$status]); continue; } } return $status_map; } private function getTaskPriorityMap(ManiphestTask $task) { $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); $current_priority = $task->getPriority(); // If the current value isn't a legitimate one, put it in the dropdown // anyway so saving the form doesn't cause a side effects. if (idx($priority_map, $current_priority) === null) { $priority_map[$current_priority] = pht( '', $current_priority); } foreach ($priority_map as $priority => $priority_name) { // Always keep the current priority. if ($priority == $current_priority) { continue; } if (ManiphestTaskPriority::isDisabledPriority($priority)) { unset($priority_map[$priority]); continue; } } return $priority_map; } protected function newEditResponse( AphrontRequest $request, $object, array $xactions) { if ($request->isAjax()) { // Reload the task to make sure we pick up the final task state. $viewer = $this->getViewer(); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withIDs(array($object->getID())) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->executeOne(); switch ($request->getStr('responseType')) { case 'card': return $this->buildCardResponse($task); default: return $this->buildListResponse($task); } } return parent::newEditResponse($request, $object, $xactions); } private function buildListResponse(ManiphestTask $task) { $controller = $this->getController(); $payload = array( 'tasks' => $controller->renderSingleTask($task), 'data' => array(), ); return id(new AphrontAjaxResponse())->setContent($payload); } private function buildCardResponse(ManiphestTask $task) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $request->getViewer(); $column_phid = $request->getStr('columnPHID'); $order = $request->getStr('order'); $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withPHIDs(array($column_phid)) ->executeOne(); if (!$column) { return new Aphront404Response(); } // If the workboard's project has been removed from the card's project // list, we are going to remove it from the board completely. $project_map = array_fuse($task->getProjectPHIDs()); $remove_card = empty($project_map[$column->getProjectPHID()]); $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) - ->withColumns(array($column)) + ->withBoardPHIDs(array($column->getProjectPHID())) + ->withColumnPHIDs(array($column->getPHID())) ->execute(); $task_phids = mpull($positions, 'getObjectPHID'); $column_tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs($task_phids) ->needProjectPHIDs(true) ->execute(); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { // TODO: This is a little bit awkward, because PHP and JS use // slightly different sort order parameters to achieve the same // effect. It would be good to unify this a bit at some point. $sort_map = array(); foreach ($positions as $position) { $sort_map[$position->getObjectPHID()] = array( -$position->getSequence(), $position->getID(), ); } } else { $sort_map = mpull( $column_tasks, 'getPrioritySortVector', 'getPHID'); } $data = array( 'removeFromBoard' => $remove_card, 'sortMap' => $sort_map, ); // TODO: This should just use HandlePool once we get through the EditEngine // transition. $owner = null; if ($task->getOwnerPHID()) { $owner = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($task->getOwnerPHID())) ->executeOne(); } $tasks = id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setTask($task) ->setOwner($owner) ->setProject($column->getProject()) ->setCanEdit(true) ->getItem(); $tasks->addClass('phui-workcard'); $payload = array( 'tasks' => $tasks, 'data' => $data, ); return id(new AphrontAjaxResponse())->setContent($payload); } } diff --git a/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php b/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php index 3348b4054d..438c558e6e 100644 --- a/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php +++ b/src/applications/project/query/PhabricatorProjectColumnPositionQuery.php @@ -1,306 +1,77 @@ ids = $ids; return $this; } public function withBoardPHIDs(array $board_phids) { $this->boardPHIDs = $board_phids; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } - /** - * Find objects in specific columns. - * - * NOTE: Using this method activates logic which constructs virtual - * column positions for objects not in any column, if you pass a default - * column. Normally these results are not returned. - * - * @param list Columns to look for objects in. - * @return this - */ - public function withColumns(array $columns) { - assert_instances_of($columns, 'PhabricatorProjectColumn'); - $this->columns = $columns; - return $this; - } - - public function needColumns($need_columns) { - $this->needColumns = true; - return $this; - } - - - /** - * Skip implicit creation of column positions which are implied but do not - * yet exist. - * - * This is primarily useful internally. - * - * @param bool True to skip implicit creation of column positions. - * @return this - */ - public function setSkipImplicitCreate($skip) { - $this->skipImplicitCreate = $skip; + public function withColumnPHIDs(array $column_phids) { + $this->columnPHIDs = $column_phids; return $this; } - // NOTE: For now, boards are always attached to projects. However, they might - // not be in the future. This generalization just anticipates a future where - // we let other types of objects (like users) have boards, or let boards - // contain other types of objects. - - private function newPositionObject() { + public function newResultObject() { return new PhabricatorProjectColumnPosition(); } - private function newColumnQuery() { - return new PhabricatorProjectColumnQuery(); - } - - private function getBoardMembershipEdgeTypes() { - return array( - PhabricatorProjectProjectHasObjectEdgeType::EDGECONST, - ); - } - - private function getBoardMembershipPHIDTypes() { - return array( - ManiphestTaskPHIDType::TYPECONST, - ); - } - protected function loadPage() { - $table = $this->newPositionObject(); - $conn_r = $table->establishConnection('r'); - - // We're going to find results by combining two queries: one query finds - // objects on a board column, while the other query finds objects not on - // any board column and virtually puts them on the default column. - - $unions = array(); - - // First, find all the stuff that's actually on a column. - - $unions[] = qsprintf( - $conn_r, - 'SELECT * FROM %T %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r)); - - // If we have a default column, find all the stuff that's not in any - // column and put it in the default column. - - $must_type_filter = false; - if ($this->columns && !$this->skipImplicitCreate) { - $default_map = array(); - foreach ($this->columns as $column) { - if ($column->isDefaultColumn()) { - $default_map[$column->getProjectPHID()] = $column->getPHID(); - } - } - - if ($default_map) { - $where = array(); - - // Find the edges attached to the boards we have default columns for. - - $where[] = qsprintf( - $conn_r, - 'e.src IN (%Ls)', - array_keys($default_map)); - - // Find only edges which describe a board relationship. - - $where[] = qsprintf( - $conn_r, - 'e.type IN (%Ld)', - $this->getBoardMembershipEdgeTypes()); - - if ($this->boardPHIDs !== null) { - // This should normally be redundant, but construct it anyway if - // the caller has told us to. - $where[] = qsprintf( - $conn_r, - 'e.src IN (%Ls)', - $this->boardPHIDs); - } - - if ($this->objectPHIDs !== null) { - $where[] = qsprintf( - $conn_r, - 'e.dst IN (%Ls)', - $this->objectPHIDs); - } - - $where[] = qsprintf( - $conn_r, - 'p.id IS NULL'); - - $where = $this->formatWhereClause($where); - - $unions[] = qsprintf( - $conn_r, - 'SELECT NULL id, e.src boardPHID, NULL columnPHID, e.dst objectPHID, - 0 sequence - FROM %T e LEFT JOIN %T p - ON e.src = p.boardPHID AND e.dst = p.objectPHID - %Q', - PhabricatorEdgeConfig::TABLE_NAME_EDGE, - $table->getTableName(), - $where); - - $must_type_filter = true; - } - } - - $data = queryfx_all( - $conn_r, - '%Q %Q %Q', - implode(' UNION ALL ', $unions), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - // If we've picked up objects not in any column, we need to filter out any - // matched objects which have the wrong edge type. - if ($must_type_filter) { - $allowed_types = array_fuse($this->getBoardMembershipPHIDTypes()); - foreach ($data as $id => $row) { - if ($row['columnPHID'] === null) { - $object_phid = $row['objectPHID']; - if (empty($allowed_types[phid_get_type($object_phid)])) { - unset($data[$id]); - } - } - } - } - - $positions = $table->loadAllFromArray($data); - - // Find the implied positions which don't exist yet. If there are any, - // we're going to create them. - $create = array(); - foreach ($positions as $position) { - if ($position->getColumnPHID() === null) { - $column_phid = idx($default_map, $position->getBoardPHID()); - $position->setColumnPHID($column_phid); - - $create[] = $position; - } - } - - if ($create) { - // If we're adding several objects to a column, insert the column - // position objects in object ID order. This means that newly added - // objects float to the top, and when a group of newly added objects - // float up at the same time, the most recently created ones end up - // highest in the list. - - $objects = id(new PhabricatorObjectQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs(mpull($create, 'getObjectPHID')) - ->execute(); - $objects = mpull($objects, null, 'getPHID'); - $objects = msort($objects, 'getID'); - - $create = mgroup($create, 'getObjectPHID'); - $create = array_select_keys($create, array_keys($objects)) + $create; - - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - foreach ($create as $object_phid => $create_positions) { - foreach ($create_positions as $create_position) { - $create_position->save(); - } - } - - unset($unguarded); - } - - return $positions; - } - - protected function willFilterPage(array $page) { - - if ($this->needColumns) { - $column_phids = mpull($page, 'getColumnPHID'); - $columns = $this->newColumnQuery() - ->setParentQuery($this) - ->setViewer($this->getViewer()) - ->withPHIDs($column_phids) - ->execute(); - $columns = mpull($columns, null, 'getPHID'); - - foreach ($page as $key => $position) { - $column = idx($columns, $position->getColumnPHID()); - if (!$column) { - unset($page[$key]); - continue; - } - - $position->attachColumn($column); - } - } - - return $page; + return $this->loadStandardPage($this->newResultObject()); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->boardPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'boardPHID IN (%Ls)', $this->boardPHIDs); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } - if ($this->columns !== null) { + if ($this->columnPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'columnPHID IN (%Ls)', - mpull($this->columns, 'getPHID')); + $this->columnPHIDs); } - // NOTE: Explicitly not building the paging clause here, since it won't - // work with the UNION. - - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } }