Skip to content

Commit

Permalink
Add FormSubmitButton to handle form submission consistently (#28701)
Browse files Browse the repository at this point in the history
Closes #28256

Signed-off-by: jchong <jhchong92@gmail.com>
  • Loading branch information
jhchong92 committed May 14, 2024
1 parent 4b6d0fb commit 5cacf86
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 47 deletions.
13 changes: 9 additions & 4 deletions js/apps/admin-ui/src/authentication/form/CreateFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 };
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 3 additions & 13 deletions js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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) => (
Expand Down
12 changes: 8 additions & 4 deletions js/apps/admin-ui/src/clients/import/ImportForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) => (
Expand Down
1 change: 1 addition & 0 deletions js/apps/admin-ui/src/clients/roles/CreateClientRole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default function CreateClientRole() {
return (
<FormProvider {...form}>
<RoleForm
form={form}
onSubmit={onSubmit}
cancelLink={toClient({
realm,
Expand Down
24 changes: 20 additions & 4 deletions js/apps/admin-ui/src/components/role-form/RoleForm.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
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";
editMode: boolean;
};

export const RoleForm = ({
form: { formState },
onSubmit,
cancelLink,
role,
Expand Down Expand Up @@ -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"
Expand Down
13 changes: 7 additions & 6 deletions js/apps/admin-ui/src/groups/GroupsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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();
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions js/apps/admin-ui/src/realm-roles/CreateRealmRole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default function CreateRealmRole() {
return (
<FormProvider {...form}>
<RoleForm
form={form}
onSubmit={onSubmit}
cancelLink={toRealmRoles({ realm })}
role="manage-realm"
Expand Down
1 change: 1 addition & 0 deletions js/apps/admin-ui/src/realm-roles/RealmRoleTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ export default function RealmRoleTabs() {
{...detailsTab}
>
<RoleForm
form={form}
onSubmit={onSubmit}
role={clientRoleMatch ? "manage-clients" : "manage-realm"}
cancelLink={
Expand Down
12 changes: 8 additions & 4 deletions js/apps/admin-ui/src/realm/add/NewRealmForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 };
Expand Down Expand Up @@ -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>
Expand Down
21 changes: 9 additions & 12 deletions js/apps/admin-ui/src/user/UserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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[]>(
[],
Expand Down Expand Up @@ -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"
Expand Down
47 changes: 47 additions & 0 deletions js/libs/ui-shared/src/buttons/FormSubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
4 changes: 4 additions & 0 deletions js/libs/ui-shared/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 5cacf86

Please sign in to comment.