1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-30 02:32:42 +01:00

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();
$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 @@ final class PhabricatorTypeaheadModularDatasourceController
}
$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 @@ final class PhabricatorTypeaheadModularDatasourceController
$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 @@ final class PhabricatorTypeaheadModularDatasourceController
}
}
$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 @@ final class PhabricatorTypeaheadModularDatasourceController
$next_link,
);
if ($request->isAjax()) {
if ($format == 'html') {
$content = array(
'markup' => hsprintf('%s', $markup),
);

View file

@ -73,6 +73,16 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject {
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();

View file

@ -5,6 +5,12 @@ final class AphrontTokenizerTemplateView extends AphrontView {
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 @@ final class AphrontTokenizerTemplateView extends AphrontView {
$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) {

View file

@ -70,8 +70,18 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
}
$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 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
'limit' => $this->limit,
'username' => $username,
'placeholder' => $placeholder,
'browseURI' => $browse_uri,
));
}

View file

@ -104,3 +104,17 @@ a.jx-tokenizer-token:hover {
.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;
}

View file

@ -45,3 +45,16 @@ input.typeahead-browse-input {
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;
}

View file

@ -45,12 +45,14 @@ JX.install('Tokenizer', {
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 @@ JX.install('Tokenizer', {
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 @@ JX.install('Tokenizer', {
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();
}
}

View file

@ -31,7 +31,7 @@ JX.behavior('typeahead-search', function(config) {
}
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,

View file

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