Skip to content

Commit eb97182

Browse files
committed
faet(query-builder): Add disallowFreeText option
1 parent 078c9f0 commit eb97182

File tree

6 files changed

+73
-4
lines changed

6 files changed

+73
-4
lines changed

static/app/components/searchQueryBuilder/index.spec.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,4 +2010,22 @@ describe('SearchQueryBuilder', function () {
20102010
).toBeInTheDocument();
20112011
});
20122012
});
2013+
2014+
describe('disallowFreeText', function () {
2015+
it('should mark free text invalid', async function () {
2016+
render(
2017+
<SearchQueryBuilder {...defaultProps} disallowFreeText initialQuery="foo" />
2018+
);
2019+
2020+
expect(screen.getByRole('row', {name: 'foo'})).toHaveAttribute(
2021+
'aria-invalid',
2022+
'true'
2023+
);
2024+
2025+
await userEvent.click(getLastInput());
2026+
expect(
2027+
await screen.findByText('Free text is not supported in this search')
2028+
).toBeInTheDocument();
2029+
});
2030+
});
20132031
});

static/app/components/searchQueryBuilder/index.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export default storyBook(SearchQueryBuilder, story => {
114114
});
115115

116116
story('Config Options', () => {
117-
const configs = ['disallowLogicalOperators', 'disallowWildcard'];
117+
const configs = ['disallowFreeText', 'disallowLogicalOperators', 'disallowWildcard'];
118118

119119
const [enabledConfigs, setEnabledConfigs] = useState<string[]>([...configs]);
120120
const queryBuilderOptions = enabledConfigs.reduce((acc, config) => {
@@ -141,7 +141,7 @@ export default storyBook(SearchQueryBuilder, story => {
141141
))}
142142
</MultipleCheckbox>
143143
<SearchQueryBuilder
144-
initialQuery="(browser.name:Firefox OR browser.name:Internet*) TypeError*"
144+
initialQuery="(browser.name:Firefox OR browser.name:Internet*) TypeError"
145145
filterKeySections={FITLER_KEY_SECTIONS}
146146
filterKeys={FILTER_KEYS}
147147
getTagValues={getTagValues}

static/app/components/searchQueryBuilder/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export interface SearchQueryBuilderProps {
4040
*/
4141
searchSource: string;
4242
className?: string;
43+
/**
44+
* When true, free text will be marked as invalid.
45+
*/
46+
disallowFreeText?: boolean;
4347
/**
4448
* When true, parens and logical operators (AND, OR) will be marked as invalid.
4549
*/
@@ -96,6 +100,7 @@ function ActionButtons() {
96100
export function SearchQueryBuilder({
97101
className,
98102
disallowLogicalOperators,
103+
disallowFreeText,
99104
disallowWildcard,
100105
label,
101106
initialQuery,
@@ -117,11 +122,13 @@ export function SearchQueryBuilder({
117122
const parsedQuery = useMemo(
118123
() =>
119124
parseQueryBuilderValue(state.query, fieldDefinitionGetter, {
125+
disallowFreeText,
120126
disallowLogicalOperators,
121127
disallowWildcard,
122128
filterKeys,
123129
}),
124130
[
131+
disallowFreeText,
125132
disallowLogicalOperators,
126133
disallowWildcard,
127134
fieldDefinitionGetter,

static/app/components/searchQueryBuilder/tokens/combobox.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type SearchQueryBuilderComboboxProps<T extends SelectOptionOrSectionWithKey<stri
8787
onInputChange?: React.ChangeEventHandler<HTMLInputElement>;
8888
onKeyDown?: (e: KeyboardEvent) => void;
8989
onKeyUp?: (e: KeyboardEvent) => void;
90+
onOpenChange?: (newOpenState: boolean) => void;
9091
onPaste?: (e: React.ClipboardEvent<HTMLInputElement>) => void;
9192
openOnFocus?: boolean;
9293
placeholder?: string;
@@ -469,6 +470,7 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
469470
onKeyDown,
470471
onKeyUp,
471472
onInputChange,
473+
onOpenChange,
472474
autoFocus,
473475
openOnFocus,
474476
onFocus,
@@ -594,6 +596,10 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
594596
hasCustomMenu,
595597
});
596598

599+
useEffect(() => {
600+
onOpenChange?.(isOpen);
601+
}, [onOpenChange, isOpen]);
602+
597603
const {
598604
overlayProps,
599605
triggerProps,

static/app/components/searchQueryBuilder/tokens/freeText.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
FocusOverride,
2323
} from 'sentry/components/searchQueryBuilder/types';
2424
import {
25+
InvalidReason,
2526
type ParseResultToken,
2627
Token,
2728
type TokenResult,
@@ -261,13 +262,38 @@ function KeyDescription({tag}: {tag: Tag}) {
261262
);
262263
}
263264

265+
function shouldHideInvalidTooltip({
266+
token,
267+
inputValue,
268+
isOpen,
269+
}: {
270+
inputValue: string;
271+
isOpen: boolean;
272+
token: TokenResult<Token.FREE_TEXT>;
273+
}) {
274+
if (!token.invalid || isOpen) {
275+
return true;
276+
}
277+
278+
switch (token.invalid.type) {
279+
case InvalidReason.FREE_TEXT_NOT_ALLOWED:
280+
return inputValue === '';
281+
case InvalidReason.WILDCARD_NOT_ALLOWED:
282+
return !inputValue.includes('*');
283+
default:
284+
return false;
285+
}
286+
}
287+
264288
function InvalidText({
265289
token,
266290
state,
267291
item,
268292
inputValue,
293+
isOpen,
269294
}: {
270295
inputValue: string;
296+
isOpen: boolean;
271297
item: Node<ParseResultToken>;
272298
state: ListState<ParseResultToken>;
273299
token: TokenResult<Token.FREE_TEXT>;
@@ -280,7 +306,9 @@ function InvalidText({
280306
state={state}
281307
token={token}
282308
item={item}
283-
forceVisible={!inputValue.includes('*') ? false : undefined}
309+
forceVisible={
310+
shouldHideInvalidTooltip({token, inputValue, isOpen}) ? false : undefined
311+
}
284312
skipWrapper={false}
285313
>
286314
<InvisibleText aria-hidden>{inputValue}</InvisibleText>
@@ -297,6 +325,7 @@ function SearchQueryBuilderInputInternal({
297325
const organization = useOrganization();
298326
const inputRef = useRef<HTMLInputElement>(null);
299327
const trimmedTokenValue = token.text.trim();
328+
const [isOpen, setIsOpen] = useState(false);
300329
const [inputValue, setInputValue] = useState(trimmedTokenValue);
301330
const [selectionIndex, setSelectionIndex] = useState(0);
302331
const isFocused =
@@ -486,6 +515,7 @@ function SearchQueryBuilderInputInternal({
486515
setSelectionIndex(e.target.selectionStart ?? 0);
487516
}}
488517
onKeyDown={onKeyDown}
518+
onOpenChange={setIsOpen}
489519
tabIndex={isFocused ? 0 : -1}
490520
maxOptions={50}
491521
onPaste={onPaste}
@@ -515,7 +545,13 @@ function SearchQueryBuilderInputInternal({
515545
)
516546
}
517547
</SearchQueryBuilderCombobox>
518-
<InvalidText token={token} state={state} item={item} inputValue={inputValue} />
548+
<InvalidText
549+
token={token}
550+
state={state}
551+
item={item}
552+
inputValue={inputValue}
553+
isOpen={isOpen}
554+
/>
519555
</Fragment>
520556
);
521557
}

static/app/components/searchQueryBuilder/utils.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,15 @@ export function parseQueryBuilderValue(
6565
getFieldDefinition: FieldDefinitionGetter,
6666
options?: {
6767
filterKeys: TagCollection;
68+
disallowFreeText?: boolean;
6869
disallowLogicalOperators?: boolean;
6970
disallowWildcard?: boolean;
7071
}
7172
): ParseResult | null {
7273
return collapseTextTokens(
7374
parseSearch(value || ' ', {
7475
flattenParenGroups: true,
76+
disallowFreeText: options?.disallowFreeText,
7577
disallowWildcard: options?.disallowWildcard,
7678
disallowedLogicalOperators: options?.disallowLogicalOperators
7779
? new Set([BooleanOperator.AND, BooleanOperator.OR])

0 commit comments

Comments
 (0)