Skip to content
Open
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
2 changes: 2 additions & 0 deletions static/app/components/searchQueryBuilder/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface SearchQueryBuilderContextData {
currentInputValueRef: React.RefObject<string>;
disabled: boolean;
disallowFreeText: boolean;
disallowLogicalOperators: boolean;
disallowWildcard: boolean;
dispatch: Dispatch<QueryBuilderActions>;
displayAskSeer: boolean;
Expand Down Expand Up @@ -204,6 +205,7 @@ export function SearchQueryBuilderProvider({
...state,
disabled,
disallowFreeText: Boolean(disallowFreeText),
disallowLogicalOperators: Boolean(disallowLogicalOperators),
disallowWildcard: Boolean(disallowWildcard),
enableAISearch,
parseQuery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ type UpdateAggregateArgsAction = {
focusOverride?: FocusOverride;
};

type UpdateBooleanOperatorAction = {
type UpdateLogicOperatorAction = {
token: TokenResult<Token.LOGIC_BOOLEAN>;
type: 'UPDATE_BOOLEAN_OPERATOR';
type: 'UPDATE_LOGIC_OPERATOR';
value: string;
};

Expand Down Expand Up @@ -245,7 +245,7 @@ export type QueryBuilderActions =
| UpdateAggregateArgsAction
| MultiSelectFilterValueAction
| ResetClearAskSeerFeedbackAction
| UpdateBooleanOperatorAction;
| UpdateLogicOperatorAction;

function removeQueryTokensFromQuery(
query: string,
Expand Down Expand Up @@ -798,9 +798,9 @@ function updateFreeTextAndReplaceText(
};
}

function updateBooleanOperator(
function updateLogicOperator(
state: QueryBuilderState,
action: UpdateBooleanOperatorAction
action: UpdateLogicOperatorAction
): QueryBuilderState {
const newQuery = replaceQueryToken(state.query, action.token, action.value);
if (newQuery === state.query) {
Expand Down Expand Up @@ -995,8 +995,8 @@ export function useQueryBuilderState({
...state,
query: modifyFilterValue(state.query, action.token, action.value),
};
case 'UPDATE_BOOLEAN_OPERATOR':
return updateBooleanOperator(state, action);
case 'UPDATE_LOGIC_OPERATOR':
return updateLogicOperator(state, action);
case 'UPDATE_AGGREGATE_ARGS':
return updateAggregateArgs(state, action, {getFieldDefinition});
case 'TOGGLE_FILTER_VALUE':
Expand Down
63 changes: 55 additions & 8 deletions static/app/components/searchQueryBuilder/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,9 @@ describe('SearchQueryBuilder', () => {

describe('filter key menu', () => {
it('breaks keys into sections', async () => {
render(<SearchQueryBuilder {...defaultProps} />);
render(<SearchQueryBuilder {...defaultProps} />, {
organization: {features: ['search-query-builder-conditionals-combobox-menus']},
});
await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));

// Should show tab button for each section
Expand Down Expand Up @@ -713,6 +715,29 @@ describe('SearchQueryBuilder', () => {
);
});
});

describe('logic category', () => {
it('does not render logic category when on first input', async () => {
render(<SearchQueryBuilder {...defaultProps} initialQuery="" />, {
organization: {features: ['search-query-builder-conditionals-combobox-menus']},
});

await userEvent.click(getLastInput());
expect(await screen.findByRole('button', {name: 'All'})).toBeInTheDocument();

expect(screen.queryByRole('button', {name: 'Logic'})).not.toBeInTheDocument();
});

it('renders logic category when not on first input', async () => {
render(<SearchQueryBuilder {...defaultProps} initialQuery="span.op:test" />, {
organization: {features: ['search-query-builder-conditionals-combobox-menus']},
});

await userEvent.click(getLastInput());
// Should show conditionals button
expect(await screen.findByRole('button', {name: 'Logic'})).toBeInTheDocument();
});
});
});

describe('mouse interactions', () => {
Expand Down Expand Up @@ -810,9 +835,9 @@ describe('SearchQueryBuilder', () => {
expect(screen.queryByRole('row', {name: '('})).not.toBeInTheDocument();
});

describe('boolean ops', () => {
describe('logic ops', () => {
describe('flag disabled', () => {
it('can remove boolean ops by clicking the delete button', async () => {
it('can remove logic ops by clicking the delete button', async () => {
render(<SearchQueryBuilder {...defaultProps} initialQuery="OR" />);

expect(screen.getByRole('row', {name: 'OR'})).toBeInTheDocument();
Expand All @@ -823,20 +848,22 @@ describe('SearchQueryBuilder', () => {
});

describe('flag enabled', () => {
it('can remove boolean selector by clicking the delete button', async () => {
it('can remove logic selector by clicking the delete button', async () => {
render(<SearchQueryBuilder {...defaultProps} initialQuery="OR" />, {
organization: {
features: ['search-query-builder-add-boolean-operator-select'],
},
});

expect(screen.getByRole('row', {name: 'OR'})).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', {name: 'Remove boolean: OR'}));
await userEvent.click(
screen.getByRole('button', {name: 'Remove logic operator: OR'})
);

expect(screen.queryByRole('row', {name: 'OR'})).not.toBeInTheDocument();
});

it('can select a different boolean operator', async () => {
it('can select a different logic operator', async () => {
render(<SearchQueryBuilder {...defaultProps} initialQuery="OR" />, {
organization: {
features: ['search-query-builder-add-boolean-operator-select'],
Expand All @@ -845,7 +872,7 @@ describe('SearchQueryBuilder', () => {

expect(screen.getByRole('row', {name: 'OR'})).toBeInTheDocument();
await userEvent.click(
screen.getByRole('button', {name: 'Edit boolean operator: OR'})
screen.getByRole('button', {name: 'Edit logic operator: OR'})
);
await userEvent.click(screen.getByRole('option', {name: 'AND'}));

Expand Down Expand Up @@ -1518,7 +1545,7 @@ describe('SearchQueryBuilder', () => {
expect(screen.queryByRole('row', {name: '('})).not.toBeInTheDocument();
});

it('can remove boolean ops with the keyboard', async () => {
it('can remove logic ops with the keyboard', async () => {
render(<SearchQueryBuilder {...defaultProps} initialQuery="and" />);

expect(screen.getByRole('row', {name: 'and'})).toBeInTheDocument();
Expand Down Expand Up @@ -3734,6 +3761,26 @@ describe('SearchQueryBuilder', () => {
await screen.findByText('Parentheses are not supported in this search')
).toBeInTheDocument();
});

it('should not add the conditionals section to filter key menu', async () => {
render(
<SearchQueryBuilder
{...defaultProps}
initialQuery="span.op:test"
disallowLogicalOperators
/>,
{
organization: {features: ['search-query-builder-conditionals-combobox-menus']},
}
);

await userEvent.click(getLastInput());
expect(await screen.findByRole('button', {name: 'All'})).toBeInTheDocument();

await waitFor(() =>
expect(screen.queryByRole('button', {name: 'Logic'})).not.toBeInTheDocument()
);
});
});

describe('disallowWildcard', () => {
Expand Down
10 changes: 5 additions & 5 deletions static/app/components/searchQueryBuilder/tokens/boolean.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function FilterDelete({token, state, item}: SearchQueryBuilderBooleanProps) {

return (
<DeleteButton
aria-label={t('Remove boolean: %s', token.text)}
aria-label={t('Remove logic operator: %s', token.text)}
onClick={() => {
dispatch({type: 'DELETE_TOKEN', token});
}}
Expand All @@ -88,7 +88,7 @@ function FilterDelete({token, state, item}: SearchQueryBuilderBooleanProps) {
);
}

const BOOLEAN_OPERATOR_OPTIONS = [
const LOGIC_OPERATOR_OPTIONS = [
{value: 'AND', label: 'AND'},
{value: 'OR', label: 'OR'},
];
Expand Down Expand Up @@ -152,12 +152,12 @@ function SearchQueryBuilderBooleanSelect({
disabled={disabled}
size="sm"
value={tokenText}
options={BOOLEAN_OPERATOR_OPTIONS}
options={LOGIC_OPERATOR_OPTIONS}
trigger={triggerProps => {
return (
<OpButton
disabled={disabled}
aria-label={t('Edit boolean operator: %s', tokenText)}
aria-label={t('Edit logic operator: %s', tokenText)}
{...mergeProps(triggerProps, filterButtonProps, focusWithinProps)}
>
<InteractionStateLayer />
Expand All @@ -167,7 +167,7 @@ function SearchQueryBuilderBooleanSelect({
}}
onOpenChange={setFilterMenuOpen}
onChange={option => {
dispatch({type: 'UPDATE_BOOLEAN_OPERATOR', token, value: option.value});
dispatch({type: 'UPDATE_LOGIC_OPERATOR', token, value: option.value});
}}
/>
</BaseGridCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ import {KeyDescription} from 'sentry/components/searchQueryBuilder/tokens/filter
import type {Section} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/types';
import {
createRecentFilterOptionKey,
LOGIC_CATEGORY_VALUE,
RECENT_SEARCH_CATEGORY_VALUE,
} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/utils';
import type {Token, TokenResult} from 'sentry/components/searchSyntax/parser';
import {getKeyLabel, getKeyName} from 'sentry/components/searchSyntax/utils';
import {IconMegaphone} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
import usePrevious from 'sentry/utils/usePrevious';

Expand Down Expand Up @@ -152,7 +152,10 @@ function useHighlightFirstOptionOnSectionChange({
state: ComboBoxState<SelectOptionOrSectionWithKey<string>>;
}) {
const displayedListItems = useMemo(() => {
if (selectedSection === RECENT_SEARCH_CATEGORY_VALUE) {
if (
selectedSection === RECENT_SEARCH_CATEGORY_VALUE ||
selectedSection === LOGIC_CATEGORY_VALUE
) {
return [...state.collection].filter(item => !hiddenOptions.has(item.key));
}
const options = state.collection.getChildren?.(selectedSection ?? sections[0]!.value);
Expand All @@ -169,6 +172,7 @@ function useHighlightFirstOptionOnSectionChange({
if (selectedSection === previousSection) {
return;
}

const firstItem = displayedListItems[0];
if (firstItem) {
state.selectionManager.setFocusedKey(firstItem.key);
Expand Down Expand Up @@ -477,7 +481,7 @@ const SectionedOverlayFooter = styled('div')`
display: flex;
align-items: center;
justify-content: flex-end;
padding: ${space(1)};
padding: ${p => p.theme.space.md};
border-top: 1px solid ${p => p.theme.innerBorder};
`;

Expand All @@ -486,8 +490,8 @@ const RecentFiltersPane = styled('ul')`
display: flex;
flex-wrap: wrap;
background: ${p => p.theme.backgroundSecondary};
padding: ${space(1)} 10px;
gap: ${space(0.25)};
padding: ${p => p.theme.space.md} 10px;
gap: ${p => p.theme.space['2xs']};
border-bottom: 1px solid ${p => p.theme.innerBorder};
margin: 0;
`;
Expand All @@ -505,10 +509,10 @@ const DetailsPane = styled('div')`

const SectionedListBoxTabPane = styled('div')`
grid-area: tabs;
padding: ${space(0.5)};
padding: ${p => p.theme.space.xs};
display: flex;
flex-wrap: wrap;
gap: ${space(0.25)};
gap: ${p => p.theme.space['2xs']};
border-bottom: 1px solid ${p => p.theme.innerBorder};
`;

Expand All @@ -519,7 +523,7 @@ const RecentFilterPill = styled('li')`
height: 22px;
font-weight: ${p => p.theme.fontWeight.normal};
font-size: ${p => p.theme.fontSize.md};
padding: 0 ${space(1.5)} 0 ${space(0.75)};
padding: 0 ${p => p.theme.space.lg} 0 ${p => p.theme.space.sm};
background-color: ${p => p.theme.background};
box-shadow: inset 0 0 0 1px ${p => p.theme.innerBorder};
border-radius: ${p => p.theme.borderRadius} 0 0 ${p => p.theme.borderRadius};
Expand All @@ -536,7 +540,7 @@ const RecentFilterPill = styled('li')`
background: linear-gradient(
to left,
${p => p.theme.backgroundSecondary} 0 2px,
transparent ${space(2)} 100%
transparent ${p => p.theme.space.xl} 100%
);
}
`;
Expand All @@ -551,7 +555,7 @@ const SectionButton = styled(Button)`
text-align: left;
font-weight: ${p => p.theme.fontWeight.normal};
font-size: ${p => p.theme.fontSize.sm};
padding: 0 ${space(1.5)};
padding: 0 ${p => p.theme.space.lg};
color: ${p => p.theme.subText};
border: 0;

Expand All @@ -574,7 +578,7 @@ const EmptyState = styled('div')`
align-items: center;
justify-content: center;
height: 100%;
padding: ${space(4)};
padding: ${p => p.theme.space['3xl']};
text-align: center;
color: ${p => p.theme.subText};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@ export interface AskSeerConsentItem extends SelectOptionWithKey<string> {
value: string;
}

export interface LogicFilterItem extends SelectOptionWithKey<string> {
type: 'logic-filter';
value: 'AND' | 'OR' | '(' | ')';
}

export type SearchKeyItem =
| KeySectionItem
| KeyItem
| RawSearchItem
| FilterValueItem
| RawSearchFilterIsValueItem
| AskSeerItem
| AskSeerConsentItem;
| AskSeerConsentItem
| LogicFilterItem;

export type FilterKeyItem =
| KeyItem
Expand All @@ -76,7 +82,8 @@ export type FilterKeyItem =
| FilterValueItem
| RawSearchFilterIsValueItem
| AskSeerItem
| AskSeerConsentItem;
| AskSeerConsentItem
| LogicFilterItem;

export type Section = {
label: ReactNode;
Expand Down
Loading