Skip to content

Commit 047debf

Browse files
committed
Merge branch 'r/20.x' into develop
2 parents f962794 + 74285b0 commit 047debf

5 files changed

Lines changed: 198 additions & 2 deletions

File tree

src/components/Header.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { setSpecificServiceFilter } from "../slices/tableFilterSlice";
88
import { getErrorCount, getHealthStatus } from "../selectors/healthSelectors";
99
import {
1010
getOrgProperties,
11+
getUserBasicInfo,
1112
getUserInformation,
1213
} from "../selectors/userInfoSelectors";
1314
import { availableHotkeys } from "../configs/hotkeysConfig";
@@ -26,6 +27,8 @@ import { ModalHandle } from "./shared/modals/Modal";
2627
import { broadcastLogout } from "../utils/broadcastSync";
2728
import BaseButton from "./shared/BaseButton";
2829
import { LuBell, LuCheck, LuChevronDown, LuCirclePlay, LuMessageCircleQuestion, LuVideo } from "react-icons/lu";
30+
import { fetchUserDetails } from "../slices/userDetailsSlice";
31+
import ChangePasswordModal from "./shared/modals/ChangePassword";
2932

3033
// References for detecting a click outside of the container of the dropdown menus
3134
const containerLang = React.createRef<HTMLDivElement>();
@@ -46,6 +49,7 @@ const Header = () => {
4649
const [displayMenuHelp, setMenuHelp] = useState(false);
4750
const registrationModalRef = useRef<ModalHandle>(null);
4851
const hotKeyCheatSheetModalRef = useRef<ModalHandle>(null);
52+
const changePasswordModalRef = useRef<ModalHandle>(null);
4953

5054
const healthStatus = useAppSelector(state => getHealthStatus(state));
5155
const errorCounter = useAppSelector(state => getErrorCount(state));
@@ -266,7 +270,11 @@ const Header = () => {
266270
<LuChevronDown className="dropdown-icon" />
267271
</BaseButton>
268272
{/* Click on username, a dropdown menu with the option to logout opens */}
269-
{displayMenuUser && <MenuUser />}
273+
{displayMenuUser &&
274+
<MenuUser
275+
changePasswordRef={changePasswordModalRef}
276+
/>
277+
}
270278
</div>
271279
</nav>
272280
</header>
@@ -279,6 +287,9 @@ const Header = () => {
279287

280288
{/* Hotkey Cheat Sheet */}
281289
<HotKeyCheatSheet modalRef={hotKeyCheatSheetModalRef}/>
290+
291+
{/* Change Password Modal */}
292+
<ChangePasswordModal modalRef={changePasswordModalRef}/>
282293
</>
283294
);
284295
};
@@ -425,16 +436,35 @@ const MenuHelp = ({
425436
);
426437
};
427438

428-
const MenuUser = () => {
439+
const MenuUser = ({
440+
changePasswordRef,
441+
}: {
442+
changePasswordRef: React.RefObject<ModalHandle | null>
443+
}) => {
429444
const { t } = useTranslation();
445+
const dispatch = useAppDispatch();
446+
447+
const user = useAppSelector(getUserBasicInfo);
430448

431449
const logout = () => {
432450
// Here we broadcast logout, in order to redirect other tabs to login page!
433451
broadcastLogout();
434452
window.location.href = "/j_spring_security_logout";
435453
};
454+
455+
const showChangePasswordModal = async () => {
456+
await dispatch(fetchUserDetails(user.username));
457+
458+
changePasswordRef.current?.open();
459+
};
460+
436461
return (
437462
<ul className="dropdown-ul">
463+
<li>
464+
<ButtonLikeAnchor onClick={() => showChangePasswordModal()}>
465+
<span>{t("USER_MENU.CHANGE_PASSWORD")}</span>
466+
</ButtonLikeAnchor>
467+
</li>
438468
<li>
439469
<ButtonLikeAnchor onClick={() => logout()}>
440470
<span className="logout-icon">{t("LOGOUT")}</span>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { Formik } from "formik";
2+
import { useAppDispatch, useAppSelector } from "../../../store";
3+
import { getUserDetails } from "../../../selectors/userDetailsSelectors";
4+
import ModalContent from "./ModalContent";
5+
import { updateUserPassword } from "../../../slices/userDetailsSlice";
6+
import { NotificationComponent } from "../Notifications";
7+
import { Field } from "../Field";
8+
import cn from "classnames";
9+
import { useTranslation } from "react-i18next";
10+
import { PasswordSchema } from "../../../utils/validate";
11+
import { Modal, ModalHandle } from "./Modal";
12+
import WizardNavigationButtons from "../wizard/WizardNavigationButtons";
13+
14+
/**
15+
* This component allows the current user to change their password.
16+
*/
17+
const ChangePasswordModal = ({
18+
modalRef,
19+
}: {
20+
modalRef: React.RefObject<ModalHandle | null>
21+
}) => {
22+
const { t } = useTranslation();
23+
24+
const hideModal = () => {
25+
modalRef.current?.close?.();
26+
};
27+
28+
29+
return (
30+
<Modal
31+
header={t("CHANGE_PASSWORD_MODAL.TITLE")}
32+
classId="theme-details-modal"
33+
ref={modalRef}
34+
>
35+
{/* component that manages tabs of theme details modal*/}
36+
<ChangePassword
37+
close={hideModal}
38+
/>
39+
</Modal>
40+
);
41+
};
42+
43+
const ChangePassword = ({
44+
close,
45+
}: {
46+
close: () => void,
47+
}) => {
48+
const { t } = useTranslation();
49+
const dispatch = useAppDispatch();
50+
51+
const userDetails = useAppSelector(state => getUserDetails(state));
52+
53+
const initialValues = {
54+
password: "",
55+
passwordConfirmation: "",
56+
};
57+
58+
const handleSubmit = (values: {
59+
password: string,
60+
}) => {
61+
dispatch(updateUserPassword({ password: values.password }));
62+
close();
63+
};
64+
65+
return (
66+
<Formik
67+
initialValues={initialValues}
68+
validationSchema={PasswordSchema}
69+
onSubmit={values => handleSubmit(values)}
70+
>
71+
{formik => (
72+
<>
73+
<ModalContent>
74+
<div className="form-container">
75+
{!userDetails.manageable && (
76+
<NotificationComponent
77+
notification={{
78+
type: "warning",
79+
message: "NOTIFICATIONS.USER_NOT_MANAGEABLE",
80+
id: 0,
81+
}}
82+
/>
83+
)}
84+
<div className="row">
85+
<label>{t("USERS.USERS.DETAILS.FORM.PASSWORD")}</label>
86+
<Field
87+
type="password"
88+
name="password"
89+
disabled={!userDetails.manageable}
90+
className={cn({
91+
error: formik.touched.password && formik.errors.password,
92+
disabled: !userDetails.manageable,
93+
})}
94+
placeholder={t("USERS.USERS.DETAILS.FORM.PASSWORD") + "..."}
95+
/>
96+
</div>
97+
<div className="row">
98+
<label>{t("USERS.USERS.DETAILS.FORM.REPEAT_PASSWORD")}</label>
99+
<Field
100+
type="password"
101+
name="passwordConfirmation"
102+
disabled={!userDetails.manageable}
103+
className={cn({
104+
error:
105+
formik.touched.passwordConfirmation &&
106+
formik.errors.passwordConfirmation,
107+
disabled: !userDetails.manageable,
108+
})}
109+
placeholder={
110+
t("USERS.USERS.DETAILS.FORM.REPEAT_PASSWORD") + "..."
111+
}
112+
/>
113+
</div>
114+
</div>
115+
</ModalContent>
116+
<WizardNavigationButtons
117+
formik={formik}
118+
previousPage={close}
119+
createTranslationString="SUBMIT"
120+
cancelTranslationString="CANCEL"
121+
isLast
122+
/>
123+
</>
124+
)}
125+
</Formik>
126+
);
127+
};
128+
129+
export default ChangePasswordModal;

src/i18n/org/opencastproject/adminui/languages/lang-en_US.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2280,5 +2280,11 @@
22802280
"TITLE": "Terms of use",
22812281
"NOCONTENT": "Content not available",
22822282
"AGREE": "I have read and agree to the terms of use"
2283+
},
2284+
"USER_MENU": {
2285+
"CHANGE_PASSWORD": "Change password"
2286+
},
2287+
"CHANGE_PASSWORD_MODAL": {
2288+
"TITLE": "Change your password"
22832289
}
22842290
}

src/slices/userDetailsSlice.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,28 @@ export const updateUserDetails = (params: {
8181
});
8282
};
8383

84+
export const updateUserPassword = (params: {
85+
password: string,
86+
}): AppThunk => dispatch => {
87+
const { password } = params;
88+
89+
// get URL params used for put request
90+
const data = new URLSearchParams();
91+
data.append("password", password);
92+
93+
// PUT request
94+
axios
95+
.put("/admin-ng/users/self", data)
96+
.then(response => {
97+
console.info(response);
98+
dispatch(addNotification({ type: "success", key: "USER_UPDATED" }));
99+
})
100+
.catch(response => {
101+
console.error(response);
102+
dispatch(addNotification({ type: "error", key: "USER_NOT_SAVED" }));
103+
});
104+
};
105+
84106
const userDetailsSlice = createSlice({
85107
name: "userDetails",
86108
initialState,

src/utils/validate.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,15 @@ export const EditUserSchema = Yup.object().shape({
252252
}),
253253
});
254254

255+
export const PasswordSchema = Yup.object().shape({
256+
passwordConfirmation: Yup.string().when("password", {
257+
is: (value: any) => !!value,
258+
then: () => Yup.string()
259+
.oneOf([Yup.ref("password"), undefined], "Passwords must match")
260+
.required("Required"),
261+
}),
262+
});
263+
255264
// Validation Schema used in group details modal
256265
export const EditGroupSchema = Yup.object().shape({
257266
name: Yup.string().required("Required"),

0 commit comments

Comments
 (0)