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