@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+214 -11
+73 -6
src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
··· 11 11 $request = $this->getRequest(); 12 12 $viewer = $request->getUser(); 13 13 $query = $request->getStr('q'); 14 + $offset = $request->getInt('offset'); 15 + $select_phid = null; 14 16 $is_browse = ($request->getURIData('action') == 'browse'); 17 + 18 + $select = $request->getStr('select'); 19 + if ($select) { 20 + $select = phutil_json_decode($select); 21 + $query = idx($select, 'q'); 22 + $offset = idx($select, 'offset'); 23 + $select_phid = idx($select, 'phid'); 24 + } 15 25 16 26 // Default this to the query string to make debugging a little bit easier. 17 27 $raw_query = nonempty($request->getStr('raw'), $query); ··· 46 56 } 47 57 48 58 $limit = 10; 49 - $offset = $request->getInt('offset'); 50 59 51 60 if (($offset + $limit) >= $hard_limit) { 52 61 // Offset-based paging is intrinsically slow; hard-cap how far we're ··· 62 71 $results = $composite->loadResults(); 63 72 64 73 if ($is_browse) { 65 - $next_link = null; 74 + // If this is a request for a specific token after the user clicks 75 + // "Select", return the token in wire format so it can be added to 76 + // the tokenizer. 77 + if ($select_phid) { 78 + $map = mpull($results, null, 'getPHID'); 79 + $token = idx($map, $select_phid); 80 + if (!$token) { 81 + return new Aphront404Response(); 82 + } 83 + 84 + $payload = array( 85 + 'key' => $token->getPHID(), 86 + 'token' => $token->getWireFormat(), 87 + ); 88 + 89 + return id(new AphrontAjaxResponse())->setContent($payload); 90 + } 91 + 92 + $format = $request->getStr('format'); 93 + switch ($format) { 94 + case 'html': 95 + case 'dialog': 96 + // These are the acceptable response formats. 97 + break; 98 + default: 99 + // Return a dialog if format information is missing or invalid. 100 + $format = 'dialog'; 101 + break; 102 + } 66 103 104 + $next_link = null; 67 105 if (count($results) > $limit) { 68 106 $results = array_slice($results, 0, $limit, $preserve_keys = true); 69 107 if (($offset + (2 * $limit)) < $hard_limit) { 70 108 $next_uri = id(new PhutilURI($request->getRequestURI())) 71 - ->setQueryParam('offset', $offset + $limit); 109 + ->setQueryParam('offset', $offset + $limit) 110 + ->setQueryParam('format', 'html'); 72 111 73 112 $next_link = javelin_tag( 74 113 'a', ··· 91 130 } 92 131 } 93 132 133 + $exclude = $request->getStrList('exclude'); 134 + $exclude = array_fuse($exclude); 135 + 136 + $select = array( 137 + 'offset' => $offset, 138 + 'q' => $query, 139 + ); 140 + 94 141 $items = array(); 95 142 foreach ($results as $result) { 96 143 $token = PhabricatorTypeaheadTokenView::newForTypeaheadResult( 97 144 $result); 145 + 146 + // Disable already-selected tokens. 147 + $disabled = isset($exclude[$result->getPHID()]); 148 + 149 + $value = $select + array('phid' => $result->getPHID()); 150 + $value = json_encode($value); 151 + 152 + $button = phutil_tag( 153 + 'button', 154 + array( 155 + 'class' => 'small grey', 156 + 'name' => 'select', 157 + 'value' => $value, 158 + 'disabled' => $disabled ? 'disabled' : null, 159 + ), 160 + pht('Select')); 161 + 98 162 $items[] = phutil_tag( 99 163 'div', 100 164 array( 101 - 'class' => 'grouped', 165 + 'class' => 'typeahead-browse-item grouped', 102 166 ), 103 - $token); 167 + array( 168 + $token, 169 + $button, 170 + )); 104 171 } 105 172 106 173 $markup = array( ··· 108 175 $next_link, 109 176 ); 110 177 111 - if ($request->isAjax()) { 178 + if ($format == 'html') { 112 179 $content = array( 113 180 'markup' => hsprintf('%s', $markup), 114 181 );
+10
src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
··· 73 73 return (string)$uri; 74 74 } 75 75 76 + public function getBrowseURI() { 77 + if (!$this->isBrowsable()) { 78 + return null; 79 + } 80 + 81 + $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/'); 82 + $uri->setQueryParams($this->parameters); 83 + return (string)$uri; 84 + } 85 + 76 86 abstract public function getPlaceholderText(); 77 87 abstract public function getDatasourceApplicationClass(); 78 88 abstract public function loadResults();
+51 -1
src/view/control/AphrontTokenizerTemplateView.php
··· 5 5 private $value; 6 6 private $name; 7 7 private $id; 8 + private $browseURI; 9 + 10 + public function setBrowseURI($browse_uri) { 11 + $this->browseURI = $browse_uri; 12 + return $this; 13 + } 8 14 9 15 public function setID($id) { 10 16 $this->id = $id; ··· 61 67 $content[] = $input; 62 68 $content[] = phutil_tag('div', array('style' => 'clear: both;'), ''); 63 69 64 - return phutil_tag( 70 + $container = phutil_tag( 65 71 'div', 66 72 array( 67 73 'id' => $id, 68 74 'class' => 'jx-tokenizer-container', 69 75 ), 70 76 $content); 77 + 78 + $browse = null; 79 + if ($this->browseURI) { 80 + $icon = id(new PHUIIconView()) 81 + ->setIconFont('fa-list-ul'); 82 + 83 + // TODO: This thing is ugly and the ugliness is not intentional. 84 + // We have to give it text or PHUIButtonView collapses. It should likely 85 + // just be an icon and look more integrated into the input. 86 + $browse = id(new PHUIButtonView()) 87 + ->setTag('a') 88 + ->setIcon($icon) 89 + ->addSigil('tokenizer-browse') 90 + ->setColor(PHUIButtonView::GREY) 91 + ->setSize(PHUIButtonView::SMALL) 92 + ->setText(pht('Browse...')); 93 + } 94 + 95 + $frame = javelin_tag( 96 + 'table', 97 + array( 98 + 'class' => 'jx-tokenizer-frame', 99 + 'sigil' => 'tokenizer-frame', 100 + ), 101 + phutil_tag( 102 + 'tr', 103 + array( 104 + ), 105 + array( 106 + phutil_tag( 107 + 'td', 108 + array( 109 + 'class' => 'jx-tokenizer-frame-input', 110 + ), 111 + $container), 112 + phutil_tag( 113 + 'td', 114 + array( 115 + 'class' => 'jx-tokenizer-frame-browse', 116 + ), 117 + $browse), 118 + ))); 119 + 120 + return $frame; 71 121 } 72 122 73 123 private function renderToken($key, $value, $icon) {
+13 -2
src/view/form/control/AphrontFormTokenizerControl.php
··· 70 70 } 71 71 72 72 $datasource_uri = null; 73 - if ($this->datasource) { 74 - $datasource_uri = $this->datasource->getDatasourceURI(); 73 + $browse_uri = null; 74 + 75 + $datasource = $this->datasource; 76 + if ($datasource) { 77 + $datasource->setViewer($this->getUser()); 78 + 79 + $datasource_uri = $datasource->getDatasourceURI(); 80 + 81 + $browse_uri = $datasource->getBrowseURI(); 82 + if ($browse_uri) { 83 + $template->setBrowseURI($browse_uri); 84 + } 75 85 } 76 86 77 87 if (!$this->disableBehavior) { ··· 83 93 'limit' => $this->limit, 84 94 'username' => $username, 85 95 'placeholder' => $placeholder, 96 + 'browseURI' => $browse_uri, 86 97 )); 87 98 } 88 99
+14
webroot/rsrc/css/aphront/tokenizer.css
··· 104 104 .tokenizer-closed { 105 105 margin-top: 2px; 106 106 } 107 + 108 + .jx-tokenizer-frame { 109 + width: 100%; 110 + } 111 + 112 + .jx-tokenizer-frame-input { 113 + width: 100%; 114 + } 115 + 116 + .jx-tokenizer-frame-browse { 117 + width: 100px; 118 + vertical-align: middle; 119 + padding: 0 0 0 4px; 120 + }
+13
webroot/rsrc/css/aphront/typeahead-browse.css
··· 45 45 margin: 0; 46 46 width: 100%; 47 47 } 48 + 49 + .typeahead-browse-item { 50 + padding: 2px 0; 51 + } 52 + 53 + .typeahead-browse-item + .typeahead-browse-item { 54 + border-top: 1px solid {$thinblueborder}; 55 + } 56 + 57 + .typeahead-browse-item button { 58 + float: right; 59 + margin: 2px 4px; 60 + }
+35 -1
webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
··· 45 45 46 46 properties : { 47 47 limit : null, 48 - renderTokenCallback : null 48 + renderTokenCallback : null, 49 + browseURI: null 49 50 }, 50 51 51 52 members : { 52 53 _containerNode : null, 53 54 _root : null, 55 + _frame: null, 54 56 _focus : null, 55 57 _orig : null, 56 58 _typeahead : null, ··· 75 77 this._orig = JX.DOM.find(this._containerNode, 'input', 'tokenizer-input'); 76 78 this._tokens = []; 77 79 this._tokenMap = {}; 80 + 81 + try { 82 + this._frame = JX.DOM.findAbove(this._orig, 'table', 'tokenizer-frame'); 83 + } catch (e) { 84 + // Ignore, this tokenizer doesn't have a frame. 85 + } 86 + 87 + if (this._frame) { 88 + JX.DOM.listen( 89 + this._frame, 90 + 'click', 91 + 'tokenizer-browse', 92 + JX.bind(this, this._onbrowse)); 93 + } 78 94 79 95 var focus = this.buildInput(this._orig.value); 80 96 this._focus = focus; ··· 429 445 false); 430 446 this._focus.value = ''; 431 447 this._redraw(); 448 + }, 449 + 450 + _onbrowse: function(e) { 451 + e.kill(); 452 + 453 + var uri = this.getBrowseURI(); 454 + if (!uri) { 455 + return; 456 + } 457 + 458 + new JX.Workflow(uri, {exclude: JX.keys(this.getTokens()).join(',')}) 459 + .setHandler( 460 + JX.bind(this, function(r) { 461 + this._typeahead.getDatasource().addResult(r.token); 462 + this.addToken(r.key); 463 + this.focus(); 464 + })) 465 + .start(); 432 466 } 433 467 434 468 }
+1 -1
webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js
··· 31 31 } 32 32 33 33 JX.DOM.alterClass(frame, 'loading', true); 34 - new JX.Workflow(config.uri, {q: value}) 34 + new JX.Workflow(config.uri, {q: value, format: 'html'}) 35 35 .setHandler(function(r) { 36 36 if (value != input.value) { 37 37 // The user typed some more stuff while the request was in flight,
+4
webroot/rsrc/js/core/Prefab.js
··· 194 194 tokenizer.setInitialValue(config.value); 195 195 } 196 196 197 + if (config.browseURI) { 198 + tokenizer.setBrowseURI(config.browseURI); 199 + } 200 + 197 201 JX.Stratcom.addData(root, {'tokenizer' : tokenizer}); 198 202 199 203 return {