supported scroll in certification popover list (#23534)

This commit is contained in:
Ashish Gupta 2025-09-26 20:17:19 +05:30 committed by GitHub
parent bcae472cc0
commit f934b79d65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 190 additions and 83 deletions

View File

@ -532,3 +532,44 @@ export const executeWithRetry = async <T>(
} }
} }
}; };
export const readElementInListWithScroll = async (
page: Page,
locator: Locator,
hierarchyElementLocator: Locator
) => {
const element = locator;
// Reset scroll position to top before starting pagination
await hierarchyElementLocator.hover();
await page.mouse.wheel(0, -99999);
await page.waitForTimeout(1000);
// Retry mechanism for pagination
let elementCount = await element.count();
let retryCount = 0;
const maxRetries = 10;
while (elementCount === 0 && retryCount < maxRetries) {
await hierarchyElementLocator.hover();
await page.mouse.wheel(0, 1000);
await page.waitForTimeout(500);
// Create fresh locator and check if the article is now visible after this retry
const freshArticle = locator;
const count = await freshArticle.count();
// Check if the article is now visible after this retry
elementCount = count;
// If we found the element, validate it and break out of the loop
if (count > 0) {
await expect(freshArticle).toBeVisible();
return; // Exit the function early since we found and validated the article
}
retryCount++;
}
};

View File

@ -26,6 +26,7 @@ import { TagClass } from '../support/tag/TagClass';
import { import {
clickOutside, clickOutside,
descriptionBox, descriptionBox,
readElementInListWithScroll,
redirectToHomePage, redirectToHomePage,
toastNotification, toastNotification,
uuid, uuid,
@ -486,8 +487,24 @@ export const assignCertification = async (
certification: TagClass, certification: TagClass,
endpoint: string endpoint: string
) => { ) => {
const certificationResponse = page.waitForResponse(
'/api/v1/tags?parent=Certification&limit=50'
);
await page.getByTestId('edit-certification').click(); await page.getByTestId('edit-certification').click();
await certificationResponse;
await page.waitForSelector('.certification-card-popover', {
state: 'visible',
});
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await readElementInListWithScroll(
page,
page.getByTestId(
`radio-btn-${certification.responseData.fullyQualifiedName}`
),
page.locator('[data-testid="certification-cards"] .ant-radio-group')
);
await page await page
.getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`) .getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`)
.click(); .click();
@ -503,6 +520,9 @@ export const assignCertification = async (
export const removeCertification = async (page: Page, endpoint: string) => { export const removeCertification = async (page: Page, endpoint: string) => {
await page.getByTestId('edit-certification').click(); await page.getByTestId('edit-certification').click();
await page.waitForSelector('.certification-card-popover', {
state: 'visible',
});
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`); const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
await page.getByTestId('clear-certification').click(); await page.getByTestId('clear-certification').click();

View File

@ -17,6 +17,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as CertificationIcon } from '../../assets/svg/ic-certification.svg'; import { ReactComponent as CertificationIcon } from '../../assets/svg/ic-certification.svg';
import { Tag } from '../../generated/entity/classification/tag'; import { Tag } from '../../generated/entity/classification/tag';
import { Paging } from '../../generated/type/paging';
import { getTags } from '../../rest/tagAPI'; import { getTags } from '../../rest/tagAPI';
import { getEntityName } from '../../utils/EntityUtils'; import { getEntityName } from '../../utils/EntityUtils';
import { stringToHTML } from '../../utils/StringsUtils'; import { stringToHTML } from '../../utils/StringsUtils';
@ -35,59 +36,84 @@ const Certification = ({
onClose, onClose,
}: CertificationProps) => { }: CertificationProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const popoverRef = useRef<any>(null); const popoverRef = useRef<{ close: () => void } | null>(null);
const [isLoadingCertificationData, setIsLoadingCertificationData] = const [isLoadingCertificationData, setIsLoadingCertificationData] =
useState<boolean>(false); useState<boolean>(false);
const [hasContentLoading, setHasContentLoading] = useState<boolean>(false);
const [certifications, setCertifications] = useState<Array<Tag>>([]); const [certifications, setCertifications] = useState<Array<Tag>>([]);
const [selectedCertification, setSelectedCertification] = useState<string>( const [selectedCertification, setSelectedCertification] = useState<string>(
currentCertificate ?? '' currentCertificate ?? ''
); );
const certificationCardData = useMemo(() => { const [paging, setPaging] = useState<Paging>({} as Paging);
return ( const [currentPage, setCurrentPage] = useState(1);
<Radio.Group
className="h-max-100 overflow-y-auto overflow-x-hidden"
value={selectedCertification}>
{certifications.map((certificate) => {
const tagSrc = getTagImageSrc(certificate.style?.iconURL ?? '');
const title = getEntityName(certificate);
const { id, fullyQualifiedName, description } = certificate;
return ( const getCertificationData = async (page = 1, append = false) => {
<div if (page === 1) {
className="certification-card-item cursor-pointer" setIsLoadingCertificationData(true);
key={id} } else {
style={{ cursor: 'pointer' }} setHasContentLoading(true);
onClick={() => { }
setSelectedCertification(fullyQualifiedName ?? '');
}}> try {
<Radio const response = await getTags({
className="certification-radio-top-right" parent: 'Certification',
data-testid={`radio-btn-${fullyQualifiedName}`} limit: 50,
value={fullyQualifiedName} after: page > 1 ? paging.after : undefined,
/> });
<div className="certification-card-content">
{tagSrc ? ( const { data, paging: newPaging } = response;
<img alt={title} src={tagSrc} />
) : ( // Sort certifications with Gold, Silver, Bronze first (only for initial load)
<div className="certification-icon"> const sortedData =
<CertificationIcon height={28} width={28} /> page === 1
</div> ? [...data].sort((a, b) => {
)} const order: Record<string, number> = {
<div> Gold: 0,
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-body"> Silver: 1,
{title} Bronze: 2,
</Typography.Paragraph> };
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
{stringToHTML(description)} const aName = getEntityName(a);
</Typography.Paragraph> const bName = getEntityName(b);
</div>
</div> const aOrder = order[aName] ?? 3;
</div> const bOrder = order[bName] ?? 3;
);
})} return aOrder - bOrder;
</Radio.Group> })
); : data;
}, [certifications, selectedCertification]);
if (append) {
setCertifications((prev) => [...prev, ...sortedData]);
} else {
setCertifications(sortedData);
}
setPaging(newPaging);
setCurrentPage(page);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.certification-plural-lowercase'),
})
);
} finally {
setIsLoadingCertificationData(false);
setHasContentLoading(false);
}
};
const handleScroll = async (e: React.UIEvent<HTMLDivElement>) => {
const { currentTarget } = e;
const isAtBottom =
currentTarget.scrollTop + currentTarget.offsetHeight >=
currentTarget.scrollHeight - 1; // -1 for precision tolerance
if (isAtBottom && paging.after && !hasContentLoading) {
await getCertificationData(currentPage + 1, true);
}
};
const updateCertificationData = async (value?: string) => { const updateCertificationData = async (value?: string) => {
setIsLoadingCertificationData(true); setIsLoadingCertificationData(true);
@ -98,43 +124,60 @@ const Certification = ({
setIsLoadingCertificationData(false); setIsLoadingCertificationData(false);
popoverRef.current?.close(); popoverRef.current?.close();
}; };
const getCertificationData = async () => {
setIsLoadingCertificationData(true);
try {
const { data } = await getTags({
parent: 'Certification',
limit: 50,
});
// Sort certifications with Gold, Silver, Bronze first const certificationCardData = useMemo(() => {
const sortedData = [...data].sort((a, b) => { return (
const order: Record<string, number> = { <div
Gold: 0, className="h-max-100 overflow-y-auto overflow-x-hidden"
Silver: 1, onScroll={handleScroll}>
Bronze: 2, <Radio.Group className="w-full" value={selectedCertification}>
}; {certifications.map((certificate) => {
const tagSrc = getTagImageSrc(certificate.style?.iconURL ?? '');
const title = getEntityName(certificate);
const { id, fullyQualifiedName, description } = certificate;
const aName = getEntityName(a); return (
const bName = getEntityName(b); <div
className="certification-card-item cursor-pointer"
const aOrder = order[aName] ?? 3; key={id}
const bOrder = order[bName] ?? 3; style={{ cursor: 'pointer' }}
onClick={() => {
return aOrder - bOrder; setSelectedCertification(fullyQualifiedName ?? '');
}); }}>
<Radio
setCertifications(sortedData); className="certification-radio-top-right"
} catch (err) { data-testid={`radio-btn-${fullyQualifiedName}`}
showErrorToast( value={fullyQualifiedName}
err as AxiosError, />
t('server.entity-fetch-error', { <div className="certification-card-content">
entity: t('label.certification-plural-lowercase'), {tagSrc ? (
}) <img alt={title} src={tagSrc} />
); ) : (
} finally { <div className="certification-icon">
setIsLoadingCertificationData(false); <CertificationIcon height={28} width={28} />
} </div>
}; )}
<div>
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-body">
{title}
</Typography.Paragraph>
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
{stringToHTML(description)}
</Typography.Paragraph>
</div>
</div>
</div>
);
})}
</Radio.Group>
{hasContentLoading && (
<div className="flex justify-center p-2">
<Loader size="small" />
</div>
)}
</div>
);
}, [certifications, selectedCertification, hasContentLoading, handleScroll]);
const handleCloseCertification = async () => { const handleCloseCertification = async () => {
popoverRef.current?.close(); popoverRef.current?.close();
@ -143,16 +186,19 @@ const Certification = ({
const onOpenChange = (visible: boolean) => { const onOpenChange = (visible: boolean) => {
if (visible) { if (visible) {
getCertificationData(); getCertificationData(1);
setSelectedCertification(currentCertificate); setSelectedCertification(currentCertificate);
setCurrentPage(1);
setPaging({} as Paging);
} else { } else {
setSelectedCertification(''); setSelectedCertification('');
setCertifications([]);
} }
}; };
useEffect(() => { useEffect(() => {
if (popoverProps?.open && certifications.length === 0) { if (popoverProps?.open && certifications.length === 0) {
getCertificationData(); getCertificationData(1);
} }
}, [popoverProps?.open]); }, [popoverProps?.open]);