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:
Karan Hotchandani 2024-03-22 13:07:05 +05:30 committed by GitHub
parent 9309d55ac1
commit 94be958b68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 355 additions and 6 deletions

View File

@ -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 {

View File

@ -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>
</>
);
}

View File

@ -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 (

View File

@ -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();
});
});

View File

@ -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>

View File

@ -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 });
};