diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 2e392ba9ffb5ac..824ce00844f07c 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -980,31 +980,43 @@ describe('SearchQueryBuilder', () => { it('can add a new token by clicking a key suggestion', async () => { const mockOnChange = jest.fn(); render(, { - organization: {features: ['search-query-builder-input-flow-changes']}, + organization: { + features: [ + 'search-query-builder-input-flow-changes', + 'search-query-builder-wildcard-operators', + 'search-query-builder-default-to-contains', + ], + }, }); await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'})); await userEvent.click(screen.getByRole('option', {name: 'browser.name'})); // New token should be added with the correct key and default value - expect(screen.getByRole('row', {name: 'browser.name:""'})).toBeInTheDocument(); + expect( + screen.getByRole('row', {name: `browser.name:${WildcardOperators.CONTAINS}""`}) + ).toBeInTheDocument(); // onChange should not be called until exiting edit mode expect(mockOnChange).not.toHaveBeenCalled(); // Should have focus on the operator option - const operatorOption = await screen.findByRole('option', {name: 'is'}); + const operatorOption = await screen.findByRole('option', {name: 'contains'}); expect(operatorOption).toHaveFocus(); await userEvent.click(operatorOption); await userEvent.click(await screen.findByRole('option', {name: 'Firefox'})); // New token should have a value - expect(screen.getByRole('row', {name: 'browser.name:Firefox'})).toBeInTheDocument(); + expect( + screen.getByRole('row', { + name: `browser.name:${WildcardOperators.CONTAINS}Firefox`, + }) + ).toBeInTheDocument(); // Now we call onChange expect(mockOnChange).toHaveBeenCalledTimes(1); expect(mockOnChange).toHaveBeenCalledWith( - 'browser.name:Firefox', + `browser.name:${WildcardOperators.CONTAINS}Firefox`, expect.anything() ); }); @@ -1127,7 +1139,15 @@ describe('SearchQueryBuilder', () => { }); it('converts text to filter when typing :', async () => { - render(); + render(, { + organization: { + features: [ + 'search-query-builder-input-flow-changes', + 'search-query-builder-wildcard-operators', + 'search-query-builder-default-to-contains', + ], + }, + }); await userEvent.click(getLastInput()); await userEvent.type( @@ -1135,7 +1155,9 @@ describe('SearchQueryBuilder', () => { 'browser.name:' ); - const browserNameFilter = await screen.findByRole('row', {name: 'browser.name:""'}); + const browserNameFilter = await screen.findByRole('row', { + name: `browser.name:${WildcardOperators.CONTAINS}""`, + }); expect(browserNameFilter).toBeInTheDocument(); }); @@ -1155,16 +1177,28 @@ describe('SearchQueryBuilder', () => { }); it('selects [Filtered] from dropdown', async () => { - render(); + render(, { + organization: { + features: [ + 'search-query-builder-input-flow-changes', + 'search-query-builder-wildcard-operators', + 'search-query-builder-default-to-contains', + ], + }, + }); await userEvent.click(getLastInput()); await userEvent.type( screen.getByRole('combobox', {name: 'Add a search term'}), 'message:' ); + await userEvent.keyboard('{enter}'); await userEvent.click(screen.getByRole('option', {name: '[Filtered]'})); + expect( - await screen.findByRole('row', {name: 'message:"[Filtered]"'}) + await screen.findByRole('row', { + name: `message:${WildcardOperators.CONTAINS}"[Filtered]"`, + }) ).toBeInTheDocument(); }); }); diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx index 503335c8eb848a..fb6564fb1e4b2f 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx @@ -54,6 +54,10 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) { getFieldDefinition(getKeyName(token.key)) ); + const hasWildcardOperators = + organization.features.includes('search-query-builder-wildcard-operators') && + organization.features.includes('search-query-builder-default-to-contains'); + const handleSelectKey = useCallback( (keyName: string) => { const newFieldDef = getFieldDefinition(keyName); @@ -108,7 +112,7 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) { dispatch({ type: 'REPLACE_TOKENS_WITH_TEXT_ON_SELECT', tokens: [token], - text: getInitialFilterText(keyName, newFieldDef), + text: getInitialFilterText(keyName, newFieldDef, hasWildcardOperators), focusOverride: { itemKey: item.key.toString(), part: 'value', @@ -122,6 +126,7 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) { currentInputValueRef, dispatch, getFieldDefinition, + hasWildcardOperators, item.key, onCommit, organization, diff --git a/static/app/components/searchQueryBuilder/tokens/freeText.tsx b/static/app/components/searchQueryBuilder/tokens/freeText.tsx index c04dae7e627c65..107772a36f273c 100644 --- a/static/app/components/searchQueryBuilder/tokens/freeText.tsx +++ b/static/app/components/searchQueryBuilder/tokens/freeText.tsx @@ -98,12 +98,13 @@ function replaceFocusedWordWithFilter( value: string, cursorPosition: number, key: string, - getFieldDefinition: FieldDefinitionGetter + getFieldDefinition: FieldDefinitionGetter, + hasWildcardOperators: boolean ) { return replaceFocusedWord( value, cursorPosition, - getInitialFilterText(key, getFieldDefinition(key)) + getInitialFilterText(key, getFieldDefinition(key), hasWildcardOperators) ); } @@ -261,6 +262,9 @@ function SearchQueryBuilderInputInternal({ const hasInputChangeFlows = organization.features.includes( 'search-query-builder-input-flow-changes' ); + const hasWildcardOperators = + organization.features.includes('search-query-builder-wildcard-operators') && + organization.features.includes('search-query-builder-default-to-contains'); const updateSelectionIndex = useCallback(() => { setSelectionIndex(inputRef.current?.selectionStart ?? 0); @@ -471,7 +475,8 @@ function SearchQueryBuilderInputInternal({ inputValue, selectionIndex, value, - getFieldDefinition + getFieldDefinition, + hasWildcardOperators ), focusOverride: calculateNextFocusForFilter( state, @@ -574,7 +579,8 @@ function SearchQueryBuilderInputInternal({ inputValue, selectionIndex, filterValue, - getFieldDefinition + getFieldDefinition, + hasWildcardOperators ), focusOverride: calculateNextFocusForFilter( state, @@ -617,7 +623,8 @@ function SearchQueryBuilderInputInternal({ inputValue, selectionIndex, filterKey, - getFieldDefinition + getFieldDefinition, + hasWildcardOperators ), focusOverride: calculateNextFocusForFilter( state, diff --git a/static/app/components/searchQueryBuilder/tokens/utils.tsx b/static/app/components/searchQueryBuilder/tokens/utils.tsx index 394b7d1578880e..405ad4973752d1 100644 --- a/static/app/components/searchQueryBuilder/tokens/utils.tsx +++ b/static/app/components/searchQueryBuilder/tokens/utils.tsx @@ -7,7 +7,11 @@ import type { SelectOptionOrSectionWithKey, SelectSectionWithKey, } from 'sentry/components/core/compactSelect/types'; -import type {ParseResultToken} from 'sentry/components/searchSyntax/parser'; +import {areWildcardOperatorsAllowed} from 'sentry/components/searchQueryBuilder/tokens/filter/utils'; +import { + WildcardOperators, + type ParseResultToken, +} from 'sentry/components/searchSyntax/parser'; import {defined} from 'sentry/utils'; import {FieldKind, FieldValueType, type FieldDefinition} from 'sentry/utils/fields'; @@ -113,13 +117,17 @@ function getInitialValueType(fieldDefinition: FieldDefinition | null) { export function getInitialFilterText( key: string, - fieldDefinition: FieldDefinition | null + fieldDefinition: FieldDefinition | null, + hasWildcardOperators: boolean ) { const defaultValue = getDefaultFilterValue({fieldDefinition}); const keyText = getInitialFilterKeyText(key, fieldDefinition); const valueType = getInitialValueType(fieldDefinition); + const allowContainsOperator = + hasWildcardOperators && areWildcardOperatorsAllowed(fieldDefinition); + switch (valueType) { case FieldValueType.INTEGER: case FieldValueType.NUMBER: @@ -127,7 +135,11 @@ export function getInitialFilterText( case FieldValueType.SIZE: case FieldValueType.PERCENTAGE: return `${keyText}:>${defaultValue}`; - case FieldValueType.STRING: + case FieldValueType.STRING: { + return allowContainsOperator + ? `${keyText}:${WildcardOperators.CONTAINS}${defaultValue}` + : `${keyText}:${defaultValue}`; + } default: return `${keyText}:${defaultValue}`; }