Skip to content

Commit

Permalink
feat(oh2-298): admin edit user
Browse files Browse the repository at this point in the history
  • Loading branch information
gasp committed Aug 29, 2024
1 parent 5a78169 commit fc5d474
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 3 deletions.
8 changes: 8 additions & 0 deletions src/components/accessories/admin/users/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ import UsersTable from "./usersTable";

import { PATHS } from "../../../../consts";

import { UserDTO } from "../../../../generated";

export const Users = () => {
const navigate = useNavigate();
const { t } = useTranslation();

const handleEditUser = (row: UserDTO) =>
navigate(PATHS.admin_users_edit.replace(":id", row.userName!), {
state: row,
});

const [tab, setTab] = useState<"users" | "groups">("users");
return (
<>
Expand All @@ -38,6 +45,7 @@ export const Users = () => {
{t("user.addUser")}
</Button>
}
onEdit={handleEditUser}
/>
) : (
<UserGroupsTable />
Expand Down
64 changes: 64 additions & 0 deletions src/components/accessories/admin/users/editUser/EditUser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { CircularProgress } from "@mui/material";
import { useAppDispatch, useAppSelector } from "libraries/hooks/redux";
import React, { useEffect, useState } from "react";
import { useParams } from "react-router";
import { Navigate } from "react-router-dom";

import { PATHS } from "../../../../../consts";
import { UserDTO } from "../../../../../generated";
import { getUserGroups } from "../../../../../state/usergroups";
import { getUsers, updateUser, updateUserReset } from "../../../../../state/users";
import { EditUserForm } from "./EditUserForm";

export const EditUser = () => {
const dispatch = useAppDispatch();
const { id } = useParams();
const [user, setUser] = useState<UserDTO | null>();
const [userNotFound, setUserNotFound] = useState<boolean>(false);
const { isLoading, hasSucceeded, hasFailed, error } = useAppSelector(
(state) => state.users.update
);
const users = useAppSelector((state) => state.users.userList);
const groups = useAppSelector((state) => state.usergroups.groupList)

useEffect(() => {
dispatch(getUsers({}))
dispatch(getUserGroups())
return () => {
dispatch(updateUserReset());
};
}, [dispatch]);

useEffect(() => {
if (users.hasSucceeded) {
const user = users.data?.find(({ userName }) => userName === id);
if (!!user) setUser(user);
else setUserNotFound(true);
}
}, [users.hasSucceeded, users.data, id]);

const handleUpdate = (user: UserDTO) => {
dispatch(updateUser(user));
};

if(userNotFound) return <Navigate to={PATHS.admin_users} />;

if (users.isLoading || groups.isLoading || !users || !user || !groups.data) {
return <CircularProgress style={{ marginLeft: "50%", position: "relative" }} />;
}
if(!user) {
console.log("user not found")
}

return (
<EditUserForm
groups={groups.data}
initialValues={user}
onSubmit={handleUpdate}
isLoading={isLoading}
hasSucceeded={hasSucceeded}
hasFailed={hasFailed}
error={error}
/>
);
};
178 changes: 178 additions & 0 deletions src/components/accessories/admin/users/editUser/EditUserForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Autocomplete } from "@mui/lab";
import {
FormControl,
FormHelperText,
TextField as MuiTextField,
} from "@mui/material";
import { useFormik } from "formik";
import React, { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";

import { UserDTO, UserGroupDTO } from "../../../../../generated";

import checkIcon from "../../../../../assets/check-icon.png";
import Button from "../../../button/Button";
import ConfirmationDialog from "../../../confirmationDialog/ConfirmationDialog";
import InfoBox from "../../../infoBox/InfoBox";
import TextField from "../../../textField/TextField";

import { PATHS } from "../../../../../consts";
import "./styles.scss";
import { userSchema } from "./validation";

interface IProps {
initialValues: UserDTO;
isLoading: boolean;
hasSucceeded: boolean;
hasFailed: boolean;
error: any;
groups: UserGroupDTO[];
onSubmit: (userValue: UserDTO) => void;
}

export const EditUserForm = ({
initialValues,
isLoading,
hasSucceeded,
hasFailed,
error,
onSubmit,
groups,
}: IProps) => {
const { t } = useTranslation();
const navigate = useNavigate();

const {
handleSubmit,
handleBlur,
getFieldProps,
isValid,
dirty,
resetForm,
errors,
touched,
values,
setFieldTouched,
setFieldValue,
} = useFormik<UserDTO>({
initialValues,
validationSchema: userSchema(t),
onSubmit,
});

return (
<div className="editUserForm">
<form className="editUserForm__form" onSubmit={handleSubmit}>
<div className="row start-sm center-xs">
<div className="editUserForm__item fullWidth">
<FormControl variant="outlined" className="autocomplete">
<Autocomplete
id="userGroupName"
options={groups}
value={values.userGroupName}
onBlur={() => setFieldTouched("userGroupName")}
onChange={(_ev: any, value: UserGroupDTO | null) => {
setFieldValue("userGroupName", value);
}}
renderInput={(params) => (
<MuiTextField
{...params}
name="userGroupName"
variant="outlined"
size="small"
error={!!(touched.userGroupName && errors.userGroupName)}
fullWidth
label={t("user.group")}
/>
)}
getOptionLabel={(option: UserGroupDTO) =>
option.code.toString() +
(option.desc ? ` - ${option.desc}` : "")
}
isOptionEqualToValue={(option, value) => option.code === value.code}
/>
{touched.userGroupName && errors.userGroupName && (
<FormHelperText error>
{
(errors.userGroupName?.code ||
errors.userGroupName) as ReactNode
}
</FormHelperText>
)}
</FormControl>
</div>

<div className="editUserForm__item fullWidth">
<TextField
field={getFieldProps("passwd")}
theme="regular"
label={t("user.password")}
isValid={!!touched.passwd && !!errors.passwd}
errorText={(touched.passwd && errors.passwd) || ""}
onBlur={handleBlur}
type="password"
// this below prevents from saving the password on the computer
InputProps={{ autoComplete: "one-time-code" }}
/>
</div>
<div className="editUserForm__item fullWidth">
<TextField
field={getFieldProps("desc")}
theme="regular"
label={t("user.description")}
isValid={!!touched.desc && !!errors.desc}
errorText={(touched.desc && errors.desc) || ""}
onBlur={handleBlur}
/>
</div>
</div>

<div className="editUserForm__item fullWidth">
{hasFailed && (
<div className="info-box-container">
<InfoBox
type="error"
message={error?.message ?? t("common.somethingwrong")}
/>
</div>
)}
</div>
<div className="editUserForm__buttonSet">
<div className="submit_button">
<Button
type="submit"
variant="contained"
disabled={!!isLoading || !isValid || !dirty}
>
{t("common.save")}
</Button>
</div>
<div className="reset_button">
<Button
type="reset"
variant="text"
disabled={!!isLoading || !dirty}
onClick={async () => {
resetForm();
}}
>
{t("common.reset")}
</Button>
</div>
</div>
</form>
<ConfirmationDialog
isOpen={hasSucceeded}
title={t("user.groupUpdated")}
icon={checkIcon}
info={t("user.groupUpdateSuccess")}
primaryButtonLabel="Ok"
handlePrimaryButtonClick={() => {
navigate(PATHS.admin_users);
}}
handleSecondaryButtonClick={() => ({})}
/>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/accessories/admin/users/editUser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EditUser } from "./EditUser";
84 changes: 84 additions & 0 deletions src/components/accessories/admin/users/editUser/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
@import "../../../../../styles/variables";
@import "../../../../../../node_modules/susy/sass/susy";

.editUserForm {
display: inline-block;
flex-direction: column;
align-items: center;
width: 100%;

.formInsertMode {
margin: 0px 0px 20px;
}

.row {
justify-content: space-between;
}

.editUserForm__item {
margin: 7px 0px;
padding: 0px 15px;
width: 50%;
@include susy-media($narrow) {
padding: 0px 10px;
}
@include susy-media($tablet_land) {
padding: 0px 10px;
}
@include susy-media($medium-up) {
width: 25%;
}
@include susy-media($tablet_port) {
width: 50%;
}
@include susy-media($smartphone) {
width: 100%;
}
.textField,
.selectField {
width: 100%;
}

&.fullWidth {
width: 100%;
}

&.halfWidth {
width: 50%;
@include susy-media($smartphone) {
width: 100%;
}
}
&.thirdWidth {
width: 33%;
@include susy-media($smartphone) {
width: 100%;
}
}
}

.editUserForm__buttonSet {
display: flex;
margin-top: 25px;
padding: 0px 15px;
flex-direction: row-reverse;
@include susy-media($smartphone_small) {
display: block;
}

.submit_button,
.reset_button {
.MuiButton-label {
font-size: smaller;
letter-spacing: 1px;
font-weight: 600;
}
button {
@include susy-media($smartphone_small) {
width: 100%;
margin-top: 10px;
}
}
}
}
}
23 changes: 23 additions & 0 deletions src/components/accessories/admin/users/editUser/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TFunction } from "react-i18next";
import { object, string } from "yup";
import { UserDTO, UserGroupDTO } from "../../../../../generated";

// min 5 characters, 1 upper case letter, 1 lower case letter, 1 numeric digit.
export const passwordRules = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{5,}$/;

export const userSchema = (t: TFunction<"translation">) =>
object().shape<Pick<UserDTO, "userGroupName" | "passwd" | "desc">>({
userGroupName: object<UserGroupDTO>({
code: string().required(t("user.validateUserNeedsGroup")),
desc: string(),
})
.nullable()
.required(t("user.validateUserNeedsGroup")),
passwd: string()
.required(t("user.validatePasswordNeeded"))
.min(5, t("user.validatePasswordTooShort"))
.matches(passwordRules, {
message: t("user.validatePasswordTooWeak"),
}),
desc: string(),
});
3 changes: 2 additions & 1 deletion src/components/accessories/admin/users/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { Users } from "./Users";
export { EditUser } from "./editUser";
export { NewUser } from "./newUser";
export { Users } from "./Users";
Loading

0 comments on commit fc5d474

Please sign in to comment.