Merge pull request #410 from jenny-s51/createUser
Users: Adds create user page to users section
This commit is contained in:
commit
7c816736d2
11 changed files with 456 additions and 8 deletions
50
cypress/integration/users_test.spec.ts
Normal file
50
cypress/integration/users_test.spec.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||
import LoginPage from "../support/pages/LoginPage";
|
||||
import CreateUserPage from "../support/pages/admin_console/manage/users/CreateUserPage";
|
||||
import Masthead from "../support/pages/admin_console/Masthead";
|
||||
import ListingPage from "../support/pages/admin_console/ListingPage";
|
||||
|
||||
describe("Users test", () => {
|
||||
const loginPage = new LoginPage();
|
||||
const sidebarPage = new SidebarPage();
|
||||
const createUserPage = new CreateUserPage();
|
||||
const masthead = new Masthead();
|
||||
const listingPage = new ListingPage();
|
||||
|
||||
let itemId = "user_crud";
|
||||
|
||||
describe("User creation", () => {
|
||||
beforeEach(function () {
|
||||
cy.visit("");
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToUsers();
|
||||
});
|
||||
|
||||
it("Go to create User page", () => {
|
||||
cy.wait(100);
|
||||
|
||||
createUserPage.goToCreateUser();
|
||||
cy.url().should("include", "users/add-user");
|
||||
|
||||
// Verify Cancel button works
|
||||
createUserPage.cancel();
|
||||
cy.url().should("not.include", "/add-user");
|
||||
});
|
||||
|
||||
it("Create user test", function () {
|
||||
itemId += "_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
// Create
|
||||
cy.wait(100);
|
||||
|
||||
createUserPage.goToCreateUser();
|
||||
|
||||
createUserPage.fillRealmRoleData(itemId).save();
|
||||
|
||||
masthead.checkNotificationMessage("The user has been created");
|
||||
|
||||
sidebarPage.goToUsers();
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
export default class CreateUserPage {
|
||||
usernameInput: string;
|
||||
usersEmptyState: string;
|
||||
emptyStateCreateUserBtn: string;
|
||||
searchPgCreateUserBtn: string;
|
||||
saveBtn: string;
|
||||
cancelBtn: string;
|
||||
|
||||
constructor() {
|
||||
this.usernameInput = "#kc-username";
|
||||
|
||||
this.usersEmptyState = "[data-testid=empty-state]";
|
||||
this.emptyStateCreateUserBtn = "[data-testid=empty-primary-action]";
|
||||
this.searchPgCreateUserBtn = "[data-testid=create-new-user]";
|
||||
this.saveBtn = "[data-testid=create-user]";
|
||||
this.cancelBtn = "[data-testid=cancel-create-user]";
|
||||
}
|
||||
|
||||
//#region General Settings
|
||||
fillRealmRoleData(username: string) {
|
||||
cy.get(this.usernameInput).clear();
|
||||
|
||||
if (username) {
|
||||
cy.get(this.usernameInput).type(username);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
goToCreateUser() {
|
||||
cy.wait(100);
|
||||
cy.get("body").then((body) => {
|
||||
if (body.find(this.usersEmptyState).length > 0) {
|
||||
cy.get(this.emptyStateCreateUserBtn).click();
|
||||
} else if (body.find("[data-testid=search-users-title]").length > 0) {
|
||||
cy.get(this.searchPgCreateUserBtn).click();
|
||||
} else {
|
||||
cy.get("[data-testid=add-user]").click();
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
save() {
|
||||
cy.get(this.saveBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
cy.get(this.cancelBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ export const ListEmptyState = ({
|
|||
}: ListEmptyStateProps) => {
|
||||
return (
|
||||
<>
|
||||
<EmptyState variant="large">
|
||||
<EmptyState data-testid="empty-state" variant="large">
|
||||
{hasIcon && isSearchVariant ? (
|
||||
<EmptyStateIcon icon={SearchIcon} />
|
||||
) : (
|
||||
|
|
|
@ -4,6 +4,7 @@ exports[`<ListEmptyState /> render 1`] = `
|
|||
<DocumentFragment>
|
||||
<div
|
||||
class="pf-c-empty-state pf-m-lg"
|
||||
data-testid="empty-state"
|
||||
>
|
||||
<div
|
||||
class="pf-c-empty-state__content"
|
||||
|
|
|
@ -27,6 +27,7 @@ export type ViewHeaderProps = {
|
|||
badge?: string;
|
||||
badgeId?: string;
|
||||
badgeIsRead?: boolean;
|
||||
dividerComponent?: "div" | "hr" | "li" | undefined;
|
||||
subKey: string;
|
||||
actionsDropdownId?: string;
|
||||
subKeyLinkProps?: FormattedLinkProps;
|
||||
|
@ -42,6 +43,7 @@ export const ViewHeader = ({
|
|||
titleKey,
|
||||
badge,
|
||||
badgeIsRead,
|
||||
dividerComponent,
|
||||
subKey,
|
||||
subKeyLinkProps,
|
||||
dropdownItems,
|
||||
|
@ -159,7 +161,7 @@ export const ViewHeader = ({
|
|||
/>
|
||||
)}
|
||||
</PageSection>
|
||||
<Divider />
|
||||
<Divider component={dividerComponent} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ import { UserFederationSection } from "./user-federation/UserFederationSection";
|
|||
import { UsersSection } from "./user/UsersSection";
|
||||
import { MappingDetails } from "./client-scopes/details/MappingDetails";
|
||||
import { ClientDetails } from "./clients/ClientDetails";
|
||||
import { UsersTabs } from "./user/UsersTabs";
|
||||
import { UserFederationKerberosSettings } from "./user-federation/UserFederationKerberosSettings";
|
||||
import { UserFederationLdapSettings } from "./user-federation/UserFederationLdapSettings";
|
||||
import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm";
|
||||
|
@ -143,6 +144,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
breadcrumb: t("users:title"),
|
||||
access: "query-users",
|
||||
},
|
||||
{
|
||||
path: "/:realm/users/add-user",
|
||||
component: UsersTabs,
|
||||
breadcrumb: t("users:createUser"),
|
||||
access: "manage-users",
|
||||
},
|
||||
{
|
||||
path: "/:realm/sessions",
|
||||
component: SessionsSection,
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
import { SearchIcon } from "@patternfly/react-icons";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
|
||||
type SearchUserProps = {
|
||||
onSearch: (search: string) => void;
|
||||
|
@ -20,9 +21,14 @@ type SearchUserProps = {
|
|||
export const SearchUser = ({ onSearch }: SearchUserProps) => {
|
||||
const { t } = useTranslation("users");
|
||||
const { register, handleSubmit } = useForm<{ search: string }>();
|
||||
const { url } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
|
||||
const goToCreate = () => history.push(`${url}/add-user`);
|
||||
|
||||
return (
|
||||
<EmptyState>
|
||||
<Title headingLevel="h4" size="lg">
|
||||
<Title data-testid="search-users-title" headingLevel="h4" size="lg">
|
||||
{t("startBySearchingAUser")}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
|
@ -44,7 +50,7 @@ export const SearchUser = ({ onSearch }: SearchUserProps) => {
|
|||
</InputGroup>
|
||||
</Form>
|
||||
</EmptyStateBody>
|
||||
<Button variant="link" onClick={() => {}}>
|
||||
<Button data-testid="create-new-user" variant="link" onClick={goToCreate}>
|
||||
{t("createNewUser")}
|
||||
</Button>
|
||||
</EmptyState>
|
||||
|
|
247
src/user/UserForm.tsx
Normal file
247
src/user/UserForm.tsx
Normal file
|
@ -0,0 +1,247 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, UseFormMethods } from "react-hook-form";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
|
||||
export type UserFormProps = {
|
||||
form: UseFormMethods<UserRepresentation>;
|
||||
save: (user: UserRepresentation) => void;
|
||||
};
|
||||
|
||||
export const UserForm = ({
|
||||
form: { handleSubmit, register, errors, watch, control },
|
||||
save,
|
||||
}: UserFormProps) => {
|
||||
const { t } = useTranslation("users");
|
||||
const { realm } = useRealm();
|
||||
|
||||
const [
|
||||
isRequiredUserActionsDropdownOpen,
|
||||
setRequiredUserActionsDropdownOpen,
|
||||
] = useState(false);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
|
||||
const watchUsernameInput = watch("username");
|
||||
|
||||
const emailRegexPattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
const requiredUserActionsOptions = [
|
||||
<SelectOption key={0} value="Configure OTP">
|
||||
{t("configureOTP")}
|
||||
</SelectOption>,
|
||||
<SelectOption key={1} value="Update Password">
|
||||
{t("updatePassword")}
|
||||
</SelectOption>,
|
||||
<SelectOption key={2} value="Update Profile">
|
||||
{t("updateProfile")}
|
||||
</SelectOption>,
|
||||
<SelectOption key={3} value="Verify Email">
|
||||
{t("verifyEmail")}
|
||||
</SelectOption>,
|
||||
<SelectOption key={4} value="Update User Locale">
|
||||
{t("updateUserLocale")}
|
||||
</SelectOption>,
|
||||
];
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelected([]);
|
||||
setRequiredUserActionsDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
role="manage-users"
|
||||
className="pf-u-mt-lg"
|
||||
>
|
||||
<FormGroup
|
||||
label={t("username")}
|
||||
fieldId="kc-username"
|
||||
isRequired
|
||||
validated={errors.username ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
id="kc-username"
|
||||
name="username"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("email")}
|
||||
fieldId="kc-description"
|
||||
validated={errors.email ? "error" : "default"}
|
||||
helperTextInvalid={t("users:emailInvalid")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({
|
||||
pattern: emailRegexPattern,
|
||||
})}
|
||||
type="email"
|
||||
id="kc-email"
|
||||
name="email"
|
||||
aria-label={t("emailInput")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("emailVerified")}
|
||||
fieldId="kc-email-verified"
|
||||
helperTextInvalid={t("common:required")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("emailVerifiedHelpText")}
|
||||
forLabel={t("emailVerified")}
|
||||
forID="email-verified"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="user-email-verified"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id={"kc-user-email-verified"}
|
||||
isDisabled={false}
|
||||
onChange={(value) => onChange([`${value}`])}
|
||||
isChecked={value[0] === "true"}
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
/>
|
||||
)}
|
||||
></Controller>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("firstName")}
|
||||
fieldId="kc-firstname"
|
||||
validated={errors.firstName ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-firstname"
|
||||
name="firstName"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("lastName")}
|
||||
fieldId="kc-name"
|
||||
validated={errors.lastName ? "error" : "default"}
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-lastname"
|
||||
name="lastName"
|
||||
aria-label={t("lastName")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("common:enabled")}
|
||||
fieldId="kc-enabled"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("disabledHelpText")}
|
||||
forLabel={t("enabled")}
|
||||
forID="enabled-label"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="user-enabled"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id={"kc-user-enabled"}
|
||||
isDisabled={false}
|
||||
onChange={(value) => onChange([`${value}`])}
|
||||
isChecked={value[0] === "true"}
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
/>
|
||||
)}
|
||||
></Controller>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("requiredUserActions")}
|
||||
fieldId="kc-required-user-actions"
|
||||
validated={errors.requiredActions ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("requiredUserActionsHelpText")}
|
||||
forLabel={t("requiredUserActions")}
|
||||
forID="required-user-actions-label"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="required-user-actions"
|
||||
defaultValue={["0"]}
|
||||
typeAheadAriaLabel="Select an action"
|
||||
control={control}
|
||||
render={() => (
|
||||
<Select
|
||||
placeholderText="Select action"
|
||||
toggleId="kc-required-user-actions"
|
||||
onToggle={() =>
|
||||
setRequiredUserActionsDropdownOpen(
|
||||
!isRequiredUserActionsDropdownOpen
|
||||
)
|
||||
}
|
||||
isOpen={isRequiredUserActionsDropdownOpen}
|
||||
selections={selected}
|
||||
onSelect={(_, value) => {
|
||||
const option = value as string;
|
||||
if (selected.includes(option)) {
|
||||
setSelected(selected.filter((item) => item !== option));
|
||||
} else {
|
||||
setSelected([...selected, option]);
|
||||
}
|
||||
}}
|
||||
onClear={clearSelection}
|
||||
variant="typeaheadmulti"
|
||||
>
|
||||
{requiredUserActionsOptions}
|
||||
</Select>
|
||||
)}
|
||||
></Controller>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="create-user"
|
||||
isDisabled={!watchUsernameInput}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
{t("common:Create")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="cancel-create-user"
|
||||
onClick={() => history.push(`/${realm}/users`)}
|
||||
variant="link"
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
|
@ -28,6 +28,7 @@ import { emptyFormatter } from "../util";
|
|||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
|
||||
import "./user-section.css";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
|
||||
type BruteUser = UserRepresentation & {
|
||||
brute?: Record<string, object>;
|
||||
|
@ -39,6 +40,8 @@ export const UsersSection = () => {
|
|||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const { realm: realmName } = useContext(RealmContext);
|
||||
const history = useHistory();
|
||||
const { url } = useRouteMatch();
|
||||
const [listUsers, setListUsers] = useState(false);
|
||||
const [initialSearch, setInitialSearch] = useState("");
|
||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||
|
@ -157,11 +160,13 @@ export const UsersSection = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const goToCreate = () => history.push(`${url}/add-user`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
<ViewHeader titleKey="users:title" subKey="" />
|
||||
<PageSection variant="light">
|
||||
<PageSection data-testid="users-page" variant="light">
|
||||
{!listUsers && !initialSearch && (
|
||||
<SearchUser
|
||||
onSearch={(search) => {
|
||||
|
@ -183,13 +188,15 @@ export const UsersSection = () => {
|
|||
message={t("noUsersFound")}
|
||||
instructions={t("emptyInstructions")}
|
||||
primaryActionText={t("createNewUser")}
|
||||
onPrimaryAction={() => {}}
|
||||
onPrimaryAction={goToCreate}
|
||||
/>
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button>{t("addUser")}</Button>
|
||||
<Button data-testid="add-user" onClick={goToCreate}>
|
||||
{t("addUser")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
|
|
57
src/user/UsersTabs.tsx
Normal file
57
src/user/UsersTabs.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React from "react";
|
||||
import { AlertVariant, PageSection } from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
import { UserForm } from "./UserForm";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
|
||||
export const UsersTabs = () => {
|
||||
const { t } = useTranslation("roles");
|
||||
const { addAlert } = useAlerts();
|
||||
const { url } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
||||
|
||||
const save = async (user: UserRepresentation) => {
|
||||
try {
|
||||
await adminClient.users.create({
|
||||
username: user!.username,
|
||||
email: user!.email,
|
||||
emailVerified: user!.emailVerified,
|
||||
firstName: user!.firstName,
|
||||
lastName: user!.lastName,
|
||||
enabled: user!.enabled,
|
||||
requiredActions: user!.requiredActions,
|
||||
});
|
||||
addAlert(t("users:userCreated"), AlertVariant.success);
|
||||
history.push(url.substr(0, url.lastIndexOf("/")));
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("users:userCreateError", {
|
||||
error: error.response.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader
|
||||
titleKey={t("users:createUser")}
|
||||
subKey=""
|
||||
dividerComponent="div"
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<UserForm form={form} save={save} />
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -3,24 +3,39 @@
|
|||
"title": "Users",
|
||||
"searchForUser": "Search user",
|
||||
"startBySearchingAUser": "Start by searching for users",
|
||||
"createUser": "Create user",
|
||||
"createNewUser": "Create new user",
|
||||
"noUsersFound": "No users found",
|
||||
"noUsersFoundError": "No users found due to {{error}}",
|
||||
"emptyInstructions": "Change your search criteria or add a user",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"emailVerified": "Email verified",
|
||||
"lastName": "Last name",
|
||||
"firstName": "First name",
|
||||
"status": "Status",
|
||||
"disabled": "Disabled",
|
||||
"disabledHelpText": "A disabled user cannot log in.",
|
||||
"emailVerifiedHelpText": "Has the user's email been verified?",
|
||||
"emailInvalid": "You must enter a valid email.",
|
||||
"temporaryDisabled": "Temporarily disabled",
|
||||
"notVerified": "Not verified",
|
||||
"requiredUserActions": "Required user actions",
|
||||
"requiredUserActionsHelpText": "Require an action when the user logs in. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure OTP' requires setup of a mobile password generator.",
|
||||
"addUser": "Add user",
|
||||
"deleteUser": "Delete user",
|
||||
"deleteConfirm": "Delete user?",
|
||||
"deleteConfirmDialog": "Are you sure you want to permanently delete {{count}} selected user",
|
||||
"deleteConfirmDialog_plural": "Are you sure you want to permanently delete {{count}} selected users",
|
||||
"userCreated": "The user has been created",
|
||||
"userCreateError": "Could not create user: {{error}}",
|
||||
"userDeletedSuccess": "The user has been deleted",
|
||||
"userDeletedError": "The user could not be deleted {{error}}"
|
||||
"userDeletedError": "The user could not be deleted {{error}}",
|
||||
"configureOTP": "Configure OTP",
|
||||
"updatePassword": "Update Password",
|
||||
"updateProfile": "Update Profile",
|
||||
"verifyEmail": "Verify Email",
|
||||
"updateUserLocale": "Update User Locale"
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue