diff --git a/src/Common/SearchableDropdown.tsx b/src/Common/SearchableDropdown.tsx new file mode 100644 index 000000000..f6e9d90ec --- /dev/null +++ b/src/Common/SearchableDropdown.tsx @@ -0,0 +1,184 @@ +import { Callout, DirectionalHint, ISearchBoxStyles, Label, SearchBox } from "@fluentui/react"; +import * as React from "react"; +import { useCallback, useRef, useState } from "react"; + +interface SearchableDropdownProps { + label: string; + items: T[]; + selectedItem: T | null; + onSelect: (item: T) => void; + getKey: (item: T) => string; + getDisplayText: (item: T) => string; + placeholder?: string; + filterPlaceholder?: string; + className?: string; + disabled?: boolean; + onDismiss?: () => void; + buttonStyles?: React.CSSProperties; + searchBoxStyles?: Partial; + listStyles?: React.CSSProperties; + itemStyles?: React.CSSProperties; +} + +export const SearchableDropdown = ({ + label, + items, + selectedItem, + onSelect, + getKey, + getDisplayText, + placeholder = "Select an item", + filterPlaceholder = "Filter items", + className, + disabled = false, + onDismiss, + buttonStyles: customButtonStyles, + searchBoxStyles: customSearchBoxStyles, + listStyles: customListStyles, + itemStyles: customItemStyles, +}: SearchableDropdownProps): React.ReactElement => { + const [isOpen, setIsOpen] = useState(false); + const [filterText, setFilterText] = useState(""); + const buttonRef = useRef(null); + + const defaultButtonStyles: React.CSSProperties = { + width: "100%", + height: "32px", + padding: "0 28px 0 8px", + border: "1px solid #8a8886", + background: "#fff", + color: "#323130", + textAlign: "left", + cursor: disabled ? "not-allowed" : "pointer", + position: "relative", + fontFamily: "inherit", + fontSize: "14px", + }; + + const defaultListStyles: React.CSSProperties = { + maxHeight: "300px", + overflowY: "auto", + }; + + const defaultItemStyles: React.CSSProperties = { + padding: "8px 12px", + cursor: "pointer", + fontSize: "14px", + }; + + // Merge custom styles with defaults + const buttonStyles = { ...defaultButtonStyles, ...customButtonStyles }; + const listStyles = { ...defaultListStyles, ...customListStyles }; + const itemStyles = { ...defaultItemStyles, ...customItemStyles }; + + const closeDropdown = useCallback(() => { + setIsOpen(false); + setFilterText(""); + }, []); + + const filteredItems = items?.filter((item) => + getDisplayText(item).toLowerCase().includes(filterText.toLowerCase()), + ); + + const handleDismiss = useCallback(() => { + closeDropdown(); + onDismiss?.(); + }, [closeDropdown, onDismiss]); + + const handleButtonClick = useCallback( + (event: React.MouseEvent) => { + if (disabled) { + return; + } + event.preventDefault(); + event.stopPropagation(); + + if (isOpen) { + closeDropdown(); + return; + } + + setIsOpen(true); + return; + }, + [isOpen, closeDropdown, disabled], + ); + + const handleSelect = useCallback( + (item: T) => { + onSelect(item); + closeDropdown(); + }, + [onSelect, closeDropdown], + ); + + const buttonLabel = + selectedItem ? getDisplayText(selectedItem) : items?.length === 0 ? `No ${label}s Found` : placeholder; + + return ( +
+ + + {isOpen && ( + +
+ setFilterText(newValue || "")} + styles={customSearchBoxStyles} + /> +
+ {filteredItems && filteredItems.length > 0 ? ( + filteredItems.map((item) => { + const key = getKey(item); + const isSelected = selectedItem ? getKey(selectedItem) === key : false; + return ( +
handleSelect(item)} + style={{ + ...itemStyles, + backgroundColor: isSelected ? "#e6e6e6" : "transparent", + }} + onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#f3f2f1")} + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = isSelected ? "#e6e6e6" : "transparent") + } + > + {getDisplayText(item)} +
+ ); + }) + ) : ( +
No items found
+ )} +
+
+
+ )} +
+ ); +}; diff --git a/src/Platform/Hosted/Components/SwitchAccount.tsx b/src/Platform/Hosted/Components/SwitchAccount.tsx index ef195ee07..346ea8cef 100644 --- a/src/Platform/Hosted/Components/SwitchAccount.tsx +++ b/src/Platform/Hosted/Components/SwitchAccount.tsx @@ -1,6 +1,6 @@ -import { Dropdown } from "@fluentui/react"; import * as React from "react"; import { FunctionComponent } from "react"; +import { SearchableDropdown } from "../../../Common/SearchableDropdown"; import { DatabaseAccount } from "../../../Contracts/DataModels"; interface Props { @@ -17,23 +17,18 @@ export const SwitchAccount: FunctionComponent = ({ dismissMenu, }: Props) => { return ( - label="Cosmos DB Account Name" + items={accounts} + selectedItem={selectedAccount} + onSelect={(account) => setSelectedAccountName(account.name)} + getKey={(account) => account.name} + getDisplayText={(account) => account.name} + placeholder="Select an Account" + filterPlaceholder="Filter accounts" className="accountSwitchAccountDropdown" - options={accounts?.map((account) => ({ - key: account.name, - text: account.name, - data: account, - }))} - onChange={(_, option) => { - setSelectedAccountName(String(option?.key)); - dismissMenu(); - }} - defaultSelectedKey={selectedAccount?.name} - placeholder={accounts && accounts.length === 0 ? "No Accounts Found" : "Select an Account"} - styles={{ - callout: "accountSwitchAccountDropdownMenu", - }} + disabled={!accounts || accounts.length === 0} + onDismiss={dismissMenu} /> ); }; diff --git a/src/Platform/Hosted/Components/SwitchSubscription.tsx b/src/Platform/Hosted/Components/SwitchSubscription.tsx index c784c5f5b..9a78b2a5f 100644 --- a/src/Platform/Hosted/Components/SwitchSubscription.tsx +++ b/src/Platform/Hosted/Components/SwitchSubscription.tsx @@ -1,6 +1,6 @@ -import { Dropdown } from "@fluentui/react"; import * as React from "react"; import { FunctionComponent } from "react"; +import { SearchableDropdown } from "../../../Common/SearchableDropdown"; import { Subscription } from "../../../Contracts/DataModels"; interface Props { @@ -15,24 +15,16 @@ export const SwitchSubscription: FunctionComponent = ({ selectedSubscription, }: Props) => { return ( - label="Subscription" + items={subscriptions} + selectedItem={selectedSubscription} + onSelect={(sub) => setSelectedSubscriptionId(sub.subscriptionId)} + getKey={(sub) => sub.subscriptionId} + getDisplayText={(sub) => sub.displayName} + placeholder="Select a Subscription" + filterPlaceholder="Filter subscriptions" className="accountSwitchSubscriptionDropdown" - options={subscriptions?.map((sub) => { - return { - key: sub.subscriptionId, - text: sub.displayName, - data: sub, - }; - })} - onChange={(_, option) => { - setSelectedSubscriptionId(String(option?.key)); - }} - defaultSelectedKey={selectedSubscription?.subscriptionId} - placeholder={subscriptions && subscriptions.length === 0 ? "No Subscriptions Found" : "Select a Subscription"} - styles={{ - callout: "accountSwitchSubscriptionDropdownMenu", - }} /> ); };