implement frontend of user edit
This commit is contained in:
1 parent
8336ea199d
commit
4f68a76674
17 files changed
+449
-60
No files matched your search
@@ -77,6 +77,7 @@ const Search = ({
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
unit = "option",
|
||||
maxWidth = "100%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -16,6 +16,7 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
marginBottom: theme.spacing(12),
|
||||
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
|
||||
}}
|
||||
|
||||
@@ -8,4 +8,10 @@ const useIsAdmin = () => {
|
||||
return isAdmin;
|
||||
};
|
||||
|
||||
export { useIsAdmin };
|
||||
const useIsSuperAdmin = () => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const isSuperAdmin = user?.role?.includes("superadmin");
|
||||
return isSuperAdmin;
|
||||
};
|
||||
|
||||
export { useIsAdmin, useIsSuperAdmin };
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const useGetUser = (userId) => {
|
||||
const [user, setUser] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchUser = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getUserById({ userId });
|
||||
setUser(response?.data?.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
fetchUser();
|
||||
}
|
||||
}, [userId, fetchUser]);
|
||||
|
||||
return [user, isLoading, error];
|
||||
};
|
||||
|
||||
export const useEditUser = (userId) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const editUser = useCallback(
|
||||
async (user) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.editUser({ userId, user });
|
||||
createToast({
|
||||
body: t("editUserPage.toast.successUserUpdate"),
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[userId]
|
||||
);
|
||||
|
||||
return [editUser, isLoading, error];
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ROLES } from "../../../../Utils/roleUtils";
|
||||
import { editUserValidation } from "../../../../Validation/validation";
|
||||
import Joi from "joi";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
export const useEditUserForm = (user) => {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [form, setForm] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
role: [],
|
||||
});
|
||||
|
||||
// Effect to set user
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setForm({
|
||||
firstName: user?.firstName,
|
||||
lastName: user?.lastName,
|
||||
email: user?.email,
|
||||
role: user?.role,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleRoleChange = (value) => {
|
||||
const hasSuperAdmin = form.role.includes(ROLES.SUPERADMIN);
|
||||
const newRoles = value.map((item) => item.role);
|
||||
if (hasSuperAdmin && !newRoles.includes(ROLES.SUPERADMIN)) {
|
||||
newRoles.push(ROLES.SUPERADMIN);
|
||||
}
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
role: newRoles,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteRole = (role) => {
|
||||
setForm({ ...form, role: form?.role?.filter((r) => r !== role) });
|
||||
};
|
||||
|
||||
const handleSearchInput = (value) => {
|
||||
setSearchInput(value);
|
||||
};
|
||||
|
||||
return [
|
||||
form,
|
||||
setForm,
|
||||
handleRoleChange,
|
||||
handleDeleteRole,
|
||||
searchInput,
|
||||
handleSearchInput,
|
||||
];
|
||||
};
|
||||
|
||||
export const useValidateEditUserForm = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
const { t } = useTranslation();
|
||||
const validateForm = (form) => {
|
||||
const { error } = editUserValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
const errs = error?.details;
|
||||
const errsObj = errs?.reduce((acc, curr) => {
|
||||
acc[curr.path[0]] = curr.message;
|
||||
return acc;
|
||||
}, {});
|
||||
setErrors(errsObj);
|
||||
|
||||
if (errs?.length > 0) {
|
||||
createToast({
|
||||
variant: "warning",
|
||||
hasIcon: true,
|
||||
title: t("editUserPage.toast.validationErrors"),
|
||||
body: errs.map((err) => t(err.message)),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateField = (name, value) => {
|
||||
const fieldSchema = Joi.object({
|
||||
[name]: editUserValidation.extract(name),
|
||||
});
|
||||
|
||||
const { error } = fieldSchema.validate({ [name]: value });
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return [errors, validateForm, validateField];
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Search from "../../../Components/Inputs/Search";
|
||||
import Button from "@mui/material/Button";
|
||||
import RoleTable from "../components/RoleTable";
|
||||
|
||||
// Utils
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetUser, useEditUser } from "../../../Hooks/userHooks";
|
||||
import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils";
|
||||
import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser";
|
||||
|
||||
const EditUser = () => {
|
||||
const { userId } = useParams();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const BREADCRUMBS = [
|
||||
{ name: t("menu.team"), path: "/account/team" },
|
||||
{ name: t("editUserPage.title"), path: "" },
|
||||
];
|
||||
|
||||
const [user, isLoading, error] = useGetUser(userId);
|
||||
const [editUser, isSaving, saveError] = useEditUser(userId);
|
||||
const [
|
||||
form,
|
||||
setForm,
|
||||
handleRoleChange,
|
||||
handleDeleteRole,
|
||||
searchInput,
|
||||
handleSearchInput,
|
||||
] = useEditUserForm(user);
|
||||
const [errors, validateForm, validateField] = useValidateEditUserForm();
|
||||
|
||||
const onChange = (e) => {
|
||||
const name = e.target.name;
|
||||
const value = e.target.value;
|
||||
validateField(name, value);
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const valid = validateForm(form);
|
||||
if (valid) {
|
||||
editUser(form);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Typography variant="h2">{t("editUserPage.title")}</Typography>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
gap={theme.spacing(12)}
|
||||
maxWidth="50%"
|
||||
>
|
||||
<TextInput
|
||||
name="firstName"
|
||||
label={t("editUserPage.form.firstName")}
|
||||
value={form?.firstName}
|
||||
onChange={onChange}
|
||||
error={errors?.firstName ? true : false}
|
||||
helperText={t(errors?.firstName)}
|
||||
/>
|
||||
<TextInput
|
||||
name="lastName"
|
||||
label={t("editUserPage.form.lastName")}
|
||||
value={form?.lastName}
|
||||
onChange={onChange}
|
||||
error={errors?.lastName ? true : false}
|
||||
helperText={t(errors?.lastName)}
|
||||
/>
|
||||
<TextInput
|
||||
name="email"
|
||||
label={t("editUserPage.form.email")}
|
||||
value={form?.email}
|
||||
disabled={true}
|
||||
error={errors?.email ? true : false}
|
||||
helperText={t(errors?.email)}
|
||||
/>
|
||||
<Search
|
||||
label={t("editUserPage.form.role")}
|
||||
filteredBy="role"
|
||||
inputValue={searchInput}
|
||||
handleInputChange={handleSearchInput}
|
||||
value={
|
||||
form?.role
|
||||
?.filter((role) => role !== ROLES.SUPERADMIN)
|
||||
.map((role) => ({ role, _id: role })) || []
|
||||
}
|
||||
options={EDITABLE_ROLES}
|
||||
multiple={true}
|
||||
handleChange={handleRoleChange}
|
||||
/>
|
||||
<RoleTable
|
||||
roles={form?.role}
|
||||
handleDeleteRole={handleDeleteRole}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isLoading || isSaving}
|
||||
>
|
||||
{t("editUserPage.form.save")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUser;
|
||||
+3
-3
@@ -2,10 +2,10 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
import Alert from "../../../Components/Alert";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
+5
-6
@@ -2,16 +2,16 @@ import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
|
||||
import Avatar from "../../Avatar";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import ImageUpload from "../../Inputs/ImageUpload";
|
||||
import Avatar from "../../../Components/Avatar";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import ImageUpload from "../../../Components/Inputs/ImageUpload";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
import Dialog from "../../Dialog";
|
||||
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
@@ -191,7 +191,6 @@ const ProfilePanel = () => {
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
className="edit-profile-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={SPACING_GAP}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DataTable from "../../../../Components/Table";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ROLES } from "../../../../Utils/roleUtils";
|
||||
|
||||
const RoleTable = ({ roles, handleDeleteRole }) => {
|
||||
const { t } = useTranslation();
|
||||
const HEADERS = [
|
||||
{
|
||||
id: "name",
|
||||
content: <Typography>{t("editUserPage.table.roleHeader")}</Typography>,
|
||||
render: (row) => {
|
||||
return row;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
content: <Typography>{t("editUserPage.table.actionHeader")}</Typography>,
|
||||
render: (row) => {
|
||||
if (row === ROLES.SUPERADMIN) return null;
|
||||
return (
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
handleDeleteRole(row);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<DataTable
|
||||
headers={HEADERS}
|
||||
data={roles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleTable;
|
||||
+19
-7
@@ -3,14 +3,16 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Select from "../../Inputs/Select";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
import DataTable from "../../Table/";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
|
||||
import DataTable from "../../../Components/Table";
|
||||
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
|
||||
/**
|
||||
* TeamPanel component manages the organization and team members,
|
||||
* providing functionalities like renaming the organization, managing team members,
|
||||
@@ -34,8 +36,9 @@ const TeamPanel = () => {
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken();
|
||||
const isSuperAdmin = useIsSuperAdmin();
|
||||
|
||||
const headers = [
|
||||
{
|
||||
@@ -184,7 +187,6 @@ const TeamPanel = () => {
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
className="team-panel table-container"
|
||||
value="team"
|
||||
sx={{
|
||||
"& h1": {
|
||||
@@ -249,7 +251,17 @@ const TeamPanel = () => {
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{ emptyView: t("teamPanel.noMembers") }}
|
||||
config={{
|
||||
emptyView: t("teamPanel.noMembers"),
|
||||
rowSX: {
|
||||
cursor: isSuperAdmin ? "pointer" : "default",
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
if (isSuperAdmin) {
|
||||
navigate(`/account/team/${row.id}`);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.account h1.MuiTypography-root,
|
||||
.account p.MuiTypography-root,
|
||||
.account input,
|
||||
.account button,
|
||||
.account td,
|
||||
.account .MuiSelect-select {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.account h1.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.account .MuiTabPanel-root {
|
||||
padding: 0;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.account button:not(.MuiIconButton-root) {
|
||||
min-height: 34px;
|
||||
}
|
||||
.account .field {
|
||||
flex: 1;
|
||||
}
|
||||
#modal-delete-account,
|
||||
#modal-edit-org-name,
|
||||
#modal-invite-member {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
|
||||
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
}
|
||||
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.account .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -5,11 +5,10 @@ import { useSelector } from "react-redux";
|
||||
import { Box, Tab, useTheme } from "@mui/material";
|
||||
import CustomTabList from "../../Components/Tab";
|
||||
import TabContext from "@mui/lab/TabContext";
|
||||
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
|
||||
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
|
||||
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
|
||||
import ProfilePanel from "./components/ProfilePanel";
|
||||
import PasswordPanel from "./components/PasswordPanel";
|
||||
import TeamPanel from "./components/TeamPanel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
|
||||
@@ -33,7 +32,9 @@ const Account = ({ open = "profile" }) => {
|
||||
{ name: t("menu.password"), value: "password" },
|
||||
{ name: t("menu.team"), value: "team" },
|
||||
];
|
||||
|
||||
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
|
||||
|
||||
if (hideTeams) {
|
||||
tabList = [
|
||||
{ name: t("menu.profile"), value: "profile" },
|
||||
@@ -63,7 +64,6 @@ const Account = ({ open = "profile" }) => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="account"
|
||||
px={theme.spacing(20)}
|
||||
py={theme.spacing(12)}
|
||||
>
|
||||
|
||||
@@ -42,6 +42,7 @@ import CreateNotifications from "../Pages/Notifications/create";
|
||||
|
||||
// Settings
|
||||
import Account from "../Pages/Account";
|
||||
import EditUser from "../Pages/Account/EditUser";
|
||||
import Settings from "../Pages/Settings";
|
||||
|
||||
import Maintenance from "../Pages/Maintenance";
|
||||
@@ -187,6 +188,14 @@ const Routes = () => {
|
||||
path="account/team"
|
||||
element={<Account open={"team"} />}
|
||||
/>
|
||||
<Route
|
||||
path="account/team/:userId"
|
||||
element={
|
||||
<RoleProtectedRoute roles={["superadmin"]}>
|
||||
<EditUser />
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="logs"
|
||||
|
||||
@@ -1099,6 +1099,24 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getUserById(config) {
|
||||
const userId = config?.userId;
|
||||
return this.axiosInstance.get(`auth/users/${userId}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async editUser(config) {
|
||||
const { userId, user } = config;
|
||||
return this.axiosInstance.put(`auth/users/${userId}`, user, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const ROLES = {
|
||||
SUPERADMIN: "superadmin",
|
||||
ADMIN: "admin",
|
||||
USER: "user",
|
||||
DEMO: "demo",
|
||||
};
|
||||
|
||||
export const EDITABLE_ROLES = [
|
||||
{ role: ROLES.ADMIN, _id: ROLES.ADMIN },
|
||||
{ role: ROLES.USER, _id: ROLES.USER },
|
||||
];
|
||||
@@ -1,5 +1,6 @@
|
||||
import joi from "joi";
|
||||
import dayjs from "dayjs";
|
||||
import { ROLES } from "../Utils/roleUtils";
|
||||
|
||||
const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number.";
|
||||
|
||||
@@ -429,6 +430,24 @@ const notificationValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const editUserValidation = joi.object({
|
||||
firstName: nameSchema,
|
||||
lastName: lastnameSchema,
|
||||
role: joi
|
||||
.array()
|
||||
.items(joi.string().valid(...Object.values(ROLES)))
|
||||
.min(1)
|
||||
.messages({
|
||||
"array.min": "auth.common.fields.role.errors.min",
|
||||
}),
|
||||
email: joi
|
||||
.string()
|
||||
.required()
|
||||
.trim()
|
||||
.email({ tlds: { allow: false } })
|
||||
.lowercase(),
|
||||
});
|
||||
|
||||
export {
|
||||
newOrChangedCredentials,
|
||||
loginCredentials,
|
||||
@@ -441,4 +460,5 @@ export {
|
||||
statusPageValidation,
|
||||
logoImageValidation,
|
||||
notificationValidation,
|
||||
editUserValidation,
|
||||
};
|
||||
@@ -32,6 +32,11 @@
|
||||
"errors": {
|
||||
"incorrect": "The password you provided does not match our records"
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"errors": {
|
||||
"min": "At least one role is required"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
@@ -445,6 +450,7 @@
|
||||
"invalidFileSize": "File size is too large!",
|
||||
"inviteNoTokenFound": "No invite token found",
|
||||
"loginHere": "Login here",
|
||||
|
||||
"logsPage": {
|
||||
"description": "System logs - last 1000 lines",
|
||||
"logLevelSelect": {
|
||||
@@ -500,6 +506,24 @@
|
||||
"mem": "Mem",
|
||||
"memory": "Memory",
|
||||
"memoryUsage": "Memory usage",
|
||||
"editUserPage": {
|
||||
"title": "Edit user",
|
||||
"form": {
|
||||
"firstName": "First name",
|
||||
"lastName": "Last name",
|
||||
"email": "Email",
|
||||
"role": "Roles",
|
||||
"save": "Save"
|
||||
},
|
||||
"table": {
|
||||
"roleHeader": "Role",
|
||||
"actionHeader": "Action"
|
||||
},
|
||||
"toast": {
|
||||
"validationErrors": "Validation errors",
|
||||
"successUserUpdate": "User updated successfully"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"changelog": "Changelog",
|
||||
"discussions": "Discussions",
|
||||
|
||||
Reference in new issue
Block a user