Index: resources/sql/patches/20131020.col1.sql =================================================================== --- /dev/null +++ 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; Index: src/__celerity_resource_map__.php =================================================================== --- src/__celerity_resource_map__.php +++ src/__celerity_resource_map__.php @@ -3966,7 +3966,7 @@ ), '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( Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -1515,15 +1515,22 @@ '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 @@ 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 @@ ), 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectNameCollisionException' => 'Exception', + 'PhabricatorProjectPHIDTypeColumn' => 'PhabricatorPHIDType', 'PhabricatorProjectPHIDTypeProject' => 'PhabricatorPHIDType', 'PhabricatorProjectProfile' => 'PhabricatorProjectDAO', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', Index: src/applications/maniphest/controller/ManiphestTaskEditController.php =================================================================== --- src/applications/maniphest/controller/ManiphestTaskEditController.php +++ src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -609,7 +609,7 @@ ->setPreviewURI($this->getApplicationURI('task/descriptionpreview/')); if ($task->getID()) { - $page_objects = array( $task->getPHID() ); + $page_objects = array($task->getPHID()); } else { $page_objects = array(); } Index: src/applications/project/application/PhabricatorApplicationProject.php =================================================================== --- src/applications/project/application/PhabricatorApplicationProject.php +++ src/applications/project/application/PhabricatorApplicationProject.php @@ -45,6 +45,7 @@ 'picture/(?P[1-9]\d*)/' => 'PhabricatorProjectProfilePictureController', 'create/' => 'PhabricatorProjectCreateController', + 'board/(?P[1-9]\d*)/' => 'PhabricatorProjectBoardController', 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', ), Index: src/applications/project/controller/PhabricatorProjectBoardController.php =================================================================== --- /dev/null +++ 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); + } + +} Index: src/applications/project/phid/PhabricatorProjectPHIDTypeColumn.php =================================================================== --- /dev/null +++ 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()); + } + } + +} Index: src/applications/project/query/PhabricatorProjectColumnQuery.php =================================================================== --- /dev/null +++ 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'; + } + +} Index: src/applications/project/storage/PhabricatorProjectColumn.php =================================================================== --- /dev/null +++ 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.'); + } + +} Index: src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php =================================================================== --- src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1688,6 +1688,10 @@ 'type' => 'sql', 'name' => $this->getPatchPath('20131020.pcustom.sql'), ), + '20131020.col1.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20131020.col1.sql'), + ), ); } } Index: src/view/layout/AphrontMultiColumnView.php =================================================================== --- src/view/layout/AphrontMultiColumnView.php +++ src/view/layout/AphrontMultiColumnView.php @@ -6,7 +6,7 @@ const GUTTER_MEDIUM = 'mmr'; const GUTTER_LARGE = 'mlr'; - private $column = array(); + private $columns = array(); private $fluidLayout = false; private $gutter; private $shadow; Index: src/view/phui/PHUIWorkpanelView.php =================================================================== --- src/view/phui/PHUIWorkpanelView.php +++ src/view/phui/PHUIWorkpanelView.php @@ -18,6 +18,7 @@ } public function setHeaderAction($header_action) { + // TODO: This doesn't do anything? $this->headerAction = $header_action; return $this; } Index: webroot/rsrc/css/phui/phui-workboard-view.css =================================================================== --- webroot/rsrc/css/phui/phui-workboard-view.css +++ 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);