mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 16:52:41 +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:
parent
ba48e05964
commit
a641601407
9 changed files with 214 additions and 11 deletions
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue