mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 10:26:09 +00:00
Minor: Fix user details page bugs (#16358)
* Fix the empty string passed to the API when no displayName input is provided * Fix no characters displaying in the avatar when displayName is an empty string * Add tests for user page bug fixes * Resolve comments * Fix the failing unit tests
This commit is contained in:
parent
fe04b0a201
commit
fd942a27a6
@ -14,6 +14,7 @@
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { Button, Divider, Input, Space, Tooltip, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as EditIcon } from '../../../../../assets/svg/edit-new.svg';
|
||||
@ -94,14 +95,16 @@ const UserProfileDetails = ({
|
||||
[userData]
|
||||
);
|
||||
|
||||
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setDisplayName(e.target.value);
|
||||
const onDisplayNameChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => setDisplayName(e.target.value),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDisplayNameSave = useCallback(async () => {
|
||||
if (displayName !== userData.displayName) {
|
||||
setIsLoading(true);
|
||||
await updateUserDetails(
|
||||
{ displayName: displayName ?? '' },
|
||||
{ displayName: isEmpty(displayName) ? undefined : displayName },
|
||||
'displayName'
|
||||
);
|
||||
setIsLoading(false);
|
||||
@ -114,67 +117,70 @@ const UserProfileDetails = ({
|
||||
setIsDisplayNameEdit(false);
|
||||
}, [userData.displayName]);
|
||||
|
||||
const displayNameRenderComponent = useMemo(
|
||||
() =>
|
||||
isDisplayNameEdit ? (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
onCancel={handleCloseEditDisplayName}
|
||||
onSave={handleDisplayNameSave}>
|
||||
<Input
|
||||
className="w-full"
|
||||
data-testid="displayName"
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
placeholder={t('label.display-name')}
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={onDisplayNameChange}
|
||||
/>
|
||||
</InlineEdit>
|
||||
) : (
|
||||
<Space align="center">
|
||||
<Typography.Text
|
||||
className="font-medium text-md"
|
||||
data-testid="user-name"
|
||||
ellipsis={{ tooltip: true }}
|
||||
style={{ maxWidth: '400px' }}>
|
||||
{hasEditPermission
|
||||
? userData.displayName ||
|
||||
t('label.add-entity', { entity: t('label.display-name') })
|
||||
: getEntityName(userData)}
|
||||
</Typography.Text>
|
||||
{hasEditPermission && (
|
||||
<Tooltip
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.display-name'),
|
||||
})}>
|
||||
<EditIcon
|
||||
className="cursor-pointer align-middle"
|
||||
color={DE_ACTIVE_COLOR}
|
||||
data-testid="edit-displayName"
|
||||
{...ICON_DIMENSION}
|
||||
onClick={(e) => {
|
||||
// Used to stop click propagation event to parent User.component collapsible panel
|
||||
e.stopPropagation();
|
||||
setIsDisplayNameEdit(true);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
[
|
||||
userData,
|
||||
displayName,
|
||||
isDisplayNameEdit,
|
||||
hasEditPermission,
|
||||
getEntityName,
|
||||
onDisplayNameChange,
|
||||
handleDisplayNameSave,
|
||||
handleCloseEditDisplayName,
|
||||
]
|
||||
);
|
||||
const displayNameRenderComponent = useMemo(() => {
|
||||
const displayNamePlaceHolder = isEmpty(userData.displayName)
|
||||
? t('label.add-entity', { entity: t('label.display-name') })
|
||||
: userData.displayName;
|
||||
|
||||
const displayNameText = hasEditPermission
|
||||
? displayNamePlaceHolder
|
||||
: getEntityName(userData);
|
||||
|
||||
return isDisplayNameEdit ? (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
onCancel={handleCloseEditDisplayName}
|
||||
onSave={handleDisplayNameSave}>
|
||||
<Input
|
||||
className="w-full"
|
||||
data-testid="displayName"
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
placeholder={t('label.display-name')}
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={onDisplayNameChange}
|
||||
/>
|
||||
</InlineEdit>
|
||||
) : (
|
||||
<Space align="center">
|
||||
<Typography.Text
|
||||
className="font-medium text-md"
|
||||
data-testid="user-name"
|
||||
ellipsis={{ tooltip: true }}
|
||||
style={{ maxWidth: '400px' }}>
|
||||
{displayNameText}
|
||||
</Typography.Text>
|
||||
{hasEditPermission && (
|
||||
<Tooltip
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.display-name'),
|
||||
})}>
|
||||
<EditIcon
|
||||
className="cursor-pointer align-middle"
|
||||
color={DE_ACTIVE_COLOR}
|
||||
data-testid="edit-displayName"
|
||||
{...ICON_DIMENSION}
|
||||
onClick={(e) => {
|
||||
// Used to stop click propagation event to parent User.component collapsible panel
|
||||
e.stopPropagation();
|
||||
setIsDisplayNameEdit(true);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}, [
|
||||
userData,
|
||||
displayName,
|
||||
isDisplayNameEdit,
|
||||
hasEditPermission,
|
||||
getEntityName,
|
||||
onDisplayNameChange,
|
||||
handleDisplayNameSave,
|
||||
handleCloseEditDisplayName,
|
||||
]);
|
||||
|
||||
const changePasswordRenderComponent = useMemo(
|
||||
() =>
|
||||
|
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { AuthProvider } from '../../../../../generated/settings/settings';
|
||||
@ -305,23 +306,25 @@ describe('Test User Profile Details Component', () => {
|
||||
});
|
||||
|
||||
it('should call updateUserDetails on click of DisplayNameButton', async () => {
|
||||
render(<UserProfileDetails {...mockPropsData} />, {
|
||||
wrapper: MemoryRouter,
|
||||
await act(async () => {
|
||||
render(<UserProfileDetails {...mockPropsData} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('edit-displayName'));
|
||||
});
|
||||
|
||||
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('displayName'), {
|
||||
target: { value: 'test' },
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('display-name-save-button'));
|
||||
});
|
||||
|
||||
@ -331,6 +334,43 @@ describe('Test User Profile Details Component', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass displayName undefined to the updateUserDetails in case of empty string', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<UserProfileDetails
|
||||
{...mockPropsData}
|
||||
userData={{ ...mockPropsData.userData, displayName: 'Test' }}
|
||||
/>,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(screen.getByTestId('edit-displayName'));
|
||||
});
|
||||
|
||||
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('displayName'), {
|
||||
target: {
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(screen.getByTestId('display-name-save-button'));
|
||||
});
|
||||
|
||||
expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith(
|
||||
{ displayName: undefined },
|
||||
'displayName'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call updateUserDetails on click of PersonaSaveButton', async () => {
|
||||
render(<UserProfileDetails {...mockPropsData} />, {
|
||||
wrapper: MemoryRouter,
|
||||
|
@ -28,13 +28,11 @@ jest.mock('../../../../../utils/ProfilerUtils', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/ProfilePicture/ProfilePicture', () => {
|
||||
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
|
||||
return jest
|
||||
.fn()
|
||||
.mockImplementation(({ displayName }) => <p>{displayName}</p>);
|
||||
});
|
||||
|
||||
jest.mock('../../../../../utils/EntityUtils', () => ({
|
||||
getEntityName: jest.fn().mockReturnValue('getEntityName'),
|
||||
}));
|
||||
|
||||
describe('Test User User Profile Image Component', () => {
|
||||
it('should render user profile image component', async () => {
|
||||
render(<UserProfileImage {...mockPropsData} />);
|
||||
@ -49,7 +47,20 @@ describe('Test User User Profile Image Component', () => {
|
||||
|
||||
expect(screen.getByTestId('profile-image-container')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('ProfilePicture')).toBeInTheDocument();
|
||||
expect(screen.queryByText(mockUserData.name)).toBeNull();
|
||||
expect(screen.getByText(mockUserData.displayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fallback to name for the displayName prop of ProfilePicture', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<UserProfileImage userData={{ ...mockUserData, displayName: '' }} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('profile-image-container')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(mockUserData.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render user profile picture component with image', async () => {
|
||||
|
@ -14,7 +14,7 @@
|
||||
import { Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import Qs from 'qs';
|
||||
import {
|
||||
default as React,
|
||||
@ -88,8 +88,8 @@ const UserPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const ErrorPlaceholder = () => {
|
||||
return (
|
||||
const errorPlaceholder = useMemo(
|
||||
() => (
|
||||
<div
|
||||
className="d-flex items-center justify-center h-full"
|
||||
data-testid="error">
|
||||
@ -103,8 +103,9 @@ const UserPage = () => {
|
||||
/>
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
),
|
||||
[username]
|
||||
);
|
||||
|
||||
const updateUserDetails = useCallback(
|
||||
async (data: Partial<User>, key: keyof User) => {
|
||||
@ -128,7 +129,7 @@ const UserPage = () => {
|
||||
...currentUser,
|
||||
...updatedKeyData,
|
||||
};
|
||||
const newUserData = { ...userData, ...updatedKeyData };
|
||||
const newUserData: User = { ...userData, ...updatedKeyData };
|
||||
|
||||
if (key === 'defaultPersona') {
|
||||
if (isUndefined(response.defaultPersona)) {
|
||||
@ -140,7 +141,8 @@ const UserPage = () => {
|
||||
if (userData.id === currentUser?.id) {
|
||||
updateCurrentUser(newCurrentUserData as User);
|
||||
}
|
||||
setUserData(newUserData);
|
||||
// Omit the undefined values from the User object
|
||||
setUserData(omitBy(newUserData, isUndefined) as User);
|
||||
} else {
|
||||
throw t('message.unexpected-error');
|
||||
}
|
||||
@ -173,7 +175,7 @@ const UserPage = () => {
|
||||
}
|
||||
|
||||
if (isError && isEmpty(userData)) {
|
||||
return <ErrorPlaceholder />;
|
||||
return errorPlaceholder;
|
||||
}
|
||||
|
||||
return (
|
||||
|
Loading…
x
Reference in New Issue
Block a user