Skip to content

Commit

Permalink
Prevent profile updates without changes (#2849)
Browse files Browse the repository at this point in the history
* fix videoLink type in edit profile slice

* fix photo and videoLink edit processing by role

* update editProfileSlice tests

* fix sending videoLink as empty string to server

* fix updating videoLink as empty string is not send to server

* fix tests to include actual initial videoLink state

* fix photo change on edit profile page

* fix edit profile tests

* add EditProfilePhoto type

* fix avatarSrc in AccountIcon type check

* fix sending unchnaged videoLink every time to server

* fix removed photo display in account icon

* add fetching user data from server on page unload to prevent showing not applied changes

* add tests for EditProfile

* extract utils and add tests

* extract hasPhotoChanges util and add tests

* improve has photo changes test to inlcude mocked functions

* remove void for dispatch in debouncedUpdateProfileData
  • Loading branch information
luiqor authored Nov 30, 2024
1 parent bbabfae commit c2908ec
Show file tree
Hide file tree
Showing 17 changed files with 290 additions and 74 deletions.
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
}
}

dispatch(updateProfileData(payload))
}, 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()
}
}, [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 @@ -149,18 +164,18 @@ const EditProfile = () => {
professionalBlock,
aboutStudent,
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 @@ -177,8 +192,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,14 +7,14 @@ import {
} from '~/redux/redux.constants'
import {
DataByRole,
EditProfileForm,
EditProfileFormSubmitData,
ErrorResponse,
MainUserRole,
NotificationSettings,
ProfessionalBlock,
AboutStudentData,
SubjectNameInterface,
UpdatedPhoto,
EditProfilePhoto,
UpdateUserParams,
UserMainSubject,
UserMainSubjectFieldValues,
Expand All @@ -31,8 +31,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
aboutStudent: AboutStudentData
Expand Down Expand Up @@ -113,8 +113,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.aboutStudent = aboutStudent || initialAboutStudent
Expand Down Expand Up @@ -180,7 +183,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 @@ -197,9 +203,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 @@ -79,7 +79,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
)
}
Loading

0 comments on commit c2908ec

Please sign in to comment.