diff --git a/src/applications/typeahead/application/PhabricatorTypeaheadApplication.php b/src/applications/typeahead/application/PhabricatorTypeaheadApplication.php index b69d4f4223..fdae374f90 100644 --- a/src/applications/typeahead/application/PhabricatorTypeaheadApplication.php +++ b/src/applications/typeahead/application/PhabricatorTypeaheadApplication.php @@ -1,26 +1,26 @@ array( - 'class/(?:(?P\w+)/)?' + '(?Pbrowse|class)/(?:(?P\w+)/)?' => 'PhabricatorTypeaheadModularDatasourceController', ), ); } public function isLaunchable() { return false; } public function canUninstall() { return false; } } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index a754d1362a..ffc8ce2af4 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -1,131 +1,167 @@ getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); + $is_browse = ($request->getURIData('action') == 'browse'); // Default this to the query string to make debugging a little bit easier. $raw_query = nonempty($request->getStr('raw'), $query); // This makes form submission easier in the debug view. $class = nonempty($request->getURIData('class'), $request->getStr('class')); $sources = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorTypeaheadDatasource') ->loadObjects(); if (isset($sources[$class])) { $source = $sources[$class]; - if ($source->getDatasourceApplicationClass()) { - if (!PhabricatorApplication::isClassInstalledForViewer( - $source->getDatasourceApplicationClass(), - $viewer)) { - return id(new Aphront404Response()); - } - } - $source->setParameters($request->getRequestData()); + // NOTE: Wrapping the source in a Composite datasource ensures we perform + // application visibility checks for the viewer, so we do not need to do + // those separately. $composite = new PhabricatorTypeaheadRuntimeCompositeDatasource(); $composite->addDatasource($source); $composite ->setViewer($viewer) ->setQuery($query) ->setRawQuery($raw_query); + if ($is_browse) { + $limit = 3; + $offset = $request->getInt('offset'); + $composite + ->setLimit($limit + 1) + ->setOffset($offset); + } + $results = $composite->loadResults(); + + if ($is_browse) { + $next_link = null; + if (count($results) > $limit) { + $results = array_slice($results, 0, $limit, $preserve_keys = true); + $next_link = phutil_tag( + 'a', + array( + 'href' => id(new PhutilURI($request->getRequestURI())) + ->setQueryParam('offset', $offset + $limit), + ), + pht('Next Page')); + } + + $rows = array(); + foreach ($results as $result) { + // TODO: Render nicely. + $rows[] = array_slice($result->getWireFormat(), 0, 3, true); + } + + $table = id(new AphrontTableView($rows)); + + return $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle(get_class($source)) // TODO: Provide nice names. + ->appendChild($table) + ->appendChild($next_link) + ->addCancelButton('/', pht('Close')); + } + + } else if ($is_browse) { + return new Aphront404Response(); } else { $results = array(); } $content = mpull($results, 'getWireFormat'); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } // If there's a non-Ajax request to this endpoint, show results in a tabular // format to make it easier to debug typeahead output. foreach ($sources as $key => $source) { // This can happen with composite sources like user or project, as well // generic ones like NoOwner if (!$source->getDatasourceApplicationClass()) { continue; } if (!PhabricatorApplication::isClassInstalledForViewer( $source->getDatasourceApplicationClass(), $viewer)) { unset($sources[$key]); } } $options = array_fuse(array_keys($sources)); asort($options); $form = id(new AphrontFormView()) ->setUser($viewer) ->setAction('/typeahead/class/') ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Source Class')) ->setName('class') ->setValue($class) ->setOptions($options)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Query')) ->setName('q') ->setValue($request->getStr('q'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Raw Query')) ->setName('raw') ->setValue($request->getStr('raw'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Query'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Token Query')) ->setForm($form); $table = new AphrontTableView($content); $table->setHeaders( array( pht('Name'), pht('URI'), pht('PHID'), pht('Priority'), pht('Display Name'), pht('Display Type'), pht('Image URI'), pht('Priority Type'), pht('Icon'), pht('Closed'), pht('Sprite'), )); $result_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Token Results (%s)', $class)) ->appendChild($table); return $this->buildApplicationPage( array( $form_box, $result_box, ), array( 'title' => pht('Typeahead Results'), 'device' => false, )); } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php index 9c34af71c7..8990460505 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php @@ -1,49 +1,66 @@ getOffset(); + $limit = $this->getLimit(); + $results = array(); foreach ($this->getUsableDatasources() as $source) { $source ->setRawQuery($this->getRawQuery()) ->setQuery($this->getQuery()) - ->setViewer($this->getViewer()) - ->setLimit($this->getLimit()); + ->setViewer($this->getViewer()); + + if ($limit) { + $source->setLimit($offset + $limit); + } $results[] = $source->loadResults(); } - return array_mergev($results); + + $results = array_mergev($results); + $results = msort($results, 'getName'); + + if ($offset || $limit) { + if (!$limit) { + $limit = count($results); + } + $results = array_slice($results, $offset, $limit, $preserve_keys = true); + } + + return $results; } private function getUsableDatasources() { $sources = $this->getComponentDatasources(); $usable = array(); foreach ($sources as $source) { $application_class = $source->getDatasourceApplicationClass(); if ($application_class) { $result = id(new PhabricatorApplicationQuery()) ->setViewer($this->getViewer()) ->withClasses(array($application_class)) ->execute(); if (!$result) { continue; } } $usable[] = $source; } return $usable; } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php index d08ea87685..ac29954fff 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -1,81 +1,91 @@ limit = $limit; return $this; } public function getLimit() { return $this->limit; } + public function setOffset($offset) { + $this->offset = $offset; + return $this; + } + + public function getOffset() { + return $this->offset; + } + public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setRawQuery($raw_query) { $this->rawQuery = $raw_query; return $this; } public function getRawQuery() { return $this->rawQuery; } public function setQuery($query) { $this->query = $query; return $this; } public function getQuery() { return $this->query; } public function setParameters(array $params) { $this->parameters = $params; return $this; } public function getParameters() { return $this->parameters; } public function getParameter($name, $default = null) { return idx($this->parameters, $name, $default); } public function getDatasourceURI() { $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/'); $uri->setQueryParams($this->parameters); return (string)$uri; } abstract public function getPlaceholderText(); abstract public function getDatasourceApplicationClass(); abstract public function loadResults(); public static function tokenizeString($string) { $string = phutil_utf8_strtolower($string); $string = trim($string); if (!strlen($string)) { return array(); } $tokens = preg_split('/\s+|[-\[\]]/', $string); return array_unique($tokens); } } diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php index 37aa314ce9..4820a23e8c 100644 --- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php +++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php @@ -1,120 +1,124 @@ icon = $icon; return $this; } public function setName($name) { $this->name = $name; return $this; } public function setURI($uri) { $this->uri = $uri; return $this; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function setPriorityString($priority_string) { $this->priorityString = $priority_string; return $this; } public function setDisplayName($display_name) { $this->displayName = $display_name; return $this; } public function setDisplayType($display_type) { $this->displayType = $display_type; return $this; } public function setImageURI($image_uri) { $this->imageURI = $image_uri; return $this; } public function setPriorityType($priority_type) { $this->priorityType = $priority_type; return $this; } public function setImageSprite($image_sprite) { $this->imageSprite = $image_sprite; return $this; } public function setClosed($closed) { $this->closed = $closed; return $this; } + public function getName() { + return $this->name; + } + public function getWireFormat() { $data = array( $this->name, $this->uri ? (string)$this->uri : null, $this->phid, $this->priorityString, $this->displayName, $this->displayType, $this->imageURI ? (string)$this->imageURI : null, $this->priorityType, ($this->icon === null) ? $this->getDefaultIcon() : $this->icon, $this->closed, $this->imageSprite ? (string)$this->imageSprite : null, ); while (end($data) === null) { array_pop($data); } return $data; } /** * If the datasource did not specify an icon explicitly, try to select a * default based on PHID type. */ private function getDefaultIcon() { static $icon_map; if ($icon_map === null) { $types = PhabricatorPHIDType::getAllTypes(); $map = array(); foreach ($types as $type) { $icon = $type->getTypeIcon(); if ($icon !== null) { $map[$type->getTypeConstant()] = "{$icon} bluegrey"; } } $icon_map = $map; } $phid_type = phid_get_type($this->phid); if (isset($icon_map[$phid_type])) { return $icon_map[$phid_type]; } return null; } }