Add FormSubmitButton
to handle form submission consistently (#28701)
Closes #28256 Signed-off-by: jchong <jhchong92@gmail.com>
This commit is contained in:
parent
4b6d0fb651
commit
5cacf8637c
12 changed files with 118 additions and 47 deletions
|
@ -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"
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -57,6 +57,7 @@ export default function CreateClientRole() {
|
|||
return (
|
||||
<FormProvider {...form}>
|
||||
<RoleForm
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
cancelLink={toClient({
|
||||
realm,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -48,6 +48,7 @@ export default function CreateRealmRole() {
|
|||
return (
|
||||
<FormProvider {...form}>
|
||||
<RoleForm
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
cancelLink={toRealmRoles({ realm })}
|
||||
role="manage-realm"
|
||||
|
|
|
@ -346,6 +346,7 @@ export default function RealmRoleTabs() {
|
|||
{...detailsTab}
|
||||
>
|
||||
<RoleForm
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
role={clientRoleMatch ? "manage-clients" : "manage-realm"}
|
||||
cancelLink={
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
47
js/libs/ui-shared/src/buttons/FormSubmitButton.tsx
Normal file
47
js/libs/ui-shared/src/buttons/FormSubmitButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue