diff --git a/src/applications/project/controller/PhabricatorProjectBoardReorderController.php b/src/applications/project/controller/PhabricatorProjectBoardReorderController.php index 425c27b5f0..011bbd069a 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardReorderController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardReorderController.php @@ -1,138 +1,149 @@ getViewer(); $projectid = $request->getURIData('projectID'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($projectid)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $project_id = $project->getID(); $board_uri = $this->getApplicationURI("board/{$project_id}/"); $reorder_uri = $this->getApplicationURI("board/{$project_id}/reorder/"); if ($request->isFormPost()) { // User clicked "Done", make sure the page reloads to show the new // column order. return id(new AphrontRedirectResponse())->setURI($board_uri); } $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); $columns = msort($columns, 'getSequence'); $column_phid = $request->getStr('columnPHID'); if ($column_phid && $request->validateCSRF()) { $columns = mpull($columns, null, 'getPHID'); if (empty($columns[$column_phid])) { return new Aphront404Response(); } $target_column = $columns[$column_phid]; $new_sequence = $request->getInt('sequence'); if ($new_sequence < 0) { return new Aphront404Response(); } // TODO: For now, we're not recording any transactions here. We probably // should, but this sort of edit is extremely trivial. // Resequence the columns so that the moved column has the correct // sequence number. Move columns after it up one place in the sequence. $new_map = array(); foreach ($columns as $phid => $column) { $value = $column->getSequence(); if ($column->getPHID() == $column_phid) { $value = $new_sequence; } else if ($column->getSequence() >= $new_sequence) { $value = $value + 1; } $new_map[$phid] = $value; } // Sort the columns into their new ordering. asort($new_map); // Now, compact the ordering and adjust any columns that need changes. $project->openTransaction(); $sequence = 0; foreach ($new_map as $phid => $ignored) { $new_value = $sequence++; $cur_value = $columns[$phid]->getSequence(); if ($new_value != $cur_value) { $columns[$phid]->setSequence($new_value)->save(); } } $project->saveTransaction(); return id(new AphrontAjaxResponse())->setContent( array( 'sequenceMap' => mpull($columns, 'getSequence', 'getPHID'), )); } $list_id = celerity_generate_unique_node_id(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setID($list_id) ->setFlush(true); foreach ($columns as $column) { + // Don't allow milestone columns to be reordered. + $proxy = $column->getProxy(); + if ($proxy && $proxy->isMilestone()) { + continue; + } + + // At least for now, don't show subproject column. + if ($proxy) { + continue; + } + $item = id(new PHUIObjectItemView()) ->setHeader($column->getDisplayName()) - ->addIcon('none', $column->getDisplayType()); + ->addIcon($column->getHeaderIcon(), $column->getDisplayType()); if ($column->isHidden()) { $item->setDisabled(true); } $item->setGrippable(true); $item->addSigil('board-column'); $item->setMetadata( array( 'columnPHID' => $column->getPHID(), 'columnSequence' => $column->getSequence(), )); $list->addItem($item); } Javelin::initBehavior( 'reorder-columns', array( 'listID' => $list_id, 'reorderURI' => $reorder_uri, )); $note = id(new PHUIInfoView()) ->appendChild(pht('Drag and drop columns to reorder them.')) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); return $this->newDialog() ->setTitle(pht('Reorder Columns')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild($note) ->appendChild($list) ->addSubmitButton(pht('Done')); } } diff --git a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php index ca25c089fb..e008c832d9 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php @@ -1,123 +1,130 @@ getViewer(); $id = $request->getURIData('id'); $project_id = $request->getURIData('projectID'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, )) ->withIDs(array($project_id)) ->needImages(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); + $project_id = $project->getID(); + $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, )) ->executeOne(); if (!$column) { return new Aphront404Response(); } $timeline = $this->buildTransactionTimeline( $column, new PhabricatorProjectColumnTransactionQuery()); $timeline->setShouldTerminate(true); $title = $column->getDisplayName(); $header = $this->buildHeaderView($column); $actions = $this->buildActionView($column); $properties = $this->buildPropertyView($column, $actions); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$project_id}/"); + $crumbs->addTextCrumb(pht('Column: %s', $title)); + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $nav = $this->getProfileMenu(); return $this->newPage() ->setTitle($title) ->setNavigation($nav) + ->setCrumbs($crumbs) ->appendChild( array( $box, $timeline, )); } private function buildHeaderView(PhabricatorProjectColumn $column) { $viewer = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($column->getDisplayName()) ->setPolicyObject($column); if ($column->isHidden()) { $header->setStatus('fa-ban', 'dark', pht('Hidden')); } return $header; } private function buildActionView(PhabricatorProjectColumn $column) { $viewer = $this->getRequest()->getUser(); $id = $column->getID(); $project_id = $this->getProject()->getID(); $base_uri = '/board/'.$project_id.'/'; $actions = id(new PhabricatorActionListView()) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $column, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Column')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI($base_uri.'edit/'.$id.'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); return $actions; } private function buildPropertyView( PhabricatorProjectColumn $column, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($column) ->setActionList($actions); $limit = $column->getPointLimit(); $properties->addProperty( pht('Point Limit'), $limit ? $limit : pht('No Limit')); return $properties; } } diff --git a/src/applications/project/controller/PhabricatorProjectColumnEditController.php b/src/applications/project/controller/PhabricatorProjectColumnEditController.php index 77f90b56cb..5ebc721c69 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnEditController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnEditController.php @@ -1,154 +1,156 @@ getViewer(); $id = $request->getURIData('id'); $project_id = $request->getURIData('projectID'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($project_id)) ->needImages(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $is_new = ($id ? false : true); if (!$is_new) { $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$column) { return new Aphront404Response(); } } else { $column = PhabricatorProjectColumn::initializeNewColumn($viewer); } $e_name = null; $e_limit = null; $v_limit = $column->getPointLimit(); $v_name = $column->getName(); $validation_exception = null; $base_uri = '/board/'.$project_id.'/'; if ($is_new) { // we want to go back to the board $view_uri = $this->getApplicationURI($base_uri); } else { $view_uri = $this->getApplicationURI($base_uri.'column/'.$id.'/'); } if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_limit = $request->getStr('limit'); if ($is_new) { $column->setProjectPHID($project->getPHID()); $column->attachProject($project); $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); $new_sequence = 1; if ($columns) { $values = mpull($columns, 'getSequence'); $new_sequence = max($values) + 1; } $column->setSequence($new_sequence); } $xactions = array(); - $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; - $xactions[] = id(new PhabricatorProjectColumnTransaction()) - ->setTransactionType($type_name) - ->setNewValue($v_name); + if (!$column->getProxy()) { + $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; + $xactions[] = id(new PhabricatorProjectColumnTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + } $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; $xactions[] = id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_limit) ->setNewValue($v_limit); try { $editor = id(new PhabricatorProjectColumnTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($column, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $e_name = $ex->getShortMessage($type_name); $e_limit = $ex->getShortMessage($type_limit); $validation_exception = $ex; } } - $form = new AphrontFormView(); - $form - ->setUser($request->getUser()) - ->appendChild( + $form = id(new AphrontFormView()) + ->setUser($request->getUser()); + + if (!$column->getProxy()) { + $form->appendChild( id(new AphrontFormTextControl()) ->setValue($v_name) ->setLabel(pht('Name')) ->setName('name') - ->setError($e_name) - ->setCaption( - pht('This will be displayed as the header of the column.'))) - ->appendChild( - id(new AphrontFormTextControl()) - ->setValue($v_limit) - ->setLabel(pht('Point Limit')) - ->setName('limit') - ->setError($e_limit) - ->setCaption( - pht('Maximum number of points of tasks allowed in the column.'))); + ->setError($e_name)); + } + $form->appendChild( + id(new AphrontFormTextControl()) + ->setValue($v_limit) + ->setLabel(pht('Point Limit')) + ->setName('limit') + ->setError($e_limit) + ->setCaption( + pht('Maximum number of points of tasks allowed in the column.'))); if ($is_new) { $title = pht('Create Column'); $submit = pht('Create Column'); } else { $title = pht('Edit %s', $column->getDisplayName()); $submit = pht('Save Column'); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue($submit) ->addCancelButton($view_uri)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setValidationException($validation_exception) ->setForm($form); $nav = $this->getProfileMenu(); return $this->newPage() ->setTitle($title) ->setNavigation($nav) ->appendChild($form_box); } } diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 31be95816b..4415e2fdf9 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -1,580 +1,582 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setBoardPHIDs(array $board_phids) { $this->boardPHIDs = array_fuse($board_phids); return $this; } public function getBoardPHIDs() { return $this->boardPHIDs; } public function setObjectPHIDs(array $object_phids) { $this->objectPHIDs = array_fuse($object_phids); return $this; } public function getObjectPHIDs() { return $this->objectPHIDs; } public function executeLayout() { $viewer = $this->getViewer(); $boards = $this->loadBoards(); if (!$boards) { return $this; } $columns = $this->loadColumns($boards); $positions = $this->loadPositions($boards); foreach ($boards as $board_phid => $board) { $board_columns = idx($columns, $board_phid); // Don't layout boards with no columns. These boards need to be formally // created first. if (!$columns) { continue; } $board_positions = idx($positions, $board_phid, array()); $this->layoutBoard($board, $board_columns, $board_positions); } return $this; } public function getColumns($board_phid) { $columns = idx($this->boardLayout, $board_phid, array()); return array_select_keys($this->columnMap, array_keys($columns)); } public function getColumnObjectPHIDs($board_phid, $column_phid) { $columns = idx($this->boardLayout, $board_phid, array()); $positions = idx($columns, $column_phid, array()); return mpull($positions, 'getObjectPHID'); } public function getObjectColumns($board_phid, $object_phid) { $board_map = idx($this->objectColumnMap, $board_phid, array()); $column_phids = idx($board_map, $object_phid); if (!$column_phids) { return array(); } return array_select_keys($this->columnMap, $column_phids); } public function queueRemovePosition( $board_phid, $column_phid, $object_phid) { $board_layout = idx($this->boardLayout, $board_phid, array()); $positions = idx($board_layout, $column_phid, array()); $position = idx($positions, $object_phid); if ($position) { $this->remQueue[] = $position; // If this position hasn't been saved yet, get it out of the add queue. if (!$position->getID()) { foreach ($this->addQueue as $key => $add_position) { if ($add_position === $position) { unset($this->addQueue[$key]); } } } } unset($this->boardLayout[$board_phid][$column_phid][$object_phid]); return $this; } public function queueAddPositionBefore( $board_phid, $column_phid, $object_phid, $before_phid) { return $this->queueAddPositionRelative( $board_phid, $column_phid, $object_phid, $before_phid, true); } public function queueAddPositionAfter( $board_phid, $column_phid, $object_phid, $after_phid) { return $this->queueAddPositionRelative( $board_phid, $column_phid, $object_phid, $after_phid, false); } public function queueAddPosition( $board_phid, $column_phid, $object_phid) { return $this->queueAddPositionRelative( $board_phid, $column_phid, $object_phid, null, true); } private function queueAddPositionRelative( $board_phid, $column_phid, $object_phid, $relative_phid, $is_before) { $board_layout = idx($this->boardLayout, $board_phid, array()); $positions = idx($board_layout, $column_phid, array()); // Check if the object is already in the column, and remove it if it is. $object_position = idx($positions, $object_phid); unset($positions[$object_phid]); if (!$object_position) { $object_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($column_phid) ->setObjectPHID($object_phid); } $found = false; if (!$positions) { $object_position->setSequence(0); } else { foreach ($positions as $position) { if (!$found) { if ($relative_phid === null) { $is_match = true; } else { $position_phid = $position->getObjectPHID(); $is_match = ($relative_phid == $position_phid); } if ($is_match) { $found = true; $sequence = $position->getSequence(); if (!$is_before) { $sequence++; } $object_position->setSequence($sequence++); if (!$is_before) { // If we're inserting after this position, continue the loop so // we don't update it. continue; } } } if ($found) { $position->setSequence($sequence++); $this->addQueue[] = $position; } } } if ($relative_phid && !$found) { throw new Exception( pht( 'Unable to find object "%s" in column "%s" on board "%s".', $relative_phid, $column_phid, $board_phid)); } $this->addQueue[] = $object_position; $positions[$object_phid] = $object_position; $positions = msort($positions, 'getOrderingKey'); $this->boardLayout[$board_phid][$column_phid] = $positions; return $this; } public function applyPositionUpdates() { foreach ($this->remQueue as $position) { if ($position->getID()) { $position->delete(); } } $this->remQueue = array(); $adds = array(); $updates = array(); foreach ($this->addQueue as $position) { $id = $position->getID(); if ($id) { $updates[$id] = $position; } else { $adds[] = $position; } } $this->addQueue = array(); $table = new PhabricatorProjectColumnPosition(); $conn_w = $table->establishConnection('w'); $pairs = array(); foreach ($updates as $id => $position) { // This is ugly because MySQL gets upset with us if it is configured // strictly and we attempt inserts which can't work. We'll never actually // do these inserts since they'll always collide (triggering the ON // DUPLICATE KEY logic), so we just provide dummy values in order to get // there. $pairs[] = qsprintf( $conn_w, '(%d, %d, "", "", "")', $id, $position->getSequence()); } if ($pairs) { queryfx( $conn_w, 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', $table->getTableName(), implode(', ', $pairs)); } foreach ($adds as $position) { $position->save(); } return $this; } private function loadBoards() { $viewer = $this->getViewer(); $board_phids = $this->getBoardPHIDs(); $boards = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs($board_phids) ->execute(); $boards = mpull($boards, null, 'getPHID'); foreach ($boards as $key => $board) { if (!$board->getHasWorkboard()) { unset($boards[$key]); } } return $boards; } private function loadColumns(array $boards) { $viewer = $this->getViewer(); $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array_keys($boards)) ->execute(); - $columns = msort($columns, 'getSequence'); + $columns = msort($columns, 'getOrderingKey'); $columns = mpull($columns, null, 'getPHID'); $need_children = array(); foreach ($boards as $phid => $board) { if ($board->getHasMilestones() || $board->getHasSubprojects()) { $need_children[] = $phid; } } if ($need_children) { $children = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withParentProjectPHIDs($need_children) ->execute(); $children = mpull($children, null, 'getPHID'); $children = mgroup($children, 'getParentProjectPHID'); } else { $children = array(); } $columns = mgroup($columns, 'getProjectPHID'); foreach ($boards as $board_phid => $board) { $board_columns = idx($columns, $board_phid, array()); // If the project has milestones, create any missing columns. if ($board->getHasMilestones() || $board->getHasSubprojects()) { $child_projects = idx($children, $board_phid, array()); $next_sequence = last($board_columns)->getSequence() + 1; $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); foreach ($child_projects as $child_phid => $child) { if (isset($proxy_columns[$child_phid])) { continue; } $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->attachProject($board) ->attachProxy($child) ->setSequence($next_sequence++) ->setProjectPHID($board_phid) ->setProxyPHID($child_phid); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $new_column->save(); unset($unguarded); $board_columns[$new_column->getPHID()] = $new_column; } } + $board_columns = msort($board_columns, 'getOrderingKey'); + $columns[$board_phid] = $board_columns; } foreach ($columns as $board_phid => $board_columns) { foreach ($board_columns as $board_column) { $column_phid = $board_column->getPHID(); $this->columnMap[$column_phid] = $board_column; } } return $columns; } private function loadPositions(array $boards) { $viewer = $this->getViewer(); $object_phids = $this->getObjectPHIDs(); if (!$object_phids) { return array(); } $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) ->withBoardPHIDs(array_keys($boards)) ->withObjectPHIDs($object_phids) ->execute(); $positions = msort($positions, 'getOrderingKey'); $positions = mgroup($positions, 'getBoardPHID'); return $positions; } private function layoutBoard( $board, array $columns, array $positions) { $viewer = $this->getViewer(); $board_phid = $board->getPHID(); $position_groups = mgroup($positions, 'getObjectPHID'); $layout = array(); foreach ($columns as $column) { $column_phid = $column->getPHID(); $layout[$column_phid] = array(); if ($column->isDefaultColumn()) { $default_phid = $column_phid; } } // Find all the columns which are proxies for other objects. $proxy_map = array(); foreach ($columns as $column) { $proxy_phid = $column->getProxyPHID(); if ($proxy_phid) { $proxy_map[$proxy_phid] = $column->getPHID(); } } $object_phids = $this->getObjectPHIDs(); // If we have proxies, we need to force cards into the correct proxy // columns. if ($proxy_map) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($object_phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); $project_phids = $edge_query->getDestinationPHIDs(); $project_phids = array_fuse($project_phids); } else { $project_phids = array(); } if ($project_phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); } else { $projects = array(); } // Build a map from every project that any task is tagged with to the // ancestor project which has a column on this board, if one exists. $ancestor_map = array(); foreach ($projects as $phid => $project) { if (isset($proxy_map[$phid])) { $ancestor_map[$phid] = $proxy_map[$phid]; } else { $seen = array($phid); foreach ($project->getAncestorProjects() as $ancestor) { $ancestor_phid = $ancestor->getPHID(); $seen[] = $ancestor_phid; if (isset($proxy_map[$ancestor_phid])) { foreach ($seen as $project_phid) { $ancestor_map[$project_phid] = $proxy_map[$ancestor_phid]; } } } } } foreach ($object_phids as $object_phid) { $positions = idx($position_groups, $object_phid, array()); // First, check for objects that have corresponding proxy columns. We're // going to overwrite normal column positions if a tag belongs to a proxy // column, since you can't be in normal columns if you're in proxy // columns. $proxy_hits = array(); if ($proxy_map) { $object_project_phids = $edge_query->getDestinationPHIDs( array( $object_phid, )); foreach ($object_project_phids as $project_phid) { if (isset($ancestor_map[$project_phid])) { $proxy_hits[] = $ancestor_map[$project_phid]; } } } if ($proxy_hits) { // TODO: For now, only one column hit is permissible. $proxy_hits = array_slice($proxy_hits, 0, 1); $proxy_hits = array_fuse($proxy_hits); // Check the object positions: we hope to find a position in each // column the object should be part of. We're going to drop any // invalid positions and create new positions where positions are // missing. foreach ($positions as $key => $position) { $column_phid = $position->getColumnPHID(); if (isset($proxy_hits[$column_phid])) { // Valid column, mark the position as found. unset($proxy_hits[$column_phid]); } else { // Invalid column, ignore the position. unset($positions[$key]); } } // Create new positions for anything we haven't found. foreach ($proxy_hits as $proxy_hit) { $new_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($proxy_hit) ->setObjectPHID($object_phid) ->setSequence(0); $this->addQueue[] = $new_position; $positions[] = $new_position; } } else { // Ignore any positions in columns which no longer exist. We don't // actively destory them because the rest of the code ignores them and // there's no real need to destroy the data. foreach ($positions as $key => $position) { $column_phid = $position->getColumnPHID(); if (empty($columns[$column_phid])) { unset($positions[$key]); } } // If the object has no position, put it on the default column. if (!$positions) { $new_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($default_phid) ->setObjectPHID($object_phid) ->setSequence(0); $this->addQueue[] = $new_position; $positions = array( $new_position, ); } } foreach ($positions as $position) { $column_phid = $position->getColumnPHID(); $layout[$column_phid][$object_phid] = $position; } } foreach ($layout as $column_phid => $map) { $map = msort($map, 'getOrderingKey'); $layout[$column_phid] = $map; foreach ($map as $object_phid => $position) { $this->objectColumnMap[$board_phid][$object_phid][] = $column_phid; } } $this->boardLayout[$board_phid] = $layout; } } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index a63312438c..0ab6ec89c0 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -1,219 +1,239 @@ setName('') - ->setStatus(self::STATUS_ACTIVE); + ->setStatus(self::STATUS_ACTIVE) + ->attachProxy(null); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'status' => 'uint32', 'sequence' => 'uint32', 'proxyPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('projectPHID', 'status', 'sequence'), ), 'key_sequence' => array( 'columns' => array('projectPHID', 'sequence'), ), 'key_proxy' => array( 'columns' => array('projectPHID', 'proxyPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProjectColumnPHIDType::TYPECONST); } public function attachProject(PhabricatorProject $project) { $this->project = $project; return $this; } public function getProject() { return $this->assertAttached($this->project); } public function attachProxy($proxy) { $this->proxy = $proxy; return $this; } public function getProxy() { return $this->assertAttached($this->proxy); } public function isDefaultColumn() { return (bool)$this->getProperty('isDefault'); } public function isHidden() { return ($this->getStatus() == self::STATUS_HIDDEN); } public function getDisplayName() { $proxy = $this->getProxy(); if ($proxy) { return $proxy->getProxyColumnName(); } $name = $this->getName(); if (strlen($name)) { return $name; } if ($this->isDefaultColumn()) { return pht('Backlog'); } return pht('Unnamed Column'); } public function getDisplayType() { if ($this->isDefaultColumn()) { return pht('(Default)'); } if ($this->isHidden()) { return pht('(Hidden)'); } return null; } public function getDisplayClass() { $proxy = $this->getProxy(); if ($proxy) { return $proxy->getProxyColumnClass(); } return null; } public function getHeaderIcon() { $proxy = $this->getProxy(); if ($proxy) { return $proxy->getProxyColumnIcon(); } if ($this->isHidden()) { return 'fa-eye-slash'; } return null; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getPointLimit() { return $this->getProperty('pointLimit'); } public function setPointLimit($limit) { $this->setProperty('pointLimit', $limit); return $this; } + public function getOrderingKey() { + $proxy = $this->getProxy(); + + // Normal columns and subproject columns go first, in a user-controlled + // order. + + // All the milestone columns go last, in their sequential order. + + if (!$proxy || !$proxy->isMilestone()) { + $group = 'A'; + $sequence = $this->getSequence(); + } else { + $group = 'B'; + $sequence = $proxy->getMilestoneNumber(); + } + + return sprintf('%s%012d', $group, $sequence); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProjectColumnTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorProjectColumnTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getProject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getProject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Users must be able to see a project to see its board.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } }