@@ -2,12 +2,13 @@ import React, { useState, useEffect } from 'react';
22import PropTypes from 'prop-types' ;
33import Modal from 'react-bootstrap/Modal' ;
44import { Tabs , Tab } from 'react-bootstrap' ;
5- import { CloseIcon , V8CustomButton , CustomInfo , SelectDropdown , CustomTextInput , ApplicationLogo } from "@formsflow/components" ;
6- import { fetchSelectLanguages , updateUserlang } from '../services/language' ;
5+ import { CloseIcon , V8CustomButton , CustomInfo , SelectDropdown , CustomTextInput , ApplicationLogo , PromptModal } from "@formsflow/components" ;
6+ import { fetchSelectLanguages } from '../services/language' ;
7+ import { requestResetPassword } from "../services/user" ;
78import { useTranslation } from "react-i18next" ;
89import i18n from '../resourceBundles/i18n' ;
910import { StorageService } from "@formsflow/service" ;
10- import { KEYCLOAK_AUTH_URL , KEYCLOAK_REALM , LANGUAGE , MULTITENANCY_ENABLED , USER_LANGUAGE_LIST } from '../constants/constants' ;
11+ import { LANGUAGE , MULTITENANCY_ENABLED , USER_LANGUAGE_LIST } from '../constants/constants' ;
1112
1213export const ProfileSettingsModal = ( { show, onClose, tenant, publish } ) => {
1314 const [ selectLanguages , setSelectLanguages ] = useState ( [ ] ) ;
@@ -16,18 +17,30 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
1617 const [ daysDifference , setDaysDifference ] = useState ( null ) ;
1718 const [ activeTab , setActiveTab ] = useState ( "Profile" ) ;
1819 const isSSO = false ;
20+ const [ showUnsavedChangesPrompt , setShowUnsavedChangesPrompt ] = useState ( false ) ;
1921 const [ profileFields , setProfileFields ] = useState ( {
2022 firstName : "" ,
2123 lastName : "" ,
2224 email : "" ,
2325 username : "" ,
2426 } ) ;
2527 const [ initialProfileFields , setInitialProfileFields ] = useState ( null ) ;
28+ const [ initialSelectedLang , setInitialSelectedLang ] = useState ( prevSelectedLang || LANGUAGE ) ;
29+ const [ emailTouched , setEmailTouched ] = useState ( false ) ;
30+ const [ usernameTouched , setUsernameTouched ] = useState ( false ) ;
31+ const [ resetPasswordState , setResetPasswordState ] = useState ( "default" ) ; // default | success | error
32+ const [ resetPasswordLoading , setResetPasswordLoading ] = useState ( false ) ;
33+ const [ lastResetPasswordError , setLastResetPasswordError ] = useState ( null ) ;
2634 const { t } = useTranslation ( ) ;
2735
2836 useEffect ( ( ) => {
2937 if ( ! show ) return ;
3038 try {
39+ // Reset language selection to current app language when modal opens
40+ const currentLang = localStorage . getItem ( "i18nextLng" ) || LANGUAGE ;
41+ setSelectedLang ( currentLang ) ;
42+ setInitialSelectedLang ( currentLang ) ;
43+
3144 const userDetail = JSON . parse ( StorageService . get ( StorageService . User . USER_DETAILS ) ) || { } ;
3245 const fullName = userDetail ?. name || "" ;
3346 const [ firstFromName = "" , ...rest ] = String ( fullName ) . trim ( ) . split ( / \s + / ) ;
@@ -41,9 +54,16 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
4154 } ;
4255 setProfileFields ( nextFields ) ;
4356 setInitialProfileFields ( nextFields ) ;
57+ setEmailTouched ( false ) ;
58+ setUsernameTouched ( false ) ;
59+ setResetPasswordState ( "default" ) ;
60+ setResetPasswordLoading ( false ) ;
61+ setLastResetPasswordError ( null ) ;
4462 } catch ( e ) {
4563 setProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
4664 setInitialProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
65+ setSelectedLang ( prevSelectedLang || LANGUAGE ) ;
66+ setInitialSelectedLang ( prevSelectedLang || LANGUAGE ) ;
4767 }
4868 } , [ show ] ) ;
4969
@@ -87,6 +107,52 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
87107 setSelectedLang ( newLang ) ;
88108 } ;
89109
110+ const isValidEmail = ( email ) => {
111+ // Simple, practical email validation (good UX, not overly strict)
112+ const value = String ( email || "" ) . trim ( ) ;
113+ if ( ! value ) return true ; // allow empty if your system permits it
114+ return / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / . test ( value ) ;
115+ } ;
116+ const emailIsInvalid = emailTouched && ! isValidEmail ( profileFields . email ) ;
117+
118+ const isValidUsername = ( username ) => {
119+ const value = String ( username || "" ) ;
120+ if ( ! value ) return true ; // allow empty if your system permits it
121+ return / ^ \S + $ / . test ( value ) ; // no whitespace
122+ } ;
123+ const usernameIsInvalid =
124+ usernameTouched && ! isValidUsername ( profileFields . username ) ;
125+
126+ useEffect ( ( ) => {
127+ setResetPasswordState ( "default" ) ;
128+ } , [ profileFields . email ] ) ;
129+
130+ const handleResetPassword = async ( ) => {
131+ setEmailTouched ( true ) ;
132+ if ( ! profileFields . email || emailIsInvalid ) return ;
133+
134+ setResetPasswordLoading ( true ) ;
135+ try {
136+ await requestResetPassword ( ) ;
137+ setResetPasswordState ( "success" ) ;
138+ setLastResetPasswordError ( null ) ;
139+ } catch ( e ) {
140+ setResetPasswordState ( "error" ) ;
141+ const status = e ?. response ?. status ;
142+ const message = e ?. response ?. data ?. message || e ?. message ;
143+ const details = { status, message, data : e ?. response ?. data } ;
144+ setLastResetPasswordError ( details ) ;
145+ try {
146+ StorageService . save ( "PROFILE_RESET_PASSWORD_LAST_ERROR" , JSON . stringify ( details ) ) ;
147+ } catch ( _ ) {
148+ }
149+
150+ console . error ( "Reset password failed:" , details ) ;
151+ } finally {
152+ setResetPasswordLoading ( false ) ;
153+ }
154+ } ;
155+
90156 const handleConfirmProfile = ( ) => {
91157 // Keep a copy for later integration; for now just save it locally and close the modal.
92158 const firstName = ( profileFields . firstName || "" ) . trim ( ) ;
@@ -121,7 +187,32 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
121187 profileFields . lastName !== initialProfileFields . lastName ||
122188 profileFields . email !== initialProfileFields . email ||
123189 profileFields . username !== initialProfileFields . username ) ;
124- const isAnythingChanged = isProfileChanged || ! isSaveDisabled ;
190+ const isLangChanged = selectedLang !== initialSelectedLang ;
191+ const isAnythingChanged = isProfileChanged || isLangChanged ;
192+
193+ const handleRequestClose = ( ) => {
194+ if ( activeTab === "Profile" && isAnythingChanged ) {
195+ setShowUnsavedChangesPrompt ( true ) ;
196+ return ;
197+ }
198+ onClose ( ) ;
199+ } ;
200+
201+ const handleDiscardAndClose = ( ) => {
202+ setShowUnsavedChangesPrompt ( false ) ;
203+ if ( initialProfileFields ) setProfileFields ( initialProfileFields ) ;
204+ setSelectedLang ( initialSelectedLang ) ;
205+ setEmailTouched ( false ) ;
206+ setUsernameTouched ( false ) ;
207+ setResetPasswordState ( "default" ) ;
208+ setLastResetPasswordError ( null ) ;
209+ onClose ( ) ;
210+ } ;
211+
212+ const handleSaveAndClose = ( ) => {
213+ setShowUnsavedChangesPrompt ( false ) ;
214+ handleConfirmProfile ( ) ;
215+ } ;
125216
126217 const selectedLangLabel = selectLanguages . find ( lang => lang . name === selectedLang ) ?. value || selectedLang ;
127218
@@ -130,18 +221,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
130221 { key : "Permissions" , label : t ( "Permissions" ) } ,
131222 ] ;
132223
133- const resetPasswordUrl =
134- KEYCLOAK_AUTH_URL && KEYCLOAK_REALM
135- ? `${ KEYCLOAK_AUTH_URL } /realms/${ KEYCLOAK_REALM } /account`
136- : null ;
137-
138224 // Get tenantId from tenant prop or StorageService
139225 const tenantId = tenant ?. tenantId || StorageService . get ( "tenantKey" ) ;
140226
141227 return (
228+ < >
142229 < Modal
143230 show = { show }
144- onHide = { onClose }
231+ onHide = { handleRequestClose }
145232 size = "lg"
146233 dialogClassName = "profile-settings-modal"
147234 data-testid = "profile-settings-modal"
@@ -153,7 +240,7 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
153240 < div className = "modal-header-content" >
154241 < div className = "modal-title pb-0" >
155242 < p > { t ( "Personal Settings" ) } </ p >
156- < CloseIcon color = "var(--gray-darkest)" onClick = { onClose } />
243+ < CloseIcon color = "var(--gray-darkest)" onClick = { handleRequestClose } />
157244 </ div >
158245 < div className = "modal-subtitle pb-0" >
159246 < div className = 'secondary-controls' >
@@ -223,7 +310,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
223310 dataTestId = "profile-email"
224311 ariaLabel = { t ( "Email" ) }
225312 disabled = { isSSO }
313+ onBlur = { ( ) => setEmailTouched ( true ) }
226314 />
315+ < div
316+ className = "profile-settings-field-error text-danger mt-1"
317+ data-testid = "profile-email-error"
318+ >
319+ { emailIsInvalid ? t ( "Email address is invalid" ) : "" }
320+ </ div >
227321 </ div >
228322 < div className = "col-12 col-md-6" >
229323 < div className = "input-label-text" > { t ( "Username" ) } </ div >
@@ -234,27 +328,50 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
234328 dataTestId = "profile-username"
235329 ariaLabel = { t ( "Username" ) }
236330 disabled = { isSSO }
331+ onBlur = { ( ) => setUsernameTouched ( true ) }
237332 />
333+ < div
334+ className = "profile-settings-field-error text-danger mt-1"
335+ data-testid = "profile-username-error"
336+ >
337+ { usernameIsInvalid ? t ( "Username is invalid" ) : "" }
338+ </ div >
238339 </ div >
239340 < div className = "col-12" >
240341 < V8CustomButton
241- label = { t ( " Reset Password") }
342+ label = { resetPasswordLoading ? "Resetting password" : " Reset Password"}
242343 variant = "secondary"
243344 dataTestId = "profile-reset-password"
244- ariaLabel = { t ( "Reset Password" ) }
245- disabled = { ! resetPasswordUrl }
246- onClick = { ( ) => {
247- if ( ! resetPasswordUrl ) return ;
248- window . open ( resetPasswordUrl , "_blank" , "noopener,noreferrer" ) ;
249- } }
345+ ariaLabel = "Reset Password"
346+ disabled = {
347+ isSSO ||
348+ resetPasswordLoading ||
349+ ! profileFields . email ||
350+ emailIsInvalid
351+ }
352+ loading = { resetPasswordLoading }
353+ onClick = { handleResetPassword }
250354 />
251355 </ div >
252356 < div className = "col-12" >
253357 < CustomInfo
254- className = "profile-settings-note-panel"
358+ className = { [
359+ "profile-settings-note-panel" ,
360+ resetPasswordState === "error"
361+ ? "profile-settings-note-panel--danger-text"
362+ : "" ,
363+ ] . join ( " " ) }
255364 variant = "secondary"
256365 icon = { < ApplicationLogo width = "1.1875rem" height = "1.4993rem" /> }
257- content = { t ( "Success! Check your email inbox for next steps." ) }
366+ content = {
367+ resetPasswordState === "success"
368+ ? t ( "Success! Check your email inbox for next steps." )
369+ : resetPasswordState === "error"
370+ ? t ( "Uh-oh! Something went wrong. Please try again." )
371+ : t ( "Resetting your password sends a reset link to {{email}}." , {
372+ email : profileFields . email || "" ,
373+ } )
374+ }
258375 />
259376 </ div >
260377 </ div >
@@ -296,13 +413,30 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
296413 onClick = { handleConfirmProfile }
297414 dataTestId = "save-profile-settings"
298415 ariaLabel = { t ( "Save Profile Settings" ) }
299- disabled = { activeTab !== "Profile" || ! isAnythingChanged }
416+ disabled = { activeTab !== "Profile" || ! isAnythingChanged || emailIsInvalid || usernameIsInvalid }
300417 variant = "primary"
301418 />
302419
303420 </ div >
304421 </ Modal . Footer >
305422 </ Modal >
423+
424+ < PromptModal
425+ show = { showUnsavedChangesPrompt }
426+ size = "sm"
427+ onClose = { ( ) => setShowUnsavedChangesPrompt ( false ) }
428+ type = "warning"
429+ title = { t ( "You have unsaved changes" ) }
430+ message = { t ( "Leaving will discard any unsaved changes. Are you sure you want to continue?" ) }
431+ primaryBtnText = { t ( "Save" ) }
432+ secondaryBtnText = { t ( "Discard Changes" ) }
433+ primaryBtnAction = { handleSaveAndClose }
434+ secondaryBtnAction = { handleDiscardAndClose }
435+ primaryBtnDisable = { emailIsInvalid || usernameIsInvalid }
436+ primaryBtndataTestid = "profile-settings-save-before-close"
437+ secondoryBtndataTestid = "profile-settings-discard-before-close"
438+ />
439+ </ >
306440 ) ;
307441} ;
308442
0 commit comments