diff --git a/resources/sql/autopatches/20140326.project.1.colxaction.sql b/resources/sql/autopatches/20140326.project.1.colxaction.sql new file mode 100644 index 0000000000..c35bcb614a --- /dev/null +++ b/resources/sql/autopatches/20140326.project.1.colxaction.sql @@ -0,0 +1,21 @@ +CREATE TABLE {$NAMESPACE}_project.project_columntransaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + commentPHID VARCHAR(64) COLLATE utf8_bin, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, + newValue LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID) + +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b458054da2..40c5e00249 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1862,8 +1862,13 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', 'PhabricatorProjectBoardDeleteController' => 'applications/project/controller/PhabricatorProjectBoardDeleteController.php', 'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php', + 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', + 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', + 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', + 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', + 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php', @@ -4668,14 +4673,19 @@ phutil_register_library_map(array( ), 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', - 'PhabricatorProjectBoardDeleteController' => 'PhabricatorProjectController', - 'PhabricatorProjectBoardEditController' => 'PhabricatorProjectController', + 'PhabricatorProjectBoardDeleteController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardEditController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumn' => array( 0 => 'PhabricatorProjectDAO', 1 => 'PhabricatorPolicyInterface', ), + 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorProjectConfiguredCustomField' => array( diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php index d5a0938fec..c6a1d92e12 100644 --- a/src/applications/project/application/PhabricatorApplicationProject.php +++ b/src/applications/project/application/PhabricatorApplicationProject.php @@ -51,12 +51,14 @@ final class PhabricatorApplicationProject extends PhabricatorApplication { 'picture/(?P[1-9]\d*)/' => 'PhabricatorProjectEditPictureController', 'create/' => 'PhabricatorProjectCreateController', - 'board/(?P[1-9]\d*)/' => 'PhabricatorProjectBoardController', + 'board/(?P[1-9]\d*)/' => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', 'board/(?P[1-9]\d*)/edit/(?:(?P\d+)/)?' => 'PhabricatorProjectBoardEditController', 'board/(?P[1-9]\d*)/delete/(?:(?P\d+)/)?' => 'PhabricatorProjectBoardDeleteController', + 'board/(?P[1-9]\d*)/column/(?:(?P\d+)/)?' + => 'PhabricatorProjectColumnDetailController', 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', 'history/(?P[1-9]\d*)/' => 'PhabricatorProjectHistoryController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index 72c2af5ffd..46bb320866 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -1,219 +1,24 @@ project = $project; + return $this; + } + protected function getProject() { + return $this->project; } - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->needImages(true) - ->withIDs(array($this->id)) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); - } - - $columns = id(new PhabricatorProjectColumnQuery()) - ->setViewer($viewer) - ->withProjectPHIDs(array($project->getPHID())) - ->withStatuses(array(PhabricatorProjectColumn::STATUS_ACTIVE)) - ->execute(); - - $columns = mpull($columns, null, 'getSequence'); - - // If there's no default column, create one now. - if (empty($columns[0])) { - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $column = PhabricatorProjectColumn::initializeNewColumn($viewer) - ->setSequence(0) - ->setProjectPHID($project->getPHID()) - ->save(); - $column->attachProject($project); - $columns[0] = $column; - unset($unguarded); - } - - ksort($columns); - - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withAllProjects(array($project->getPHID())) - ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) - ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) - ->execute(); - $tasks = mpull($tasks, null, 'getPHID'); - $task_phids = array_keys($tasks); - - if ($task_phids) { - $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; - $edge_query = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs($task_phids) - ->withEdgeTypes(array($edge_type)) - ->withDestinationPHIDs(mpull($columns, 'getPHID')); - $edge_query->execute(); - } - - $task_map = array(); - $default_phid = $columns[0]->getPHID(); - foreach ($tasks as $task) { - $task_phid = $task->getPHID(); - $column_phids = $edge_query->getDestinationPHIDs(array($task_phid)); - - $column_phid = head($column_phids); - $column_phid = nonempty($column_phid, $default_phid); - - $task_map[$column_phid][] = $task_phid; - } - - $task_can_edit_map = id(new PhabricatorPolicyFilter()) - ->setViewer($viewer) - ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) - ->apply($tasks); - - $board_id = celerity_generate_unique_node_id(); - - $board = id(new PHUIWorkboardView()) - ->setUser($viewer) - ->setFluidishLayout(true) - ->setID($board_id); - - $this->initBehavior( - 'project-boards', - array( - 'boardID' => $board_id, - 'projectPHID' => $project->getPHID(), - 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), - 'createURI' => '/maniphest/task/create/', - )); - - $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); - - foreach ($columns as $column) { - $panel = id(new PHUIWorkpanelView()) - ->setHeader($column->getDisplayName()) - ->setHeaderColor($column->getHeaderColor()); - if (!$column->isDefaultColumn()) { - $panel->setEditURI('edit/'.$column->getID().'/'); - } - $panel->setHeaderAction(id(new PHUIIconView()) - ->setSpriteSheet(PHUIIconView::SPRITE_ACTIONS) - ->setSpriteIcon('new-grey') - ->setHref('/maniphest/task/create/') - ->addSigil('column-add-task') - ->setMetadata( - array('columnPHID' => $column->getPHID()))); - - $cards = id(new PHUIObjectItemListView()) - ->setUser($viewer) - ->setCards(true) - ->setFlush(true) - ->setAllowEmptyList(true) - ->addSigil('project-column') - ->setMetadata( - array( - 'columnPHID' => $column->getPHID(), - )); - $task_phids = idx($task_map, $column->getPHID(), array()); - foreach (array_select_keys($tasks, $task_phids) as $task) { - $owner = null; - if ($task->getOwnerPHID()) { - $owner = $this->handles[$task->getOwnerPHID()]; - } - $can_edit = idx($task_can_edit_map, $task->getPHID(), false); - $cards->addItem(id(new ProjectBoardTaskCard()) - ->setViewer($viewer) - ->setTask($task) - ->setOwner($owner) - ->setCanEdit($can_edit) - ->getItem()); - } - $panel->setCards($cards); - - if (!$task_phids) { - $cards->addClass('project-column-empty'); - } - - $board->addPanel($panel); - } - - $crumbs = $this->buildApplicationCrumbs(); + protected function buildApplicationCrumbs() { + $project = $this->getProject(); + $crumbs = parent::buildApplicationCrumbs(); $crumbs->addTextCrumb( $project->getName(), $this->getApplicationURI('view/'.$project->getID().'/')); - $crumbs->addTextCrumb(pht('Board')); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $project, - PhabricatorPolicyCapability::CAN_EDIT); - - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Add Column')) - ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) - ->setIcon('create') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)) - ->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Delete Column')) - ->setHref($this->getApplicationURI('board/'.$this->id.'/delete/')) - ->setIcon('delete') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $plist = id(new PHUIPropertyListView()); - - // TODO: Need this to get actions to render. - $plist->addProperty( - pht('Project Boards'), - phutil_tag( - 'em', - array(), - pht( - 'This feature is beta, but should mostly work.'))); - $plist->setActionList($actions); - - $header = id(new PHUIHeaderView()) - ->setHeader($project->getName()) - ->setUser($viewer) - ->setImage($project->getProfileImageURI()) - ->setPolicyObject($project); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($plist); - - $board_box = id(new PHUIBoxView()) - ->appendChild($board) - ->addMargin(PHUI::MARGIN_LARGE); - - return $this->buildApplicationPage( - array( - $crumbs, - $box, - $board_box, - ), - array( - 'title' => pht('%s Board', $project->getName()), - 'device' => true, - )); + return $crumbs; } - } diff --git a/src/applications/project/controller/PhabricatorProjectBoardDeleteController.php b/src/applications/project/controller/PhabricatorProjectBoardDeleteController.php index 0b8b581f9f..8d1ca3911b 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardDeleteController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardDeleteController.php @@ -1,7 +1,7 @@ getRequest(); $viewer = $request->getUser(); - $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( @@ -28,89 +27,89 @@ final class PhabricatorProjectBoardDeleteController if (!$project) { return new Aphront404Response(); } + $this->setProject($project); - $columns = id(new PhabricatorProjectColumnQuery()) + $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) - ->withProjectPHIDs(array($project->getPHID())) - ->withStatuses(array(PhabricatorProjectColumn::STATUS_ACTIVE)) + ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT)) - ->execute(); - - if (!$columns) { + ->executeOne(); + if (!$column) { return new Aphront404Response(); } - $columns = mpull($columns, null, 'getSequence'); - $columns = mfilter($columns, 'isDefaultColumn', true); - ksort($columns); - $options = mpull($columns, 'getName', 'getPHID'); - - $view_uri = $this->getApplicationURI('/board/'.$this->projectID.'/'); $error_view = null; - if ($request->isFormPost()) { - $columns = mpull($columns, null, 'getPHID'); - $column_phid = $request->getStr('columnPHID'); - $column = $columns[$column_phid]; + $column_phid = $column->getPHID(); + $has_task_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $column_phid, + PhabricatorEdgeConfig::TYPE_COLUMN_HAS_OBJECT); - $has_task_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $column_phid, - PhabricatorEdgeConfig::TYPE_COLUMN_HAS_OBJECT); - - if ($has_task_phids) { - $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Column has Tasks!')) - ->setErrors(array(pht('A column can not be deleted if it has tasks '. - 'in it. Please remove the tasks and try '. - 'again.'))); + if ($has_task_phids) { + $error_view = id(new AphrontErrorView()) + ->setTitle(pht('Column has Tasks!')); + if ($column->isDeleted()) { + $error_view->setErrors(array(pht( + 'A column can not be activated if it has tasks '. + 'in it. Please remove the tasks and try again.'))); } else { - $column->setStatus(PhabricatorProjectColumn::STATUS_DELETED); - $column->save(); - - return id(new AphrontRedirectResponse())->setURI($view_uri); + $error_view->setErrors(array(pht( + 'A column can not be deleted if it has tasks '. + 'in it. Please remove the tasks and try again.'))); } } - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild($error_view) - ->appendChild(id(new AphrontFormSelectControl()) - ->setName('columnPHID') - ->setValue(head_key($options)) - ->setOptions($options) - ->setLabel(pht('Column'))); + $view_uri = $this->getApplicationURI( + '/board/'.$this->projectID.'/column/'.$this->id.'/'); - $title = pht('Delete Column'); + if ($request->isFormPost() && !$error_view) { + if ($column->isDeleted()) { + $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; + } else { + $new_status = PhabricatorProjectColumn::STATUS_DELETED; + } + + $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; + $xactions = array(id(new PhabricatorProjectColumnTransaction()) + ->setTransactionType($type_status) + ->setNewValue($new_status)); + + $editor = id(new PhabricatorProjectColumnTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($column, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + if ($column->isDeleted()) { + $title = pht('Activate Column'); + } else { + $title = pht('Delete Column'); + } $submit = $title; + if ($error_view) { + $body = $error_view; + } else if ($column->isDeleted()) { + $body = pht('Are you sure you want to activate this column?'); + } else { + $body = pht('Are you sure you want to delete this column?'); + } - $form->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue($submit) - ->addCancelButton($view_uri)); + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle($title) + ->appendChild($body) + ->setDisableWorkflowOnCancel(true) + ->addSubmitButton($title) + ->addCancelButton($view_uri); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb( - $project->getName(), - $this->getApplicationURI('view/'.$project->getID().'/')); - $crumbs->addTextCrumb( - pht('Board'), - $this->getApplicationURI('board/'.$project->getID().'/')); - $crumbs->addTextCrumb($title); + return id(new AphrontDialogResponse()) + ->setDialog($dialog); - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setForm($form); - - return $this->buildApplicationPage( - array( - $crumbs, - $form_box, - ), - array( - 'title' => $title, - 'device' => true, - )); } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardEditController.php b/src/applications/project/controller/PhabricatorProjectBoardEditController.php index 83f3e4abce..36d61408e1 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardEditController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardEditController.php @@ -1,7 +1,7 @@ setProject($project); $is_new = ($this->id ? false : true); @@ -48,21 +49,18 @@ final class PhabricatorProjectBoardEditController $column = PhabricatorProjectColumn::initializeNewColumn($viewer); } - $errors = array(); - $e_name = true; - $error_view = null; - $view_uri = $this->getApplicationURI('/board/'.$this->projectID.'/'); + $e_name = null; + $validation_exception = null; + $base_uri = '/board/'.$this->projectID.'/'; + 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/'.$this->id.'/'); + } if ($request->isFormPost()) { $new_name = $request->getStr('name'); - $column->setName($new_name); - - if (!strlen($column->getName())) { - $errors[] = pht('Column name is required.'); - $e_name = pht('Required'); - } else { - $e_name = null; - } if ($is_new) { $column->setProjectPHID($project->getPHID()); @@ -81,9 +79,21 @@ final class PhabricatorProjectBoardEditController $column->setSequence($new_sequence); } - if (!$errors) { - $column->save(); + $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; + $xactions = array(id(new PhabricatorProjectColumnTransaction()) + ->setTransactionType($type_name) + ->setNewValue($new_name)); + + 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); + $validation_exception = $ex; } } @@ -112,9 +122,6 @@ final class PhabricatorProjectBoardEditController ->addCancelButton($view_uri)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb( - $project->getName(), - $this->getApplicationURI('view/'.$project->getID().'/')); $crumbs->addTextCrumb( pht('Board'), $this->getApplicationURI('board/'.$project->getID().'/')); @@ -122,7 +129,7 @@ final class PhabricatorProjectBoardEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormErrors($errors) + ->setValidationException($validation_exception) ->setForm($form); return $this->buildApplicationPage( diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php new file mode 100644 index 0000000000..b127b12f45 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -0,0 +1,210 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->needImages(true) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + $this->setProject($project); + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array($project->getPHID())) + ->withStatuses(array(PhabricatorProjectColumn::STATUS_ACTIVE)) + ->execute(); + + $columns = mpull($columns, null, 'getSequence'); + + // If there's no default column, create one now. + if (empty($columns[0])) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $column = PhabricatorProjectColumn::initializeNewColumn($viewer) + ->setSequence(0) + ->setProjectPHID($project->getPHID()) + ->save(); + $column->attachProject($project); + $columns[0] = $column; + unset($unguarded); + } + + ksort($columns); + + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withAllProjects(array($project->getPHID())) + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) + ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) + ->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + $task_phids = array_keys($tasks); + + if ($task_phids) { + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($task_phids) + ->withEdgeTypes(array($edge_type)) + ->withDestinationPHIDs(mpull($columns, 'getPHID')); + $edge_query->execute(); + } + + $task_map = array(); + $default_phid = $columns[0]->getPHID(); + foreach ($tasks as $task) { + $task_phid = $task->getPHID(); + $column_phids = $edge_query->getDestinationPHIDs(array($task_phid)); + + $column_phid = head($column_phids); + $column_phid = nonempty($column_phid, $default_phid); + + $task_map[$column_phid][] = $task_phid; + } + + $task_can_edit_map = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($tasks); + + $board_id = celerity_generate_unique_node_id(); + + $board = id(new PHUIWorkboardView()) + ->setUser($viewer) + ->setFluidishLayout(true) + ->setID($board_id); + + $this->initBehavior( + 'project-boards', + array( + 'boardID' => $board_id, + 'projectPHID' => $project->getPHID(), + 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), + 'createURI' => '/maniphest/task/create/', + )); + + $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); + + foreach ($columns as $column) { + $panel = id(new PHUIWorkpanelView()) + ->setHeader($column->getDisplayName()) + ->setHeaderColor($column->getHeaderColor()); + if (!$column->isDefaultColumn()) { + $panel->setEditURI('column/'.$column->getID().'/'); + } + $panel->setHeaderAction(id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ACTIONS) + ->setSpriteIcon('new-grey') + ->setHref('/maniphest/task/create/') + ->addSigil('column-add-task') + ->setMetadata( + array('columnPHID' => $column->getPHID()))); + + $cards = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setCards(true) + ->setFlush(true) + ->setAllowEmptyList(true) + ->addSigil('project-column') + ->setMetadata( + array( + 'columnPHID' => $column->getPHID(), + )); + $task_phids = idx($task_map, $column->getPHID(), array()); + foreach (array_select_keys($tasks, $task_phids) as $task) { + $owner = null; + if ($task->getOwnerPHID()) { + $owner = $this->handles[$task->getOwnerPHID()]; + } + $can_edit = idx($task_can_edit_map, $task->getPHID(), false); + $cards->addItem(id(new ProjectBoardTaskCard()) + ->setViewer($viewer) + ->setTask($task) + ->setOwner($owner) + ->setCanEdit($can_edit) + ->getItem()); + } + $panel->setCards($cards); + + if (!$task_phids) { + $cards->addClass('project-column-empty'); + } + + $board->addPanel($panel); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Board')); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Add Column')) + ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) + ->setIcon('create') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $plist = id(new PHUIPropertyListView()); + + // TODO: Need this to get actions to render. + $plist->addProperty( + pht('Project Boards'), + phutil_tag( + 'em', + array(), + pht( + 'This feature is beta, but should mostly work.'))); + $plist->setActionList($actions); + + $header = id(new PHUIHeaderView()) + ->setHeader($project->getName()) + ->setUser($viewer) + ->setImage($project->getProfileImageURI()) + ->setPolicyObject($project); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($plist); + + $board_box = id(new PHUIBoxView()) + ->appendChild($board) + ->addMargin(PHUI::MARGIN_LARGE); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $board_box, + ), + array( + 'title' => pht('%s Board', $project->getName()), + 'device' => true, + )); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php new file mode 100644 index 0000000000..e671236fa3 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php @@ -0,0 +1,165 @@ +projectID = $data['projectID']; + $this->id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->withIDs(array($this->projectID)) + ->executeOne(); + + if (!$project) { + return new Aphront404Response(); + } + $this->setProject($project); + + $column = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + + $xactions = id(new PhabricatorProjectColumnTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($column->getPHID())) + ->execute(); + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($viewer); + + $timeline = id(new PhabricatorApplicationTransactionView()) + ->setUser($viewer) + ->setObjectPHID($column->getPHID()) + ->setTransactions($xactions); + + $title = pht('%s', $column->getName()); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Board'), + $this->getApplicationURI('board/'.$project->getID().'/')); + $crumbs->addTextCrumb($title); + + $header = $this->buildHeaderView($column); + $actions = $this->buildActionView($column); + $properties = $this->buildPropertyView($column, $actions); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $timeline, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + + private function buildHeaderView(PhabricatorProjectColumn $column) { + $viewer = $this->getRequest()->getUser(); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($column->getName()) + ->setPolicyObject($column); + + if ($column->isDeleted()) { + $header->setStatus('reject', 'red', pht('Deleted')); + } + + 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()) + ->setObjectURI($this->getApplicationURI($base_uri.'column/'.$id.'/')) + ->setUser($viewer); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $column, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit column')) + ->setIcon('edit') + ->setHref($this->getApplicationURI($base_uri.'edit/'.$id.'/')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + if (!$column->isDeleted()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Delete column')) + ->setIcon('delete') + ->setHref($this->getApplicationURI($base_uri.'delete/'.$id.'/')) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } else { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Activate column')) + ->setIcon('enable') + ->setHref($this->getApplicationURI($base_uri.'delete/'.$id.'/')) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } + + return $actions; + } + + private function buildPropertyView( + PhabricatorProjectColumn $column, + PhabricatorActionListView $actions) { + $viewer = $this->getRequest()->getUser(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($column) + ->setActionList($actions); + + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( + $viewer, + $column); + + $properties->addProperty( + pht('Editable By'), + $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); + + return $properties; + } + +} diff --git a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php new file mode 100644 index 0000000000..5107f400cf --- /dev/null +++ b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php @@ -0,0 +1,118 @@ +getTransactionType()) { + case PhabricatorProjectColumnTransaction::TYPE_NAME: + return $object->getName(); + case PhabricatorProjectColumnTransaction::TYPE_STATUS: + return $object->getStatus(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorProjectColumnTransaction::TYPE_NAME: + case PhabricatorProjectColumnTransaction::TYPE_STATUS: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorProjectColumnTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + case PhabricatorProjectColumnTransaction::TYPE_STATUS: + $object->setStatus($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorProjectColumnTransaction::TYPE_NAME: + case PhabricatorProjectColumnTransaction::TYPE_STATUS: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PhabricatorProjectColumnTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Column name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + + + protected function requireCapabilities( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorProjectColumnTransaction::TYPE_NAME: + case PhabricatorProjectColumnTransaction::TYPE_STATUS: + PhabricatorPolicyFilter::requireCapability( + $this->requireActor(), + $object, + PhabricatorPolicyCapability::CAN_EDIT); + return; + } + + return parent::requireCapabilities($object, $xaction); + } + +} diff --git a/src/applications/project/query/PhabricatorProjectColumnTransactionQuery.php b/src/applications/project/query/PhabricatorProjectColumnTransactionQuery.php new file mode 100644 index 0000000000..f536aad05e --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectColumnTransactionQuery.php @@ -0,0 +1,10 @@ +getSequence() == 0); } + public function isDeleted() { + return ($this->getStatus() == self::STATUS_DELETED); + } + public function getDisplayName() { if ($this->isDefaultColumn()) { return pht('Backlog'); diff --git a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php new file mode 100644 index 0000000000..4381734684 --- /dev/null +++ b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php @@ -0,0 +1,52 @@ +getOldValue(); + $new = $this->getNewValue(); + $author_handle = $this->renderHandleLink($this->getAuthorPHID()); + + switch ($this->getTransactionType()) { + case PhabricatorProjectColumnTransaction::TYPE_NAME: + if (!strlen($old)) { + return pht( + '%s created this column.', + $author_handle); + } else { + return pht( + '%s renamed this column from "%s" to "%s".', + $author_handle, + $old, + $new); + } + case PhabricatorProjectColumnTransaction::TYPE_STATUS: + switch ($new) { + case PhabricatorProjectColumn::STATUS_ACTIVE: + return pht( + '%s activated this column.', + $author_handle); + case PhabricatorProjectColumn::STATUS_DELETED: + return pht( + '%s deleted this column.', + $author_handle); + } + break; + } + + return parent::getTitle(); + } + +}