Centralize user form to user representation conversion (#22851)

This commit is contained in:
Jon Koops 2023-09-04 06:55:34 +02:00 committed by GitHub
parent 28c2cba3b9
commit bb4495adb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 51 additions and 49 deletions

View file

@ -1,5 +1,4 @@
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation"; import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { AlertVariant, PageSection } from "@patternfly/react-core"; import { AlertVariant, PageSection } from "@patternfly/react-core";
import { useState } from "react"; import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
@ -16,6 +15,7 @@ import {
isUserProfileError, isUserProfileError,
userProfileErrorToString, userProfileErrorToString,
} from "./UserProfileFields"; } from "./UserProfileFields";
import { UserFormFields, toUserRepresentation } from "./form-state";
import { toUser } from "./routes/User"; import { toUser } from "./routes/User";
import "./user-section.css"; import "./user-section.css";
@ -25,14 +25,13 @@ export default function CreateUser() {
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const navigate = useNavigate(); const navigate = useNavigate();
const { realm } = useRealm(); const { realm } = useRealm();
const userForm = useForm<UserRepresentation>({ mode: "onChange" }); const userForm = useForm<UserFormFields>({ mode: "onChange" });
const [addedGroups, setAddedGroups] = useState<GroupRepresentation[]>([]); const [addedGroups, setAddedGroups] = useState<GroupRepresentation[]>([]);
const save = async (formUser: UserRepresentation) => { const save = async (data: UserFormFields) => {
try { try {
const createdUser = await adminClient.users.create({ const createdUser = await adminClient.users.create({
...formUser, ...toUserRepresentation(data),
username: formUser.username?.trim(),
groups: addedGroups.map((group) => group.path!), groups: addedGroups.map((group) => group.path!),
enabled: true, enabled: true,
}); });

View file

@ -15,10 +15,6 @@ import { useNavigate } from "react-router-dom";
import { adminClient } from "../admin-client"; import { adminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import {
KeyValueType,
keyValueToArray,
} from "../components/key-value-form/key-value-convert";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { import {
RoutableTabs, RoutableTabs,
@ -44,6 +40,11 @@ import {
} from "./UserProfileFields"; } from "./UserProfileFields";
import { UserRoleMapping } from "./UserRoleMapping"; import { UserRoleMapping } from "./UserRoleMapping";
import { UserSessions } from "./UserSessions"; import { UserSessions } from "./UserSessions";
import {
UserFormFields,
toUserFormFields,
toUserRepresentation,
} from "./form-state";
import { UserParams, UserTab, toUser } from "./routes/User"; import { UserParams, UserTab, toUser } from "./routes/User";
import { toUsers } from "./routes/Users"; import { toUsers } from "./routes/Users";
@ -97,17 +98,15 @@ type EditUserFormProps = {
refresh: () => void; refresh: () => void;
}; };
type FormFields = Omit<UserRepresentation, "userProfileMetadata">;
const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => { const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => {
const { t } = useTranslation("users"); const { t } = useTranslation("users");
const { realm } = useRealm(); const { realm } = useRealm();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const navigate = useNavigate(); const navigate = useNavigate();
const { hasAccess } = useAccess(); const { hasAccess } = useAccess();
const userForm = useForm<FormFields>({ const userForm = useForm<UserFormFields>({
mode: "onChange", mode: "onChange",
defaultValues: user, defaultValues: toUserFormFields(user),
}); });
const [realmRepresentation, setRealmRepresentattion] = const [realmRepresentation, setRealmRepresentattion] =
@ -148,22 +147,13 @@ const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => {
const sessionsTab = useTab("sessions"); const sessionsTab = useTab("sessions");
// Ensure the form remains up-to-date when the user is updated. // Ensure the form remains up-to-date when the user is updated.
useUpdateEffect(() => userForm.reset(user), [user]); useUpdateEffect(() => userForm.reset(toUserFormFields(user)), [user]);
const save = async (formUser: UserRepresentation) => { const save = async (data: UserFormFields) => {
const attributes =
"key" in (formUser.attributes?.[0] || [])
? keyValueToArray(formUser.attributes as KeyValueType[])
: [];
try { try {
await adminClient.users.update( await adminClient.users.update(
{ id: user.id! }, { id: user.id! },
{ toUserRepresentation(data),
...user,
...formUser,
username: formUser.username?.trim(),
attributes,
},
); );
addAlert(t("userSaved"), AlertVariant.success); addAlert(t("userSaved"), AlertVariant.success);
refresh(); refresh();
@ -237,7 +227,9 @@ const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => {
{t("common:delete")} {t("common:delete")}
</DropdownItem>, </DropdownItem>,
]} ]}
onToggle={(value) => save({ ...user, enabled: value })} onToggle={(value) =>
save({ ...toUserFormFields(user), enabled: value })
}
isEnabled={user.enabled} isEnabled={user.enabled}
/> />

View file

@ -1,31 +1,17 @@
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { PageSection, PageSectionVariants } from "@patternfly/react-core"; import { PageSection, PageSectionVariants } from "@patternfly/react-core";
import { useEffect } from "react"; import { useFormContext } from "react-hook-form";
import { useForm } from "react-hook-form";
import { import { AttributesForm } from "../components/key-value-form/AttributeForm";
AttributeForm, import { UserFormFields, toUserFormFields } from "./form-state";
AttributesForm,
} from "../components/key-value-form/AttributeForm";
import { arrayToKeyValue } from "../components/key-value-form/key-value-convert";
import { useUserProfile } from "../realm-settings/user-profile/UserProfileContext";
type UserAttributesProps = { type UserAttributesProps = {
user: UserRepresentation; user: UserRepresentation;
save: (user: UserRepresentation) => void; save: (user: UserFormFields) => void;
}; };
export const UserAttributes = ({ user, save }: UserAttributesProps) => { export const UserAttributes = ({ user, save }: UserAttributesProps) => {
const form = useForm<AttributeForm>({ mode: "onChange" }); const form = useFormContext<UserFormFields>();
const { config } = useUserProfile();
const convertAttributes = () => {
return arrayToKeyValue<UserRepresentation>(user.attributes!);
};
useEffect(() => {
form.setValue("attributes", convertAttributes());
}, [user, config]);
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
@ -35,7 +21,8 @@ export const UserAttributes = ({ user, save }: UserAttributesProps) => {
fineGrainedAccess={user.access?.manage} fineGrainedAccess={user.access?.manage}
reset={() => reset={() =>
form.reset({ form.reset({
attributes: convertAttributes(), ...form.getValues(),
attributes: toUserFormFields(user).attributes,
}) })
} }
/> />

View file

@ -29,6 +29,7 @@ import useFormatDate from "../utils/useFormatDate";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled"; import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
import { FederatedUserLink } from "./FederatedUserLink"; import { FederatedUserLink } from "./FederatedUserLink";
import { UserProfileFields } from "./UserProfileFields"; import { UserProfileFields } from "./UserProfileFields";
import { UserFormFields } from "./form-state";
import { RequiredActionMultiSelect } from "./user-credentials/RequiredActionMultiSelect"; import { RequiredActionMultiSelect } from "./user-credentials/RequiredActionMultiSelect";
export type BruteForced = { export type BruteForced = {
@ -40,13 +41,13 @@ export type UserFormProps = {
user?: UserRepresentation; user?: UserRepresentation;
bruteForce?: BruteForced; bruteForce?: BruteForced;
realm?: RealmRepresentation; realm?: RealmRepresentation;
save: (user: UserRepresentation) => void; save: (user: UserFormFields) => void;
onGroupsUpdate?: (groups: GroupRepresentation[]) => void; onGroupsUpdate?: (groups: GroupRepresentation[]) => void;
}; };
const EmailVerified = () => { const EmailVerified = () => {
const { t } = useTranslation("users"); const { t } = useTranslation("users");
const { control } = useFormContext(); const { control } = useFormContext<UserFormFields>();
return ( return (
<FormGroup <FormGroup
label={t("emailVerified")} label={t("emailVerified")}
@ -106,7 +107,7 @@ export const UserForm = ({
control, control,
reset, reset,
formState: { errors }, formState: { errors },
} = useFormContext(); } = useFormContext<UserFormFields>();
const watchUsernameInput = watch("username"); const watchUsernameInput = watch("username");
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>( const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
[], [],

View file

@ -0,0 +1,23 @@
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import {
KeyValueType,
arrayToKeyValue,
keyValueToArray,
} from "../components/key-value-form/key-value-convert";
export type UserFormFields = Omit<UserRepresentation, "userProfileMetadata"> & {
attributes?: KeyValueType[];
};
export function toUserFormFields(data: UserRepresentation): UserFormFields {
const attributes = arrayToKeyValue(data.attributes);
return { ...data, attributes };
}
export function toUserRepresentation(data: UserFormFields): UserRepresentation {
const username = data.username?.trim();
const attributes = keyValueToArray(data.attributes);
return { ...data, username, attributes };
}