From 9b309f129353c359a7f5df399b2bc26660b8fbda Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 29 May 2025 13:53:44 -0700 Subject: [PATCH] Change add vendor ui Signed-off-by: Sacha Al Himdani --- .../organizations/vendors/ListVendorView.tsx | 294 ++++++++++-------- 1 file changed, 165 insertions(+), 129 deletions(-) diff --git a/apps/console/src/pages/organizations/vendors/ListVendorView.tsx b/apps/console/src/pages/organizations/vendors/ListVendorView.tsx index a038730c3..49bbdbf4e 100644 --- a/apps/console/src/pages/organizations/vendors/ListVendorView.tsx +++ b/apps/console/src/pages/organizations/vendors/ListVendorView.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect, useState, useTransition } from "react"; +import { Suspense, useEffect, useState, useTransition, useRef } from "react"; import { graphql, PreloadedQuery, @@ -199,7 +199,7 @@ function ListVendorContent({ const [, setSearchParams] = useSearchParams(); const [, startTransition] = useTransition(); const [searchTerm, setSearchTerm] = useState(""); - const [showAddVendorDropdown, setShowAddVendorDropdown] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(-1); const [filteredVendors, setFilteredVendors] = useState([]); const [vendorsData, setVendorsData] = useState([]); const [isLoadingVendors, setIsLoadingVendors] = useState(false); @@ -208,6 +208,7 @@ function ListVendorContent({ const [deleteVendor] = useMutation(deleteVendorMutation); const { organizationId } = useParams(); + const searchInputRef = useRef(null); useEffect(() => { const loadVendorsData = async () => { @@ -255,6 +256,7 @@ function ListVendorContent({ const handleSearchChange = (e: React.ChangeEvent) => { const value = e.target.value; setSearchTerm(value); + setSelectedIndex(-1); if (value.trim() === "") { setFilteredVendors([]); } else { @@ -263,6 +265,110 @@ function ListVendorContent({ } }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!searchTerm.trim()) return; + + const totalOptions = filteredVendors.length + 1; // +1 for "Create new vendor" option + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setSelectedIndex(prev => (prev + 1) % totalOptions); + break; + case 'ArrowUp': + e.preventDefault(); + setSelectedIndex(prev => (prev - 1 + totalOptions) % totalOptions); + break; + case 'Enter': + e.preventDefault(); + if (selectedIndex === -1) { + // No suggestion selected, create new vendor + handleCreateNewVendor(); + } else if (selectedIndex === filteredVendors.length) { + // Create new vendor option selected + handleCreateNewVendor(); + } else { + // Select existing vendor + handleSelectVendor(filteredVendors[selectedIndex]); + } + break; + case 'Escape': + e.preventDefault(); + setSearchTerm(""); + setFilteredVendors([]); + setSelectedIndex(-1); + searchInputRef.current?.focus(); + break; + } + }; + + const handleCreateNewVendor = () => { + createVendor({ + variables: { + connections: [vendorsConnection.vendors.__id], + input: { + organizationId: data.organization.id, + name: searchTerm.trim(), + description: "", + }, + }, + onCompleted() { + setSearchTerm(""); + setFilteredVendors([]); + setSelectedIndex(-1); + searchInputRef.current?.focus(); + toast({ + title: "Vendor created", + description: "The new vendor has been created successfully", + }); + }, + }); + }; + + const handleSelectVendor = (vendor: Vendor) => { + createVendor({ + variables: { + connections: [vendorsConnection.vendors.__id], + input: { + organizationId: data.organization.id, + name: vendor.name, + description: vendor.description, + headquarterAddress: vendor.headquarterAddress, + legalName: vendor.legalName, + websiteUrl: vendor.websiteUrl, + category: vendor.category as VendorCategory || null, + privacyPolicyUrl: vendor.privacyPolicyUrl, + serviceLevelAgreementUrl: vendor.serviceLevelAgreementUrl, + dataProcessingAgreementUrl: vendor.dataProcessingAgreementUrl, + businessAssociateAgreementUrl: vendor.businessAssociateAgreementUrl, + subprocessorsListUrl: vendor.subprocessorsListUrl, + certifications: vendor.certifications, + securityPageUrl: vendor.securityPageUrl, + trustPageUrl: vendor.trustPageUrl, + statusPageUrl: vendor.statusPageUrl, + termsOfServiceUrl: vendor.termsOfServiceUrl, + }, + }, + onCompleted() { + setSearchTerm(""); + setFilteredVendors([]); + setSelectedIndex(-1); + searchInputRef.current?.focus(); + toast({ + title: "Vendor added", + description: "The vendor has been added successfully", + }); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message || "Failed to create vendor", + variant: "destructive", + }); + } + }); + }; + // Helper to format date (similar to "Mon, 8 Mar. 2025" in the design) const formatDate = (dateString: string) => { const date = new Date(dateString); @@ -334,141 +440,71 @@ function ListVendorContent({ return ( setShowAddVendorDropdown(!showAddVendorDropdown)} - > - - Add vendor - - } > - {showAddVendorDropdown && ( -
-
- -

Add a vendor

-
-
- - {isLoadingVendors && ( -
-
-
- )} +
+
+ +

Add a vendor

+
+
+ + {isLoadingVendors && ( +
+
+
+ )} - {searchTerm.trim() !== "" && ( -
- {filteredVendors.map((vendor) => ( - - ))} + {searchTerm.trim() !== "" && ( +
+ {filteredVendors.map((vendor, index) => ( -
- )} -
+ ))} + +
+ )}
- )} +