Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent profile updates without changes #2849

Merged
merged 18 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/containers/edit-profile/profile-tab/ProfileTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
updateProfileData,
updateValidityStatus
} from '~/redux/features/editProfileSlice'
import { EditProfileForm, MainUserRole } from '~/types'
import { EditProfileForm, MainUserRole, UserRoleEnum } from '~/types'
import { styles } from '~/containers/edit-profile/profile-tab/ProfileTab.styles'
import { scrollToAndHighlight } from '~/utils/scroll-and-highlight'
import { fieldsWithIncreasedHeight } from '~/components/profile-item/complete-profile.constants'
Expand Down Expand Up @@ -42,10 +42,7 @@ const ProfileTab: FC = () => {
nativeLanguage,
photo: photo ?? '',
professionalSummary: professionalSummary || '',
videoLink:
typeof videoLink === 'string'
? videoLink
: (videoLink?.[userRole as MainUserRole] ?? '')
videoLink: videoLink?.[userRole as MainUserRole] || ''
}

const {
Expand All @@ -62,7 +59,15 @@ const ProfileTab: FC = () => {
})

const debouncedUpdateProfileData = useDebounce(() => {
void dispatch(updateProfileData(data))
const payload = {
...data,
videoLink: {
student: userRole === UserRoleEnum.Student ? data.videoLink : null,
tutor: userRole === UserRoleEnum.Tutor ? data.videoLink : null
}
}

void dispatch(updateProfileData(payload))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider removing the void operator for better maintainability.
Link

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved ✅

}, 300)

const { hash, pathname } = useLocation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ const ProfileTabForm: FC<ProfileTabFormProps> = ({
void resizeImage(files[0])
}
const handleRemovePhoto = () => {
const updatedPhoto =
typeof photo === 'string' ? null : { src: '', name: '' }
const updatedPhoto = ''
handleNonInputValueChange('photo', updatedPhoto)
}

Expand Down
14 changes: 11 additions & 3 deletions src/containers/navigation-icons/AccountIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { defaultResponses } from '~/constants'

import { styles } from '~/containers/navigation-icons/NavigationIcons.styles'

import { UserResponse, UserRole } from '~/types'
import { UpdatedPhoto, UserResponse, UserRole } from '~/types'
import { createUrlPath } from '~/utils/helper-functions'
import { isUpdatedPhoto } from '~/utils/is-updated-photo'

interface AccountIconProps {
openMenu: (event: MouseEvent) => void
Expand All @@ -40,8 +41,15 @@ const AccountIcon: FC<AccountIconProps> = ({ openMenu }) => {
const { photo: statePhoto } = useAppSelector((state) => state.editProfile)

const avatarSrc = useMemo(() => {
if (statePhoto?.src) {
return statePhoto.src
if (isUpdatedPhoto(statePhoto)) {
return (statePhoto as UpdatedPhoto).src
}

if (typeof statePhoto === 'string') {
return createUrlPath(
import.meta.env.VITE_APP_IMG_USER_URL || '',
statePhoto
)
}

if (photo) {
Expand Down
51 changes: 33 additions & 18 deletions src/pages/edit-profile/EditProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import Box from '@mui/material/Box'
import Divider from '@mui/material/Divider'

import useConfirm from '~/hooks/use-confirm'

import { useAppDispatch, useAppSelector } from '~/hooks/use-redux'
import Loader from '~/components/loader/Loader'
import PageWrapper from '~/components/page-wrapper/PageWrapper'
Expand All @@ -23,11 +22,10 @@ import {
SizeEnum,
UpdateUserParams,
UserProfileTabsEnum,
UserRole
UserRole,
DataByRole
} from '~/types'
import { tabsData } from '~/pages/edit-profile/EditProfile.constants'

import { styles } from '~/pages/edit-profile/EditProfile.styles'
import {
fetchUserById,
updateUser,
Expand All @@ -38,6 +36,9 @@ import { openAlert } from '~/redux/features/snackbarSlice'
import { snackbarVariants } from '~/constants'
import { authRoutes } from '~/router/constants/authRoutes'

import { styles } from '~/pages/edit-profile/EditProfile.styles'
import { hasPhotoChanges } from '~/utils/has-photo-changes'

const EditProfile = () => {
const [initialEditProfileState, setInitialEditProfileState] = useState<
typeof profileState | null
Expand Down Expand Up @@ -74,8 +75,8 @@ const EditProfile = () => {
activeTab === UserProfileTabsEnum.PasswordAndSecurity

const hasChanges = (
initialData: Partial<EditProfileState>,
currentData: Partial<EditProfileState>
initialData: Partial<EditProfileState> | DataByRole<string>,
currentData: Partial<EditProfileState> | DataByRole<string>
): boolean => {
return JSON.stringify(initialData) !== JSON.stringify(currentData)
}
Expand All @@ -87,6 +88,10 @@ const EditProfile = () => {
)
}
void fetchData()

return () => {
void fetchData()
}
ShadowOfTheSpace marked this conversation as resolved.
Show resolved Hide resolved
}, [dispatch, userId, userRole])

useEffect(() => {
Expand All @@ -100,19 +105,29 @@ const EditProfile = () => {

const changedFields = useMemo<Partial<EditProfileState>>(() => {
if (!initialEditProfileState || !profileState) return {}
const { videoLink: initialVideoLink } = initialEditProfileState
const { videoLink: currentVideoLink } = profileState

const { photo: initialPhoto, ...initialData } = initialEditProfileState
const { photo: currentPhoto, ...currentData } = profileState

const hasChanged =
hasChanges(initialData, currentData) || initialPhoto !== currentPhoto
const hasPhotoChanged = hasPhotoChanges(initialPhoto, currentPhoto)

const hasChanged = hasChanges(initialData, currentData) || hasPhotoChanged

if (hasChanged) {
const changes: Partial<EditProfileState> = { ...currentData }
const changes: Partial<EditProfileState> = {
...currentData
}

if (!hasChanges(initialVideoLink, currentVideoLink)) {
delete changes.videoLink
}

if (initialPhoto === currentPhoto) {
delete changes.photo
if (hasPhotoChanged) {
changes.photo = currentPhoto
}

return changes
} else {
return {}
Expand Down Expand Up @@ -148,18 +163,18 @@ const EditProfile = () => {
notificationSettings,
professionalBlock,
categories,
photo,
...rest
} = changedFields

const dataToUpdate: UpdateUserParams = rest

if (city && country) dataToUpdate.address = { city, country }

if (typeof videoLink === 'string' || typeof videoLink === 'undefined') {
dataToUpdate.videoLink = videoLink ?? ''
} else if (typeof videoLink === 'object') {
dataToUpdate.videoLink =
videoLink[userRole as keyof typeof videoLink] || ''
if (videoLink) {
const updatedVideolink = videoLink[userRole as keyof DataByRole<string>]

dataToUpdate.videoLink = updatedVideolink
}

if (notificationSettings)
Expand All @@ -172,8 +187,8 @@ const EditProfile = () => {
dataToUpdate.mainSubjects = categories
}

if (typeof profileState.photo === 'object') {
dataToUpdate.photo = profileState.photo
if (typeof photo === 'object' || photo === '') {
dataToUpdate.photo = photo
}

await dispatch(
Expand Down
31 changes: 22 additions & 9 deletions src/redux/features/editProfileSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
} from '~/redux/redux.constants'
import {
DataByRole,
EditProfileForm,
EditProfileFormSubmitData,
ErrorResponse,
MainUserRole,
NotificationSettings,
ProfessionalBlock,
SubjectNameInterface,
UpdatedPhoto,
EditProfilePhoto,
UpdateUserParams,
UserMainSubject,
UserMainSubjectFieldValues,
Expand All @@ -30,8 +30,8 @@ export interface EditProfileState {
city: string | null
professionalSummary?: string
nativeLanguage: string | null
videoLink: DataByRole<string> | string
photo?: UpdatedPhoto | null
videoLink: DataByRole<string>
photo: EditProfilePhoto
categories: DataByRole<UserMainSubject[]>
professionalBlock: ProfessionalBlock
notificationSettings: NotificationSettings
Expand Down Expand Up @@ -104,8 +104,11 @@ const updateStateFromPayload = (
state.city = address?.city ?? null
state.professionalSummary = professionalSummary
state.nativeLanguage = nativeLanguage
state.photo = photo as UpdatedPhoto | null
state.videoLink = videoLink
state.photo = photo ?? null
state.videoLink = {
[UserRoleEnum.Tutor]: videoLink?.[UserRoleEnum.Tutor] ?? '',
[UserRoleEnum.Student]: videoLink?.[UserRoleEnum.Student] ?? ''
}
state.categories = mainSubjects
state.professionalBlock = professionalBlock || initialProfessoinalBlock
state.notificationSettings =
Expand Down Expand Up @@ -170,7 +173,10 @@ const editProfileSlice = createSlice({
const { tab, value } = action.payload
state.tabValidityStatus[tab] = value
},
updateProfileData: (state, action: PayloadAction<EditProfileForm>) => {
updateProfileData: (
state,
action: PayloadAction<EditProfileFormSubmitData>
) => {
const {
city,
country,
Expand All @@ -187,9 +193,16 @@ const editProfileSlice = createSlice({
state.firstName = firstName
state.lastName = lastName
state.nativeLanguage = nativeLanguage
state.photo = photo as UpdatedPhoto
state.photo = photo ?? null
state.professionalSummary = professionalSummary
state.videoLink = videoLink

if (videoLink?.[UserRoleEnum.Tutor] !== null) {
state.videoLink[UserRoleEnum.Tutor] = videoLink[UserRoleEnum.Tutor]
}

if (videoLink?.[UserRoleEnum.Student] !== null) {
state.videoLink[UserRoleEnum.Student] = videoLink[UserRoleEnum.Student]
}
},
addCategory: (
state,
Expand Down
12 changes: 8 additions & 4 deletions src/types/edit-profile/interfaces/editProfile.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {
CategoryInterface,
SubjectNameInterface,
UpdatedPhoto
SubjectNameInterface
} from '~/types/common/common.index'
import { UserResponse } from '~/types/user/user.index'
import { EditProfilePhoto, UserResponse, VideoUserRole } from '~/types'

export interface EditProfileForm
extends Pick<UserResponse, 'firstName' | 'lastName'> {
Expand All @@ -12,7 +11,12 @@ export interface EditProfileForm
professionalSummary: string
nativeLanguage: string | null
videoLink: string
photo: string | UpdatedPhoto | null
photo: EditProfilePhoto
}

export interface EditProfileFormSubmitData
extends Omit<EditProfileForm, 'videoLink'> {
videoLink: { [key in VideoUserRole]: string | null }
}

export interface ProfessionalCategory {
Expand Down
5 changes: 4 additions & 1 deletion src/types/edit-profile/types/editProfile.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
CategoryNameInterface,
SubjectNameInterface,
UserMainSubject
UserMainSubject,
UpdatedPhoto
} from '~/types'

export type OpenProfessionalCategoryModalHandler = (
Expand All @@ -13,3 +14,5 @@ export type UserMainSubjectFieldValues = string &
boolean &
CategoryNameInterface &
SubjectNameInterface[]

export type EditProfilePhoto = UpdatedPhoto | string | null
4 changes: 2 additions & 2 deletions src/types/user/user-interfaces/user.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
RequestParams,
Faq,
DataByRole,
UpdatedPhoto,
EditProfilePhoto,
UpdateFields,
UserStatusEnum,
UserMainSubject,
Expand Down Expand Up @@ -78,7 +78,7 @@ export interface UpdateUserParams
extends Partial<Pick<UserResponse, UpdateFields>> {
mainSubjects?: DataByRole<UserMainSubject[]>
videoLink?: string
photo?: UpdatedPhoto | null
photo?: EditProfilePhoto
}

export interface LoginParams {
Expand Down
7 changes: 7 additions & 0 deletions src/utils/are-all-values-empty-strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const areAllValuesEmptyStrings = (obj: {
[key: string]: string
}): boolean => {
return Object.values(obj).every(
(value) => typeof value === 'string' && value === ''
)
}
38 changes: 38 additions & 0 deletions src/utils/has-photo-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { EditProfilePhoto, UpdatedPhoto } from '~/types'
import { isUpdatedPhoto } from '~/utils/is-updated-photo'
import { areAllValuesEmptyStrings } from '~/utils/are-all-values-empty-strings'

export const hasPhotoChanges = (
initialPhoto: EditProfilePhoto,
currentPhoto: EditProfilePhoto
): boolean => {
if (initialPhoto !== '' && currentPhoto === '') {
return true
}

if (
typeof initialPhoto === 'string' &&
isUpdatedPhoto(currentPhoto) &&
(currentPhoto as UpdatedPhoto).name !== initialPhoto
) {
return true
}

if (
initialPhoto === null &&
isUpdatedPhoto(currentPhoto) &&
areAllValuesEmptyStrings(currentPhoto as UpdatedPhoto)
) {
return true
}

if (
isUpdatedPhoto(initialPhoto) &&
isUpdatedPhoto(currentPhoto) &&
(initialPhoto as UpdatedPhoto).name !== (currentPhoto as UpdatedPhoto).name
) {
return true
}

return false
}
10 changes: 10 additions & 0 deletions src/utils/is-updated-photo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { EditProfilePhoto } from '~/types'

export const isUpdatedPhoto = (photo: EditProfilePhoto): boolean => {
return (
photo !== null &&
typeof photo === 'object' &&
'name' in photo &&
'src' in photo
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('ProfileTabForm', () => {
const removePhotoBtn = screen.getByRole('button', { name: 'common.remove' })
fireEvent.click(removePhotoBtn)

expect(handleNonInputValueChange).toHaveBeenCalledWith('photo', null)
expect(handleNonInputValueChange).toHaveBeenCalledWith('photo', '')
})

it('should handle adding a photo', async () => {
Expand Down
Loading
Loading