diff --git a/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx b/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx index 549fc1e952fb..9191002ff351 100644 --- a/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx +++ b/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx @@ -205,6 +205,7 @@ type MultiSelectFilterValueAction = { token: TokenResult; type: 'TOGGLE_FILTER_VALUE'; value: string; + op?: TermOperator; }; type UpdateAggregateArgsAction = { @@ -693,7 +694,8 @@ export function modifyFilterValue( function updateFilterMultipleValues( state: QueryBuilderState, token: TokenResult, - values: string[] + values: string[], + op?: TermOperator ) { // Deduplicate by canonical form while preserving the original text of the // first occurrence (so the query string keeps the user's original formatting) @@ -712,7 +714,7 @@ function updateFilterMultipleValues( return true; }); if (uniqNonEmptyValues.length === 0) { - return {...state, query: replaceQueryToken(state.query, token.value, '""')}; + return {...state, query: modifyFilterValue(state.query, token, '""', op)}; } const newValue = @@ -720,7 +722,7 @@ function updateFilterMultipleValues( ? `[${uniqNonEmptyValues.join(',')}]` : uniqNonEmptyValues[0]!; - return {...state, query: replaceQueryToken(state.query, token.value, newValue)}; + return {...state, query: modifyFilterValue(state.query, token, newValue, op)}; } // Normalizes a filter value so that different surface representations of the @@ -803,10 +805,12 @@ export function multiSelectTokenValue( ); if (!containsValue) { - return updateFilterMultipleValues(state, action.token, [ - ...values.map(({text}) => text), - action.value, - ]); + return updateFilterMultipleValues( + state, + action.token, + [...values.map(({text}) => text), action.value], + action.op + ); } // The selected value was already present, so this is a deselect. Filter it @@ -820,18 +824,18 @@ export function multiSelectTokenValue( } } - return updateFilterMultipleValues(state, action.token, newValues); + return updateFilterMultipleValues(state, action.token, newValues, action.op); } default: { // Single values use the same toggle semantics as lists: if the canonical // value is already selected, clear it; otherwise expand it into a list. if (canonicalizeSearchValue(tokenValue.value ?? '') === normalizedActionValue) { - return updateFilterMultipleValues(state, action.token, ['']); + return updateFilterMultipleValues(state, action.token, [''], action.op); } const newValue = tokenValue.value ? [tokenValue.text, action.value] : [action.value]; - return updateFilterMultipleValues(state, action.token, newValue); + return updateFilterMultipleValues(state, action.token, newValue, action.op); } } } diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 3f918deb5b2c..cdcfff190dd2 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -3831,6 +3831,26 @@ describe('SearchQueryBuilder', () => { expect(optionsAfterToggle).toEqual(initialOptions); }); + it('switches from contains to is when selecting an initial value via checkbox', async () => { + render( + + ); + + await userEvent.click( + screen.getByRole('button', {name: 'Edit value for filter: browser.name'}) + ); + await userEvent.click( + await screen.findByRole('checkbox', {name: 'Toggle Chrome'}) + ); + + expect( + await screen.findByRole('row', {name: 'browser.name:Chrome'}) + ).toBeInTheDocument(); + }); + it('does not reset frozen order when predefined sections rebuild', async () => { render( +): TermOperator | undefined { + if ( + token.operator === TermOperator.CONTAINS && + token.value.type === Token.VALUE_TEXT && + !token.value.value + ) { + return token.negated ? TermOperator.NOT_EQUAL : TermOperator.DEFAULT; + } + + return undefined; +} + function ValueComboboxCustomMenu( props: CustomComboboxMenuProps> ) { @@ -1103,16 +1118,9 @@ export function SearchQueryBuilderValueCombobox({ } // When selecting from dropdown with no existing value, switch from "contains" to "is" - let newOp: TermOperator | undefined; - if ( - token.operator === TermOperator.CONTAINS && - token.value.type === Token.VALUE_TEXT && - !token.value.value - ) { - newOp = token.negated ? TermOperator.NOT_EQUAL : TermOperator.DEFAULT; - } - - updateFilterValue(value, newOp, {escapeSearchValue: true}); + updateFilterValue(value, getOperatorForSelectedValue(token), { + escapeSearchValue: true, + }); trackAnalytics('search.value_autocompleted', { ...analyticsData, filter_value: value, diff --git a/static/app/utils/fields/index.ts b/static/app/utils/fields/index.ts index 8e7cbebf0fd1..a44288980577 100644 --- a/static/app/utils/fields/index.ts +++ b/static/app/utils/fields/index.ts @@ -2712,6 +2712,7 @@ const PREPROD_FIELD_DEFINITIONS: Record = { desc: t('Status of the snapshot in the comparison pipeline'), kind: FieldKind.FIELD, valueType: FieldValueType.STRING, + allowWildcard: false, values: [ 'approved', 'auto_approved',