feat(ui): support async delete for glossary and terms (#20711)

* support async delete for glossary and terms

* fix playwright test according to async delete one

* remove unit test

* after delete of glossary, we will stay on same page and let user to move or refresh the page there

* updated the success message
This commit is contained in:
Ashish Gupta 2025-04-10 10:40:42 +05:30 committed by GitHub
parent 67caf5e3a8
commit 9b99c6db04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 41 additions and 94 deletions

View File

@ -118,7 +118,7 @@ export const getEntityTypeSearchIndexMapping = (entityType: string) => {
export const toastNotification = async ( export const toastNotification = async (
page: Page, page: Page,
message: string | RegExp, message: string | RegExp,
timeout: number timeout?: number
) => { ) => {
await expect(page.getByTestId('alert-bar')).toHaveText(message, { timeout }); await expect(page.getByTestId('alert-bar')).toHaveText(message, { timeout });

View File

@ -829,17 +829,13 @@ export const deleteGlossaryOrGlossaryTerm = async (
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE'); await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
const endpoint = isGlossaryTerm const endpoint = isGlossaryTerm
? '/api/v1/glossaryTerms/*' ? '/api/v1/glossaryTerms/async/*'
: '/api/v1/glossaries/*'; : '/api/v1/glossaries/async/*';
const deleteRes = page.waitForResponse(endpoint); const deleteRes = page.waitForResponse(endpoint);
await page.click('[data-testid="confirm-button"]'); await page.click('[data-testid="confirm-button"]');
await deleteRes; await deleteRes;
if (isGlossaryTerm) { await toastNotification(page, /deleted successfully!/);
await toastNotification(page, /"Glossary Term" deleted successfully!/);
} else {
await toastNotification(page, /"Glossary" deleted successfully!/);
}
}; };
export const addSynonyms = async (page: Page, synonyms: string[]) => { export const addSynonyms = async (page: Page, synonyms: string[]) => {

View File

@ -23,6 +23,7 @@ import React, {
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { DeleteType } from '../../../components/common/DeleteWidget/DeleteWidget.interface';
import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../../../components/common/Loader/Loader'; import Loader from '../../../components/common/Loader/Loader';
import ResizableLeftPanels from '../../../components/common/ResizablePanels/ResizableLeftPanels'; import ResizableLeftPanels from '../../../components/common/ResizablePanels/ResizableLeftPanels';
@ -39,10 +40,15 @@ import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { PAGE_SIZE_LARGE, ROUTES } from '../../../constants/constants'; import { PAGE_SIZE_LARGE, ROUTES } from '../../../constants/constants';
import { GLOSSARIES_DOCS } from '../../../constants/docs.constants'; import { GLOSSARIES_DOCS } from '../../../constants/docs.constants';
import { observerOptions } from '../../../constants/Mydata.constants'; import { observerOptions } from '../../../constants/Mydata.constants';
import { useAsyncDeleteProvider } from '../../../context/AsyncDeleteProvider/AsyncDeleteProvider';
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../../enums/common.enum'; import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../../enums/common.enum';
import { EntityAction, TabSpecificField } from '../../../enums/entity.enum'; import {
EntityAction,
EntityType,
TabSpecificField,
} from '../../../enums/entity.enum';
import { Glossary } from '../../../generated/entity/data/glossary'; import { Glossary } from '../../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { Operation } from '../../../generated/entity/policies/policy'; import { Operation } from '../../../generated/entity/policies/policy';
@ -51,8 +57,6 @@ import { usePaging } from '../../../hooks/paging/usePaging';
import { useElementInView } from '../../../hooks/useElementInView'; import { useElementInView } from '../../../hooks/useElementInView';
import { useFqn } from '../../../hooks/useFqn'; import { useFqn } from '../../../hooks/useFqn';
import { import {
deleteGlossary,
deleteGlossaryTerm,
getGlossariesList, getGlossariesList,
getGlossaryTermByFQN, getGlossaryTermByFQN,
patchGlossaries, patchGlossaries,
@ -64,7 +68,7 @@ import Fqn from '../../../utils/Fqn';
import i18n from '../../../utils/i18next/LocalUtil'; import i18n from '../../../utils/i18next/LocalUtil';
import { checkPermission } from '../../../utils/PermissionsUtils'; import { checkPermission } from '../../../utils/PermissionsUtils';
import { getGlossaryPath } from '../../../utils/RouterUtils'; import { getGlossaryPath } from '../../../utils/RouterUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { showErrorToast } from '../../../utils/ToastUtils';
import GlossaryLeftPanel from '../GlossaryLeftPanel/GlossaryLeftPanel.component'; import GlossaryLeftPanel from '../GlossaryLeftPanel/GlossaryLeftPanel.component';
const GlossaryPage = () => { const GlossaryPage = () => {
@ -72,6 +76,7 @@ const GlossaryPage = () => {
const { fqn: glossaryFqn } = useFqn(); const { fqn: glossaryFqn } = useFqn();
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const { handleOnAsyncEntityDeleteConfirm } = useAsyncDeleteProvider();
const { action } = useParams<{ action: EntityAction }>(); const { action } = useParams<{ action: EntityAction }>();
const [initialised, setInitialised] = useState(false); const [initialised, setInitialised] = useState(false);
@ -328,22 +333,14 @@ const GlossaryPage = () => {
const handleGlossaryDelete = useCallback( const handleGlossaryDelete = useCallback(
async (id: string) => { async (id: string) => {
try { try {
await deleteGlossary(id); await handleOnAsyncEntityDeleteConfirm({
showSuccessToast( entityName: activeGlossary?.name,
t('server.entity-deleted-successfully', { entityId: id,
entity: t('label.glossary'), entityType: EntityType.GLOSSARY,
}) deleteType: DeleteType.HARD_DELETE,
); prepareType: true,
setIsLoading(true); isRecursiveDelete: true,
// check if the glossary is available });
const updatedGlossaries = glossaries.filter((item) => item.id !== id);
setGlossaries(updatedGlossaries);
const glossaryPath =
updatedGlossaries.length > 0
? getGlossaryPath(updatedGlossaries[0].fullyQualifiedName)
: getGlossaryPath();
history.push(glossaryPath);
} catch (error) { } catch (error) {
showErrorToast( showErrorToast(
error as AxiosError, error as AxiosError,
@ -351,11 +348,9 @@ const GlossaryPage = () => {
entity: t('label.glossary'), entity: t('label.glossary'),
}) })
); );
} finally {
setIsLoading(false);
} }
}, },
[glossaries, history] [glossaries, activeGlossary]
); );
const handleGlossaryTermUpdate = useCallback( const handleGlossaryTermUpdate = useCallback(
@ -393,13 +388,14 @@ const GlossaryPage = () => {
const handleGlossaryTermDelete = useCallback( const handleGlossaryTermDelete = useCallback(
async (id: string) => { async (id: string) => {
try { try {
await deleteGlossaryTerm(id); await handleOnAsyncEntityDeleteConfirm({
entityName: activeGlossary?.name,
showSuccessToast( entityId: id,
t('server.entity-deleted-successfully', { entityType: EntityType.GLOSSARY_TERM,
entity: t('label.glossary-term'), deleteType: DeleteType.HARD_DELETE,
}) prepareType: true,
); isRecursiveDelete: true,
});
let fqn; let fqn;
if (glossaryFqn) { if (glossaryFqn) {
@ -407,10 +403,7 @@ const GlossaryPage = () => {
fqnArr.pop(); fqnArr.pop();
fqn = fqnArr.join(FQN_SEPARATOR_CHAR); fqn = fqnArr.join(FQN_SEPARATOR_CHAR);
} }
setIsLoading(true);
history.push(getGlossaryPath(fqn)); history.push(getGlossaryPath(fqn));
fetchGlossaryList();
} catch (err) { } catch (err) {
showErrorToast( showErrorToast(
err as AxiosError, err as AxiosError,
@ -420,7 +413,7 @@ const GlossaryPage = () => {
); );
} }
}, },
[glossaryFqn, history, fetchGlossaryList] [glossaryFqn, activeGlossary]
); );
const handleAssetClick = useCallback( const handleAssetClick = useCallback(

View File

@ -14,11 +14,7 @@
import { act, fireEvent, render, screen } from '@testing-library/react'; import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { MOCK_GLOSSARY } from '../../../mocks/Glossary.mock'; import { MOCK_GLOSSARY } from '../../../mocks/Glossary.mock';
import { import { patchGlossaryTerm } from '../../../rest/glossaryAPI';
deleteGlossary,
deleteGlossaryTerm,
patchGlossaryTerm,
} from '../../../rest/glossaryAPI';
import GlossaryPage from './GlossaryPage.component'; import GlossaryPage from './GlossaryPage.component';
jest.mock('../../../hooks/useFqn', () => ({ jest.mock('../../../hooks/useFqn', () => ({
@ -101,16 +97,18 @@ jest.mock('../GlossaryLeftPanel/GlossaryLeftPanel.component', () => {
<div data-testid="glossary-left-panel-container">Left Panel</div> <div data-testid="glossary-left-panel-container">Left Panel</div>
)); ));
}); });
jest.mock('../../../rest/glossaryAPI', () => ({ jest.mock('../../../rest/glossaryAPI', () => ({
deleteGlossary: jest.fn().mockImplementation(() => Promise.resolve()), deleteGlossary: jest.fn().mockImplementation(() => Promise.resolve()),
deleteGlossaryTerm: jest.fn().mockImplementation(() => Promise.resolve()), deleteGlossaryTerm: jest.fn().mockImplementation(() => Promise.resolve()),
getGlossaryTermByFQN: jest getGlossaryTermByFQN: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve({ data: MOCK_GLOSSARY })), .mockImplementation(() => Promise.resolve({ data: MOCK_GLOSSARY })),
getGlossariesList: jest getGlossariesList: jest.fn().mockImplementation(() =>
.fn() Promise.resolve({
.mockImplementation(() => data: [MOCK_GLOSSARY],
Promise.resolve({ data: [MOCK_GLOSSARY], paging: { total: 1 } }) paging: { total: 1 },
})
), ),
patchGlossaryTerm: jest patchGlossaryTerm: jest
.fn() .fn()
@ -180,38 +178,6 @@ describe('Test GlossaryComponent page', () => {
}); });
describe('Render Sad Paths', () => { describe('Render Sad Paths', () => {
it('show error if deleteGlossaryTerm API fails', async () => {
(deleteGlossaryTerm as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
render(<GlossaryPage />);
const handleGlossaryTermDelete = await screen.findByTestId(
'handleGlossaryTermDelete'
);
expect(handleGlossaryTermDelete).toBeInTheDocument();
await act(async () => {
fireEvent.click(handleGlossaryTermDelete);
});
});
it('show error if deleteGlossary API fails', async () => {
(deleteGlossary as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
render(<GlossaryPage />);
const handleGlossaryDelete = await screen.findByTestId(
'handleGlossaryDelete'
);
expect(handleGlossaryDelete).toBeInTheDocument();
await act(async () => {
fireEvent.click(handleGlossaryDelete);
});
});
it('show error if patchGlossaryTerm API resolves without data', async () => { it('show error if patchGlossaryTerm API resolves without data', async () => {
(patchGlossaryTerm as jest.Mock).mockImplementation(() => (patchGlossaryTerm as jest.Mock).mockImplementation(() =>
Promise.resolve({ data: '' }) Promise.resolve({ data: '' })

View File

@ -164,16 +164,6 @@ export const patchGlossaryTerm = async (id: string, patch: Operation[]) => {
return response.data; return response.data;
}; };
export const deleteGlossary = (id: string) => {
return APIClient.delete(`/glossaries/${id}?recursive=true&hardDelete=true`);
};
export const deleteGlossaryTerm = (id: string) => {
return APIClient.delete(
`/glossaryTerms/${id}?recursive=true&hardDelete=true`
);
};
export const exportGlossaryInCSVFormat = async (glossaryName: string) => { export const exportGlossaryInCSVFormat = async (glossaryName: string) => {
const response = await APIClient.get<CSVExportResponse>( const response = await APIClient.get<CSVExportResponse>(
`/glossaries/name/${getEncodedFqn(glossaryName)}/exportAsync` `/glossaries/name/${getEncodedFqn(glossaryName)}/exportAsync`

View File

@ -35,6 +35,8 @@ class DeleteWidgetClassBase {
return `services/${entityType}s`; return `services/${entityType}s`;
} else if (entityType === EntityType.GLOSSARY) { } else if (entityType === EntityType.GLOSSARY) {
return `glossaries`; return `glossaries`;
} else if (entityType === EntityType.GLOSSARY_TERM) {
return `glossaryTerms`;
} else if (entityType === EntityType.POLICY) { } else if (entityType === EntityType.POLICY) {
return 'policies'; return 'policies';
} else if (entityType === EntityType.KPI) { } else if (entityType === EntityType.KPI) {