Change client description component to use utils (#19165)
This commit is contained in:
parent
632d315c94
commit
d29f3e4dfc
11 changed files with 247 additions and 128 deletions
|
@ -437,7 +437,7 @@ describe("Clients test", () => {
|
|||
it("Should fail to create imported client with empty ID", () => {
|
||||
commonPage.sidebar().goToClients();
|
||||
cy.findByTestId("importClient").click();
|
||||
cy.findByTestId("kc-client-id").click();
|
||||
cy.findByTestId("clientId").click();
|
||||
cy.findByText("Save").click();
|
||||
cy.findByText("Required field");
|
||||
});
|
||||
|
@ -463,7 +463,7 @@ describe("Clients test", () => {
|
|||
|
||||
cy.wait(1000);
|
||||
//cy.findByTestId("realm-file").contains('"clientId": "identical"')
|
||||
cy.findByTestId("kc-client-id").click();
|
||||
cy.findByTestId("clientId").click();
|
||||
cy.findByText("Save").click();
|
||||
commonPage
|
||||
.masthead()
|
||||
|
|
|
@ -3,9 +3,9 @@ import CommonPage from "../../../CommonPage";
|
|||
export default class CreateClientPage extends CommonPage {
|
||||
private clientTypeDrpDwn = ".pf-c-select__toggle";
|
||||
private clientTypeList = ".pf-c-select__toggle + ul";
|
||||
private clientIdInput = "#kc-client-id";
|
||||
private clientIdError = "#kc-client-id + div";
|
||||
private clientNameInput = "#kc-name";
|
||||
private clientIdInput = "#clientId";
|
||||
private clientIdError = "#clientId + div";
|
||||
private clientNameInput = "#name";
|
||||
private clientDescriptionInput = "#kc-description";
|
||||
private alwaysDisplayInUISwitch =
|
||||
'[for="kc-always-display-in-ui-switch"] .pf-c-switch__toggle';
|
||||
|
|
|
@ -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 { HelpItem } from "ui-shared";
|
||||
import { TextControl, TextAreaControl } from "ui-shared";
|
||||
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextArea";
|
||||
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { FormFields } from "./ClientDetails";
|
||||
import { DefaultSwitchControl } from "../components/SwitchControl";
|
||||
|
||||
type ClientDescriptionProps = {
|
||||
protocol?: string;
|
||||
|
@ -17,105 +13,35 @@ export const ClientDescription = ({
|
|||
hasConfigureAccess: configure,
|
||||
}: ClientDescriptionProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<FormFields>();
|
||||
return (
|
||||
<FormAccess role="manage-clients" fineGrainedAccess={configure} unWrap>
|
||||
<FormGroup
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("clients-help:clientId")}
|
||||
fieldLabelId="clientId"
|
||||
/>
|
||||
}
|
||||
<TextControl
|
||||
name="clientId"
|
||||
label={t("common:clientId")}
|
||||
fieldId="kc-client-id"
|
||||
helperTextInvalid={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>
|
||||
<FormGroup
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("clients-help:clientName")}
|
||||
fieldLabelId="name"
|
||||
/>
|
||||
}
|
||||
labelIcon={t("clients-help:clientId")}
|
||||
rules={{ required: { value: true, message: t("common:required") } }}
|
||||
/>
|
||||
<TextControl
|
||||
name="name"
|
||||
label={t("common:name")}
|
||||
fieldId="kc-name"
|
||||
>
|
||||
<KeycloakTextInput {...register("name")} id="kc-name" />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("clients-help:description")}
|
||||
fieldLabelId="description"
|
||||
/>
|
||||
}
|
||||
labelIcon={t("clients-help:clientName")}
|
||||
/>
|
||||
<TextAreaControl
|
||||
name="description"
|
||||
label={t("common:description")}
|
||||
fieldId="kc-description"
|
||||
validated={
|
||||
errors.description ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={errors.description?.message}
|
||||
>
|
||||
<KeycloakTextArea
|
||||
{...register("description", {
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: t("common:maxLength", { length: 255 }),
|
||||
},
|
||||
})}
|
||||
id="kc-description"
|
||||
validated={
|
||||
errors.description
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
labelIcon={t("clients-help:description")}
|
||||
rules={{
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: t("common:maxLength", { length: 255 }),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<DefaultSwitchControl
|
||||
name="alwaysDisplayInConsole"
|
||||
label={t("alwaysDisplayInUI")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("clients-help:alwaysDisplayInUI")}
|
||||
fieldLabelId="clients:alwaysDisplayInUI"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-always-display-in-ui"
|
||||
hasNoPaddingTop
|
||||
>
|
||||
<Controller
|
||||
name="alwaysDisplayInConsole"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
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>
|
||||
labelIcon={t("clients-help:alwaysDisplayInUI")}
|
||||
/>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
||||
|
|
25
js/apps/admin-ui/src/components/SwitchControl.tsx
Normal file
25
js/apps/admin-ui/src/components/SwitchControl.tsx
Normal 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")} />;
|
||||
};
|
36
js/libs/ui-shared/src/controls/FormLabel.tsx
Normal file
36
js/libs/ui-shared/src/controls/FormLabel.tsx
Normal 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>
|
||||
);
|
|
@ -8,12 +8,12 @@ import {
|
|||
UseControllerProps,
|
||||
} from "react-hook-form";
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectProps,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { FormLabel } from "./FormLabel";
|
||||
|
||||
type Option = {
|
||||
key: string;
|
||||
|
@ -50,14 +50,11 @@ export const SelectControl = <
|
|||
} = useFormContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<FormGroup
|
||||
<FormLabel
|
||||
name={name}
|
||||
label={label}
|
||||
isRequired={controller.rules?.required === true}
|
||||
label={label || name}
|
||||
fieldId={name}
|
||||
helperTextInvalid={errors[name]?.message as string}
|
||||
validated={
|
||||
errors[name] ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
error={errors[name]}
|
||||
>
|
||||
<Controller
|
||||
{...controller}
|
||||
|
@ -97,6 +94,6 @@ export const SelectControl = <
|
|||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormLabel>
|
||||
);
|
||||
};
|
||||
|
|
56
js/libs/ui-shared/src/controls/SwitchControl.tsx
Normal file
56
js/libs/ui-shared/src/controls/SwitchControl.tsx
Normal 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>
|
||||
);
|
||||
};
|
56
js/libs/ui-shared/src/controls/TextAreaControl.tsx
Normal file
56
js/libs/ui-shared/src/controls/TextAreaControl.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { FormGroup, ValidatedOptions } from "@patternfly/react-core";
|
||||
import { ValidatedOptions } from "@patternfly/react-core";
|
||||
import {
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
|
@ -8,7 +8,7 @@ import {
|
|||
} from "react-hook-form";
|
||||
|
||||
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
|
||||
import { HelpItem } from "./HelpItem";
|
||||
import { FormLabel } from "./FormLabel";
|
||||
|
||||
export type TextControlProps<
|
||||
T extends FieldValues,
|
||||
|
@ -34,29 +34,23 @@ export const TextControl = <
|
|||
});
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={required}
|
||||
<FormLabel
|
||||
name={props.name}
|
||||
label={props.label}
|
||||
labelIcon={
|
||||
props.labelIcon ? (
|
||||
<HelpItem helpText={props.labelIcon} fieldLabelId={props.name} />
|
||||
) : undefined
|
||||
}
|
||||
fieldId={props.name}
|
||||
helperTextInvalid={fieldState.error?.message}
|
||||
validated={
|
||||
fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
labelIcon={props.labelIcon}
|
||||
isRequired={required}
|
||||
error={fieldState.error}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
isRequired={required}
|
||||
id={props.name}
|
||||
data-testid={props.name}
|
||||
validated={
|
||||
fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
isDisabled={props.isDisabled}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormLabel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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";
|
|
@ -1,6 +1,8 @@
|
|||
export { ContinueCancelModal } from "./continue-cancel/ContinueCancelModal";
|
||||
export { SelectControl } from "./controls/SelectControl";
|
||||
export { SwitchControl } from "./controls/SwitchControl";
|
||||
export { TextControl } from "./controls/TextControl";
|
||||
export { TextAreaControl } from "./controls/TextAreaControl";
|
||||
export { HelpItem } from "./controls/HelpItem";
|
||||
export { useHelp, Help } from "./context/HelpContext";
|
||||
export { KeycloakTextInput } from "./keycloak-text-input/KeycloakTextInput";
|
||||
|
|
Loading…
Reference in a new issue