From eb971823cd0d035355ffdd519910c3a69aeed78d Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 22 Jul 2024 20:59:06 -0700 Subject: [PATCH] faet(query-builder): Add disallowFreeText option --- .../searchQueryBuilder/index.spec.tsx | 18 +++++++++ .../searchQueryBuilder/index.stories.tsx | 4 +- .../components/searchQueryBuilder/index.tsx | 7 ++++ .../searchQueryBuilder/tokens/combobox.tsx | 6 +++ .../searchQueryBuilder/tokens/freeText.tsx | 40 ++++++++++++++++++- .../components/searchQueryBuilder/utils.tsx | 2 + 6 files changed, 73 insertions(+), 4 deletions(-) diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 69cdfbdcbaa00a..af1e1828ae3723 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -2010,4 +2010,22 @@ describe('SearchQueryBuilder', function () { ).toBeInTheDocument(); }); }); + + describe('disallowFreeText', function () { + it('should mark free text invalid', async function () { + render( + + ); + + expect(screen.getByRole('row', {name: 'foo'})).toHaveAttribute( + 'aria-invalid', + 'true' + ); + + await userEvent.click(getLastInput()); + expect( + await screen.findByText('Free text is not supported in this search') + ).toBeInTheDocument(); + }); + }); }); diff --git a/static/app/components/searchQueryBuilder/index.stories.tsx b/static/app/components/searchQueryBuilder/index.stories.tsx index 1eb1e4604afb88..6b5ce33602da5c 100644 --- a/static/app/components/searchQueryBuilder/index.stories.tsx +++ b/static/app/components/searchQueryBuilder/index.stories.tsx @@ -114,7 +114,7 @@ export default storyBook(SearchQueryBuilder, story => { }); story('Config Options', () => { - const configs = ['disallowLogicalOperators', 'disallowWildcard']; + const configs = ['disallowFreeText', 'disallowLogicalOperators', 'disallowWildcard']; const [enabledConfigs, setEnabledConfigs] = useState([...configs]); const queryBuilderOptions = enabledConfigs.reduce((acc, config) => { @@ -141,7 +141,7 @@ export default storyBook(SearchQueryBuilder, story => { ))} parseQueryBuilderValue(state.query, fieldDefinitionGetter, { + disallowFreeText, disallowLogicalOperators, disallowWildcard, filterKeys, }), [ + disallowFreeText, disallowLogicalOperators, disallowWildcard, fieldDefinitionGetter, diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx index 2978a7f11b83cd..255e46698c35f6 100644 --- a/static/app/components/searchQueryBuilder/tokens/combobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/combobox.tsx @@ -87,6 +87,7 @@ type SearchQueryBuilderComboboxProps; onKeyDown?: (e: KeyboardEvent) => void; onKeyUp?: (e: KeyboardEvent) => void; + onOpenChange?: (newOpenState: boolean) => void; onPaste?: (e: React.ClipboardEvent) => void; openOnFocus?: boolean; placeholder?: string; @@ -469,6 +470,7 @@ function SearchQueryBuilderComboboxInner { + onOpenChange?.(isOpen); + }, [onOpenChange, isOpen]); + const { overlayProps, triggerProps, diff --git a/static/app/components/searchQueryBuilder/tokens/freeText.tsx b/static/app/components/searchQueryBuilder/tokens/freeText.tsx index 2f5b4f591b63e0..f14245c1866ddc 100644 --- a/static/app/components/searchQueryBuilder/tokens/freeText.tsx +++ b/static/app/components/searchQueryBuilder/tokens/freeText.tsx @@ -22,6 +22,7 @@ import type { FocusOverride, } from 'sentry/components/searchQueryBuilder/types'; import { + InvalidReason, type ParseResultToken, Token, type TokenResult, @@ -261,13 +262,38 @@ function KeyDescription({tag}: {tag: Tag}) { ); } +function shouldHideInvalidTooltip({ + token, + inputValue, + isOpen, +}: { + inputValue: string; + isOpen: boolean; + token: TokenResult; +}) { + if (!token.invalid || isOpen) { + return true; + } + + switch (token.invalid.type) { + case InvalidReason.FREE_TEXT_NOT_ALLOWED: + return inputValue === ''; + case InvalidReason.WILDCARD_NOT_ALLOWED: + return !inputValue.includes('*'); + default: + return false; + } +} + function InvalidText({ token, state, item, inputValue, + isOpen, }: { inputValue: string; + isOpen: boolean; item: Node; state: ListState; token: TokenResult; @@ -280,7 +306,9 @@ function InvalidText({ state={state} token={token} item={item} - forceVisible={!inputValue.includes('*') ? false : undefined} + forceVisible={ + shouldHideInvalidTooltip({token, inputValue, isOpen}) ? false : undefined + } skipWrapper={false} > {inputValue} @@ -297,6 +325,7 @@ function SearchQueryBuilderInputInternal({ const organization = useOrganization(); const inputRef = useRef(null); const trimmedTokenValue = token.text.trim(); + const [isOpen, setIsOpen] = useState(false); const [inputValue, setInputValue] = useState(trimmedTokenValue); const [selectionIndex, setSelectionIndex] = useState(0); const isFocused = @@ -486,6 +515,7 @@ function SearchQueryBuilderInputInternal({ setSelectionIndex(e.target.selectionStart ?? 0); }} onKeyDown={onKeyDown} + onOpenChange={setIsOpen} tabIndex={isFocused ? 0 : -1} maxOptions={50} onPaste={onPaste} @@ -515,7 +545,13 @@ function SearchQueryBuilderInputInternal({ ) } - + ); } diff --git a/static/app/components/searchQueryBuilder/utils.tsx b/static/app/components/searchQueryBuilder/utils.tsx index 3314e464d49956..22d4028165618c 100644 --- a/static/app/components/searchQueryBuilder/utils.tsx +++ b/static/app/components/searchQueryBuilder/utils.tsx @@ -65,6 +65,7 @@ export function parseQueryBuilderValue( getFieldDefinition: FieldDefinitionGetter, options?: { filterKeys: TagCollection; + disallowFreeText?: boolean; disallowLogicalOperators?: boolean; disallowWildcard?: boolean; } @@ -72,6 +73,7 @@ export function parseQueryBuilderValue( return collapseTextTokens( parseSearch(value || ' ', { flattenParenGroups: true, + disallowFreeText: options?.disallowFreeText, disallowWildcard: options?.disallowWildcard, disallowedLogicalOperators: options?.disallowLogicalOperators ? new Set([BooleanOperator.AND, BooleanOperator.OR])