11import React , { useState , useEffect } from 'react' ;
22import PropTypes from 'prop-types' ;
33import Modal from 'react-bootstrap/Modal' ;
4- import { CloseIcon , V8CustomButton , CustomInfo , SelectDropdown } from "@formsflow/components" ;
4+ import { Tabs , Tab } from 'react-bootstrap' ;
5+ import { CloseIcon , V8CustomButton , CustomInfo , SelectDropdown , CustomTextInput , ApplicationLogo } from "@formsflow/components" ;
56import { fetchSelectLanguages , updateUserlang } from '../services/language' ;
67import { useTranslation } from "react-i18next" ;
78import i18n from '../resourceBundles/i18n' ;
89import { StorageService } from "@formsflow/service" ;
9- import { LANGUAGE , MULTITENANCY_ENABLED , USER_LANGUAGE_LIST } from '../constants/constants' ;
10+ import { KEYCLOAK_AUTH_URL , KEYCLOAK_REALM , LANGUAGE , MULTITENANCY_ENABLED , USER_LANGUAGE_LIST } from '../constants/constants' ;
1011
1112export const ProfileSettingsModal = ( { show, onClose, tenant, publish } ) => {
1213 const [ selectLanguages , setSelectLanguages ] = useState ( [ ] ) ;
1314 const prevSelectedLang = localStorage . getItem ( 'i18nextLng' ) ;
1415 const [ selectedLang , setSelectedLang ] = useState ( prevSelectedLang || LANGUAGE ) ;
1516 const [ daysDifference , setDaysDifference ] = useState ( null ) ;
17+ const [ activeTab , setActiveTab ] = useState ( "Profile" ) ;
18+ const [ profileFields , setProfileFields ] = useState ( {
19+ firstName : "" ,
20+ lastName : "" ,
21+ email : "" ,
22+ username : "" ,
23+ } ) ;
24+ const [ initialProfileFields , setInitialProfileFields ] = useState ( null ) ;
1625 const { t } = useTranslation ( ) ;
1726
27+ useEffect ( ( ) => {
28+ if ( ! show ) return ;
29+ try {
30+ const userDetail = JSON . parse ( StorageService . get ( StorageService . User . USER_DETAILS ) ) || { } ;
31+ const fullName = userDetail ?. name || "" ;
32+ const [ firstFromName = "" , ...rest ] = String ( fullName ) . trim ( ) . split ( / \s + / ) ;
33+ const lastFromName = rest . join ( " " ) ;
34+
35+ const nextFields = {
36+ firstName : userDetail ?. given_name || firstFromName || "" ,
37+ lastName : userDetail ?. family_name || lastFromName || "" ,
38+ email : userDetail ?. email || "" ,
39+ username : userDetail ?. preferred_username || userDetail ?. username || "" ,
40+ } ;
41+ setProfileFields ( nextFields ) ;
42+ setInitialProfileFields ( nextFields ) ;
43+ } catch ( e ) {
44+ setProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
45+ setInitialProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
46+ }
47+ } , [ show ] ) ;
48+
1849 useEffect ( ( ) => {
1950
2051 fetchSelectLanguages ( ( languages ) => {
@@ -56,59 +87,192 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
5687 } ;
5788
5889 const handleConfirmProfile = ( ) => {
59- updateUserlang ( selectedLang ) ;
60- i18n . changeLanguage ( selectedLang ) ;
61- if ( tenant ?. tenantData ?. details ) {
62- tenant . tenantData . details . locale = selectedLang ;
63- }
64- if ( selectedLang ) {
65- publish ( "ES_CHANGE_LANGUAGE" , selectedLang ) ;
90+ // Keep a copy for later integration; for now just save it locally and close the modal.
91+ const firstName = ( profileFields . firstName || "" ) . trim ( ) ;
92+ const lastName = ( profileFields . lastName || "" ) . trim ( ) ;
93+
94+ const userCopy = {
95+ user : {
96+ firstName,
97+ lastName,
98+ userName : ( profileFields . username || "" ) . trim ( ) ,
99+ email : ( profileFields . email || "" ) . trim ( ) ,
100+ attributes : {
101+ locale : [ selectedLang ] ,
102+ } ,
103+ } ,
104+ } ;
105+
106+ try {
107+ StorageService . save ( "PROFILE_SETTINGS_USER_COPY" , JSON . stringify ( userCopy ) ) ;
108+ } catch ( e ) {
109+ // ignore
66110 }
67111
68- onClose ( ) ;
112+ onClose ( ) ;
69113 } ;
70114
71115
72116 const isSaveDisabled = selectedLang === prevSelectedLang ;
117+ const isProfileChanged =
118+ ! ! initialProfileFields &&
119+ ( profileFields . firstName !== initialProfileFields . firstName ||
120+ profileFields . lastName !== initialProfileFields . lastName ||
121+ profileFields . email !== initialProfileFields . email ||
122+ profileFields . username !== initialProfileFields . username ) ;
123+ const isAnythingChanged = isProfileChanged || ! isSaveDisabled ;
73124
74125 const selectedLangLabel = selectLanguages . find ( lang => lang . name === selectedLang ) ?. value || selectedLang ;
75126
127+ const tabs = [
128+ { key : "Profile" , label : t ( "Profile" ) } ,
129+ { key : "Permissions" , label : t ( "Permissions" ) } ,
130+ ] ;
131+
132+ const resetPasswordUrl =
133+ KEYCLOAK_AUTH_URL && KEYCLOAK_REALM
134+ ? `${ KEYCLOAK_AUTH_URL } /realms/${ KEYCLOAK_REALM } /account`
135+ : null ;
136+
76137 // Get tenantId from tenant prop or StorageService
77138 const tenantId = tenant ?. tenantId || StorageService . get ( "tenantKey" ) ;
78139
79140 return (
80141 < Modal
81142 show = { show }
82143 onHide = { onClose }
83- size = "sm"
144+ size = "lg"
145+ dialogClassName = "profile-settings-modal"
84146 data-testid = "profile-settings-modal"
85147 aria-labelledby = { t ( "profile settings modal title" ) }
86148 aria-describedby = "profile-settings-modal"
87149 backdrop = "static"
88150 >
89- < Modal . Header className = "justify-content-between" >
90- < Modal . Title id = "profile-modal-title" >
91- < p > { t ( "Settings" ) } </ p >
92- </ Modal . Title >
93- < div className = "icon-close" onClick = { onClose } >
94- < CloseIcon />
151+ < Modal . Header >
152+ < div className = "modal-header-content" >
153+ < div className = "modal-title pb-0" >
154+ < p > { t ( "Personal Settings" ) } </ p >
155+ < CloseIcon color = "var(--gray-darkest)" onClick = { onClose } />
156+ </ div >
157+ < div className = "modal-subtitle pb-0" >
158+ < div className = 'secondary-controls' >
159+ < div className = 'pill-tabs-container' >
160+ < Tabs
161+ activeKey = { activeTab }
162+ onSelect = { ( key ) => key && setActiveTab ( key ) }
163+ id = "profile-settings-tabs"
164+ data-testid = "profile-settings-tabs"
165+ className = "pill-tabs"
166+ >
167+ { tabs . map ( ( tab ) => (
168+ < Tab
169+ key = { tab . key }
170+ eventKey = { tab . key }
171+ title = {
172+ < span data-testid = { `profile-settings-${ tab . key } -tab` } >
173+ { tab . label }
174+ </ span >
175+ }
176+ >
177+ { /* Empty content; this is navigation. Body renders based on activeTab. */ }
178+ </ Tab >
179+ ) ) }
180+ </ Tabs >
181+ </ div >
182+ </ div >
183+
184+ </ div >
95185 </ div >
186+
96187 </ Modal . Header >
97188
98189 < Modal . Body >
99- < SelectDropdown
100- options = { selectLanguages . map ( ( lang ) => ( {
101- label : lang . value ,
102- value : lang . name ,
103- } ) ) }
104- defaultValue = { selectedLangLabel }
105- dataTestId = "settings-language-dropdown"
106- ariaLabel = { t ( "Language Dropdown" ) }
107- value = { selectedLangLabel }
108- variant = "primary"
109- className = "mb-3 w-100"
110- onChange = { handleLanguageChange }
111- />
190+ { activeTab === "Profile" ? (
191+ < >
192+ < div className = "profile-settings-details-box p-3 mb-3 border rounded" >
193+ < div className = "row g-3" >
194+ < div className = "col-12 col-md-6" >
195+ < div className = "input-label-text" > { t ( "First Name" ) } </ div >
196+ < CustomTextInput
197+ value = { profileFields . firstName }
198+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , firstName : v } ) ) }
199+ placeholder = "First Name"
200+ dataTestId = "profile-first-name"
201+ ariaLabel = { t ( "First Name" ) }
202+ />
203+ </ div >
204+ < div className = "col-12 col-md-6" >
205+ < div className = "input-label-text" > { t ( "Last Name" ) } </ div >
206+ < CustomTextInput
207+ value = { profileFields . lastName }
208+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , lastName : v } ) ) }
209+ placeholder = "Last Name"
210+ dataTestId = "profile-last-name"
211+ ariaLabel = { t ( "Last Name" ) }
212+ />
213+ </ div >
214+ < div className = "col-12 col-md-6" >
215+ < div className = "input-label-text" > { t ( "Email" ) } </ div >
216+ < CustomTextInput
217+ value = { profileFields . email }
218+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , email : v } ) ) }
219+ placeholder = "Email"
220+ dataTestId = "profile-email"
221+ ariaLabel = { t ( "Email" ) }
222+ />
223+ </ div >
224+ < div className = "col-12 col-md-6" >
225+ < div className = "input-label-text" > { t ( "Username" ) } </ div >
226+ < CustomTextInput
227+ value = { profileFields . username }
228+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , username : v } ) ) }
229+ placeholder = "Username"
230+ dataTestId = "profile-username"
231+ ariaLabel = { t ( "Username" ) }
232+ />
233+ </ div >
234+ < div className = "col-12" >
235+ < V8CustomButton
236+ label = { t ( "Reset Password" ) }
237+ variant = "secondary"
238+ dataTestId = "profile-reset-password"
239+ ariaLabel = { t ( "Reset Password" ) }
240+ disabled = { ! resetPasswordUrl }
241+ onClick = { ( ) => {
242+ if ( ! resetPasswordUrl ) return ;
243+ window . open ( resetPasswordUrl , "_blank" , "noopener,noreferrer" ) ;
244+ } }
245+ />
246+ </ div >
247+ < div className = "col-12" >
248+ < CustomInfo
249+ className = "profile-settings-note-panel"
250+ variant = "secondary"
251+ icon = { < ApplicationLogo width = "1.1875rem" height = "1.4993rem" /> }
252+ content = { t ( "Success! Check your email inbox for next steps." ) }
253+ />
254+ </ div >
255+ </ div >
256+ </ div >
257+
258+ < SelectDropdown
259+ options = { selectLanguages . map ( ( lang ) => ( {
260+ label : lang . value ,
261+ value : lang . name ,
262+ } ) ) }
263+ width = "22rem"
264+ defaultValue = { selectedLangLabel }
265+ dataTestId = "settings-language-dropdown"
266+ ariaLabel = { t ( "Language Dropdown" ) }
267+ value = { selectedLangLabel }
268+ variant = "primary"
269+ className = "mb-3"
270+ onChange = { handleLanguageChange }
271+ />
272+ </ >
273+ ) : (
274+ < div > </ div >
275+ ) }
112276 { tenantId && daysDifference !== null ? (
113277 < CustomInfo
114278 className = "note"
@@ -123,20 +287,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
123287 < Modal . Footer >
124288 < div className = "buttons-row" >
125289 < V8CustomButton
126- label = { t ( "Save Changes " ) }
290+ label = { t ( "Update " ) }
127291 onClick = { handleConfirmProfile }
128292 dataTestId = "save-profile-settings"
129293 ariaLabel = { t ( "Save Profile Settings" ) }
130- disabled = { isSaveDisabled }
294+ disabled = { activeTab !== "Profile" || ! isAnythingChanged }
131295 variant = "primary"
132296 />
133- < V8CustomButton
134- label = { t ( "Cancel" ) }
135- onClick = { onClose }
136- dataTestId = "cancel-profile-settings"
137- ariaLabel = { t ( "Cancel profile settings" ) }
138- variant = "secondary"
139- />
297+
140298 </ div >
141299 </ Modal . Footer >
142300 </ Modal >
0 commit comments