public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); // 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. $this->class = nonempty($request->getStr('class'), $this->class); $sources = id(new PhutilSymbolLoader())->setAncestorClass('PhabricatorTypeaheadDatasource')->loadObjects(); if (isset($sources[$this->class])) { $source = $sources[$this->class]; $source->setParameters($request->getRequestData()); $composite = new PhabricatorTypeaheadRuntimeCompositeDatasource(); $composite->addDatasource($source); $composite->setViewer($viewer)->setQuery($query)->setRawQuery($raw_query); $results = $composite->loadResults(); } 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. $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($this->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)', $this->class))->appendChild($table); return $this->buildApplicationPage(array($form_box, $result_box), array('title' => pht('Typeahead Results'), 'device' => false)); }
public function handleRequest(AphrontRequest $request) { $request = $this->getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); $offset = $request->getInt('offset'); $select_phid = null; $is_browse = $request->getURIData('action') == 'browse'; $select = $request->getStr('select'); if ($select) { $select = phutil_json_decode($select); $query = idx($select, 'q'); $offset = idx($select, 'offset'); $select_phid = idx($select, 'phid'); } // 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 PhutilClassMapQuery())->setAncestorClass('PhabricatorTypeaheadDatasource')->execute(); if (isset($sources[$class])) { $source = $sources[$class]; $source->setParameters($request->getRequestData()); $source->setViewer($viewer); // 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); $hard_limit = 1000; $limit = 100; $composite->setViewer($viewer)->setQuery($query)->setRawQuery($raw_query)->setLimit($limit + 1); if ($is_browse) { if (!$composite->isBrowsable()) { return new Aphront404Response(); } if ($offset + $limit >= $hard_limit) { // Offset-based paging is intrinsically slow; hard-cap how far we're // willing to go with it. return new Aphront404Response(); } $composite->setOffset($offset)->setIsBrowse(true); } $results = $composite->loadResults(); if ($is_browse) { // If this is a request for a specific token after the user clicks // "Select", return the token in wire format so it can be added to // the tokenizer. if ($select_phid !== null) { $map = mpull($results, null, 'getPHID'); $token = idx($map, $select_phid); if (!$token) { return new Aphront404Response(); } $payload = array('key' => $token->getPHID(), 'token' => $token->getWireFormat()); return id(new AphrontAjaxResponse())->setContent($payload); } $format = $request->getStr('format'); switch ($format) { case 'html': case 'dialog': // These are the acceptable response formats. break; default: // Return a dialog if format information is missing or invalid. $format = 'dialog'; break; } $next_link = null; if (count($results) > $limit) { $results = array_slice($results, 0, $limit, $preserve_keys = true); if ($offset + 2 * $limit < $hard_limit) { $next_uri = id(new PhutilURI($request->getRequestURI()))->setQueryParam('offset', $offset + $limit)->setQueryParam('q', $query)->setQueryParam('raw', $raw_query)->setQueryParam('format', 'html'); $next_link = javelin_tag('a', array('href' => $next_uri, 'class' => 'typeahead-browse-more', 'sigil' => 'typeahead-browse-more', 'mustcapture' => true), pht('More Results')); } else { // If the user has paged through more than 1K results, don't // offer to page any further. $next_link = javelin_tag('div', array('class' => 'typeahead-browse-hard-limit'), pht('You reach the edge of the abyss.')); } } $exclude = $request->getStrList('exclude'); $exclude = array_fuse($exclude); $select = array('offset' => $offset, 'q' => $query); $items = array(); foreach ($results as $result) { // Disable already-selected tokens. $disabled = isset($exclude[$result->getPHID()]); $value = $select + array('phid' => $result->getPHID()); $value = json_encode($value); $button = phutil_tag('button', array('class' => 'small grey', 'name' => 'select', 'value' => $value, 'disabled' => $disabled ? 'disabled' : null), pht('Select')); $information = $this->renderBrowseResult($result, $button); $items[] = phutil_tag('div', array('class' => 'typeahead-browse-item grouped'), $information); } $markup = array($items, $next_link); if ($format == 'html') { $content = array('markup' => hsprintf('%s', $markup)); return id(new AphrontAjaxResponse())->setContent($content); } $this->requireResource('typeahead-browse-css'); $this->initBehavior('typeahead-browse'); $input_id = celerity_generate_unique_node_id(); $frame_id = celerity_generate_unique_node_id(); $config = array('inputID' => $input_id, 'frameID' => $frame_id, 'uri' => (string) $request->getRequestURI()); $this->initBehavior('typeahead-search', $config); $search = javelin_tag('input', array('type' => 'text', 'id' => $input_id, 'class' => 'typeahead-browse-input', 'autocomplete' => 'off', 'placeholder' => $source->getPlaceholderText())); $frame = phutil_tag('div', array('class' => 'typeahead-browse-frame', 'id' => $frame_id), $markup); $browser = array(phutil_tag('div', array('class' => 'typeahead-browse-header'), $search), $frame); $function_help = null; if ($source->getAllDatasourceFunctions()) { $reference_uri = '/typeahead/help/' . get_class($source) . '/'; $reference_link = phutil_tag('a', array('href' => $reference_uri, 'target' => '_blank'), pht('Reference: Advanced Functions')); $function_help = array(id(new PHUIIconView())->setIcon('fa-book'), ' ', $reference_link); } return $this->newDialog()->setWidth(AphrontDialogView::WIDTH_FORM)->setRenderDialogAsDiv(true)->setTitle($source->getBrowseTitle())->appendChild($browser)->setResizeX(true)->setResizeY($frame_id)->addFooter($function_help)->addCancelButton('/', pht('Close')); } } else { if ($is_browse) { return new Aphront404Response(); } else { $results = array(); } } $content = mpull($results, 'getWireFormat'); $content = array_values($content); 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 or generic sources. 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'))->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)->setForm($form); // Make "\n" delimiters more visible. foreach ($content as $key => $row) { $content[$key][0] = str_replace("\n", '<\\n>', $row[0]); } $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'), pht('Color'), pht('Type'), pht('Unique'), pht('Auto'), pht('Phase'))); $result_box = id(new PHUIObjectBoxView())->setHeaderText(pht('Token Results (%s)', $class))->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)->appendChild($table); $title = pht('Typeahead Results'); $header = id(new PHUIHeaderView())->setHeader($title); $view = id(new PHUITwoColumnView())->setHeader($header)->setFooter(array($form_box, $result_box)); return $this->newPage()->setTitle($title)->appendChild($view); }