Skip to content

Commit

Permalink
feat(query-builder): Add disabled prop (#74909)
Browse files Browse the repository at this point in the history
  • Loading branch information
malwilley authored and Christinarlong committed Jul 26, 2024
1 parent ff92134 commit c68d63b
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 12 deletions.
3 changes: 2 additions & 1 deletion static/app/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export const inputStyles = (p: InputStylesProps & {theme: Theme}) => css`
opacity: 1;
}
&[disabled] {
&[disabled],
&[aria-disabled='true'] {
background: ${p.theme.backgroundSecondary};
color: ${p.theme.disabled};
cursor: not-allowed;
Expand Down
2 changes: 2 additions & 0 deletions static/app/components/searchQueryBuilder/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {SavedSearchType, Tag, TagCollection} from 'sentry/types/group';
import type {FieldDefinition} from 'sentry/utils/fields';

interface ContextData {
disabled: boolean;
dispatch: Dispatch<QueryBuilderActions>;
filterKeySections: FilterKeySection[];
filterKeys: TagCollection;
Expand Down Expand Up @@ -43,4 +44,5 @@ export const SearchQueryBuilerContext = createContext<ContextData>({
handleSearch: () => {},
searchSource: '',
size: 'normal',
disabled: false,
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {type Reducer, useCallback, useReducer} from 'react';

import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
import {parseFilterValueDate} from 'sentry/components/searchQueryBuilder/tokens/filter/parsers/date/parser';
import type {
FieldDefinitionGetter,
Expand Down Expand Up @@ -373,12 +372,23 @@ function deleteLastMultiSelectTokenValue(
}
}

export function useQueryBuilderState({initialQuery}: {initialQuery: string}) {
const {getFieldDefinition} = useSearchQueryBuilder();
export function useQueryBuilderState({
initialQuery,
getFieldDefinition,
disabled,
}: {
disabled: boolean;
getFieldDefinition: FieldDefinitionGetter;
initialQuery: string;
}) {
const initialState: QueryBuilderState = {query: initialQuery, focusOverride: null};

const reducer: Reducer<QueryBuilderState, QueryBuilderActions> = useCallback(
(state, action): QueryBuilderState => {
if (disabled) {
return state;
}

switch (action.type) {
case 'CLEAR':
return {
Expand Down Expand Up @@ -428,7 +438,7 @@ export function useQueryBuilderState({initialQuery}: {initialQuery: string}) {
return state;
}
},
[getFieldDefinition]
[disabled, getFieldDefinition]
);

const [state, dispatch] = useReducer(reducer, initialState);
Expand Down
28 changes: 28 additions & 0 deletions static/app/components/searchQueryBuilder/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,34 @@ describe('SearchQueryBuilder', function () {
});
});

describe('disabled', function () {
it('disables all interactable elements', function () {
const mockOnChange = jest.fn();
render(
<SearchQueryBuilder
{...defaultProps}
initialQuery="browser.name:firefox"
onChange={mockOnChange}
disabled
/>
);

expect(getLastInput()).toBeDisabled();
expect(
screen.queryByRole('button', {name: 'Clear search query'})
).not.toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Remove filter: browser.name'})
).toBeDisabled();
expect(
screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
).toBeDisabled();
expect(
screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
).toBeDisabled();
});
});

describe('plain text interface', function () {
beforeEach(() => {
localStorageWrapper.setItem(
Expand Down
12 changes: 12 additions & 0 deletions static/app/components/searchQueryBuilder/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,18 @@ export default storyBook(SearchQueryBuilder, story => {
);
});

story('Disabled', () => {
return (
<SearchQueryBuilder
initialQuery="is:unresolved assigned:me"
filterKeys={FILTER_KEYS}
getTagValues={getTagValues}
searchSource="storybook"
disabled
/>
);
});

story('Migrating from SmartSearchBar', () => {
return (
<Fragment>
Expand Down
17 changes: 15 additions & 2 deletions static/app/components/searchQueryBuilder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface SearchQueryBuilderProps {
*/
searchSource: string;
className?: string;
disabled?: boolean;
/**
* When true, free text will be marked as invalid.
*/
Expand Down Expand Up @@ -88,7 +89,11 @@ export interface SearchQueryBuilderProps {
}

function ActionButtons() {
const {dispatch, handleSearch} = useSearchQueryBuilder();
const {dispatch, handleSearch, disabled} = useSearchQueryBuilder();

if (disabled) {
return null;
}

return (
<ButtonsWrapper>
Expand All @@ -108,6 +113,7 @@ function ActionButtons() {

export function SearchQueryBuilder({
className,
disabled = false,
disallowLogicalOperators,
disallowFreeText,
disallowUnsupportedFilters,
Expand All @@ -128,7 +134,11 @@ export function SearchQueryBuilder({
queryInterface = QueryInterfaceType.TOKENIZED,
}: SearchQueryBuilderProps) {
const wrapperRef = useRef<HTMLDivElement>(null);
const {state, dispatch} = useQueryBuilderState({initialQuery});
const {state, dispatch} = useQueryBuilderState({
initialQuery,
getFieldDefinition: fieldDefinitionGetter,
disabled,
});

const parsedQuery = useMemo(
() =>
Expand Down Expand Up @@ -172,6 +182,7 @@ export function SearchQueryBuilder({
const contextValue = useMemo(() => {
return {
...state,
disabled,
parsedQuery,
filterKeySections: filterKeySections ?? [],
filterKeys,
Expand All @@ -188,6 +199,7 @@ export function SearchQueryBuilder({
};
}, [
state,
disabled,
parsedQuery,
filterKeySections,
filterKeys,
Expand All @@ -209,6 +221,7 @@ export function SearchQueryBuilder({
className={className}
onBlur={() => onBlur?.(state.query)}
ref={wrapperRef}
aria-disabled={disabled}
>
{size !== 'small' && <PositionedSearchIcon size="sm" />}
{!parsedQuery || queryInterface === QueryInterfaceType.TEXT ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface PlainTextQueryInputProps {

export function PlainTextQueryInput({label}: PlainTextQueryInputProps) {
const inputRef = useRef<HTMLTextAreaElement>(null);
const {query, parsedQuery, dispatch, handleSearch, size, placeholder} =
const {query, parsedQuery, dispatch, handleSearch, size, placeholder, disabled} =
useSearchQueryBuilder();
const [cursorPosition, setCursorPosition] = useState(0);

Expand Down Expand Up @@ -72,6 +72,7 @@ export function PlainTextQueryInput({label}: PlainTextQueryInputProps) {
spellCheck={false}
size={size}
placeholder={placeholder}
disabled={disabled}
/>
</InputWrapper>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function findNearestFreeTextKey(
*/
export const SelectionKeyHandler = forwardRef(
({state, undo}: SelectionKeyHandlerProps, ref: ForwardedRef<HTMLInputElement>) => {
const {dispatch} = useSearchQueryBuilder();
const {dispatch, disabled} = useSearchQueryBuilder();

const selectedTokens = Array.from(state.selectionManager.selectedKeys)
.map(key => state.collection.getItem(key)?.value)
Expand Down Expand Up @@ -175,6 +175,7 @@ export const SelectionKeyHandler = forwardRef(
tabIndex={-1}
onPaste={onPaste}
onKeyDown={onKeyDown}
disabled={disabled}
/>
</VisuallyHidden>
);
Expand Down
3 changes: 3 additions & 0 deletions static/app/components/searchQueryBuilder/tokens/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
}: SearchQueryBuilderComboboxProps<T>,
ref: ForwardedRef<HTMLInputElement>
) {
const {disabled} = useSearchQueryBuilder();
const listBoxRef = useRef<HTMLUListElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -519,6 +520,7 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
onSelectionChange,
allowsCustomValue: true,
disabledKeys,
isDisabled: disabled,
};

const state = useComboBoxState<T>({
Expand Down Expand Up @@ -673,6 +675,7 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
onChange={onInputChange}
tabIndex={tabIndex}
onPaste={onPaste}
disabled={disabled}
/>
<StyledPositionWrapper {...overlayProps} visible={isOpen}>
<OverlayContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function FilterValueText({token}: {token: TokenResult<Token.FILTER>}) {

function FilterValue({token, state, item, filterRef, onActiveChange}: FilterValueProps) {
const ref = useRef<HTMLDivElement>(null);
const {dispatch, focusOverride} = useSearchQueryBuilder();
const {dispatch, focusOverride, disabled} = useSearchQueryBuilder();

const [isEditing, setIsEditing] = useState(false);

Expand Down Expand Up @@ -142,6 +142,7 @@ function FilterValue({token, state, item, filterRef, onActiveChange}: FilterValu
setIsEditing(true);
onActiveChange(true);
}}
disabled={disabled}
{...filterButtonProps}
>
<InteractionStateLayer />
Expand All @@ -151,13 +152,14 @@ function FilterValue({token, state, item, filterRef, onActiveChange}: FilterValu
}

function FilterDelete({token, state, item}: SearchQueryTokenProps) {
const {dispatch} = useSearchQueryBuilder();
const {dispatch, disabled} = useSearchQueryBuilder();
const filterButtonProps = useFilterButtonProps({state, item});

return (
<DeleteButton
aria-label={t('Remove filter: %s', token.key.text)}
onClick={() => dispatch({type: 'DELETE_TOKEN', token})}
disabled={disabled}
{...filterButtonProps}
>
<InteractionStateLayer />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,18 @@ export function FilterKeyOperator({
onOpenChange,
}: FilterOperatorProps) {
const organization = useOrganization();
const {dispatch, searchSource, query, savedSearchType} = useSearchQueryBuilder();
const {dispatch, searchSource, query, savedSearchType, disabled} =
useSearchQueryBuilder();
const filterButtonProps = useFilterButtonProps({state, item});

const {operator, label, options} = useMemo(() => getOperatorInfo(token), [token]);

return (
<CompactSelect
disabled={disabled}
trigger={triggerProps => (
<OpButton
disabled={disabled}
aria-label={t('Edit operator for filter: %s', token.key.text)}
{...mergeProps(triggerProps, filterButtonProps)}
>
Expand Down

0 comments on commit c68d63b

Please sign in to comment.