@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

Workboards - add column detail page

Summary: followup to D8544. This ends up creating an editor + transactions to get the job done.

Test Plan: made a column - saw a nice created transaction. edited the name - saw a nice name edit. deleted the column - saw a deleted transaction, updated "deleted" ui, and hte action change to activate. "Activated" the column and saw a transaction and updated UI. Tried to delete a column with tasks in it and got an error.

Reviewers: epriestley

Reviewed By: epriestley

Subscribers: epriestley, Korvin

Differential Revision: https://secure.phabricator.com/D8620

+697 -294
+21
resources/sql/autopatches/20140326.project.1.colxaction.sql
··· 1 + CREATE TABLE {$NAMESPACE}_project.project_columntransaction ( 2 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 3 + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 5 + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 6 + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, 7 + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, 8 + commentPHID VARCHAR(64) COLLATE utf8_bin, 9 + commentVersion INT UNSIGNED NOT NULL, 10 + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, 11 + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, 12 + newValue LONGTEXT NOT NULL COLLATE utf8_bin, 13 + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, 14 + metadata LONGTEXT NOT NULL COLLATE utf8_bin, 15 + dateCreated INT UNSIGNED NOT NULL, 16 + dateModified INT UNSIGNED NOT NULL, 17 + 18 + UNIQUE KEY `key_phid` (phid), 19 + KEY `key_object` (objectPHID) 20 + 21 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+12 -2
src/__phutil_library_map__.php
··· 1862 1862 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', 1863 1863 'PhabricatorProjectBoardDeleteController' => 'applications/project/controller/PhabricatorProjectBoardDeleteController.php', 1864 1864 'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php', 1865 + 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 1865 1866 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 1867 + 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 1866 1868 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 1869 + 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', 1870 + 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', 1871 + 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', 1867 1872 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 1868 1873 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 1869 1874 'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php', ··· 4668 4673 ), 4669 4674 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 4670 4675 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', 4671 - 'PhabricatorProjectBoardDeleteController' => 'PhabricatorProjectController', 4672 - 'PhabricatorProjectBoardEditController' => 'PhabricatorProjectController', 4676 + 'PhabricatorProjectBoardDeleteController' => 'PhabricatorProjectBoardController', 4677 + 'PhabricatorProjectBoardEditController' => 'PhabricatorProjectBoardController', 4678 + 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 4673 4679 'PhabricatorProjectColumn' => 4674 4680 array( 4675 4681 0 => 'PhabricatorProjectDAO', 4676 4682 1 => 'PhabricatorPolicyInterface', 4677 4683 ), 4684 + 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 4678 4685 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4686 + 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', 4687 + 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 4688 + 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 4679 4689 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 4680 4690 'PhabricatorProjectConfiguredCustomField' => 4681 4691 array(
+3 -1
src/applications/project/application/PhabricatorApplicationProject.php
··· 51 51 'picture/(?P<id>[1-9]\d*)/' => 52 52 'PhabricatorProjectEditPictureController', 53 53 'create/' => 'PhabricatorProjectCreateController', 54 - 'board/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectBoardController', 54 + 'board/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectBoardViewController', 55 55 'move/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectMoveController', 56 56 'board/(?P<projectID>[1-9]\d*)/edit/(?:(?P<id>\d+)/)?' 57 57 => 'PhabricatorProjectBoardEditController', 58 58 'board/(?P<projectID>[1-9]\d*)/delete/(?:(?P<id>\d+)/)?' 59 59 => 'PhabricatorProjectBoardDeleteController', 60 + 'board/(?P<projectID>[1-9]\d*)/column/(?:(?P<id>\d+)/)?' 61 + => 'PhabricatorProjectColumnDetailController', 60 62 'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/' 61 63 => 'PhabricatorProjectUpdateController', 62 64 'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController',
+11 -206
src/applications/project/controller/PhabricatorProjectBoardController.php
··· 1 1 <?php 2 2 3 - final class PhabricatorProjectBoardController 3 + abstract class PhabricatorProjectBoardController 4 4 extends PhabricatorProjectController { 5 5 6 - private $id; 7 - private $handles; 6 + private $project; 8 7 9 - public function shouldAllowPublic() { 10 - return true; 8 + protected function setProject(PhabricatorProject $project) { 9 + $this->project = $project; 10 + return $this; 11 11 } 12 - 13 - public function willProcessRequest(array $data) { 14 - $this->id = $data['id']; 12 + protected function getProject() { 13 + return $this->project; 15 14 } 16 15 17 - public function processRequest() { 18 - $request = $this->getRequest(); 19 - $viewer = $request->getUser(); 20 - 21 - $project = id(new PhabricatorProjectQuery()) 22 - ->setViewer($viewer) 23 - ->needImages(true) 24 - ->withIDs(array($this->id)) 25 - ->executeOne(); 26 - if (!$project) { 27 - return new Aphront404Response(); 28 - } 29 - 30 - $columns = id(new PhabricatorProjectColumnQuery()) 31 - ->setViewer($viewer) 32 - ->withProjectPHIDs(array($project->getPHID())) 33 - ->withStatuses(array(PhabricatorProjectColumn::STATUS_ACTIVE)) 34 - ->execute(); 35 - 36 - $columns = mpull($columns, null, 'getSequence'); 37 - 38 - // If there's no default column, create one now. 39 - if (empty($columns[0])) { 40 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 41 - $column = PhabricatorProjectColumn::initializeNewColumn($viewer) 42 - ->setSequence(0) 43 - ->setProjectPHID($project->getPHID()) 44 - ->save(); 45 - $column->attachProject($project); 46 - $columns[0] = $column; 47 - unset($unguarded); 48 - } 49 - 50 - ksort($columns); 51 - 52 - $tasks = id(new ManiphestTaskQuery()) 53 - ->setViewer($viewer) 54 - ->withAllProjects(array($project->getPHID())) 55 - ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) 56 - ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) 57 - ->execute(); 58 - $tasks = mpull($tasks, null, 'getPHID'); 59 - $task_phids = array_keys($tasks); 60 - 61 - if ($task_phids) { 62 - $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; 63 - $edge_query = id(new PhabricatorEdgeQuery()) 64 - ->withSourcePHIDs($task_phids) 65 - ->withEdgeTypes(array($edge_type)) 66 - ->withDestinationPHIDs(mpull($columns, 'getPHID')); 67 - $edge_query->execute(); 68 - } 69 - 70 - $task_map = array(); 71 - $default_phid = $columns[0]->getPHID(); 72 - foreach ($tasks as $task) { 73 - $task_phid = $task->getPHID(); 74 - $column_phids = $edge_query->getDestinationPHIDs(array($task_phid)); 75 - 76 - $column_phid = head($column_phids); 77 - $column_phid = nonempty($column_phid, $default_phid); 78 - 79 - $task_map[$column_phid][] = $task_phid; 80 - } 81 - 82 - $task_can_edit_map = id(new PhabricatorPolicyFilter()) 83 - ->setViewer($viewer) 84 - ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) 85 - ->apply($tasks); 86 - 87 - $board_id = celerity_generate_unique_node_id(); 88 - 89 - $board = id(new PHUIWorkboardView()) 90 - ->setUser($viewer) 91 - ->setFluidishLayout(true) 92 - ->setID($board_id); 93 - 94 - $this->initBehavior( 95 - 'project-boards', 96 - array( 97 - 'boardID' => $board_id, 98 - 'projectPHID' => $project->getPHID(), 99 - 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 100 - 'createURI' => '/maniphest/task/create/', 101 - )); 102 - 103 - $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); 104 - 105 - foreach ($columns as $column) { 106 - $panel = id(new PHUIWorkpanelView()) 107 - ->setHeader($column->getDisplayName()) 108 - ->setHeaderColor($column->getHeaderColor()); 109 - if (!$column->isDefaultColumn()) { 110 - $panel->setEditURI('edit/'.$column->getID().'/'); 111 - } 112 - $panel->setHeaderAction(id(new PHUIIconView()) 113 - ->setSpriteSheet(PHUIIconView::SPRITE_ACTIONS) 114 - ->setSpriteIcon('new-grey') 115 - ->setHref('/maniphest/task/create/') 116 - ->addSigil('column-add-task') 117 - ->setMetadata( 118 - array('columnPHID' => $column->getPHID()))); 119 - 120 - $cards = id(new PHUIObjectItemListView()) 121 - ->setUser($viewer) 122 - ->setCards(true) 123 - ->setFlush(true) 124 - ->setAllowEmptyList(true) 125 - ->addSigil('project-column') 126 - ->setMetadata( 127 - array( 128 - 'columnPHID' => $column->getPHID(), 129 - )); 130 - $task_phids = idx($task_map, $column->getPHID(), array()); 131 - foreach (array_select_keys($tasks, $task_phids) as $task) { 132 - $owner = null; 133 - if ($task->getOwnerPHID()) { 134 - $owner = $this->handles[$task->getOwnerPHID()]; 135 - } 136 - $can_edit = idx($task_can_edit_map, $task->getPHID(), false); 137 - $cards->addItem(id(new ProjectBoardTaskCard()) 138 - ->setViewer($viewer) 139 - ->setTask($task) 140 - ->setOwner($owner) 141 - ->setCanEdit($can_edit) 142 - ->getItem()); 143 - } 144 - $panel->setCards($cards); 145 - 146 - if (!$task_phids) { 147 - $cards->addClass('project-column-empty'); 148 - } 149 - 150 - $board->addPanel($panel); 151 - } 152 - 153 - $crumbs = $this->buildApplicationCrumbs(); 16 + protected function buildApplicationCrumbs() { 17 + $project = $this->getProject(); 18 + $crumbs = parent::buildApplicationCrumbs(); 154 19 $crumbs->addTextCrumb( 155 20 $project->getName(), 156 21 $this->getApplicationURI('view/'.$project->getID().'/')); 157 - $crumbs->addTextCrumb(pht('Board')); 158 - 159 - $can_edit = PhabricatorPolicyFilter::hasCapability( 160 - $viewer, 161 - $project, 162 - PhabricatorPolicyCapability::CAN_EDIT); 163 - 164 - $actions = id(new PhabricatorActionListView()) 165 - ->setUser($viewer) 166 - ->addAction( 167 - id(new PhabricatorActionView()) 168 - ->setName(pht('Add Column')) 169 - ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) 170 - ->setIcon('create') 171 - ->setDisabled(!$can_edit) 172 - ->setWorkflow(!$can_edit)) 173 - ->addAction( 174 - id(new PhabricatorActionView()) 175 - ->setName(pht('Delete Column')) 176 - ->setHref($this->getApplicationURI('board/'.$this->id.'/delete/')) 177 - ->setIcon('delete') 178 - ->setDisabled(!$can_edit) 179 - ->setWorkflow(!$can_edit)); 180 - 181 - $plist = id(new PHUIPropertyListView()); 182 - 183 - // TODO: Need this to get actions to render. 184 - $plist->addProperty( 185 - pht('Project Boards'), 186 - phutil_tag( 187 - 'em', 188 - array(), 189 - pht( 190 - 'This feature is beta, but should mostly work.'))); 191 - $plist->setActionList($actions); 192 - 193 - $header = id(new PHUIHeaderView()) 194 - ->setHeader($project->getName()) 195 - ->setUser($viewer) 196 - ->setImage($project->getProfileImageURI()) 197 - ->setPolicyObject($project); 198 - 199 - $box = id(new PHUIObjectBoxView()) 200 - ->setHeader($header) 201 - ->addPropertyList($plist); 202 - 203 - $board_box = id(new PHUIBoxView()) 204 - ->appendChild($board) 205 - ->addMargin(PHUI::MARGIN_LARGE); 206 - 207 - return $this->buildApplicationPage( 208 - array( 209 - $crumbs, 210 - $box, 211 - $board_box, 212 - ), 213 - array( 214 - 'title' => pht('%s Board', $project->getName()), 215 - 'device' => true, 216 - )); 22 + return $crumbs; 217 23 } 218 - 219 24 }
+65 -66
src/applications/project/controller/PhabricatorProjectBoardDeleteController.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorProjectBoardDeleteController 4 - extends PhabricatorProjectController { 4 + extends PhabricatorProjectBoardController { 5 5 6 6 private $id; 7 7 private $projectID; ··· 14 14 public function processRequest() { 15 15 $request = $this->getRequest(); 16 16 $viewer = $request->getUser(); 17 - 18 17 $project = id(new PhabricatorProjectQuery()) 19 18 ->setViewer($viewer) 20 19 ->requireCapabilities( ··· 28 27 if (!$project) { 29 28 return new Aphront404Response(); 30 29 } 30 + $this->setProject($project); 31 31 32 - $columns = id(new PhabricatorProjectColumnQuery()) 32 + $column = id(new PhabricatorProjectColumnQuery()) 33 33 ->setViewer($viewer) 34 - ->withProjectPHIDs(array($project->getPHID())) 35 - ->withStatuses(array(PhabricatorProjectColumn::STATUS_ACTIVE)) 34 + ->withIDs(array($this->id)) 36 35 ->requireCapabilities( 37 36 array( 38 37 PhabricatorPolicyCapability::CAN_VIEW, 39 38 PhabricatorPolicyCapability::CAN_EDIT)) 40 - ->execute(); 41 - 42 - if (!$columns) { 39 + ->executeOne(); 40 + if (!$column) { 43 41 return new Aphront404Response(); 44 42 } 45 43 46 - $columns = mpull($columns, null, 'getSequence'); 47 - $columns = mfilter($columns, 'isDefaultColumn', true); 48 - ksort($columns); 49 - $options = mpull($columns, 'getName', 'getPHID'); 50 - 51 - $view_uri = $this->getApplicationURI('/board/'.$this->projectID.'/'); 52 44 $error_view = null; 53 - if ($request->isFormPost()) { 54 - $columns = mpull($columns, null, 'getPHID'); 55 - $column_phid = $request->getStr('columnPHID'); 56 - $column = $columns[$column_phid]; 45 + $column_phid = $column->getPHID(); 46 + $has_task_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 47 + $column_phid, 48 + PhabricatorEdgeConfig::TYPE_COLUMN_HAS_OBJECT); 57 49 58 - $has_task_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 59 - $column_phid, 60 - PhabricatorEdgeConfig::TYPE_COLUMN_HAS_OBJECT); 50 + if ($has_task_phids) { 51 + $error_view = id(new AphrontErrorView()) 52 + ->setTitle(pht('Column has Tasks!')); 53 + if ($column->isDeleted()) { 54 + $error_view->setErrors(array(pht( 55 + 'A column can not be activated if it has tasks '. 56 + 'in it. Please remove the tasks and try again.'))); 57 + } else { 58 + $error_view->setErrors(array(pht( 59 + 'A column can not be deleted if it has tasks '. 60 + 'in it. Please remove the tasks and try again.'))); 61 + } 62 + } 61 63 62 - if ($has_task_phids) { 63 - $error_view = id(new AphrontErrorView()) 64 - ->setTitle(pht('Column has Tasks!')) 65 - ->setErrors(array(pht('A column can not be deleted if it has tasks '. 66 - 'in it. Please remove the tasks and try '. 67 - 'again.'))); 64 + $view_uri = $this->getApplicationURI( 65 + '/board/'.$this->projectID.'/column/'.$this->id.'/'); 66 + 67 + if ($request->isFormPost() && !$error_view) { 68 + if ($column->isDeleted()) { 69 + $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; 68 70 } else { 69 - $column->setStatus(PhabricatorProjectColumn::STATUS_DELETED); 70 - $column->save(); 71 + $new_status = PhabricatorProjectColumn::STATUS_DELETED; 72 + } 73 + 74 + $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; 75 + $xactions = array(id(new PhabricatorProjectColumnTransaction()) 76 + ->setTransactionType($type_status) 77 + ->setNewValue($new_status)); 78 + 79 + $editor = id(new PhabricatorProjectColumnTransactionEditor()) 80 + ->setActor($viewer) 81 + ->setContinueOnNoEffect(true) 82 + ->setContentSourceFromRequest($request) 83 + ->applyTransactions($column, $xactions); 71 84 72 - return id(new AphrontRedirectResponse())->setURI($view_uri); 73 - } 85 + return id(new AphrontRedirectResponse())->setURI($view_uri); 74 86 } 75 87 76 - $form = id(new AphrontFormView()) 77 - ->setUser($viewer) 78 - ->appendChild($error_view) 79 - ->appendChild(id(new AphrontFormSelectControl()) 80 - ->setName('columnPHID') 81 - ->setValue(head_key($options)) 82 - ->setOptions($options) 83 - ->setLabel(pht('Column'))); 84 - 85 - $title = pht('Delete Column'); 88 + if ($column->isDeleted()) { 89 + $title = pht('Activate Column'); 90 + } else { 91 + $title = pht('Delete Column'); 92 + } 86 93 $submit = $title; 94 + if ($error_view) { 95 + $body = $error_view; 96 + } else if ($column->isDeleted()) { 97 + $body = pht('Are you sure you want to activate this column?'); 98 + } else { 99 + $body = pht('Are you sure you want to delete this column?'); 100 + } 87 101 88 - $form->appendChild( 89 - id(new AphrontFormSubmitControl()) 90 - ->setValue($submit) 91 - ->addCancelButton($view_uri)); 92 - 93 - $crumbs = $this->buildApplicationCrumbs(); 94 - $crumbs->addTextCrumb( 95 - $project->getName(), 96 - $this->getApplicationURI('view/'.$project->getID().'/')); 97 - $crumbs->addTextCrumb( 98 - pht('Board'), 99 - $this->getApplicationURI('board/'.$project->getID().'/')); 100 - $crumbs->addTextCrumb($title); 102 + $dialog = id(new AphrontDialogView()) 103 + ->setUser($viewer) 104 + ->setWidth(AphrontDialogView::WIDTH_FORM) 105 + ->setTitle($title) 106 + ->appendChild($body) 107 + ->setDisableWorkflowOnCancel(true) 108 + ->addSubmitButton($title) 109 + ->addCancelButton($view_uri); 101 110 102 - $form_box = id(new PHUIObjectBoxView()) 103 - ->setHeaderText($title) 104 - ->setForm($form); 111 + return id(new AphrontDialogResponse()) 112 + ->setDialog($dialog); 105 113 106 - return $this->buildApplicationPage( 107 - array( 108 - $crumbs, 109 - $form_box, 110 - ), 111 - array( 112 - 'title' => $title, 113 - 'device' => true, 114 - )); 115 114 } 116 115 }
+26 -19
src/applications/project/controller/PhabricatorProjectBoardEditController.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorProjectBoardEditController 4 - extends PhabricatorProjectController { 4 + extends PhabricatorProjectBoardController { 5 5 6 6 private $id; 7 7 private $projectID; ··· 28 28 if (!$project) { 29 29 return new Aphront404Response(); 30 30 } 31 + $this->setProject($project); 31 32 32 33 $is_new = ($this->id ? false : true); 33 34 ··· 48 49 $column = PhabricatorProjectColumn::initializeNewColumn($viewer); 49 50 } 50 51 51 - $errors = array(); 52 - $e_name = true; 53 - $error_view = null; 54 - $view_uri = $this->getApplicationURI('/board/'.$this->projectID.'/'); 52 + $e_name = null; 53 + $validation_exception = null; 54 + $base_uri = '/board/'.$this->projectID.'/'; 55 + if ($is_new) { 56 + // we want to go back to the board 57 + $view_uri = $this->getApplicationURI($base_uri); 58 + } else { 59 + $view_uri = $this->getApplicationURI($base_uri.'column/'.$this->id.'/'); 60 + } 55 61 56 62 if ($request->isFormPost()) { 57 63 $new_name = $request->getStr('name'); 58 - $column->setName($new_name); 59 - 60 - if (!strlen($column->getName())) { 61 - $errors[] = pht('Column name is required.'); 62 - $e_name = pht('Required'); 63 - } else { 64 - $e_name = null; 65 - } 66 64 67 65 if ($is_new) { 68 66 $column->setProjectPHID($project->getPHID()); ··· 81 79 $column->setSequence($new_sequence); 82 80 } 83 81 84 - if (!$errors) { 85 - $column->save(); 82 + $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; 83 + $xactions = array(id(new PhabricatorProjectColumnTransaction()) 84 + ->setTransactionType($type_name) 85 + ->setNewValue($new_name)); 86 + 87 + try { 88 + $editor = id(new PhabricatorProjectColumnTransactionEditor()) 89 + ->setActor($viewer) 90 + ->setContinueOnNoEffect(true) 91 + ->setContentSourceFromRequest($request) 92 + ->applyTransactions($column, $xactions); 86 93 return id(new AphrontRedirectResponse())->setURI($view_uri); 94 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 95 + $e_name = $ex->getShortMessage($type_name); 96 + $validation_exception = $ex; 87 97 } 88 98 } 89 99 ··· 113 123 114 124 $crumbs = $this->buildApplicationCrumbs(); 115 125 $crumbs->addTextCrumb( 116 - $project->getName(), 117 - $this->getApplicationURI('view/'.$project->getID().'/')); 118 - $crumbs->addTextCrumb( 119 126 pht('Board'), 120 127 $this->getApplicationURI('board/'.$project->getID().'/')); 121 128 $crumbs->addTextCrumb($title); 122 129 123 130 $form_box = id(new PHUIObjectBoxView()) 124 131 ->setHeaderText($title) 125 - ->setFormErrors($errors) 132 + ->setValidationException($validation_exception) 126 133 ->setForm($form); 127 134 128 135 return $this->buildApplicationPage(
+210
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectBoardViewController 4 + extends PhabricatorProjectBoardController { 5 + 6 + private $id; 7 + private $handles; 8 + 9 + public function shouldAllowPublic() { 10 + return true; 11 + } 12 + 13 + public function willProcessRequest(array $data) { 14 + $this->id = $data['id']; 15 + } 16 + 17 + public function processRequest() { 18 + $request = $this->getRequest(); 19 + $viewer = $request->getUser(); 20 + 21 + $project = id(new PhabricatorProjectQuery()) 22 + ->setViewer($viewer) 23 + ->needImages(true) 24 + ->withIDs(array($this->id)) 25 + ->executeOne(); 26 + if (!$project) { 27 + return new Aphront404Response(); 28 + } 29 + $this->setProject($project); 30 + 31 + $columns = id(new PhabricatorProjectColumnQuery()) 32 + ->setViewer($viewer) 33 + ->withProjectPHIDs(array($project->getPHID())) 34 + ->withStatuses(array(PhabricatorProjectColumn::STATUS_ACTIVE)) 35 + ->execute(); 36 + 37 + $columns = mpull($columns, null, 'getSequence'); 38 + 39 + // If there's no default column, create one now. 40 + if (empty($columns[0])) { 41 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 42 + $column = PhabricatorProjectColumn::initializeNewColumn($viewer) 43 + ->setSequence(0) 44 + ->setProjectPHID($project->getPHID()) 45 + ->save(); 46 + $column->attachProject($project); 47 + $columns[0] = $column; 48 + unset($unguarded); 49 + } 50 + 51 + ksort($columns); 52 + 53 + $tasks = id(new ManiphestTaskQuery()) 54 + ->setViewer($viewer) 55 + ->withAllProjects(array($project->getPHID())) 56 + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) 57 + ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) 58 + ->execute(); 59 + $tasks = mpull($tasks, null, 'getPHID'); 60 + $task_phids = array_keys($tasks); 61 + 62 + if ($task_phids) { 63 + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; 64 + $edge_query = id(new PhabricatorEdgeQuery()) 65 + ->withSourcePHIDs($task_phids) 66 + ->withEdgeTypes(array($edge_type)) 67 + ->withDestinationPHIDs(mpull($columns, 'getPHID')); 68 + $edge_query->execute(); 69 + } 70 + 71 + $task_map = array(); 72 + $default_phid = $columns[0]->getPHID(); 73 + foreach ($tasks as $task) { 74 + $task_phid = $task->getPHID(); 75 + $column_phids = $edge_query->getDestinationPHIDs(array($task_phid)); 76 + 77 + $column_phid = head($column_phids); 78 + $column_phid = nonempty($column_phid, $default_phid); 79 + 80 + $task_map[$column_phid][] = $task_phid; 81 + } 82 + 83 + $task_can_edit_map = id(new PhabricatorPolicyFilter()) 84 + ->setViewer($viewer) 85 + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) 86 + ->apply($tasks); 87 + 88 + $board_id = celerity_generate_unique_node_id(); 89 + 90 + $board = id(new PHUIWorkboardView()) 91 + ->setUser($viewer) 92 + ->setFluidishLayout(true) 93 + ->setID($board_id); 94 + 95 + $this->initBehavior( 96 + 'project-boards', 97 + array( 98 + 'boardID' => $board_id, 99 + 'projectPHID' => $project->getPHID(), 100 + 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 101 + 'createURI' => '/maniphest/task/create/', 102 + )); 103 + 104 + $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); 105 + 106 + foreach ($columns as $column) { 107 + $panel = id(new PHUIWorkpanelView()) 108 + ->setHeader($column->getDisplayName()) 109 + ->setHeaderColor($column->getHeaderColor()); 110 + if (!$column->isDefaultColumn()) { 111 + $panel->setEditURI('column/'.$column->getID().'/'); 112 + } 113 + $panel->setHeaderAction(id(new PHUIIconView()) 114 + ->setSpriteSheet(PHUIIconView::SPRITE_ACTIONS) 115 + ->setSpriteIcon('new-grey') 116 + ->setHref('/maniphest/task/create/') 117 + ->addSigil('column-add-task') 118 + ->setMetadata( 119 + array('columnPHID' => $column->getPHID()))); 120 + 121 + $cards = id(new PHUIObjectItemListView()) 122 + ->setUser($viewer) 123 + ->setCards(true) 124 + ->setFlush(true) 125 + ->setAllowEmptyList(true) 126 + ->addSigil('project-column') 127 + ->setMetadata( 128 + array( 129 + 'columnPHID' => $column->getPHID(), 130 + )); 131 + $task_phids = idx($task_map, $column->getPHID(), array()); 132 + foreach (array_select_keys($tasks, $task_phids) as $task) { 133 + $owner = null; 134 + if ($task->getOwnerPHID()) { 135 + $owner = $this->handles[$task->getOwnerPHID()]; 136 + } 137 + $can_edit = idx($task_can_edit_map, $task->getPHID(), false); 138 + $cards->addItem(id(new ProjectBoardTaskCard()) 139 + ->setViewer($viewer) 140 + ->setTask($task) 141 + ->setOwner($owner) 142 + ->setCanEdit($can_edit) 143 + ->getItem()); 144 + } 145 + $panel->setCards($cards); 146 + 147 + if (!$task_phids) { 148 + $cards->addClass('project-column-empty'); 149 + } 150 + 151 + $board->addPanel($panel); 152 + } 153 + 154 + $crumbs = $this->buildApplicationCrumbs(); 155 + $crumbs->addTextCrumb(pht('Board')); 156 + 157 + $can_edit = PhabricatorPolicyFilter::hasCapability( 158 + $viewer, 159 + $project, 160 + PhabricatorPolicyCapability::CAN_EDIT); 161 + 162 + $actions = id(new PhabricatorActionListView()) 163 + ->setUser($viewer) 164 + ->addAction( 165 + id(new PhabricatorActionView()) 166 + ->setName(pht('Add Column')) 167 + ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) 168 + ->setIcon('create') 169 + ->setDisabled(!$can_edit) 170 + ->setWorkflow(!$can_edit)); 171 + 172 + $plist = id(new PHUIPropertyListView()); 173 + 174 + // TODO: Need this to get actions to render. 175 + $plist->addProperty( 176 + pht('Project Boards'), 177 + phutil_tag( 178 + 'em', 179 + array(), 180 + pht( 181 + 'This feature is beta, but should mostly work.'))); 182 + $plist->setActionList($actions); 183 + 184 + $header = id(new PHUIHeaderView()) 185 + ->setHeader($project->getName()) 186 + ->setUser($viewer) 187 + ->setImage($project->getProfileImageURI()) 188 + ->setPolicyObject($project); 189 + 190 + $box = id(new PHUIObjectBoxView()) 191 + ->setHeader($header) 192 + ->addPropertyList($plist); 193 + 194 + $board_box = id(new PHUIBoxView()) 195 + ->appendChild($board) 196 + ->addMargin(PHUI::MARGIN_LARGE); 197 + 198 + return $this->buildApplicationPage( 199 + array( 200 + $crumbs, 201 + $box, 202 + $board_box, 203 + ), 204 + array( 205 + 'title' => pht('%s Board', $project->getName()), 206 + 'device' => true, 207 + )); 208 + } 209 + 210 + }
+165
src/applications/project/controller/PhabricatorProjectColumnDetailController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectColumnDetailController 4 + extends PhabricatorProjectBoardController { 5 + 6 + private $id; 7 + private $projectID; 8 + 9 + public function willProcessRequest(array $data) { 10 + $this->projectID = $data['projectID']; 11 + $this->id = idx($data, 'id'); 12 + } 13 + 14 + public function processRequest() { 15 + $request = $this->getRequest(); 16 + $viewer = $request->getUser(); 17 + 18 + $project = id(new PhabricatorProjectQuery()) 19 + ->setViewer($viewer) 20 + ->requireCapabilities( 21 + array( 22 + PhabricatorPolicyCapability::CAN_VIEW, 23 + )) 24 + ->withIDs(array($this->projectID)) 25 + ->executeOne(); 26 + 27 + if (!$project) { 28 + return new Aphront404Response(); 29 + } 30 + $this->setProject($project); 31 + 32 + $column = id(new PhabricatorProjectColumnQuery()) 33 + ->setViewer($viewer) 34 + ->withIDs(array($this->id)) 35 + ->requireCapabilities( 36 + array( 37 + PhabricatorPolicyCapability::CAN_VIEW, 38 + )) 39 + ->executeOne(); 40 + if (!$column) { 41 + return new Aphront404Response(); 42 + } 43 + 44 + $xactions = id(new PhabricatorProjectColumnTransactionQuery()) 45 + ->setViewer($viewer) 46 + ->withObjectPHIDs(array($column->getPHID())) 47 + ->execute(); 48 + 49 + $engine = id(new PhabricatorMarkupEngine()) 50 + ->setViewer($viewer); 51 + 52 + $timeline = id(new PhabricatorApplicationTransactionView()) 53 + ->setUser($viewer) 54 + ->setObjectPHID($column->getPHID()) 55 + ->setTransactions($xactions); 56 + 57 + $title = pht('%s', $column->getName()); 58 + $crumbs = $this->buildApplicationCrumbs(); 59 + $crumbs->addTextCrumb( 60 + pht('Board'), 61 + $this->getApplicationURI('board/'.$project->getID().'/')); 62 + $crumbs->addTextCrumb($title); 63 + 64 + $header = $this->buildHeaderView($column); 65 + $actions = $this->buildActionView($column); 66 + $properties = $this->buildPropertyView($column, $actions); 67 + 68 + $box = id(new PHUIObjectBoxView()) 69 + ->setHeader($header) 70 + ->addPropertyList($properties); 71 + 72 + return $this->buildApplicationPage( 73 + array( 74 + $crumbs, 75 + $box, 76 + $timeline, 77 + ), 78 + array( 79 + 'title' => $title, 80 + 'device' => true, 81 + )); 82 + } 83 + 84 + private function buildHeaderView(PhabricatorProjectColumn $column) { 85 + $viewer = $this->getRequest()->getUser(); 86 + 87 + $header = id(new PHUIHeaderView()) 88 + ->setUser($viewer) 89 + ->setHeader($column->getName()) 90 + ->setPolicyObject($column); 91 + 92 + if ($column->isDeleted()) { 93 + $header->setStatus('reject', 'red', pht('Deleted')); 94 + } 95 + 96 + return $header; 97 + } 98 + 99 + private function buildActionView(PhabricatorProjectColumn $column) { 100 + $viewer = $this->getRequest()->getUser(); 101 + 102 + $id = $column->getID(); 103 + $project_id = $this->getProject()->getID(); 104 + $base_uri = '/board/'.$project_id.'/'; 105 + 106 + $actions = id(new PhabricatorActionListView()) 107 + ->setObjectURI($this->getApplicationURI($base_uri.'column/'.$id.'/')) 108 + ->setUser($viewer); 109 + 110 + $can_edit = PhabricatorPolicyFilter::hasCapability( 111 + $viewer, 112 + $column, 113 + PhabricatorPolicyCapability::CAN_EDIT); 114 + 115 + $actions->addAction( 116 + id(new PhabricatorActionView()) 117 + ->setName(pht('Edit column')) 118 + ->setIcon('edit') 119 + ->setHref($this->getApplicationURI($base_uri.'edit/'.$id.'/')) 120 + ->setDisabled(!$can_edit) 121 + ->setWorkflow(!$can_edit)); 122 + 123 + if (!$column->isDeleted()) { 124 + $actions->addAction( 125 + id(new PhabricatorActionView()) 126 + ->setName(pht('Delete column')) 127 + ->setIcon('delete') 128 + ->setHref($this->getApplicationURI($base_uri.'delete/'.$id.'/')) 129 + ->setDisabled(!$can_edit) 130 + ->setWorkflow(true)); 131 + } else { 132 + $actions->addAction( 133 + id(new PhabricatorActionView()) 134 + ->setName(pht('Activate column')) 135 + ->setIcon('enable') 136 + ->setHref($this->getApplicationURI($base_uri.'delete/'.$id.'/')) 137 + ->setDisabled(!$can_edit) 138 + ->setWorkflow(true)); 139 + } 140 + 141 + return $actions; 142 + } 143 + 144 + private function buildPropertyView( 145 + PhabricatorProjectColumn $column, 146 + PhabricatorActionListView $actions) { 147 + $viewer = $this->getRequest()->getUser(); 148 + 149 + $properties = id(new PHUIPropertyListView()) 150 + ->setUser($viewer) 151 + ->setObject($column) 152 + ->setActionList($actions); 153 + 154 + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( 155 + $viewer, 156 + $column); 157 + 158 + $properties->addProperty( 159 + pht('Editable By'), 160 + $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); 161 + 162 + return $properties; 163 + } 164 + 165 + }
+118
src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectColumnTransactionEditor 4 + extends PhabricatorApplicationTransactionEditor { 5 + 6 + public function getTransactionTypes() { 7 + $types = parent::getTransactionTypes(); 8 + 9 + $types[] = PhabricatorProjectColumnTransaction::TYPE_NAME; 10 + $types[] = PhabricatorProjectColumnTransaction::TYPE_STATUS; 11 + 12 + return $types; 13 + } 14 + 15 + protected function getCustomTransactionOldValue( 16 + PhabricatorLiskDAO $object, 17 + PhabricatorApplicationTransaction $xaction) { 18 + 19 + switch ($xaction->getTransactionType()) { 20 + case PhabricatorProjectColumnTransaction::TYPE_NAME: 21 + return $object->getName(); 22 + case PhabricatorProjectColumnTransaction::TYPE_STATUS: 23 + return $object->getStatus(); 24 + } 25 + 26 + return parent::getCustomTransactionOldValue($object, $xaction); 27 + } 28 + 29 + protected function getCustomTransactionNewValue( 30 + PhabricatorLiskDAO $object, 31 + PhabricatorApplicationTransaction $xaction) { 32 + 33 + switch ($xaction->getTransactionType()) { 34 + case PhabricatorProjectColumnTransaction::TYPE_NAME: 35 + case PhabricatorProjectColumnTransaction::TYPE_STATUS: 36 + return $xaction->getNewValue(); 37 + } 38 + 39 + return parent::getCustomTransactionNewValue($object, $xaction); 40 + } 41 + 42 + protected function applyCustomInternalTransaction( 43 + PhabricatorLiskDAO $object, 44 + PhabricatorApplicationTransaction $xaction) { 45 + 46 + switch ($xaction->getTransactionType()) { 47 + case PhabricatorProjectColumnTransaction::TYPE_NAME: 48 + $object->setName($xaction->getNewValue()); 49 + return; 50 + case PhabricatorProjectColumnTransaction::TYPE_STATUS: 51 + $object->setStatus($xaction->getNewValue()); 52 + return; 53 + } 54 + 55 + return parent::applyCustomInternalTransaction($object, $xaction); 56 + } 57 + 58 + protected function applyCustomExternalTransaction( 59 + PhabricatorLiskDAO $object, 60 + PhabricatorApplicationTransaction $xaction) { 61 + 62 + switch ($xaction->getTransactionType()) { 63 + case PhabricatorProjectColumnTransaction::TYPE_NAME: 64 + case PhabricatorProjectColumnTransaction::TYPE_STATUS: 65 + return; 66 + } 67 + 68 + return parent::applyCustomExternalTransaction($object, $xaction); 69 + } 70 + 71 + protected function validateTransaction( 72 + PhabricatorLiskDAO $object, 73 + $type, 74 + array $xactions) { 75 + 76 + $errors = parent::validateTransaction($object, $type, $xactions); 77 + 78 + switch ($type) { 79 + case PhabricatorProjectColumnTransaction::TYPE_NAME: 80 + $missing = $this->validateIsEmptyTextField( 81 + $object->getName(), 82 + $xactions); 83 + 84 + if ($missing) { 85 + $error = new PhabricatorApplicationTransactionValidationError( 86 + $type, 87 + pht('Required'), 88 + pht('Column name is required.'), 89 + nonempty(last($xactions), null)); 90 + 91 + $error->setIsMissingFieldError(true); 92 + $errors[] = $error; 93 + } 94 + break; 95 + } 96 + 97 + return $errors; 98 + } 99 + 100 + 101 + protected function requireCapabilities( 102 + PhabricatorLiskDAO $object, 103 + PhabricatorApplicationTransaction $xaction) { 104 + 105 + switch ($xaction->getTransactionType()) { 106 + case PhabricatorProjectColumnTransaction::TYPE_NAME: 107 + case PhabricatorProjectColumnTransaction::TYPE_STATUS: 108 + PhabricatorPolicyFilter::requireCapability( 109 + $this->requireActor(), 110 + $object, 111 + PhabricatorPolicyCapability::CAN_EDIT); 112 + return; 113 + } 114 + 115 + return parent::requireCapabilities($object, $xaction); 116 + } 117 + 118 + }
+10
src/applications/project/query/PhabricatorProjectColumnTransactionQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectColumnTransactionQuery 4 + extends PhabricatorApplicationTransactionQuery { 5 + 6 + public function getTemplateApplicationTransaction() { 7 + return new PhabricatorProjectColumnTransaction(); 8 + } 9 + 10 + }
+4
src/applications/project/storage/PhabricatorProjectColumn.php
··· 44 44 return ($this->getSequence() == 0); 45 45 } 46 46 47 + public function isDeleted() { 48 + return ($this->getStatus() == self::STATUS_DELETED); 49 + } 50 + 47 51 public function getDisplayName() { 48 52 if ($this->isDefaultColumn()) { 49 53 return pht('Backlog');
+52
src/applications/project/storage/PhabricatorProjectColumnTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectColumnTransaction 4 + extends PhabricatorApplicationTransaction { 5 + 6 + const TYPE_NAME = 'project:col:name'; 7 + const TYPE_STATUS = 'project:col:status'; 8 + 9 + public function getApplicationName() { 10 + return 'project'; 11 + } 12 + 13 + public function getApplicationTransactionType() { 14 + return PhabricatorProjectPHIDTypeColumn::TYPECONST; 15 + } 16 + 17 + public function getTitle() { 18 + $old = $this->getOldValue(); 19 + $new = $this->getNewValue(); 20 + $author_handle = $this->renderHandleLink($this->getAuthorPHID()); 21 + 22 + switch ($this->getTransactionType()) { 23 + case PhabricatorProjectColumnTransaction::TYPE_NAME: 24 + if (!strlen($old)) { 25 + return pht( 26 + '%s created this column.', 27 + $author_handle); 28 + } else { 29 + return pht( 30 + '%s renamed this column from "%s" to "%s".', 31 + $author_handle, 32 + $old, 33 + $new); 34 + } 35 + case PhabricatorProjectColumnTransaction::TYPE_STATUS: 36 + switch ($new) { 37 + case PhabricatorProjectColumn::STATUS_ACTIVE: 38 + return pht( 39 + '%s activated this column.', 40 + $author_handle); 41 + case PhabricatorProjectColumn::STATUS_DELETED: 42 + return pht( 43 + '%s deleted this column.', 44 + $author_handle); 45 + } 46 + break; 47 + } 48 + 49 + return parent::getTitle(); 50 + } 51 + 52 + }