Skip to content

Commit ccc1927

Browse files
Merge pull request #1105 from AOT-Technologies/feature/FWF-5979-User-details-update-API-integration
FWF-5979:[Feature] - User Details updation API integration
2 parents 5b9cb30 + 45340f1 commit ccc1927

File tree

4 files changed

+185
-52
lines changed

4 files changed

+185
-52
lines changed

forms-flow-nav/src/endpoints/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ const API = {
55
RESET_PASSWORD: (userId) => `${WEB_BASE_URL}/user/${userId}/reset-password`,
66
GET_TENANT_DATA: `${MT_ADMIN_BASE_URL}/${MT_ADMIN_BASE_URL_VERSION}/tenant`,
77
INTEGRATION_ENABLE_DETAILS: `${WEB_BASE_URL}/integrations/embed/display`,
8-
GET_PERMISSIONS: `${WEB_BASE_URL}/roles/permissions`
8+
USER_PROFILE_UPDATE: `${WEB_BASE_URL}/user/<user_id>/profile`,
9+
GET_PERMISSIONS: `${WEB_BASE_URL}/roles/permissions`,
910
USER_LOGIN_DETAILS: (userId) => `${WEB_BASE_URL}/user/${userId}/login-details`,
1011
}
1112

forms-flow-nav/src/services/user/index.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,21 @@ export const requestResetPassword = () => {
5555
);
5656
};
5757

58-
58+
/**
59+
* @param {string} userId
60+
* @param {Object} profileData
61+
* @param {string} [profileData.firstName]
62+
* @param {string} [profileData.lastName]
63+
* @param {string} [profileData.username]
64+
* @param {string} [profileData.email]
65+
* @param {Object} [profileData.attributes]
66+
* @returns {Promise}
67+
*/
68+
export const updateUserProfile = (userId, profileData) => {
69+
const url = API.USER_PROFILE_UPDATE.replace("<user_id>", userId);
70+
return RequestService.httpPUTRequest(
71+
url,
72+
profileData,
73+
StorageService.get(StorageService.User.AUTH_TOKEN)
74+
);
75+
};

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

Lines changed: 158 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import Modal from 'react-bootstrap/Modal';
44
import { Tabs, Tab } from 'react-bootstrap';
55
import { CloseIcon, V8CustomButton, CustomInfo, SelectDropdown, CustomTextInput, ApplicationLogo, PromptModal } from "@formsflow/components";
66
import { fetchSelectLanguages } from '../services/language';
7-
import { requestResetPassword } from "../services/user";
7+
import { updateUserProfile, requestResetPassword } from '../services/user';
88
import { useTranslation } from "react-i18next";
99
import i18n from '../resourceBundles/i18n';
1010
import { StorageService } from "@formsflow/service";
11-
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';
1212
import { fetchPermissions } from '../services/permissions';
1313
import { getUserPermissionsByCategory } from '../helper/helper';
1414

@@ -17,7 +17,7 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
1717
const prevSelectedLang = localStorage.getItem('i18nextLng');
1818
const [selectedLang, setSelectedLang] = useState(prevSelectedLang || LANGUAGE );
1919
const [activeTab, setActiveTab] = useState("Profile");
20-
const isSSO = false;
20+
const [isSSO, setIsSSO] = useState(false);
2121
const [showUnsavedChangesPrompt, setShowUnsavedChangesPrompt] = useState(false);
2222
const [profileFields, setProfileFields] = useState({
2323
firstName: "",
@@ -33,10 +33,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
3333
const [resetPasswordState, setResetPasswordState] = useState("default"); // default | success | error
3434
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
3535
const [lastResetPasswordError, setLastResetPasswordError] = useState(null);
36+
const [userId, setUserId] = useState("");
37+
const [isLoading, setIsLoading] = useState(false);
38+
const [error, setError] = useState(null);
3639
const { t } = useTranslation();
3740

3841
useEffect(() => {
3942
if (!show) return;
43+
setError(null);
4044
try {
4145
// Reset language selection to current app language when modal opens
4246
const currentLang = localStorage.getItem("i18nextLng") || LANGUAGE;
@@ -48,6 +52,26 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
4852
const [firstFromName = "", ...rest] = String(fullName).trim().split(/\s+/);
4953
const lastFromName = rest.join(" ");
5054

55+
// Check if user is logged in via federated identity provider from USER_LOGIN_DETAILS
56+
// loginType can be "internal" (Keycloak) or "external" (external IDP like Google/Microsoft)
57+
// If loginType is "external", disable profile editing fields
58+
let federatedLogin = false;
59+
try {
60+
const userLoginDetailsStr = localStorage.getItem("USER_LOGIN_DETAILS");
61+
if (userLoginDetailsStr) {
62+
const userLoginDetails = JSON.parse(userLoginDetailsStr);
63+
// Check loginType: "external" means SSO/external IDP, "internal" means Keycloak
64+
if (userLoginDetails?.loginType !== undefined) {
65+
const loginType = String(userLoginDetails.loginType).trim().toLowerCase();
66+
federatedLogin = loginType === "external";
67+
}
68+
}
69+
} catch (e) {
70+
// Fallback to checking userDetail if USER_LOGIN_DETAILS is not available
71+
federatedLogin = !!(userDetail?.identityProvider || userDetail?.identity_provider);
72+
}
73+
setIsSSO(federatedLogin);
74+
5175
const nextFields = {
5276
firstName: userDetail?.given_name || firstFromName || "",
5377
lastName: userDetail?.family_name || lastFromName || "",
@@ -61,11 +85,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
6185
setResetPasswordState("default");
6286
setResetPasswordLoading(false);
6387
setLastResetPasswordError(null);
88+
setUserId(userDetail?.sub || userDetail?.id || "");
6489
} catch (e) {
6590
setProfileFields({ firstName: "", lastName: "", email: "", username: "" });
6691
setInitialProfileFields({ firstName: "", lastName: "", email: "", username: "" });
6792
setSelectedLang(prevSelectedLang || LANGUAGE);
6893
setInitialSelectedLang(prevSelectedLang || LANGUAGE);
94+
setUserId("");
95+
setIsSSO(false);
6996
}
7097
}, [show]);
7198

@@ -78,29 +105,6 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
78105
const supportedLanguages = languages.filter(item => userLanguagesArray.includes(item.name));
79106
setSelectLanguages(supportedLanguages.length > 0 ? supportedLanguages : languages);
80107
});
81-
82-
// Calculate remaining days from expiry_dt
83-
try {
84-
const tenantDataStr = StorageService.get("tenantData");
85-
const expiry_dt = tenantDataStr
86-
? JSON.parse(tenantDataStr)?.expiry_dt
87-
: tenant?.tenantData?.expiry_dt;
88-
89-
if (expiry_dt && !Number.isNaN(Date.parse(expiry_dt))) {
90-
const expiry = new Date(expiry_dt);
91-
const currentDate = new Date();
92-
currentDate.setHours(0, 0, 0, 0);
93-
expiry.setHours(0, 0, 0, 0);
94-
const timeDifference = expiry.getTime() - currentDate.getTime();
95-
const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
96-
setDaysDifference(days);
97-
} else {
98-
setDaysDifference(null);
99-
}
100-
} catch (error) {
101-
console.error("Error calculating days difference:", error);
102-
setDaysDifference(null);
103-
}
104108
}, []);
105109

106110
// Fetch user permissions when modal opens
@@ -177,37 +181,128 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
177181
StorageService.save("PROFILE_RESET_PASSWORD_LAST_ERROR", JSON.stringify(details));
178182
} catch (_) {
179183
}
180-
184+
181185
console.error("Reset password failed:", details);
182186
} finally {
183187
setResetPasswordLoading(false);
184188
}
185189
};
186190

187-
const handleConfirmProfile = () => {
188-
// Keep a copy for later integration; for now just save it locally and close the modal.
189-
const firstName = (profileFields.firstName || "").trim();
190-
const lastName = (profileFields.lastName || "").trim();
191-
192-
const userCopy = {
193-
user: {
194-
firstName,
195-
lastName,
196-
userName: (profileFields.username || "").trim(),
197-
email: (profileFields.email || "").trim(),
198-
attributes: {
199-
locale: [selectedLang],
200-
},
201-
},
191+
// Helper: Build profile payload with only changed fields
192+
const buildProfilePayload = (currentFields, initialFields, currentLang, prevLang) => {
193+
const payload = {};
194+
const trimmedFields = {
195+
firstName: (currentFields.firstName || "").trim(),
196+
lastName: (currentFields.lastName || "").trim(),
197+
username: (currentFields.username || "").trim(),
198+
email: (currentFields.email || "").trim(),
202199
};
203200

204-
try {
205-
StorageService.save("PROFILE_SETTINGS_USER_COPY", JSON.stringify(userCopy));
206-
} catch (e) {
207-
// ignore
201+
if (initialFields) {
202+
const fieldKeys = ["firstName", "lastName", "username", "email"];
203+
fieldKeys.forEach((key) => {
204+
if (trimmedFields[key] !== initialFields[key]) {
205+
payload[key] = trimmedFields[key];
206+
}
207+
});
208208
}
209209

210-
onClose();
210+
// If language changed, add locale and ensure username is included
211+
if (currentLang !== prevLang) {
212+
payload.attributes = { locale: [currentLang] };
213+
payload.username = payload.username || trimmedFields.username;
214+
}
215+
216+
return payload;
217+
};
218+
219+
// Helper: Build updated user detail object from API response
220+
const buildUpdatedUserDetail = (userDetail, responseData, profileData, newLang, hasLangChange) => {
221+
const updated = {
222+
...userDetail,
223+
// Use response data if available
224+
...(responseData.firstName && { given_name: responseData.firstName }),
225+
...(responseData.lastName && { family_name: responseData.lastName }),
226+
...(responseData.email && { email: responseData.email }),
227+
...(responseData.username && { preferred_username: responseData.username }),
228+
// Fallback to profileData if response doesn't include the fields
229+
...(!responseData.firstName && profileData.firstName && { given_name: profileData.firstName }),
230+
...(!responseData.lastName && profileData.lastName && { family_name: profileData.lastName }),
231+
...(!responseData.email && profileData.email && { email: profileData.email }),
232+
...(!responseData.username && profileData.username && { preferred_username: profileData.username }),
233+
};
234+
235+
// Update the name field if first or last name changed
236+
if (profileData.firstName || profileData.lastName) {
237+
const newFirst = responseData.firstName || profileData.firstName || userDetail.given_name || "";
238+
const newLast = responseData.lastName || profileData.lastName || userDetail.family_name || "";
239+
updated.name = `${newFirst} ${newLast}`.trim();
240+
}
241+
242+
// Update locale if language changed
243+
if (hasLangChange) {
244+
updated.locale = newLang;
245+
}
246+
247+
return updated;
248+
};
249+
250+
// Helper: Apply language change to i18n and localStorage
251+
const applyLanguageChange = (newLang) => {
252+
i18n.changeLanguage(newLang);
253+
localStorage.setItem("i18nextLng", newLang);
254+
};
255+
256+
const handleConfirmProfile = async () => {
257+
// Prevent profile updates for SSO/federated users
258+
if (isSSO) {
259+
setError(t("Profile editing is disabled for federated login users."));
260+
return;
261+
}
262+
263+
if (!userId) {
264+
setError(t("User ID not found. Please try again."));
265+
return;
266+
}
267+
268+
const hasLanguageChange = selectedLang !== prevSelectedLang;
269+
const profileData = buildProfilePayload(profileFields, initialProfileFields, selectedLang, prevSelectedLang);
270+
271+
if (Object.keys(profileData).length === 0) {
272+
onClose();
273+
return;
274+
}
275+
276+
setIsLoading(true);
277+
setError(null);
278+
279+
try {
280+
const response = await updateUserProfile(userId, profileData);
281+
const responseData = response?.data || {};
282+
const userDetail = JSON.parse(StorageService.get(StorageService.User.USER_DETAILS)) || {};
283+
284+
const updatedUserDetail = buildUpdatedUserDetail(
285+
userDetail, responseData, profileData, selectedLang, hasLanguageChange
286+
);
287+
288+
StorageService.save(StorageService.User.USER_DETAILS, JSON.stringify(updatedUserDetail));
289+
290+
if (hasLanguageChange) {
291+
applyLanguageChange(selectedLang);
292+
}
293+
294+
if (publish) {
295+
publish("profileUpdated", { ...responseData, userId });
296+
}
297+
298+
onClose();
299+
} catch (err) {
300+
console.error("Error updating profile:", err);
301+
const errorMessage = err?.response?.data?.message || err?.message || t("Failed to update profile. Please try again.");
302+
setError(errorMessage);
303+
} finally {
304+
setIsLoading(false);
305+
}
211306
};
212307

213308

@@ -321,6 +416,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
321416
<Modal.Body>
322417
{activeTab === "Profile" ? (
323418
<>
419+
{isSSO && (
420+
<CustomInfo
421+
className="mb-3"
422+
variant="secondary"
423+
icon={<ApplicationLogo width="1.1875rem" height="1.4993rem" />}
424+
content={t("Your profile is managed by your identity provider. Profile editing is disabled for federated login users.")}
425+
/>
426+
)}
324427
<div className="profile-settings-details-box p-3 mb-3 border rounded">
325428
<div className="row g-3">
326429
<div className="col-12 col-md-6">
@@ -434,6 +537,7 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
434537
variant="primary"
435538
className="mb-3"
436539
onChange={handleLanguageChange}
540+
disabled={isSSO}
437541
/>
438542
</>
439543
) : (
@@ -472,13 +576,18 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
472576
</Modal.Body>
473577

474578
<Modal.Footer>
579+
{error && (
580+
<div className="profile-error-message text-danger mb-2 w-100">
581+
{error}
582+
</div>
583+
)}
475584
<div className="buttons-row d-flex justify-content-end">
476585
<V8CustomButton
477-
label={t("Update")}
586+
label={isLoading ? t("Updating...") : t("Update")}
478587
onClick={handleConfirmProfile}
479588
dataTestId="save-profile-settings"
480589
ariaLabel={t("Save Profile Settings")}
481-
disabled={activeTab !== "Profile" || !isAnythingChanged || emailIsInvalid || usernameIsInvalid}
590+
disabled={activeTab !== "Profile" || !isAnythingChanged || emailIsInvalid || usernameIsInvalid || isLoading || isSSO}
482591
variant="primary"
483592
/>
484593
</div>

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ const Sidebar = React.memo(({ props, sidenavHeight="100%" }) => {
243243
setForm(data);
244244
}
245245
});
246+
247+
// Subscribe to profile updates to refresh user details in navbar
248+
props.subscribe("profileUpdated", () => {
249+
const updatedUserDetail = JSON.parse(StorageService.get(StorageService.User.USER_DETAILS)) || {};
250+
setUserDetail(updatedUserDetail);
251+
});
246252
}, []);
247253

248254
// On successful authentication, load federated login details and integration config
@@ -382,7 +388,7 @@ const Sidebar = React.memo(({ props, sidenavHeight="100%" }) => {
382388
options.push({
383389
name: "Submissions",
384390
path: SUBMISSION_ROUTE,
385-
});
391+
});
386392
}
387393

388394
return options;

0 commit comments

Comments
 (0)