Fixes on cert request UI

This commit is contained in:
Carlos Monastyrski
2025-12-18 21:47:18 -03:00
parent eb66a20b3a
commit 47f80b5241
5 changed files with 60 additions and 36 deletions

View File

@@ -4297,6 +4297,8 @@ interface ListCertificateRequestsEvent {
limit: number; limit: number;
search?: string; search?: string;
status?: string; status?: string;
count: number;
certificateRequestIds: string[];
}; };
} }

View File

@@ -448,7 +448,9 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
offset: req.query.offset, offset: req.query.offset,
limit: req.query.limit, limit: req.query.limit,
search: req.query.search, search: req.query.search,
status: req.query.status status: req.query.status,
count: certificateRequests.length,
certificateRequestIds: certificateRequests.map((certReq) => certReq.id)
} }
} }
}); });

View File

@@ -9,6 +9,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
IconButton,
Td, Td,
Tooltip, Tooltip,
Tr Tr
@@ -77,13 +78,15 @@ export const CertificateRequestRow = ({ request, onViewCertificates }: Props) =>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <IconButton
<Tooltip content="More options"> variant="plain"
<FontAwesomeIcon size="lg" icon={faEllipsis} /> ariaLabel="More options"
</Tooltip> className="h-max bg-transparent p-0"
</div> >
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end" sideOffset={3}>
<DropdownMenuItem <DropdownMenuItem
onClick={() => request.certificateId && onViewCertificates?.(request.certificateId)} onClick={() => request.certificateId && onViewCertificates?.(request.certificateId)}
disabled={!request.certificateId} disabled={!request.certificateId}

View File

@@ -63,6 +63,7 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
const [appliedFilters, setAppliedFilters] = useState<CertificateRequestFilters>({}); const [appliedFilters, setAppliedFilters] = useState<CertificateRequestFilters>({});
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(PAGE_SIZE);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["issueCertificate"] as const); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
@@ -84,15 +85,15 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
const queryParams: TListCertificateRequestsParams = useMemo( const queryParams: TListCertificateRequestsParams = useMemo(
() => ({ () => ({
projectSlug: currentProject?.slug || "", projectSlug: currentProject?.slug || "",
offset: (currentPage - 1) * PAGE_SIZE, offset: (currentPage - 1) * perPage,
limit: PAGE_SIZE, limit: perPage,
sortBy: "createdAt", sortBy: "createdAt",
sortOrder: "desc", sortOrder: "desc",
...(debouncedSearch && { search: debouncedSearch }), ...(debouncedSearch && { search: debouncedSearch }),
...(appliedFilters.status && { status: appliedFilters.status }), ...(appliedFilters.status && { status: appliedFilters.status }),
...(profileIds && { profileIds }) ...(profileIds && { profileIds })
}), }),
[currentProject?.slug, currentPage, debouncedSearch, appliedFilters.status, profileIds] [currentProject?.slug, currentPage, perPage, debouncedSearch, appliedFilters.status, profileIds]
); );
const { const {
@@ -130,14 +131,18 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
}; };
const isTableFiltered = useMemo( const isTableFiltered = useMemo(
() => Boolean(debouncedSearch || appliedFilters.status || appliedProfileIds.length), () => Boolean(appliedFilters.status || appliedProfileIds.length),
[debouncedSearch, appliedFilters.status, appliedProfileIds.length] [appliedFilters.status, appliedProfileIds.length]
); );
const hasPendingChanges = useMemo(() => { const hasPendingChanges = useMemo(() => {
if (pendingFilters.status !== appliedFilters.status) return true; const pendingStatus = pendingFilters.status ?? undefined;
if (pendingProfileIds.length !== appliedProfileIds.length) return true; const appliedStatus = appliedFilters.status ?? undefined;
return pendingProfileIds.some((id, index) => id !== appliedProfileIds[index]); const statusChanged = pendingStatus !== appliedStatus;
const profileIdsChanged =
JSON.stringify([...pendingProfileIds].sort()) !==
JSON.stringify([...appliedProfileIds].sort());
return statusChanged || profileIdsChanged;
}, [pendingFilters.status, appliedFilters.status, pendingProfileIds, appliedProfileIds]); }, [pendingFilters.status, appliedFilters.status, pendingProfileIds, appliedProfileIds]);
return ( return (
@@ -203,7 +208,7 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
<button <button
type="button" type="button"
onClick={handleClearFilters} onClick={handleClearFilters}
className="text-primary hover:text-primary-600" className="cursor-pointer text-primary hover:text-primary-600"
> >
Clear filters Clear filters
</button> </button>
@@ -220,7 +225,7 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
<button <button
type="button" type="button"
onClick={handleClearProfiles} onClick={handleClearProfiles}
className="text-xs text-primary hover:text-primary-600" className="cursor-pointer text-xs text-primary hover:text-primary-600"
> >
Clear Clear
</button> </button>
@@ -258,7 +263,7 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
<button <button
type="button" type="button"
onClick={handleClearStatus} onClick={handleClearStatus}
className="text-xs text-primary hover:text-primary-600" className="cursor-pointer text-xs text-primary hover:text-primary-600"
> >
Clear Clear
</button> </button>
@@ -274,6 +279,8 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
}} }}
placeholder="All events" placeholder="All events"
className="w-full border-mineshaft-600 bg-mineshaft-700 text-bunker-200" className="w-full border-mineshaft-600 bg-mineshaft-700 text-bunker-200"
position="popper"
dropdownContainerClassName="max-w-none"
> >
<SelectItem value="all">All events</SelectItem> <SelectItem value="all">All events</SelectItem>
<SelectItem value="pending">Pending</SelectItem> <SelectItem value="pending">Pending</SelectItem>
@@ -287,7 +294,7 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
onClick={handleApplyFilters} onClick={handleApplyFilters}
className="w-full bg-primary font-medium text-black hover:bg-primary-600" className="w-full bg-primary font-medium text-black hover:bg-primary-600"
size="sm" size="sm"
disabled={!hasPendingChanges} isDisabled={!hasPendingChanges}
> >
Apply Apply
</Button> </Button>
@@ -349,10 +356,11 @@ export const CertificateRequestsSection = ({ onViewCertificateFromRequest }: Pro
<Pagination <Pagination
count={certificateRequestsData.totalCount} count={certificateRequestsData.totalCount}
page={currentPage} page={currentPage}
perPage={PAGE_SIZE} perPage={perPage}
onChangePage={(page) => setCurrentPage(page)} onChangePage={(page) => setCurrentPage(page)}
onChangePerPage={() => { onChangePerPage={(newPerPage) => {
setCurrentPage(1); setCurrentPage(1);
setPerPage(newPerPage);
}} }}
/> />
</div> </div>

View File

@@ -220,9 +220,17 @@ export const CertificatesTable = ({ handlePopUpOpen, externalFilter }: Props) =>
setPendingProfileIds([]); setPendingProfileIds([]);
}; };
const isTableFiltered = Boolean( const isTableFiltered = Boolean(appliedFilters.status || appliedProfileIds.length);
appliedSearch || appliedFilters.status || appliedProfileIds.length
); const hasFilterChanges = useMemo(() => {
const pendingStatus = pendingFilters.status ?? undefined;
const appliedStatus = appliedFilters.status ?? undefined;
const statusChanged = pendingStatus !== appliedStatus;
const profileIdsChanged =
JSON.stringify([...pendingProfileIds].sort()) !==
JSON.stringify([...appliedProfileIds].sort());
return statusChanged || profileIdsChanged;
}, [pendingFilters.status, appliedFilters.status, pendingProfileIds, appliedProfileIds]);
return ( return (
<div> <div>
@@ -261,7 +269,7 @@ export const CertificatesTable = ({ handlePopUpOpen, externalFilter }: Props) =>
<button <button
type="button" type="button"
onClick={handleClearFilters} onClick={handleClearFilters}
className="text-primary hover:text-primary-600" className="cursor-pointer text-primary hover:text-primary-600"
> >
Clear filters Clear filters
</button> </button>
@@ -278,7 +286,7 @@ export const CertificatesTable = ({ handlePopUpOpen, externalFilter }: Props) =>
<button <button
type="button" type="button"
onClick={handleClearProfiles} onClick={handleClearProfiles}
className="text-xs text-primary hover:text-primary-600" className="cursor-pointer text-xs text-primary hover:text-primary-600"
> >
Clear Clear
</button> </button>
@@ -316,7 +324,7 @@ export const CertificatesTable = ({ handlePopUpOpen, externalFilter }: Props) =>
<button <button
type="button" type="button"
onClick={handleClearStatus} onClick={handleClearStatus}
className="text-xs text-primary hover:text-primary-600" className="cursor-pointer text-xs text-primary hover:text-primary-600"
> >
Clear Clear
</button> </button>
@@ -332,6 +340,8 @@ export const CertificatesTable = ({ handlePopUpOpen, externalFilter }: Props) =>
}} }}
placeholder="All statuses" placeholder="All statuses"
className="w-full border-mineshaft-600 bg-mineshaft-700 text-bunker-200" className="w-full border-mineshaft-600 bg-mineshaft-700 text-bunker-200"
position="popper"
dropdownContainerClassName="max-w-none"
> >
<SelectItem value="all">All statuses</SelectItem> <SelectItem value="all">All statuses</SelectItem>
<SelectItem value={CertificateStatus.Active}>Active</SelectItem> <SelectItem value={CertificateStatus.Active}>Active</SelectItem>
@@ -349,10 +359,7 @@ export const CertificatesTable = ({ handlePopUpOpen, externalFilter }: Props) =>
}} }}
className="w-full bg-primary font-medium text-black hover:bg-primary-600" className="w-full bg-primary font-medium text-black hover:bg-primary-600"
size="sm" size="sm"
disabled={ isDisabled={!hasFilterChanges}
pendingFilters.status === appliedFilters.status &&
JSON.stringify(pendingProfileIds) === JSON.stringify(appliedProfileIds)
}
> >
Apply Filters Apply Filters
</Button> </Button>
@@ -503,11 +510,13 @@ export const CertificatesTable = ({ handlePopUpOpen, externalFilter }: Props) =>
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <IconButton
<Tooltip content="More options"> variant="plain"
<FontAwesomeIcon size="lg" icon={faEllipsis} /> ariaLabel="More options"
</Tooltip> className="h-max bg-transparent p-0"
</div> >
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan <ProjectPermissionCan