Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
2 changes: 2 additions & 0 deletions client/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions client/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions client/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions client/public/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@
"fieldRole": "角色",
"fieldProgram": "程序",
"fieldRegion": "地区",
"fieldProfilePicture": "头像",
"fieldProfilePictureUpdated": "已更新",
"saveSuccess": "用户保存成功!",
"emailExistsTitle": "电子邮件已存在",
"emailExistsDesc": "此邮箱号已被注册。请使用不同的电子邮件地址。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ export const AccountFormDrawer = ({
<Button
variant="outline"
onClick={onCancel}
isDisabled={isLoading}
>
{t('common.cancel')}
</Button>
Expand Down
14 changes: 13 additions & 1 deletion client/src/components/accounts/AccountForm/changedFields.js
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down
1 change: 1 addition & 0 deletions client/src/components/accounts/AccountForm/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
: {}),
Expand Down
107 changes: 67 additions & 40 deletions client/src/components/accounts/AccountForm/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';

import { useDisclosure, useToast } from '@chakra-ui/react';

Expand Down Expand Up @@ -34,6 +34,8 @@ 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 pictureRequestIdRef = useRef(0);

const exitModal = useDisclosure();
const deleteModal = useDisclosure();
Expand All @@ -46,23 +48,34 @@ 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
useEffect(() => {
if (!isOpen) return;

pictureRequestIdRef.current += 1;
setValidationErrors({});
setShowPassword(false);
setIsFullScreen(false);
setProfilePictureUrl('');
setProfilePictureKey('');
setInitialProfilePictureKey('');

if (!targetUser) {
const newState = {
Expand Down Expand Up @@ -103,13 +116,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;
Comment thread
jenniyan marked this conversation as resolved.
setProfilePictureUrl(urlResponse.data.url || '');
setProfilePictureKey(targetUser.picture);
} catch {
// picture unavailable — leave blank
}
Expand Down Expand Up @@ -319,47 +336,31 @@ 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);
Comment thread
jenniyan marked this conversation as resolved.

if (previousStagedKey && previousStagedKey !== key) {
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(
`/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);
if (pictureRequestIdRef.current !== requestId) return;
setProfilePictureUrl(urlResponse.data.url || '');
Comment thread
jenniyan marked this conversation as resolved.
} catch (err) {
console.error('Error saving profile picture:', err);
console.error('Error loading profile picture preview:', err);
}
};

Expand Down Expand Up @@ -437,6 +438,15 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => {
if (formData.password && formData.password.trim().length > 0) {
userData.password = formData.password;
}
const pictureChanged = profilePictureKey !== initialProfilePictureKey;
if (pictureChanged) {
await backend.post('/images/profile-picture', {
Comment thread
jenniyan marked this conversation as resolved.
key: profilePictureKey || null,
userId: targetUserId,
});
setInitialProfilePictureKey(profilePictureKey);
}

await backend.put('/gcf-users/admin/update-user', userData);

if (userId) {
Expand All @@ -447,10 +457,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(),
Expand Down Expand Up @@ -580,6 +594,19 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => {
isOpen={exitModal.isOpen}
onClose={exitModal.onClose}
onExitWithoutSaving={() => {
if (
profilePictureKey &&
profilePictureKey !== initialProfilePictureKey
) {
backend
.delete(`/images/${encodeURIComponent(profilePictureKey)}`)
Comment thread
jenniyan marked this conversation as resolved.
Comment thread
jenniyan marked this conversation as resolved.
.catch((err) => {
console.error(
'Error deleting unsaved profile picture upload:',
err
);
});
}
exitModal.onClose();
onClose();
}}
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/profile/ProfileView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const ProfileView = (props) => {
role,
roleSpecificData,
isEditing,
isSaving,
formData,
showPassword,
setShowPassword,
Expand Down Expand Up @@ -476,6 +477,7 @@ export const ProfileView = (props) => {
leftIcon={<FiX />}
variant="outline"
onClick={handleCancel}
isDisabled={isSaving}
>
{t('common.cancel')}
</Button>
Expand All @@ -485,6 +487,7 @@ export const ProfileView = (props) => {
color="white"
_hover={{ bg: 'teal.600' }}
onClick={handleSave}
isLoading={isSaving}
>
{t('common.save')}
</Button>
Expand Down
Loading