mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 18:36:08 +00:00
Accept/Reject All Suggestions (#15656)
* allow accept all and reject all suggestions * add tests --------- Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
parent
9309d55ac1
commit
94be958b68
@ -10,7 +10,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Suggestion } from '../../../generated/entity/feed/suggestion';
|
||||
import {
|
||||
Suggestion,
|
||||
SuggestionType,
|
||||
} from '../../../generated/entity/feed/suggestion';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
|
||||
export interface SuggestionsContextType {
|
||||
@ -18,6 +21,8 @@ export interface SuggestionsContextType {
|
||||
suggestions: Suggestion[];
|
||||
suggestionsByUser: Map<string, Suggestion[]>;
|
||||
loading: boolean;
|
||||
loadingAccept: boolean;
|
||||
loadingReject: boolean;
|
||||
allSuggestionsUsers: EntityReference[];
|
||||
onUpdateActiveUser: (user: EntityReference) => void;
|
||||
fetchSuggestions: (entityFqn: string) => void;
|
||||
@ -25,6 +30,10 @@ export interface SuggestionsContextType {
|
||||
suggestion: Suggestion,
|
||||
action: SuggestionAction
|
||||
) => void;
|
||||
acceptRejectAllSuggestions: (
|
||||
suggestionType: SuggestionType,
|
||||
status: SuggestionAction
|
||||
) => void;
|
||||
}
|
||||
|
||||
export enum SuggestionAction {
|
||||
|
@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SuggestionType } from '../../../generated/entity/feed/suggestion';
|
||||
import { mockEntityPermissions } from '../../../pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock';
|
||||
import {
|
||||
aproveRejectAllSuggestions,
|
||||
getSuggestionsList,
|
||||
updateSuggestionStatus,
|
||||
} from '../../../rest/suggestionsAPI';
|
||||
import SuggestionsProvider, {
|
||||
useSuggestionsContext,
|
||||
} from './SuggestionsProvider';
|
||||
import { SuggestionAction } from './SuggestionsProvider.interface';
|
||||
|
||||
const suggestions = [
|
||||
{
|
||||
id: '1',
|
||||
description: 'Test suggestion1',
|
||||
createdBy: { id: '1', name: 'Avatar 1', type: 'user' },
|
||||
entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
description: 'Test suggestion2',
|
||||
createdBy: { id: '2', name: 'Avatar 2', type: 'user' },
|
||||
entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('../../../hooks/useFqn', () => ({
|
||||
useFqn: jest.fn().mockReturnValue({ fqn: 'mockFQN' }),
|
||||
}));
|
||||
|
||||
jest.mock('../../../rest/suggestionsAPI', () => ({
|
||||
getSuggestionsList: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
aproveRejectAllSuggestions: jest.fn(),
|
||||
updateSuggestionStatus: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../context/PermissionProvider/PermissionProvider', () => ({
|
||||
usePermissionProvider: jest.fn().mockImplementation(() => ({
|
||||
permissions: mockEntityPermissions,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('SuggestionsProvider', () => {
|
||||
it('renders provider and fetches data', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<SuggestionsProvider>
|
||||
<TestComponent />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
});
|
||||
|
||||
expect(getSuggestionsList).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls approveRejectAllSuggestions when button is clicked', () => {
|
||||
render(
|
||||
<SuggestionsProvider>
|
||||
<TestComponent />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
|
||||
const button = screen.getByText('Active User');
|
||||
fireEvent.click(button);
|
||||
|
||||
const acceptAllBtn = screen.getByText('Accept All');
|
||||
fireEvent.click(acceptAllBtn);
|
||||
|
||||
expect(aproveRejectAllSuggestions).toHaveBeenCalledWith(
|
||||
'1',
|
||||
'mockFQN',
|
||||
SuggestionType.SuggestDescription,
|
||||
SuggestionAction.Accept
|
||||
);
|
||||
});
|
||||
|
||||
it('calls approveRejectAllSuggestions when reject button is clicked', () => {
|
||||
render(
|
||||
<SuggestionsProvider>
|
||||
<TestComponent />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
|
||||
const button = screen.getByText('Active User');
|
||||
fireEvent.click(button);
|
||||
|
||||
const rejectAll = screen.getByText('Reject All');
|
||||
fireEvent.click(rejectAll);
|
||||
|
||||
expect(aproveRejectAllSuggestions).toHaveBeenCalledWith(
|
||||
'1',
|
||||
'mockFQN',
|
||||
SuggestionType.SuggestDescription,
|
||||
SuggestionAction.Reject
|
||||
);
|
||||
});
|
||||
|
||||
it('calls accept suggestion when accept button is clicked', () => {
|
||||
render(
|
||||
<SuggestionsProvider>
|
||||
<TestComponent />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
|
||||
const acceptBtn = screen.getByText('Accept One');
|
||||
fireEvent.click(acceptBtn);
|
||||
|
||||
expect(updateSuggestionStatus).toHaveBeenCalledWith(
|
||||
suggestions[0],
|
||||
SuggestionAction.Accept
|
||||
);
|
||||
});
|
||||
|
||||
it('calls reject suggestion when accept button is clicked', () => {
|
||||
render(
|
||||
<SuggestionsProvider>
|
||||
<TestComponent />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
|
||||
const rejectBtn = screen.getByText('Reject One');
|
||||
fireEvent.click(rejectBtn);
|
||||
|
||||
expect(updateSuggestionStatus).toHaveBeenCalledWith(
|
||||
suggestions[0],
|
||||
SuggestionAction.Reject
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function TestComponent() {
|
||||
const {
|
||||
acceptRejectAllSuggestions,
|
||||
onUpdateActiveUser,
|
||||
acceptRejectSuggestion,
|
||||
} = useSuggestionsContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() =>
|
||||
acceptRejectAllSuggestions(
|
||||
SuggestionType.SuggestDescription,
|
||||
SuggestionAction.Accept
|
||||
)
|
||||
}>
|
||||
Accept All
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
acceptRejectAllSuggestions(
|
||||
SuggestionType.SuggestDescription,
|
||||
SuggestionAction.Reject
|
||||
)
|
||||
}>
|
||||
Reject All
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onUpdateActiveUser({ id: '1', name: 'Avatar 1', type: 'user' })
|
||||
}>
|
||||
Active User
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
acceptRejectSuggestion(suggestions[0], SuggestionAction.Accept)
|
||||
}>
|
||||
Accept One
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
acceptRejectSuggestion(suggestions[0], SuggestionAction.Reject)
|
||||
}>
|
||||
Reject One
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
@ -25,11 +25,15 @@ import React, {
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||
import { Suggestion } from '../../../generated/entity/feed/suggestion';
|
||||
import {
|
||||
Suggestion,
|
||||
SuggestionType,
|
||||
} from '../../../generated/entity/feed/suggestion';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import { useFqn } from '../../../hooks/useFqn';
|
||||
import { usePub } from '../../../hooks/usePubSub';
|
||||
import {
|
||||
aproveRejectAllSuggestions,
|
||||
getSuggestionsList,
|
||||
updateSuggestionStatus,
|
||||
} from '../../../rest/suggestionsAPI';
|
||||
@ -45,6 +49,9 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const { fqn: entityFqn } = useFqn();
|
||||
const [activeUser, setActiveUser] = useState<EntityReference>();
|
||||
const [loadingAccept, setLoadingAccept] = useState(false);
|
||||
const [loadingReject, setLoadingReject] = useState(false);
|
||||
|
||||
const [allSuggestionsUsers, setAllSuggestionsUsers] = useState<
|
||||
EntityReference[]
|
||||
>([]);
|
||||
@ -122,6 +129,38 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
||||
return suggestionsByUser.get(activeUser?.name ?? '') ?? [];
|
||||
}, [activeUser, suggestionsByUser]);
|
||||
|
||||
const acceptRejectAllSuggestions = useCallback(
|
||||
async (suggestionType: SuggestionType, status: SuggestionAction) => {
|
||||
if (status === SuggestionAction.Accept) {
|
||||
setLoadingAccept(true);
|
||||
} else {
|
||||
setLoadingReject(true);
|
||||
}
|
||||
try {
|
||||
await aproveRejectAllSuggestions(
|
||||
activeUser?.id ?? '',
|
||||
entityFqn,
|
||||
suggestionType,
|
||||
status
|
||||
);
|
||||
|
||||
await fetchSuggestions(entityFqn);
|
||||
if (status === SuggestionAction.Accept) {
|
||||
selectedUserSuggestions.forEach((suggestion) => {
|
||||
publish('updateDetails', suggestion);
|
||||
});
|
||||
}
|
||||
setActiveUser(undefined);
|
||||
} catch (err) {
|
||||
showErrorToast(err as AxiosError);
|
||||
} finally {
|
||||
setLoadingAccept(false);
|
||||
setLoadingReject(false);
|
||||
}
|
||||
},
|
||||
[activeUser, entityFqn, selectedUserSuggestions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(permissions) && !isEmpty(entityFqn)) {
|
||||
fetchSuggestions(entityFqn);
|
||||
@ -135,10 +174,13 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
||||
selectedUserSuggestions,
|
||||
entityFqn,
|
||||
loading,
|
||||
loadingAccept,
|
||||
loadingReject,
|
||||
allSuggestionsUsers,
|
||||
onUpdateActiveUser,
|
||||
fetchSuggestions,
|
||||
acceptRejectSuggestion,
|
||||
acceptRejectAllSuggestions,
|
||||
};
|
||||
}, [
|
||||
suggestions,
|
||||
@ -146,10 +188,13 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
||||
selectedUserSuggestions,
|
||||
entityFqn,
|
||||
loading,
|
||||
loadingAccept,
|
||||
loadingReject,
|
||||
allSuggestionsUsers,
|
||||
onUpdateActiveUser,
|
||||
fetchSuggestions,
|
||||
acceptRejectSuggestion,
|
||||
acceptRejectAllSuggestions,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider';
|
||||
import SuggestionsSlider from './SuggestionsSlider';
|
||||
|
||||
jest.mock('../SuggestionsProvider/SuggestionsProvider', () => ({
|
||||
useSuggestionsContext: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/AvatarCarousel/AvatarCarousel', () => {
|
||||
return jest.fn(() => <p>Avatar Carousel</p>);
|
||||
});
|
||||
|
||||
describe('SuggestionsSlider', () => {
|
||||
it('renders buttons when there are selected user suggestions', () => {
|
||||
(useSuggestionsContext as jest.Mock).mockReturnValue({
|
||||
selectedUserSuggestions: [{ id: '1' }, { id: '2' }],
|
||||
acceptRejectAllSuggestions: jest.fn(),
|
||||
loadingAccept: false,
|
||||
loadingReject: false,
|
||||
});
|
||||
|
||||
render(<SuggestionsSlider />);
|
||||
|
||||
expect(screen.getByTestId('accept-all-suggestions')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('reject-all-suggestions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls acceptRejectAllSuggestions on button click', () => {
|
||||
const acceptRejectAllSuggestions = jest.fn();
|
||||
(useSuggestionsContext as jest.Mock).mockReturnValue({
|
||||
selectedUserSuggestions: [{ id: '1' }, { id: '2' }],
|
||||
acceptRejectAllSuggestions,
|
||||
loadingAccept: false,
|
||||
loadingReject: false,
|
||||
});
|
||||
|
||||
render(<SuggestionsSlider />);
|
||||
fireEvent.click(screen.getByTestId('accept-all-suggestions'));
|
||||
|
||||
expect(acceptRejectAllSuggestions).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -13,11 +13,18 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
import React from 'react';
|
||||
import { SuggestionType } from '../../../generated/entity/feed/suggestion';
|
||||
import AvatarCarousel from '../../common/AvatarCarousel/AvatarCarousel';
|
||||
import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider';
|
||||
import { SuggestionAction } from '../SuggestionsProvider/SuggestionsProvider.interface';
|
||||
|
||||
const SuggestionsSlider = () => {
|
||||
const { selectedUserSuggestions } = useSuggestionsContext();
|
||||
const {
|
||||
selectedUserSuggestions,
|
||||
acceptRejectAllSuggestions,
|
||||
loadingAccept,
|
||||
loadingReject,
|
||||
} = useSuggestionsContext();
|
||||
|
||||
return (
|
||||
<div className="d-flex items-center gap-2">
|
||||
@ -27,12 +34,33 @@ const SuggestionsSlider = () => {
|
||||
<AvatarCarousel />
|
||||
{selectedUserSuggestions.length > 0 && (
|
||||
<>
|
||||
<Button size="small" type="primary">
|
||||
<Button
|
||||
data-testid="accept-all-suggestions"
|
||||
loading={loadingAccept}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
acceptRejectAllSuggestions(
|
||||
SuggestionType.SuggestDescription,
|
||||
SuggestionAction.Accept
|
||||
)
|
||||
}>
|
||||
<Typography.Text className="text-xs text-white">
|
||||
{t('label.accept-all')}
|
||||
</Typography.Text>
|
||||
</Button>
|
||||
<Button ghost size="small" type="primary">
|
||||
<Button
|
||||
ghost
|
||||
data-testid="reject-all-suggestions"
|
||||
loading={loadingReject}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
acceptRejectAllSuggestions(
|
||||
SuggestionType.SuggestDescription,
|
||||
SuggestionAction.Reject
|
||||
)
|
||||
}>
|
||||
<Typography.Text className="text-xs text-primary">
|
||||
{t('label.reject-all')}
|
||||
</Typography.Text>
|
||||
|
@ -13,7 +13,10 @@
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { PagingResponse } from 'Models';
|
||||
import { SuggestionAction } from '../components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface';
|
||||
import { Suggestion } from '../generated/entity/feed/suggestion';
|
||||
import {
|
||||
Suggestion,
|
||||
SuggestionType,
|
||||
} from '../generated/entity/feed/suggestion';
|
||||
import { ListParams } from '../interface/API.interface';
|
||||
import APIClient from './index';
|
||||
|
||||
@ -39,3 +42,19 @@ export const updateSuggestionStatus = (
|
||||
|
||||
return APIClient.put(url, {});
|
||||
};
|
||||
|
||||
export const aproveRejectAllSuggestions = (
|
||||
userId: string,
|
||||
entityFQN: string,
|
||||
suggestionType: SuggestionType,
|
||||
action: SuggestionAction
|
||||
): Promise<AxiosResponse> => {
|
||||
const url = `${BASE_URL}/${action}-all`;
|
||||
const params = {
|
||||
userId,
|
||||
entityFQN,
|
||||
suggestionType,
|
||||
};
|
||||
|
||||
return APIClient.put(url, {}, { params });
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user