Просмотр исходного кода

ref(search): Convert Search Item Type to Enum (#27167)

Converts search item type to enum and also enables default search items to autocomplete when search-syntax-highlight is on
David Wang 3 лет назад
Родитель
Сommit
289b48b069

+ 46 - 40
static/app/components/smartSearchBar/index.tsx

@@ -66,7 +66,7 @@ const ACTION_OVERFLOW_STEPS = 75;
 /**
  * Is the SearchItem a default item
  */
-const isDefaultDropdownItem = (item: SearchItem) => item?.type === 'default';
+const isDefaultDropdownItem = (item: SearchItem) => item?.type === ItemType.DEFAULT;
 
 const makeQueryState = (query: string) => ({
   query,
@@ -83,7 +83,7 @@ const generateOpAutocompleteGroup = (
     searchItems: operatorItems,
     recentSearchItems: undefined,
     tagName: '',
-    type: 'tag-operator' as ItemType,
+    type: ItemType.TAG_OPERATOR,
   };
 };
 
@@ -656,7 +656,7 @@ class SmartSearchBar extends React.Component<Props, State> {
             ? `"${value.replace(/"/g, '\\"')}"`
             : value;
 
-        return {value: escapedValue, desc: escapedValue, type: 'tag-value' as ItemType};
+        return {value: escapedValue, desc: escapedValue, type: ItemType.TAG_VALUE};
       });
     },
     DEFAULT_DEBOUNCE_DURATION,
@@ -673,7 +673,7 @@ class SmartSearchBar extends React.Component<Props, State> {
       .map((value, i) => ({
         value,
         desc: value,
-        type: 'tag-value',
+        type: ItemType.TAG_VALUE,
         ignoreMaxSearchItems: tag.maxSuggestedValues ? i < tag.maxSuggestedValues : false,
       }));
 
@@ -715,7 +715,7 @@ class SmartSearchBar extends React.Component<Props, State> {
       return recentSearches.map(searches => ({
         desc: searches.query,
         value: searches.query,
-        type: 'recent-search',
+        type: ItemType.RECENT_SEARCH,
       }));
     } catch (e) {
       Sentry.captureException(e);
@@ -731,14 +731,14 @@ class SmartSearchBar extends React.Component<Props, State> {
       const tags = this.getPredefinedTagValues(tag, query);
       const tagValues = tags.map<SearchItem>(v => ({
         ...v,
-        type: 'first-release',
+        type: ItemType.FIRST_RELEASE,
       }));
 
       const releases = await releasePromise;
       const releaseValues = releases.map<SearchItem>((r: any) => ({
         value: r.shortVersion,
         desc: r.shortVersion,
-        type: 'first-release',
+        type: ItemType.FIRST_RELEASE,
       }));
 
       return [...tagValues, ...releaseValues];
@@ -790,7 +790,7 @@ class SmartSearchBar extends React.Component<Props, State> {
       searchItems: tagKeys,
       recentSearchItems: recentSearches ?? [],
       tagName,
-      type: 'tag-key' as ItemType,
+      type: ItemType.TAG_KEY,
     };
   }
 
@@ -824,7 +824,7 @@ class SmartSearchBar extends React.Component<Props, State> {
         searchItems: [],
         recentSearchItems: [],
         tagName,
-        type: 'invalid-tag',
+        type: ItemType.INVALID_TAG,
       };
     }
 
@@ -850,7 +850,7 @@ class SmartSearchBar extends React.Component<Props, State> {
       searchItems: tagValues ?? [],
       recentSearchItems: recentSearches ?? [],
       tagName: tag.key,
-      type: 'tag-value',
+      type: ItemType.TAG_VALUE,
     };
   };
 
@@ -867,13 +867,18 @@ class SmartSearchBar extends React.Component<Props, State> {
       const tagKeys = this.getTagKeys('');
       const recentSearches = await this.getRecentSearches();
 
-      this.updateAutoCompleteState(tagKeys, recentSearches ?? [], '', 'tag-key');
+      this.updateAutoCompleteState(tagKeys, recentSearches ?? [], '', ItemType.TAG_KEY);
       return;
     }
     // cursor on whitespace show default "help" search terms
     this.setState({searchTerm: ''});
 
-    this.updateAutoCompleteState(defaultSearchItems, defaultRecentItems, '', 'default');
+    this.updateAutoCompleteState(
+      defaultSearchItems,
+      defaultRecentItems,
+      '',
+      ItemType.DEFAULT
+    );
     return;
   };
 
@@ -1009,7 +1014,7 @@ class SmartSearchBar extends React.Component<Props, State> {
         autoCompleteItems,
         recentSearches ?? [],
         matchValue,
-        'tag-key'
+        ItemType.TAG_KEY
       );
       return;
     }
@@ -1105,6 +1110,21 @@ class SmartSearchBar extends React.Component<Props, State> {
     this.setState(searchGroups);
   };
 
+  updateQuery = (newQuery: string, cursorPosition?: number) =>
+    this.setState(makeQueryState(newQuery), () => {
+      // setting a new input value will lose focus; restore it
+      if (this.searchInput.current) {
+        this.searchInput.current.focus();
+        if (cursorPosition) {
+          this.searchInput.current.selectionStart = cursorPosition;
+          this.searchInput.current.selectionEnd = cursorPosition;
+        }
+      }
+      // then update the autocomplete box with new items
+      this.updateAutoCompleteItems();
+      this.props.onChange?.(newQuery, new MouseEvent('click') as any);
+    });
+
   onAutoCompleteFromAst = (replaceText: string, item: SearchItem) => {
     const cursor = this.getCursorPosition();
     const {parsedQuery, query} = this.state;
@@ -1112,6 +1132,11 @@ class SmartSearchBar extends React.Component<Props, State> {
       return;
     }
     const cursorToken = this.getCursorToken(parsedQuery, cursor);
+
+    if (!cursorToken && isDefaultDropdownItem(item)) {
+      this.updateQuery(`${query}${replaceText}`);
+    }
+
     if (!cursorToken) {
       return;
     }
@@ -1122,7 +1147,7 @@ class SmartSearchBar extends React.Component<Props, State> {
     // the new text that will exist between clauseStart and clauseEnd
     let replaceToken = replaceText;
     if (cursorToken.type === Token.Filter) {
-      if (item.type === 'tag-operator') {
+      if (item.type === ItemType.TAG_OPERATOR) {
         const valueLocation = cursorToken.value.location;
         clauseStart = cursorToken.location.start.offset;
         clauseEnd = valueLocation.start.offset;
@@ -1137,6 +1162,7 @@ class SmartSearchBar extends React.Component<Props, State> {
         // Include everything after the ':'
         clauseStart = keyLocation.end.offset + 1;
         clauseEnd = location.end.offset;
+        replaceToken += ' ';
       } else if (isWithinToken(cursorToken.key, cursor)) {
         const location = cursorToken.key.location;
         clauseStart = location.start.offset;
@@ -1154,24 +1180,12 @@ class SmartSearchBar extends React.Component<Props, State> {
       const beforeClause = query.substring(0, clauseStart);
       const endClause = query.substring(clauseEnd);
       const newQuery = `${beforeClause}${replaceToken}${endClause}`;
-      this.setState(makeQueryState(newQuery), () => {
-        // setting a new input value will lose focus; restore it
-        if (this.searchInput.current) {
-          this.searchInput.current.focus();
-          // set cursor to be end of the autocomplete clause
-          const newCursorPosition = beforeClause.length + replaceToken.length;
-          this.searchInput.current.selectionStart = newCursorPosition;
-          this.searchInput.current.selectionEnd = newCursorPosition;
-        }
-        // then update the autocomplete box with new items
-        this.updateAutoCompleteItems();
-        this.props.onChange?.(newQuery, new MouseEvent('click') as any);
-      });
+      this.updateQuery(newQuery, beforeClause.length + replaceToken.length);
     }
   };
 
   onAutoComplete = (replaceText: string, item: SearchItem) => {
-    if (item.type === 'recent-search') {
+    if (item.type === ItemType.RECENT_SEARCH) {
       trackAnalyticsEvent({
         eventKey: 'search.searched',
         eventName: 'Search: Performed search',
@@ -1203,9 +1217,10 @@ class SmartSearchBar extends React.Component<Props, State> {
     let newQuery: string;
 
     // If not postfixed with : (tag value), add trailing space
-    replaceText += item.type !== 'tag-value' || cursor < query.length ? '' : ' ';
+    replaceText += item.type !== ItemType.TAG_VALUE || cursor < query.length ? '' : ' ';
 
-    const isNewTerm = query.charAt(query.length - 1) === ' ' && item.type !== 'tag-value';
+    const isNewTerm =
+      query.charAt(query.length - 1) === ' ' && item.type !== ItemType.TAG_VALUE;
 
     if (!terms) {
       newQuery = replaceText;
@@ -1240,16 +1255,7 @@ class SmartSearchBar extends React.Component<Props, State> {
       newQuery = newQuery.concat(query.slice(lastTermIndex));
     }
 
-    this.setState(makeQueryState(newQuery), () => {
-      // setting a new input value will lose focus; restore it
-      if (this.searchInput.current) {
-        this.searchInput.current.focus();
-      }
-
-      // then update the autocomplete box with new items
-      this.updateAutoCompleteItems();
-      this.props.onChange?.(newQuery, new MouseEvent('click') as any);
-    });
+    this.updateQuery(newQuery);
   };
 
   render() {

+ 2 - 2
static/app/components/smartSearchBar/searchDropdown.tsx

@@ -6,7 +6,7 @@ import {t} from 'app/locale';
 import overflowEllipsis from 'app/styles/overflowEllipsis';
 import space from 'app/styles/space';
 
-import {SearchGroup, SearchItem} from './types';
+import {ItemType, SearchGroup, SearchItem} from './types';
 
 type Props = {
   className?: string;
@@ -86,7 +86,7 @@ class SearchDropdown extends PureComponent<Props> {
           <SearchItemsList>
             {items.map(item => {
               const isEmpty = item.children && !item.children.length;
-              const invalidTag = item.type === 'invalid-tag';
+              const invalidTag = item.type === ItemType.INVALID_TAG;
 
               // Hide header if `item.children` is defined, an array, and is empty
               return (

+ 9 - 8
static/app/components/smartSearchBar/types.tsx

@@ -1,11 +1,12 @@
-export type ItemType =
-  | 'default'
-  | 'tag-key'
-  | 'tag-value'
-  | 'tag-operator'
-  | 'first-release'
-  | 'invalid-tag'
-  | 'recent-search';
+export enum ItemType {
+  DEFAULT = 'default',
+  TAG_KEY = 'tag-key',
+  TAG_VALUE = 'tag-value',
+  TAG_OPERATOR = 'tag-operator',
+  FIRST_RELEASE = 'first-release',
+  INVALID_TAG = 'invalid-tag',
+  RECENT_SEARCH = 'recent-search',
+}
 
 export type SearchGroup = {
   type: ItemType | 'header';

+ 14 - 14
static/app/components/smartSearchBar/utils.tsx

@@ -46,19 +46,19 @@ export function getQueryTerms(query: string, cursor: number) {
 }
 
 function getTitleForType(type: ItemType) {
-  if (type === 'tag-value') {
+  if (type === ItemType.TAG_VALUE) {
     return t('Tag Values');
   }
 
-  if (type === 'recent-search') {
+  if (type === ItemType.RECENT_SEARCH) {
     return t('Recent Searches');
   }
 
-  if (type === 'default') {
+  if (type === ItemType.DEFAULT) {
     return t('Common Search Terms');
   }
 
-  if (type === 'tag-operator') {
+  if (type === ItemType.TAG_OPERATOR) {
     return t('Operator Helpers');
   }
 
@@ -66,11 +66,11 @@ function getTitleForType(type: ItemType) {
 }
 
 function getIconForTypeAndTag(type: ItemType, tagName: string) {
-  if (type === 'recent-search') {
+  if (type === ItemType.RECENT_SEARCH) {
     return <IconClock size="xs" />;
   }
 
-  if (type === 'default') {
+  if (type === ItemType.DEFAULT) {
     return <IconStar size="xs" />;
   }
 
@@ -120,7 +120,7 @@ export function createSearchGroups(
 
   const searchGroup: SearchGroup = {
     title: getTitleForType(type),
-    type: type === 'invalid-tag' ? type : 'header',
+    type: type === ItemType.INVALID_TAG ? type : 'header',
     icon: getIconForTypeAndTag(type, tagName),
     children: [...searchItems],
   };
@@ -175,37 +175,37 @@ export function filterSearchGroupsByIndex(items: SearchGroup[], index: number) {
 export function generateOperatorEntryMap(tag: string) {
   return {
     [TermOperator.Default]: {
-      type: 'tag-operator' as ItemType,
+      type: ItemType.TAG_OPERATOR,
       value: ':',
       desc: `${tag}:${t('[value] is equal to')}`,
     },
     [TermOperator.GreaterThanEqual]: {
-      type: 'tag-operator' as ItemType,
+      type: ItemType.TAG_OPERATOR,
       value: ':>=',
       desc: `${tag}:${t('>=[value] is greater than or equal to')}`,
     },
     [TermOperator.LessThanEqual]: {
-      type: 'tag-operator' as ItemType,
+      type: ItemType.TAG_OPERATOR,
       value: ':<=',
       desc: `${tag}:${t('<=[value] is less than or equal to')}`,
     },
     [TermOperator.GreaterThan]: {
-      type: 'tag-operator' as ItemType,
+      type: ItemType.TAG_OPERATOR,
       value: ':>',
       desc: `${tag}:${t('>[value] is greater than')}`,
     },
     [TermOperator.LessThan]: {
-      type: 'tag-operator' as ItemType,
+      type: ItemType.TAG_OPERATOR,
       value: ':<',
       desc: `${tag}:${t('<[value] is less than')}`,
     },
     [TermOperator.Equal]: {
-      type: 'tag-operator' as ItemType,
+      type: ItemType.TAG_OPERATOR,
       value: ':=',
       desc: `${tag}:${t('=[value] is equal to')}`,
     },
     [TermOperator.NotEqual]: {
-      type: 'tag-operator' as ItemType,
+      type: ItemType.TAG_OPERATOR,
       value: '!:',
       desc: `!${tag}:${t('[value] is not equal to')}`,
     },

+ 7 - 7
static/app/views/issueList/searchBar.tsx

@@ -8,7 +8,7 @@ import {
   makeSaveSearchAction,
   makeSearchBuilderAction,
 } from 'app/components/smartSearchBar/actions';
-import {SearchItem} from 'app/components/smartSearchBar/types';
+import {ItemType, SearchItem} from 'app/components/smartSearchBar/types';
 import {t} from 'app/locale';
 import {Organization, SavedSearch, SavedSearchType, Tag} from 'app/types';
 import withApi from 'app/utils/withApi';
@@ -21,31 +21,31 @@ const SEARCH_ITEMS: SearchItem[] = [
     title: t('Tag'),
     desc: 'browser:"Chrome 34", has:browser',
     value: 'browser:',
-    type: 'default',
+    type: ItemType.DEFAULT,
   },
   {
     title: t('Status'),
     desc: 'is:resolved, unresolved, ignored, assigned, unassigned',
     value: 'is:',
-    type: 'default',
+    type: ItemType.DEFAULT,
   },
   {
     title: t('Time or Count'),
     desc: 'firstSeen, lastSeen, event.timestamp, timesSeen',
     value: '',
-    type: 'default',
+    type: ItemType.DEFAULT,
   },
   {
     title: t('Assigned'),
     desc: 'assigned, assigned_or_suggested:[me|[me, none]|user@example.com|#team-example]',
     value: '',
-    type: 'default',
+    type: ItemType.DEFAULT,
   },
   {
     title: t('Bookmarked By'),
     desc: 'bookmarks:[me|user@example.com]',
     value: 'bookmarks:',
-    type: 'default',
+    type: ItemType.DEFAULT,
   },
 ];
 
@@ -87,7 +87,7 @@ class IssueListSearchBar extends React.Component<Props, State> {
           ? resp.map(query => ({
               desc: query,
               value: query,
-              type: 'recent-search',
+              type: ItemType.RECENT_SEARCH,
             }))
           : [],
       ],