Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ type MultiSelectFilterValueAction = {
token: TokenResult<Token.FILTER>;
type: 'TOGGLE_FILTER_VALUE';
value: string;
op?: TermOperator;
};

type UpdateAggregateArgsAction = {
Expand Down Expand Up @@ -693,7 +694,8 @@ export function modifyFilterValue(
function updateFilterMultipleValues(
state: QueryBuilderState,
token: TokenResult<Token.FILTER>,
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)
Expand All @@ -712,15 +714,15 @@ 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 =
uniqNonEmptyValues.length > 1
? `[${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
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions static/app/components/searchQueryBuilder/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<SearchQueryBuilder
{...defaultProps}
initialQuery={`browser.name:${WildcardOperators.CONTAINS}""`}
/>
);

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(
<SearchQueryBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ function ItemCheckbox({disabled, value}: {disabled: boolean; value: string}) {
type: 'TOGGLE_FILTER_VALUE',
token,
value: escapedValue,
op: getOperatorForSelectedValue(token),
});

const {selected: currentlySelected, selectedCount} = getMultiSelectValueState(
Expand All @@ -609,6 +610,20 @@ function ItemCheckbox({disabled, value}: {disabled: boolean; value: string}) {
);
}

function getOperatorForSelectedValue(
token: TokenResult<Token.FILTER>
): 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<SelectOptionWithKey<string>>
) {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2712,6 +2712,7 @@ const PREPROD_FIELD_DEFINITIONS: Record<string, FieldDefinition> = {
desc: t('Status of the snapshot in the comparison pipeline'),
kind: FieldKind.FIELD,
valueType: FieldValueType.STRING,
allowWildcard: false,
values: [
'approved',
'auto_approved',
Expand Down
Loading