From 993e6344032aa7b3975a721f9282fbf9b60493c6 Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Sun, 24 May 2026 14:34:02 +0100 Subject: [PATCH 1/8] Normalize email verification index names --- .../core/migrations/0022_emailverificationtoken.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flowbit-backend/core/migrations/0022_emailverificationtoken.py b/flowbit-backend/core/migrations/0022_emailverificationtoken.py index c706875..50b91f3 100644 --- a/flowbit-backend/core/migrations/0022_emailverificationtoken.py +++ b/flowbit-backend/core/migrations/0022_emailverificationtoken.py @@ -37,10 +37,10 @@ class Migration(migrations.Migration): ), migrations.AddIndex( model_name="emailverificationtoken", - index=models.Index(fields=["selector"], name="core_emailv_selecto_3d1c2c_idx"), + index=models.Index(fields=["selector"], name="core_emailv_selecto_ddf40e_idx"), ), migrations.AddIndex( model_name="emailverificationtoken", - index=models.Index(fields=["expires_at"], name="core_emailv_expires_21b7a3_idx"), + index=models.Index(fields=["expires_at"], name="core_emailv_expires_591e95_idx"), ), ] From 3041d69927b17ea703df27e068c4f9ba10cd05da Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Mon, 25 May 2026 23:34:50 +0100 Subject: [PATCH 2/8] Enhance avatar upload preview flow --- .../profile/profile-avatar-card.tsx | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx index c11783b..425340a 100644 --- a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx @@ -1,9 +1,8 @@ "use client"; -import { ChangeEvent, useState } from "react"; +import { ChangeEvent, useEffect, useRef, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faImagePortrait, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { Button } from "@/components/ui/button"; +import { faImagePortrait, faRotate, faSpinner } from "@fortawesome/free-solid-svg-icons"; import { uploadProfileAvatar, type AuthUser } from "@/lib/auth-client"; import { ProfileAvatar } from "@/components/profile/profile-avatar"; @@ -15,27 +14,34 @@ type ProfileAvatarCardProps = { export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvatarCardProps) { const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); const [errorMessage, setErrorMessage] = useState(""); const [isUploading, setIsUploading] = useState(false); + const inputRef = useRef(null); - function handleFileChange(event: ChangeEvent) { - setSelectedFile(event.target.files?.[0] || null); - setErrorMessage(""); - } - - async function handleUpload() { - if (!selectedFile) { - setErrorMessage("Choose an image to upload."); - return; - } + useEffect(() => { + return () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl]); + async function uploadSelectedFile(file: File) { setErrorMessage(""); setIsUploading(true); try { - const updatedUser = await uploadProfileAvatar(selectedFile); + const updatedUser = await uploadProfileAvatar(file); onUserChange(updatedUser); setSelectedFile(null); + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + setPreviewUrl(null); + if (inputRef.current) { + inputRef.current.value = ""; + } onNotify("Profile photo updated successfully."); } catch (error) { setErrorMessage(error instanceof Error ? error.message : "Unable to upload profile photo."); @@ -44,6 +50,23 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata } } + function handleFileChange(event: ChangeEvent) { + const file = event.target.files?.[0] || null; + setSelectedFile(file); + setErrorMessage(""); + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + if (!file) { + setPreviewUrl(null); + return; + } + + const nextPreviewUrl = URL.createObjectURL(file); + setPreviewUrl(nextPreviewUrl); + void uploadSelectedFile(file); + } + return (
@@ -52,22 +75,38 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata
- +
+ {previewUrl ? ( + Selected profile preview + ) : ( + + )} + {isUploading ? ( +
+ +
+ ) : null} +

- {selectedFile ? selectedFile.name : "Upload a clear square image for the best result."} + {isUploading + ? `Uploading ${selectedFile?.name || "photo"}...` + : selectedFile + ? `${selectedFile.name} selected. Uploading now.` + : "Upload a clear square image for the best result."} +

+

+ {previewUrl ? "Previewing selected photo" : "Image updates everywhere after upload"}

-
- -
From 20f6bde73a7c5db081b39e2844b3bbf2d994875c Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Mon, 25 May 2026 23:36:02 +0100 Subject: [PATCH 3/8] Add profile avatar remove endpoint --- flowbit-backend/core/tests.py | 77 +++++++++++++++++++++++++++-------- flowbit-backend/core/views.py | 21 ++++++++++ 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/flowbit-backend/core/tests.py b/flowbit-backend/core/tests.py index b60cff5..2bc44f3 100644 --- a/flowbit-backend/core/tests.py +++ b/flowbit-backend/core/tests.py @@ -1,6 +1,7 @@ from io import StringIO from decimal import Decimal from datetime import datetime, time, timedelta +import tempfile from unittest.mock import patch from django.core.management import call_command @@ -545,25 +546,65 @@ def test_avatar_upload_updates_profile(self): token = Token.objects.create(user=self.user) self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}') - avatar_file = SimpleUploadedFile( - 'avatar.png', - ( - b'\x89PNG\r\n\x1a\n' - b'\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde' - b'\x00\x00\x00\x0cIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xf6\x178U' - b'\x00\x00\x00\x00IEND\xaeB`\x82' - ), - content_type='image/png', - ) - - response = self.client.post('/api/auth/avatar/', {'avatar': avatar_file}) + with tempfile.TemporaryDirectory() as media_root: + with override_settings( + MEDIA_ROOT=media_root, + STORAGES={ + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}, + }, + ): + avatar_file = SimpleUploadedFile( + 'avatar.png', + ( + b'\x89PNG\r\n\x1a\n' + b'\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde' + b'\x00\x00\x00\x0cIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xf6\x178U' + b'\x00\x00\x00\x00IEND\xaeB`\x82' + ), + content_type='image/png', + ) + + response = self.client.post('/api/auth/avatar/', {'avatar': avatar_file}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user.refresh_from_db() + self.assertTrue(bool(self.user.profile.avatar)) + self.assertIsNotNone(response.data['user']['avatar_url']) + self.assertIn('?v=', response.data['user']['avatar_url']) + self.assertTrue(AuditLog.objects.filter(action='auth.avatar_updated', target_id=self.user.id).exists()) + + def test_avatar_delete_clears_profile_photo(self): + token = Token.objects.create(user=self.user) + self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.user.refresh_from_db() - self.assertTrue(bool(self.user.profile.avatar)) - self.assertIsNotNone(response.data['user']['avatar_url']) - self.assertIn('?v=', response.data['user']['avatar_url']) - self.assertTrue(AuditLog.objects.filter(action='auth.avatar_updated', target_id=self.user.id).exists()) + with tempfile.TemporaryDirectory() as media_root: + with override_settings( + MEDIA_ROOT=media_root, + STORAGES={ + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}, + }, + ): + avatar_file = SimpleUploadedFile( + 'avatar.png', + ( + b'\x89PNG\r\n\x1a\n' + b'\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde' + b'\x00\x00\x00\x0cIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xf6\x178U' + b'\x00\x00\x00\x00IEND\xaeB`\x82' + ), + content_type='image/png', + ) + self.client.post('/api/auth/avatar/', {'avatar': avatar_file}) + + response = self.client.delete('/api/auth/avatar/') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.user.refresh_from_db() + self.assertFalse(bool(self.user.profile.avatar)) + self.assertIsNone(response.data['user']['avatar_url']) + self.assertTrue(AuditLog.objects.filter(action='auth.avatar_removed', target_id=self.user.id).exists()) def test_regular_user_cannot_delete_account_without_admin_override_code(self): token = Token.objects.create(user=self.user) diff --git a/flowbit-backend/core/views.py b/flowbit-backend/core/views.py index 87924de..afd059f 100644 --- a/flowbit-backend/core/views.py +++ b/flowbit-backend/core/views.py @@ -5150,6 +5150,27 @@ def post(self, request): status=status.HTTP_200_OK, ) + def delete(self, request): + profile, _ = Profile.objects.get_or_create(user=request.user) + previous_avatar = profile.avatar.name if profile.avatar else '' + if profile.avatar: + profile.avatar.delete(save=False) + profile.avatar = None + profile.save(update_fields=['avatar', 'updated_at']) + + record_audit_log( + request, + 'auth.avatar_removed', + target=request.user, + details=f"User '{request.user.username}' removed profile avatar", + changes={'before_avatar': previous_avatar, 'after_avatar': ''}, + ) + + return Response( + {'user': UserProfileSerializer(request.user, context={'request': request}).data}, + status=status.HTTP_200_OK, + ) + class UserManagementViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet): queryset = User.objects.all().order_by('username') From 7d7fd82c67bf24c8fcf3400bc7de5788ca7908b6 Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Mon, 25 May 2026 23:37:03 +0100 Subject: [PATCH 4/8] Add profile avatar remove flow --- .../profile/profile-avatar-card.tsx | 64 ++++++++++++++++++- flowbit-frontend/src/lib/auth-client.ts | 18 ++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx index 425340a..e1a95a0 100644 --- a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx @@ -2,8 +2,10 @@ import { ChangeEvent, useEffect, useRef, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faImagePortrait, faRotate, faSpinner } from "@fortawesome/free-solid-svg-icons"; -import { uploadProfileAvatar, type AuthUser } from "@/lib/auth-client"; +import { faImagePortrait, faRotate, faSpinner, faTrashCan } from "@fortawesome/free-solid-svg-icons"; +import { Button } from "@/components/ui/button"; +import { ProfileDeleteModal } from "@/components/profile/profile-delete-modal"; +import { removeProfileAvatar, uploadProfileAvatar, type AuthUser } from "@/lib/auth-client"; import { ProfileAvatar } from "@/components/profile/profile-avatar"; type ProfileAvatarCardProps = { @@ -17,6 +19,8 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata const [previewUrl, setPreviewUrl] = useState(null); const [errorMessage, setErrorMessage] = useState(""); const [isUploading, setIsUploading] = useState(false); + const [showRemoveConfirm, setShowRemoveConfirm] = useState(false); + const [isRemoving, setIsRemoving] = useState(false); const inputRef = useRef(null); useEffect(() => { @@ -67,6 +71,30 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata void uploadSelectedFile(file); } + async function handleRemoveAvatar() { + setErrorMessage(""); + setIsRemoving(true); + + try { + const updatedUser = await removeProfileAvatar(); + onUserChange(updatedUser); + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + setPreviewUrl(null); + setSelectedFile(null); + setShowRemoveConfirm(false); + if (inputRef.current) { + inputRef.current.value = ""; + } + onNotify("Profile photo removed successfully."); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : "Unable to remove profile photo."); + } finally { + setIsRemoving(false); + } + } + return (
@@ -107,6 +135,20 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata

{previewUrl ? "Previewing selected photo" : "Image updates everywhere after upload"}

+ {user.avatar_url ? ( +
+ +
+ ) : null}
@@ -115,6 +157,24 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata {errorMessage} ) : null} + + { + if (!isRemoving) { + setShowRemoveConfirm(false); + } + }} + onConfirm={handleRemoveAvatar} + confirmLabel="Confirm photo removal" + isSubmitting={isRemoving} + > +

+ You can upload a new photo again at any time from this profile page. +

+
); } diff --git a/flowbit-frontend/src/lib/auth-client.ts b/flowbit-frontend/src/lib/auth-client.ts index dc9c6be..8801c58 100644 --- a/flowbit-frontend/src/lib/auth-client.ts +++ b/flowbit-frontend/src/lib/auth-client.ts @@ -258,6 +258,24 @@ export async function uploadProfileAvatar(file: File) { return data.user as AuthUser; } +export async function removeProfileAvatar() { + const token = getStoredToken(); + if (!token) { + throw new Error("No session found."); + } + + const response = await apiRequest<{ user: AuthUser }>("/auth/avatar/", { + method: "DELETE", + headers: authHeaders(token), + }); + + if (typeof window !== "undefined") { + window.localStorage.setItem(AUTH_USER_STORAGE_KEY, JSON.stringify(response.user)); + } + + return response.user; +} + export async function logoutFromBackend() { const token = getStoredToken(); if (token) { From eddbc1ef920bb356b4826d60d2114b3fc4d83501 Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Mon, 25 May 2026 23:50:56 +0100 Subject: [PATCH 5/8] Refine avatar action button layout --- .../profile/profile-avatar-card.tsx | 108 +++++++++++------- 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx index e1a95a0..d40ea40 100644 --- a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx @@ -100,55 +100,75 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata

Avatar

Profile photo

+

+ Keep your profile recognizable with a clear square photo. Changes apply across your FlowBit workspace. +

-
-
- {previewUrl ? ( - Selected profile preview - ) : ( - - )} - {isUploading ? ( -
- +
+
+
+
+ {previewUrl ? ( + Selected profile preview + ) : ( + + )} + {isUploading ? ( +
+ +
+ ) : null}
- ) : null} -
-
-
+ +
- -

- {isUploading - ? `Uploading ${selectedFile?.name || "photo"}...` - : selectedFile - ? `${selectedFile.name} selected. Uploading now.` - : "Upload a clear square image for the best result."} -

-

- {previewUrl ? "Previewing selected photo" : "Image updates everywhere after upload"} -

- {user.avatar_url ? ( -
- + + +
+ {user.avatar_url ? ( + + ) : null}
- ) : null} +
From 3172fc431eafb52297d8b4394c41c729945805e7 Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Mon, 25 May 2026 23:51:50 +0100 Subject: [PATCH 6/8] Restructure profile overview card --- .../profile/profile-overview-card.tsx | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/flowbit-frontend/src/components/profile/profile-overview-card.tsx b/flowbit-frontend/src/components/profile/profile-overview-card.tsx index 24128f4..e673f7c 100644 --- a/flowbit-frontend/src/components/profile/profile-overview-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-overview-card.tsx @@ -8,28 +8,34 @@ type ProfileOverviewCardProps = { export function ProfileOverviewCard({ user }: ProfileOverviewCardProps) { return (
-
-
- -
-

User Profile

-

{user.full_name || user.username}

-

@{user.username}

+
+
+
+ +
+

User Profile

+
+

{user.full_name || user.username}

+ + {user.role || "User"} + +
+

@{user.username}

+

+ This profile card shows the identity and contact details used across your FlowBit workspace. +

+
-
-
-

Role

-

{user.role}

+
+
+

Contact Email

+

{user.email || "Not provided"}

-
-

Email

-

{user.email || "Not provided"}

-
-
-

Phone

-

{user.phone_number || "Not provided"}

+
+

Phone Number

+

{user.phone_number || "Not provided"}

From 2f29cefaea6b5b53d90ea61acbe351cebbbceb05 Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Mon, 25 May 2026 23:53:36 +0100 Subject: [PATCH 7/8] Polish profile overview and details cards --- .../profile/profile-details-card.tsx | 191 ++++++++++++------ .../profile/profile-overview-card.tsx | 7 +- 2 files changed, 134 insertions(+), 64 deletions(-) diff --git a/flowbit-frontend/src/components/profile/profile-details-card.tsx b/flowbit-frontend/src/components/profile/profile-details-card.tsx index 2ce0bdc..bc6cc6f 100644 --- a/flowbit-frontend/src/components/profile/profile-details-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-details-card.tsx @@ -1,8 +1,8 @@ "use client"; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faFloppyDisk } from "@fortawesome/free-solid-svg-icons"; +import { faCircleCheck, faFloppyDisk, faPenToSquare } from "@fortawesome/free-solid-svg-icons"; import { AuthInput } from "@/components/auth/auth-input"; import { Button } from "@/components/ui/button"; import { updateCurrentUserProfile, type AuthUser } from "@/lib/auth-client"; @@ -13,6 +13,20 @@ type ProfileDetailsCardProps = { onNotify: (message: string) => void; }; +function normalizeProfileForm(values: { + full_name: string; + username: string; + email: string; + phone_number: string; +}) { + return { + full_name: values.full_name.trim(), + username: values.username.trim(), + email: values.email.trim(), + phone_number: values.phone_number.trim(), + }; +} + export function ProfileDetailsCard({ user, onUserChange, onNotify }: ProfileDetailsCardProps) { const [formValues, setFormValues] = useState({ full_name: user.full_name || "", @@ -29,6 +43,32 @@ export function ProfileDetailsCard({ user, onUserChange, onNotify }: ProfileDeta const [errorMessage, setErrorMessage] = useState(""); const [isSaving, setIsSaving] = useState(false); + useEffect(() => { + setFormValues({ + full_name: user.full_name || "", + username: user.username, + email: user.email || "", + phone_number: user.phone_number || "", + }); + }, [user.full_name, user.username, user.email, user.phone_number]); + + const normalizedInitialValues = useMemo( + () => + normalizeProfileForm({ + full_name: user.full_name || "", + username: user.username, + email: user.email || "", + phone_number: user.phone_number || "", + }), + [user.full_name, user.username, user.email, user.phone_number], + ); + const normalizedFormValues = useMemo(() => normalizeProfileForm(formValues), [formValues]); + const hasChanges = + normalizedFormValues.full_name !== normalizedInitialValues.full_name || + normalizedFormValues.username !== normalizedInitialValues.username || + normalizedFormValues.email !== normalizedInitialValues.email || + normalizedFormValues.phone_number !== normalizedInitialValues.phone_number; + function validateForm() { const nextErrors: { full_name?: string; @@ -75,7 +115,7 @@ export function ProfileDetailsCard({ user, onUserChange, onNotify }: ProfileDeta setIsSaving(true); try { - const updatedUser = await updateCurrentUserProfile(formValues); + const updatedUser = await updateCurrentUserProfile(normalizedFormValues); onUserChange(updatedUser); setFormValues({ full_name: updatedUser.full_name || "", @@ -104,64 +144,94 @@ export function ProfileDetailsCard({ user, onUserChange, onNotify }: ProfileDeta

Account Details

Edit profile information

- You can update your full name, username, and phone number here. + Update the account details shown across your FlowBit workspace. Changes stay local until you save them.

-
- { - setFormValues((current) => ({ ...current, full_name: event.target.value })); - setFieldErrors((current) => ({ ...current, full_name: undefined })); - }} - /> - - { - setFormValues((current) => ({ ...current, username: event.target.value })); - setFieldErrors((current) => ({ ...current, username: undefined })); - }} - /> - - { - setFormValues((current) => ({ ...current, email: event.target.value })); - setFieldErrors((current) => ({ ...current, email: undefined })); - }} - /> - - { - setFormValues((current) => ({ ...current, phone_number: event.target.value })); - setFieldErrors((current) => ({ ...current, phone_number: undefined })); - }} - /> +
+
+
+

Profile editing

+

+ {hasChanges ? "You have unsaved changes." : "Everything is saved and up to date."} +

+
+ + + {hasChanges ? "Needs save" : "Saved"} + +
+
+
+

Identity details

+
+ { + setFormValues((current) => ({ ...current, full_name: event.target.value })); + setFieldErrors((current) => ({ ...current, full_name: undefined })); + }} + /> + + { + setFormValues((current) => ({ ...current, username: event.target.value })); + setFieldErrors((current) => ({ ...current, username: undefined })); + }} + /> +
+
+ +
+

Contact details

+
+ { + setFormValues((current) => ({ ...current, email: event.target.value })); + setFieldErrors((current) => ({ ...current, email: undefined })); + }} + /> + + { + setFormValues((current) => ({ ...current, phone_number: event.target.value })); + setFieldErrors((current) => ({ ...current, phone_number: undefined })); + }} + /> +
+
+
{errorMessage ? ( @@ -170,8 +240,11 @@ export function ProfileDetailsCard({ user, onUserChange, onNotify }: ProfileDeta
) : null} -
- diff --git a/flowbit-frontend/src/components/profile/profile-overview-card.tsx b/flowbit-frontend/src/components/profile/profile-overview-card.tsx index e673f7c..c27d9b0 100644 --- a/flowbit-frontend/src/components/profile/profile-overview-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-overview-card.tsx @@ -14,16 +14,13 @@ export function ProfileOverviewCard({ user }: ProfileOverviewCardProps) {

User Profile

-
-

{user.full_name || user.username}

+
+

{user.full_name || user.username}

{user.role || "User"}

@{user.username}

-

- This profile card shows the identity and contact details used across your FlowBit workspace. -

From 5cd7022b4477a87c55edb195aaecaa2880ab22cc Mon Sep 17 00:00:00 2001 From: Dana Khaing Date: Mon, 25 May 2026 23:55:21 +0100 Subject: [PATCH 8/8] Fix profile review follow-ups --- .../profile/profile-avatar-card.tsx | 3 +++ .../profile/profile-details-card.tsx | 22 +++++++++++-------- .../profile/profile-overview-card.tsx | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx index d40ea40..7222b12 100644 --- a/flowbit-frontend/src/components/profile/profile-avatar-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-avatar-card.tsx @@ -49,6 +49,9 @@ export function ProfileAvatarCard({ user, onUserChange, onNotify }: ProfileAvata onNotify("Profile photo updated successfully."); } catch (error) { setErrorMessage(error instanceof Error ? error.message : "Unable to upload profile photo."); + if (inputRef.current) { + inputRef.current.value = ""; + } } finally { setIsUploading(false); } diff --git a/flowbit-frontend/src/components/profile/profile-details-card.tsx b/flowbit-frontend/src/components/profile/profile-details-card.tsx index bc6cc6f..c60af29 100644 --- a/flowbit-frontend/src/components/profile/profile-details-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-details-card.tsx @@ -43,15 +43,6 @@ export function ProfileDetailsCard({ user, onUserChange, onNotify }: ProfileDeta const [errorMessage, setErrorMessage] = useState(""); const [isSaving, setIsSaving] = useState(false); - useEffect(() => { - setFormValues({ - full_name: user.full_name || "", - username: user.username, - email: user.email || "", - phone_number: user.phone_number || "", - }); - }, [user.full_name, user.username, user.email, user.phone_number]); - const normalizedInitialValues = useMemo( () => normalizeProfileForm({ @@ -69,6 +60,19 @@ export function ProfileDetailsCard({ user, onUserChange, onNotify }: ProfileDeta normalizedFormValues.email !== normalizedInitialValues.email || normalizedFormValues.phone_number !== normalizedInitialValues.phone_number; + useEffect(() => { + if (hasChanges || isSaving) { + return; + } + + setFormValues({ + full_name: user.full_name || "", + username: user.username, + email: user.email || "", + phone_number: user.phone_number || "", + }); + }, [user.full_name, user.username, user.email, user.phone_number, hasChanges, isSaving]); + function validateForm() { const nextErrors: { full_name?: string; diff --git a/flowbit-frontend/src/components/profile/profile-overview-card.tsx b/flowbit-frontend/src/components/profile/profile-overview-card.tsx index c27d9b0..0cb01f6 100644 --- a/flowbit-frontend/src/components/profile/profile-overview-card.tsx +++ b/flowbit-frontend/src/components/profile/profile-overview-card.tsx @@ -14,8 +14,8 @@ export function ProfileOverviewCard({ user }: ProfileOverviewCardProps) {

User Profile

-
-

{user.full_name || user.username}

+
+

{user.full_name || user.username}

{user.role || "User"}