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 {
clickOutside,
descriptionBox,
readElementInListWithScroll,
redirectToHomePage,
toastNotification,
uuid,
@ -486,8 +487,24 @@ export const assignCertification = async (
certification: TagClass,
endpoint: string
) => {
const certificationResponse = page.waitForResponse(
'/api/v1/tags?parent=Certification&limit=50'
);
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 readElementInListWithScroll(
page,
page.getByTestId(
`radio-btn-${certification.responseData.fullyQualifiedName}`
),
page.locator('[data-testid="certification-cards"] .ant-radio-group')
);
await page
.getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`)
.click();
@ -503,6 +520,9 @@ export const assignCertification = async (
export const removeCertification = async (page: Page, endpoint: string) => {
await page.getByTestId('edit-certification').click();
await page.waitForSelector('.certification-card-popover', {
state: 'visible',
});
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
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 { ReactComponent as CertificationIcon } from '../../assets/svg/ic-certification.svg';
import { Tag } from '../../generated/entity/classification/tag';
import { Paging } from '../../generated/type/paging';
import { getTags } from '../../rest/tagAPI';
import { getEntityName } from '../../utils/EntityUtils';
import { stringToHTML } from '../../utils/StringsUtils';
@ -35,59 +36,84 @@ const Certification = ({
onClose,
}: CertificationProps) => {
const { t } = useTranslation();
const popoverRef = useRef<any>(null);
const popoverRef = useRef<{ close: () => void } | null>(null);
const [isLoadingCertificationData, setIsLoadingCertificationData] =
useState<boolean>(false);
const [hasContentLoading, setHasContentLoading] = useState<boolean>(false);
const [certifications, setCertifications] = useState<Array<Tag>>([]);
const [selectedCertification, setSelectedCertification] = useState<string>(
currentCertificate ?? ''
);
const certificationCardData = useMemo(() => {
return (
<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;
const [paging, setPaging] = useState<Paging>({} as Paging);
const [currentPage, setCurrentPage] = useState(1);
return (
<div
className="certification-card-item cursor-pointer"
key={id}
style={{ cursor: 'pointer' }}
onClick={() => {
setSelectedCertification(fullyQualifiedName ?? '');
}}>
<Radio
className="certification-radio-top-right"
data-testid={`radio-btn-${fullyQualifiedName}`}
value={fullyQualifiedName}
/>
<div className="certification-card-content">
{tagSrc ? (
<img alt={title} src={tagSrc} />
) : (
<div className="certification-icon">
<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>
);
}, [certifications, selectedCertification]);
const getCertificationData = async (page = 1, append = false) => {
if (page === 1) {
setIsLoadingCertificationData(true);
} else {
setHasContentLoading(true);
}
try {
const response = await getTags({
parent: 'Certification',
limit: 50,
after: page > 1 ? paging.after : undefined,
});
const { data, paging: newPaging } = response;
// Sort certifications with Gold, Silver, Bronze first (only for initial load)
const sortedData =
page === 1
? [...data].sort((a, b) => {
const order: Record<string, number> = {
Gold: 0,
Silver: 1,
Bronze: 2,
};
const aName = getEntityName(a);
const bName = getEntityName(b);
const aOrder = order[aName] ?? 3;
const bOrder = order[bName] ?? 3;
return aOrder - bOrder;
})
: data;
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) => {
setIsLoadingCertificationData(true);
@ -98,43 +124,60 @@ const Certification = ({
setIsLoadingCertificationData(false);
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 sortedData = [...data].sort((a, b) => {
const order: Record<string, number> = {
Gold: 0,
Silver: 1,
Bronze: 2,
};
const certificationCardData = useMemo(() => {
return (
<div
className="h-max-100 overflow-y-auto overflow-x-hidden"
onScroll={handleScroll}>
<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);
const bName = getEntityName(b);
const aOrder = order[aName] ?? 3;
const bOrder = order[bName] ?? 3;
return aOrder - bOrder;
});
setCertifications(sortedData);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.certification-plural-lowercase'),
})
);
} finally {
setIsLoadingCertificationData(false);
}
};
return (
<div
className="certification-card-item cursor-pointer"
key={id}
style={{ cursor: 'pointer' }}
onClick={() => {
setSelectedCertification(fullyQualifiedName ?? '');
}}>
<Radio
className="certification-radio-top-right"
data-testid={`radio-btn-${fullyQualifiedName}`}
value={fullyQualifiedName}
/>
<div className="certification-card-content">
{tagSrc ? (
<img alt={title} src={tagSrc} />
) : (
<div className="certification-icon">
<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 () => {
popoverRef.current?.close();
@ -143,16 +186,19 @@ const Certification = ({
const onOpenChange = (visible: boolean) => {
if (visible) {
getCertificationData();
getCertificationData(1);
setSelectedCertification(currentCertificate);
setCurrentPage(1);
setPaging({} as Paging);
} else {
setSelectedCertification('');
setCertifications([]);
}
};
useEffect(() => {
if (popoverProps?.open && certifications.length === 0) {
getCertificationData();
getCertificationData(1);
}
}, [popoverProps?.open]);