From 3dc9c100e92f6b5cfc525c582ede6657df010bb9 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 10 Aug 2026 11:02:44 -0400 Subject: [PATCH] better form feedback when saving roles --- .../users/[userId]/access-controls/page.tsx | 44 +++++---------- src/components/OrgRolesTagField.tsx | 55 ++----------------- 2 files changed, 18 insertions(+), 81 deletions(-) diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index a3ed1e2df..2f94e78da 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -38,18 +38,6 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -const accessControlsFormSchema = z.object({ - username: z.string(), - autoProvisioned: z.boolean(), - roles: z.array( - z.object({ - id: z.string(), - text: z.string(), - isAdmin: z.boolean().optional() - }) - ) -}); - export default function AccessControlsPage() { const { orgUser: user, updateOrgUser } = userOrgUserContext(); const { user: sessionUser } = useUserContext(); @@ -69,6 +57,20 @@ export default function AccessControlsPage() { (build === "enterprise" && !isPaid) || (build === "oss" && !isPaid)); + const accessControlsFormSchema = z.object({ + username: z.string(), + autoProvisioned: z.boolean(), + roles: z + .array( + z.object({ + id: z.string(), + text: z.string(), + isAdmin: z.boolean().optional() + }) + ) + .min(1, { message: t("accessRoleSelectPlease") }) + }); + const form = useForm({ resolver: zodResolver(accessControlsFormSchema), defaultValues: { @@ -108,15 +110,6 @@ export default function AccessControlsPage() { async function executeSave() { const values = form.getValues(); - if (values.roles.length === 0) { - toast({ - variant: "destructive", - title: t("accessRoleRequired"), - description: t("accessRoleSelectPlease") - }); - return; - } - setIsSaving(true); try { const roleIds = values.roles.map((r) => parseInt(r.id, 10)); @@ -170,15 +163,6 @@ export default function AccessControlsPage() { const values = form.getValues(); - if (values.roles.length === 0) { - toast({ - variant: "destructive", - title: t("accessRoleRequired"), - description: t("accessRoleSelectPlease") - }); - return; - } - const willHaveAdminRole = values.roles.some((r) => r.isAdmin === true); const isRemovingOwnAdmin = diff --git a/src/components/OrgRolesTagField.tsx b/src/components/OrgRolesTagField.tsx index eef1e0570..32d96794a 100644 --- a/src/components/OrgRolesTagField.tsx +++ b/src/components/OrgRolesTagField.tsx @@ -9,17 +9,15 @@ import { FormMessage } from "@app/components/ui/form"; -import { toast } from "@app/hooks/useToast"; import { useTranslations } from "next-intl"; -import { useRef } from "react"; import type { FieldValues, Path, UseFormReturn } from "react-hook-form"; import { RolesSelector, type SelectedRole } from "./roles-selector"; type OrgRolesTagFieldProps = { form: Pick< UseFormReturn, - "control" | "getValues" | "setValue" + "control" | "getValues" | "setValue" | "clearErrors" >; orgId: string; /** Field in the form that holds Tag[] (role tags). Default: `"roles"`. */ @@ -42,46 +40,6 @@ export default function OrgRolesTagField({ disabled }: OrgRolesTagFieldProps) { const t = useTranslations(); - const isPopoverOpenRef = useRef(false); - const lastValidRolesRef = useRef( - (form.getValues(name) as SelectedRole[]) ?? [] - ); - - function validateRolesSelection() { - const current = form.getValues(name) as SelectedRole[]; - - if (current.length === 0 && lastValidRolesRef.current.length > 0) { - form.setValue(name, lastValidRolesRef.current as never, { - shouldDirty: true - }); - toast({ - variant: "destructive", - title: t("accessRoleRequired"), - description: t("accessRoleSelectPlease") - }); - return false; - } - - if (current.length > 0) { - lastValidRolesRef.current = current; - } - - return true; - } - - function handlePopoverOpenChange(open: boolean) { - isPopoverOpenRef.current = open; - - if (open) { - const current = form.getValues(name) as SelectedRole[]; - if (current.length > 0) { - lastValidRolesRef.current = current; - } - return; - } - - validateRolesSelection(); - } function setRoleTags(nextValue: SelectedRole[]) { const prev = form.getValues(name) as SelectedRole[]; @@ -99,15 +57,14 @@ export default function OrgRolesTagField({ form.setValue(name, [prev[prev.length - 1]] as never, { shouldDirty: true }); + form.clearErrors(name); return; } form.setValue(name, next as never, { shouldDirty: true }); - if (next.length > 0 && !isPopoverOpenRef.current) { - lastValidRolesRef.current = next; - } else if (!isPopoverOpenRef.current) { - validateRolesSelection(); + if (next.length > 0) { + form.clearErrors(name); } } @@ -117,9 +74,6 @@ export default function OrgRolesTagField({ name={name} render={({ field }) => { const selectedRoles = (field.value ?? []) as SelectedRole[]; - if (!isPopoverOpenRef.current && selectedRoles.length > 0) { - lastValidRolesRef.current = selectedRoles; - } return ( @@ -129,7 +83,6 @@ export default function OrgRolesTagField({ orgId={orgId} selectedRoles={selectedRoles} onSelectRoles={setRoleTags} - onPopoverOpenChange={handlePopoverOpenChange} disabled={disabled} />