Change client description component to use utils (#19165)

This commit is contained in:
Erik Jan de Wit 2023-03-24 16:53:43 +01:00 committed by GitHub
parent 632d315c94
commit d29f3e4dfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 247 additions and 128 deletions

View file

@ -437,7 +437,7 @@ describe("Clients test", () => {
it("Should fail to create imported client with empty ID", () => { it("Should fail to create imported client with empty ID", () => {
commonPage.sidebar().goToClients(); commonPage.sidebar().goToClients();
cy.findByTestId("importClient").click(); cy.findByTestId("importClient").click();
cy.findByTestId("kc-client-id").click(); cy.findByTestId("clientId").click();
cy.findByText("Save").click(); cy.findByText("Save").click();
cy.findByText("Required field"); cy.findByText("Required field");
}); });
@ -463,7 +463,7 @@ describe("Clients test", () => {
cy.wait(1000); cy.wait(1000);
//cy.findByTestId("realm-file").contains('"clientId": "identical"') //cy.findByTestId("realm-file").contains('"clientId": "identical"')
cy.findByTestId("kc-client-id").click(); cy.findByTestId("clientId").click();
cy.findByText("Save").click(); cy.findByText("Save").click();
commonPage commonPage
.masthead() .masthead()

View file

@ -3,9 +3,9 @@ import CommonPage from "../../../CommonPage";
export default class CreateClientPage extends CommonPage { export default class CreateClientPage extends CommonPage {
private clientTypeDrpDwn = ".pf-c-select__toggle"; private clientTypeDrpDwn = ".pf-c-select__toggle";
private clientTypeList = ".pf-c-select__toggle + ul"; private clientTypeList = ".pf-c-select__toggle + ul";
private clientIdInput = "#kc-client-id"; private clientIdInput = "#clientId";
private clientIdError = "#kc-client-id + div"; private clientIdError = "#clientId + div";
private clientNameInput = "#kc-name"; private clientNameInput = "#name";
private clientDescriptionInput = "#kc-description"; private clientDescriptionInput = "#kc-description";
private alwaysDisplayInUISwitch = private alwaysDisplayInUISwitch =
'[for="kc-always-display-in-ui-switch"] .pf-c-switch__toggle'; '[for="kc-always-display-in-ui-switch"] .pf-c-switch__toggle';

View file

@ -1,12 +1,8 @@
import { FormGroup, Switch, ValidatedOptions } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared"; import { TextControl, TextAreaControl } from "ui-shared";
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
import { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextArea"; import { DefaultSwitchControl } from "../components/SwitchControl";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { FormFields } from "./ClientDetails";
type ClientDescriptionProps = { type ClientDescriptionProps = {
protocol?: string; protocol?: string;
@ -17,105 +13,35 @@ export const ClientDescription = ({
hasConfigureAccess: configure, hasConfigureAccess: configure,
}: ClientDescriptionProps) => { }: ClientDescriptionProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const {
register,
control,
formState: { errors },
} = useFormContext<FormFields>();
return ( return (
<FormAccess role="manage-clients" fineGrainedAccess={configure} unWrap> <FormAccess role="manage-clients" fineGrainedAccess={configure} unWrap>
<FormGroup <TextControl
labelIcon={ name="clientId"
<HelpItem
helpText={t("clients-help:clientId")}
fieldLabelId="clientId"
/>
}
label={t("common:clientId")} label={t("common:clientId")}
fieldId="kc-client-id" labelIcon={t("clients-help:clientId")}
helperTextInvalid={t("common:required")} rules={{ required: { value: true, message: t("common:required") } }}
validated={
errors.clientId ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<KeycloakTextInput
{...register("clientId", { required: true })}
id="kc-client-id"
data-testid="kc-client-id"
validated={
errors.clientId ? ValidatedOptions.error : ValidatedOptions.default
}
/> />
</FormGroup> <TextControl
<FormGroup name="name"
labelIcon={
<HelpItem
helpText={t("clients-help:clientName")}
fieldLabelId="name"
/>
}
label={t("common:name")} label={t("common:name")}
fieldId="kc-name" labelIcon={t("clients-help:clientName")}
>
<KeycloakTextInput {...register("name")} id="kc-name" />
</FormGroup>
<FormGroup
labelIcon={
<HelpItem
helpText={t("clients-help:description")}
fieldLabelId="description"
/> />
} <TextAreaControl
name="description"
label={t("common:description")} label={t("common:description")}
fieldId="kc-description" labelIcon={t("clients-help:description")}
validated={ rules={{
errors.description ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={errors.description?.message}
>
<KeycloakTextArea
{...register("description", {
maxLength: { maxLength: {
value: 255, value: 255,
message: t("common:maxLength", { length: 255 }), message: t("common:maxLength", { length: 255 }),
}, },
})} }}
id="kc-description"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
/> />
</FormGroup> <DefaultSwitchControl
<FormGroup
label={t("alwaysDisplayInUI")}
labelIcon={
<HelpItem
helpText={t("clients-help:alwaysDisplayInUI")}
fieldLabelId="clients:alwaysDisplayInUI"
/>
}
fieldId="kc-always-display-in-ui"
hasNoPaddingTop
>
<Controller
name="alwaysDisplayInConsole" name="alwaysDisplayInConsole"
defaultValue={false} label={t("alwaysDisplayInUI")}
control={control} labelIcon={t("clients-help:alwaysDisplayInUI")}
render={({ field }) => (
<Switch
id="kc-always-display-in-ui-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("alwaysDisplayInUI")}
/> />
)}
/>
</FormGroup>
</FormAccess> </FormAccess>
); );
}; };

View file

@ -0,0 +1,25 @@
import { SwitchProps } from "@patternfly/react-core";
import { FieldPath, FieldValues, UseControllerProps } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { SwitchControl } from "ui-shared";
type AdminSwitchControlProps<
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>
> = SwitchProps &
UseControllerProps<T, P> & {
name: string;
label?: string;
labelIcon?: string;
};
export const DefaultSwitchControl = <
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>
>(
props: AdminSwitchControlProps<T, P>
) => {
const { t } = useTranslation("common");
return <SwitchControl {...props} labelOn={t("on")} labelOff={t("off")} />;
};

View file

@ -0,0 +1,36 @@
import { FormGroup, ValidatedOptions } from "@patternfly/react-core";
import { PropsWithChildren } from "react";
import { FieldError, FieldValues, Merge } from "react-hook-form";
import { HelpItem } from "./HelpItem";
export type FormLabelProps<T extends FieldValues = FieldValues> = {
label?: string;
name: string;
labelIcon?: string;
error?: FieldError | Merge<FieldError, T>;
isRequired: boolean;
};
export const FormLabel = ({
name,
label,
labelIcon,
error,
children,
...rest
}: PropsWithChildren<FormLabelProps>) => (
<FormGroup
label={label || name}
fieldId={name}
labelIcon={
labelIcon ? (
<HelpItem helpText={labelIcon} fieldLabelId={name} />
) : undefined
}
helperTextInvalid={error?.message}
validated={error ? ValidatedOptions.error : ValidatedOptions.default}
{...rest}
>
{children}
</FormGroup>
);

View file

@ -8,12 +8,12 @@ import {
UseControllerProps, UseControllerProps,
} from "react-hook-form"; } from "react-hook-form";
import { import {
FormGroup,
Select, Select,
SelectOption, SelectOption,
SelectProps, SelectProps,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { FormLabel } from "./FormLabel";
type Option = { type Option = {
key: string; key: string;
@ -50,14 +50,11 @@ export const SelectControl = <
} = useFormContext(); } = useFormContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<FormGroup <FormLabel
name={name}
label={label}
isRequired={controller.rules?.required === true} isRequired={controller.rules?.required === true}
label={label || name} error={errors[name]}
fieldId={name}
helperTextInvalid={errors[name]?.message as string}
validated={
errors[name] ? ValidatedOptions.error : ValidatedOptions.default
}
> >
<Controller <Controller
{...controller} {...controller}
@ -97,6 +94,6 @@ export const SelectControl = <
</Select> </Select>
)} )}
/> />
</FormGroup> </FormLabel>
); );
}; };

View file

@ -0,0 +1,56 @@
import {
Controller,
FieldValues,
FieldPath,
UseControllerProps,
PathValue,
useFormContext,
} from "react-hook-form";
import { SwitchProps, Switch } from "@patternfly/react-core";
import { FormLabel } from "./FormLabel";
export type SwitchControlProps<
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>
> = SwitchProps &
UseControllerProps<T, P> & {
name: string;
label?: string;
labelIcon?: string;
labelOn: string;
labelOff: string;
};
export const SwitchControl = <
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>
>(
props: SwitchControlProps<T, P>
) => {
const defaultValue = props.defaultValue ?? (false as PathValue<T, P>);
const { control } = useFormContext();
return (
<FormLabel
name={props.name}
isRequired={props.rules?.required === true}
label={props.label}
labelIcon={props.labelIcon}
>
<Controller
control={control}
name={props.name}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => (
<Switch
id={props.name}
data-testid={props.name}
label={props.labelOn}
labelOff={props.labelOff}
isChecked={value}
onChange={(checked) => onChange(checked)}
/>
)}
/>
</FormLabel>
);
};

View file

@ -0,0 +1,56 @@
import { ValidatedOptions } from "@patternfly/react-core";
import {
FieldPath,
FieldValues,
PathValue,
useController,
UseControllerProps,
} from "react-hook-form";
import { FormLabel } from "./FormLabel";
import { KeycloakTextArea } from "./keycloak-text-area/KeycloakTextArea";
export type TextAreaControlProps<
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>
> = UseControllerProps<T, P> & {
label: string;
labelIcon?: string;
isDisabled?: boolean;
};
export const TextAreaControl = <
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>
>(
props: TextAreaControlProps<T, P>
) => {
const required = !!props.rules?.required;
const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
const { field, fieldState } = useController({
...props,
defaultValue,
});
return (
<FormLabel
isRequired={required}
label={props.label}
labelIcon={props.labelIcon}
name={props.name}
error={fieldState.error}
>
<KeycloakTextArea
isRequired={required}
id={props.name}
data-testid={props.name}
validated={
fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
}
isDisabled={props.isDisabled}
{...field}
/>
</FormLabel>
);
};

View file

@ -1,4 +1,4 @@
import { FormGroup, ValidatedOptions } from "@patternfly/react-core"; import { ValidatedOptions } from "@patternfly/react-core";
import { import {
FieldPath, FieldPath,
FieldValues, FieldValues,
@ -8,7 +8,7 @@ import {
} from "react-hook-form"; } from "react-hook-form";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import { HelpItem } from "./HelpItem"; import { FormLabel } from "./FormLabel";
export type TextControlProps< export type TextControlProps<
T extends FieldValues, T extends FieldValues,
@ -34,29 +34,23 @@ export const TextControl = <
}); });
return ( return (
<FormGroup <FormLabel
isRequired={required} name={props.name}
label={props.label} label={props.label}
labelIcon={ labelIcon={props.labelIcon}
props.labelIcon ? ( isRequired={required}
<HelpItem helpText={props.labelIcon} fieldLabelId={props.name} /> error={fieldState.error}
) : undefined
}
fieldId={props.name}
helperTextInvalid={fieldState.error?.message}
validated={
fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
}
> >
<KeycloakTextInput <KeycloakTextInput
isRequired={required} isRequired={required}
id={props.name} id={props.name}
data-testid={props.name}
validated={ validated={
fieldState.error ? ValidatedOptions.error : ValidatedOptions.default fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
} }
isDisabled={props.isDisabled} isDisabled={props.isDisabled}
{...field} {...field}
/> />
</FormGroup> </FormLabel>
); );
}; };

View file

@ -0,0 +1,27 @@
import { TextArea, TextAreaProps } from "@patternfly/react-core";
import { ComponentProps, forwardRef, HTMLProps } from "react";
// PatternFly changes the signature of the 'onChange' handler for textarea elements.
// This causes issues with React Hook Form as it expects the default signature for a textarea element.
// So we have to create this wrapper component that takes care of converting these signatures for us.
export type KeycloakTextAreaProps = Omit<
ComponentProps<typeof TextArea>,
"onChange"
> &
Pick<HTMLProps<HTMLTextAreaElement>, "onChange">;
export const KeycloakTextArea = forwardRef<
HTMLTextAreaElement,
KeycloakTextAreaProps
>(({ onChange, ...props }, ref) => {
const onChangeForward: TextAreaProps["onChange"] = (_, event) =>
onChange?.(event);
return <TextArea {...props} ref={ref} onChange={onChangeForward} />;
});
// We need to fake the displayName to match what PatternFly expects.
// This is because PatternFly uses it to filter children in certain aspects.
// This is a stupid approach, but it's not like we can change that.
KeycloakTextArea.displayName = "TextArea";

View file

@ -1,6 +1,8 @@
export { ContinueCancelModal } from "./continue-cancel/ContinueCancelModal"; export { ContinueCancelModal } from "./continue-cancel/ContinueCancelModal";
export { SelectControl } from "./controls/SelectControl"; export { SelectControl } from "./controls/SelectControl";
export { SwitchControl } from "./controls/SwitchControl";
export { TextControl } from "./controls/TextControl"; export { TextControl } from "./controls/TextControl";
export { TextAreaControl } from "./controls/TextAreaControl";
export { HelpItem } from "./controls/HelpItem"; export { HelpItem } from "./controls/HelpItem";
export { useHelp, Help } from "./context/HelpContext"; export { useHelp, Help } from "./context/HelpContext";
export { KeycloakTextInput } from "./keycloak-text-input/KeycloakTextInput"; export { KeycloakTextInput } from "./keycloak-text-input/KeycloakTextInput";