Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions static/app/components/searchQueryBuilder/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2010,4 +2010,22 @@ describe('SearchQueryBuilder', function () {
).toBeInTheDocument();
});
});

describe('disallowFreeText', function () {
it('should mark free text invalid', async function () {
render(
<SearchQueryBuilder {...defaultProps} disallowFreeText initialQuery="foo" />
);

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();
});
});
});
4 changes: 2 additions & 2 deletions static/app/components/searchQueryBuilder/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>([...configs]);
const queryBuilderOptions = enabledConfigs.reduce((acc, config) => {
Expand All @@ -141,7 +141,7 @@ export default storyBook(SearchQueryBuilder, story => {
))}
</MultipleCheckbox>
<SearchQueryBuilder
initialQuery="(browser.name:Firefox OR browser.name:Internet*) TypeError*"
initialQuery="(browser.name:Firefox OR browser.name:Internet*) TypeError"
filterKeySections={FITLER_KEY_SECTIONS}
filterKeys={FILTER_KEYS}
getTagValues={getTagValues}
Expand Down
7 changes: 7 additions & 0 deletions static/app/components/searchQueryBuilder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface SearchQueryBuilderProps {
*/
searchSource: string;
className?: string;
/**
* When true, free text will be marked as invalid.
*/
disallowFreeText?: boolean;
/**
* When true, parens and logical operators (AND, OR) will be marked as invalid.
*/
Expand Down Expand Up @@ -96,6 +100,7 @@ function ActionButtons() {
export function SearchQueryBuilder({
className,
disallowLogicalOperators,
disallowFreeText,
disallowWildcard,
label,
initialQuery,
Expand All @@ -117,11 +122,13 @@ export function SearchQueryBuilder({
const parsedQuery = useMemo(
() =>
parseQueryBuilderValue(state.query, fieldDefinitionGetter, {
disallowFreeText,
disallowLogicalOperators,
disallowWildcard,
filterKeys,
}),
[
disallowFreeText,
disallowLogicalOperators,
disallowWildcard,
fieldDefinitionGetter,
Expand Down
6 changes: 6 additions & 0 deletions static/app/components/searchQueryBuilder/tokens/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type SearchQueryBuilderComboboxProps<T extends SelectOptionOrSectionWithKey<stri
onInputChange?: React.ChangeEventHandler<HTMLInputElement>;
onKeyDown?: (e: KeyboardEvent) => void;
onKeyUp?: (e: KeyboardEvent) => void;
onOpenChange?: (newOpenState: boolean) => void;
onPaste?: (e: React.ClipboardEvent<HTMLInputElement>) => void;
openOnFocus?: boolean;
placeholder?: string;
Expand Down Expand Up @@ -469,6 +470,7 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
onKeyDown,
onKeyUp,
onInputChange,
onOpenChange,
autoFocus,
openOnFocus,
onFocus,
Expand Down Expand Up @@ -594,6 +596,10 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
hasCustomMenu,
});

useEffect(() => {
onOpenChange?.(isOpen);
}, [onOpenChange, isOpen]);

const {
overlayProps,
triggerProps,
Expand Down
40 changes: 38 additions & 2 deletions static/app/components/searchQueryBuilder/tokens/freeText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
FocusOverride,
} from 'sentry/components/searchQueryBuilder/types';
import {
InvalidReason,
type ParseResultToken,
Token,
type TokenResult,
Expand Down Expand Up @@ -261,13 +262,38 @@ function KeyDescription({tag}: {tag: Tag}) {
);
}

function shouldHideInvalidTooltip({
token,
inputValue,
isOpen,
}: {
inputValue: string;
isOpen: boolean;
token: TokenResult<Token.FREE_TEXT>;
}) {
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<ParseResultToken>;
state: ListState<ParseResultToken>;
token: TokenResult<Token.FREE_TEXT>;
Expand All @@ -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}
>
<InvisibleText aria-hidden>{inputValue}</InvisibleText>
Expand All @@ -297,6 +325,7 @@ function SearchQueryBuilderInputInternal({
const organization = useOrganization();
const inputRef = useRef<HTMLInputElement>(null);
const trimmedTokenValue = token.text.trim();
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState(trimmedTokenValue);
const [selectionIndex, setSelectionIndex] = useState(0);
const isFocused =
Expand Down Expand Up @@ -486,6 +515,7 @@ function SearchQueryBuilderInputInternal({
setSelectionIndex(e.target.selectionStart ?? 0);
}}
onKeyDown={onKeyDown}
onOpenChange={setIsOpen}
tabIndex={isFocused ? 0 : -1}
maxOptions={50}
onPaste={onPaste}
Expand Down Expand Up @@ -515,7 +545,13 @@ function SearchQueryBuilderInputInternal({
)
}
</SearchQueryBuilderCombobox>
<InvalidText token={token} state={state} item={item} inputValue={inputValue} />
<InvalidText
token={token}
state={state}
item={item}
inputValue={inputValue}
isOpen={isOpen}
/>
</Fragment>
);
}
Expand Down
2 changes: 2 additions & 0 deletions static/app/components/searchQueryBuilder/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ export function parseQueryBuilderValue(
getFieldDefinition: FieldDefinitionGetter,
options?: {
filterKeys: TagCollection;
disallowFreeText?: boolean;
disallowLogicalOperators?: boolean;
disallowWildcard?: boolean;
}
): ParseResult | null {
return collapseTextTokens(
parseSearch(value || ' ', {
flattenParenGroups: true,
disallowFreeText: options?.disallowFreeText,
disallowWildcard: options?.disallowWildcard,
disallowedLogicalOperators: options?.disallowLogicalOperators
? new Set([BooleanOperator.AND, BooleanOperator.OR])
Expand Down