mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-20 20:40:56 +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',
|
||||
'AphrontResponse' => 'aphront/response/base',
|
||||
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/scopeguard',
|
||||
'AphrontSideNavFilterView' => 'view/layout/sidenavfilter',
|
||||
'AphrontSideNavView' => 'view/layout/sidenav',
|
||||
'AphrontTableView' => 'view/control/table',
|
||||
'AphrontTokenizerTemplateView' => 'view/control/tokenizer',
|
||||
|
@ -568,6 +569,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectProfile' => 'applications/project/storage/profile',
|
||||
'PhabricatorProjectProfileController' => 'applications/project/controller/profile',
|
||||
'PhabricatorProjectProfileEditController' => 'applications/project/controller/profileedit',
|
||||
'PhabricatorProjectQuery' => 'applications/project/query/project',
|
||||
'PhabricatorProjectStatus' => 'applications/project/constants/status',
|
||||
'PhabricatorProjectSubproject' => 'applications/project/storage/subproject',
|
||||
'PhabricatorRedirectController' => 'applications/base/controller/redirect',
|
||||
|
@ -815,6 +817,7 @@ phutil_register_library_map(array(
|
|||
'AphrontRedirectResponse' => 'AphrontResponse',
|
||||
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
||||
'AphrontRequestFailureView' => 'AphrontView',
|
||||
'AphrontSideNavFilterView' => 'AphrontView',
|
||||
'AphrontSideNavView' => 'AphrontView',
|
||||
'AphrontTableView' => 'AphrontView',
|
||||
'AphrontTokenizerTemplateView' => 'AphrontView',
|
||||
|
|
|
@ -199,6 +199,7 @@ class AphrontDefaultApplicationConfiguration
|
|||
|
||||
'/project/' => array(
|
||||
'$' => 'PhabricatorProjectListController',
|
||||
'filter/(?P<filter>[^/]+)/$' => 'PhabricatorProjectListController',
|
||||
'edit/(?P<id>\d+)/$' => 'PhabricatorProjectProfileEditController',
|
||||
'view/(?P<id>\d+)/(?:(?P<page>\w+)/)?$'
|
||||
=> 'PhabricatorProjectProfileController',
|
||||
|
|
|
@ -19,10 +19,54 @@
|
|||
class PhabricatorProjectListController
|
||||
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');
|
||||
|
||||
$profiles = array();
|
||||
|
@ -121,12 +165,15 @@ class PhabricatorProjectListController
|
|||
));
|
||||
|
||||
$panel = new AphrontPanelView();
|
||||
$panel->appendChild($table);
|
||||
$panel->setHeader('Project');
|
||||
$panel->setHeader($table_header);
|
||||
$panel->setCreateButton('Create New Project', '/project/create/');
|
||||
$panel->appendChild($table);
|
||||
$panel->appendChild($pager);
|
||||
|
||||
$nav->appendChild($panel);
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$panel,
|
||||
$nav,
|
||||
array(
|
||||
'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/project/constants/status');
|
||||
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/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/layout/panel');
|
||||
phutil_require_module('phabricator', 'view/layout/sidenavfilter');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
phutil_require_module('phutil', 'parser/uri');
|
||||
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