Browse Source

feat(query-builder): Render multiple values with 'or' in between (#71680)

Malachi Willey 9 months ago
parent
commit
ac67cf929d

+ 33 - 3
static/app/components/searchQueryBuilder/filter.tsx

@@ -1,4 +1,4 @@
-import {useCallback, useMemo, useRef, useState} from 'react';
+import {Fragment, useCallback, useMemo, useRef, useState} from 'react';
 import styled from '@emotion/styled';
 import {useFocusWithin} from '@react-aria/interactions';
 import {mergeProps} from '@react-aria/utils';
@@ -17,7 +17,7 @@ import {SearchQueryBuilderValueCombobox} from 'sentry/components/searchQueryBuil
 import {
   type ParseResultToken,
   TermOperator,
-  type Token,
+  Token,
   type TokenResult,
 } from 'sentry/components/searchSyntax/parser';
 import {IconClose} from 'sentry/icons';
@@ -116,6 +116,26 @@ function FilterKey({token, state, item}: SearchQueryTokenProps) {
   );
 }
 
+function FilterValueText({token}: {token: TokenResult<Token.FILTER>}) {
+  switch (token.value.type) {
+    case Token.VALUE_TEXT_LIST:
+    case Token.VALUE_NUMBER_LIST:
+      const items = token.value.items;
+      return (
+        <FilterValueList>
+          {items.map((item, index) => (
+            <Fragment key={index}>
+              <span>{formatFilterValue(item.value)}</span>
+              {index !== items.length - 1 ? <FilterValueOr>or</FilterValueOr> : null}
+            </Fragment>
+          ))}
+        </FilterValueList>
+      );
+    default:
+      return formatFilterValue(token.value);
+  }
+}
+
 function FilterValue({token, state, item}: SearchQueryTokenProps) {
   const ref = useRef<HTMLDivElement>(null);
 
@@ -158,7 +178,7 @@ function FilterValue({token, state, item}: SearchQueryTokenProps) {
       {...filterButtonProps}
     >
       <InteractionStateLayer />
-      {formatFilterValue(token)}
+      <FilterValueText token={token} />
     </ValueButton>
   );
 }
@@ -321,3 +341,13 @@ const DeleteButton = styled(UnstyledButton)`
     border-left: 1px solid ${p => p.theme.innerBorder};
   }
 `;
+
+const FilterValueList = styled('div')`
+  display: flex;
+  align-items: center;
+  gap: ${space(0.5)};
+`;
+
+const FilterValueOr = styled('span')`
+  color: ${p => p.theme.subText};
+`;

+ 6 - 5
static/app/components/searchQueryBuilder/index.spec.tsx

@@ -160,11 +160,12 @@ describe('SearchQueryBuilder', function () {
       expect(
         screen.getByRole('row', {name: 'browser.name:[firefox,Chrome]'})
       ).toBeInTheDocument();
-      expect(
-        within(
-          screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
-        ).getByText('[firefox,Chrome]')
-      ).toBeInTheDocument();
+      const valueButton = screen.getByRole('button', {
+        name: 'Edit value for filter: browser.name',
+      });
+      expect(within(valueButton).getByText('firefox')).toBeInTheDocument();
+      expect(within(valueButton).getByText('or')).toBeInTheDocument();
+      expect(within(valueButton).getByText('Chrome')).toBeInTheDocument();
     });
 
     it('escapes values with spaces and reserved characters', async function () {

+ 4 - 4
static/app/components/searchQueryBuilder/utils.tsx

@@ -99,11 +99,11 @@ export function unescapeTagValue(value: string): string {
   return value.replace(/\\"/g, '"');
 }
 
-export function formatFilterValue(token: TokenResult<Token.FILTER>): string {
-  switch (token.value.type) {
+export function formatFilterValue(token: TokenResult<Token.FILTER>['value']): string {
+  switch (token.type) {
     case Token.VALUE_TEXT:
-      return unescapeTagValue(token.value.value);
+      return unescapeTagValue(token.value);
     default:
-      return token.value.text;
+      return token.text;
   }
 }

+ 1 - 1
static/app/components/searchQueryBuilder/valueCombobox.tsx

@@ -242,7 +242,7 @@ export function SearchQueryBuilderValueCombobox({
         onOptionSelected={handleSelectValue}
         onCustomValueSelected={handleSelectValue}
         inputValue={inputValue}
-        placeholder={canSelectMultipleValues ? '' : formatFilterValue(token)}
+        placeholder={canSelectMultipleValues ? '' : formatFilterValue(token.value)}
         token={token}
         inputLabel={t('Edit filter value')}
         onInputChange={e => setInputValue(e.target.value)}