diff --git a/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts b/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts index bce4fbf05f1..1321c48d728 100644 --- a/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/clients_test.spec.ts @@ -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() diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/CreateClientPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/CreateClientPage.ts index 07deade5c32..b0822501728 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/CreateClientPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/CreateClientPage.ts @@ -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'; diff --git a/js/apps/admin-ui/src/clients/ClientDescription.tsx b/js/apps/admin-ui/src/clients/ClientDescription.tsx index d25d8c5a9fa..ae9a60247f9 100644 --- a/js/apps/admin-ui/src/clients/ClientDescription.tsx +++ b/js/apps/admin-ui/src/clients/ClientDescription.tsx @@ -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(); return ( - - } + - - - - } + labelIcon={t("clients-help:clientId")} + rules={{ required: { value: true, message: t("common:required") } }} + /> + - - - - } + labelIcon={t("clients-help:clientName")} + /> + - - - + - } - fieldId="kc-always-display-in-ui" - hasNoPaddingTop - > - ( - - )} - /> - + labelIcon={t("clients-help:alwaysDisplayInUI")} + /> ); }; diff --git a/js/apps/admin-ui/src/components/SwitchControl.tsx b/js/apps/admin-ui/src/components/SwitchControl.tsx new file mode 100644 index 00000000000..9c9ed23fd41 --- /dev/null +++ b/js/apps/admin-ui/src/components/SwitchControl.tsx @@ -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 = FieldPath +> = SwitchProps & + UseControllerProps & { + name: string; + label?: string; + labelIcon?: string; + }; + +export const DefaultSwitchControl = < + T extends FieldValues, + P extends FieldPath = FieldPath +>( + props: AdminSwitchControlProps +) => { + const { t } = useTranslation("common"); + + return ; +}; diff --git a/js/libs/ui-shared/src/controls/FormLabel.tsx b/js/libs/ui-shared/src/controls/FormLabel.tsx new file mode 100644 index 00000000000..c584a1ad883 --- /dev/null +++ b/js/libs/ui-shared/src/controls/FormLabel.tsx @@ -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 = { + label?: string; + name: string; + labelIcon?: string; + error?: FieldError | Merge; + isRequired: boolean; +}; + +export const FormLabel = ({ + name, + label, + labelIcon, + error, + children, + ...rest +}: PropsWithChildren) => ( + + ) : undefined + } + helperTextInvalid={error?.message} + validated={error ? ValidatedOptions.error : ValidatedOptions.default} + {...rest} + > + {children} + +); diff --git a/js/libs/ui-shared/src/controls/SelectControl.tsx b/js/libs/ui-shared/src/controls/SelectControl.tsx index ae9a9c52e34..a3e76982135 100644 --- a/js/libs/ui-shared/src/controls/SelectControl.tsx +++ b/js/libs/ui-shared/src/controls/SelectControl.tsx @@ -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 ( - )} /> - + ); }; diff --git a/js/libs/ui-shared/src/controls/SwitchControl.tsx b/js/libs/ui-shared/src/controls/SwitchControl.tsx new file mode 100644 index 00000000000..62728d89a9d --- /dev/null +++ b/js/libs/ui-shared/src/controls/SwitchControl.tsx @@ -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 = FieldPath +> = SwitchProps & + UseControllerProps & { + name: string; + label?: string; + labelIcon?: string; + labelOn: string; + labelOff: string; + }; + +export const SwitchControl = < + T extends FieldValues, + P extends FieldPath = FieldPath +>( + props: SwitchControlProps +) => { + const defaultValue = props.defaultValue ?? (false as PathValue); + const { control } = useFormContext(); + return ( + + ( + onChange(checked)} + /> + )} + /> + + ); +}; diff --git a/js/libs/ui-shared/src/controls/TextAreaControl.tsx b/js/libs/ui-shared/src/controls/TextAreaControl.tsx new file mode 100644 index 00000000000..ba7113da85b --- /dev/null +++ b/js/libs/ui-shared/src/controls/TextAreaControl.tsx @@ -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 = FieldPath +> = UseControllerProps & { + label: string; + labelIcon?: string; + isDisabled?: boolean; +}; + +export const TextAreaControl = < + T extends FieldValues, + P extends FieldPath = FieldPath +>( + props: TextAreaControlProps +) => { + const required = !!props.rules?.required; + const defaultValue = props.defaultValue ?? ("" as PathValue); + + const { field, fieldState } = useController({ + ...props, + defaultValue, + }); + + return ( + + + + ); +}; diff --git a/js/libs/ui-shared/src/controls/TextControl.tsx b/js/libs/ui-shared/src/controls/TextControl.tsx index 510528e93b3..e90d3d392d3 100644 --- a/js/libs/ui-shared/src/controls/TextControl.tsx +++ b/js/libs/ui-shared/src/controls/TextControl.tsx @@ -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 ( - - ) : undefined - } - fieldId={props.name} - helperTextInvalid={fieldState.error?.message} - validated={ - fieldState.error ? ValidatedOptions.error : ValidatedOptions.default - } + labelIcon={props.labelIcon} + isRequired={required} + error={fieldState.error} > - + ); }; diff --git a/js/libs/ui-shared/src/controls/keycloak-text-area/KeycloakTextArea.tsx b/js/libs/ui-shared/src/controls/keycloak-text-area/KeycloakTextArea.tsx new file mode 100644 index 00000000000..5981bed3785 --- /dev/null +++ b/js/libs/ui-shared/src/controls/keycloak-text-area/KeycloakTextArea.tsx @@ -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, + "onChange" +> & + Pick, "onChange">; + +export const KeycloakTextArea = forwardRef< + HTMLTextAreaElement, + KeycloakTextAreaProps +>(({ onChange, ...props }, ref) => { + const onChangeForward: TextAreaProps["onChange"] = (_, event) => + onChange?.(event); + + return