mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-19 03:50:54 +01:00
Take the first step on the long journey of fixing "Projects"
Summary: - Allow more than the 100 most recent projects to be viewed. - Provide some useful filters. - Default the view to your projects, not all projects. - Put query logic in a query object. - Put filter view logic in a view object. We can port more stuff to it later. Test Plan: Looked at active/owned/all projects. Set page size to 5 and paged through projects. Reviewers: btrahan, jungejason, zeeg Reviewed By: btrahan CC: aran, btrahan Differential Revision: 1227
This commit is contained in:
parent
770beb5cd5
commit
81acf588e2
8 changed files with 322 additions and 7 deletions
|
@ -73,6 +73,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontRequestFailureView' => 'view/page/failure',
|
'AphrontRequestFailureView' => 'view/page/failure',
|
||||||
'AphrontResponse' => 'aphront/response/base',
|
'AphrontResponse' => 'aphront/response/base',
|
||||||
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/scopeguard',
|
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/scopeguard',
|
||||||
|
'AphrontSideNavFilterView' => 'view/layout/sidenavfilter',
|
||||||
'AphrontSideNavView' => 'view/layout/sidenav',
|
'AphrontSideNavView' => 'view/layout/sidenav',
|
||||||
'AphrontTableView' => 'view/control/table',
|
'AphrontTableView' => 'view/control/table',
|
||||||
'AphrontTokenizerTemplateView' => 'view/control/tokenizer',
|
'AphrontTokenizerTemplateView' => 'view/control/tokenizer',
|
||||||
|
@ -568,6 +569,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorProjectProfile' => 'applications/project/storage/profile',
|
'PhabricatorProjectProfile' => 'applications/project/storage/profile',
|
||||||
'PhabricatorProjectProfileController' => 'applications/project/controller/profile',
|
'PhabricatorProjectProfileController' => 'applications/project/controller/profile',
|
||||||
'PhabricatorProjectProfileEditController' => 'applications/project/controller/profileedit',
|
'PhabricatorProjectProfileEditController' => 'applications/project/controller/profileedit',
|
||||||
|
'PhabricatorProjectQuery' => 'applications/project/query/project',
|
||||||
'PhabricatorProjectStatus' => 'applications/project/constants/status',
|
'PhabricatorProjectStatus' => 'applications/project/constants/status',
|
||||||
'PhabricatorProjectSubproject' => 'applications/project/storage/subproject',
|
'PhabricatorProjectSubproject' => 'applications/project/storage/subproject',
|
||||||
'PhabricatorRedirectController' => 'applications/base/controller/redirect',
|
'PhabricatorRedirectController' => 'applications/base/controller/redirect',
|
||||||
|
@ -815,6 +817,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontRedirectResponse' => 'AphrontResponse',
|
'AphrontRedirectResponse' => 'AphrontResponse',
|
||||||
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
||||||
'AphrontRequestFailureView' => 'AphrontView',
|
'AphrontRequestFailureView' => 'AphrontView',
|
||||||
|
'AphrontSideNavFilterView' => 'AphrontView',
|
||||||
'AphrontSideNavView' => 'AphrontView',
|
'AphrontSideNavView' => 'AphrontView',
|
||||||
'AphrontTableView' => 'AphrontView',
|
'AphrontTableView' => 'AphrontView',
|
||||||
'AphrontTokenizerTemplateView' => 'AphrontView',
|
'AphrontTokenizerTemplateView' => 'AphrontView',
|
||||||
|
|
|
@ -199,6 +199,7 @@ class AphrontDefaultApplicationConfiguration
|
||||||
|
|
||||||
'/project/' => array(
|
'/project/' => array(
|
||||||
'$' => 'PhabricatorProjectListController',
|
'$' => 'PhabricatorProjectListController',
|
||||||
|
'filter/(?P<filter>[^/]+)/$' => 'PhabricatorProjectListController',
|
||||||
'edit/(?P<id>\d+)/$' => 'PhabricatorProjectProfileEditController',
|
'edit/(?P<id>\d+)/$' => 'PhabricatorProjectProfileEditController',
|
||||||
'view/(?P<id>\d+)/(?:(?P<page>\w+)/)?$'
|
'view/(?P<id>\d+)/(?:(?P<page>\w+)/)?$'
|
||||||
=> 'PhabricatorProjectProfileController',
|
=> 'PhabricatorProjectProfileController',
|
||||||
|
|
|
@ -19,10 +19,54 @@
|
||||||
class PhabricatorProjectListController
|
class PhabricatorProjectListController
|
||||||
extends PhabricatorProjectController {
|
extends PhabricatorProjectController {
|
||||||
|
|
||||||
public function processRequest() {
|
private $filter;
|
||||||
|
|
||||||
|
public function willProcessRequest(array $data) {
|
||||||
|
$this->filter = idx($data, 'filter');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
|
||||||
|
$nav = new AphrontSideNavFilterView();
|
||||||
|
$nav
|
||||||
|
->setBaseURI(new PhutilURI('/project/filter/'))
|
||||||
|
->addLabel('User')
|
||||||
|
->addFilter('active', 'Active')
|
||||||
|
->addFilter('owned', 'Owned')
|
||||||
|
->addSpacer()
|
||||||
|
->addLabel('All')
|
||||||
|
->addFilter('all', 'All Projects');
|
||||||
|
$this->filter = $nav->selectFilter($this->filter, 'active');
|
||||||
|
|
||||||
|
$pager = new AphrontPagerView();
|
||||||
|
$pager->setPageSize(250);
|
||||||
|
$pager->setURI($request->getRequestURI(), 'page');
|
||||||
|
$pager->setOffset($request->getInt('page'));
|
||||||
|
|
||||||
|
$query = new PhabricatorProjectQuery();
|
||||||
|
$query->setOffset($pager->getOffset());
|
||||||
|
$query->setLimit($pager->getPageSize() + 1);
|
||||||
|
|
||||||
|
$view_phid = $request->getUser()->getPHID();
|
||||||
|
|
||||||
|
switch ($this->filter) {
|
||||||
|
case 'active':
|
||||||
|
$table_header = 'Active Projects';
|
||||||
|
$query->setMembers(array($view_phid));
|
||||||
|
break;
|
||||||
|
case 'owned':
|
||||||
|
$table_header = 'Owned Projects';
|
||||||
|
$query->setOwners(array($view_phid));
|
||||||
|
break;
|
||||||
|
case 'all':
|
||||||
|
$table_header = 'All Projects';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$projects = $query->execute();
|
||||||
|
$projects = $pager->sliceResults($projects);
|
||||||
|
|
||||||
$projects = id(new PhabricatorProject())->loadAllWhere(
|
|
||||||
'1 = 1 ORDER BY id DESC limit 100');
|
|
||||||
$project_phids = mpull($projects, 'getPHID');
|
$project_phids = mpull($projects, 'getPHID');
|
||||||
|
|
||||||
$profiles = array();
|
$profiles = array();
|
||||||
|
@ -121,12 +165,15 @@ class PhabricatorProjectListController
|
||||||
));
|
));
|
||||||
|
|
||||||
$panel = new AphrontPanelView();
|
$panel = new AphrontPanelView();
|
||||||
$panel->appendChild($table);
|
$panel->setHeader($table_header);
|
||||||
$panel->setHeader('Project');
|
|
||||||
$panel->setCreateButton('Create New Project', '/project/create/');
|
$panel->setCreateButton('Create New Project', '/project/create/');
|
||||||
|
$panel->appendChild($table);
|
||||||
|
$panel->appendChild($pager);
|
||||||
|
|
||||||
|
$nav->appendChild($panel);
|
||||||
|
|
||||||
return $this->buildStandardPageResponse(
|
return $this->buildStandardPageResponse(
|
||||||
$panel,
|
$nav,
|
||||||
array(
|
array(
|
||||||
'title' => 'Projects',
|
'title' => 'Projects',
|
||||||
));
|
));
|
||||||
|
|
|
@ -10,13 +10,16 @@ phutil_require_module('phabricator', 'applications/maniphest/query');
|
||||||
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||||
phutil_require_module('phabricator', 'applications/project/constants/status');
|
phutil_require_module('phabricator', 'applications/project/constants/status');
|
||||||
phutil_require_module('phabricator', 'applications/project/controller/base');
|
phutil_require_module('phabricator', 'applications/project/controller/base');
|
||||||
|
phutil_require_module('phabricator', 'applications/project/query/project');
|
||||||
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
|
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
|
||||||
phutil_require_module('phabricator', 'applications/project/storage/profile');
|
phutil_require_module('phabricator', 'applications/project/storage/profile');
|
||||||
phutil_require_module('phabricator', 'applications/project/storage/project');
|
phutil_require_module('phabricator', 'view/control/pager');
|
||||||
phutil_require_module('phabricator', 'view/control/table');
|
phutil_require_module('phabricator', 'view/control/table');
|
||||||
phutil_require_module('phabricator', 'view/layout/panel');
|
phutil_require_module('phabricator', 'view/layout/panel');
|
||||||
|
phutil_require_module('phabricator', 'view/layout/sidenavfilter');
|
||||||
|
|
||||||
phutil_require_module('phutil', 'markup');
|
phutil_require_module('phutil', 'markup');
|
||||||
|
phutil_require_module('phutil', 'parser/uri');
|
||||||
phutil_require_module('phutil', 'utils');
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2011 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class PhabricatorProjectQuery {
|
||||||
|
|
||||||
|
private $owners;
|
||||||
|
private $members;
|
||||||
|
|
||||||
|
private $limit;
|
||||||
|
private $offset;
|
||||||
|
|
||||||
|
public function setLimit($limit) {
|
||||||
|
$this->limit = $limit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOffset($offset) {
|
||||||
|
$this->offset = $offset;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOwners(array $owners) {
|
||||||
|
$this->owners = $owners;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMembers(array $members) {
|
||||||
|
$this->members = $members;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute() {
|
||||||
|
$table = id(new PhabricatorProject());
|
||||||
|
$conn_r = $table->establishConnection('r');
|
||||||
|
|
||||||
|
$joins = $this->buildJoinsClause($conn_r);
|
||||||
|
|
||||||
|
$limit = null;
|
||||||
|
if ($this->limit) {
|
||||||
|
$limit = qsprintf(
|
||||||
|
$conn_r,
|
||||||
|
'LIMIT %d, %d',
|
||||||
|
$this->offset,
|
||||||
|
$this->limit);
|
||||||
|
} else if ($this->offset) {
|
||||||
|
$limit = qsprintf(
|
||||||
|
$conn_r,
|
||||||
|
'LIMIT %d, %d',
|
||||||
|
$this->offset,
|
||||||
|
PHP_INT_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = queryfx_all(
|
||||||
|
$conn_r,
|
||||||
|
'SELECT * FROM %T p %Q %Q',
|
||||||
|
$table->getTableName(),
|
||||||
|
$joins,
|
||||||
|
$limit);
|
||||||
|
|
||||||
|
return $table->loadAllFromArray($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildJoinsClause($conn_r) {
|
||||||
|
$affil_table = new PhabricatorProjectAffiliation();
|
||||||
|
|
||||||
|
$joins = array();
|
||||||
|
if ($this->owners) {
|
||||||
|
$joins[] = qsprintf(
|
||||||
|
$conn_r,
|
||||||
|
'JOIN %T owner ON owner.projectPHID = p.phid AND owner.isOwner = 1
|
||||||
|
AND owner.userPHID in (%Ls)',
|
||||||
|
$affil_table->getTableName(),
|
||||||
|
$this->owners);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->members) {
|
||||||
|
$joins[] = qsprintf(
|
||||||
|
$conn_r,
|
||||||
|
'JOIN %T member ON member.projectPHID = p.phid
|
||||||
|
AND member.userPHID in (%Ls)',
|
||||||
|
$affil_table->getTableName(),
|
||||||
|
$this->members);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ', $joins);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
src/applications/project/query/project/__init__.php
Normal file
17
src/applications/project/query/project/__init__.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
|
||||||
|
phutil_require_module('phabricator', 'applications/project/storage/project');
|
||||||
|
phutil_require_module('phabricator', 'storage/qsprintf');
|
||||||
|
phutil_require_module('phabricator', 'storage/queryfx');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('PhabricatorProjectQuery.php');
|
126
src/view/layout/sidenavfilter/AphrontSideNavFilterView.php
Normal file
126
src/view/layout/sidenavfilter/AphrontSideNavFilterView.php
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2011 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like an @{class:AphrontSideNavView}, but with a little bit of logic for the
|
||||||
|
* common case where you're using the side nav to filter some view of objects.
|
||||||
|
*
|
||||||
|
* For example:
|
||||||
|
*
|
||||||
|
* $nav = new AphrontSideNavFilterView();
|
||||||
|
* $nav
|
||||||
|
* ->setBaseURI($some_uri)
|
||||||
|
* ->addLabel('Cats')
|
||||||
|
* ->addFilter('meow', 'Meow')
|
||||||
|
* ->addFilter('purr', 'Purr')
|
||||||
|
* ->addSpacer()
|
||||||
|
* ->addLabel('Dogs')
|
||||||
|
* ->addFilter('woof', 'Woof')
|
||||||
|
* ->addFilter('bark', 'Bark');
|
||||||
|
* $valid_filter = $nav->selectFilter($user_selection, $default = 'meow');
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
final class AphrontSideNavFilterView extends AphrontView {
|
||||||
|
|
||||||
|
private $items = array();
|
||||||
|
private $baseURI;
|
||||||
|
private $selectedFilter = false;
|
||||||
|
|
||||||
|
public function addFilter($key, $name) {
|
||||||
|
$this->items[] = array('filter', $key, $name);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addLabel($name) {
|
||||||
|
$this->items[] = array('label', null, $name);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSpacer() {
|
||||||
|
$this->items[] = array('spacer', null, null);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBaseURI(PhutilURI $uri) {
|
||||||
|
$this->baseURI = $uri;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectFilter($key, $default) {
|
||||||
|
$this->selectedFilter = $default;
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
if ($item[0] == 'filter') {
|
||||||
|
if ($item[1] == $key) {
|
||||||
|
$this->selectedFilter = $key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->selectedFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render() {
|
||||||
|
if (!$this->baseURI) {
|
||||||
|
throw new Exception("Call setBaseURI() before render()!");
|
||||||
|
}
|
||||||
|
if ($this->selectedFilter === false) {
|
||||||
|
throw new Exception("Call selectFilter() before render()!");
|
||||||
|
}
|
||||||
|
|
||||||
|
$view = new AphrontSideNavView();
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
list($type, $key, $name) = $item;
|
||||||
|
switch ($type) {
|
||||||
|
case 'spacer':
|
||||||
|
$view->addNavItem('<br />');
|
||||||
|
break;
|
||||||
|
case 'label':
|
||||||
|
$view->addNavItem(
|
||||||
|
phutil_render_tag(
|
||||||
|
'span',
|
||||||
|
array(),
|
||||||
|
phutil_escape_html($name)));
|
||||||
|
break;
|
||||||
|
case 'filter':
|
||||||
|
$class = ($key == $this->selectedFilter)
|
||||||
|
? 'aphront-side-nav-selected'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$href = clone $this->baseURI;
|
||||||
|
$href->setPath($href->getPath().$key.'/');
|
||||||
|
$href = (string)$href;
|
||||||
|
|
||||||
|
$view->addNavItem(
|
||||||
|
phutil_render_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'href' => $href,
|
||||||
|
'class' => $class,
|
||||||
|
),
|
||||||
|
phutil_escape_html($name)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown item type '{$type}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$view->appendChild($this->renderChildren());
|
||||||
|
|
||||||
|
return $view->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
src/view/layout/sidenavfilter/__init__.php
Normal file
15
src/view/layout/sidenavfilter/__init__.php
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is automatically generated. Lint this module to rebuild it.
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_module('phabricator', 'view/base');
|
||||||
|
phutil_require_module('phabricator', 'view/layout/sidenav');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'markup');
|
||||||
|
|
||||||
|
|
||||||
|
phutil_require_source('AphrontSideNavFilterView.php');
|
Loading…
Reference in a new issue