diff --git a/js/apps/admin-ui/src/user/CreateUser.tsx b/js/apps/admin-ui/src/user/CreateUser.tsx index 78cca457bc9..9f7152516f7 100644 --- a/js/apps/admin-ui/src/user/CreateUser.tsx +++ b/js/apps/admin-ui/src/user/CreateUser.tsx @@ -1,5 +1,4 @@ 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 { useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; @@ -16,6 +15,7 @@ import { isUserProfileError, userProfileErrorToString, } from "./UserProfileFields"; +import { UserFormFields, toUserRepresentation } from "./form-state"; import { toUser } from "./routes/User"; import "./user-section.css"; @@ -25,14 +25,13 @@ export default function CreateUser() { const { addAlert, addError } = useAlerts(); const navigate = useNavigate(); const { realm } = useRealm(); - const userForm = useForm({ mode: "onChange" }); + const userForm = useForm({ mode: "onChange" }); const [addedGroups, setAddedGroups] = useState([]); - const save = async (formUser: UserRepresentation) => { + const save = async (data: UserFormFields) => { try { const createdUser = await adminClient.users.create({ - ...formUser, - username: formUser.username?.trim(), + ...toUserRepresentation(data), groups: addedGroups.map((group) => group.path!), enabled: true, }); diff --git a/js/apps/admin-ui/src/user/EditUser.tsx b/js/apps/admin-ui/src/user/EditUser.tsx index e0edfd30e93..768ca122995 100644 --- a/js/apps/admin-ui/src/user/EditUser.tsx +++ b/js/apps/admin-ui/src/user/EditUser.tsx @@ -15,10 +15,6 @@ import { useNavigate } from "react-router-dom"; import { adminClient } from "../admin-client"; import { useAlerts } from "../components/alert/Alerts"; 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 { RoutableTabs, @@ -44,6 +40,11 @@ import { } from "./UserProfileFields"; import { UserRoleMapping } from "./UserRoleMapping"; import { UserSessions } from "./UserSessions"; +import { + UserFormFields, + toUserFormFields, + toUserRepresentation, +} from "./form-state"; import { UserParams, UserTab, toUser } from "./routes/User"; import { toUsers } from "./routes/Users"; @@ -103,9 +104,9 @@ const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => { const { addAlert, addError } = useAlerts(); const navigate = useNavigate(); const { hasAccess } = useAccess(); - const userForm = useForm({ + const userForm = useForm({ mode: "onChange", - defaultValues: user, + defaultValues: toUserFormFields(user), }); const [realmRepresentation, setRealmRepresentattion] = @@ -146,22 +147,13 @@ const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => { const sessionsTab = useTab("sessions"); // 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 attributes = - "key" in (formUser.attributes?.[0] || []) - ? keyValueToArray(formUser.attributes as KeyValueType[]) - : []; + const save = async (data: UserFormFields) => { try { await adminClient.users.update( { id: user.id! }, - { - ...user, - ...formUser, - username: formUser.username?.trim(), - attributes, - }, + toUserRepresentation(data), ); addAlert(t("userSaved"), AlertVariant.success); refresh(); @@ -235,7 +227,9 @@ const EditUserForm = ({ user, bruteForced, refresh }: EditUserFormProps) => { {t("common:delete")} , ]} - onToggle={(value) => save({ ...user, enabled: value })} + onToggle={(value) => + save({ ...toUserFormFields(user), enabled: value }) + } isEnabled={user.enabled} /> diff --git a/js/apps/admin-ui/src/user/UserAttributes.tsx b/js/apps/admin-ui/src/user/UserAttributes.tsx index a12db5c9ffd..aaa75058409 100644 --- a/js/apps/admin-ui/src/user/UserAttributes.tsx +++ b/js/apps/admin-ui/src/user/UserAttributes.tsx @@ -1,31 +1,17 @@ import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import { PageSection, PageSectionVariants } from "@patternfly/react-core"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; +import { useFormContext } from "react-hook-form"; -import { - AttributeForm, - 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"; +import { AttributesForm } from "../components/key-value-form/AttributeForm"; +import { UserFormFields, toUserFormFields } from "./form-state"; type UserAttributesProps = { user: UserRepresentation; - save: (user: UserRepresentation) => void; + save: (user: UserFormFields) => void; }; export const UserAttributes = ({ user, save }: UserAttributesProps) => { - const form = useForm({ mode: "onChange" }); - const { config } = useUserProfile(); - - const convertAttributes = () => { - return arrayToKeyValue(user.attributes!); - }; - - useEffect(() => { - form.setValue("attributes", convertAttributes()); - }, [user, config]); + const form = useFormContext(); return ( @@ -35,7 +21,8 @@ export const UserAttributes = ({ user, save }: UserAttributesProps) => { fineGrainedAccess={user.access?.manage} reset={() => form.reset({ - attributes: convertAttributes(), + ...form.getValues(), + attributes: toUserFormFields(user).attributes, }) } /> diff --git a/js/apps/admin-ui/src/user/UserForm.tsx b/js/apps/admin-ui/src/user/UserForm.tsx index f49d071991d..eda83fbbfb4 100644 --- a/js/apps/admin-ui/src/user/UserForm.tsx +++ b/js/apps/admin-ui/src/user/UserForm.tsx @@ -29,6 +29,7 @@ import useFormatDate from "../utils/useFormatDate"; import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled"; import { FederatedUserLink } from "./FederatedUserLink"; import { UserProfileFields } from "./UserProfileFields"; +import { UserFormFields } from "./form-state"; import { RequiredActionMultiSelect } from "./user-credentials/RequiredActionMultiSelect"; export type BruteForced = { @@ -40,13 +41,13 @@ export type UserFormProps = { user?: UserRepresentation; bruteForce?: BruteForced; realm?: RealmRepresentation; - save: (user: UserRepresentation) => void; + save: (user: UserFormFields) => void; onGroupsUpdate?: (groups: GroupRepresentation[]) => void; }; const EmailVerified = () => { const { t } = useTranslation("users"); - const { control } = useFormContext(); + const { control } = useFormContext(); return ( (); const watchUsernameInput = watch("username"); const [selectedGroups, setSelectedGroups] = useState( [], diff --git a/js/apps/admin-ui/src/user/form-state.ts b/js/apps/admin-ui/src/user/form-state.ts new file mode 100644 index 00000000000..8668fafd963 --- /dev/null +++ b/js/apps/admin-ui/src/user/form-state.ts @@ -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 & { + 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 }; +}