From 5038ab850c373b2e986b2f9d5dc2ae0a137da099 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 3 Apr 2011 19:20:47 -0700 Subject: [PATCH] Some owners read workflows. --- src/__celerity_resource_map__.php | 81 +++++-- src/__phutil_library_map__.php | 6 + ...AphrontDefaultApplicationConfiguration.php | 7 + .../DiffusionPathCompleteController.php | 68 ++++++ .../controller/pathcomplete/__init__.php | 20 ++ .../DiffusionPathValidateController.php | 80 +++++++ .../controller/pathvalidate/__init__.php | 19 ++ .../browse/base/DiffusionBrowseQuery.php | 10 + .../browse/git/DiffusionGitBrowseQuery.php | 4 + .../browse/svn/DiffusionSvnBrowseQuery.php | 4 + .../request/base/DiffusionRequest.php | 4 +- .../request/git/DiffusionGitRequest.php | 10 +- .../PhabricatorOwnersDetailController.php | 130 ++++++++++- .../owners/controller/detail/__init__.php | 11 + .../list/PhabricatorOwnersListController.php | 53 ++++- .../owners/controller/list/__init__.php | 10 + .../PhabricatorRepositoryEditController.php | 17 ++ .../AphrontTypeaheadTemplateView.php | 81 +++++++ src/view/control/typeahead/__init__.php | 16 ++ webroot/rsrc/css/aphront/tokenizer.css | 2 + .../application/owners/owners-path-editor.css | 37 ++++ .../js/application/herald/HeraldRuleEditor.js | 1 + .../js/application/herald/PathTypeahead.js | 202 ++++++++++++++++++ .../js/application/owners/OwnersPathEditor.js | 171 +++++++++++++++ .../application/owners/owners-path-editor.js | 10 + 25 files changed, 1026 insertions(+), 28 deletions(-) create mode 100644 src/applications/diffusion/controller/pathcomplete/DiffusionPathCompleteController.php create mode 100644 src/applications/diffusion/controller/pathcomplete/__init__.php create mode 100644 src/applications/diffusion/controller/pathvalidate/DiffusionPathValidateController.php create mode 100644 src/applications/diffusion/controller/pathvalidate/__init__.php create mode 100644 src/view/control/typeahead/AphrontTypeaheadTemplateView.php create mode 100644 src/view/control/typeahead/__init__.php create mode 100644 webroot/rsrc/css/application/owners/owners-path-editor.css create mode 100644 webroot/rsrc/js/application/herald/PathTypeahead.js create mode 100644 webroot/rsrc/js/application/owners/OwnersPathEditor.js create mode 100644 webroot/rsrc/js/application/owners/owners-path-editor.js diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index bb005d0a03..758a39c077 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -117,7 +117,7 @@ celerity_register_resource_map(array( ), 'aphront-tokenizer-control-css' => array( - 'uri' => '/res/a3d23074/rsrc/css/aphront/tokenizer.css', + 'uri' => '/res/190349be/rsrc/css/aphront/tokenizer.css', 'type' => 'css', 'requires' => array( @@ -297,6 +297,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/objectselector/object-selector.css', ), + 'owners-path-editor-css' => + array( + 'uri' => '/res/f40dc6b1/rsrc/css/application/owners/owners-path-editor.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/owners/owners-path-editor.css', + ), 'phabricator-profile-css' => array( 'uri' => '/res/259ad37f/rsrc/css/application/people/profile.css', @@ -503,16 +512,28 @@ celerity_register_resource_map(array( ), 'herald-rule-editor' => array( - 'uri' => '/res/8b5e9d5e/rsrc/js/application/herald/HeraldRuleEditor.js', + 'uri' => '/res/ec8e2110/rsrc/js/application/herald/HeraldRuleEditor.js', 'type' => 'js', 'requires' => array( 0 => 'multirow-row-manager', 1 => 'javelin-lib-dev', 2 => 'javelin-typeahead-dev', + 3 => 'path-typeahead', ), 'disk' => '/rsrc/js/application/herald/HeraldRuleEditor.js', ), + 'path-typeahead' => + array( + 'uri' => '/res/42fb76c3/rsrc/js/application/herald/PathTypeahead.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-lib-dev', + 1 => 'javelin-typeahead-dev', + ), + 'disk' => '/rsrc/js/application/herald/PathTypeahead.js', + ), 'javelin-behavior-maniphest-transaction-controls' => array( 'uri' => '/res/fc6a8722/rsrc/js/application/maniphest/behavior-transaction-controls.js', @@ -523,6 +544,30 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/maniphest/behavior-transaction-controls.js', ), + 'javelin-behavior-owners-path-editor' => + array( + 'uri' => '/res/7568aa22/rsrc/js/application/owners/owners-path-editor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'owners-path-editor', + 1 => 'javelin-lib-dev', + ), + 'disk' => '/rsrc/js/application/owners/owners-path-editor.js', + ), + 'owners-path-editor' => + array( + 'uri' => '/res/b01c1ca9/rsrc/js/application/owners/OwnersPathEditor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'multirow-row-manager', + 1 => 'javelin-lib-dev', + 2 => 'javelin-typeahead-dev', + 3 => 'path-typeahead', + ), + 'disk' => '/rsrc/js/application/owners/OwnersPathEditor.js', + ), 'javelin-magical-init' => array( 'uri' => '/res/76614f84/rsrc/js/javelin/init.dev.js', @@ -598,7 +643,7 @@ celerity_register_resource_map(array( ), array ( 'packages' => array ( - 'ce1b9ed3' => + 'e3ec35d7' => array ( 'name' => 'core.pkg.css', 'symbols' => @@ -618,7 +663,7 @@ celerity_register_resource_map(array( 12 => 'phabricator-remarkup-css', 13 => 'syntax-highlighting-css', ), - 'uri' => '/res/pkg/ce1b9ed3/core.pkg.css', + 'uri' => '/res/pkg/e3ec35d7/core.pkg.css', 'type' => 'css', ), '76f3c1f8' => @@ -665,20 +710,20 @@ celerity_register_resource_map(array( ), 'reverse' => array ( - 'phabricator-core-css' => 'ce1b9ed3', - 'phabricator-core-buttons-css' => 'ce1b9ed3', - 'phabricator-standard-page-view' => 'ce1b9ed3', - 'aphront-dialog-view-css' => 'ce1b9ed3', - 'aphront-form-view-css' => 'ce1b9ed3', - 'aphront-panel-view-css' => 'ce1b9ed3', - 'aphront-side-nav-view-css' => 'ce1b9ed3', - 'aphront-table-view-css' => 'ce1b9ed3', - 'aphront-crumbs-view-css' => 'ce1b9ed3', - 'aphront-tokenizer-control-css' => 'ce1b9ed3', - 'aphront-typeahead-control-css' => 'ce1b9ed3', - 'phabricator-directory-css' => 'ce1b9ed3', - 'phabricator-remarkup-css' => 'ce1b9ed3', - 'syntax-highlighting-css' => 'ce1b9ed3', + 'phabricator-core-css' => 'e3ec35d7', + 'phabricator-core-buttons-css' => 'e3ec35d7', + 'phabricator-standard-page-view' => 'e3ec35d7', + 'aphront-dialog-view-css' => 'e3ec35d7', + 'aphront-form-view-css' => 'e3ec35d7', + 'aphront-panel-view-css' => 'e3ec35d7', + 'aphront-side-nav-view-css' => 'e3ec35d7', + 'aphront-table-view-css' => 'e3ec35d7', + 'aphront-crumbs-view-css' => 'e3ec35d7', + 'aphront-tokenizer-control-css' => 'e3ec35d7', + 'aphront-typeahead-control-css' => 'e3ec35d7', + 'phabricator-directory-css' => 'e3ec35d7', + 'phabricator-remarkup-css' => 'e3ec35d7', + 'syntax-highlighting-css' => 'e3ec35d7', 'differential-core-view-css' => '76f3c1f8', 'differential-changeset-view-css' => '76f3c1f8', 'differential-revision-detail-css' => '76f3c1f8', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 790bce9fc7..e8d4a40a1d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -61,6 +61,7 @@ phutil_register_library_map(array( 'AphrontSideNavView' => 'view/layout/sidenav', 'AphrontTableView' => 'view/control/table', 'AphrontTokenizerTemplateView' => 'view/control/tokenizer', + 'AphrontTypeaheadTemplateView' => 'view/control/typeahead', 'AphrontURIMapper' => 'aphront/mapper', 'AphrontView' => 'view/base', 'AphrontWebpageResponse' => 'aphront/response/webpage', @@ -180,6 +181,8 @@ phutil_register_library_map(array( 'DiffusionLastModifiedQuery' => 'applications/diffusion/query/lastmodified/base', 'DiffusionPathChange' => 'applications/diffusion/data/pathchange', 'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/base', + 'DiffusionPathCompleteController' => 'applications/diffusion/controller/pathcomplete', + 'DiffusionPathValidateController' => 'applications/diffusion/controller/pathvalidate', 'DiffusionRepositoryController' => 'applications/diffusion/controller/repository', 'DiffusionRepositoryPath' => 'applications/diffusion/data/repositorypath', 'DiffusionRequest' => 'applications/diffusion/request/base', @@ -487,6 +490,7 @@ phutil_register_library_map(array( 'AphrontSideNavView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontTokenizerTemplateView' => 'AphrontView', + 'AphrontTypeaheadTemplateView' => 'AphrontView', 'AphrontWebpageResponse' => 'AphrontResponse', 'CelerityResourceController' => 'AphrontController', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', @@ -566,6 +570,8 @@ phutil_register_library_map(array( 'DiffusionHistoryTableView' => 'DiffusionView', 'DiffusionHomeController' => 'DiffusionController', 'DiffusionLastModifiedController' => 'DiffusionController', + 'DiffusionPathCompleteController' => 'DiffusionController', + 'DiffusionPathValidateController' => 'DiffusionController', 'DiffusionRepositoryController' => 'DiffusionController', 'DiffusionSvnBrowseQuery' => 'DiffusionBrowseQuery', 'DiffusionSvnDiffQuery' => 'DiffusionDiffQuery', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 1aba28e37d..59819b28ec 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -218,6 +218,12 @@ class AphrontDefaultApplicationConfiguration '$' => 'DiffusionLastModifiedController', ), + 'services/' => array( + 'path/' => array( + 'complete/$' => 'DiffusionPathCompleteController', + 'validate/$' => 'DiffusionPathValidateController', + ), + ), ), '/daemon/' => array( @@ -255,6 +261,7 @@ class AphrontDefaultApplicationConfiguration '$' => 'PhabricatorOwnersListController', 'view/(?P[^/]+)/$' => 'PhabricatorOwnersListController', 'package/(?P\d+)/$' => 'PhabricatorOwnersDetailController', + 'new/$' => 'PhabricatorOwnersDetailController', ), ); diff --git a/src/applications/diffusion/controller/pathcomplete/DiffusionPathCompleteController.php b/src/applications/diffusion/controller/pathcomplete/DiffusionPathCompleteController.php new file mode 100644 index 0000000000..ccb5fbfed0 --- /dev/null +++ b/src/applications/diffusion/controller/pathcomplete/DiffusionPathCompleteController.php @@ -0,0 +1,68 @@ +getRequest(); + + $repository_phid = $request->getStr('repositoryPHID'); + $repository = id(new PhabricatorRepository())->loadOneWhere( + 'phid = %s', + $repository_phid); + if (!$repository) { + return new Aphront400Response(); + } + + $query_path = $request->getStr('q'); + $query_path = ltrim($query_path, '/'); + if (preg_match('@/$@', $query_path)) { + $query_dir = $query_path; + } else { + $query_dir = dirname($query_path); + if ($query_dir == '.') { + $query_dir = ''; + } + } + + $drequest = DiffusionRequest::newFromAphrontRequestDictionary( + array( + 'callsign' => $repository->getCallsign(), + 'path' => $query_dir, + 'nobranch' => true, + )); + + $browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest); + $paths = $browse_query->loadPaths(); + + $output = array(); + foreach ($paths as $path) { + $full_path = $query_dir.$path->getPath(); + if ($path->getFileType() == DifferentialChangeType::FILE_DIRECTORY) { + $full_path .= '/'; + } + $output[] = array('/'.$full_path, null, substr(md5($full_path), 0, 7)); + } + + return id(new AphrontAjaxResponse())->setContent($output); + } +} diff --git a/src/applications/diffusion/controller/pathcomplete/__init__.php b/src/applications/diffusion/controller/pathcomplete/__init__.php new file mode 100644 index 0000000000..618561986f --- /dev/null +++ b/src/applications/diffusion/controller/pathcomplete/__init__.php @@ -0,0 +1,20 @@ +getRequest(); + + $repository_phid = $request->getStr('repositoryPHID'); + $repository = id(new PhabricatorRepository())->loadOneWhere( + 'phid = %s', + $repository_phid); + if (!$repository) { + return new Aphront400Response(); + } + + $path = $request->getStr('path'); + $path = ltrim($path, '/'); + + $drequest = DiffusionRequest::newFromAphrontRequestDictionary( + array( + 'callsign' => $repository->getCallsign(), + 'path' => $path, + 'nobranch' => true, + )); + + $browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest); + $browse_query->needValidityOnly(true); + $valid = $browse_query->loadPaths(); + + if (!$valid) { + switch ($browse_query->getReasonForEmptyResultSet()) { + case DiffusionBrowseQuery::REASON_IS_FILE: + $valid = true; + break; + case DiffusionBrowseQuery::REASON_IS_EMPTY: + $valid = true; + break; + } + } + + $output = array( + 'valid' => (bool)$valid, + ); + + if (!$valid) { + $branch = $drequest->getBranch(); + if ($branch) { + $message = 'Not found in '.$branch; + } else { + $message = 'Not found at HEAD'; + } + } else { + $message = 'OK'; + } + + $output['message'] = $message; + + return id(new AphrontAjaxResponse())->setContent($output); + } +} diff --git a/src/applications/diffusion/controller/pathvalidate/__init__.php b/src/applications/diffusion/controller/pathvalidate/__init__.php new file mode 100644 index 0000000000..dab479a12f --- /dev/null +++ b/src/applications/diffusion/controller/pathvalidate/__init__.php @@ -0,0 +1,19 @@ +executeQuery(); } + final public function shouldOnlyTestValidity() { + return $this->validityOnly; + } + + final public function needValidityOnly($need_validity_only) { + $this->validityOnly = $need_validity_only; + return $this; + } + abstract protected function executeQuery(); } diff --git a/src/applications/diffusion/query/browse/git/DiffusionGitBrowseQuery.php b/src/applications/diffusion/query/browse/git/DiffusionGitBrowseQuery.php index 503fdbfff0..a044b022b8 100644 --- a/src/applications/diffusion/query/browse/git/DiffusionGitBrowseQuery.php +++ b/src/applications/diffusion/query/browse/git/DiffusionGitBrowseQuery.php @@ -63,6 +63,10 @@ final class DiffusionGitBrowseQuery extends DiffusionBrowseQuery { return array(); } + if ($this->shouldOnlyTestValidity()) { + return true; + } + list($stdout) = execx( "(cd %s && git ls-tree -l %s:%s)", $local_path, diff --git a/src/applications/diffusion/query/browse/svn/DiffusionSvnBrowseQuery.php b/src/applications/diffusion/query/browse/svn/DiffusionSvnBrowseQuery.php index db676941d3..7356093598 100644 --- a/src/applications/diffusion/query/browse/svn/DiffusionSvnBrowseQuery.php +++ b/src/applications/diffusion/query/browse/svn/DiffusionSvnBrowseQuery.php @@ -103,6 +103,10 @@ final class DiffusionSvnBrowseQuery extends DiffusionBrowseQuery { return array(); } + if ($this->shouldOnlyTestValidity()) { + return true; + } + $sql = array(); foreach ($index as $row) { $sql[] = '('.(int)$row['pathID'].', '.(int)$row['maxCommit'].')'; diff --git a/src/applications/diffusion/request/base/DiffusionRequest.php b/src/applications/diffusion/request/base/DiffusionRequest.php index 378dd9e7ed..4498d136c1 100644 --- a/src/applications/diffusion/request/base/DiffusionRequest.php +++ b/src/applications/diffusion/request/base/DiffusionRequest.php @@ -67,12 +67,12 @@ class DiffusionRequest { $object->commit = idx($data, 'commit'); $object->path = idx($data, 'path'); - $object->initializeFromAphrontRequestDictionary(); + $object->initializeFromAphrontRequestDictionary($data); return $object; } - protected function initializeFromAphrontRequestDictionary() { + protected function initializeFromAphrontRequestDictionary(array $data) { } diff --git a/src/applications/diffusion/request/git/DiffusionGitRequest.php b/src/applications/diffusion/request/git/DiffusionGitRequest.php index e1b0c4d520..3ac4e8aa32 100644 --- a/src/applications/diffusion/request/git/DiffusionGitRequest.php +++ b/src/applications/diffusion/request/git/DiffusionGitRequest.php @@ -18,14 +18,16 @@ class DiffusionGitRequest extends DiffusionRequest { - protected function initializeFromAphrontRequestDictionary() { - parent::initializeFromAphrontRequestDictionary(); + protected function initializeFromAphrontRequestDictionary(array $data) { + parent::initializeFromAphrontRequestDictionary($data); $path = $this->path; $parts = explode('/', $path); - $branch = array_shift($parts); - $this->branch = $this->decodeBranchName($branch); + if (empty($data['nobranch'])) { + $branch = array_shift($parts); + $this->branch = $this->decodeBranchName($branch); + } foreach ($parts as $key => $part) { // Prevent any hyjinx since we're ultimately shipping this to the diff --git a/src/applications/owners/controller/detail/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/detail/PhabricatorOwnersDetailController.php index f3451eb807..4dba3244a5 100644 --- a/src/applications/owners/controller/detail/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/detail/PhabricatorOwnersDetailController.php @@ -18,11 +18,135 @@ class PhabricatorOwnersDetailController extends PhabricatorOwnersController { + private $id; + + public function willProcessRequest(array $data) { + $this->id = idx($data, 'id'); + } + public function processRequest() { - return $this->buildStandardPageResponse( - 'quack', + $request = $this->getRequest(); + $user = $request->getUser(); + + if ($this->id) { + $package = id(new PhabricatorOwnersPackage())->load($this->id); + if (!$package) { + return new Aphront404Response(); + } + } else { + $package = new PhabricatorOwnersPackage(); + $package->setPrimaryOwnerPHID($user->getPHID()); + } + + $e_name = true; + $e_primary = true; + + + $token_primary_owner = array(); + $token_all_owners = array(); + + $title = $package->getID() ? 'Edit Package' : 'New Package'; + + $repos = id(new PhabricatorRepository())->loadAll(); + + $default_paths = array(); + foreach ($repos as $repo) { + $default_path = $repo->getDetail('default-owners-path'); + if ($default_path) { + $default_paths[$repo->getPHID()] = $default_path; + } + } + + $repos = mpull($repos, 'getCallsign', 'getPHID'); + + $template = new AphrontTypeaheadTemplateView(); + $template = $template->render(); + + + Javelin::initBehavior( + 'owners-path-editor', array( - 'title' => 'detail', + 'root' => 'path-editor', + 'table' => 'paths', + 'add_button' => 'addpath', + 'repositories' => $repos, + 'input_template' => $template, + 'path_refs' => array(), + + 'completeURI' => '/diffusion/services/path/complete/', + 'validateURI' => '/diffusion/services/path/validate/', + + 'repositoryDefaultPaths' => $default_paths, + )); + + require_celerity_resource('owners-path-editor-css'); + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setValue($package->getName()) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/users/') + ->setLabel('Primary Owner') + ->setName('primary') + ->setLimit(1) + ->setValue($token_primary_owner) + ->setError($e_primary)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/users/') + ->setLabel('Owners') + ->setName('owners') + ->setValue($token_all_owners) + ->setError($e_primary)) + ->appendChild( + '

Paths

'. + '
'. + '
'. + javelin_render_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button green', + 'sigil' => 'addpath', + 'mustcapture' => true, + ), + 'Add New Path'). + '
'. + '

Specify the files and directories which comprise this '. + 'package.

'. + '
'. + javelin_render_tag( + 'table', + array( + 'class' => 'owners-path-editor-table', + 'sigil' => 'paths', + ), + ''). + '
') + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Description') + ->setName('description') + ->setValue($package->getDescription())) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Save Package')); + + $panel = new AphrontPanelView(); + $panel->setHeader($title); + $panel->setWidth(AphrontPanelView::WIDTH_WIDE); + $panel->appendChild($form); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => $title, )); } diff --git a/src/applications/owners/controller/detail/__init__.php b/src/applications/owners/controller/detail/__init__.php index c06db617d0..4a02ae3b99 100644 --- a/src/applications/owners/controller/detail/__init__.php +++ b/src/applications/owners/controller/detail/__init__.php @@ -6,7 +6,18 @@ +phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'applications/owners/controller/base'); +phutil_require_module('phabricator', 'applications/owners/storage/package'); +phutil_require_module('phabricator', 'applications/repository/storage/repository'); +phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/api'); +phutil_require_module('phabricator', 'view/control/typeahead'); +phutil_require_module('phabricator', 'view/form/base'); +phutil_require_module('phabricator', 'view/form/control/submit'); +phutil_require_module('phabricator', 'view/layout/panel'); + +phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorOwnersDetailController.php'); diff --git a/src/applications/owners/controller/list/PhabricatorOwnersListController.php b/src/applications/owners/controller/list/PhabricatorOwnersListController.php index b21594d2d3..540d655405 100644 --- a/src/applications/owners/controller/list/PhabricatorOwnersListController.php +++ b/src/applications/owners/controller/list/PhabricatorOwnersListController.php @@ -26,6 +26,9 @@ class PhabricatorOwnersListController extends PhabricatorOwnersController { public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + $views = array( 'owned' => 'Owned Packages', 'all' => 'All Packages', @@ -57,7 +60,7 @@ class PhabricatorOwnersListController extends PhabricatorOwnersController { switch ($this->view) { case 'search': - $content = 'search goes here'; + $content = $this->renderPackageTable(array(), 'Search Results'); break; case 'owned': $content = $this->renderOwnedView(); @@ -67,6 +70,54 @@ class PhabricatorOwnersListController extends PhabricatorOwnersController { break; } + $filter = new AphrontListFilterView(); + $filter->addButton( + phutil_render_tag( + 'a', + array( + 'href' => '/owners/new/', + 'class' => 'green button', + ), + 'Create New Package')); + + $owners_search_value = array(); + if ($request->getArr('owner')) { + $phids = $request->getArr('owner'); + $phid = reset($phids); + $handles = id(new PhabricatorObjectHandleData(array($phid))) + ->loadHandles(); + $owners_search_value = array( + $phid => $handles[$phid]->getFullName(), + ); + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->setAction('/owners/view/search/') + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('name') + ->setLabel('Name') + ->setValue($request->getStr('name'))) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/users/') + ->setLimit(1) + ->setName('owner') + ->setLabel('Owner') + ->setValue($owners_search_value)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('path') + ->setLabel('Path') + ->setValue($request->getStr('path'))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Search for Packages')); + + $filter->appendChild($form); + + $nav->appendChild($filter); $nav->appendChild($content); return $this->buildStandardPageResponse( diff --git a/src/applications/owners/controller/list/__init__.php b/src/applications/owners/controller/list/__init__.php index 91565a263b..e6920b2e11 100644 --- a/src/applications/owners/controller/list/__init__.php +++ b/src/applications/owners/controller/list/__init__.php @@ -7,6 +7,16 @@ phutil_require_module('phabricator', 'applications/owners/controller/base'); +phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'view/control/table'); +phutil_require_module('phabricator', 'view/form/base'); +phutil_require_module('phabricator', 'view/form/control/submit'); +phutil_require_module('phabricator', 'view/layout/listfilter'); +phutil_require_module('phabricator', 'view/layout/panel'); +phutil_require_module('phabricator', 'view/layout/sidenav'); + +phutil_require_module('phutil', 'markup'); +phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorOwnersListController.php'); diff --git a/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php b/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php index 46ce7413fe..6dc2f7a0c0 100644 --- a/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php +++ b/src/applications/repository/controller/edit/PhabricatorRepositoryEditController.php @@ -234,6 +234,12 @@ class PhabricatorRepositoryEditController $request->getStr('default-branch')); } + $repository->setDetail( + 'default-owners-path', + $request->getStr( + 'default-owners-path', + '/')); + $repository->setDetail( 'detail-parser', $request->getStr( @@ -358,6 +364,17 @@ class PhabricatorRepositoryEditController 'Default remote branch to show in Diffusion.')); } + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('default-owners-path') + ->setLabel('Default Owners Path') + ->setValue( + $repository->getDetail( + 'default-owners-path', + '/')) + ->setCaption('Default path in Owners tool.')); + $parsers = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorRepositoryCommitMessageDetailParser') ->selectSymbolsWithoutLoading(); diff --git a/src/view/control/typeahead/AphrontTypeaheadTemplateView.php b/src/view/control/typeahead/AphrontTypeaheadTemplateView.php new file mode 100644 index 0000000000..a67b8acea1 --- /dev/null +++ b/src/view/control/typeahead/AphrontTypeaheadTemplateView.php @@ -0,0 +1,81 @@ +id = $id; + return $this; + } + + public function setValue(array $value) { + $this->value = $value; + return $this; + } + + public function getValue() { + return $this->value; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function render() { + require_celerity_resource('aphront-typeahead-control-css'); + + $id = $this->id; + $name = $this->getName(); + $values = nonempty($this->getValue(), array()); + + $tokens = array(); + foreach ($values as $key => $value) { + $tokens[] = $this->renderToken($key, $value); + } + + $input = javelin_render_tag( + 'input', + array( + 'name' => $name, + 'class' => 'jx-typeahead-input', + 'sigil' => 'typeahead', + 'type' => 'text', + 'value' => $this->value, + 'autocomplete' => 'off', + )); + + return javelin_render_tag( + 'div', + array( + 'id' => $id, + 'sigil' => 'typeahead-hardpoint', + 'class' => 'jx-typeahead-hardpoint', + ), + $input. + '
'); + } +} diff --git a/src/view/control/typeahead/__init__.php b/src/view/control/typeahead/__init__.php new file mode 100644 index 0000000000..53a704b096 --- /dev/null +++ b/src/view/control/typeahead/__init__.php @@ -0,0 +1,16 @@ + elem for choosing the repository of a path. + */ + _repositorySelect : null, + /* + * DOM parent div "hardpoint" to be passed to the JX.Typeahead. + */ + _hardpoint : null, + /* + * DOM element to display errors. + */ + _errorDisplay : null, + /* + * URI to query for typeahead results, to be passed to the + * TypeaheadOnDemandSource. + */ + _completeURI : null, + + /* + * Underlying JX.TypeaheadOnDemandSource instance + */ + _datasource : null, + + /* + * Underlying JX.Typeahead instance + */ + _typeahead : null, + + /* + * Underlying input + */ + _input : null, + + /* + * Whenever the user changes the typeahead value, we track the change + * here, keyed by the selected repository ID. That way, we can restore + * typed values if they change the repository choice and then change back. + */ + _textInputValues : null, + + /* + * Configurable endpoint for server-side path validation + */ + _validateURI : null, + + /* + * Keep the validation AJAX request so we don't send several. + */ + _validationInflight : null, + + /* + * Installs path-specific behaviors and then starts the underlying + * typeahead. + */ + start : function() { + if (this._typeahead.getValue()) { + this._textInputValues[this._repositorySelect.value] = + this._typeahead.getValue(); + } + + this._typeahead.listen( + 'change', + JX.bind(this, function(value) { + this._textInputValues[this._repositorySelect.value] = value; + this._validate(); + })); + + this._typeahead.listen( + 'choose', + JX.bind(this, function() { + JX.defer( + JX.bind(this._typeahead, this._typeahead.refresh)); + })); + + var repo_set_input = JX.bind(this, this._onrepochange); + + this._typeahead.listen('start', repo_set_input); + JX.DOM.listen( + this._repositorySelect, + 'change', + null, + repo_set_input); + + this._typeahead.start(); + this._validate(); + }, + + _onrepochange : function() { + this._setPathInputBasedOnRepository( + this._typeahead, + this._textInputValues); + + this._datasource.setAuxiliaryData( + {repositoryPHID : this._repositorySelect.value} + ); + }, + + _setPathInputBasedOnRepository : function(typeahead, lookup) { + if (lookup[this._repositorySelect.value]) { + typeahead.setValue(lookup[this._repositorySelect.value]); + } else { + typeahead.setValue('/'); + } + }, + + _initializeDatasource : function() { + this._datasource = new JX.TypeaheadOnDemandSource(this._completeURI); + this._datasource.setNormalizer(this._datasourceNormalizer); + this._datasource.setQueryDelay(40); + }, + + /* + * Construct and initialize the Typeahead. + * Must be called after initializing the datasource. + */ + _initializeTypeahead : function(path_input) { + this._typeahead = new JX.Typeahead(this._hardpoint, path_input); + this._datasource.setMaximumResultCount(15); + this._typeahead.setDatasource(this._datasource); + }, + + _datasourceNormalizer : function(str) { + return ('' + str).replace(/[\/]+/g, '\/'); + }, + + _validate : function() { + var input = this._input; + var repo_id = this._repositorySelect.value; + var input_value = input.value; + var error_display = this._errorDisplay; + + if (!input_value.length) { + input.value = '/'; + input_value = '/'; + } + + if (this._validationInflight) { + this._validationInflight.abort(); + this._validationInflight = null; + } + + var validation_request = new JX.Request( + this._validateURI, + function(payload) { + // Don't change validation display state if the input has been + // changed since we started validation + if (input.value === input_value) { + if (payload.valid) { + JX.DOM.alterClass(error_display, 'invalid', false); + JX.DOM.alterClass(error_display, 'valid', true); + } else { + JX.DOM.alterClass(error_display, 'invalid', true); + JX.DOM.alterClass(error_display, 'valid', false); + } + JX.DOM.setContent(error_display, payload.message); + } + }); + + validation_request.listen('finally', function() { + JX.DOM.alterClass(error_display, 'validating', false); + this._validationInflight = null; + }); + + validation_request.setData( + { + repositoryPHID : repo_id, + path : input_value + }); + + this._validationInflight = validation_request; + + validation_request.setTimeout(750); + validation_request.send(); + } + } +}); diff --git a/webroot/rsrc/js/application/owners/OwnersPathEditor.js b/webroot/rsrc/js/application/owners/OwnersPathEditor.js new file mode 100644 index 0000000000..a3c0db7066 --- /dev/null +++ b/webroot/rsrc/js/application/owners/OwnersPathEditor.js @@ -0,0 +1,171 @@ +/** + * @requires multirow-row-manager + * javelin-lib-dev + * javelin-typeahead-dev + * path-typeahead + * @provides owners-path-editor + * @javelin + */ + +JX.install('OwnersPathEditor', { + construct : function(config) { + var root = JX.$(config.root); + + this._rowManager = new JX.MultirowRowManager( + JX.DOM.find(root, 'table', config.table)); + + JX.DOM.listen( + JX.DOM.find(root, 'a', config.add_button), + 'click', + null, + JX.bind(this, this._onaddpath)); + + this._count = 0; + this._repositories = config.repositories; + this._inputTemplate = config.input_template; + + this._completeURI = config.completeURI; + this._validateURI = config.validateURI; + this._repositoryDefaultPaths = config.repositoryDefaultPaths; + + this._initializePaths(config.pathRefs); + }, + members : { + /* + * MultirowRowManager for controlling add/remove behavior + */ + _rowManager : null, + + /* + * Array of objects with 'name' and 'repo_id' keys for + * selecting the repository of a path. + */ + _repositories : null, + + /* + * How many rows have been created, for form name generation. + */ + _count : 0, + /* + * URL for the typeahead datasource. + */ + _completeURI : null, + /* + * URL for path validation requests. + */ + _validateURI : null, + /* + * Template typeahead markup to be copied per row. + */ + _inputTemplate : null, + /* + * Most packages will be in one repository, so remember whenever + * the user chooses a repository, and use that repository as the + * default for future rows. + */ + _lastRepositoryChoice : null, + + _repositoryDefaultPaths : null, + + /* + * Initialize with 0 or more rows. + * Adds one initial row if none are given. + */ + _initializePaths : function(path_refs) { + for (var k in path_refs) { + this.addPath(path_refs[k]); + } + if (!JX.keys(path_refs).length) { + this.addPath(); + } + }, + + /* + * Build a row. + */ + addPath : function(path_ref) { + // Smart default repository. See _lastRepositoryChoice. + if (path_ref) { + this._lastRepositoryChoice = path_ref.repositoryPHID; + } + path_ref = path_ref || {}; + + var selected_repository = path_ref.repositoryPHID || + this._lastRepositoryChoice; + var options = this._buildRepositoryOptions(selected_repository); + var attrs = { + name : "repo[" + this._count + "]" + }; + var repo_select = JX.$N('select', attrs, options); + + JX.DOM.listen(repo_select, 'change', null, JX.bind(this, function(e) { + this._lastRepositoryChoice = repo_select.value; + })); + + var repo_cell = JX.$N('td', {}, repo_select); + var typeahead_cell = JX.$N( + 'td', + JX.HTML(this._inputTemplate)); + + // Text input for path. + var path_input = JX.DOM.find(typeahead_cell, 'input'); + JX.copy( + path_input, + { + value : path_ref.path || "", + name : "path[" + this._count + "]", + }); + + // The Typeahead requires a display div called hardpoint. + var hardpoint = JX.DOM.find( + typeahead_cell, + 'div', + 'typeahead-hardpoint'); + + var error_display = JX.$N( + 'div', + { + className : "error-display validating" + }, + 'Validating...'); + + var error_display_cell = JX.$N('td', {}, error_display); + + var row = this._rowManager.addRow( + [repo_cell, typeahead_cell, error_display_cell]); + + new JX.PathTypeahead({ + repositoryDefaultPaths : this._repositoryDefaultPaths, + repo_select : repo_select, + path_input : path_input, + hardpoint : hardpoint, + error_display : error_display, + completeURI : this._completeURI, + validateURI : this._validateURI}).start(); + + this._count++; + return row; + }, + + _onaddpath : function(e) { + e.kill(); + this.addPath(); + }, + + /** + * Helper to build the options for the repository choice dropdown. + */ + _buildRepositoryOptions : function(selected) { + var repos = this._repositories; + var result = []; + for (var k in repos) { + var attr = { + value : k, + selected : (selected == k) + }; + result.push(JX.$N('option', attr, repos[k])); + } + return result; + } + } +}); diff --git a/webroot/rsrc/js/application/owners/owners-path-editor.js b/webroot/rsrc/js/application/owners/owners-path-editor.js new file mode 100644 index 0000000000..55f1f7ee38 --- /dev/null +++ b/webroot/rsrc/js/application/owners/owners-path-editor.js @@ -0,0 +1,10 @@ +/** + * @requires owners-path-editor + * javelin-lib-dev + * @provides javelin-behavior-owners-path-editor + * @javelin + */ + +JX.behavior('owners-path-editor', function(config) { + new JX.OwnersPathEditor(config); +});