Browse Source

feat(query-builder): Add disabled prop (#74909)

Malachi Willey 7 months ago
parent
commit
0abc867a5d

+ 2 - 1
static/app/components/input.tsx

@@ -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;

+ 2 - 0
static/app/components/searchQueryBuilder/context.tsx

@@ -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;
@@ -43,4 +44,5 @@ export const SearchQueryBuilerContext = createContext<ContextData>({
   handleSearch: () => {},
   searchSource: '',
   size: 'normal',
+  disabled: false,
 });

+ 14 - 4
static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx

@@ -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,
@@ -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 {
@@ -428,7 +438,7 @@ export function useQueryBuilderState({initialQuery}: {initialQuery: string}) {
           return state;
       }
     },
-    [getFieldDefinition]
+    [disabled, getFieldDefinition]
   );
 
   const [state, dispatch] = useReducer(reducer, initialState);

+ 28 - 0
static/app/components/searchQueryBuilder/index.spec.tsx

@@ -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(

+ 12 - 0
static/app/components/searchQueryBuilder/index.stories.tsx

@@ -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>

+ 15 - 2
static/app/components/searchQueryBuilder/index.tsx

@@ -41,6 +41,7 @@ export interface SearchQueryBuilderProps {
    */
   searchSource: string;
   className?: string;
+  disabled?: boolean;
   /**
    * When true, free text will be marked as invalid.
    */
@@ -88,7 +89,11 @@ export interface SearchQueryBuilderProps {
 }
 
 function ActionButtons() {
-  const {dispatch, handleSearch} = useSearchQueryBuilder();
+  const {dispatch, handleSearch, disabled} = useSearchQueryBuilder();
+
+  if (disabled) {
+    return null;
+  }
 
   return (
     <ButtonsWrapper>
@@ -108,6 +113,7 @@ function ActionButtons() {
 
 export function SearchQueryBuilder({
   className,
+  disabled = false,
   disallowLogicalOperators,
   disallowFreeText,
   disallowUnsupportedFilters,
@@ -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(
     () =>
@@ -172,6 +182,7 @@ export function SearchQueryBuilder({
   const contextValue = useMemo(() => {
     return {
       ...state,
+      disabled,
       parsedQuery,
       filterKeySections: filterKeySections ?? [],
       filterKeys,
@@ -188,6 +199,7 @@ export function SearchQueryBuilder({
     };
   }, [
     state,
+    disabled,
     parsedQuery,
     filterKeySections,
     filterKeys,
@@ -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 ? (

+ 2 - 1
static/app/components/searchQueryBuilder/plainTextQueryInput.tsx

@@ -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);
 
@@ -72,6 +72,7 @@ export function PlainTextQueryInput({label}: PlainTextQueryInputProps) {
         spellCheck={false}
         size={size}
         placeholder={placeholder}
+        disabled={disabled}
       />
     </InputWrapper>
   );

+ 2 - 1
static/app/components/searchQueryBuilder/selectionKeyHandler.tsx

@@ -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)
@@ -175,6 +175,7 @@ export const SelectionKeyHandler = forwardRef(
           tabIndex={-1}
           onPaste={onPaste}
           onKeyDown={onKeyDown}
+          disabled={disabled}
         />
       </VisuallyHidden>
     );

+ 3 - 0
static/app/components/searchQueryBuilder/tokens/combobox.tsx

@@ -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);
@@ -519,6 +520,7 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
     onSelectionChange,
     allowsCustomValue: true,
     disabledKeys,
+    isDisabled: disabled,
   };
 
   const state = useComboBoxState<T>({
@@ -673,6 +675,7 @@ function SearchQueryBuilderComboboxInner<T extends SelectOptionOrSectionWithKey<
         onChange={onInputChange}
         tabIndex={tabIndex}
         onPaste={onPaste}
+        disabled={disabled}
       />
       <StyledPositionWrapper {...overlayProps} visible={isOpen}>
         <OverlayContent

+ 4 - 2
static/app/components/searchQueryBuilder/tokens/filter/filter.tsx

@@ -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);
 
@@ -142,6 +142,7 @@ function FilterValue({token, state, item, filterRef, onActiveChange}: FilterValu
         setIsEditing(true);
         onActiveChange(true);
       }}
+      disabled={disabled}
       {...filterButtonProps}
     >
       <InteractionStateLayer />
@@ -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 />

Some files were not shown because too many files changed in this diff