diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 22a2fb68f6..0466ad9950 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -1,78 +1,79 @@ getViewer(); $raw_query = $this->getRawQuery(); // Allow users to type "#qa" or "qa" to find "Quality Assurance". $raw_query = ltrim($raw_query, '#'); $tokens = self::tokenizeString($raw_query); $query = id(new PhabricatorProjectQuery()) ->needImages(true) ->needSlugs(true); if ($tokens) { $query->withNameTokens($tokens); } $projs = $this->executeQuery($query); $projs = mpull($projs, null, 'getPHID'); $must_have_cols = $this->getParameter('mustHaveColumns', false); if ($must_have_cols) { $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array_keys($projs)) ->execute(); $has_cols = mgroup($columns, 'getProjectPHID'); } else { $has_cols = array_fill_keys(array_keys($projs), true); } $results = array(); foreach ($projs as $proj) { if (!isset($has_cols[$proj->getPHID()])) { continue; } $closed = null; if ($proj->isArchived()) { $closed = pht('Archived'); } $all_strings = mpull($proj->getSlugs(), 'getSlug'); $all_strings[] = $proj->getName(); $all_strings = implode(' ', $all_strings); $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($all_strings) ->setDisplayName($proj->getName()) ->setDisplayType('Project') ->setURI('/tag/'.$proj->getPrimarySlug().'/') ->setPHID($proj->getPHID()) - ->setIcon($proj->getIcon().' '.$proj->getColor()) + ->setIcon($proj->getIcon()) + ->setColor($proj->getColor()) ->setPriorityType('proj') ->setClosed($closed); $proj_result->setImageURI($proj->getProfileImageURI()); $results[] = $proj_result; } return $results; } } diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php index f364b142e8..516617093f 100644 --- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php +++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php @@ -1,169 +1,180 @@ icon = $icon; return $this; } public function setName($name) { $this->name = $name; return $this; } public function setURI($uri) { $this->uri = $uri; return $this; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function setPriorityString($priority_string) { $this->priorityString = $priority_string; return $this; } public function setDisplayName($display_name) { $this->displayName = $display_name; return $this; } public function setDisplayType($display_type) { $this->displayType = $display_type; return $this; } public function setImageURI($image_uri) { $this->imageURI = $image_uri; return $this; } public function setPriorityType($priority_type) { $this->priorityType = $priority_type; return $this; } public function setImageSprite($image_sprite) { $this->imageSprite = $image_sprite; return $this; } public function setClosed($closed) { $this->closed = $closed; return $this; } public function getName() { return $this->name; } public function getDisplayName() { return coalesce($this->displayName, $this->getName()); } public function getIcon() { return nonempty($this->icon, $this->getDefaultIcon()); } public function getPHID() { return $this->phid; } public function setUnique($unique) { $this->unique = $unique; return $this; } public function setTokenType($type) { $this->tokenType = $type; return $this; } public function getTokenType() { if ($this->closed && !$this->tokenType) { return PhabricatorTypeaheadTokenView::TYPE_DISABLED; } return $this->tokenType; } + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + public function getSortKey() { // Put unique results (special parameter functions) ahead of other // results. if ($this->unique) { $prefix = 'A'; } else { $prefix = 'B'; } return $prefix.phutil_utf8_strtolower($this->getName()); } public function getWireFormat() { $data = array( $this->name, $this->uri ? (string)$this->uri : null, $this->phid, $this->priorityString, $this->displayName, $this->displayType, $this->imageURI ? (string)$this->imageURI : null, $this->priorityType, $this->getIcon(), $this->closed, $this->imageSprite ? (string)$this->imageSprite : null, + $this->color, $this->tokenType, $this->unique ? 1 : null, ); while (end($data) === null) { array_pop($data); } return $data; } /** * If the datasource did not specify an icon explicitly, try to select a * default based on PHID type. */ private function getDefaultIcon() { static $icon_map; if ($icon_map === null) { $types = PhabricatorPHIDType::getAllTypes(); $map = array(); foreach ($types as $type) { $icon = $type->getTypeIcon(); if ($icon !== null) { - $map[$type->getTypeConstant()] = "{$icon} bluegrey"; + $map[$type->getTypeConstant()] = $icon; } } $icon_map = $map; } $phid_type = phid_get_type($this->phid); if (isset($icon_map[$phid_type])) { return $icon_map[$phid_type]; } return null; } } diff --git a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php index 0322fe544a..425de130fb 100644 --- a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php +++ b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php @@ -1,148 +1,163 @@ setKey($result->getPHID()) ->setIcon($result->getIcon()) + ->setColor($result->getColor()) ->setValue($result->getDisplayName()) ->setTokenType($result->getTokenType()); } public static function newFromHandle( PhabricatorObjectHandle $handle) { $token = id(new PhabricatorTypeaheadTokenView()) ->setKey($handle->getPHID()) ->setValue($handle->getFullName()) - ->setIcon(rtrim($handle->getIcon().' '.$handle->getIconColor())); + ->setIcon($handle->getIcon()); if ($handle->isDisabled() || $handle->getStatus() == PhabricatorObjectHandleStatus::STATUS_CLOSED) { $token->setTokenType(self::TYPE_DISABLED); + } else { + $token->setColor($handle->getTagColor()); } return $token; } public function setKey($key) { $this->key = $key; return $this; } public function getKey() { return $this->key; } public function setTokenType($token_type) { $this->tokenType = $token_type; return $this; } public function getTokenType() { return $this->tokenType; } public function setInputName($input_name) { $this->inputName = $input_name; return $this; } public function getInputName() { return $this->inputName; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function getIcon() { return $this->icon; } + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + public function setValue($value) { $this->value = $value; return $this; } public function getValue() { return $this->value; } protected function getTagName() { return 'a'; } protected function getTagAttributes() { $classes = array(); $classes[] = 'jx-tokenizer-token'; switch ($this->getTokenType()) { case self::TYPE_FUNCTION: $classes[] = 'jx-tokenizer-token-function'; break; case self::TYPE_INVALID: $classes[] = 'jx-tokenizer-token-invalid'; break; case self::TYPE_DISABLED: $classes[] = 'jx-tokenizer-token-disabled'; break; case self::TYPE_OBJECT: default: break; } + $classes[] = $this->getColor(); + return array( 'class' => $classes, ); } protected function getTagContent() { $input_name = $this->getInputName(); if ($input_name) { $input_name .= '[]'; } $value = $this->getValue(); $icon = $this->getIcon(); if ($icon) { $value = array( phutil_tag( 'span', array( - 'class' => 'phui-icon-view phui-font-fa bluetext '.$icon, + 'class' => 'phui-icon-view phui-font-fa '.$icon, )), $value, ); } return array( $value, phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $input_name, 'value' => $this->getKey(), )), phutil_tag('span', array('class' => 'jx-tokenizer-x-placeholder'), ''), ); } } diff --git a/src/applications/uiexample/examples/PHUITypeaheadExample.php b/src/applications/uiexample/examples/PHUITypeaheadExample.php index 1c039c380d..810a6cc15e 100644 --- a/src/applications/uiexample/examples/PHUITypeaheadExample.php +++ b/src/applications/uiexample/examples/PHUITypeaheadExample.php @@ -1,57 +1,58 @@ setValue(pht('Normal Object')) ->setIcon('fa-user'); $token_list[] = id(new PhabricatorTypeaheadTokenView()) ->setValue(pht('Disabled Object')) ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_DISABLED) ->setIcon('fa-user'); $token_list[] = id(new PhabricatorTypeaheadTokenView()) - ->setValue(pht('Custom Object')) - ->setIcon('fa-tag green'); + ->setValue(pht('Object with Color')) + ->setIcon('fa-tag') + ->setColor('green'); $token_list[] = id(new PhabricatorTypeaheadTokenView()) ->setValue(pht('Function Token')) ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setIcon('fa-users'); $token_list[] = id(new PhabricatorTypeaheadTokenView()) ->setValue(pht('Invalid Token')) ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_INVALID) ->setIcon('fa-exclamation-circle'); $token_list = phutil_tag( 'div', array( 'class' => 'grouped', 'style' => 'padding: 8px', ), $token_list); $output = array(); $output[] = id(new PHUIObjectBoxView()) ->setHeaderText('Tokens') ->appendChild($token_list); return $output; } } diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php index 079349d12c..13fcd6b143 100644 --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -1,163 +1,164 @@ datasource = $datasource; return $this; } public function setDisableBehavior($disable) { $this->disableBehavior = $disable; return $this; } protected function getCustomControlClass() { return 'aphront-form-control-tokenizer'; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function setPlaceholder($placeholder) { $this->placeholder = $placeholder; return $this; } public function willRender() { // Load the handles now so we'll get a bulk load later on when we actually // render them. $this->loadHandles(); } protected function renderInput() { $name = $this->getName(); $handles = $this->loadHandles(); $handles = iterator_to_array($handles); if ($this->getID()) { $id = $this->getID(); } else { $id = celerity_generate_unique_node_id(); } $datasource = $this->datasource; if ($datasource) { $datasource->setViewer($this->getUser()); } $placeholder = null; if (!strlen($this->placeholder)) { if ($datasource) { $placeholder = $datasource->getPlaceholderText(); } } else { $placeholder = $this->placeholder; } $tokens = array(); $values = nonempty($this->getValue(), array()); foreach ($values as $value) { if (isset($handles[$value])) { $token = PhabricatorTypeaheadTokenView::newFromHandle($handles[$value]); } else { $token = null; if ($datasource) { $function = $datasource->parseFunction($value); if ($function) { $token_list = $datasource->renderFunctionTokens( $function['name'], array($function['argv'])); $token = head($token_list); } } if (!$token) { $name = pht('Invalid Function: %s', $value); $token = $datasource->newInvalidToken($name); } $type = $token->getTokenType(); if ($type == PhabricatorTypeaheadTokenView::TYPE_INVALID) { $token->setKey($value); } } $token->setInputName($this->getName()); $tokens[] = $token; } $template = new AphrontTokenizerTemplateView(); $template->setName($name); $template->setID($id); $template->setValue($tokens); $username = null; if ($this->user) { $username = $this->user->getUsername(); } $datasource_uri = null; $browse_uri = null; if ($datasource) { $datasource->setViewer($this->getUser()); $datasource_uri = $datasource->getDatasourceURI(); $browse_uri = $datasource->getBrowseURI(); if ($browse_uri) { $template->setBrowseURI($browse_uri); } } if (!$this->disableBehavior) { Javelin::initBehavior('aphront-basic-tokenizer', array( 'id' => $id, 'src' => $datasource_uri, 'value' => mpull($tokens, 'getValue', 'getKey'), 'icons' => mpull($tokens, 'getIcon', 'getKey'), 'types' => mpull($tokens, 'getTokenType', 'getKey'), + 'colors' => mpull($tokens, 'getColor', 'getKey'), 'limit' => $this->limit, 'username' => $username, 'placeholder' => $placeholder, 'browseURI' => $browse_uri, )); } return $template->render(); } private function loadHandles() { if ($this->handles === null) { $viewer = $this->getUser(); if (!$viewer) { throw new Exception( pht( 'Call setUser() before rendering tokenizers. Use appendControl() '. 'on AphrontFormView to do this easily.')); } $values = nonempty($this->getValue(), array()); $phids = array(); foreach ($values as $value) { if (!PhabricatorTypeaheadDatasource::isFunctionToken($value)) { $phids[] = $value; } } $this->handles = $viewer->loadHandles($phids); } return $this->handles; } } diff --git a/webroot/rsrc/js/core/Prefab.js b/webroot/rsrc/js/core/Prefab.js index 085a2a4e1a..15e0333e41 100644 --- a/webroot/rsrc/js/core/Prefab.js +++ b/webroot/rsrc/js/core/Prefab.js @@ -1,308 +1,315 @@ /** * @provides phabricator-prefab * @requires javelin-install * javelin-util * javelin-dom * javelin-typeahead * javelin-tokenizer * javelin-typeahead-preloaded-source * javelin-typeahead-ondemand-source * javelin-dom * javelin-stratcom * javelin-util * @javelin */ /** * Utilities for client-side rendering (the greatest thing in the world). */ JX.install('Prefab', { statics : { renderSelect : function(map, selected, attrs) { var select = JX.$N('select', attrs || {}); for (var k in map) { select.options[select.options.length] = new Option(map[k], k); if (k == selected) { select.value = k; } } select.value = select.value || JX.keys(map)[0]; return select; }, newTokenizerFromTemplate: function(markup, config) { var template = JX.$H(markup).getFragment().firstChild; var container = JX.DOM.find(template, 'div', 'tokenizer-container'); container.id = ''; config.root = container; var build = JX.Prefab.buildTokenizer(config); build.node = template; return build; }, /** * Build a Phabricator tokenizer out of a configuration with application * sorting, datasource and placeholder rules. * * - `id` Root tokenizer ID (alternatively, pass `root`). * - `root` Root tokenizer node (replaces `id`). * - `src` Datasource URI. * - `ondemand` Optional, use an ondemand source. * - `value` Optional, initial value. * - `limit` Optional, token limit. * - `placeholder` Optional, placeholder text. * - `username` Optional, username to sort first (i.e., viewer). * - `icons` Optional, map of icons. * */ buildTokenizer : function(config) { config.icons = config.icons || {}; var root; try { root = config.root || JX.$(config.id); } catch (ex) { // If the root element does not exist, just return without building // anything. This happens in some cases -- like Conpherence -- where we // may load a tokenizer but not put it in the document. return; } var datasource; // Default to an ondemand source if no alternate configuration is // provided. var ondemand = true; if ('ondemand' in config) { ondemand = config.ondemand; } if (ondemand) { datasource = new JX.TypeaheadOnDemandSource(config.src); } else { datasource = new JX.TypeaheadPreloadedSource(config.src); } // Sort results so that the viewing user always comes up first; after // that, prefer unixname matches to realname matches. var sort_handler = function(value, list, cmp) { var priority_hits = {}; var self_hits = {}; var tokens = this.tokenize(value); for (var ii = 0; ii < list.length; ii++) { var item = list[ii]; for (var jj = 0; jj < tokens.length; jj++) { if (item.name.indexOf(tokens[jj]) === 0) { priority_hits[item.id] = true; } } if (!item.priority) { continue; } if (config.username && item.priority == config.username) { self_hits[item.id] = true; } for (var hh = 0; hh < tokens.length; hh++) { if (item.priority.substr(0, tokens[hh].length) == tokens[hh]) { priority_hits[item.id] = true; } } } list.sort(function(u, v) { if (self_hits[u.id] != self_hits[v.id]) { return self_hits[v.id] ? 1 : -1; } // If one result is open and one is closed, show the open result // first. The "!" tricks here are becaused closed values are display // strings, so the value is either `null` or some truthy string. If // we compare the values directly, we'll apply this rule to two // objects which are both closed but for different reasons, like // "Archived" and "Disabled". var u_open = !u.closed; var v_open = !v.closed; if (u_open != v_open) { if (u_open) { return -1; } else { return 1; } } if (priority_hits[u.id] != priority_hits[v.id]) { return priority_hits[v.id] ? 1 : -1; } // Sort users ahead of other result types. if (u.priorityType != v.priorityType) { if (u.priorityType == 'user') { return -1; } if (v.priorityType == 'user') { return 1; } } return cmp(u, v); }); }; datasource.setSortHandler(JX.bind(datasource, sort_handler)); datasource.setFilterHandler(JX.Prefab.filterClosedResults); datasource.setTransformer(JX.Prefab.transformDatasourceResults); var typeahead = new JX.Typeahead( root, JX.DOM.find(root, 'input', 'tokenizer-input')); typeahead.setDatasource(datasource); var tokenizer = new JX.Tokenizer(root); tokenizer.setTypeahead(typeahead); tokenizer.setRenderTokenCallback(function(value, key, container) { var result = datasource.getResult(key); var icon; var type; + var color; if (result) { icon = result.icon; value = result.displayName; type = result.tokenType; + color = result.color; } else { icon = config.icons[key]; type = config.types[key]; + color = config.colors[key]; } if (icon) { icon = JX.Prefab._renderIcon(icon); } - if (type) { - JX.DOM.alterClass(container, 'jx-tokenizer-token-' + type, true); + type = type || 'object'; + JX.DOM.alterClass(container, 'jx-tokenizer-token-' + type, true); + + if (color) { + JX.DOM.alterClass(container, color, true); } return [icon, value]; }); if (config.placeholder) { tokenizer.setPlaceholder(config.placeholder); } if (config.limit) { tokenizer.setLimit(config.limit); } if (config.value) { tokenizer.setInitialValue(config.value); } if (config.browseURI) { tokenizer.setBrowseURI(config.browseURI); } JX.Stratcom.addData(root, {'tokenizer' : tokenizer}); return { tokenizer: tokenizer }; }, /** * Filter callback for tokenizers and typeaheads which filters out closed * or disabled objects unless they are the only options. */ filterClosedResults: function(value, list) { // Look for any open result. var has_open = false; var ii; for (ii = 0; ii < list.length; ii++) { if (!list[ii].closed) { has_open = true; break; } } if (!has_open) { // Everything is closed, so just use it as-is. return list; } // Otherwise, only display the open results. var results = []; for (ii = 0; ii < list.length; ii++) { if (!list[ii].closed) { results.push(list[ii]); } } return results; }, /** * Transform results from a wire format into a usable format in a standard * way. */ transformDatasourceResults: function(fields) { var closed = fields[9]; var closed_ui; if (closed) { closed_ui = JX.$N( 'div', {className: 'tokenizer-closed'}, closed); } var icon = fields[8]; var icon_ui; if (icon) { icon_ui = JX.Prefab._renderIcon(icon); } var display = JX.$N( 'div', {className: 'tokenizer-result'}, [icon_ui, fields[4] || fields[0], closed_ui]); if (closed) { JX.DOM.alterClass(display, 'tokenizer-result-closed', true); } return { name: fields[0], displayName: fields[4] || fields[0], display: display, uri: fields[1], id: fields[2], priority: fields[3], priorityType: fields[7], imageURI: fields[6], icon: icon, closed: closed, type: fields[5], sprite: fields[10], - tokenType: fields[11], - unique: fields[12] || false + color: fields[11], + tokenType: fields[12], + unique: fields[13] || false }; }, _renderIcon: function(icon) { return JX.$N( 'span', {className: 'phui-icon-view phui-font-fa ' + icon}); } } });