Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
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
79 changes: 39 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,12 +48,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
Expand All @@ -63,6 +74,7 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => {
setIsFullScreen(false);
setProfilePictureUrl('');
setProfilePictureKey('');
setInitialProfilePictureKey('');

if (!targetUser) {
const newState = {
Expand Down Expand Up @@ -103,13 +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;
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 +335,18 @@ export const AccountForm = ({ targetUser, isOpen, onClose, onSave }) => {
if (!uploadedFiles?.length) return;

const key = uploadedFiles[0].s3_key;
setProfilePictureKey(key);
Comment thread
jenniyan marked this conversation as resolved.

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 +424,14 @@ 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,
});
}

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

if (userId) {
Expand All @@ -447,10 +442,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