Migrate authenticate settings and policy detail to react-hook-form v7 (#3984)

This commit is contained in:
Erik Jan de Wit 2023-01-16 12:34:18 +01:00 committed by GitHub
parent 498034ee2e
commit e0246c70d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 250 additions and 99 deletions

View file

@ -3,7 +3,7 @@ import { ActionGroup, ActionGroupProps, Button } from "@patternfly/react-core";
type SaveResetProps = ActionGroupProps & { type SaveResetProps = ActionGroupProps & {
name: string; name: string;
save: () => void; save?: () => void;
reset: () => void; reset: () => void;
isActive?: boolean; isActive?: boolean;
}; };
@ -18,7 +18,12 @@ export const SaveReset = ({
const { t } = useTranslation("common"); const { t } = useTranslation("common");
return ( return (
<ActionGroup {...rest}> <ActionGroup {...rest}>
<Button isDisabled={!isActive} data-testid={name + "Save"} onClick={save}> <Button
isDisabled={!isActive}
data-testid={name + "Save"}
onClick={save}
type={save ? "button" : "submit"}
>
{t("save")} {t("save")}
</Button> </Button>
<Button <Button

View file

@ -10,7 +10,7 @@ import {
Switch, Switch,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useState } from "react"; import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form-v7"; import { Controller, FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
@ -213,7 +213,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
control={control} control={control}
defaultValue={[]} defaultValue={[]}
rules={{ validate: (value) => (value || "").length > 0 }} rules={{ validate: (value) => (value || "").length > 0 }}
render={({ field }) => ( render={(field) => (
<Select <Select
placeholderText={t("selectARole")} placeholderText={t("selectARole")}
variant={SelectVariant.typeaheadMulti} variant={SelectVariant.typeaheadMulti}
@ -314,7 +314,8 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
<KeycloakTextInput <KeycloakTextInput
id="alias" id="alias"
data-testid="alias" data-testid="alias"
{...register("alias", { required: true })} name="alias"
ref={register({ required: true })}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
@ -331,7 +332,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
name="authScopes" name="authScopes"
defaultValue={[]} defaultValue={[]}
control={control} control={control}
render={({ field }) => ( render={(field) => (
<Select <Select
toggleId="authScopes" toggleId="authScopes"
onToggle={setScopesDropdownOpen} onToggle={setScopesDropdownOpen}

View file

@ -1,5 +1,5 @@
import { FormGroup, Radio } from "@patternfly/react-core"; import { FormGroup, Radio } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
@ -35,7 +35,7 @@ export const DecisionStrategySelect = ({
data-testid="decisionStrategy" data-testid="decisionStrategy"
defaultValue={DECISION_STRATEGY[0]} defaultValue={DECISION_STRATEGY[0]}
control={control} control={control}
render={(field) => ( render={({ field }) => (
<> <>
{(isLimited {(isLimited
? DECISION_STRATEGY.slice(0, 2) ? DECISION_STRATEGY.slice(0, 2)

View file

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form"; import { Controller, FormProvider, useForm } from "react-hook-form-v7";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -20,7 +20,7 @@ import { SaveReset } from "../advanced/SaveReset";
import { ImportDialog } from "./ImportDialog"; import { ImportDialog } from "./ImportDialog";
import useToggle from "../../utils/useToggle"; import useToggle from "../../utils/useToggle";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { DecisionStrategySelect } from "./DecisionStragegySelect"; import { DecisionStrategySelect } from "./DecisionStrategySelect";
const POLICY_ENFORCEMENT_MODES = [ const POLICY_ENFORCEMENT_MODES = [
"ENFORCING", "ENFORCING",
@ -28,12 +28,17 @@ const POLICY_ENFORCEMENT_MODES = [
"DISABLED", "DISABLED",
] as const; ] as const;
export type FormFields = Omit<
ResourceServerRepresentation,
"scopes" | "resources"
>;
export const AuthorizationSettings = ({ clientId }: { clientId: string }) => { export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const [resource, setResource] = useState<ResourceServerRepresentation>(); const [resource, setResource] = useState<ResourceServerRepresentation>();
const [importDialog, toggleImportDialog] = useToggle(); const [importDialog, toggleImportDialog] = useToggle();
const form = useForm<ResourceServerRepresentation>({}); const form = useForm<FormFields>({});
const { control, reset, handleSubmit } = form; const { control, reset, handleSubmit } = form;
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
@ -58,7 +63,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
} }
}; };
const save = async (resource: ResourceServerRepresentation) => { const onSubmit = async (resource: ResourceServerRepresentation) => {
try { try {
await adminClient.clients.updateResourceServer( await adminClient.clients.updateResourceServer(
{ id: clientId }, { id: clientId },
@ -82,7 +87,11 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
closeDialog={toggleImportDialog} closeDialog={toggleImportDialog}
/> />
)} )}
<FormAccess role="view-clients" isHorizontal> <FormAccess
role="view-clients"
isHorizontal
onSubmit={handleSubmit(onSubmit)}
>
<FormGroup <FormGroup
label={t("import")} label={t("import")}
fieldId="import" fieldId="import"
@ -114,16 +123,16 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
data-testid="policyEnforcementMode" data-testid="policyEnforcementMode"
defaultValue={POLICY_ENFORCEMENT_MODES[0]} defaultValue={POLICY_ENFORCEMENT_MODES[0]}
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<> <>
{POLICY_ENFORCEMENT_MODES.map((mode) => ( {POLICY_ENFORCEMENT_MODES.map((mode) => (
<Radio <Radio
id={mode} id={mode}
key={mode} key={mode}
data-testid={mode} data-testid={mode}
isChecked={value === mode} isChecked={field.value === mode}
name="policyEnforcementMode" name="policyEnforcementMode"
onChange={() => onChange(mode)} onChange={() => field.onChange(mode)}
label={t(`policyEnforcementModes.${mode}`)} label={t(`policyEnforcementModes.${mode}`)}
className="pf-u-mb-md" className="pf-u-mb-md"
/> />
@ -151,13 +160,13 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
data-testid="allowRemoteResourceManagement" data-testid="allowRemoteResourceManagement"
defaultValue={false} defaultValue={false}
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<Switch <Switch
id="allowRemoteResourceManagement" id="allowRemoteResourceManagement"
label={t("common:on")} label={t("common:on")}
labelOff={t("common:off")} labelOff={t("common:off")}
isChecked={value} isChecked={field.value}
onChange={onChange} onChange={field.onChange}
aria-label={t("allowRemoteResourceManagement")} aria-label={t("allowRemoteResourceManagement")}
/> />
)} )}
@ -165,7 +174,6 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
</FormGroup> </FormGroup>
<SaveReset <SaveReset
name="authenticationSettings" name="authenticationSettings"
save={() => handleSubmit(save)()}
reset={() => reset(resource)} reset={() => reset(resource)}
isActive isActive
/> />

View file

@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { HelpItem } from "../../../components/help-enabler/HelpItem"; import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { useParams } from "../../../utils/useParams"; import { useParams } from "../../../utils/useParams";
import type { PolicyDetailsParams } from "../../routes/PolicyDetails"; import type { PolicyDetailsParams } from "../../routes/PolicyDetails";
import { DecisionStrategySelect } from "../DecisionStragegySelect"; import { DecisionStrategySelect } from "../DecisionStrategySelect";
import { ResourcesPolicySelect } from "../ResourcesPolicySelect"; import { ResourcesPolicySelect } from "../ResourcesPolicySelect";
export const Aggregate = () => { export const Aggregate = () => {

View file

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form-v7";
import { import {
SelectOption, SelectOption,
FormGroup, FormGroup,
@ -84,23 +84,25 @@ export const Client = () => {
rules={{ rules={{
validate: (value) => value.length > 0, validate: (value) => value.length > 0,
}} }}
render={({ onChange, value }) => ( render={({ field }) => (
<Select <Select
toggleId="clients" toggleId="clients"
variant={SelectVariant.typeaheadMulti} variant={SelectVariant.typeaheadMulti}
onToggle={(open) => setOpen(open)} onToggle={(open) => setOpen(open)}
isOpen={open} isOpen={open}
selections={value} selections={field.value}
onFilter={(_, value) => { onFilter={(_, value) => {
setSearch(value); setSearch(value);
return convert(clients); return convert(clients);
}} }}
onSelect={(_, v) => { onSelect={(_, v) => {
const option = v.toString(); const option = v.toString();
if (value.includes(option)) { if (field.value.includes(option)) {
onChange(value.filter((item: string) => item !== option)); field.onChange(
field.value.filter((item: string) => item !== option)
);
} else { } else {
onChange([...value, option]); field.onChange([...field.value, option]);
} }
setOpen(false); setOpen(false);
}} }}

View file

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFormContext, Controller } from "react-hook-form"; import { useFormContext, Controller } from "react-hook-form-v7";
import { FormGroup, Button, Checkbox } from "@patternfly/react-core"; import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
import { MinusCircleIcon } from "@patternfly/react-icons"; import { MinusCircleIcon } from "@patternfly/react-icons";
import { import {
@ -76,13 +76,15 @@ export const ClientScope = () => {
validate: (value: RequiredIdValue[]) => validate: (value: RequiredIdValue[]) =>
value.filter((c) => c.id).length > 0, value.filter((c) => c.id).length > 0,
}} }}
render={({ onChange, value }) => ( render={({ field }) => (
<> <>
{open && ( {open && (
<AddScopeDialog <AddScopeDialog
clientScopes={scopes.filter( clientScopes={scopes.filter(
(scope) => (scope) =>
!value.map((c: RequiredIdValue) => c.id).includes(scope.id!) !field.value
.map((c: RequiredIdValue) => c.id)
.includes(scope.id!)
)} )}
isClientScopesConditionType isClientScopesConditionType
open={open} open={open}
@ -92,8 +94,8 @@ export const ClientScope = () => {
...selectedScopes, ...selectedScopes,
...scopes.map((s) => s.scope), ...scopes.map((s) => s.scope),
]); ]);
onChange([ field.onChange([
...value, ...field.value,
...scopes ...scopes
.map((scope) => scope.scope) .map((scope) => scope.scope)
.map((item) => ({ id: item.id!, required: false })), .map((item) => ({ id: item.id!, required: false })),
@ -128,16 +130,16 @@ export const ClientScope = () => {
<Td>{scope.name}</Td> <Td>{scope.name}</Td>
<Td> <Td>
<Controller <Controller
name={`clientScopes[${index}].required`} name={`clientScopes.${index}.required`}
defaultValue={false} defaultValue={false}
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<Checkbox <Checkbox
id="required" id="required"
data-testid="standard" data-testid="standard"
name="required" name="required"
isChecked={value} isChecked={field.value}
onChange={onChange} onChange={field.onChange}
/> />
)} )}
/> />

View file

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFormContext, Controller } from "react-hook-form"; import { useFormContext, Controller } from "react-hook-form-v7";
import { MinusCircleIcon } from "@patternfly/react-icons"; import { MinusCircleIcon } from "@patternfly/react-icons";
import { FormGroup, Button, Checkbox } from "@patternfly/react-core"; import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
import { import {
@ -18,6 +18,11 @@ import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { GroupPickerDialog } from "../../../components/group/GroupPickerDialog"; import { GroupPickerDialog } from "../../../components/group/GroupPickerDialog";
import { KeycloakTextInput } from "../../../components/keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../../../components/keycloak-text-input/KeycloakTextInput";
type GroupForm = {
groups?: GroupValue[];
groupsClaim: string;
};
export type GroupValue = { export type GroupValue = {
id: string; id: string;
extendChildren: boolean; extendChildren: boolean;
@ -31,9 +36,7 @@ export const Group = () => {
getValues, getValues,
setValue, setValue,
formState: { errors }, formState: { errors },
} = useFormContext<{ } = useFormContext<GroupForm>();
groups?: GroupValue[];
}>();
const values = getValues("groups"); const values = getValues("groups");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -73,9 +76,8 @@ export const Group = () => {
<KeycloakTextInput <KeycloakTextInput
type="text" type="text"
id="groupsClaim" id="groupsClaim"
name="groupsClaim"
data-testid="groupsClaim" data-testid="groupsClaim"
ref={register} {...register("groupsClaim")}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
@ -96,10 +98,10 @@ export const Group = () => {
control={control} control={control}
defaultValue={[]} defaultValue={[]}
rules={{ rules={{
validate: (value: GroupValue[]) => validate: (value?: GroupValue[]) =>
value.filter(({ id }) => id).length > 0, value && value.filter(({ id }) => id).length > 0,
}} }}
render={({ onChange, value }) => ( render={({ field }) => (
<> <>
{open && ( {open && (
<GroupPickerDialog <GroupPickerDialog
@ -109,8 +111,8 @@ export const Group = () => {
ok: "common:add", ok: "common:add",
}} }}
onConfirm={(groups) => { onConfirm={(groups) => {
onChange([ field.onChange([
...value, ...(field.value || []),
...(groups || []).map(({ id }) => ({ id })), ...(groups || []).map(({ id }) => ({ id })),
]); ]);
setSelectedGroups([...selectedGroups, ...(groups || [])]); setSelectedGroups([...selectedGroups, ...(groups || [])]);
@ -149,16 +151,16 @@ export const Group = () => {
<Td>{group.path}</Td> <Td>{group.path}</Td>
<Td> <Td>
<Controller <Controller
name={`groups[${index}].extendChildren`} name={`groups.${index}.extendChildren`}
defaultValue={false} defaultValue={false}
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<Checkbox <Checkbox
id="extendChildren" id="extendChildren"
data-testid="standard" data-testid="standard"
name="extendChildren" name="extendChildren"
isChecked={value} isChecked={field.value}
onChange={onChange} onChange={field.onChange}
isDisabled={group.subGroups?.length === 0} isDisabled={group.subGroups?.length === 0}
/> />
)} )}

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form-v7";
import { FormGroup } from "@patternfly/react-core"; import { FormGroup } from "@patternfly/react-core";
import { CodeEditor, Language } from "@patternfly/react-code-editor"; import { CodeEditor, Language } from "@patternfly/react-code-editor";
@ -25,13 +25,12 @@ export const JavaScript = () => {
name="code" name="code"
defaultValue="" defaultValue=""
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<CodeEditor <CodeEditor
id="code" id="code"
data-testid="code" data-testid="code"
type="text" onChange={field.onChange}
onChange={onChange} code={field.value}
code={value}
height="600px" height="600px"
language={Language.javascript} language={Language.javascript}
/> />

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form-v7";
import { FormGroup, Radio } from "@patternfly/react-core"; import { FormGroup, Radio } from "@patternfly/react-core";
import { HelpItem } from "../../../components/help-enabler/HelpItem"; import { HelpItem } from "../../../components/help-enabler/HelpItem";
@ -24,16 +24,16 @@ export const LogicSelector = () => {
data-testid="logic" data-testid="logic"
defaultValue={LOGIC_TYPES[0]} defaultValue={LOGIC_TYPES[0]}
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<> <>
{LOGIC_TYPES.map((type) => ( {LOGIC_TYPES.map((type) => (
<Radio <Radio
id={type} id={type}
key={type} key={type}
data-testid={type} data-testid={type}
isChecked={value === type} isChecked={field.value === type}
name="logic" name="logic"
onChange={() => onChange(type)} onChange={() => field.onChange(type)}
label={t(`logicType.${type.toLowerCase()}`)} label={t(`logicType.${type.toLowerCase()}`)}
className="pf-u-mb-md" className="pf-u-mb-md"
/> />

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form-v7";
import { FormGroup, ValidatedOptions } from "@patternfly/react-core"; import { FormGroup, ValidatedOptions } from "@patternfly/react-core";
import { HelpItem } from "../../../components/help-enabler/HelpItem"; import { HelpItem } from "../../../components/help-enabler/HelpItem";
@ -35,11 +35,9 @@ export const NameDescription = ({ prefix }: NameDescriptionProps) => {
} }
> >
<KeycloakTextInput <KeycloakTextInput
type="text"
id="kc-name" id="kc-name"
name="name"
data-testid="name" data-testid="name"
ref={register({ required: true })} {...register("name", { required: true })}
validated={ validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default errors.name ? ValidatedOptions.error : ValidatedOptions.default
} }
@ -60,15 +58,13 @@ export const NameDescription = ({ prefix }: NameDescriptionProps) => {
helperTextInvalid={errors.description?.message} helperTextInvalid={errors.description?.message}
> >
<KeycloakTextArea <KeycloakTextArea
ref={register({ {...register("description", {
maxLength: { maxLength: {
value: 255, value: 255,
message: t("common:maxLength", { length: 255 }), message: t("common:maxLength", { length: 255 }),
}, },
})} })}
type="text"
id="kc-description" id="kc-description"
name="description"
data-testid="description" data-testid="description"
validated={ validated={
errors.description errors.description

View file

@ -8,7 +8,7 @@ import {
PageSection, PageSection,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { FunctionComponent, useState } from "react"; import { FunctionComponent, useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form-v7";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom-v5-compat"; import { Link, useNavigate } from "react-router-dom-v5-compat";
@ -105,7 +105,7 @@ export default function PolicyDetails() {
[] []
); );
const save = async (policy: Policy) => { const onSubmit = async (policy: Policy) => {
// remove entries that only have the boolean set and no id // remove entries that only have the boolean set and no id
policy.groups = policy.groups?.filter((g) => g.id); policy.groups = policy.groups?.filter((g) => g.id);
policy.clientScopes = policy.clientScopes?.filter((c) => c.id); policy.clientScopes = policy.clientScopes?.filter((c) => c.id);
@ -194,7 +194,7 @@ export default function PolicyDetails() {
<PageSection variant="light"> <PageSection variant="light">
<FormAccess <FormAccess
isHorizontal isHorizontal
onSubmit={handleSubmit(save)} onSubmit={handleSubmit(onSubmit)}
role="view-clients" role="view-clients"
> >
<FormProvider {...form}> <FormProvider {...form}>

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form-v7";
import { FormGroup } from "@patternfly/react-core"; import { FormGroup } from "@patternfly/react-core";
import { HelpItem } from "../../../components/help-enabler/HelpItem"; import { HelpItem } from "../../../components/help-enabler/HelpItem";
@ -28,11 +28,9 @@ export const Regex = () => {
} }
> >
<KeycloakTextInput <KeycloakTextInput
type="text"
id="targetClaim" id="targetClaim"
name="targetClaim"
data-testid="targetClaim" data-testid="targetClaim"
ref={register({ required: true })} {...register("targetClaim", { required: true })}
validated={errors.targetClaim ? "error" : "default"} validated={errors.targetClaim ? "error" : "default"}
/> />
</FormGroup> </FormGroup>
@ -50,10 +48,8 @@ export const Regex = () => {
helperTextInvalid={t("common:required")} helperTextInvalid={t("common:required")}
> >
<KeycloakTextInput <KeycloakTextInput
ref={register({ required: true })} {...register("pattern", { required: true })}
type="text"
id="pattern" id="pattern"
name="pattern"
data-testid="regexPattern" data-testid="regexPattern"
validated={errors.pattern ? "error" : "default"} validated={errors.pattern ? "error" : "default"}
/> />

View file

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFormContext, Controller } from "react-hook-form"; import { useFormContext, Controller } from "react-hook-form-v7";
import { FormGroup, Button, Checkbox } from "@patternfly/react-core"; import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
import { MinusCircleIcon } from "@patternfly/react-icons"; import { MinusCircleIcon } from "@patternfly/react-icons";
import { import {
@ -77,18 +77,18 @@ export const Role = () => {
control={control} control={control}
defaultValue={[]} defaultValue={[]}
rules={{ rules={{
validate: (value: RequiredIdValue[]) => validate: (value?: RequiredIdValue[]) =>
value.filter((c) => c.id).length > 0, value && value.filter((c) => c.id).length > 0,
}} }}
render={({ onChange, value }) => ( render={({ field }) => (
<> <>
{open && ( {open && (
<AddRoleMappingModal <AddRoleMappingModal
id="role" id="role"
type="roles" type="roles"
onAssign={(rows) => { onAssign={(rows) => {
onChange([ field.onChange([
...value, ...(field.value || []),
...rows.map((row) => ({ id: row.role.id })), ...rows.map((row) => ({ id: row.role.id })),
]); ]);
setSelectedRoles([...selectedRoles, ...rows]); setSelectedRoles([...selectedRoles, ...rows]);
@ -129,16 +129,16 @@ export const Role = () => {
</Td> </Td>
<Td> <Td>
<Controller <Controller
name={`roles[${index}].required`} name={`roles.${index}.required`}
defaultValue={false} defaultValue={false}
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<Checkbox <Checkbox
id="required" id="required"
data-testid="standard" data-testid="standard"
name="required" name="required"
isChecked={value} isChecked={field.value}
onChange={onChange} onChange={field.onChange}
/> />
)} )}
/> />

View file

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form-v7";
import { import {
DatePicker, DatePicker,
Flex, Flex,
@ -59,15 +59,20 @@ const DateTime = ({ name }: { name: string }) => {
defaultValue="" defaultValue=""
control={control} control={control}
rules={{ required: true }} rules={{ required: true }}
render={({ onChange, value }) => { render={({ field }) => {
const dateTime = value.match(DATE_TIME_FORMAT) || ["", "", "0", "00"]; const dateTime = field.value.match(DATE_TIME_FORMAT) || [
"",
"",
"0",
"00",
];
return ( return (
<Split hasGutter id={name}> <Split hasGutter id={name}>
<SplitItem> <SplitItem>
<DatePicker <DatePicker
value={dateTime[1]} value={dateTime[1]}
onChange={(_, date) => { onChange={(_, date) => {
onChange(parseDate(value, date)); field.onChange(parseDate(field.value, date));
}} }}
/> />
</SplitItem> </SplitItem>
@ -75,7 +80,7 @@ const DateTime = ({ name }: { name: string }) => {
<TimePicker <TimePicker
time={`${dateTime[2]}:${dateTime[3]}`} time={`${dateTime[2]}:${dateTime[3]}`}
onChange={(_, hour, minute) => onChange={(_, hour, minute) =>
onChange(parseTime(value, hour, minute)) field.onChange(parseTime(field.value, hour, minute))
} }
is24Hour is24Hour
/> />
@ -102,17 +107,17 @@ const NumberControl = ({ name, min, max }: NumberControlProps) => {
name={name} name={name}
defaultValue="" defaultValue=""
control={control} control={control}
render={({ onChange, value }) => ( render={({ field }) => (
<NumberInput <NumberInput
id={name} id={name}
value={value} value={field.value}
min={min} min={min}
max={max} max={max}
onPlus={() => onChange(Number(value) + 1)} onPlus={() => field.onChange(Number(field.value) + 1)}
onMinus={() => onChange(Number(value) - 1)} onMinus={() => field.onChange(Number(field.value) - 1)}
onChange={(event) => { onChange={(event) => {
const newValue = Number(event.currentTarget.value); const newValue = Number(event.currentTarget.value);
onChange(setValue(!isNaN(newValue) ? newValue : 0)); field.onChange(setValue(!isNaN(newValue) ? newValue : 0));
}} }}
/> />
)} )}
@ -149,7 +154,10 @@ const FromTo = ({ name, ...rest }: NumberControlProps) => {
export const Time = () => { export const Time = () => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const { getValues, errors } = useFormContext(); const {
getValues,
formState: { errors },
} = useFormContext();
const [repeat, setRepeat] = useState(getValues("month")); const [repeat, setRepeat] = useState(getValues("month"));
return ( return (
<> <>

View file

@ -1,4 +1,4 @@
import { UserSelect } from "../../../components/users/UserSelect"; import { UserSelect } from "../../../components/users/hook-form-v7/UserSelect";
export const User = () => ( export const User = () => (
<UserSelect <UserSelect

View file

@ -0,0 +1,132 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form-v7";
import {
SelectOption,
FormGroup,
Select,
SelectVariant,
} from "@patternfly/react-core";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import type { UserQuery } from "@keycloak/keycloak-admin-client/lib/resources/users";
import type { ComponentProps } from "../../dynamic/components";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { HelpItem } from "../../help-enabler/HelpItem";
import useToggle from "../../../utils/useToggle";
type UserSelectProps = ComponentProps & {
variant?: SelectVariant;
isRequired?: boolean;
};
export const UserSelect = ({
name,
label,
helpText,
defaultValue,
isRequired,
variant = SelectVariant.typeaheadMulti,
}: UserSelectProps) => {
const { t } = useTranslation("clients");
const {
control,
getValues,
formState: { errors },
} = useFormContext();
const values: string[] | undefined = getValues(name!);
const [open, toggleOpen] = useToggle();
const [users, setUsers] = useState<(UserRepresentation | undefined)[]>([]);
const [search, setSearch] = useState("");
const { adminClient } = useAdminClient();
useFetch(
() => {
const params: UserQuery = {
max: 20,
};
if (search) {
params.username = search;
}
if (values?.length && !search) {
return Promise.all(
values.map((id: string) => adminClient.users.findOne({ id }))
);
}
return adminClient.users.find(params);
},
setUsers,
[search]
);
const convert = (clients: (UserRepresentation | undefined)[]) =>
clients
.filter((c) => c !== undefined)
.map((option) => (
<SelectOption
key={option!.id}
value={option!.id}
selected={values?.includes(option!.id!)}
>
{option!.username}
</SelectOption>
));
return (
<FormGroup
label={t(label!)}
isRequired={isRequired}
labelIcon={
<HelpItem helpText={helpText!} fieldLabelId={`clients:${label}`} />
}
fieldId={name!}
validated={errors[name!] ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<Controller
name={name!}
defaultValue={defaultValue}
control={control}
rules={
isRequired && variant === SelectVariant.typeaheadMulti
? { validate: (value) => value.length > 0 }
: isRequired
? { required: true }
: {}
}
render={({ field }) => (
<Select
toggleId={name!}
variant={variant}
placeholderText={t("selectAUser")}
onToggle={toggleOpen}
isOpen={open}
selections={field.value}
onFilter={(_, value) => {
setSearch(value);
return convert(users);
}}
onSelect={(_, v) => {
const option = v.toString();
if (field.value.includes(option)) {
field.onChange(
field.value.filter((item: string) => item !== option)
);
} else {
field.onChange([...field.value, option]);
}
toggleOpen();
}}
aria-label={t(name!)}
>
{convert(users)}
</Select>
)}
/>
</FormGroup>
);
};