diff --git a/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx index f881104cb..30a87ffc8 100644 --- a/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx @@ -14,7 +14,7 @@ import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { SwitchInput } from "@app/components/SwitchInput"; import { Button } from "@app/components/ui/button"; @@ -46,7 +46,7 @@ import { import { cn } from "@app/lib/cn"; import { Binary, Bot, Key, Plus } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyAuthMethodsSectionForm ─────────────────────────────────────── @@ -70,7 +70,7 @@ export type CreatePolicyAuthMethodsSectionFormProps = { }; export function CreatePolicyAuthMethodsSectionForm({ - form + form: parentForm }: CreatePolicyAuthMethodsSectionFormProps) { const t = useTranslations(); const [isExpanded, setIsExpanded] = useState(false); @@ -78,6 +78,30 @@ export function CreatePolicyAuthMethodsSectionForm({ const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + password: true, + pincode: true, + headerAuth: true + }) + ), + defaultValues: { + password: null, + pincode: null, + headerAuth: null + } + }); + + useEffect(() => { + const subscription = form.watch((values) => { + parentForm.setValue("password", values.password as any); + parentForm.setValue("pincode", values.pincode as any); + parentForm.setValue("headerAuth", values.headerAuth as any); + }); + return () => subscription.unsubscribe(); + }, [form, parentForm]); + const password = useWatch({ control: form.control, name: "password" diff --git a/src/components/resource-policy/CreatePolicyForm.tsx b/src/components/resource-policy/CreatePolicyForm.tsx index 9cc2d06b4..b3a5599a9 100644 --- a/src/components/resource-policy/CreatePolicyForm.tsx +++ b/src/components/resource-policy/CreatePolicyForm.tsx @@ -43,7 +43,7 @@ import { } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; -import { useMemo, useActionState } from "react"; +import { useMemo, useTransition } from "react"; import { useForm } from "react-hook-form"; import { CreatePolicyUsersRolesSectionForm } from "./CreatePolicyUserRolesSectionForm"; import { CreatePolicyAuthMethodsSectionForm } from "./CreatePolicyAuthMethodsSectionForm"; @@ -59,7 +59,7 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); - const [, formAction, isSubmitting] = useActionState(onSubmit, null); + const [isSubmitting, startTransition] = useTransition(); const { isPaidUser } = usePaidStatus(); const router = useRouter(); @@ -202,8 +202,7 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { return (
- - + {/* Name */} @@ -258,14 +257,14 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
- ); } diff --git a/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx index ce8ac54b9..fb324cced 100644 --- a/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx @@ -9,30 +9,31 @@ import { SettingsSectionTitle } from "@app/components/Settings"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { + Form, FormControl, FormDescription, FormField, FormItem, - FormLabel, - FormMessage + FormLabel } from "@app/components/ui/form"; import { InfoPopup } from "@app/components/ui/info-popup"; import { InfoIcon, Plus } from "lucide-react"; -import { useState } from "react"; -import { type UseFormReturn } from "react-hook-form"; +import { useEffect, useState } from "react"; +import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyOtpEmailSectionForm ────────────────────────────────────────── @@ -42,16 +43,44 @@ export type CreatePolicyOtpEmailSectionFormProps = { }; export function CreatePolicyOtpEmailSectionForm({ - form, + form: parentForm, emailEnabled }: CreatePolicyOtpEmailSectionFormProps) { const t = useTranslations(); const [isExpanded, setIsExpanded] = useState(false); - const [whitelistEnabled, setWhitelistEnabled] = useState(false); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + emailWhitelistEnabled: true, + emails: true + }) + ), + defaultValues: { + emailWhitelistEnabled: false, + emails: [] + } + }); + + useEffect(() => { + const subscription = form.watch((values) => { + parentForm.setValue( + "emailWhitelistEnabled", + values.emailWhitelistEnabled as boolean + ); + parentForm.setValue("emails", values.emails as [Tag, ...Tag[]]); + }); + return () => subscription.unsubscribe(); + }, [form, parentForm]); + + const whitelistEnabled = useWatch({ + control: form.control, + name: "emailWhitelistEnabled" + }); + if (!isExpanded) { return ( @@ -78,100 +107,107 @@ export function CreatePolicyOtpEmailSectionForm({ } return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - { - setWhitelistEnabled(val); - form.setValue("emailWhitelistEnabled", val); - }} - disabled={!emailEnabled} - /> - - {whitelistEnabled && emailEnabled && ( - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag).success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t("otpEmailEnter")} - tags={form.getValues().emails} - setTags={(newEmails) => { - form.setValue( - "emails", - newEmails as [Tag, ...Tag[]] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - - )} +
+ + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + + )} + { + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} /> - )} - - - + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t("otpEmailEnter")} + tags={form.getValues().emails} + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + ); } diff --git a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx index 87152fa09..c8635c5a3 100644 --- a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx @@ -13,7 +13,7 @@ import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { toast } from "@app/hooks/useToast"; import { SwitchInput } from "@app/components/SwitchInput"; @@ -81,8 +81,8 @@ import { Plus } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; -import { type UseFormReturn, useForm } from "react-hook-form"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyRulesSectionForm ───────────────────────────────────────────── @@ -111,18 +111,43 @@ export type CreatePolicyRulesSectionFormProps = { }; export function CreatePolicyRulesSectionForm({ - form, + form: parentForm, isMaxmindAvailable, isMaxmindAsnAvailable }: CreatePolicyRulesSectionFormProps) { const t = useTranslations(); const [isExpanded, setIsExpanded] = useState(false); const [rules, setRules] = useState([]); - const [rulesEnabled, setRulesEnabled] = useState(false); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + applyRules: true, + rules: true + }) + ), + defaultValues: { + applyRules: false, + rules: [] + } + }); + + useEffect(() => { + const subscription = form.watch((values) => { + parentForm.setValue("applyRules", values.applyRules as boolean); + parentForm.setValue("rules", values.rules as any); + }); + return () => subscription.unsubscribe(); + }, [form, parentForm]); + + const rulesEnabled = useWatch({ + control: form.control, + name: "applyRules" + }); + const addRuleForm = useForm({ resolver: zodResolver(addRuleSchema), defaultValues: { @@ -656,7 +681,6 @@ export function CreatePolicyRulesSectionForm({ label={t("rulesEnable")} defaultChecked={false} onCheckedChange={(val) => { - setRulesEnabled(val); form.setValue("applyRules", val); }} /> diff --git a/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx b/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx index 48d8b94f8..132363fc1 100644 --- a/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx @@ -9,9 +9,11 @@ import { SettingsSectionTitle } from "@app/components/Settings"; +import { zodResolver } from "@hookform/resolvers/zod"; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { + Form, FormControl, FormDescription, FormField, @@ -26,10 +28,10 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { useTranslations } from "next-intl"; -import { useState } from "react"; -import { type UseFormReturn, useWatch } from "react-hook-form"; +import { useEffect, useState } from "react"; +import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyUsersRolesSectionForm ──────────────────────────────────────── @@ -41,12 +43,40 @@ export type CreatePolicyUsersRolesSectionFormProps = { }; export function CreatePolicyUsersRolesSectionForm({ - form, + form: parentForm, allRoles, allUsers, allIdps }: CreatePolicyUsersRolesSectionFormProps) { const t = useTranslations(); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + sso: true, + skipToIdpId: true, + roles: true, + users: true + }) + ), + defaultValues: { + sso: true, + skipToIdpId: null, + roles: [], + users: [] + } + }); + + useEffect(() => { + const subscription = form.watch((values) => { + parentForm.setValue("sso", values.sso as boolean); + parentForm.setValue("skipToIdpId", values.skipToIdpId as number | null); + parentForm.setValue("roles", values.roles as [Tag, ...Tag[]]); + parentForm.setValue("users", values.users as [Tag, ...Tag[]]); + }); + return () => subscription.unsubscribe(); + }, [form, parentForm]); + const ssoEnabled = useWatch({ control: form.control, name: "sso" }); const selectedIdpId = useWatch({ control: form.control, @@ -60,165 +90,168 @@ export function CreatePolicyUsersRolesSectionForm({ >(null); return ( - - - - {t("resourceUsersRoles")} - - - {t("resourcePolicyUsersRolesDescription")} - - - - - { - console.log(`form.setValue("sso", ${val})`); - form.setValue("sso", val); - }} - /> +
+ + + + {t("resourceUsersRoles")} + + + {t("resourcePolicyUsersRolesDescription")} + + + + + { + form.setValue("sso", val); + }} + /> - {ssoEnabled && ( - <> - ( - - {t("roles")} - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allRoles} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t("resourceRoleDescription")} - - - )} - /> - ( - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allUsers} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} + {ssoEnabled && ( + <> + ( + + {t("roles")} + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allRoles} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t("resourceRoleDescription")} + + + )} + /> + ( + + {t("users")} + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allUsers} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} - {ssoEnabled && allIdps.length > 0 && ( -
- - { + if (value === "none") { + form.setValue("skipToIdpId", null); + } else { + const id = parseInt(value); + form.setValue("skipToIdpId", id); + } + }} + value={ + selectedIdpId + ? selectedIdpId.toString() + : "none" } - }} - value={ - selectedIdpId - ? selectedIdpId.toString() - : "none" - } - > - - - - - - {t("none")} - - {allIdps.map((idp) => ( - - {idp.text} + > + + + + + + {t("none")} - ))} - - -

- {t("defaultIdentityProviderDescription")} -

-
- )} -
-
-
+ {allIdps.map((idp) => ( + + {idp.text} + + ))} + + +

+ {t("defaultIdentityProviderDescription")} +

+ + )} + + + +
); }