add create user page
This commit is contained in:
parent
d45bb1d5ad
commit
669c8d7377
4 changed files with 278 additions and 134 deletions
|
@ -27,6 +27,7 @@ export type ViewHeaderProps = {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
badgeId?: string;
|
badgeId?: string;
|
||||||
badgeIsRead?: boolean;
|
badgeIsRead?: boolean;
|
||||||
|
dividerComponent?: "div" | "hr" | "li" | undefined;
|
||||||
subKey: string;
|
subKey: string;
|
||||||
actionsDropdownId?: string;
|
actionsDropdownId?: string;
|
||||||
subKeyLinkProps?: FormattedLinkProps;
|
subKeyLinkProps?: FormattedLinkProps;
|
||||||
|
@ -43,6 +44,7 @@ export const ViewHeader = ({
|
||||||
badge,
|
badge,
|
||||||
badgeId,
|
badgeId,
|
||||||
badgeIsRead,
|
badgeIsRead,
|
||||||
|
dividerComponent,
|
||||||
subKey,
|
subKey,
|
||||||
subKeyLinkProps,
|
subKeyLinkProps,
|
||||||
dropdownItems,
|
dropdownItems,
|
||||||
|
@ -157,7 +159,7 @@ export const ViewHeader = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
<Divider />
|
<Divider component={dividerComponent} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,88 +1,226 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActionGroup,
|
ActionGroup,
|
||||||
Button,
|
Button,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
Switch,
|
||||||
TextArea,
|
TextArea,
|
||||||
TextInput,
|
TextInput,
|
||||||
// ValidatedOptions,
|
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UseFormMethods } from "react-hook-form";
|
import { Controller, UseFormMethods } from "react-hook-form";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||||
// import { RoleFormType } from "./RealmRoleTabs";
|
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||||
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
|
|
||||||
export type UserFormProps = {
|
export type UserFormProps = {
|
||||||
form: UseFormMethods<UserRepresentation>;
|
form: UseFormMethods<UserRepresentation>;
|
||||||
save: (user: UserRepresentation) => void;
|
save: (user: UserRepresentation) => void;
|
||||||
editMode: boolean;
|
|
||||||
reset: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserForm = ({
|
export const UserForm = ({ form, save }: UserFormProps) => {
|
||||||
form,
|
|
||||||
save,
|
|
||||||
editMode,
|
|
||||||
reset,
|
|
||||||
}: UserFormProps) => {
|
|
||||||
const { t } = useTranslation("users");
|
const { t } = useTranslation("users");
|
||||||
|
const { realm } = useRealm();
|
||||||
|
|
||||||
|
const [
|
||||||
|
isRequiredUserActionsDropdownOpen,
|
||||||
|
setRequiredUserActionsDropdownOpen,
|
||||||
|
] = useState(false);
|
||||||
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<FormAccess
|
<FormAccess
|
||||||
isHorizontal
|
isHorizontal
|
||||||
onSubmit={form.handleSubmit(save)}
|
onSubmit={form.handleSubmit(save)}
|
||||||
role="manage-realm"
|
role="manage-users"
|
||||||
className="pf-u-mt-lg"
|
className="pf-u-mt-lg"
|
||||||
>
|
>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("roleName")}
|
label={t("username")}
|
||||||
fieldId="kc-name"
|
fieldId="kc-username"
|
||||||
isRequired
|
isRequired
|
||||||
// validated={form.errors.name ? "error" : "default"}
|
validated={form.errors.username ? "error" : "default"}
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={form.register({ required: !editMode })}
|
ref={form.register()}
|
||||||
type="text"
|
type="text"
|
||||||
id="kc-name"
|
id="kc-username"
|
||||||
name="name"
|
name="username"
|
||||||
isReadOnly={editMode}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("common:description")}
|
label={t("email")}
|
||||||
fieldId="kc-description"
|
fieldId="kc-description"
|
||||||
// validated={
|
validated={form.errors.email ? "error" : "default"}
|
||||||
// form.errors.description
|
helperTextInvalid={form.errors.email?.message}
|
||||||
// ? ValidatedOptions.error
|
|
||||||
// : ValidatedOptions.default
|
|
||||||
// }
|
|
||||||
// helperTextInvalid={form.errors.description?.message}
|
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextInput
|
||||||
name="description"
|
ref={form.register()}
|
||||||
ref={form.register({
|
|
||||||
maxLength: {
|
|
||||||
value: 255,
|
|
||||||
message: t("common:maxLength", { length: 255 }),
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
type="text"
|
type="text"
|
||||||
// validated={
|
id="kc-email"
|
||||||
// form.errors.description
|
name="email"
|
||||||
// ? ValidatedOptions.error
|
|
||||||
// : ValidatedOptions.default
|
|
||||||
// }
|
|
||||||
id="kc-role-description"
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</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={form.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={form.errors.firstName ? "error" : "default"}
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={form.register()}
|
||||||
|
type="text"
|
||||||
|
id="kc-firstname"
|
||||||
|
name="firstname"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("lastName")}
|
||||||
|
fieldId="kc-name"
|
||||||
|
validated={form.errors.lastName ? "error" : "default"}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={form.register()}
|
||||||
|
type="text"
|
||||||
|
id="kc-lastname"
|
||||||
|
name="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={form.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={form.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={form.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>
|
<ActionGroup>
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">
|
||||||
{t("common:save")}
|
{t("common:Create")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => reset()} variant="link">
|
<Button onClick={() => history.push(`/${realm}/users`)} variant="link">
|
||||||
{editMode ? t("common:reload") : t("common:cancel")}
|
{t("common:cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
</FormAccess>
|
</FormAccess>
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
||||||
import {
|
import { Divider, PageSection } from "@patternfly/react-core";
|
||||||
PageSection,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
@ -16,104 +14,99 @@ import { UserForm } from "./UserForm";
|
||||||
export const UsersTabs = () => {
|
export const UsersTabs = () => {
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
||||||
// const history = useHistory();
|
// const history = useHistory();
|
||||||
|
|
||||||
// const adminClient = useAdminClient();
|
// const adminClient = useAdminClient();
|
||||||
// const [role, setRole] = useState<RoleFormType>();
|
// const [role, setRole] = useState<RoleFormType>();
|
||||||
|
|
||||||
// const { id, clientId } = useParams<{ id: string; clientId: string }>();
|
// const { id, clientId } = useParams<{ id: string; clientId: string }>();
|
||||||
// const { url } = useRouteMatch();
|
// const { url } = useRouteMatch();
|
||||||
|
|
||||||
// const { realm } = useRealm();
|
// const { realm } = useRealm();
|
||||||
|
|
||||||
// const [key, setKey] = useState("");
|
// const [key, setKey] = useState("");
|
||||||
|
|
||||||
|
// const { addAlert } = useAlerts();
|
||||||
|
|
||||||
// const { addAlert } = useAlerts();
|
// const { fields, append, remove } = useFieldArray({
|
||||||
|
// control: form.control,
|
||||||
|
// name: "attributes",
|
||||||
|
// });
|
||||||
|
|
||||||
// const { fields, append, remove } = useFieldArray({
|
// useEffect(() => append({ key: "", value: "" }), [append, role]);
|
||||||
// control: form.control,
|
|
||||||
// name: "attributes",
|
|
||||||
// });
|
|
||||||
|
|
||||||
useEffect(() => append({ key: "", value: "" }), [append, role]);
|
// const save = async (user: UserRepresentation) => {
|
||||||
|
// try {
|
||||||
|
// const { attributes, ...rest } = role;
|
||||||
|
// const roleRepresentation: RoleRepresentation = rest;
|
||||||
|
// if (id) {
|
||||||
|
// if (attributes) {
|
||||||
|
// roleRepresentation.attributes = arrayToAttributes(attributes);
|
||||||
|
// }
|
||||||
|
// if (!clientId) {
|
||||||
|
// await adminClient.roles.updateById({ id }, roleRepresentation);
|
||||||
|
// } else {
|
||||||
|
// await adminClient.clients.updateRole(
|
||||||
|
// { id: clientId, roleName: role.name! },
|
||||||
|
// roleRepresentation
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
// const save = async (user: UserRepresentation) => {
|
// await adminClient.roles.createComposite(
|
||||||
// try {
|
// { roleId: id, realm },
|
||||||
// const { attributes, ...rest } = role;
|
// additionalRoles
|
||||||
// const roleRepresentation: RoleRepresentation = rest;
|
// );
|
||||||
// if (id) {
|
|
||||||
// if (attributes) {
|
|
||||||
// roleRepresentation.attributes = arrayToAttributes(attributes);
|
|
||||||
// }
|
|
||||||
// if (!clientId) {
|
|
||||||
// await adminClient.roles.updateById({ id }, roleRepresentation);
|
|
||||||
// } else {
|
|
||||||
// await adminClient.clients.updateRole(
|
|
||||||
// { id: clientId, roleName: role.name! },
|
|
||||||
// roleRepresentation
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await adminClient.roles.createComposite(
|
// setRole(role);
|
||||||
// { roleId: id, realm },
|
// } else {
|
||||||
// additionalRoles
|
// let createdRole;
|
||||||
// );
|
// if (!clientId) {
|
||||||
|
// await adminClient.roles.create(roleRepresentation);
|
||||||
// setRole(role);
|
// createdRole = await adminClient.roles.findOneByName({
|
||||||
// } else {
|
// name: role.name!,
|
||||||
// let createdRole;
|
// });
|
||||||
// if (!clientId) {
|
// } else {
|
||||||
// await adminClient.roles.create(roleRepresentation);
|
// await adminClient.clients.createRole({
|
||||||
// createdRole = await adminClient.roles.findOneByName({
|
// id: clientId,
|
||||||
// name: role.name!,
|
// name: role.name,
|
||||||
// });
|
// });
|
||||||
// } else {
|
// if (role.description) {
|
||||||
// await adminClient.clients.createRole({
|
// await adminClient.clients.updateRole(
|
||||||
// id: clientId,
|
// { id: clientId, roleName: role.name! },
|
||||||
// name: role.name,
|
// roleRepresentation
|
||||||
// });
|
// );
|
||||||
// if (role.description) {
|
// }
|
||||||
// await adminClient.clients.updateRole(
|
// createdRole = await adminClient.clients.findRole({
|
||||||
// { id: clientId, roleName: role.name! },
|
// id: clientId,
|
||||||
// roleRepresentation
|
// roleName: role.name!,
|
||||||
// );
|
// });
|
||||||
// }
|
// }
|
||||||
// createdRole = await adminClient.clients.findRole({
|
// setRole(convert(createdRole));
|
||||||
// id: clientId,
|
// history.push(
|
||||||
// roleName: role.name!,
|
// url.substr(0, url.lastIndexOf("/") + 1) + createdRole.id + "/details"
|
||||||
// });
|
// );
|
||||||
// }
|
// }
|
||||||
// setRole(convert(createdRole));
|
// addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success);
|
||||||
// history.push(
|
// } catch (error) {
|
||||||
// url.substr(0, url.lastIndexOf("/") + 1) + createdRole.id + "/details"
|
// addAlert(
|
||||||
// );
|
// t((id ? "roleSave" : "roleCreate") + "Error", {
|
||||||
// }
|
// error: error.response.data?.errorMessage || error,
|
||||||
// addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success);
|
// }),
|
||||||
// } catch (error) {
|
// AlertVariant.danger
|
||||||
// addAlert(
|
// );
|
||||||
// t((id ? "roleSave" : "roleCreate") + "Error", {
|
// }
|
||||||
// error: error.response.data?.errorMessage || error,
|
// };
|
||||||
// }),
|
|
||||||
// AlertVariant.danger
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader
|
<ViewHeader titleKey={t("users:createUser")} subKey="" dividerComponent="div" />
|
||||||
titleKey={role?.name || t("createRole")}
|
|
||||||
subKey={id ? "" : "roles:roleCreateExplain"}
|
|
||||||
actionsDropdownId="roles-actions-dropdown"
|
|
||||||
/>
|
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
<UserForm
|
<UserForm
|
||||||
reset={() => form.reset()}
|
reset={() => form.reset()}
|
||||||
form={form}
|
form={form}
|
||||||
save={save}
|
save={() => {}}
|
||||||
editMode={false}
|
editMode={false}
|
||||||
/>
|
/>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,18 +10,29 @@
|
||||||
"emptyInstructions": "Change your search criteria or add a user",
|
"emptyInstructions": "Change your search criteria or add a user",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"emailVerified": "Email verified",
|
||||||
"lastName": "Last name",
|
"lastName": "Last name",
|
||||||
"firstName": "First name",
|
"firstName": "First name",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
|
"disabledHelpText": "A disabled user cannot log in.",
|
||||||
|
"emailVerifiedHelpText": "Has the user's email been verified?",
|
||||||
"temporaryDisabled": "Temporarily disabled",
|
"temporaryDisabled": "Temporarily disabled",
|
||||||
"notVerified": "Not verified",
|
"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",
|
"addUser": "Add user",
|
||||||
"deleteUser": "Delete user",
|
"deleteUser": "Delete user",
|
||||||
"deleteConfirm": "Delete user?",
|
"deleteConfirm": "Delete user?",
|
||||||
"deleteConfirmDialog": "Are you sure you want to permanently delete {{count}} selected 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",
|
"deleteConfirmDialog_plural": "Are you sure you want to permanently delete {{count}} selected users",
|
||||||
"userDeletedSuccess": "The user has been deleted",
|
"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