From 53c0bcbbbca7a61112bf10ee020f5f8a2b186d9a Mon Sep 17 00:00:00 2001 From: nishthaAhujaa Date: Wed, 31 Dec 2025 16:06:13 +0530 Subject: [PATCH] added filtering items --- .../Hosted/Components/SwitchAccount.tsx | 209 ++++++++++++-- .../Hosted/Components/SwitchSubscription.tsx | 255 ++++++++++++++++-- 2 files changed, 422 insertions(+), 42 deletions(-) diff --git a/src/Platform/Hosted/Components/SwitchAccount.tsx b/src/Platform/Hosted/Components/SwitchAccount.tsx index ef195ee07..1a12feba8 100644 --- a/src/Platform/Hosted/Components/SwitchAccount.tsx +++ b/src/Platform/Hosted/Components/SwitchAccount.tsx @@ -1,13 +1,13 @@ -import { Dropdown } from "@fluentui/react"; +import { Callout, DirectionalHint, Label, SearchBox, useTheme } from "@fluentui/react"; import * as React from "react"; -import { FunctionComponent } from "react"; +import { FunctionComponent, useCallback, useEffect, useRef, useState } from "react"; import { DatabaseAccount } from "../../../Contracts/DataModels"; interface Props { accounts: DatabaseAccount[]; selectedAccount: DatabaseAccount; setSelectedAccountName: (id: string) => void; - dismissMenu: () => void; + dismissMenu?: () => void; } export const SwitchAccount: FunctionComponent = ({ @@ -16,24 +16,191 @@ export const SwitchAccount: FunctionComponent = ({ selectedAccount, dismissMenu, }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [filterText, setFilterText] = useState(""); + const buttonRef = useRef(null); + const calloutContentRef = useRef(null); + const theme = useTheme(); + const { semanticColors, palette } = theme; + + const buttonStyles: React.CSSProperties = { + width: "100%", + height: "32px", + padding: "0 28px 0 8px", + border: `1px solid ${semanticColors.inputBorder || palette.neutralSecondary}`, + background: semanticColors.inputBackground || palette.white, + color: semanticColors.inputText || semanticColors.bodyText, + textAlign: "left", + cursor: accounts && accounts.length === 0 ? "not-allowed" : "pointer", + position: "relative", + fontFamily: "inherit", + fontSize: "14px", + }; + + const listContainerStyles: React.CSSProperties = { + width: buttonRef.current?.offsetWidth || 300, + maxHeight: "400px", + display: "flex", + flexDirection: "column", + background: semanticColors.bodyBackground || palette.white, + }; + + const listStyles: React.CSSProperties = { + maxHeight: "300px", + overflowY: "auto", + background: semanticColors.bodyBackground || palette.white, + }; + + const hoverBackground = semanticColors.menuItemBackgroundHovered || palette.neutralLighterAlt; + const selectedBackground = semanticColors.menuItemBackgroundChecked || palette.neutralLighter; + const placeholderColor = semanticColors.bodySubtext || palette.neutralTertiary; + + const getItemStyles = (isSelected: boolean): React.CSSProperties => ({ + padding: "8px 12px", + cursor: "pointer", + backgroundColor: isSelected ? selectedBackground : "transparent", + fontSize: "14px", + color: semanticColors.bodyText, + }); + + const closeDropdown = useCallback(() => { + setIsOpen(false); + setFilterText(""); + }, []); + + useEffect(() => { + if (!isOpen) { + return; + } + + const handleDocumentClick = (event: MouseEvent | TouchEvent) => { + const target = event.target as Node; + if (buttonRef.current?.contains(target) || calloutContentRef.current?.contains(target)) { + return; + } + + closeDropdown(); + }; + + document.addEventListener("mousedown", handleDocumentClick); + document.addEventListener("touchstart", handleDocumentClick); + + return () => { + document.removeEventListener("mousedown", handleDocumentClick); + document.removeEventListener("touchstart", handleDocumentClick); + }; + }, [isOpen, closeDropdown]); + + const filteredAccounts = accounts?.filter((account) => + account.name.toLowerCase().includes(filterText.toLowerCase()), + ); + + const handleDismiss = useCallback(() => { + closeDropdown(); + }, [closeDropdown]); + + const handleButtonClick = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (isOpen) { + closeDropdown(); + return; + } + + setIsOpen(true); + }, + [isOpen, closeDropdown], + ); + + const handleSelect = useCallback( + (account: DatabaseAccount) => { + setSelectedAccountName(account.name); + closeDropdown(); + dismissMenu?.(); + }, + [setSelectedAccountName, closeDropdown, dismissMenu], + ); + + const buttonLabel = selectedAccount?.name || (accounts && accounts.length === 0 ? "No Accounts Found" : "Select an Account"); + return ( - ({ - 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", - }} - /> +
+ + + {isOpen && ( + +
+
+ setFilterText(newValue || "")} + styles={{ + root: { + padding: "8px", + borderBottom: `1px solid ${semanticColors.inputBorder || palette.neutralLight}`, + background: semanticColors.bodyBackground || palette.white, + }, + field: { + color: semanticColors.inputText || semanticColors.bodyText, + }, + }} + /> +
+ {filteredAccounts && filteredAccounts.length > 0 ? ( + filteredAccounts.map((account) => ( +
handleSelect(account)} + style={getItemStyles(account.name === selectedAccount?.name)} + onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = hoverBackground)} + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = + account.name === selectedAccount?.name ? selectedBackground : "transparent") + } + > + {account.name} +
+ )) + ) : ( +
No accounts found
+ )} +
+
+
+
+ )} +
); }; diff --git a/src/Platform/Hosted/Components/SwitchSubscription.tsx b/src/Platform/Hosted/Components/SwitchSubscription.tsx index c784c5f5b..4ca548f5e 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 { Callout, DirectionalHint, Label, SearchBox, mergeStyleSets, useTheme } from "@fluentui/react"; import * as React from "react"; -import { FunctionComponent } from "react"; +import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Subscription } from "../../../Contracts/DataModels"; interface Props { @@ -14,25 +14,238 @@ export const SwitchSubscription: FunctionComponent = ({ setSelectedSubscriptionId, selectedSubscription, }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [filterText, setFilterText] = useState(""); + const buttonRef = useRef(null); + const calloutContentRef = useRef(null); + const theme = useTheme(); + const { semanticColors, palette } = theme; + + const hoverBackground = semanticColors.menuItemBackgroundHovered || palette.neutralLighterAlt; + const selectedBackground = semanticColors.menuItemBackgroundChecked || palette.neutralLighter; + const placeholderColor = semanticColors.bodySubtext || palette.neutralTertiary; + + const classNames = useMemo( + () => + mergeStyleSets({ + container: { + display: "flex", + flexDirection: "column", + gap: 12, + }, + trigger: { + width: "100%", + height: 32, + padding: "0 28px 0 8px", + border: `1px solid ${semanticColors.inputBorder || palette.neutralSecondary}`, + backgroundColor: semanticColors.inputBackground || palette.white, + color: semanticColors.inputText || semanticColors.bodyText, + textAlign: "left", + cursor: "pointer", + position: "relative", + fontFamily: "inherit", + fontSize: 14, + borderRadius: 2, + selectors: { + ":hover": { + borderColor: semanticColors.inputBorderHovered || palette.neutralPrimary, + }, + ":focus-visible": { + outline: `1px solid ${semanticColors.focusBorder || palette.themePrimary}`, + outlineOffset: 1, + }, + }, + }, + triggerText: { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + display: "block", + }, + triggerChevron: { + position: "absolute", + right: 8, + top: "50%", + transform: "translateY(-50%)", + pointerEvents: "none", + }, + callout: { + backgroundColor: semanticColors.bodyBackground || palette.white, + }, + listContainer: { + display: "flex", + flexDirection: "column", + maxHeight: 400, + backgroundColor: semanticColors.bodyBackground || palette.white, + }, + list: { + maxHeight: 300, + overflowY: "auto", + }, + item: { + padding: "8px 12px", + cursor: "pointer", + fontSize: 14, + color: semanticColors.bodyText, + backgroundColor: "transparent", + selectors: { + ":hover": { + backgroundColor: hoverBackground, + }, + }, + }, + itemSelected: { + backgroundColor: selectedBackground, + selectors: { + ":hover": { + backgroundColor: selectedBackground, + }, + }, + }, + emptyState: { + padding: "8px 12px", + color: placeholderColor, + }, + }), + [hoverBackground, palette, placeholderColor, semanticColors, selectedBackground], + ); + + const searchBoxStyles = useMemo( + () => ({ + root: { + padding: 8, + borderBottom: `1px solid ${semanticColors.inputBorder || palette.neutralLight}`, + background: semanticColors.bodyBackground || palette.white, + }, + field: { + color: semanticColors.inputText || semanticColors.bodyText, + }, + icon: { + color: placeholderColor, + }, + clearButton: { + color: placeholderColor, + }, + }), + [palette, placeholderColor, semanticColors], + ); + + const closeDropdown = useCallback(() => { + setIsOpen(false); + setFilterText(""); + }, []); + + useEffect(() => { + if (!isOpen) { + return; + } + + const handleDocumentClick = (event: MouseEvent | TouchEvent) => { + const target = event.target as Node; + if (buttonRef.current?.contains(target) || calloutContentRef.current?.contains(target)) { + return; + } + + closeDropdown(); + }; + + document.addEventListener("mousedown", handleDocumentClick); + document.addEventListener("touchstart", handleDocumentClick); + + return () => { + document.removeEventListener("mousedown", handleDocumentClick); + document.removeEventListener("touchstart", handleDocumentClick); + }; + }, [isOpen, closeDropdown]); + + const filteredSubscriptions = subscriptions?.filter((sub) => + sub.displayName.toLowerCase().includes(filterText.toLowerCase()), + ); + + const handleDismiss = useCallback(() => { + closeDropdown(); + }, [closeDropdown]); + + const handleButtonClick = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (isOpen) { + closeDropdown(); + return; + } + + setIsOpen(true); + }, + [isOpen, closeDropdown], + ); + + const handleSelect = useCallback( + (subscription: Subscription) => { + setSelectedSubscriptionId(subscription.subscriptionId); + closeDropdown(); + }, + [setSelectedSubscriptionId, closeDropdown], + ); + return ( - { - 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", - }} - /> +
+ + + {isOpen && ( + +
+
+ setFilterText(newValue || "")} + styles={searchBoxStyles} + /> +
+ {filteredSubscriptions && filteredSubscriptions.length > 0 ? ( + filteredSubscriptions.map((sub) => { + const isSelected = sub.subscriptionId === selectedSubscription?.subscriptionId; + const itemClassName = isSelected + ? `${classNames.item} ${classNames.itemSelected}` + : classNames.item; + + return ( +
handleSelect(sub)} className={itemClassName}> + {sub.displayName} +
+ ); + }) + ) : ( +
No subscriptions found
+ )} +
+
+
+
+ )} +
); };