mirror of
				https://github.com/Azure/cosmos-explorer.git
				synced 2025-10-30 22:50:32 +00:00 
			
		
		
		
	Improve DocumentsTab filter input (#1998)
* Rework Input and dropdown in DocumentsTab * Improve input: implement Escape and add clear button * Undo body :focus outline, since fluent UI has a nicer focus style * Close dropdown if last element is tabbed * Fix unit tests * Fix theme and remove autocomplete * Load theme inside rendering function to fix using correct colors * Remove commented code * Add aria-label to clear filter button * Fix format * Fix keyboard navigation with tab and arrow up/down. Clear button becomes down button. --------- Co-authored-by: Laurent Nguyen <languye@microsoft.com>
This commit is contained in:
		
							parent
							
								
									056be2a74d
								
							
						
					
					
						commit
						d42eebaa5a
					
				
							
								
								
									
										314
									
								
								src/Explorer/Controls/InputDataList/InputDataList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								src/Explorer/Controls/InputDataList/InputDataList.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,314 @@ | |||||||
|  | // This component is used to create a dropdown list of options for the user to select from.
 | ||||||
|  | // The options are displayed in a dropdown list when the user clicks on the input field.
 | ||||||
|  | // The user can then select an option from the list. The selected option is then displayed in the input field.
 | ||||||
|  | 
 | ||||||
|  | import { getTheme } from "@fluentui/react"; | ||||||
|  | import { | ||||||
|  |   Button, | ||||||
|  |   Divider, | ||||||
|  |   Input, | ||||||
|  |   Link, | ||||||
|  |   makeStyles, | ||||||
|  |   Popover, | ||||||
|  |   PopoverProps, | ||||||
|  |   PopoverSurface, | ||||||
|  |   PositioningImperativeRef, | ||||||
|  | } from "@fluentui/react-components"; | ||||||
|  | import { ArrowDownRegular, DismissRegular } from "@fluentui/react-icons"; | ||||||
|  | import { NormalizedEventKey } from "Common/Constants"; | ||||||
|  | import { tokens } from "Explorer/Theme/ThemeUtil"; | ||||||
|  | import React, { FC, useEffect, useRef } from "react"; | ||||||
|  | 
 | ||||||
|  | const useStyles = makeStyles({ | ||||||
|  |   container: { | ||||||
|  |     padding: 0, | ||||||
|  |   }, | ||||||
|  |   input: { | ||||||
|  |     flexGrow: 1, | ||||||
|  |     paddingRight: 0, | ||||||
|  |     outline: "none", | ||||||
|  |     "& input:focus": { | ||||||
|  |       outline: "none", // Undo body :focus dashed outline
 | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   inputButton: { | ||||||
|  |     border: 0, | ||||||
|  |   }, | ||||||
|  |   dropdownHeader: { | ||||||
|  |     width: "100%", | ||||||
|  |     fontSize: tokens.fontSizeBase300, | ||||||
|  |     fontWeight: 600, | ||||||
|  |     padding: `${tokens.spacingVerticalM} 0 0 ${tokens.spacingVerticalM}`, | ||||||
|  |   }, | ||||||
|  |   dropdownStack: { | ||||||
|  |     display: "flex", | ||||||
|  |     flexDirection: "column", | ||||||
|  |     gap: tokens.spacingVerticalS, | ||||||
|  |     marginTop: tokens.spacingVerticalS, | ||||||
|  |     marginBottom: "1px", | ||||||
|  |   }, | ||||||
|  |   dropdownOption: { | ||||||
|  |     fontSize: tokens.fontSizeBase300, | ||||||
|  |     fontWeight: 400, | ||||||
|  |     justifyContent: "left", | ||||||
|  |     padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`, | ||||||
|  |     overflow: "hidden", | ||||||
|  |     whiteSpace: "nowrap", | ||||||
|  |     textOverflow: "ellipsis", | ||||||
|  |     border: 0, | ||||||
|  |     ":hover": { | ||||||
|  |       outline: `1px dashed ${tokens.colorNeutralForeground1Hover}`, | ||||||
|  |       backgroundColor: tokens.colorNeutralBackground2Hover, | ||||||
|  |       color: tokens.colorNeutralForeground1, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   bottomSection: { | ||||||
|  |     fontSize: tokens.fontSizeBase300, | ||||||
|  |     fontWeight: 400, | ||||||
|  |     padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`, | ||||||
|  |     overflow: "hidden", | ||||||
|  |     whiteSpace: "nowrap", | ||||||
|  |     textOverflow: "ellipsis", | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export interface InputDatalistDropdownOptionSection { | ||||||
|  |   label: string; | ||||||
|  |   options: string[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface InputDataListProps { | ||||||
|  |   dropdownOptions: InputDatalistDropdownOptionSection[]; | ||||||
|  |   placeholder?: string; | ||||||
|  |   title?: string; | ||||||
|  |   value: string; | ||||||
|  |   onChange: (value: string) => void; | ||||||
|  |   onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void; | ||||||
|  |   autofocus?: boolean; // true: acquire focus on first render
 | ||||||
|  |   bottomLink?: { | ||||||
|  |     text: string; | ||||||
|  |     url: string; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const InputDataList: FC<InputDataListProps> = ({ | ||||||
|  |   dropdownOptions, | ||||||
|  |   placeholder, | ||||||
|  |   title, | ||||||
|  |   value, | ||||||
|  |   onChange, | ||||||
|  |   onKeyDown, | ||||||
|  |   autofocus, | ||||||
|  |   bottomLink, | ||||||
|  | }) => { | ||||||
|  |   const styles = useStyles(); | ||||||
|  |   const [showDropdown, setShowDropdown] = React.useState(false); | ||||||
|  |   const inputRef = useRef<HTMLInputElement>(null); | ||||||
|  |   const positioningRef = React.useRef<PositioningImperativeRef>(null); | ||||||
|  |   const [isInputFocused, setIsInputFocused] = React.useState(autofocus); | ||||||
|  |   const [autofocusFirstDropdownItem, setAutofocusFirstDropdownItem] = React.useState(false); | ||||||
|  | 
 | ||||||
|  |   const theme = getTheme(); | ||||||
|  |   const itemRefs = useRef([]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (inputRef.current) { | ||||||
|  |       positioningRef.current?.setTarget(inputRef.current); | ||||||
|  |     } | ||||||
|  |   }, [inputRef, positioningRef]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (isInputFocused) { | ||||||
|  |       inputRef.current?.focus(); | ||||||
|  |     } | ||||||
|  |   }, [isInputFocused]); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (autofocusFirstDropdownItem && showDropdown) { | ||||||
|  |       // Autofocus on first item if input isn't focused
 | ||||||
|  |       itemRefs.current[0]?.focus(); | ||||||
|  |       setAutofocusFirstDropdownItem(false); | ||||||
|  |     } | ||||||
|  |   }, [autofocusFirstDropdownItem, showDropdown]); | ||||||
|  | 
 | ||||||
|  |   const handleOpenChange: PopoverProps["onOpenChange"] = (e, data) => { | ||||||
|  |     if (isInputFocused && !data.open) { | ||||||
|  |       // Don't close if input is focused and we're opening the dropdown (which will steal the focus)
 | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     setShowDropdown(data.open || false); | ||||||
|  |     if (data.open) { | ||||||
|  |       setIsInputFocused(true); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||||||
|  |     if (e.key === NormalizedEventKey.Escape) { | ||||||
|  |       setShowDropdown(false); | ||||||
|  |     } else if (e.key === NormalizedEventKey.DownArrow) { | ||||||
|  |       setShowDropdown(true); | ||||||
|  |       setAutofocusFirstDropdownItem(true); | ||||||
|  |     } | ||||||
|  |     onKeyDown(e); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleDownDropdownItemKeyDown = ( | ||||||
|  |     e: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>, | ||||||
|  |     index: number, | ||||||
|  |   ) => { | ||||||
|  |     if (e.key === NormalizedEventKey.Enter) { | ||||||
|  |       e.currentTarget.click(); | ||||||
|  |     } else if (e.key === NormalizedEventKey.Escape) { | ||||||
|  |       setShowDropdown(false); | ||||||
|  |       inputRef.current?.focus(); | ||||||
|  |     } else if (e.key === NormalizedEventKey.DownArrow) { | ||||||
|  |       if (index + 1 < itemRefs.current.length) { | ||||||
|  |         itemRefs.current[index + 1].focus(); | ||||||
|  |       } else { | ||||||
|  |         setIsInputFocused(true); | ||||||
|  |       } | ||||||
|  |     } else if (e.key === NormalizedEventKey.UpArrow) { | ||||||
|  |       if (index - 1 >= 0) { | ||||||
|  |         itemRefs.current[index - 1].focus(); | ||||||
|  |       } else { | ||||||
|  |         // Last item, focus back to input
 | ||||||
|  |         setIsInputFocused(true); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // Flatten dropdownOptions to better manage refs and focus
 | ||||||
|  |   let flatIndex = 0; | ||||||
|  |   const indexMap = new Map<string, number>(); | ||||||
|  |   for (let sectionIndex = 0; sectionIndex < dropdownOptions.length; sectionIndex++) { | ||||||
|  |     const section = dropdownOptions[sectionIndex]; | ||||||
|  |     for (let optionIndex = 0; optionIndex < section.options.length; optionIndex++) { | ||||||
|  |       indexMap.set(`${sectionIndex}-${optionIndex}`, flatIndex); | ||||||
|  |       flatIndex++; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Input | ||||||
|  |         id="filterInput" | ||||||
|  |         ref={inputRef} | ||||||
|  |         type="text" | ||||||
|  |         size="small" | ||||||
|  |         autoComplete="off" | ||||||
|  |         className={`filterInput ${styles.input}`} | ||||||
|  |         title={title} | ||||||
|  |         placeholder={placeholder} | ||||||
|  |         value={value} | ||||||
|  |         autoFocus | ||||||
|  |         onKeyDown={handleInputKeyDown} | ||||||
|  |         onChange={(e) => { | ||||||
|  |           const newValue = e.target.value; | ||||||
|  |           // Don't show dropdown if there is already a value in the input field (when user is typing)
 | ||||||
|  |           setShowDropdown(!(newValue.length > 0)); | ||||||
|  |           onChange(newValue); | ||||||
|  |         }} | ||||||
|  |         onClick={(e) => { | ||||||
|  |           e.stopPropagation(); | ||||||
|  |         }} | ||||||
|  |         onFocus={() => { | ||||||
|  |           // Don't show dropdown if there is already a value in the input field
 | ||||||
|  |           // or isInputFocused is undefined which means component is mounting
 | ||||||
|  |           setShowDropdown(!(value.length > 0) && isInputFocused !== undefined); | ||||||
|  | 
 | ||||||
|  |           setIsInputFocused(true); | ||||||
|  |         }} | ||||||
|  |         onBlur={() => { | ||||||
|  |           setIsInputFocused(false); | ||||||
|  |         }} | ||||||
|  |         contentAfter={ | ||||||
|  |           value.length > 0 ? ( | ||||||
|  |             <Button | ||||||
|  |               aria-label="Clear filter" | ||||||
|  |               className={styles.inputButton} | ||||||
|  |               size="small" | ||||||
|  |               icon={<DismissRegular />} | ||||||
|  |               onClick={() => { | ||||||
|  |                 onChange(""); | ||||||
|  |                 setIsInputFocused(true); | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |           ) : ( | ||||||
|  |             <Button | ||||||
|  |               aria-label="Open dropdown" | ||||||
|  |               className={styles.inputButton} | ||||||
|  |               size="small" | ||||||
|  |               icon={<ArrowDownRegular />} | ||||||
|  |               onClick={() => { | ||||||
|  |                 setShowDropdown(true); | ||||||
|  |                 setAutofocusFirstDropdownItem(true); | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |           ) | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|  |       <Popover | ||||||
|  |         inline | ||||||
|  |         unstable_disableAutoFocus | ||||||
|  |         // trapFocus
 | ||||||
|  |         open={showDropdown} | ||||||
|  |         onOpenChange={handleOpenChange} | ||||||
|  |         positioning={{ positioningRef, position: "below", align: "start", offset: 4 }} | ||||||
|  |       > | ||||||
|  |         <PopoverSurface className={styles.container}> | ||||||
|  |           {dropdownOptions.map((section, sectionIndex) => ( | ||||||
|  |             <div key={section.label}> | ||||||
|  |               <div className={styles.dropdownHeader} style={{ color: theme.palette.themePrimary }}> | ||||||
|  |                 {section.label} | ||||||
|  |               </div> | ||||||
|  |               <div className={styles.dropdownStack}> | ||||||
|  |                 {section.options.map((option, index) => ( | ||||||
|  |                   <Button | ||||||
|  |                     key={option} | ||||||
|  |                     ref={(el) => (itemRefs.current[indexMap.get(`${sectionIndex}-${index}`)] = el)} | ||||||
|  |                     appearance="transparent" | ||||||
|  |                     shape="square" | ||||||
|  |                     className={styles.dropdownOption} | ||||||
|  |                     onClick={() => { | ||||||
|  |                       onChange(option); | ||||||
|  |                       setShowDropdown(false); | ||||||
|  |                       setIsInputFocused(true); | ||||||
|  |                     }} | ||||||
|  |                     onBlur={() => | ||||||
|  |                       !bottomLink && | ||||||
|  |                       sectionIndex === dropdownOptions.length - 1 && | ||||||
|  |                       index === section.options.length - 1 && | ||||||
|  |                       setShowDropdown(false) | ||||||
|  |                     } | ||||||
|  |                     onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => | ||||||
|  |                       handleDownDropdownItemKeyDown(e, indexMap.get(`${sectionIndex}-${index}`)) | ||||||
|  |                     } | ||||||
|  |                   > | ||||||
|  |                     {option} | ||||||
|  |                   </Button> | ||||||
|  |                 ))} | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           ))} | ||||||
|  |           {bottomLink && ( | ||||||
|  |             <> | ||||||
|  |               <Divider /> | ||||||
|  |               <div className={styles.bottomSection}> | ||||||
|  |                 <Link | ||||||
|  |                   ref={(el) => (itemRefs.current[flatIndex] = el)} | ||||||
|  |                   href={bottomLink.url} | ||||||
|  |                   target="_blank" | ||||||
|  |                   onBlur={() => setShowDropdown(false)} | ||||||
|  |                   onKeyDown={(e: React.KeyboardEvent<HTMLAnchorElement>) => handleDownDropdownItemKeyDown(e, flatIndex)} | ||||||
|  |                 > | ||||||
|  |                   {bottomLink.text} | ||||||
|  |                 </Link> | ||||||
|  |               </div> | ||||||
|  |             </> | ||||||
|  |           )} | ||||||
|  |         </PopoverSurface> | ||||||
|  |       </Popover> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @ -385,22 +385,6 @@ describe("Documents tab (noSql API)", () => { | |||||||
|     it("should render the page", () => { |     it("should render the page", () => { | ||||||
|       expect(wrapper).toMatchSnapshot(); |       expect(wrapper).toMatchSnapshot(); | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     it("clicking on Edit filter should render the Apply Filter button", () => { |  | ||||||
|       wrapper |  | ||||||
|         .findWhere((node) => node.text() === "Edit Filter") |  | ||||||
|         .at(0) |  | ||||||
|         .simulate("click"); |  | ||||||
|       expect(wrapper.findWhere((node) => node.text() === "Apply Filter").exists()).toBeTruthy(); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it("clicking on Edit filter should render input for filter", () => { |  | ||||||
|       wrapper |  | ||||||
|         .findWhere((node) => node.text() === "Edit Filter") |  | ||||||
|         .at(0) |  | ||||||
|         .simulate("click"); |  | ||||||
|       expect(wrapper.find("Input.filterInput").exists()).toBeTruthy(); |  | ||||||
|     }); |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe("Command bar buttons", () => { |   describe("Command bar buttons", () => { | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; | import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; | ||||||
| import { | import { | ||||||
|   Button, |   Button, | ||||||
|   Input, |  | ||||||
|   Link, |   Link, | ||||||
|   MessageBar, |   MessageBar, | ||||||
|   MessageBarBody, |   MessageBarBody, | ||||||
| @ -10,8 +9,7 @@ import { | |||||||
|   makeStyles, |   makeStyles, | ||||||
|   shorthands, |   shorthands, | ||||||
| } from "@fluentui/react-components"; | } from "@fluentui/react-components"; | ||||||
| import { Dismiss16Filled } from "@fluentui/react-icons"; | import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; | ||||||
| import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; |  | ||||||
| import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; | import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; | ||||||
| import MongoUtility from "Common/MongoUtility"; | import MongoUtility from "Common/MongoUtility"; | ||||||
| import { createDocument } from "Common/dataAccess/createDocument"; | import { createDocument } from "Common/dataAccess/createDocument"; | ||||||
| @ -26,6 +24,7 @@ import { Platform, configContext } from "ConfigContext"; | |||||||
| import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; | import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; | ||||||
| import { useDialog } from "Explorer/Controls/Dialog"; | import { useDialog } from "Explorer/Controls/Dialog"; | ||||||
| import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; | import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; | ||||||
|  | import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList"; | ||||||
| import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; | import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; | ||||||
| import Explorer from "Explorer/Explorer"; | import Explorer from "Explorer/Explorer"; | ||||||
| import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; | import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; | ||||||
| @ -74,6 +73,7 @@ const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we | |||||||
| const NO_SQL_THROTTLING_DOC_URL = | const NO_SQL_THROTTLING_DOC_URL = | ||||||
|   "https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large"; |   "https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large"; | ||||||
| const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors"; | const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors"; | ||||||
|  | const DATA_EXPLORER_DOC_URL = "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer"; | ||||||
| 
 | 
 | ||||||
| const loadMoreHeight = LayoutConstants.rowHeight; | const loadMoreHeight = LayoutConstants.rowHeight; | ||||||
| export const useDocumentsTabStyles = makeStyles({ | export const useDocumentsTabStyles = makeStyles({ | ||||||
| @ -90,12 +90,6 @@ export const useDocumentsTabStyles = makeStyles({ | |||||||
|     alignItems: "center", |     alignItems: "center", | ||||||
|     ...cosmosShorthands.borderBottom(), |     ...cosmosShorthands.borderBottom(), | ||||||
|   }, |   }, | ||||||
|   filterInput: { |  | ||||||
|     flexGrow: 1, |  | ||||||
|   }, |  | ||||||
|   appliedFilter: { |  | ||||||
|     flexGrow: 1, |  | ||||||
|   }, |  | ||||||
|   tableContainer: { |   tableContainer: { | ||||||
|     marginRight: tokens.spacingHorizontalXXXL, |     marginRight: tokens.spacingHorizontalXXXL, | ||||||
|   }, |   }, | ||||||
| @ -556,8 +550,6 @@ export interface IDocumentsTabComponentProps { | |||||||
|   isTabActive: boolean; |   isTabActive: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`; |  | ||||||
| 
 |  | ||||||
| const getDefaultSqlFilters = (partitionKeys: string[]) => | const getDefaultSqlFilters = (partitionKeys: string[]) => | ||||||
|   ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat( |   ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat( | ||||||
|     partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`), |     partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`), | ||||||
| @ -583,14 +575,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|   onIsExecutingChange, |   onIsExecutingChange, | ||||||
|   isTabActive, |   isTabActive, | ||||||
| }): JSX.Element => { | }): JSX.Element => { | ||||||
|   const [isFilterCreated, setIsFilterCreated] = useState<boolean>(true); |  | ||||||
|   const [isFilterExpanded, setIsFilterExpanded] = useState<boolean>(false); |  | ||||||
|   const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false); |  | ||||||
|   const [appliedFilter, setAppliedFilter] = useState<string>(""); |  | ||||||
|   const [filterContent, setFilterContent] = useState<string>(""); |   const [filterContent, setFilterContent] = useState<string>(""); | ||||||
|   const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]); |   const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]); | ||||||
|   const [isExecuting, setIsExecuting] = useState<boolean>(false); |   const [isExecuting, setIsExecuting] = useState<boolean>(false); | ||||||
|   const filterInput = useRef<HTMLInputElement>(null); |  | ||||||
|   const styles = useDocumentsTabStyles(); |   const styles = useDocumentsTabStyles(); | ||||||
| 
 | 
 | ||||||
|   // Query
 |   // Query
 | ||||||
| @ -657,12 +644,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
| 
 | 
 | ||||||
|   const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); |   const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |  | ||||||
|     if (isFilterFocused) { |  | ||||||
|       filterInput.current?.focus(); |  | ||||||
|     } |  | ||||||
|   }, [isFilterFocused]); |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * Recursively delete all documents by retrying throttled requests (429). |    * Recursively delete all documents by retrying throttled requests (429). | ||||||
|    * This only works for NoSQL, because the bulk response includes status for each delete document request. |    * This only works for NoSQL, because the bulk response includes status for each delete document request. | ||||||
| @ -756,11 +737,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|     }, timeout); |     }, timeout); | ||||||
|   }, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]); |   }, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]); | ||||||
| 
 | 
 | ||||||
|   const applyFilterButton = { |  | ||||||
|     enabled: true, |  | ||||||
|     visible: true, |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const partitionKey: DataModels.PartitionKey = useMemo( |   const partitionKey: DataModels.PartitionKey = useMemo( | ||||||
|     () => _partitionKey || (_collection && _collection.partitionKey), |     () => _partitionKey || (_collection && _collection.partitionKey), | ||||||
|     [_collection, _partitionKey], |     [_collection, _partitionKey], | ||||||
| @ -831,10 +807,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|   // This is executed in onActivate() in the original code.
 |   // This is executed in onActivate() in the original code.
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     setKeyboardActions({ |     setKeyboardActions({ | ||||||
|       [KeyboardAction.SEARCH]: () => { |  | ||||||
|         onShowFilterClick(); |  | ||||||
|         return true; |  | ||||||
|       }, |  | ||||||
|       [KeyboardAction.CLEAR_SEARCH]: () => { |       [KeyboardAction.CLEAR_SEARCH]: () => { | ||||||
|         setFilterContent(""); |         setFilterContent(""); | ||||||
|         refreshDocumentsGrid(true); |         refreshDocumentsGrid(true); | ||||||
| @ -1317,12 +1289,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|     ], |     ], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   const onShowFilterClick = () => { |  | ||||||
|     setIsFilterCreated(true); |  | ||||||
|     setIsFilterExpanded(true); |  | ||||||
|     setIsFilterFocused(true); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const queryTimeoutEnabled = useCallback( |   const queryTimeoutEnabled = useCallback( | ||||||
|     (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), |     (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), | ||||||
|     [isPreferredApiMongoDB], |     [isPreferredApiMongoDB], | ||||||
| @ -1364,19 +1330,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|     selectedColumnIds, |     selectedColumnIds, | ||||||
|   ]); |   ]); | ||||||
| 
 | 
 | ||||||
|   const onHideFilterClick = (): void => { |  | ||||||
|     setIsFilterExpanded(false); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const onCloseButtonKeyDown: KeyboardEventHandler<HTMLSpanElement> = (event) => { |  | ||||||
|     if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { |  | ||||||
|       onHideFilterClick(); |  | ||||||
|       event.stopPropagation(); |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => { |   const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => { | ||||||
|     setDocumentIds(newDocumentsIds); |     setDocumentIds(newDocumentsIds); | ||||||
| 
 | 
 | ||||||
| @ -1518,14 +1471,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => { |   const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => { | ||||||
|     if (e.key === "Enter") { |     if (e.key === Constants.NormalizedEventKey.Enter) { | ||||||
|       onApplyFilterClick(); |       onApplyFilterClick(); | ||||||
| 
 | 
 | ||||||
|       // Suppress the default behavior of the key
 |  | ||||||
|       e.preventDefault(); |  | ||||||
|     } else if (e.key === "Escape") { |  | ||||||
|       onHideFilterClick(); |  | ||||||
| 
 |  | ||||||
|       // Suppress the default behavior of the key
 |       // Suppress the default behavior of the key
 | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|     } |     } | ||||||
| @ -2023,10 +1971,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|           applyFilterButtonPressed, |           applyFilterButtonPressed, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         // collapse filter
 |  | ||||||
|         setAppliedFilter(filterContent); |  | ||||||
|         setIsFilterExpanded(false); |  | ||||||
| 
 |  | ||||||
|         // If apply filter is pressed, reset current selected document
 |         // If apply filter is pressed, reset current selected document
 | ||||||
|         if (applyFilterButtonPressed) { |         if (applyFilterButtonPressed) { | ||||||
|           setClickedRowIndex(RESET_INDEX); |           setClickedRowIndex(RESET_INDEX); | ||||||
| @ -2103,97 +2047,59 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|     (partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled); |     (partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled); | ||||||
|   //  -------------------------------------------------------
 |   //  -------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|  |   const getFilterChoices = (): InputDatalistDropdownOptionSection[] => { | ||||||
|  |     const options: InputDatalistDropdownOptionSection[] = []; | ||||||
|  |     const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== ""); | ||||||
|  |     if (nonBlankLastFilters.length > 0) { | ||||||
|  |       options.push({ | ||||||
|  |         label: "Saved filters", | ||||||
|  |         options: nonBlankLastFilters, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     options.push({ | ||||||
|  |       label: "Default filters", | ||||||
|  |       options: isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties), | ||||||
|  |     }); | ||||||
|  |     return options; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <CosmosFluentProvider className={styles.container}> |     <CosmosFluentProvider className={styles.container}> | ||||||
|       <div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}> |       <div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}> | ||||||
|         {isFilterCreated && ( |  | ||||||
|           <> |  | ||||||
|             {!isFilterExpanded && !isPreferredApiMongoDB && ( |  | ||||||
|               <div className={styles.filterRow}> |  | ||||||
|                 <span>SELECT * FROM c</span> |  | ||||||
|                 <span className={styles.appliedFilter}>{appliedFilter}</span> |  | ||||||
|                 <Button appearance="primary" size="small" onClick={onShowFilterClick}> |  | ||||||
|                   Edit Filter |  | ||||||
|                 </Button> |  | ||||||
|               </div> |  | ||||||
|             )} |  | ||||||
|             {!isFilterExpanded && isPreferredApiMongoDB && ( |  | ||||||
|               <div className={styles.filterRow}> |  | ||||||
|                 {appliedFilter.length > 0 && <span>Filter :</span>} |  | ||||||
|                 {!(appliedFilter.length > 0) && <span className="noFilterApplied">No filter applied</span>} |  | ||||||
|                 <span className={styles.appliedFilter}>{appliedFilter}</span> |  | ||||||
|                 <Button appearance="primary" size="small" onClick={onShowFilterClick}> |  | ||||||
|                   Edit Filter |  | ||||||
|                 </Button> |  | ||||||
|               </div> |  | ||||||
|             )} |  | ||||||
|             {isFilterExpanded && ( |  | ||||||
|         <div className={styles.filterRow}> |         <div className={styles.filterRow}> | ||||||
|           {!isPreferredApiMongoDB && <span> SELECT * FROM c </span>} |           {!isPreferredApiMongoDB && <span> SELECT * FROM c </span>} | ||||||
|                 <Input |           <InputDataList | ||||||
|                   ref={filterInput} |             dropdownOptions={getFilterChoices()} | ||||||
|                   type="text" |  | ||||||
|                   size="small" |  | ||||||
|                   list={`filtersList-${getUniqueId(_collection)}`} |  | ||||||
|                   className={`filterInput ${styles.filterInput}`} |  | ||||||
|                   title="Type a query predicate or choose one from the list." |  | ||||||
|             placeholder={ |             placeholder={ | ||||||
|               isPreferredApiMongoDB |               isPreferredApiMongoDB | ||||||
|                 ? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents." |                 ? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents." | ||||||
|                 : "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents." |                 : "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents." | ||||||
|             } |             } | ||||||
|  |             title="Type a query predicate or choose one from the list." | ||||||
|             value={filterContent} |             value={filterContent} | ||||||
|                   autoFocus={true} |             onChange={(value) => setFilterContent(value)} | ||||||
|             onKeyDown={onFilterKeyDown} |             onKeyDown={onFilterKeyDown} | ||||||
|                   onChange={(e) => setFilterContent(e.target.value)} |             bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }} | ||||||
|                   onBlur={() => setIsFilterFocused(false)} |  | ||||||
|           /> |           /> | ||||||
| 
 |  | ||||||
|                 <datalist id={`filtersList-${getUniqueId(_collection)}`}> |  | ||||||
|                   {addStringsNoDuplicate( |  | ||||||
|                     lastFilterContents, |  | ||||||
|                     isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties), |  | ||||||
|                   ).map((filter) => ( |  | ||||||
|                     <option key={filter} value={filter} /> |  | ||||||
|                   ))} |  | ||||||
|                 </datalist> |  | ||||||
| 
 |  | ||||||
|           <Button |           <Button | ||||||
|             appearance="primary" |             appearance="primary" | ||||||
|             size="small" |             size="small" | ||||||
|                   onClick={onApplyFilterClick} |             onClick={() => { | ||||||
|                   disabled={!applyFilterButton.enabled} |               if (isExecuting) { | ||||||
|                   aria-label="Apply filter" |                 if (!isPreferredApiMongoDB) { | ||||||
|  |                   queryAbortController.abort(); | ||||||
|  |                 } | ||||||
|  |               } else { | ||||||
|  |                 onApplyFilterClick(); | ||||||
|  |               } | ||||||
|  |             }} | ||||||
|  |             disabled={isExecuting && isPreferredApiMongoDB} | ||||||
|  |             aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"} | ||||||
|             tabIndex={0} |             tabIndex={0} | ||||||
|           > |           > | ||||||
|                   Apply Filter |             {!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"} | ||||||
|           </Button> |           </Button> | ||||||
|                 {!isPreferredApiMongoDB && isExecuting && ( |  | ||||||
|                   <Button |  | ||||||
|                     appearance="primary" |  | ||||||
|                     size="small" |  | ||||||
|                     aria-label="Cancel Query" |  | ||||||
|                     onClick={() => queryAbortController.abort()} |  | ||||||
|                     tabIndex={0} |  | ||||||
|                   > |  | ||||||
|                     Cancel Query |  | ||||||
|                   </Button> |  | ||||||
|                 )} |  | ||||||
|                 <Button |  | ||||||
|                   aria-label="close filter" |  | ||||||
|                   tabIndex={0} |  | ||||||
|                   onClick={onHideFilterClick} |  | ||||||
|                   onKeyDown={onCloseButtonKeyDown} |  | ||||||
|                   appearance="transparent" |  | ||||||
|                   size="small" |  | ||||||
|                   icon={<Dismiss16Filled />} |  | ||||||
|                 /> |  | ||||||
|         </div> |         </div> | ||||||
|             )} |  | ||||||
|           </> |  | ||||||
|         )} |  | ||||||
|         {/* <Split> doesn't like to be a flex child */} |  | ||||||
|         <div style={{ overflow: "hidden", height: "100%" }}> |  | ||||||
|         <Allotment |         <Allotment | ||||||
|           onDragEnd={(sizes: number[]) => { |           onDragEnd={(sizes: number[]) => { | ||||||
|             tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); |             tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); | ||||||
| @ -2265,7 +2171,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone | |||||||
|           </Allotment.Pane> |           </Allotment.Pane> | ||||||
|         </Allotment> |         </Allotment> | ||||||
|       </div> |       </div> | ||||||
|       </div> |  | ||||||
|       {bulkDeleteOperation && ( |       {bulkDeleteOperation && ( | ||||||
|         <ProgressModalDialog |         <ProgressModalDialog | ||||||
|           isOpen={isBulkDeleteDialogOpen} |           isOpen={isBulkDeleteDialogOpen} | ||||||
|  | |||||||
| @ -19,25 +19,44 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` | |||||||
|       <span> |       <span> | ||||||
|          SELECT * FROM c  |          SELECT * FROM c  | ||||||
|       </span> |       </span> | ||||||
|       <span |       <InputDataList | ||||||
|         className="___r7kt3y0_0000000 fqerorx" |         bottomLink={ | ||||||
|  |           { | ||||||
|  |             "text": "Learn more", | ||||||
|  |             "url": "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer", | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         dropdownOptions={ | ||||||
|  |           [ | ||||||
|  |             { | ||||||
|  |               "label": "Default filters", | ||||||
|  |               "options": [ | ||||||
|  |                 "WHERE c.id = "foo"", | ||||||
|  |                 "ORDER BY c._ts DESC", | ||||||
|  |                 "WHERE c.id = "foo" ORDER BY c._ts DESC", | ||||||
|  |                 "ORDER BY c._ts ASC", | ||||||
|  |                 "WHERE c.foo = "foo"", | ||||||
|  |               ], | ||||||
|  |             }, | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |         onChange={[Function]} | ||||||
|  |         onKeyDown={[Function]} | ||||||
|  |         placeholder="Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents." | ||||||
|  |         title="Type a query predicate or choose one from the list." | ||||||
|  |         value="" | ||||||
|       /> |       /> | ||||||
|       <Button |       <Button | ||||||
|         appearance="primary" |         appearance="primary" | ||||||
|  |         aria-label="Apply filter" | ||||||
|  |         disabled={false} | ||||||
|         onClick={[Function]} |         onClick={[Function]} | ||||||
|         size="small" |         size="small" | ||||||
|  |         tabIndex={0} | ||||||
|       > |       > | ||||||
|         Edit Filter |         Apply Filter | ||||||
|       </Button> |       </Button> | ||||||
|     </div> |     </div> | ||||||
|     <div |  | ||||||
|       style={ |  | ||||||
|         { |  | ||||||
|           "height": "100%", |  | ||||||
|           "overflow": "hidden", |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     > |  | ||||||
|     <Allotment |     <Allotment | ||||||
|       onDragEnd={[Function]} |       onDragEnd={[Function]} | ||||||
|     > |     > | ||||||
| @ -117,6 +136,5 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` | |||||||
|       </Allotment.Pane> |       </Allotment.Pane> | ||||||
|     </Allotment> |     </Allotment> | ||||||
|   </div> |   </div> | ||||||
|   </div> |  | ||||||
| </CosmosFluentProvider> | </CosmosFluentProvider> | ||||||
| `; | `; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user