Skip to content

Commit

Permalink
feat(query-builder): Add alias for the 'is' key (#71946)
Browse files Browse the repository at this point in the history
- Displays `is` as `issue.status` so that we aren't showing `is is
unresolved`. It also describes the key a bit better and places it next
to other similar fields like issue.priority in the dropdown.
- Still accepts `is:` as before if typed manually
- Translates `issue.status` to `is:` if typed manually
  • Loading branch information
malwilley committed Jun 3, 2024
1 parent d495519 commit 0de29cd
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 8 deletions.
6 changes: 5 additions & 1 deletion static/app/components/searchQueryBuilder/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/contex
import {useQueryBuilderGridItem} from 'sentry/components/searchQueryBuilder/useQueryBuilderGridItem';
import {
formatFilterValue,
getKeyLabel,
getValidOpsForFilter,
} from 'sentry/components/searchQueryBuilder/utils';
import {SearchQueryBuilderValueCombobox} from 'sentry/components/searchQueryBuilder/valueCombobox';
Expand Down Expand Up @@ -99,7 +100,10 @@ function FilterOperator({token, state, item}: SearchQueryTokenProps) {
}

function FilterKey({token, state, item}: SearchQueryTokenProps) {
const label = token.key.text;
const {keys} = useSearchQueryBuilder();
const key = token.key.text;
const tag = keys[key];
const label = tag ? getKeyLabel(tag) : key;

const filterButtonProps = useFilterButtonProps({state, item});
// TODO(malwilley): Add edit functionality
Expand Down
33 changes: 33 additions & 0 deletions static/app/components/searchQueryBuilder/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ const MOCK_SUPPORTED_KEYS: TagCollection = {
predefined: true,
values: ['Chrome', 'Firefox', 'Safari', 'Edge'],
},
[FieldKey.IS]: {
key: FieldKey.IS,
name: 'is',
alias: 'status',
predefined: true,
},
custom_tag_name: {key: 'custom_tag_name', name: 'Custom_Tag_Name', kind: FieldKind.TAG},
};

Expand Down Expand Up @@ -101,6 +107,33 @@ describe('SearchQueryBuilder', function () {
});
});

describe('filter key aliases', function () {
it('displays the key alias instead of the actual value', async function () {
render(<SearchQueryBuilder {...defaultProps} initialQuery="is:resolved" />);

expect(
await screen.findByRole('button', {name: 'Edit filter key: status'})
).toBeInTheDocument();
});

it('when adding a filter by typing, replaces aliases tokens', async function () {
const mockOnChange = jest.fn();
render(
<SearchQueryBuilder {...defaultProps} initialQuery="" onChange={mockOnChange} />
);

await userEvent.click(screen.getByRole('grid'));
await userEvent.keyboard('status:');

// Component should display alias `status`
expect(
await screen.findByRole('button', {name: 'Edit filter key: status'})
).toBeInTheDocument();
// Query should use the actual key `is`
expect(mockOnChange).toHaveBeenCalledWith('is:');
});
});

describe('actions', function () {
it('can clear the query', async function () {
const mockOnChange = jest.fn();
Expand Down
44 changes: 37 additions & 7 deletions static/app/components/searchQueryBuilder/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/contex
import type {FocusOverride} from 'sentry/components/searchQueryBuilder/types';
import {useQueryBuilderGridItem} from 'sentry/components/searchQueryBuilder/useQueryBuilderGridItem';
import {replaceTokenWithPadding} from 'sentry/components/searchQueryBuilder/useQueryBuilderState';
import {useShiftFocusToChild} from 'sentry/components/searchQueryBuilder/utils';
import {
getKeyLabel,
useShiftFocusToChild,
} from 'sentry/components/searchQueryBuilder/utils';
import {
type ParseResultToken,
Token,
Expand Down Expand Up @@ -81,6 +84,24 @@ function replaceFocusedWordWithFilter(
return value;
}

/**
* Takes a string that contains a filter value `<key>:` and replaces with any aliases that may exist.
*
* Example:
* replaceAliasedFilterKeys('foo issue: bar', {'status': 'is'}) => 'foo is: bar'
*/
function replaceAliasedFilterKeys(value: string, aliasToKeyMap: Record<string, string>) {
const key = value.match(/(\w+):/);
const matchedKey = key?.[1];
if (matchedKey && aliasToKeyMap[matchedKey]) {
const actualKey = aliasToKeyMap[matchedKey];
const replacedValue = value.replace(`${matchedKey}:`, `${actualKey}:`);
return replacedValue;
}

return value;
}

function getItemsBySection(allKeys: Tag[]) {
const itemsBySection = allKeys.reduce<{
[section: string]: Array<SelectOptionWithKey<string>>;
Expand All @@ -89,7 +110,7 @@ function getItemsBySection(allKeys: Tag[]) {

const section = tag.kind ?? fieldDefinition?.kind ?? t('other');
const item = {
label: tag.key,
label: getKeyLabel(tag),
key: getEscapedKey(tag.key),
value: tag.key,
textValue: tag.key,
Expand Down Expand Up @@ -171,6 +192,12 @@ function KeyDescription({tag}: {tag: Tag}) {
<div>{fieldDefinition.desc}</div>
<Separator />
<DescriptionList>
{tag.alias ? (
<Fragment>
<Term>{t('Alias')}</Term>
<Details>{tag.key}</Details>
</Fragment>
) : null}
{fieldDefinition.valueType ? (
<Fragment>
<Term>{t('Type')}</Term>
Expand Down Expand Up @@ -201,9 +228,13 @@ function SearchQueryBuilderInputInternal({
const filterValue = getWordAtCursorPosition(inputValue, selectionIndex);

const {query, keys, dispatch, onSearch} = useSearchQueryBuilder();

const aliasToKeyMap = useMemo(() => {
return Object.fromEntries(Object.values(keys).map(key => [key.alias, key.key]));
}, [keys]);
const allKeys = useMemo(() => {
return Object.values(keys).sort((a, b) => a.key.localeCompare(b.key));
return Object.values(keys).sort((a, b) =>
getKeyLabel(a).localeCompare(getKeyLabel(b))
);
}, [keys]);
const sections = useMemo(() => getItemsBySection(allKeys), [allKeys]);
const items = useMemo(() => sections.flatMap(section => section.children), [sections]);
Expand Down Expand Up @@ -281,15 +312,14 @@ function SearchQueryBuilderInputInternal({
text: e.target.value,
focusOverride: calculateNextFocusForParen(item),
});
resetInputValue();
return;
}

if (e.target.value.includes(':')) {
dispatch({
type: 'UPDATE_FREE_TEXT',
token,
text: e.target.value,
text: replaceAliasedFilterKeys(e.target.value, aliasToKeyMap),
focusOverride: calculateNextFocusForFilter(state),
});
resetInputValue();
Expand Down Expand Up @@ -372,7 +402,7 @@ const Separator = styled('hr')`
const DescriptionList = styled('dl')`
display: grid;
grid-template-columns: max-content 1fr;
gap: ${space(1.5)};
gap: ${space(0.5)};
margin: 0;
`;

Expand Down
5 changes: 5 additions & 0 deletions static/app/components/searchQueryBuilder/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Token,
type TokenResult,
} from 'sentry/components/searchSyntax/parser';
import type {Tag} from 'sentry/types';
import {escapeDoubleQuotes} from 'sentry/utils';

export const INTERFACE_TYPE_LOCALSTORAGE_KEY = 'search-query-builder-interface';
Expand Down Expand Up @@ -39,6 +40,10 @@ const isSimpleTextToken = (
return [Token.FREE_TEXT, Token.SPACES].includes(token.type);
};

export function getKeyLabel(key: Tag) {
return key.alias ?? key.key;
}

/**
* Collapse adjacent FREE_TEXT and SPACES tokens into a single token.
* This is useful for rendering the minimum number of inputs in the UI.
Expand Down
1 change: 1 addition & 0 deletions static/app/stores/tagStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const storeConfig: TagStoreDefinition = {

const tagCollection = {
[FieldKey.IS]: {
alias: 'issue.status',
key: FieldKey.IS,
name: 'Status',
values: isSuggestions,
Expand Down
1 change: 1 addition & 0 deletions static/app/types/group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export type EventAttachment = IssueAttachment;
export type Tag = {
key: string;
name: string;
alias?: string;

isInput?: boolean;

Expand Down

0 comments on commit 0de29cd

Please sign in to comment.