Add FormSubmitButton to handle form submission consistently (#28701)

Closes #28256

Signed-off-by: jchong <jhchong92@gmail.com>
This commit is contained in:
jhchong92 2024-05-14 22:34:30 +08:00 committed by GitHub
parent 4b6d0fb651
commit 5cacf8637c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 118 additions and 47 deletions

View file

@ -8,7 +8,7 @@ import {
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { SelectControl } from "@keycloak/keycloak-ui-shared";
import { FormSubmitButton, SelectControl } from "@keycloak/keycloak-ui-shared";
import { useAdminClient } from "../../admin-client";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form/FormAccess";
@ -28,7 +28,7 @@ export default function CreateFlow() {
const { realm } = useRealm();
const { addAlert } = useAlerts();
const form = useForm<AuthenticationFlowRepresentation>();
const { handleSubmit } = form;
const { handleSubmit, formState } = form;
const onSubmit = async (formValues: AuthenticationFlowRepresentation) => {
const flow = { ...formValues, builtIn: false, topLevel: true };
@ -77,9 +77,14 @@ export default function CreateFlow() {
}))}
/>
<ActionGroup>
<Button data-testid="create" type="submit">
<FormSubmitButton
formState={formState}
data-testid="create"
allowInvalid
allowNonDirty
>
{t("create")}
</Button>
</FormSubmitButton>
<Button
data-testid="cancel"
variant="link"

View file

@ -6,6 +6,7 @@ import { FormProvider, useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {
FormSubmitButton,
SelectControl,
TextAreaControl,
TextControl,
@ -32,12 +33,7 @@ type ScopeFormProps = {
export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
const { t } = useTranslation();
const form = useForm<ClientScopeDefaultOptionalType>({ mode: "onChange" });
const {
control,
handleSubmit,
setValue,
formState: { isDirty, isValid },
} = form;
const { control, handleSubmit, setValue, formState } = form;
const { realm } = useRealm();
const providers = useLoginProviders();
@ -194,13 +190,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
min={0}
/>
<ActionGroup>
<Button
variant="primary"
type="submit"
isDisabled={!isDirty || !isValid}
>
{t("save")}
</Button>
<FormSubmitButton formState={formState}>{t("save")}</FormSubmitButton>
<Button
variant="link"
component={(props) => (

View file

@ -11,7 +11,7 @@ import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { TextControl } from "@keycloak/keycloak-ui-shared";
import { FormSubmitButton, TextControl } from "@keycloak/keycloak-ui-shared";
import { useAdminClient } from "../../admin-client";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form/FormAccess";
@ -39,7 +39,7 @@ export default function ImportForm() {
const navigate = useNavigate();
const { realm } = useRealm();
const form = useForm<FormFields>();
const { handleSubmit, setValue } = form;
const { handleSubmit, setValue, formState } = form;
const [imported, setImported] = useState<ClientRepresentation>({});
const { addAlert, addError } = useAlerts();
@ -119,9 +119,13 @@ export default function ImportForm() {
<TextControl name="protocol" label={t("type")} readOnly />
<CapabilityConfig unWrap={true} />
<ActionGroup>
<Button variant="primary" type="submit">
<FormSubmitButton
formState={formState}
allowInvalid
allowNonDirty
>
{t("save")}
</Button>
</FormSubmitButton>
<Button
variant="link"
component={(props) => (

View file

@ -57,6 +57,7 @@ export default function CreateClientRole() {
return (
<FormProvider {...form}>
<RoleForm
form={form}
onSubmit={onSubmit}
cancelLink={toClient({
realm,

View file

@ -1,14 +1,24 @@
import { ActionGroup, Button, PageSection } from "@patternfly/react-core";
import { SubmitHandler, useFormContext, useWatch } from "react-hook-form";
import {
SubmitHandler,
UseFormReturn,
useFormContext,
useWatch,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, To } from "react-router-dom";
import { TextAreaControl, TextControl } from "@keycloak/keycloak-ui-shared";
import {
FormSubmitButton,
TextAreaControl,
TextControl,
} from "@keycloak/keycloak-ui-shared";
import { FormAccess } from "../form/FormAccess";
import { AttributeForm } from "../key-value-form/AttributeForm";
import { ViewHeader } from "../view-header/ViewHeader";
export type RoleFormProps = {
form: UseFormReturn<AttributeForm>;
onSubmit: SubmitHandler<AttributeForm>;
cancelLink: To;
role: "manage-realm" | "manage-clients";
@ -16,6 +26,7 @@ export type RoleFormProps = {
};
export const RoleForm = ({
form: { formState },
onSubmit,
cancelLink,
role,
@ -65,9 +76,14 @@ export const RoleForm = ({
isDisabled={roleName?.includes("default-roles") ?? false}
/>
<ActionGroup>
<Button data-testid="save" type="submit" variant="primary">
<FormSubmitButton
formState={formState}
data-testid="save"
allowInvalid
allowNonDirty
>
{t("save")}
</Button>
</FormSubmitButton>
<Button
data-testid="cancel"
variant="link"

View file

@ -9,7 +9,7 @@ import {
} from "@patternfly/react-core";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { TextControl } from "@keycloak/keycloak-ui-shared";
import { FormSubmitButton, TextControl } from "@keycloak/keycloak-ui-shared";
import { useAdminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts";
@ -34,7 +34,7 @@ export const GroupsModal = ({
const form = useForm({
defaultValues: { name: rename?.name },
});
const { handleSubmit } = form;
const { handleSubmit, formState } = form;
const submitForm = async (group: GroupRepresentation) => {
group.name = group.name?.trim();
@ -71,15 +71,16 @@ export const GroupsModal = ({
isOpen={true}
onClose={handleModalToggle}
actions={[
<Button
<FormSubmitButton
formState={formState}
data-testid={`${rename ? "rename" : "create"}Group`}
key="confirm"
variant="primary"
type="submit"
form="group-form"
allowInvalid
allowNonDirty
>
{t(rename ? "rename" : "create")}
</Button>,
</FormSubmitButton>,
<Button
id="modal-cancel"
data-testid="cancel"

View file

@ -48,6 +48,7 @@ export default function CreateRealmRole() {
return (
<FormProvider {...form}>
<RoleForm
form={form}
onSubmit={onSubmit}
cancelLink={toRealmRoles({ realm })}
role="manage-realm"

View file

@ -346,6 +346,7 @@ export default function RealmRoleTabs() {
{...detailsTab}
>
<RoleForm
form={form}
onSubmit={onSubmit}
role={clientRoleMatch ? "manage-clients" : "manage-realm"}
cancelLink={

View file

@ -4,7 +4,7 @@ import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { TextControl } from "@keycloak/keycloak-ui-shared";
import { FormSubmitButton, TextControl } from "@keycloak/keycloak-ui-shared";
import { useAdminClient } from "../../admin-client";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import { useAlerts } from "../../components/alert/Alerts";
@ -30,7 +30,7 @@ export default function NewRealmForm() {
mode: "onChange",
});
const { handleSubmit, setValue } = form;
const { handleSubmit, setValue, formState } = form;
const handleFileChange = (obj?: object) => {
const defaultRealm = { id: "", realm: "", enabled: true };
@ -81,9 +81,13 @@ export default function NewRealmForm() {
defaultValue={true}
/>
<ActionGroup>
<Button variant="primary" type="submit">
<FormSubmitButton
formState={formState}
allowInvalid
allowNonDirty
>
{t("create")}
</Button>
</FormSubmitButton>
<Button variant="link" onClick={() => navigate(-1)}>
{t("cancel")}
</Button>

View file

@ -4,6 +4,7 @@ import { UserProfileMetadata } from "@keycloak/keycloak-admin-client/lib/defs/us
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import {
FormErrorText,
FormSubmitButton,
HelpItem,
SwitchControl,
TextControl,
@ -78,14 +79,9 @@ export const UserForm = ({
const { whoAmI } = useWhoAmI();
const currentLocale = whoAmI.getLocale();
const {
handleSubmit,
setValue,
watch,
control,
reset,
formState: { errors },
} = form;
const { handleSubmit, setValue, watch, control, reset, formState } = form;
const { errors } = formState;
const watchUsernameInput = watch("username");
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
[],
@ -330,18 +326,19 @@ export const UserForm = ({
)}
<ActionGroup>
<Button
<FormSubmitButton
formState={formState}
data-testid={!user?.id ? "create-user" : "save-user"}
isDisabled={
!user?.id &&
!watchUsernameInput &&
realm.registrationEmailAsUsername === false
}
variant="primary"
type="submit"
allowNonDirty
allowInvalid
>
{user?.id ? t("save") : t("create")}
</Button>
</FormSubmitButton>
<Button
data-testid="cancel-create-user"
variant="link"

View file

@ -0,0 +1,47 @@
import { Button, ButtonProps } from "@patternfly/react-core";
import { PropsWithChildren } from "react";
import { FieldValues, FormState } from "react-hook-form";
export type FormSubmitButtonProps = Omit<ButtonProps, "isDisabled"> & {
formState: FormState<FieldValues>;
allowNonDirty?: boolean;
allowInvalid?: boolean;
isDisabled?: boolean;
};
const isSubmittable = (
formState: FormState<FieldValues>,
allowNonDirty: boolean,
allowInvalid: boolean,
) => {
return (
(formState.isValid || allowInvalid) &&
(formState.isDirty || allowNonDirty) &&
!formState.isLoading &&
!formState.isValidating &&
!formState.isSubmitting
);
};
export const FormSubmitButton = ({
formState,
isDisabled = false,
allowInvalid = false,
allowNonDirty = false,
children,
...rest
}: PropsWithChildren<FormSubmitButtonProps>) => {
return (
<Button
variant="primary"
isDisabled={
(formState && !isSubmittable(formState, allowNonDirty, allowInvalid)) ||
isDisabled
}
{...rest}
type="submit"
>
{children}
</Button>
);
};

View file

@ -36,6 +36,10 @@ export {
export { IconMapper } from "./icons/IconMapper";
export { FormPanel } from "./scroll-form/FormPanel";
export { ScrollForm, mainPageContentId } from "./scroll-form/ScrollForm";
export {
FormSubmitButton,
type FormSubmitButtonProps,
} from "./buttons/FormSubmitButton";
export { UserProfileFields } from "./user-profile/UserProfileFields";
export {
beerify,