1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

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
This commit is contained in:
Bob Trahan 2014-03-26 14:40:47 -07:00
parent 543a313387
commit 655ac9927f
12 changed files with 698 additions and 295 deletions

View file

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

View file

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

View file

@ -51,12 +51,14 @@ final class PhabricatorApplicationProject extends PhabricatorApplication {
'picture/(?P<id>[1-9]\d*)/' =>
'PhabricatorProjectEditPictureController',
'create/' => 'PhabricatorProjectCreateController',
'board/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectBoardController',
'board/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectBoardViewController',
'move/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectMoveController',
'board/(?P<projectID>[1-9]\d*)/edit/(?:(?P<id>\d+)/)?'
=> 'PhabricatorProjectBoardEditController',
'board/(?P<projectID>[1-9]\d*)/delete/(?:(?P<id>\d+)/)?'
=> 'PhabricatorProjectBoardDeleteController',
'board/(?P<projectID>[1-9]\d*)/column/(?:(?P<id>\d+)/)?'
=> 'PhabricatorProjectColumnDetailController',
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
=> 'PhabricatorProjectUpdateController',
'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController',

View file

@ -1,219 +1,24 @@
<?php
final class PhabricatorProjectBoardController
abstract class PhabricatorProjectBoardController
extends PhabricatorProjectController {
private $id;
private $handles;
private $project;
public function shouldAllowPublic() {
return true;
protected function setProject(PhabricatorProject $project) {
$this->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;
}
}

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorProjectBoardDeleteController
extends PhabricatorProjectController {
extends PhabricatorProjectBoardController {
private $id;
private $projectID;
@ -14,7 +14,6 @@ final class PhabricatorProjectBoardDeleteController
public function processRequest() {
$request = $this->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,
));
}
}

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorProjectBoardEditController
extends PhabricatorProjectController {
extends PhabricatorProjectBoardController {
private $id;
private $projectID;
@ -28,6 +28,7 @@ final class PhabricatorProjectBoardEditController
if (!$project) {
return new Aphront404Response();
}
$this->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(

View file

@ -0,0 +1,210 @@
<?php
final class PhabricatorProjectBoardViewController
extends PhabricatorProjectBoardController {
private $id;
private $handles;
public function shouldAllowPublic() {
return true;
}
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();
}
$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,
));
}
}

View file

@ -0,0 +1,165 @@
<?php
final class PhabricatorProjectColumnDetailController
extends PhabricatorProjectBoardController {
private $id;
private $projectID;
public function willProcessRequest(array $data) {
$this->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;
}
}

View file

@ -0,0 +1,118 @@
<?php
final class PhabricatorProjectColumnTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorProjectColumnTransaction::TYPE_NAME;
$types[] = PhabricatorProjectColumnTransaction::TYPE_STATUS;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->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);
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorProjectColumnTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhabricatorProjectColumnTransaction();
}
}

View file

@ -44,6 +44,10 @@ final class PhabricatorProjectColumn
return ($this->getSequence() == 0);
}
public function isDeleted() {
return ($this->getStatus() == self::STATUS_DELETED);
}
public function getDisplayName() {
if ($this->isDefaultColumn()) {
return pht('Backlog');

View file

@ -0,0 +1,52 @@
<?php
final class PhabricatorProjectColumnTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'project:col:name';
const TYPE_STATUS = 'project:col:status';
public function getApplicationName() {
return 'project';
}
public function getApplicationTransactionType() {
return PhabricatorProjectPHIDTypeColumn::TYPECONST;
}
public function getTitle() {
$old = $this->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();
}
}