Skip to content

Commit 8c39f2a

Browse files
authored
Merge pull request #1096 from Bonymol-aot/FWF-5972/user-settings-modal
Fwf 5972/user settings modal
2 parents 5a3b241 + 9920cf1 commit 8c39f2a

File tree

4 files changed

+277
-48
lines changed

4 files changed

+277
-48
lines changed

forms-flow-components/src/components/CustomComponents/CustomInfo.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface CustomInfoProps {
99
variant?: InfoVariant;
1010
className?: string;
1111
dataTestId?: string;
12+
/** Optional icon override (replaces the default InfoIcon). */
13+
icon?: React.ReactNode;
1214
}
1315

1416
/**
@@ -22,15 +24,17 @@ export const CustomInfo: FC<CustomInfoProps> = ({
2224
content,
2325
variant = "primary",
2426
className,
25-
dataTestId
27+
dataTestId,
28+
icon
2629
}) => {
2730
const { t } = useTranslation();
2831

29-
// Replace `\n` with <br /> tags and use the line itself as a key
30-
const formattedContent = content.split("\n").map((line) => (
31-
<React.Fragment key={line.trim().replace(/\s+/g, "-")}>
32+
// Replace `\n` with <br /> tags (no trailing <br />)
33+
const contentLines = content.split("\n");
34+
const formattedContent = contentLines.map((line, idx) => (
35+
<React.Fragment key={`${idx}-${line.trim().replace(/\s+/g, "-")}`}>
3236
{t(line)}
33-
<br />
37+
{idx < contentLines.length - 1 ? <br /> : null}
3438
</React.Fragment>
3539
));
3640

@@ -43,7 +47,7 @@ export const CustomInfo: FC<CustomInfoProps> = ({
4347
return (
4448
<div className={panelClassName} data-testid={dataTestId}>
4549
<div className="info-icon">
46-
<InfoIcon variant={variant} />
50+
{icon ?? <InfoIcon variant={variant} />}
4751
</div>
4852
<div className="info-content">{formattedContent}</div>
4953
</div>

forms-flow-components/src/components/SvgIcons/index.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,17 @@ export const BackToPrevIcon = ({ color = baseColor, onClick, ...props }) => (
398398
</svg>
399399
);
400400

401-
export const ApplicationLogo = ({ color = baseColor, ...props }) => (
401+
export const ApplicationLogo = ({
402+
color = "#1B34FB",
403+
width = 16,
404+
height = 21,
405+
...props
406+
}: {
407+
color?: string;
408+
width?: number | string;
409+
height?: number | string;
410+
[key: string]: any;
411+
}) => (
402412
// <svg
403413
// xmlns="http://www.w3.org/2000/svg"
404414
// width="144"
@@ -458,8 +468,15 @@ export const ApplicationLogo = ({ color = baseColor, ...props }) => (
458468
// fill="black"
459469
// />
460470
// </svg>
461-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="21" viewBox="0 0 16 21" fill="none">
462-
<path fillRule="evenodd" clipRule="evenodd" d="M7.30992 0.241904C6.98738 -0.0806347 6.46444 -0.0806347 6.1419 0.241904C5.81937 0.564443 5.81937 1.08738 6.1419 1.40992L7.1741 2.44212C3.14386 2.85546 0 6.26059 0 10.4C0 13.2954 1.53818 15.8316 3.84197 17.2359C4.27304 17.4987 4.8126 17.2432 4.94327 16.7555C5.04336 16.382 4.86452 15.994 4.53938 15.7846C2.77105 14.6458 1.6 12.6596 1.6 10.4C1.6 7.45211 3.59305 4.96973 6.30516 4.22682L6.1419 4.39008C5.81937 4.71262 5.81937 5.23556 6.1419 5.5581C6.46444 5.88064 6.98738 5.88064 7.30992 5.5581L9.38401 3.48401C9.70655 3.16147 9.70655 2.63853 9.38401 2.31599L7.30992 0.241904ZM8.21601 19.9581C8.53855 20.2806 9.06149 20.2806 9.38402 19.9581C9.70656 19.6356 9.70656 19.1126 9.38402 18.7901L8.93937 18.3454C12.915 17.8804 16 14.5005 16 10.4C16 7.41613 14.3664 4.8138 11.9448 3.4386C11.5152 3.19463 10.9928 3.4512 10.865 3.92841C10.7625 4.31082 10.9532 4.70681 11.2925 4.91077C13.1544 6.02993 14.4 8.0695 14.4 10.4C14.4 13.798 11.7518 16.5775 8.40666 16.7873L9.38402 15.8099C9.70656 15.4874 9.70656 14.9645 9.38402 14.6419C9.06149 14.3194 8.53855 14.3194 8.21601 14.6419L6.14192 16.716C5.81938 17.0385 5.81938 17.5615 6.14192 17.884L8.21601 19.9581ZM11.5259 7.53431C11.2135 7.22189 10.7065 7.22189 10.394 7.53431L7.1997 10.7286L5.80517 9.33412C5.49276 9.02201 4.98664 9.02189 4.67431 9.33412C4.36189 9.64654 4.36189 10.1535 4.67431 10.466L6.6411 12.4318C6.95352 12.7442 7.45954 12.7442 7.77196 12.4318C7.80682 12.3969 7.83766 12.3596 7.86474 12.3204C7.88561 12.3034 7.90582 12.2852 7.92528 12.2658L11.5259 8.66517C11.8379 8.35277 11.838 7.84664 11.5259 7.53431Z" fill="#253DF4"/>
471+
<svg
472+
xmlns="http://www.w3.org/2000/svg"
473+
width={width}
474+
height={height}
475+
viewBox="0 0 16 21"
476+
fill="none"
477+
{...props}
478+
>
479+
<path fillRule="evenodd" clipRule="evenodd" d="M7.30992 0.241904C6.98738 -0.0806347 6.46444 -0.0806347 6.1419 0.241904C5.81937 0.564443 5.81937 1.08738 6.1419 1.40992L7.1741 2.44212C3.14386 2.85546 0 6.26059 0 10.4C0 13.2954 1.53818 15.8316 3.84197 17.2359C4.27304 17.4987 4.8126 17.2432 4.94327 16.7555C5.04336 16.382 4.86452 15.994 4.53938 15.7846C2.77105 14.6458 1.6 12.6596 1.6 10.4C1.6 7.45211 3.59305 4.96973 6.30516 4.22682L6.1419 4.39008C5.81937 4.71262 5.81937 5.23556 6.1419 5.5581C6.46444 5.88064 6.98738 5.88064 7.30992 5.5581L9.38401 3.48401C9.70655 3.16147 9.70655 2.63853 9.38401 2.31599L7.30992 0.241904ZM8.21601 19.9581C8.53855 20.2806 9.06149 20.2806 9.38402 19.9581C9.70656 19.6356 9.70656 19.1126 9.38402 18.7901L8.93937 18.3454C12.915 17.8804 16 14.5005 16 10.4C16 7.41613 14.3664 4.8138 11.9448 3.4386C11.5152 3.19463 10.9928 3.4512 10.865 3.92841C10.7625 4.31082 10.9532 4.70681 11.2925 4.91077C13.1544 6.02993 14.4 8.0695 14.4 10.4C14.4 13.798 11.7518 16.5775 8.40666 16.7873L9.38402 15.8099C9.70656 15.4874 9.70656 14.9645 9.38402 14.6419C9.06149 14.3194 8.53855 14.3194 8.21601 14.6419L6.14192 16.716C5.81938 17.0385 5.81938 17.5615 6.14192 17.884L8.21601 19.9581ZM11.5259 7.53431C11.2135 7.22189 10.7065 7.22189 10.394 7.53431L7.1997 10.7286L5.80517 9.33412C5.49276 9.02201 4.98664 9.02189 4.67431 9.33412C4.36189 9.64654 4.36189 10.1535 4.67431 10.466L6.6411 12.4318C6.95352 12.7442 7.45954 12.7442 7.77196 12.4318C7.80682 12.3969 7.83766 12.3596 7.86474 12.3204C7.88561 12.3034 7.90582 12.2852 7.92528 12.2658L11.5259 8.66517C11.8379 8.35277 11.838 7.84664 11.5259 7.53431Z" fill={color}/>
463480
</svg>
464481
);
465482
export const MenuToggleIcon = () => (

forms-flow-nav/src/sidenav/ProfileSettingsModal.jsx

Lines changed: 202 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,52 @@
11
import React, { useState, useEffect } from 'react';
22
import PropTypes from 'prop-types';
33
import 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";
56
import { fetchSelectLanguages, updateUserlang } from '../services/language';
67
import { useTranslation } from "react-i18next";
78
import i18n from '../resourceBundles/i18n';
89
import { 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

1112
export 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

Comments
 (0)