Implement a rough browse view for tokenizers

Summary: Ref T5750. This adds a basic browse view. Design is a bit rough, see T7841 for some screenshots.

Test Plan: Used browse view to add tokens to tokenizers.

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5750

Differential Revision: https://secure.phabricator.com/D12441
This commit is contained in:
epriestley
2015-04-16 13:37:12 -07:00
parent ba48e05964
commit a641601407
9 changed files with 214 additions and 11 deletions

View File

@@ -11,8 +11,18 @@ final class PhabricatorTypeaheadModularDatasourceController
$request = $this->getRequest(); $request = $this->getRequest();
$viewer = $request->getUser(); $viewer = $request->getUser();
$query = $request->getStr('q'); $query = $request->getStr('q');
$offset = $request->getInt('offset');
$select_phid = null;
$is_browse = ($request->getURIData('action') == 'browse'); $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. // Default this to the query string to make debugging a little bit easier.
$raw_query = nonempty($request->getStr('raw'), $query); $raw_query = nonempty($request->getStr('raw'), $query);
@@ -46,7 +56,6 @@ final class PhabricatorTypeaheadModularDatasourceController
} }
$limit = 10; $limit = 10;
$offset = $request->getInt('offset');
if (($offset + $limit) >= $hard_limit) { if (($offset + $limit) >= $hard_limit) {
// Offset-based paging is intrinsically slow; hard-cap how far we're // Offset-based paging is intrinsically slow; hard-cap how far we're
@@ -62,13 +71,43 @@ final class PhabricatorTypeaheadModularDatasourceController
$results = $composite->loadResults(); $results = $composite->loadResults();
if ($is_browse) { if ($is_browse) {
$next_link = null; // 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) {
$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) { if (count($results) > $limit) {
$results = array_slice($results, 0, $limit, $preserve_keys = true); $results = array_slice($results, 0, $limit, $preserve_keys = true);
if (($offset + (2 * $limit)) < $hard_limit) { if (($offset + (2 * $limit)) < $hard_limit) {
$next_uri = id(new PhutilURI($request->getRequestURI())) $next_uri = id(new PhutilURI($request->getRequestURI()))
->setQueryParam('offset', $offset + $limit); ->setQueryParam('offset', $offset + $limit)
->setQueryParam('format', 'html');
$next_link = javelin_tag( $next_link = javelin_tag(
'a', 'a',
@@ -91,16 +130,44 @@ final class PhabricatorTypeaheadModularDatasourceController
} }
} }
$exclude = $request->getStrList('exclude');
$exclude = array_fuse($exclude);
$select = array(
'offset' => $offset,
'q' => $query,
);
$items = array(); $items = array();
foreach ($results as $result) { foreach ($results as $result) {
$token = PhabricatorTypeaheadTokenView::newForTypeaheadResult( $token = PhabricatorTypeaheadTokenView::newForTypeaheadResult(
$result); $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'));
$items[] = phutil_tag( $items[] = phutil_tag(
'div', 'div',
array( array(
'class' => 'grouped', 'class' => 'typeahead-browse-item grouped',
), ),
$token); array(
$token,
$button,
));
} }
$markup = array( $markup = array(
@@ -108,7 +175,7 @@ final class PhabricatorTypeaheadModularDatasourceController
$next_link, $next_link,
); );
if ($request->isAjax()) { if ($format == 'html') {
$content = array( $content = array(
'markup' => hsprintf('%s', $markup), 'markup' => hsprintf('%s', $markup),
); );

View File

@@ -73,6 +73,16 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject {
return (string)$uri; return (string)$uri;
} }
public function getBrowseURI() {
if (!$this->isBrowsable()) {
return null;
}
$uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/');
$uri->setQueryParams($this->parameters);
return (string)$uri;
}
abstract public function getPlaceholderText(); abstract public function getPlaceholderText();
abstract public function getDatasourceApplicationClass(); abstract public function getDatasourceApplicationClass();
abstract public function loadResults(); abstract public function loadResults();

View File

@@ -5,6 +5,12 @@ final class AphrontTokenizerTemplateView extends AphrontView {
private $value; private $value;
private $name; private $name;
private $id; private $id;
private $browseURI;
public function setBrowseURI($browse_uri) {
$this->browseURI = $browse_uri;
return $this;
}
public function setID($id) { public function setID($id) {
$this->id = $id; $this->id = $id;
@@ -61,13 +67,57 @@ final class AphrontTokenizerTemplateView extends AphrontView {
$content[] = $input; $content[] = $input;
$content[] = phutil_tag('div', array('style' => 'clear: both;'), ''); $content[] = phutil_tag('div', array('style' => 'clear: both;'), '');
return phutil_tag( $container = phutil_tag(
'div', 'div',
array( array(
'id' => $id, 'id' => $id,
'class' => 'jx-tokenizer-container', 'class' => 'jx-tokenizer-container',
), ),
$content); $content);
$browse = null;
if ($this->browseURI) {
$icon = id(new PHUIIconView())
->setIconFont('fa-list-ul');
// TODO: This thing is ugly and the ugliness is not intentional.
// We have to give it text or PHUIButtonView collapses. It should likely
// just be an icon and look more integrated into the input.
$browse = id(new PHUIButtonView())
->setTag('a')
->setIcon($icon)
->addSigil('tokenizer-browse')
->setColor(PHUIButtonView::GREY)
->setSize(PHUIButtonView::SMALL)
->setText(pht('Browse...'));
}
$frame = javelin_tag(
'table',
array(
'class' => 'jx-tokenizer-frame',
'sigil' => 'tokenizer-frame',
),
phutil_tag(
'tr',
array(
),
array(
phutil_tag(
'td',
array(
'class' => 'jx-tokenizer-frame-input',
),
$container),
phutil_tag(
'td',
array(
'class' => 'jx-tokenizer-frame-browse',
),
$browse),
)));
return $frame;
} }
private function renderToken($key, $value, $icon) { private function renderToken($key, $value, $icon) {

View File

@@ -70,8 +70,18 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
} }
$datasource_uri = null; $datasource_uri = null;
if ($this->datasource) { $browse_uri = null;
$datasource_uri = $this->datasource->getDatasourceURI();
$datasource = $this->datasource;
if ($datasource) {
$datasource->setViewer($this->getUser());
$datasource_uri = $datasource->getDatasourceURI();
$browse_uri = $datasource->getBrowseURI();
if ($browse_uri) {
$template->setBrowseURI($browse_uri);
}
} }
if (!$this->disableBehavior) { if (!$this->disableBehavior) {
@@ -83,6 +93,7 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
'limit' => $this->limit, 'limit' => $this->limit,
'username' => $username, 'username' => $username,
'placeholder' => $placeholder, 'placeholder' => $placeholder,
'browseURI' => $browse_uri,
)); ));
} }

View File

@@ -104,3 +104,17 @@ a.jx-tokenizer-token:hover {
.tokenizer-closed { .tokenizer-closed {
margin-top: 2px; margin-top: 2px;
} }
.jx-tokenizer-frame {
width: 100%;
}
.jx-tokenizer-frame-input {
width: 100%;
}
.jx-tokenizer-frame-browse {
width: 100px;
vertical-align: middle;
padding: 0 0 0 4px;
}

View File

@@ -45,3 +45,16 @@ input.typeahead-browse-input {
margin: 0; margin: 0;
width: 100%; width: 100%;
} }
.typeahead-browse-item {
padding: 2px 0;
}
.typeahead-browse-item + .typeahead-browse-item {
border-top: 1px solid {$thinblueborder};
}
.typeahead-browse-item button {
float: right;
margin: 2px 4px;
}

View File

@@ -45,12 +45,14 @@ JX.install('Tokenizer', {
properties : { properties : {
limit : null, limit : null,
renderTokenCallback : null renderTokenCallback : null,
browseURI: null
}, },
members : { members : {
_containerNode : null, _containerNode : null,
_root : null, _root : null,
_frame: null,
_focus : null, _focus : null,
_orig : null, _orig : null,
_typeahead : null, _typeahead : null,
@@ -76,6 +78,20 @@ JX.install('Tokenizer', {
this._tokens = []; this._tokens = [];
this._tokenMap = {}; this._tokenMap = {};
try {
this._frame = JX.DOM.findAbove(this._orig, 'table', 'tokenizer-frame');
} catch (e) {
// Ignore, this tokenizer doesn't have a frame.
}
if (this._frame) {
JX.DOM.listen(
this._frame,
'click',
'tokenizer-browse',
JX.bind(this, this._onbrowse));
}
var focus = this.buildInput(this._orig.value); var focus = this.buildInput(this._orig.value);
this._focus = focus; this._focus = focus;
@@ -429,6 +445,24 @@ JX.install('Tokenizer', {
false); false);
this._focus.value = ''; this._focus.value = '';
this._redraw(); this._redraw();
},
_onbrowse: function(e) {
e.kill();
var uri = this.getBrowseURI();
if (!uri) {
return;
}
new JX.Workflow(uri, {exclude: JX.keys(this.getTokens()).join(',')})
.setHandler(
JX.bind(this, function(r) {
this._typeahead.getDatasource().addResult(r.token);
this.addToken(r.key);
this.focus();
}))
.start();
} }
} }

View File

@@ -31,7 +31,7 @@ JX.behavior('typeahead-search', function(config) {
} }
JX.DOM.alterClass(frame, 'loading', true); JX.DOM.alterClass(frame, 'loading', true);
new JX.Workflow(config.uri, {q: value}) new JX.Workflow(config.uri, {q: value, format: 'html'})
.setHandler(function(r) { .setHandler(function(r) {
if (value != input.value) { if (value != input.value) {
// The user typed some more stuff while the request was in flight, // The user typed some more stuff while the request was in flight,

View File

@@ -194,6 +194,10 @@ JX.install('Prefab', {
tokenizer.setInitialValue(config.value); tokenizer.setInitialValue(config.value);
} }
if (config.browseURI) {
tokenizer.setBrowseURI(config.browseURI);
}
JX.Stratcom.addData(root, {'tokenizer' : tokenizer}); JX.Stratcom.addData(root, {'tokenizer' : tokenizer});
return { return {