diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts index beae0a748df..78152fcea5d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts @@ -30,6 +30,7 @@ export interface SuggestionsContextType { allSuggestionsUsers: EntityReference[]; onUpdateActiveUser: (user?: EntityReference) => void; fetchSuggestions: () => void; + fetchSuggestionsByUserId: (userId: string, limit?: number) => void; acceptRejectSuggestion: ( suggestion: Suggestion, action: SuggestionAction diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.test.tsx index 1108fdb5c76..e72ac4bc1fe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.test.tsx @@ -45,6 +45,7 @@ jest.mock('../../../hooks/useFqn', () => ({ jest.mock('../../../rest/suggestionsAPI', () => ({ getSuggestionsList: jest.fn().mockImplementation(() => Promise.resolve()), + getSuggestionsByUserId: jest.fn().mockImplementation(() => Promise.resolve()), approveRejectAllSuggestions: jest.fn(), updateSuggestionStatus: jest.fn(), })); @@ -144,6 +145,26 @@ describe('SuggestionsProvider', () => { SuggestionAction.Reject ); }); + + it('calls fetchSuggestionsByUserId when button is clicked', async () => { + const { getSuggestionsByUserId } = await import( + '../../../rest/suggestionsAPI' + ); + + render( + + + + ); + + const fetchByUserIdBtn = screen.getByText('Fetch By User ID'); + fireEvent.click(fetchByUserIdBtn); + + expect(getSuggestionsByUserId).toHaveBeenCalledWith('test-user-id', { + entityFQN: 'mockFQN', + limit: 10, + }); + }); }); function TestComponent() { @@ -151,6 +172,7 @@ function TestComponent() { acceptRejectAllSuggestions, onUpdateActiveUser, acceptRejectSuggestion, + fetchSuggestionsByUserId, } = useSuggestionsContext(); return ( @@ -181,6 +203,9 @@ function TestComponent() { }> Reject One + ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx index 97be6d74488..e0f41c8d92a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx @@ -35,6 +35,7 @@ import { useFqn } from '../../../hooks/useFqn'; import { usePub } from '../../../hooks/usePubSub'; import { approveRejectAllSuggestions, + getSuggestionsByUserId, getSuggestionsList, updateSuggestionStatus, } from '../../../rest/suggestionsAPI'; @@ -97,6 +98,43 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { [entityFqn, suggestionLimit] ); + const fetchSuggestionsByUserId = useCallback( + async (userId: string, limit?: number) => { + setLoading(true); + try { + const { data } = await getSuggestionsByUserId(userId, { + entityFQN: entityFqn, + limit: limit ?? suggestionLimit, + }); + + // Merge new suggestions with existing ones, removing duplicates by ID + setSuggestions((prevSuggestions) => { + const existingIds = new Set(prevSuggestions.map((s) => s.id)); + const newSuggestions = data.filter((s) => !existingIds.has(s.id)); + const mergedSuggestions = [...prevSuggestions, ...newSuggestions]; + + // Update grouped suggestions with merged data + const { allUsersList, groupedSuggestions } = + getSuggestionByType(mergedSuggestions); + setAllSuggestionsUsers(uniqWith(allUsersList, isEqual)); + setSuggestionsByUser(groupedSuggestions); + + return mergedSuggestions; + }); + } catch (err) { + showErrorToast( + err as AxiosError, + t('server.entity-fetch-error', { + entity: t('label.suggestion-lowercase-plural'), + }) + ); + } finally { + setLoading(false); + } + }, + [entityFqn, suggestionLimit] + ); + const acceptRejectSuggestion = useCallback( async (suggestion: Suggestion, status: SuggestionAction) => { try { @@ -188,6 +226,7 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { allSuggestionsUsers, onUpdateActiveUser, fetchSuggestions, + fetchSuggestionsByUserId, acceptRejectSuggestion, acceptRejectAllSuggestions, }; @@ -203,6 +242,7 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { allSuggestionsUsers, onUpdateActiveUser, fetchSuggestions, + fetchSuggestionsByUserId, acceptRejectSuggestion, acceptRejectAllSuggestions, ]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.test.tsx index d0eefcd2203..8203e566cd4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.test.tsx @@ -13,6 +13,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { EntityReference } from '../../../generated/entity/type'; +import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; import AvatarCarouselItem from './AvatarCarouselItem'; const suggestions = [ @@ -46,6 +47,7 @@ jest.mock('../../Suggestions/SuggestionsProvider/SuggestionsProvider', () => ({ acceptRejectSuggestion: jest.fn(), selectedUserSuggestions: [], onUpdateActiveUser: jest.fn(), + fetchSuggestionsByUserId: jest.fn(), })), __esModule: true, default: 'SuggestionsProvider', @@ -101,6 +103,37 @@ describe('AvatarCarouselItem', () => { expect(onAvatarClick).toHaveBeenCalledWith(index); }); + it('calls fetchSuggestionsByUserId when avatar is clicked', () => { + const mockFetchSuggestionsByUserId = jest.fn(); + (useSuggestionsContext as jest.Mock).mockImplementation(() => ({ + suggestions: suggestions, + suggestionsByUser: suggByUser, + allSuggestionsUsers: [ + { id: '1', name: 'Avatar 1', type: 'user' }, + { id: '2', name: 'Avatar 2', type: 'user' }, + ], + acceptRejectSuggestion: jest.fn(), + selectedUserSuggestions: [], + onUpdateActiveUser: jest.fn(), + fetchSuggestionsByUserId: mockFetchSuggestionsByUserId, + })); + + const { getByTestId } = render( + + ); + + const button = getByTestId(`avatar-carousel-item-${avatar.id}`); + button.click(); + + expect(mockFetchSuggestionsByUserId).toHaveBeenCalledWith(avatar.id); + }); + it('sets isActive class when isActive is true', () => { const { getByTestId } = render( { - const { suggestionsByUser } = useSuggestionsContext(); + const { suggestionsByUser, fetchSuggestionsByUserId } = + useSuggestionsContext(); const buttonRef = useRef(null); avatarBtnRefs.current[index] = buttonRef; const getUserSuggestionsCount = useCallback( @@ -42,6 +43,16 @@ const AvatarCarouselItem = ({ [suggestionsByUser] ); + const handleAvatarClick = useCallback(() => { + // Call the original onAvatarClick function + onAvatarClick(index); + + // Fetch suggestions for this specific user + if (avatar.id) { + fetchSuggestionsByUserId(avatar.id); + } + }, [onAvatarClick, index, avatar.id, fetchSuggestionsByUserId]); + const button = ( ); diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/suggestionsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/suggestionsAPI.ts index 4d188aa2884..469c509bb23 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/suggestionsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/suggestionsAPI.ts @@ -25,6 +25,7 @@ const BASE_URL = '/suggestions'; export type ListSuggestionsParams = ListParams & { entityFQN?: string; limit?: number; + userId?: string; }; export const getSuggestionsList = async (params?: ListSuggestionsParams) => { @@ -35,6 +36,20 @@ export const getSuggestionsList = async (params?: ListSuggestionsParams) => { return response.data; }; +export const getSuggestionsByUserId = async ( + userId: string, + params?: Omit +) => { + const response = await APIClient.get>(BASE_URL, { + params: { + ...params, + userId, + }, + }); + + return response.data; +}; + export const updateSuggestionStatus = ( data: Suggestion, action: SuggestionAction