From 5fc88610e4541b5611877413e87fd98c248e88ad Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 15 Sep 2025 14:42:14 -0300 Subject: [PATCH] Improve UI dropdown --- .../UpgradePathPage/UpgradePathPage.tsx | 267 ++++-------------- 1 file changed, 60 insertions(+), 207 deletions(-) diff --git a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx index a8a8551f2..74317d02f 100644 --- a/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx +++ b/frontend/src/pages/public/UpgradePathPage/UpgradePathPage.tsx @@ -1,18 +1,31 @@ /* eslint-disable no-nested-ternary */ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useState } from "react"; import { Helmet } from "react-helmet"; -import { faExternalLink, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { SingleValue } from "react-select"; +import { faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useMutation } from "@tanstack/react-query"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input } from "@app/components/v2"; +import { Button, FilterableSelect, FormControl } from "@app/components/v2"; import { useCalculateUpgradePath, useGetUpgradePathVersions } from "../../../hooks/api/upgradePath/queries"; +type VersionOption = { + label: string; + value: string; + isLatest: boolean; +}; + +const formatVersionOption = (option: VersionOption) => ( +
+ {option.label} + {option.isLatest && (Latest)} +
+); interface UpgradeResult { path: Array<{ version: string; @@ -39,66 +52,9 @@ interface UpgradeResult { } export const UpgradePathPage = () => { - const [fromVersion, setFromVersion] = useState(""); - const [toVersion, setToVersion] = useState(""); - const [fromSearch, setFromSearch] = useState(""); - const [toSearch, setToSearch] = useState(""); - const [showFromDropdown, setShowFromDropdown] = useState(false); - const [showToDropdown, setShowToDropdown] = useState(false); + const [fromVersion, setFromVersion] = useState(null); + const [toVersion, setToVersion] = useState(null); const [upgradeResult, setUpgradeResult] = useState(null); - const [debouncedFromSearch, setDebouncedFromSearch] = useState(""); - const [debouncedToSearch, setDebouncedToSearch] = useState(""); - - const fromDropdownRef = useRef(null); - const toDropdownRef = useRef(null); - const fromSearchTimeoutRef = useRef(); - const toSearchTimeoutRef = useRef(); - - useEffect(() => { - if (fromSearchTimeoutRef.current) { - clearTimeout(fromSearchTimeoutRef.current); - } - fromSearchTimeoutRef.current = setTimeout(() => { - setDebouncedFromSearch(fromSearch); - }, 300); - - return () => { - if (fromSearchTimeoutRef.current) { - clearTimeout(fromSearchTimeoutRef.current); - } - }; - }, [fromSearch]); - - useEffect(() => { - if (toSearchTimeoutRef.current) { - clearTimeout(toSearchTimeoutRef.current); - } - toSearchTimeoutRef.current = setTimeout(() => { - setDebouncedToSearch(toSearch); - }, 300); - - return () => { - if (toSearchTimeoutRef.current) { - clearTimeout(toSearchTimeoutRef.current); - } - }; - }, [toSearch]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (fromDropdownRef.current && !fromDropdownRef.current.contains(event.target as Node)) { - setShowFromDropdown(false); - } - if (toDropdownRef.current && !toDropdownRef.current.contains(event.target as Node)) { - setShowToDropdown(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); const { data: versions, @@ -128,44 +84,26 @@ export const UpgradePathPage = () => { } }); - const filteredFromVersions = useMemo(() => { + const versionOptions = useMemo(() => { if (!versions?.versions) return []; - const filtered = versions.versions + return versions.versions .filter((version) => !version.tagName.includes("nightly")) - .filter((version) => { - if (!debouncedFromSearch) return true; - const searchTerm = debouncedFromSearch.toLowerCase(); - return version.tagName.toLowerCase().includes(searchTerm); - }); + .map((version) => ({ + label: version.tagName, + value: version.tagName, + isLatest: versions.versions[0]?.tagName === version.tagName + })); + }, [versions?.versions]); - return filtered.slice(0, 25); - }, [versions?.versions, debouncedFromSearch]); - - const filteredToVersions = useMemo(() => { - if (!versions?.versions) return []; - - const filtered = versions.versions - .filter((version) => !version.tagName.includes("nightly")) - .filter((version) => { - if (!debouncedToSearch) return true; - const searchTerm = debouncedToSearch.toLowerCase(); - return version.tagName.toLowerCase().includes(searchTerm); - }); - - return filtered.slice(0, 25); - }, [versions?.versions, debouncedToSearch]); - - const handleFromVersionSelect = (version: string) => { - setFromVersion(version); - setFromSearch(version); - setShowFromDropdown(false); + const handleFromVersionSelect = (value: unknown) => { + const selected = value as SingleValue; + setFromVersion(selected?.value || null); }; - const handleToVersionSelect = (version: string) => { - setToVersion(version); - setToSearch(version); - setShowToDropdown(false); + const handleToVersionSelect = (value: unknown) => { + const selected = value as SingleValue; + setToVersion(selected?.value || null); }; const handleCalculate = () => { @@ -232,121 +170,36 @@ export const UpgradePathPage = () => {
{/* From Version Selector */} -
- -
- { - setFromSearch(e.target.value); - setFromVersion(""); - setShowFromDropdown(true); - }} - onFocus={() => setShowFromDropdown(true)} - placeholder="Search or select version..." - className="border-mineshaft-600 bg-mineshaft-900" - leftIcon={} - isDisabled={versionsLoading || versionsFetching} - /> - {showFromDropdown && ( -
- {(() => { - if (versionsLoading || versionsFetching) { - return ( -
-
- - Loading versions... - -
- ); - } - if (filteredFromVersions.length > 0) { - return filteredFromVersions.slice(0, 8).map((version) => ( - - )); - } - return ( -
- {debouncedFromSearch - ? `No versions found matching "${debouncedFromSearch}"` - : "No versions available"} -
- ); - })()} -
- )} -
- -
+ + opt.value === fromVersion) || null} + onChange={handleFromVersionSelect} + placeholder="Search or select version..." + isLoading={versionsLoading || versionsFetching} + isDisabled={versionsLoading || versionsFetching} + isSearchable + isClearable + menuPortalTarget={document.body} + formatOptionLabel={formatVersionOption} + /> + {/* To Version Selector */} -
- -
- { - setToSearch(e.target.value); - setToVersion(""); - setShowToDropdown(true); - }} - onFocus={() => setShowToDropdown(true)} - placeholder="Search or select version..." - className="border-mineshaft-600 bg-mineshaft-900" - leftIcon={} - isDisabled={versionsLoading || versionsFetching} - /> - {showToDropdown && ( -
- {(() => { - if (versionsLoading || versionsFetching) { - return ( -
-
- - Loading versions... - -
- ); - } - if (filteredToVersions.length > 0) { - return filteredToVersions.slice(0, 8).map((version) => ( - - )); - } - return ( -
- {debouncedToSearch - ? `No versions found matching "${debouncedToSearch}"` - : "No versions available"} -
- ); - })()} -
- )} -
- -
+ + opt.value === toVersion) || null} + onChange={handleToVersionSelect} + placeholder="Search or select version..." + isLoading={versionsLoading || versionsFetching} + isDisabled={versionsLoading || versionsFetching} + isSearchable + isClearable + menuPortalTarget={document.body} + formatOptionLabel={formatVersionOption} + /> +