Page MenuHomePhabricator

D12441.id.diff
No OneTemporary

D12441.id.diff

diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
@@ -11,8 +11,18 @@
$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);
@@ -46,7 +56,6 @@
}
$limit = 10;
- $offset = $request->getInt('offset');
if (($offset + $limit) >= $hard_limit) {
// Offset-based paging is intrinsically slow; hard-cap how far we're
@@ -62,13 +71,43 @@
$results = $composite->loadResults();
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) {
$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('offset', $offset + $limit)
+ ->setQueryParam('format', 'html');
$next_link = javelin_tag(
'a',
@@ -91,16 +130,44 @@
}
}
+ $exclude = $request->getStrList('exclude');
+ $exclude = array_fuse($exclude);
+
+ $select = array(
+ 'offset' => $offset,
+ 'q' => $query,
+ );
+
$items = array();
foreach ($results as $result) {
$token = PhabricatorTypeaheadTokenView::newForTypeaheadResult(
$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(
'div',
array(
- 'class' => 'grouped',
+ 'class' => 'typeahead-browse-item grouped',
),
- $token);
+ array(
+ $token,
+ $button,
+ ));
}
$markup = array(
@@ -108,7 +175,7 @@
$next_link,
);
- if ($request->isAjax()) {
+ if ($format == 'html') {
$content = array(
'markup' => hsprintf('%s', $markup),
);
diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
--- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
+++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
@@ -73,6 +73,16 @@
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 getDatasourceApplicationClass();
abstract public function loadResults();
diff --git a/src/view/control/AphrontTokenizerTemplateView.php b/src/view/control/AphrontTokenizerTemplateView.php
--- a/src/view/control/AphrontTokenizerTemplateView.php
+++ b/src/view/control/AphrontTokenizerTemplateView.php
@@ -5,6 +5,12 @@
private $value;
private $name;
private $id;
+ private $browseURI;
+
+ public function setBrowseURI($browse_uri) {
+ $this->browseURI = $browse_uri;
+ return $this;
+ }
public function setID($id) {
$this->id = $id;
@@ -61,13 +67,57 @@
$content[] = $input;
$content[] = phutil_tag('div', array('style' => 'clear: both;'), '');
- return phutil_tag(
+ $container = phutil_tag(
'div',
array(
'id' => $id,
'class' => 'jx-tokenizer-container',
),
$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) {
diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php
--- a/src/view/form/control/AphrontFormTokenizerControl.php
+++ b/src/view/form/control/AphrontFormTokenizerControl.php
@@ -70,8 +70,18 @@
}
$datasource_uri = null;
- if ($this->datasource) {
- $datasource_uri = $this->datasource->getDatasourceURI();
+ $browse_uri = null;
+
+ $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) {
@@ -83,6 +93,7 @@
'limit' => $this->limit,
'username' => $username,
'placeholder' => $placeholder,
+ 'browseURI' => $browse_uri,
));
}
diff --git a/webroot/rsrc/css/aphront/tokenizer.css b/webroot/rsrc/css/aphront/tokenizer.css
--- a/webroot/rsrc/css/aphront/tokenizer.css
+++ b/webroot/rsrc/css/aphront/tokenizer.css
@@ -104,3 +104,17 @@
.tokenizer-closed {
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;
+}
diff --git a/webroot/rsrc/css/aphront/typeahead-browse.css b/webroot/rsrc/css/aphront/typeahead-browse.css
--- a/webroot/rsrc/css/aphront/typeahead-browse.css
+++ b/webroot/rsrc/css/aphront/typeahead-browse.css
@@ -45,3 +45,16 @@
margin: 0;
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;
+}
diff --git a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
--- a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
+++ b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
@@ -45,12 +45,14 @@
properties : {
limit : null,
- renderTokenCallback : null
+ renderTokenCallback : null,
+ browseURI: null
},
members : {
_containerNode : null,
_root : null,
+ _frame: null,
_focus : null,
_orig : null,
_typeahead : null,
@@ -76,6 +78,20 @@
this._tokens = [];
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);
this._focus = focus;
@@ -429,6 +445,24 @@
false);
this._focus.value = '';
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();
}
}
diff --git a/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js b/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js
--- a/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js
+++ b/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js
@@ -31,7 +31,7 @@
}
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) {
if (value != input.value) {
// The user typed some more stuff while the request was in flight,
diff --git a/webroot/rsrc/js/core/Prefab.js b/webroot/rsrc/js/core/Prefab.js
--- a/webroot/rsrc/js/core/Prefab.js
+++ b/webroot/rsrc/js/core/Prefab.js
@@ -194,6 +194,10 @@
tokenizer.setInitialValue(config.value);
}
+ if (config.browseURI) {
+ tokenizer.setBrowseURI(config.browseURI);
+ }
+
JX.Stratcom.addData(root, {'tokenizer' : tokenizer});
return {

File Metadata

Mime Type
text/plain
Expires
Mon, May 13, 12:44 PM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6273703
Default Alt Text
D12441.id.diff (11 KB)

Event Timeline