Change add vendor ui

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-05-29 13:53:44 -07:00
parent 4ea66749ed
commit 9b309f1293

View File

@@ -1,4 +1,4 @@
import { Suspense, useEffect, useState, useTransition } from "react"; import { Suspense, useEffect, useState, useTransition, useRef } from "react";
import { import {
graphql, graphql,
PreloadedQuery, PreloadedQuery,
@@ -199,7 +199,7 @@ function ListVendorContent({
const [, setSearchParams] = useSearchParams(); const [, setSearchParams] = useSearchParams();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [showAddVendorDropdown, setShowAddVendorDropdown] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1);
const [filteredVendors, setFilteredVendors] = useState<Vendors>([]); const [filteredVendors, setFilteredVendors] = useState<Vendors>([]);
const [vendorsData, setVendorsData] = useState<Vendors>([]); const [vendorsData, setVendorsData] = useState<Vendors>([]);
const [isLoadingVendors, setIsLoadingVendors] = useState(false); const [isLoadingVendors, setIsLoadingVendors] = useState(false);
@@ -208,6 +208,7 @@ function ListVendorContent({
const [deleteVendor] = const [deleteVendor] =
useMutation<ListVendorViewDeleteVendorMutation>(deleteVendorMutation); useMutation<ListVendorViewDeleteVendorMutation>(deleteVendorMutation);
const { organizationId } = useParams(); const { organizationId } = useParams();
const searchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
const loadVendorsData = async () => { const loadVendorsData = async () => {
@@ -255,6 +256,7 @@ function ListVendorContent({
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; const value = e.target.value;
setSearchTerm(value); setSearchTerm(value);
setSelectedIndex(-1);
if (value.trim() === "") { if (value.trim() === "") {
setFilteredVendors([]); setFilteredVendors([]);
} else { } else {
@@ -263,6 +265,110 @@ function ListVendorContent({
} }
}; };
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
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) // Helper to format date (similar to "Mon, 8 Mar. 2025" in the design)
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
@@ -334,17 +440,7 @@ function ListVendorContent({
return ( return (
<PageTemplate <PageTemplate
title="Vendors" title="Vendors"
actions={
<Button
className="flex items-center gap-2"
onClick={() => setShowAddVendorDropdown(!showAddVendorDropdown)}
> >
<Plus className="h-4 w-4" />
Add vendor
</Button>
}
>
{showAddVendorDropdown && (
<div className="mb-6 p-4 border rounded-xl bg-white relative"> <div className="mb-6 p-4 border rounded-xl bg-white relative">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Store className="h-5 w-5" /> <Store className="h-5 w-5" />
@@ -357,7 +453,9 @@ function ListVendorContent({
value={searchTerm} value={searchTerm}
style={{ borderRadius: "0.3rem" }} style={{ borderRadius: "0.3rem" }}
onChange={handleSearchChange} onChange={handleSearchChange}
onKeyDown={handleKeyDown}
disabled={isLoadingVendors} disabled={isLoadingVendors}
ref={searchInputRef}
/> />
{isLoadingVendors && ( {isLoadingVendors && (
<div className="absolute inset-0 flex items-center justify-center bg-background/50"> <div className="absolute inset-0 flex items-center justify-center bg-background/50">
@@ -370,53 +468,12 @@ function ListVendorContent({
style={{ borderRadius: "0.3rem" }} style={{ borderRadius: "0.3rem" }}
className="absolute top-full left-0 mt-1 w-[calc(100%-100px)] max-h-48 overflow-y-auto border bg-white shadow-md z-10" className="absolute top-full left-0 mt-1 w-[calc(100%-100px)] max-h-48 overflow-y-auto border bg-white shadow-md z-10"
> >
{filteredVendors.map((vendor) => ( {filteredVendors.map((vendor, index) => (
<button <button
key={vendor.name} key={vendor.name}
className="w-full px-3 py-2 text-left hover:bg-gray-50" className={`w-full px-3 py-2 text-left hover:bg-gray-50 ${selectedIndex === index ? 'bg-gray-100' : ''}`}
onClick={() => { onClick={() => handleSelectVendor(vendor)}
createVendor({ onMouseEnter={() => setSelectedIndex(index)}
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([]);
setShowAddVendorDropdown(false);
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",
});
}
});
}}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{vendor.websiteUrl && ( {vendor.websiteUrl && (
@@ -434,29 +491,9 @@ function ListVendorContent({
</button> </button>
))} ))}
<button <button
className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 border-t" className={`w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 border-t ${selectedIndex === filteredVendors.length ? 'bg-gray-100' : ''}`}
onClick={(e) => { onClick={handleCreateNewVendor}
createVendor({ onMouseEnter={() => setSelectedIndex(filteredVendors.length)}
variables: {
connections: [vendorsConnection.vendors.__id],
input: {
organizationId: data.organization.id,
name: searchTerm.trim(),
description: "",
},
},
onCompleted() {
setSearchTerm("");
setFilteredVendors([]);
setShowAddVendorDropdown(false);
toast({
title: "Vendor created",
description:
"The new vendor has been created successfully",
});
},
});
}}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
@@ -468,7 +505,6 @@ function ListVendorContent({
)} )}
</div> </div>
</div> </div>
)}
<div className="rounded-xl border overflow-hidden"> <div className="rounded-xl border overflow-hidden">
<table className="w-full bg-white"> <table className="w-full bg-white">