From fb5e07d099bb8260057a1c4255c4fa6c4a6204ef Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 5 Aug 2026 18:28:55 -0700 Subject: [PATCH 01/12] fix: profile picture only updates officially on save" --- .../components/accounts/AccountForm/index.jsx | 67 +++++++++---------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/client/src/components/accounts/AccountForm/index.jsx b/client/src/components/accounts/AccountForm/index.jsx index 31c38499..955a3bf4 100644 --- a/client/src/components/accounts/AccountForm/index.jsx +++ b/client/src/components/accounts/AccountForm/index.jsx @@ -34,6 +34,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { const [currentRegions, setCurrentRegions] = useState(null); const [profilePictureUrl, setProfilePictureUrl] = useState(''); const [profilePictureKey, setProfilePictureKey] = useState(''); + const [initialProfilePictureKey, setInitialProfilePictureKey] = useState(''); const exitModal = useDisclosure(); const deleteModal = useDisclosure(); @@ -46,12 +47,21 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { }); const isDirty = useMemo(() => { - return JSON.stringify(formData) !== JSON.stringify(initialFormData); - }, [formData, initialFormData]); + return ( + JSON.stringify(formData) !== JSON.stringify(initialFormData) || + profilePictureKey !== initialProfilePictureKey + ); + }, [formData, initialFormData, profilePictureKey, initialProfilePictureKey]); const changedFields = useMemo( - () => computeChangedFields(formData, initialFormData, t), - [formData, initialFormData, t] + () => + computeChangedFields( + formData, + initialFormData, + t, + profilePictureKey !== initialProfilePictureKey + ), + [formData, initialFormData, t, profilePictureKey, initialProfilePictureKey] ); // Reset form when drawer opens @@ -63,6 +73,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { setIsFullScreen(false); setProfilePictureUrl(''); setProfilePictureKey(''); + setInitialProfilePictureKey(''); if (!targetUser) { const newState = { @@ -110,6 +121,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { ); setProfilePictureUrl(urlResponse.data.url || ''); setProfilePictureKey(targetUser.picture); + setInitialProfilePictureKey(targetUser.picture); } catch { // picture unavailable — leave blank } @@ -324,42 +336,11 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { const urlResponse = await backend.get( `/images/url/${encodeURIComponent(key)}` ); - const nextUrl = urlResponse.data.url || ''; - const prevKey = profilePictureKey || null; - - if (targetUserId) { - await backend.post('/images/profile-picture', { - key, - userId: targetUserId, - }); - - await logAccountChange({ - user_id: String(targetUserId), - author_id: String(userId), - change_type: 'Update', - old_values: { - first_name: formData.first_name, - last_name: formData.last_name, - email: formData.email, - picture: prevKey, - bio: '', - }, - new_values: { - first_name: formData.first_name, - last_name: formData.last_name, - email: formData.email, - picture: key, - bio: '', - }, - resolved: false, - last_modified: new Date().toISOString(), - }); - } setProfilePictureKey(key); - setProfilePictureUrl(nextUrl); + setProfilePictureUrl(urlResponse.data.url || ''); } catch (err) { - console.error('Error saving profile picture:', err); + console.error('Error loading profile picture preview:', err); } }; @@ -439,6 +420,14 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { } await backend.put('/gcf-users/admin/update-user', userData); + const pictureChanged = profilePictureKey !== initialProfilePictureKey; + if (pictureChanged) { + await backend.post('/images/profile-picture', { + key: profilePictureKey || null, + userId: targetUserId, + }); + } + if (userId) { await logAccountChange({ user_id: String(targetUserId), @@ -447,10 +436,14 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { old_values: formStateToAuditSnapshot(initialFormData, { currentUserId: userId, targetId: targetUserId, + ...(pictureChanged + ? { picture: initialProfilePictureKey || null } + : {}), }), new_values: formStateToAuditSnapshot(formData, { currentUserId: userId, targetId: targetUserId, + ...(pictureChanged ? { picture: profilePictureKey || null } : {}), }), resolved: true, last_modified: new Date().toISOString(), From 4cd71fcd3013101f1bb33542b9371f32593d30be Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 5 Aug 2026 18:29:33 -0700 Subject: [PATCH 02/12] fix: translations --- client/public/locales/en/translation.json | 2 ++ client/public/locales/es/translation.json | 2 ++ client/public/locales/fr/translation.json | 2 ++ client/public/locales/zh/translation.json | 2 ++ .../accounts/AccountForm/changedFields.js | 14 +++++++++++++- .../components/accounts/AccountForm/constants.js | 1 + 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 063ede7d..c536aded 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -451,6 +451,8 @@ "fieldRole": "Role", "fieldProgram": "Program", "fieldRegion": "Region", + "fieldProfilePicture": "Profile Picture", + "fieldProfilePictureUpdated": "Updated", "saveSuccess": "User saved successfully!", "emailExistsTitle": "Email already exists", "emailExistsDesc": "This email is already registered. Please use a different email address.", diff --git a/client/public/locales/es/translation.json b/client/public/locales/es/translation.json index d060f49e..488e3cf7 100644 --- a/client/public/locales/es/translation.json +++ b/client/public/locales/es/translation.json @@ -451,6 +451,8 @@ "fieldRole": "Rol", "fieldProgram": "Programa", "fieldRegion": "Región", + "fieldProfilePicture": "Foto de perfil", + "fieldProfilePictureUpdated": "Actualizada", "saveSuccess": "¡Usuario guardado exitosamente!", "emailExistsTitle": "El correo electrónico ya existe", "emailExistsDesc": "Este correo electrónico ya está registrado. Utilice una dirección de correo electrónico diferente.", diff --git a/client/public/locales/fr/translation.json b/client/public/locales/fr/translation.json index 54f2054d..dd1132c3 100644 --- a/client/public/locales/fr/translation.json +++ b/client/public/locales/fr/translation.json @@ -451,6 +451,8 @@ "fieldRole": "Rôle", "fieldProgram": "Programme", "fieldRegion": "Région", + "fieldProfilePicture": "Photo de profil", + "fieldProfilePictureUpdated": "Mise à jour", "saveSuccess": "L'utilisateur a été enregistré avec succès !", "emailExistsTitle": "L'e-mail existe déjà", "emailExistsDesc": "Cet e-mail est déjà enregistré. Veuillez utiliser une autre adresse e-mail.", diff --git a/client/public/locales/zh/translation.json b/client/public/locales/zh/translation.json index 0e357f0c..19c17697 100644 --- a/client/public/locales/zh/translation.json +++ b/client/public/locales/zh/translation.json @@ -447,6 +447,8 @@ "fieldRole": "角色", "fieldProgram": "程序", "fieldRegion": "地区", + "fieldProfilePicture": "头像", + "fieldProfilePictureUpdated": "已更新", "saveSuccess": "用户保存成功!", "emailExistsTitle": "电子邮件已存在", "emailExistsDesc": "此邮箱号已被注册。请使用不同的电子邮件地址。", diff --git a/client/src/components/accounts/AccountForm/changedFields.js b/client/src/components/accounts/AccountForm/changedFields.js index d72bef40..0824bd41 100644 --- a/client/src/components/accounts/AccountForm/changedFields.js +++ b/client/src/components/accounts/AccountForm/changedFields.js @@ -1,6 +1,18 @@ -export function computeChangedFields(formData, initialFormData, t) { +export function computeChangedFields( + formData, + initialFormData, + t, + pictureChanged = false +) { const mask = t('accountForm.passwordMaskStars'); const changes = []; + if (pictureChanged) { + changes.push({ + label: t('accountForm.fieldProfilePicture'), + old: '', + new: t('accountForm.fieldProfilePictureUpdated'), + }); + } if (formData.first_name !== initialFormData.first_name) { changes.push({ label: t('accountForm.fieldFirstName'), diff --git a/client/src/components/accounts/AccountForm/constants.js b/client/src/components/accounts/AccountForm/constants.js index 8899fcee..6ddd3c99 100644 --- a/client/src/components/accounts/AccountForm/constants.js +++ b/client/src/components/accounts/AccountForm/constants.js @@ -36,6 +36,7 @@ export const formStateToAuditSnapshot = (fd, meta = {}) => ({ ...(fd.role === 'Program Director' ? { bio: String(fd.bio ?? '').trim() || null } : {}), + ...(meta.picture !== undefined ? { picture: meta.picture } : {}), ...(meta.currentUserId !== undefined && meta.currentUserId !== null ? { currentUserId: meta.currentUserId } : {}), From a3a1ca016e68aae25edbbb43b9e81cca685c2461 Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 5 Aug 2026 18:45:13 -0700 Subject: [PATCH 03/12] fix: race condition where uploaded pictures can get lost --- client/src/components/accounts/AccountForm/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/accounts/AccountForm/index.jsx b/client/src/components/accounts/AccountForm/index.jsx index 955a3bf4..3c124acc 100644 --- a/client/src/components/accounts/AccountForm/index.jsx +++ b/client/src/components/accounts/AccountForm/index.jsx @@ -331,13 +331,13 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { if (!uploadedFiles?.length) return; const key = uploadedFiles[0].s3_key; + setProfilePictureKey(key); try { const urlResponse = await backend.get( `/images/url/${encodeURIComponent(key)}` ); - setProfilePictureKey(key); setProfilePictureUrl(urlResponse.data.url || ''); } catch (err) { console.error('Error loading profile picture preview:', err); From 3b01ac63fa5a6448fab51a036f7d844c37107954 Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 5 Aug 2026 18:46:35 -0700 Subject: [PATCH 04/12] fix: if the picture update fails, nothing is committed yet --- client/src/components/accounts/AccountForm/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/accounts/AccountForm/index.jsx b/client/src/components/accounts/AccountForm/index.jsx index 3c124acc..e06cd2de 100644 --- a/client/src/components/accounts/AccountForm/index.jsx +++ b/client/src/components/accounts/AccountForm/index.jsx @@ -418,8 +418,6 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { if (formData.password && formData.password.trim().length > 0) { userData.password = formData.password; } - await backend.put('/gcf-users/admin/update-user', userData); - const pictureChanged = profilePictureKey !== initialProfilePictureKey; if (pictureChanged) { await backend.post('/images/profile-picture', { @@ -428,6 +426,8 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { }); } + await backend.put('/gcf-users/admin/update-user', userData); + if (userId) { await logAccountChange({ user_id: String(targetUserId), From bad0f4de78d10dd4c9ea8d3d04599fcc1d328bf0 Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 5 Aug 2026 18:48:27 -0700 Subject: [PATCH 05/12] fix: profile picture only updates officially on save --- client/src/components/accounts/AccountForm/index.jsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/components/accounts/AccountForm/index.jsx b/client/src/components/accounts/AccountForm/index.jsx index e06cd2de..e67041d3 100644 --- a/client/src/components/accounts/AccountForm/index.jsx +++ b/client/src/components/accounts/AccountForm/index.jsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useDisclosure, useToast } from '@chakra-ui/react'; @@ -35,6 +35,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { const [profilePictureUrl, setProfilePictureUrl] = useState(''); const [profilePictureKey, setProfilePictureKey] = useState(''); const [initialProfilePictureKey, setInitialProfilePictureKey] = useState(''); + const pictureRequestIdRef = useRef(0); const exitModal = useDisclosure(); const deleteModal = useDisclosure(); @@ -114,14 +115,17 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { } if (targetUser.picture) { + setProfilePictureKey(targetUser.picture); + setInitialProfilePictureKey(targetUser.picture); + + const requestId = ++pictureRequestIdRef.current; const fetchPictureUrl = async () => { try { const urlResponse = await backend.get( `/images/url/${encodeURIComponent(targetUser.picture)}` ); + if (pictureRequestIdRef.current !== requestId) return; setProfilePictureUrl(urlResponse.data.url || ''); - setProfilePictureKey(targetUser.picture); - setInitialProfilePictureKey(targetUser.picture); } catch { // picture unavailable — leave blank } @@ -333,11 +337,13 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { const key = uploadedFiles[0].s3_key; setProfilePictureKey(key); + const requestId = ++pictureRequestIdRef.current; try { const urlResponse = await backend.get( `/images/url/${encodeURIComponent(key)}` ); + if (pictureRequestIdRef.current !== requestId) return; setProfilePictureUrl(urlResponse.data.url || ''); } catch (err) { console.error('Error loading profile picture preview:', err); From 761d499d7dfde6bef6ae8adc46fc028432c96f1a Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 5 Aug 2026 19:01:50 -0700 Subject: [PATCH 06/12] fix: copilot changes --- .../components/accounts/AccountForm/index.jsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/client/src/components/accounts/AccountForm/index.jsx b/client/src/components/accounts/AccountForm/index.jsx index e67041d3..15fd5bdd 100644 --- a/client/src/components/accounts/AccountForm/index.jsx +++ b/client/src/components/accounts/AccountForm/index.jsx @@ -69,6 +69,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { useEffect(() => { if (!isOpen) return; + pictureRequestIdRef.current += 1; setValidationErrors({}); setShowPassword(false); setIsFullScreen(false); @@ -335,8 +336,21 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { if (!uploadedFiles?.length) return; const key = uploadedFiles[0].s3_key; + const previousStagedKey = + profilePictureKey && profilePictureKey !== initialProfilePictureKey + ? profilePictureKey + : null; + setProfilePictureKey(key); + if (previousStagedKey) { + backend + .delete(`/images/${encodeURIComponent(previousStagedKey)}`) + .catch((err) => { + console.error('Error deleting replaced profile picture upload:', err); + }); + } + const requestId = ++pictureRequestIdRef.current; try { const urlResponse = await backend.get( @@ -579,6 +593,19 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { isOpen={exitModal.isOpen} onClose={exitModal.onClose} onExitWithoutSaving={() => { + if ( + profilePictureKey && + profilePictureKey !== initialProfilePictureKey + ) { + backend + .delete(`/images/${encodeURIComponent(profilePictureKey)}`) + .catch((err) => { + console.error( + 'Error deleting unsaved profile picture upload:', + err + ); + }); + } exitModal.onClose(); onClose(); }} From df4b24064626c7ae5bc236b46bfba710be9b10fe Mon Sep 17 00:00:00 2001 From: Jennifer Yan Date: Sun, 9 Aug 2026 10:14:48 -0700 Subject: [PATCH 07/12] Update client/src/components/accounts/AccountForm/index.jsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- client/src/components/accounts/AccountForm/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/accounts/AccountForm/index.jsx b/client/src/components/accounts/AccountForm/index.jsx index 15fd5bdd..72fa9883 100644 --- a/client/src/components/accounts/AccountForm/index.jsx +++ b/client/src/components/accounts/AccountForm/index.jsx @@ -343,7 +343,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { setProfilePictureKey(key); - if (previousStagedKey) { + if (previousStagedKey && previousStagedKey !== key) { backend .delete(`/images/${encodeURIComponent(previousStagedKey)}`) .catch((err) => { From 2199eaa680f5be3515ed4cdd9081fa12c6244232 Mon Sep 17 00:00:00 2001 From: Jennifer Date: Sun, 9 Aug 2026 10:19:36 -0700 Subject: [PATCH 08/12] fix: prevent exit-cleanup from deleting an already-committed profile picture --- client/src/components/accounts/AccountForm/index.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/accounts/AccountForm/index.jsx b/client/src/components/accounts/AccountForm/index.jsx index 72fa9883..be5d1e59 100644 --- a/client/src/components/accounts/AccountForm/index.jsx +++ b/client/src/components/accounts/AccountForm/index.jsx @@ -444,6 +444,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => { key: profilePictureKey || null, userId: targetUserId, }); + setInitialProfilePictureKey(profilePictureKey); } await backend.put('/gcf-users/admin/update-user', userData); From 98f5b43eb6b04507d4624b8fa50e7ae31705cd1a Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 12 Aug 2026 16:55:55 -0700 Subject: [PATCH 09/12] fix: address race condition --- client/src/components/accounts/AccountForm/AccountFormDrawer.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/accounts/AccountForm/AccountFormDrawer.jsx b/client/src/components/accounts/AccountForm/AccountFormDrawer.jsx index a4aa092f..201972ba 100644 --- a/client/src/components/accounts/AccountForm/AccountFormDrawer.jsx +++ b/client/src/components/accounts/AccountForm/AccountFormDrawer.jsx @@ -544,6 +544,7 @@ export const AccountFormDrawer = ({ From 4fcda44c588bc3c59466f69f641a2231e90912ba Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 12 Aug 2026 17:12:27 -0700 Subject: [PATCH 10/12] fix: correct pfp upload logic in /profile --- client/src/components/profile/ProfileView.jsx | 3 + client/src/components/profile/useProfile.js | 109 +++++++++--------- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/client/src/components/profile/ProfileView.jsx b/client/src/components/profile/ProfileView.jsx index ad938170..28503930 100644 --- a/client/src/components/profile/ProfileView.jsx +++ b/client/src/components/profile/ProfileView.jsx @@ -55,6 +55,7 @@ export const ProfileView = (props) => { role, roleSpecificData, isEditing, + isSaving, formData, showPassword, setShowPassword, @@ -476,6 +477,7 @@ export const ProfileView = (props) => { leftIcon={} variant="outline" onClick={handleCancel} + isDisabled={isSaving} > {t('common.cancel')} @@ -485,6 +487,7 @@ export const ProfileView = (props) => { color="white" _hover={{ bg: 'teal.600' }} onClick={handleSave} + isLoading={isSaving} > {t('common.save')} diff --git a/client/src/components/profile/useProfile.js b/client/src/components/profile/useProfile.js index 48560877..a4d95a01 100644 --- a/client/src/components/profile/useProfile.js +++ b/client/src/components/profile/useProfile.js @@ -29,6 +29,7 @@ export const useProfile = () => { const [roleSpecificData, setRoleSpecificData] = useState(null); const [loading, setLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); const [showPassword, setShowPassword] = useState(false); const [passwordEdited, setPasswordEdited] = useState(false); const [newPassword, setNewPassword] = useState(''); @@ -186,59 +187,29 @@ export const useProfile = () => { if (!uploadedFiles?.length) return; const key = uploadedFiles[0].s3_key; + const previousStagedKey = + pendingPictureKey && pendingPictureKey !== (gcfUser?.pictureKey || null) + ? pendingPictureKey + : null; try { const urlResponse = await backend.get( `/images/url/${encodeURIComponent(key)}` ); - if (role === 'Program Director') { - setPendingPictureKey(key); - setPendingPicturePreviewUrl(urlResponse.data.url); - return; - } - - await backend.post('/images/profile-picture', { - key: key, - userId: currentUser.uid, - }); - - const prevPictureKey = gcfUser?.pictureKey || null; - const nextPictureKey = key; - - if (prevPictureKey !== nextPictureKey) { - try { - await backend.post('/accountChange', { - user_id: currentUser.uid, - author_id: currentUser.uid, - change_type: 'Update', - old_values: { - first_name: gcfUser?.firstName || '', - last_name: gcfUser?.lastName || '', - email: currentUser?.email || '', - picture: prevPictureKey, - bio: '', - }, - new_values: { - first_name: gcfUser?.firstName || '', - last_name: gcfUser?.lastName || '', - email: currentUser?.email || '', - picture: nextPictureKey, - bio: '', - }, - resolved: false, - last_modified: new Date().toISOString(), + setPendingPictureKey(key); + setPendingPicturePreviewUrl(urlResponse.data.url); + + if (previousStagedKey && previousStagedKey !== key) { + backend + .delete(`/images/${encodeURIComponent(previousStagedKey)}`) + .catch((err) => { + console.error( + 'Error deleting replaced profile picture upload:', + err + ); }); - } catch (changeErr) { - console.error('Error logging account change:', changeErr); - } } - - setGcfUser((prev) => ({ - ...prev, - pictureKey: nextPictureKey, - picture: urlResponse.data.url, - })); } catch (err) { console.error('Error saving profile picture:', err); } @@ -270,6 +241,19 @@ export const useProfile = () => { }; const handleCancel = () => { + if ( + pendingPictureKey && + pendingPictureKey !== (gcfUser?.pictureKey || null) + ) { + backend + .delete(`/images/${encodeURIComponent(pendingPictureKey)}`) + .catch((err) => { + console.error( + 'Error deleting discarded profile picture upload:', + err + ); + }); + } setIsEditing(false); setShowPassword(false); setNewPassword(''); @@ -280,6 +264,7 @@ export const useProfile = () => { }; const saveProfileEdits = async () => { + setIsSaving(true); try { if (role === 'Program Director') { const prefLang = @@ -408,18 +393,22 @@ export const useProfile = () => { return; } + const prevPictureKey = gcfUser?.pictureKey || null; + const nextPictureKey = pendingPictureKey ?? prevPictureKey; + const pictureChanged = nextPictureKey !== prevPictureKey; + const oldValues = { first_name: gcfUser?.firstName || '', last_name: gcfUser?.lastName || '', email: currentUser?.email || '', - picture: gcfUser?.pictureKey || null, + picture: prevPictureKey, bio: '', }; const newValues = { first_name: formData.firstName, last_name: formData.lastName, email: currentUser?.email || '', - picture: gcfUser?.pictureKey || null, + picture: nextPictureKey, bio: '', }; @@ -428,6 +417,13 @@ export const useProfile = () => { last_name: formData.lastName, }); + if (pictureChanged) { + await backend.post('/images/profile-picture', { + key: nextPictureKey, + userId: currentUser.uid, + }); + } + if (JSON.stringify(oldValues) !== JSON.stringify(newValues)) { try { await backend.post('/accountChange', { @@ -454,7 +450,12 @@ export const useProfile = () => { preferredLanguage: formData.language, firstName: formData.firstName, lastName: formData.lastName, + ...(pictureChanged + ? { pictureKey: nextPictureKey, picture: pendingPicturePreviewUrl } + : {}), })); + setPendingPictureKey(null); + setPendingPicturePreviewUrl(null); window.dispatchEvent(new Event('profile-updated')); const now = new Date(); @@ -489,6 +490,8 @@ export const useProfile = () => { variant: 'subtle', position: 'bottom-right', }); + } finally { + setIsSaving(false); } }; @@ -532,12 +535,11 @@ export const useProfile = () => { setFormData((prev) => ({ ...prev, [field]: e.target.value })); }; - const profilePicture = - pendingPicturePreviewUrl && role === 'Program Director' - ? pendingPicturePreviewUrl - : gcfUser?.picture && gcfUser.picture.trim() !== '' - ? gcfUser.picture - : DEFAULT_PROFILE_IMAGE; + const profilePicture = pendingPicturePreviewUrl + ? pendingPicturePreviewUrl + : gcfUser?.picture && gcfUser.picture.trim() !== '' + ? gcfUser.picture + : DEFAULT_PROFILE_IMAGE; const pdPending = role === 'Program Director' && !!pendingAccountChange; const ov = pendingAccountChange?.oldValues || {}; @@ -573,6 +575,7 @@ export const useProfile = () => { role, roleSpecificData, isEditing, + isSaving, formData, showPassword, setShowPassword, From 4f9797d4a3f8c55d2b6ba219a4b23549a02ecc9a Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 12 Aug 2026 17:27:23 -0700 Subject: [PATCH 11/12] fix: discard stale picture upload if canceled/saved before it resolves --- client/src/components/profile/useProfile.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/src/components/profile/useProfile.js b/client/src/components/profile/useProfile.js index a4d95a01..9607f6d0 100644 --- a/client/src/components/profile/useProfile.js +++ b/client/src/components/profile/useProfile.js @@ -35,6 +35,7 @@ export const useProfile = () => { const [newPassword, setNewPassword] = useState(''); const profileEditBaselineRef = useRef(null); + const editSessionRef = useRef(0); const [pendingPictureKey, setPendingPictureKey] = useState(null); const [pendingPicturePreviewUrl, setPendingPicturePreviewUrl] = useState(null); @@ -187,6 +188,7 @@ export const useProfile = () => { if (!uploadedFiles?.length) return; const key = uploadedFiles[0].s3_key; + const sessionId = editSessionRef.current; const previousStagedKey = pendingPictureKey && pendingPictureKey !== (gcfUser?.pictureKey || null) ? pendingPictureKey @@ -197,6 +199,13 @@ export const useProfile = () => { `/images/url/${encodeURIComponent(key)}` ); + if (sessionId !== editSessionRef.current) { + backend.delete(`/images/${encodeURIComponent(key)}`).catch((err) => { + console.error('Error deleting stale profile picture upload:', err); + }); + return; + } + setPendingPictureKey(key); setPendingPicturePreviewUrl(urlResponse.data.url); @@ -216,6 +225,7 @@ export const useProfile = () => { }; const handleEdit = () => { + editSessionRef.current += 1; const prefLang = gcfUser.preferredLanguage && isAppLocale(String(gcfUser.preferredLanguage)) @@ -241,6 +251,7 @@ export const useProfile = () => { }; const handleCancel = () => { + editSessionRef.current += 1; if ( pendingPictureKey && pendingPictureKey !== (gcfUser?.pictureKey || null) @@ -264,6 +275,7 @@ export const useProfile = () => { }; const saveProfileEdits = async () => { + editSessionRef.current += 1; setIsSaving(true); try { if (role === 'Program Director') { From c17461463a2f0d92c094e52ec719cfea2752eba7 Mon Sep 17 00:00:00 2001 From: Jennifer Date: Wed, 12 Aug 2026 17:37:42 -0700 Subject: [PATCH 12/12] fix: race condiitons --- client/src/components/profile/ProfileView.jsx | 1 + client/src/components/profile/useProfile.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/client/src/components/profile/ProfileView.jsx b/client/src/components/profile/ProfileView.jsx index 28503930..cec3b2c9 100644 --- a/client/src/components/profile/ProfileView.jsx +++ b/client/src/components/profile/ProfileView.jsx @@ -179,6 +179,7 @@ export const ProfileView = (props) => { bottom={2} right={2} onClick={onOpen} + isDisabled={isSaving} _hover={{ bg: 'gray.100' }} /> )} diff --git a/client/src/components/profile/useProfile.js b/client/src/components/profile/useProfile.js index 9607f6d0..ca576a24 100644 --- a/client/src/components/profile/useProfile.js +++ b/client/src/components/profile/useProfile.js @@ -36,6 +36,7 @@ export const useProfile = () => { const profileEditBaselineRef = useRef(null); const editSessionRef = useRef(0); + const isSavingRef = useRef(false); const [pendingPictureKey, setPendingPictureKey] = useState(null); const [pendingPicturePreviewUrl, setPendingPicturePreviewUrl] = useState(null); @@ -188,6 +189,17 @@ export const useProfile = () => { if (!uploadedFiles?.length) return; const key = uploadedFiles[0].s3_key; + + if (isSavingRef.current) { + backend.delete(`/images/${encodeURIComponent(key)}`).catch((err) => { + console.error( + 'Error deleting profile picture uploaded during save:', + err + ); + }); + return; + } + const sessionId = editSessionRef.current; const previousStagedKey = pendingPictureKey && pendingPictureKey !== (gcfUser?.pictureKey || null) @@ -276,6 +288,7 @@ export const useProfile = () => { const saveProfileEdits = async () => { editSessionRef.current += 1; + isSavingRef.current = true; setIsSaving(true); try { if (role === 'Program Director') { @@ -503,6 +516,7 @@ export const useProfile = () => { position: 'bottom-right', }); } finally { + isSavingRef.current = false; setIsSaving(false); } };