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 isSSO = false ;
19+ const [ profileFields , setProfileFields ] = useState ( {
20+ firstName : "" ,
21+ lastName : "" ,
22+ email : "" ,
23+ username : "" ,
24+ } ) ;
25+ const [ initialProfileFields , setInitialProfileFields ] = useState ( null ) ;
1626 const { t } = useTranslation ( ) ;
1727
28+ useEffect ( ( ) => {
29+ if ( ! show ) return ;
30+ try {
31+ const userDetail = JSON . parse ( StorageService . get ( StorageService . User . USER_DETAILS ) ) || { } ;
32+ const fullName = userDetail ?. name || "" ;
33+ const [ firstFromName = "" , ...rest ] = String ( fullName ) . trim ( ) . split ( / \s + / ) ;
34+ const lastFromName = rest . join ( " " ) ;
35+
36+ const nextFields = {
37+ firstName : userDetail ?. given_name || firstFromName || "" ,
38+ lastName : userDetail ?. family_name || lastFromName || "" ,
39+ email : userDetail ?. email || "" ,
40+ username : userDetail ?. preferred_username || userDetail ?. username || "" ,
41+ } ;
42+ setProfileFields ( nextFields ) ;
43+ setInitialProfileFields ( nextFields ) ;
44+ } catch ( e ) {
45+ setProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
46+ setInitialProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
47+ }
48+ } , [ show ] ) ;
49+
1850 useEffect ( ( ) => {
1951
2052 fetchSelectLanguages ( ( languages ) => {
@@ -56,59 +88,196 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
5688 } ;
5789
5890 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 ) ;
91+ // Keep a copy for later integration; for now just save it locally and close the modal.
92+ const firstName = ( profileFields . firstName || "" ) . trim ( ) ;
93+ const lastName = ( profileFields . lastName || "" ) . trim ( ) ;
94+
95+ const userCopy = {
96+ user : {
97+ firstName,
98+ lastName,
99+ userName : ( profileFields . username || "" ) . trim ( ) ,
100+ email : ( profileFields . email || "" ) . trim ( ) ,
101+ attributes : {
102+ locale : [ selectedLang ] ,
103+ } ,
104+ } ,
105+ } ;
106+
107+ try {
108+ StorageService . save ( "PROFILE_SETTINGS_USER_COPY" , JSON . stringify ( userCopy ) ) ;
109+ } catch ( e ) {
110+ // ignore
66111 }
67112
68- onClose ( ) ;
113+ onClose ( ) ;
69114 } ;
70115
71116
72117 const isSaveDisabled = selectedLang === prevSelectedLang ;
118+ const isProfileChanged =
119+ ! ! initialProfileFields &&
120+ ( profileFields . firstName !== initialProfileFields . firstName ||
121+ profileFields . lastName !== initialProfileFields . lastName ||
122+ profileFields . email !== initialProfileFields . email ||
123+ profileFields . username !== initialProfileFields . username ) ;
124+ const isAnythingChanged = isProfileChanged || ! isSaveDisabled ;
73125
74126 const selectedLangLabel = selectLanguages . find ( lang => lang . name === selectedLang ) ?. value || selectedLang ;
75127
128+ const tabs = [
129+ { key : "Profile" , label : t ( "Profile" ) } ,
130+ { key : "Permissions" , label : t ( "Permissions" ) } ,
131+ ] ;
132+
133+ const resetPasswordUrl =
134+ KEYCLOAK_AUTH_URL && KEYCLOAK_REALM
135+ ? `${ KEYCLOAK_AUTH_URL } /realms/${ KEYCLOAK_REALM } /account`
136+ : null ;
137+
76138 // Get tenantId from tenant prop or StorageService
77139 const tenantId = tenant ?. tenantId || StorageService . get ( "tenantKey" ) ;
78140
79141 return (
80142 < Modal
81143 show = { show }
82144 onHide = { onClose }
83- size = "sm"
145+ size = "lg"
146+ dialogClassName = "profile-settings-modal"
84147 data-testid = "profile-settings-modal"
85148 aria-labelledby = { t ( "profile settings modal title" ) }
86149 aria-describedby = "profile-settings-modal"
87150 backdrop = "static"
88151 >
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 />
152+ < Modal . Header >
153+ < div className = "modal-header-content" >
154+ < div className = "modal-title pb-0" >
155+ < p > { t ( "Personal Settings" ) } </ p >
156+ < CloseIcon color = "var(--gray-darkest)" onClick = { onClose } />
157+ </ div >
158+ < div className = "modal-subtitle pb-0" >
159+ < div className = 'secondary-controls' >
160+ < div className = 'pill-tabs-container' >
161+ < Tabs
162+ activeKey = { activeTab }
163+ onSelect = { ( key ) => key && setActiveTab ( key ) }
164+ id = "profile-settings-tabs"
165+ data-testid = "profile-settings-tabs"
166+ className = "pill-tabs"
167+ >
168+ { tabs . map ( ( tab ) => (
169+ < Tab
170+ key = { tab . key }
171+ eventKey = { tab . key }
172+ title = {
173+ < span data-testid = { `profile-settings-${ tab . key } -tab` } >
174+ { tab . label }
175+ </ span >
176+ }
177+ >
178+ { /* Empty content; this is navigation. Body renders based on activeTab. */ }
179+ </ Tab >
180+ ) ) }
181+ </ Tabs >
182+ </ div >
183+ </ div >
184+
185+ </ div >
95186 </ div >
187+
96188 </ Modal . Header >
97189
98190 < 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- />
191+ { activeTab === "Profile" ? (
192+ < >
193+ < div className = "profile-settings-details-box p-3 mb-3 border rounded" >
194+ < div className = "row g-3" >
195+ < div className = "col-12 col-md-6" >
196+ < div className = "input-label-text" > { t ( "First Name" ) } </ div >
197+ < CustomTextInput
198+ value = { profileFields . firstName }
199+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , firstName : v } ) ) }
200+ placeholder = "First Name"
201+ dataTestId = "profile-first-name"
202+ ariaLabel = { t ( "First Name" ) }
203+ disabled = { isSSO }
204+ />
205+ </ div >
206+ < div className = "col-12 col-md-6" >
207+ < div className = "input-label-text" > { t ( "Last Name" ) } </ div >
208+ < CustomTextInput
209+ value = { profileFields . lastName }
210+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , lastName : v } ) ) }
211+ placeholder = "Last Name"
212+ dataTestId = "profile-last-name"
213+ ariaLabel = { t ( "Last Name" ) }
214+ disabled = { isSSO }
215+ />
216+ </ div >
217+ < div className = "col-12 col-md-6" >
218+ < div className = "input-label-text" > { t ( "Email" ) } </ div >
219+ < CustomTextInput
220+ value = { profileFields . email }
221+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , email : v } ) ) }
222+ placeholder = "Email"
223+ dataTestId = "profile-email"
224+ ariaLabel = { t ( "Email" ) }
225+ disabled = { isSSO }
226+ />
227+ </ div >
228+ < div className = "col-12 col-md-6" >
229+ < div className = "input-label-text" > { t ( "Username" ) } </ div >
230+ < CustomTextInput
231+ value = { profileFields . username }
232+ setValue = { ( v ) => setProfileFields ( ( p ) => ( { ...p , username : v } ) ) }
233+ placeholder = "Username"
234+ dataTestId = "profile-username"
235+ ariaLabel = { t ( "Username" ) }
236+ disabled = { isSSO }
237+ />
238+ </ div >
239+ < div className = "col-12" >
240+ < V8CustomButton
241+ label = { t ( "Reset Password" ) }
242+ variant = "secondary"
243+ 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+ } }
250+ />
251+ </ div >
252+ < div className = "col-12" >
253+ < CustomInfo
254+ className = "profile-settings-note-panel"
255+ variant = "secondary"
256+ icon = { < ApplicationLogo width = "1.1875rem" height = "1.4993rem" /> }
257+ content = { t ( "Success! Check your email inbox for next steps." ) }
258+ />
259+ </ div >
260+ </ div >
261+ </ div >
262+
263+ < SelectDropdown
264+ options = { selectLanguages . map ( ( lang ) => ( {
265+ label : lang . value ,
266+ value : lang . name ,
267+ } ) ) }
268+ width = "22rem"
269+ defaultValue = { selectedLangLabel }
270+ dataTestId = "settings-language-dropdown"
271+ ariaLabel = { t ( "Language Dropdown" ) }
272+ value = { selectedLangLabel }
273+ variant = "primary"
274+ className = "mb-3"
275+ onChange = { handleLanguageChange }
276+ />
277+ </ >
278+ ) : (
279+ < div > </ div >
280+ ) }
112281 { tenantId && daysDifference !== null ? (
113282 < CustomInfo
114283 className = "note"
@@ -123,20 +292,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
123292 < Modal . Footer >
124293 < div className = "buttons-row" >
125294 < V8CustomButton
126- label = { t ( "Save Changes " ) }
295+ label = { t ( "Update " ) }
127296 onClick = { handleConfirmProfile }
128297 dataTestId = "save-profile-settings"
129298 ariaLabel = { t ( "Save Profile Settings" ) }
130- disabled = { isSaveDisabled }
299+ disabled = { activeTab !== "Profile" || ! isAnythingChanged }
131300 variant = "primary"
132301 />
133- < V8CustomButton
134- label = { t ( "Cancel" ) }
135- onClick = { onClose }
136- dataTestId = "cancel-profile-settings"
137- ariaLabel = { t ( "Cancel profile settings" ) }
138- variant = "secondary"
139- />
302+
140303 </ div >
141304 </ Modal . Footer >
142305 </ Modal >
0 commit comments