From d7a276346f345b3ce4f5b0d46c6343fe8775b259 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Oct 2013 21:11:36 -0700 Subject: [PATCH] Add a secret board view to Projects Summary: Ref T1344. This is //very// rough. Some UI issues: - Empty states for the board and columns are junky. - Column widths are crazy. I think we need to set them to fixed-width, since we may have an arbitrarily large number of columns? - I don't think we have the header UI elements in M10 yet and that mock is pretty old, so I sort of very roughly approximated it. - What should we do when you click a task title? Popping the whole task in a dialog is possible but needs a bunch of work to actually work. Might need to build "sheets" or something. - Icons are slightly clipped for some reason. - All the backend stuff is totally faked. Generally, my plan is just to use these to implement all of T390. Specifically: - "Kanban" projects will have "Backlog" on the left. You'll drag them toward the right as you make progress. - "Milestone" projects will have "No Milestone" on the left, then "Milestone 9", "Milestone 8", etc. - "Sprint" projects will have "Backlog" on the left, then "Sprint 31", "Sprint 30", etc. So all of these things end up being pretty much exactly the same, with some minor text changes and new columns showing up on the left vs the right or whatever. Test Plan: See screenshot. Reviewers: chad, btrahan Reviewed By: btrahan CC: chad, aran, sascha-egerer Maniphest Tasks: T1344 Differential Revision: https://secure.phabricator.com/D7374 --- resources/sql/patches/20131020.col1.sql | 9 ++ src/__celerity_resource_map__.php | 2 +- src/__phutil_library_map__.php | 18 +++ .../ManiphestTaskEditController.php | 2 +- .../PhabricatorApplicationProject.php | 1 + .../PhabricatorProjectBoardController.php | 152 ++++++++++++++++++ .../phid/PhabricatorProjectPHIDTypeColumn.php | 39 +++++ .../query/PhabricatorProjectColumnQuery.php | 99 ++++++++++++ .../storage/PhabricatorProjectColumn.php | 58 +++++++ .../patch/PhabricatorBuiltinPatchList.php | 4 + src/view/layout/AphrontMultiColumnView.php | 2 +- src/view/phui/PHUIWorkpanelView.php | 1 + webroot/rsrc/css/phui/phui-workboard-view.css | 2 +- 13 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 resources/sql/patches/20131020.col1.sql create mode 100644 src/applications/project/controller/PhabricatorProjectBoardController.php create mode 100644 src/applications/project/phid/PhabricatorProjectPHIDTypeColumn.php create mode 100644 src/applications/project/query/PhabricatorProjectColumnQuery.php create mode 100644 src/applications/project/storage/PhabricatorProjectColumn.php diff --git a/resources/sql/patches/20131020.col1.sql b/resources/sql/patches/20131020.col1.sql new file mode 100644 index 0000000000..fb1a1a948b --- /dev/null +++ b/resources/sql/patches/20131020.col1.sql @@ -0,0 +1,9 @@ +CREATE TABLE {$NAMESPACE}_project.project_column ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(255) NOT NULL, + sequence INT UNSIGNED NOT NULL, + projectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + UNIQUE KEY `key_sequence` (projectPHID, sequence), + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 019ce18779..757dd23513 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -3966,7 +3966,7 @@ celerity_register_resource_map(array( ), 'phui-workboard-view-css' => array( - 'uri' => '/res/628679e5/rsrc/css/phui/phui-workboard-view.css', + 'uri' => '/res/445c7c7e/rsrc/css/phui/phui-workboard-view.css', 'type' => 'css', 'requires' => array( diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a96a7cde78..bf322909d4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1515,15 +1515,22 @@ phutil_register_library_map(array( 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', + 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', + 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', + 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', 'PhabricatorProjectCreateController' => 'applications/project/controller/PhabricatorProjectCreateController.php', + 'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php', + 'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php', + 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectEditor' => 'applications/project/editor/PhabricatorProjectEditor.php', 'PhabricatorProjectEditorTestCase' => 'applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 'PhabricatorProjectNameCollisionException' => 'applications/project/exception/PhabricatorProjectNameCollisionException.php', + 'PhabricatorProjectPHIDTypeColumn' => 'applications/project/phid/PhabricatorProjectPHIDTypeColumn.php', 'PhabricatorProjectPHIDTypeProject' => 'applications/project/phid/PhabricatorProjectPHIDTypeProject.php', 'PhabricatorProjectProfile' => 'applications/project/storage/PhabricatorProjectProfile.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', @@ -3733,8 +3740,18 @@ phutil_register_library_map(array( 0 => 'PhabricatorProjectDAO', 1 => 'PhabricatorPolicyInterface', ), + 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', + 'PhabricatorProjectColumn' => + array( + 0 => 'PhabricatorProjectDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectController' => 'PhabricatorController', 'PhabricatorProjectCreateController' => 'PhabricatorProjectController', + 'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', + 'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage', + 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectEditor' => 'PhabricatorEditor', 'PhabricatorProjectEditorTestCase' => 'PhabricatorTestCase', @@ -3745,6 +3762,7 @@ phutil_register_library_map(array( ), 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectNameCollisionException' => 'Exception', + 'PhabricatorProjectPHIDTypeColumn' => 'PhabricatorPHIDType', 'PhabricatorProjectPHIDTypeProject' => 'PhabricatorPHIDType', 'PhabricatorProjectProfile' => 'PhabricatorProjectDAO', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index 1b01a66dee..6a6ae560ee 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -609,7 +609,7 @@ final class ManiphestTaskEditController extends ManiphestController { ->setPreviewURI($this->getApplicationURI('task/descriptionpreview/')); if ($task->getID()) { - $page_objects = array( $task->getPHID() ); + $page_objects = array($task->getPHID()); } else { $page_objects = array(); } diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php index 629588a9cf..e3c455385f 100644 --- a/src/applications/project/application/PhabricatorApplicationProject.php +++ b/src/applications/project/application/PhabricatorApplicationProject.php @@ -45,6 +45,7 @@ final class PhabricatorApplicationProject extends PhabricatorApplication { 'picture/(?P[1-9]\d*)/' => 'PhabricatorProjectProfilePictureController', 'create/' => 'PhabricatorProjectCreateController', + 'board/(?P[1-9]\d*)/' => 'PhabricatorProjectBoardController', 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', ), diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php new file mode 100644 index 0000000000..ebdf937ea9 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -0,0 +1,152 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withProjectPHIDs(array($project->getPHID())) + ->execute(); + + // TODO: Completely making this part up. + $columns[] = id(new PhabricatorProjectColumn()) + ->setName('Backlog') + ->setPHID(0) + ->setSequence(0); + + $columns[] = id(new PhabricatorProjectColumn()) + ->setName('Assigned') + ->setPHID(1) + ->setSequence(1); + + $columns[] = id(new PhabricatorProjectColumn()) + ->setName('In Progress') + ->setPHID(2) + ->setSequence(2); + + $columns[] = id(new PhabricatorProjectColumn()) + ->setName('Completed') + ->setPHID(3) + ->setSequence(3); + + msort($columns, 'getSequence'); + + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withAllProjects(array($project->getPHID())) + ->withStatus(ManiphestTaskQuery::STATUS_OPEN) + ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) + ->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + + // TODO: This is also made up. + $task_map = array(); + foreach ($tasks as $task) { + $task_map[mt_rand(0, 3)][] = $task->getPHID(); + } + + $board = id(new PHUIWorkboardView()) + ->setUser($viewer); + + foreach ($columns as $column) { + $panel = id(new PHUIWorkpanelView()) + ->setHeader($column->getName()); + + $cards = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setCards(true) + ->setFlush(true); + $task_phids = idx($task_map, $column->getPHID(), array()); + foreach (array_select_keys($tasks, $task_phids) as $task) { + $cards->addItem($this->renderTaskCard($task)); + } + $panel->setCards($cards); + + $board->addPanel($panel); + } + + $crumbs = $this->buildApplicationCrumbs(); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Add Column/Milestone/Sprint')) + ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) + ->setIcon('create')); + + $plist = id(new PHUIPropertyListView()); + // TODO: Need this to get actions to render. + $plist->addProperty(pht('Ignore'), pht('This Property')); + $plist->setActionList($actions); + + $header = id(new PHUIObjectBoxView()) + ->setHeaderText($project->getName()) + ->addPropertyList($plist); + + $board_box = id(new PHUIBoxView()) + ->appendChild($board) + ->addMargin(PHUI::MARGIN_LARGE); + + return $this->buildApplicationPage( + array( + $crumbs, + $header, + $board_box, + ), + array( + 'title' => pht('Board'), + 'device' => true, + )); + } + + private function renderTaskCard(ManiphestTask $task) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $color_map = ManiphestTaskPriority::getColorMap(); + $bar_color = idx($color_map, $task->getPriority(), 'grey'); + + // TODO: Batch this earlier on. + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $task, + PhabricatorPolicyCapability::CAN_EDIT); + + return id(new PHUIObjectItemView()) + ->setObjectName('T'.$task->getID()) + ->setHeader($task->getTitle()) + ->setGrippable($can_edit) + ->setHref('/T'.$task->getID()) + ->addAction( + id(new PHUIListItemView()) + ->setName(pht('Edit')) + ->setIcon('edit') + ->setHref('/maniphest/task/edit/'.$task->getID().'/') + ->setWorkflow(true)) + ->setBarColor($bar_color); + } + +} diff --git a/src/applications/project/phid/PhabricatorProjectPHIDTypeColumn.php b/src/applications/project/phid/PhabricatorProjectPHIDTypeColumn.php new file mode 100644 index 0000000000..bbff30d133 --- /dev/null +++ b/src/applications/project/phid/PhabricatorProjectPHIDTypeColumn.php @@ -0,0 +1,39 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $column = $objects[$phid]; + + $handle->setName($column->getName()); + } + } + +} diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php new file mode 100644 index 0000000000..71da13794e --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php @@ -0,0 +1,99 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withProjectPHIDs(array $project_phids) { + $this->projectPHIDs = $project_phids; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorProjectColumn(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function willFilterPage(array $page) { + $projects = array(); + + $project_phids = array_filter(mpull($page, 'getProjectPHID')); + if ($project_phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($project_phids) + ->execute(); + $projects = mpull($projects, null, 'getPHID'); + } + + foreach ($page as $key => $column) { + $phid = $column->getProjectPHID(); + $project = idx($projects, $phid); + if (!$project) { + unset($page[$key]); + continue; + } + $column->attachProject($project); + } + + return $page; + } + + private function buildWhereClause($conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->projectPHIDs) { + $where[] = qsprintf( + $conn_r, + 'projectPHID IN (%Ls)', + $this->projectPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationProject'; + } + +} diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php new file mode 100644 index 0000000000..8d9c32af0d --- /dev/null +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -0,0 +1,58 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorProjectPHIDTypeColumn::TYPECONST); + } + + public function attachProject(PhabricatorProject $project) { + $this->project = $project; + return $this; + } + + public function getProject() { + return $this->assertAttached($this->project); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getProject()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getProject()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Users must be able to see a project to see its board.'); + } + +} diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 5672f76c2e..e196f16784 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1688,6 +1688,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('20131020.pcustom.sql'), ), + '20131020.col1.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20131020.col1.sql'), + ), ); } } diff --git a/src/view/layout/AphrontMultiColumnView.php b/src/view/layout/AphrontMultiColumnView.php index 57a36fd4c5..a9358c95eb 100644 --- a/src/view/layout/AphrontMultiColumnView.php +++ b/src/view/layout/AphrontMultiColumnView.php @@ -6,7 +6,7 @@ final class AphrontMultiColumnView extends AphrontView { const GUTTER_MEDIUM = 'mmr'; const GUTTER_LARGE = 'mlr'; - private $column = array(); + private $columns = array(); private $fluidLayout = false; private $gutter; private $shadow; diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index 351bb37c35..cedcc653a1 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -18,6 +18,7 @@ final class PHUIWorkpanelView extends AphrontView { } public function setHeaderAction($header_action) { + // TODO: This doesn't do anything? $this->headerAction = $header_action; return $this; } diff --git a/webroot/rsrc/css/phui/phui-workboard-view.css b/webroot/rsrc/css/phui/phui-workboard-view.css index aeeb0cc7ff..d2e706bb8a 100644 --- a/webroot/rsrc/css/phui/phui-workboard-view.css +++ b/webroot/rsrc/css/phui/phui-workboard-view.css @@ -9,7 +9,7 @@ .phui-workboard-view-shadow { padding: 8px; min-height: 120px; - overflow-x: scroll; + overflow-x: auto; border-radius: 5px; background: rgba(150,150,150,.1); box-shadow: inset 0 0 5px rgba(0,0,0,.5); -- 2.51.2