migrated user forms to ui-shared (#27593)

* migrated user forms to ui-shared

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* review comments

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-03-06 17:06:13 +01:00 committed by GitHub
parent 39299eeb38
commit b0b967d8d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 284 additions and 451 deletions

View file

@ -6,16 +6,16 @@ export default class CredentialsPage {
readonly #setPasswordBtn = "confirm"; readonly #setPasswordBtn = "confirm";
readonly #credentialResetModal = "credential-reset-modal"; readonly #credentialResetModal = "credential-reset-modal";
readonly #resetModalActionsToggleBtn = readonly #resetModalActionsToggleBtn =
"[data-testid=credential-reset-modal] #actions-actions"; "[data-testid=credential-reset-modal] #actions";
readonly #passwordField = "passwordField"; readonly #passwordField = "passwordField";
readonly #passwordConfirmationField = "passwordConfirmationField"; readonly #passwordConfirmationField = "passwordConfirmationField";
readonly #resetActions = [ readonly #resetActions = [
"VERIFY_EMAIL-option", "Verify Email",
"UPDATE_PROFILE-option", "Update Profile",
"CONFIGURE_TOTP-option", "Configure OTP",
"UPDATE_PASSWORD-option", "Update Password",
"TERMS_AND_CONDITIONS-option", "Terms and Conditions",
]; ];
readonly #confirmationButton = "confirm"; readonly #confirmationButton = "confirm";
readonly #editLabelBtn = "editUserLabelBtn"; readonly #editLabelBtn = "editUserLabelBtn";
@ -57,7 +57,9 @@ export default class CredentialsPage {
} }
clickResetModalAction(index: number) { clickResetModalAction(index: number) {
cy.findByTestId(this.#resetActions[index]).click(); cy.get("[data-testid=credential-reset-modal] .pf-c-select__menu")
.contains(this.#resetActions[index])
.click();
return this; return this;
} }

View file

@ -9,8 +9,8 @@ export default class IdentityProviderLinksTab {
#availableProvidersSection = ".kc-available-idps"; #availableProvidersSection = ".kc-available-idps";
#linkAccountBtn = ".pf-c-button.pf-m-link"; #linkAccountBtn = ".pf-c-button.pf-m-link";
#linkAccountModalIdentityProviderInput = "idpNameInput"; #linkAccountModalIdentityProviderInput = "idpNameInput";
#linkAccountModalUserIdInput = "userIdInput"; #linkAccountModalUserIdInput = "userId";
#linkAccountModalUsernameInput = "usernameInput"; #linkAccountModalUsernameInput = "userName";
public clickLinkAccount(idpName: string) { public clickLinkAccount(idpName: string) {
cy.get(this.#availableProvidersSection + " tr") cy.get(this.#availableProvidersSection + " tr")

View file

@ -2853,6 +2853,7 @@ dropNonexistingGroupsDuringSync=Drop non-existing groups during sync
clientAssertionSigningAlgHelp=Signature algorithm to create JWT assertion as client authentication. In the case of JWT signed with private key or JWT signed with client secret, it is required. If no algorithm is specified, the following algorithm is adapted. RS256 is adapted in the case of JWT signed with private key. HS256 is adapted in the case of JWT signed with client secret. clientAssertionSigningAlgHelp=Signature algorithm to create JWT assertion as client authentication. In the case of JWT signed with private key or JWT signed with client secret, it is required. If no algorithm is specified, the following algorithm is adapted. RS256 is adapted in the case of JWT signed with private key. HS256 is adapted in the case of JWT signed with client secret.
jwtX509HeadersEnabledHelp=If enabled, the x5t (X.509 Certificate SHA-1 Thumbprint) header will be added to the JWT to reference the certificate used to sign it. Otherwise, the kid (Key ID) header will be used instead. jwtX509HeadersEnabledHelp=If enabled, the x5t (X.509 Certificate SHA-1 Thumbprint) header will be added to the JWT to reference the certificate used to sign it. Otherwise, the kid (Key ID) header will be used instead.
addProvider_other=Add {{provider}} providers addProvider_other=Add {{provider}} providers
resetAction=Reset action
cibaExpiresIn=Expires In cibaExpiresIn=Expires In
dynamicScopeFormatHelp=This is the regular expression that the system will use to extract the scope name and variable. dynamicScopeFormatHelp=This is the regular expression that the system will use to extract the scope name and variable.
updateTranslationError=Error updating translation. updateTranslationError=Error updating translation.

View file

@ -13,13 +13,18 @@ import {
Switch, Switch,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { TFunction } from "i18next"; import { TFunction } from "i18next";
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { Controller, UseFormReturn } from "react-hook-form"; import { Controller, FormProvider, UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { HelpItem, UserProfileFields } from "ui-shared"; import {
HelpItem,
SwitchControl,
TextControl,
UserProfileFields,
} from "ui-shared";
import { adminClient } from "../admin-client"; import { adminClient } from "../admin-client";
import { DefaultSwitchControl } from "../components/SwitchControl";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { FormAccess } from "../components/form/FormAccess"; import { FormAccess } from "../components/form/FormAccess";
import { GroupPickerDialog } from "../components/group/GroupPickerDialog"; import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
@ -68,7 +73,6 @@ export const UserForm = ({
const { const {
handleSubmit, handleSubmit,
register,
setValue, setValue,
watch, watch,
control, control,
@ -133,284 +137,214 @@ export const UserForm = ({
fineGrainedAccess={user?.access?.manage} fineGrainedAccess={user?.access?.manage}
className="pf-u-mt-lg" className="pf-u-mt-lg"
> >
{open && ( <FormProvider {...form}>
<GroupPickerDialog {open && (
type="selectMany" <GroupPickerDialog
text={{ type="selectMany"
title: "selectGroups", text={{
ok: "join", title: "selectGroups",
}} ok: "join",
canBrowse={isManager} }}
onConfirm={(groups) => { canBrowse={isManager}
user?.id ? addGroups(groups || []) : addChips(groups || []); onConfirm={(groups) => {
setOpen(false); user?.id ? addGroups(groups || []) : addChips(groups || []);
}} setOpen(false);
onClose={() => setOpen(false)} }}
filterGroups={selectedGroups} onClose={() => setOpen(false)}
filterGroups={selectedGroups}
/>
)}
{user?.id && (
<>
<FormGroup label={t("id")} fieldId="kc-id" isRequired>
<KeycloakTextInput
id={user.id}
aria-label={t("userID")}
value={user.id}
readOnly
/>
</FormGroup>
<FormGroup
label={t("createdAt")}
fieldId="kc-created-at"
isRequired
>
<KeycloakTextInput
value={formatDate(new Date(user.createdTimestamp!))}
id="kc-created-at"
readOnly
/>
</FormGroup>
</>
)}
<RequiredActionMultiSelect
name="requiredActions"
label="requiredUserActions"
help="requiredUserActionsHelp"
/> />
)} {(user?.federationLink || user?.origin) && canViewFederationLink && (
{user?.id && (
<>
<FormGroup label={t("id")} fieldId="kc-id" isRequired>
<KeycloakTextInput
id={user.id}
aria-label={t("userID")}
value={user.id}
type="text"
isReadOnly
/>
</FormGroup>
<FormGroup label={t("createdAt")} fieldId="kc-created-at" isRequired>
<KeycloakTextInput
value={formatDate(new Date(user.createdTimestamp!))}
type="text"
id="kc-created-at"
aria-label={t("createdAt")}
name="createdTimestamp"
isReadOnly
/>
</FormGroup>
</>
)}
<RequiredActionMultiSelect
control={control}
name="requiredActions"
label="requiredUserActions"
help="requiredUserActionsHelp"
/>
{(user?.federationLink || user?.origin) && canViewFederationLink && (
<FormGroup
label={t("federationLink")}
labelIcon={
<HelpItem
helpText={t("federationLinkHelp")}
fieldLabelId="federationLink"
/>
}
>
<FederatedUserLink user={user} />
</FormGroup>
)}
{userProfileMetadata ? (
<>
<FormGroup <FormGroup
label={t("emailVerified")} label={t("federationLink")}
fieldId="kc-email-verified"
helperTextInvalid={t("required")}
labelIcon={ labelIcon={
<HelpItem <HelpItem
helpText={t("emailVerifiedHelp")} helpText={t("federationLinkHelp")}
fieldLabelId="emailVerified" fieldLabelId="federationLink"
/> />
} }
> >
<Controller <FederatedUserLink user={user} />
name="emailVerified"
defaultValue={false}
control={control}
render={({ field }) => (
<Switch
data-testid="email-verified-switch"
id="kc-user-email-verified"
onChange={(value) => field.onChange(value)}
isChecked={field.value}
label={t("yes")}
labelOff={t("no")}
/>
)}
/>
</FormGroup> </FormGroup>
<UserProfileFields )}
form={form} {userProfileMetadata ? (
userProfileMetadata={userProfileMetadata} <>
hideReadOnly={!user} <DefaultSwitchControl
supportedLocales={realm.supportedLocales || []} name="emailVerified"
t={ label={t("emailVerified")}
((key: unknown, params) => labelIcon={t("emailVerifiedHelp")}
t(key as string, params as any)) as TFunction />
} <UserProfileFields
/> form={form}
</> userProfileMetadata={userProfileMetadata}
) : ( hideReadOnly={!user}
<> supportedLocales={realm.supportedLocales || []}
{!realm.registrationEmailAsUsername && ( t={
<FormGroup ((key: unknown, params) =>
label={t("username")} t(key as string, params as any)) as TFunction
fieldId="kc-username" }
isRequired />
validated={errors.username ? "error" : "default"} </>
helperTextInvalid={t("required")} ) : (
> <>
<KeycloakTextInput {!realm.registrationEmailAsUsername && (
id="kc-username" <TextControl
isReadOnly={ name="username"
label={t("username")}
readOnly={
!!user?.id && !!user?.id &&
!realm.editUsernameAllowed && !realm.editUsernameAllowed &&
realm.editUsernameAllowed !== undefined realm.editUsernameAllowed !== undefined
} }
{...register("username")} rules={{
required: t("required"),
}}
/> />
</FormGroup> )}
)} <TextControl
<FormGroup name="email"
label={t("email")} label={t("email")}
fieldId="kc-email"
validated={errors.email ? "error" : "default"}
helperTextInvalid={t("emailInvalid")}
>
<KeycloakTextInput
type="email" type="email"
id="kc-email" rules={{
data-testid="email-input" pattern: {
{...register("email", { value: emailRegexPattern,
pattern: emailRegexPattern, message: t("emailInvalid"),
})} },
}}
/> />
</FormGroup> <SwitchControl
name="emailVerified"
label={t("emailVerified")}
labelIcon={t("emailVerifiedHelp")}
labelOn={t("yes")}
labelOff={t("no")}
/>
<TextControl name="firstName" label={t("firstName")} />
<TextControl name="lastName" label={t("lastName")} />
</>
)}
{isBruteForceProtected && (
<FormGroup <FormGroup
label={t("emailVerified")} label={t("temporaryLocked")}
fieldId="kc-email-verified" fieldId="temporaryLocked"
helperTextInvalid={t("required")}
labelIcon={ labelIcon={
<HelpItem <HelpItem
helpText={t("emailVerifiedHelp")} helpText={t("temporaryLockedHelp")}
fieldLabelId="emailVerified" fieldLabelId="temporaryLocked"
/> />
} }
>
<Switch
data-testid="user-locked-switch"
id="temporaryLocked"
onChange={(value) => {
unLockUser();
setLocked(value);
}}
isChecked={locked}
isDisabled={!locked}
label={t("on")}
labelOff={t("off")}
/>
</FormGroup>
)}
{!user?.id && (
<FormGroup
label={t("groups")}
fieldId="kc-groups"
validated={errors.requiredActions ? "error" : "default"}
helperTextInvalid={t("required")}
labelIcon={
<HelpItem helpText={t("groups")} fieldLabelId="groups" />
}
> >
<Controller <Controller
name="emailVerified" name="groups"
defaultValue={false} defaultValue={[]}
control={control} control={control}
render={({ field }) => ( render={() => (
<Switch <InputGroup>
data-testid="email-verified-switch" <ChipGroup categoryName={" "}>
id="kc-user-email-verified" {selectedGroups.map((currentChip) => (
onChange={(value) => field.onChange(value)} <Chip
isChecked={field.value} key={currentChip.id}
label={t("yes")} onClick={() => deleteItem(currentChip.name!)}
labelOff={t("no")} >
/> {currentChip.path}
</Chip>
))}
</ChipGroup>
<Button
id="kc-join-groups-button"
onClick={toggleModal}
variant="secondary"
data-testid="join-groups-button"
>
{t("joinGroups")}
</Button>
</InputGroup>
)} )}
/> />
</FormGroup> </FormGroup>
<FormGroup )}
label={t("firstName")}
fieldId="kc-firstName"
validated={errors.firstName ? "error" : "default"}
helperTextInvalid={t("required")}
>
<KeycloakTextInput
data-testid="firstName-input"
id="kc-firstName"
{...register("firstName")}
/>
</FormGroup>
<FormGroup
label={t("lastName")}
fieldId="kc-lastName"
validated={errors.lastName ? "error" : "default"}
>
<KeycloakTextInput
data-testid="lastName-input"
id="kc-lastName"
aria-label={t("lastName")}
{...register("lastName")}
/>
</FormGroup>
</>
)}
{isBruteForceProtected && (
<FormGroup
label={t("temporaryLocked")}
fieldId="temporaryLocked"
labelIcon={
<HelpItem
helpText={t("temporaryLockedHelp")}
fieldLabelId="temporaryLocked"
/>
}
>
<Switch
data-testid="user-locked-switch"
id="temporaryLocked"
onChange={(value) => {
unLockUser();
setLocked(value);
}}
isChecked={locked}
isDisabled={!locked}
label={t("on")}
labelOff={t("off")}
/>
</FormGroup>
)}
{!user?.id && (
<FormGroup
label={t("groups")}
fieldId="kc-groups"
validated={errors.requiredActions ? "error" : "default"}
helperTextInvalid={t("required")}
labelIcon={<HelpItem helpText={t("groups")} fieldLabelId="groups" />}
>
<Controller
name="groups"
defaultValue={[]}
control={control}
render={() => (
<InputGroup>
<ChipGroup categoryName={" "}>
{selectedGroups.map((currentChip) => (
<Chip
key={currentChip.id}
onClick={() => deleteItem(currentChip.name!)}
>
{currentChip.path}
</Chip>
))}
</ChipGroup>
<Button
id="kc-join-groups-button"
onClick={toggleModal}
variant="secondary"
data-testid="join-groups-button"
>
{t("joinGroups")}
</Button>
</InputGroup>
)}
/>
</FormGroup>
)}
<ActionGroup> <ActionGroup>
<Button <Button
data-testid={!user?.id ? "create-user" : "save-user"} data-testid={!user?.id ? "create-user" : "save-user"}
isDisabled={ isDisabled={
!user?.id && !user?.id &&
!watchUsernameInput && !watchUsernameInput &&
realm.registrationEmailAsUsername === false realm.registrationEmailAsUsername === false
} }
variant="primary" variant="primary"
type="submit" type="submit"
> >
{user?.id ? t("save") : t("create")} {user?.id ? t("save") : t("create")}
</Button> </Button>
<Button <Button
data-testid="cancel-create-user" data-testid="cancel-create-user"
variant="link" variant="link"
onClick={user?.id ? () => reset(toUserFormFields(user)) : undefined} onClick={user?.id ? () => reset(toUserFormFields(user)) : undefined}
component={ component={
!user?.id !user?.id
? (props) => ( ? (props) => (
<Link {...props} to={toUsers({ realm: realm.realm! })} /> <Link {...props} to={toUsers({ realm: realm.realm! })} />
) )
: undefined : undefined
} }
> >
{user?.id ? t("revert") : t("cancel")} {user?.id ? t("revert") : t("cancel")}
</Button> </Button>
</ActionGroup> </ActionGroup>
</FormProvider>
</FormAccess> </FormAccess>
); );
}; };

View file

@ -7,12 +7,11 @@ import {
FormGroup, FormGroup,
Modal, Modal,
ModalVariant, ModalVariant,
ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { capitalize } from "lodash-es"; import { capitalize } from "lodash-es";
import { useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TextControl } from "ui-shared";
import { adminClient } from "../admin-client"; import { adminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
@ -32,13 +31,13 @@ export const UserIdpModal = ({
}: UserIdpModalProps) => { }: UserIdpModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { const form = useForm<FederatedIdentityRepresentation>({
register,
handleSubmit,
formState: { isValid, errors },
} = useForm<FederatedIdentityRepresentation>({
mode: "onChange", mode: "onChange",
}); });
const {
handleSubmit,
formState: { isValid },
} = form;
const onSubmit = async ( const onSubmit = async (
federatedIdentity: FederatedIdentityRepresentation, federatedIdentity: FederatedIdentityRepresentation,
@ -87,55 +86,33 @@ export const UserIdpModal = ({
isOpen isOpen
> >
<Form id="group-form" onSubmit={handleSubmit(onSubmit)}> <Form id="group-form" onSubmit={handleSubmit(onSubmit)}>
<FormGroup label={t("identityProvider")} fieldId="identityProvider"> <FormProvider {...form}>
<KeycloakTextInput <FormGroup label={t("identityProvider")} fieldId="identityProvider">
id="identityProvider" <KeycloakTextInput
data-testid="idpNameInput" id="identityProvider"
value={capitalize(federatedId)} data-testid="idpNameInput"
isReadOnly value={capitalize(federatedId)}
/> readOnly
</FormGroup> />
<FormGroup </FormGroup>
label={t("userID")} <TextControl
fieldId="userID" name="userId"
helperText={t("userIdHelperText")} label={t("userID")}
helperTextInvalid={t("required")} helperText={t("userIdHelperText")}
validated={
errors.userId ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<KeycloakTextInput
id="userID"
data-testid="userIdInput"
validated={
errors.userId ? ValidatedOptions.error : ValidatedOptions.default
}
autoFocus autoFocus
{...register("userId", { required: true })} rules={{
required: t("required"),
}}
/> />
</FormGroup> <TextControl
<FormGroup name="userName"
label={t("username")} label={t("username")}
fieldId="username" helperText={t("usernameHelperText")}
helperText={t("usernameHelperText")} rules={{
helperTextInvalid={t("required")} required: t("required"),
validated={ }}
errors.userName ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<KeycloakTextInput
id="username"
data-testid="usernameInput"
validated={
errors.userName
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("userName", { required: true })}
/> />
</FormGroup> </FormProvider>
</Form> </Form>
</Modal> </Modal>
); );

View file

@ -1,37 +1,20 @@
import { FormGroup } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TimeSelectorControl } from "../../components/time-selector/TimeSelectorControl";
import { HelpItem } from "ui-shared";
import { TimeSelector } from "../../components/time-selector/TimeSelector";
import { credResetFormDefaultValues } from "./ResetCredentialDialog"; import { credResetFormDefaultValues } from "./ResetCredentialDialog";
export const LifespanField = () => { export const LifespanField = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { control } = useFormContext();
return ( return (
<FormGroup <TimeSelectorControl
fieldId="lifespan" name="lifespan"
label={t("lifespan")} label={t("lifespan")}
isStack labelIcon={t("lifespanHelp")}
labelIcon={ units={["minute", "hour", "day"]}
<HelpItem helpText={t("lifespanHelp")} fieldLabelId="lifespan" /> menuAppendTo="parent"
} controller={{
> defaultValue: credResetFormDefaultValues.lifespan,
<Controller }}
name="lifespan" />
defaultValue={credResetFormDefaultValues.lifespan}
control={control}
render={({ field }) => (
<TimeSelector
value={field.value}
units={["minute", "hour", "day"]}
onChange={field.onChange}
menuAppendTo="parent"
/>
)}
/>
</FormGroup>
); );
}; };

View file

@ -1,20 +1,9 @@
import type RequiredActionProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation"; import type RequiredActionProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation";
import { import { SelectVariant } from "@patternfly/react-core";
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useState } from "react"; import { useState } from "react";
import { import { FieldPathByValue, FieldValues } from "react-hook-form";
Control,
Controller,
FieldPathByValue,
FieldValues,
PathValue,
} from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared"; import { SelectControl } from "ui-shared";
import { adminClient } from "../../admin-client"; import { adminClient } from "../../admin-client";
import { useFetch } from "../../utils/useFetch"; import { useFetch } from "../../utils/useFetch";
@ -23,7 +12,6 @@ export type RequiredActionMultiSelectProps<
T extends FieldValues, T extends FieldValues,
P extends FieldPathByValue<T, string[] | undefined>, P extends FieldPathByValue<T, string[] | undefined>,
> = { > = {
control: Control<T>;
name: P; name: P;
label: string; label: string;
help: string; help: string;
@ -33,13 +21,11 @@ export const RequiredActionMultiSelect = <
T extends FieldValues, T extends FieldValues,
P extends FieldPathByValue<T, string[] | undefined>, P extends FieldPathByValue<T, string[] | undefined>,
>({ >({
control,
name, name,
label, label,
help, help,
}: RequiredActionMultiSelectProps<T, P>) => { }: RequiredActionMultiSelectProps<T, P>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [requiredActions, setRequiredActions] = useState< const [requiredActions, setRequiredActions] = useState<
RequiredActionProviderRepresentation[] RequiredActionProviderRepresentation[]
>([]); >([]);
@ -56,54 +42,23 @@ export const RequiredActionMultiSelect = <
); );
return ( return (
<FormGroup <SelectControl
name={name}
label={t(label)} label={t(label)}
labelIcon={<HelpItem helpText={t(help)} fieldLabelId="resetAction" />} labelIcon={t(help)}
fieldId="actions" controller={{ defaultValue: [] }}
> maxHeight={375}
<Controller variant={SelectVariant.typeaheadMulti}
name={name} chipGroupProps={{
defaultValue={[] as PathValue<T, P>} numChips: 3,
control={control} }}
render={({ field }) => ( placeholderText={t("requiredActionPlaceholder")}
<Select menuAppendTo="parent"
maxHeight={375} typeAheadAriaLabel={t("resetAction")}
toggleId={`${name}-actions`} options={requiredActions.map(({ alias, name }) => ({
variant={SelectVariant.typeaheadMulti} key: alias!,
chipGroupProps={{ value: name || alias!,
numChips: 3, }))}
}} />
placeholderText={t("requiredActionPlaceholder")}
menuAppendTo="parent"
onToggle={(open) => setOpen(open)}
isOpen={open}
selections={field.value as string[]}
onSelect={(_, selectedValue) => {
const value: string[] = field.value;
field.onChange(
value.find((item) => item === selectedValue)
? value.filter((item) => item !== selectedValue)
: [...value, selectedValue],
);
}}
onClear={(event) => {
event.stopPropagation();
field.onChange([]);
}}
typeAheadAriaLabel={t("resetAction")}
>
{requiredActions.map(({ alias, name }) => (
<SelectOption
key={alias}
value={alias}
data-testid={`${alias}-option`}
>
{name}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
); );
}; };

View file

@ -82,13 +82,12 @@ export const ResetCredentialDialog = ({
isHorizontal isHorizontal
data-testid="credential-reset-modal" data-testid="credential-reset-modal"
> >
<RequiredActionMultiSelect
control={control}
name="actions"
label="resetAction"
help="resetActions"
/>
<FormProvider {...form}> <FormProvider {...form}>
<RequiredActionMultiSelect
name="actions"
label="resetAction"
help="resetActions"
/>
<LifespanField /> <LifespanField />
</FormProvider> </FormProvider>
</Form> </Form>

View file

@ -1,18 +1,16 @@
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation"; import { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { import {
AlertVariant, AlertVariant,
ButtonVariant, ButtonVariant,
Form, Form,
FormGroup, FormGroup,
Switch,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { Controller, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";
import { adminClient } from "../../admin-client"; import { adminClient } from "../../admin-client";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { import {
ConfirmDialogModal, ConfirmDialogModal,
@ -49,18 +47,18 @@ export const ResetPasswordDialog = ({
onClose, onClose,
}: ResetPasswordDialogProps) => { }: ResetPasswordDialogProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useForm<CredentialsForm>({
defaultValues: credFormDefaultValues,
mode: "onChange",
});
const { const {
register, register,
control,
formState: { isValid, errors }, formState: { isValid, errors },
watch, watch,
handleSubmit, handleSubmit,
clearErrors, clearErrors,
setError, setError,
} = useForm<CredentialsForm>({ } = form;
defaultValues: credFormDefaultValues,
mode: "onChange",
});
const [confirm, toggle] = useToggle(true); const [confirm, toggle] = useToggle(true);
const password = watch("password", ""); const password = watch("password", "");
@ -201,32 +199,14 @@ export const ResetPasswordDialog = ({
})} })}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormProvider {...form}>
label={t("temporaryPassword")} <DefaultSwitchControl
labelIcon={
<HelpItem
helpText={t("temporaryPasswordHelpText")}
fieldLabelId="temporaryPassword"
/>
}
fieldId="kc-temporaryPassword"
>
<Controller
name="temporaryPassword" name="temporaryPassword"
defaultValue={true} label={t("temporaryPassword")}
control={control} labelIcon={t("temporaryPasswordHelpText")}
render={({ field }) => ( defaultValue="true"
<Switch
className="kc-temporaryPassword"
onChange={field.onChange}
isChecked={field.value}
label={t("on")}
labelOff={t("off")}
aria-label={t("temporaryPassword")}
/>
)}
/> />
</FormGroup> </FormProvider>
</Form> </Form>
</ConfirmDialogModal> </ConfirmDialogModal>
</> </>

View file

@ -18,6 +18,7 @@ export type TextControlProps<
label: string; label: string;
labelIcon?: string; labelIcon?: string;
isDisabled?: boolean; isDisabled?: boolean;
helperText?: string;
}; };
export const TextControl = < export const TextControl = <
@ -42,6 +43,7 @@ export const TextControl = <
labelIcon={labelIcon} labelIcon={labelIcon}
isRequired={required} isRequired={required}
error={fieldState.error} error={fieldState.error}
helperText={props.helperText}
> >
<KeycloakTextInput <KeycloakTextInput
isRequired={required} isRequired={required}