mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-28 18:35:07 +00:00
supported scroll in certification popover list (#23534)
This commit is contained in:
parent
bcae472cc0
commit
f934b79d65
@ -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++;
|
||||
}
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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]);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user