From cc8f18d41747808a2fb2c38019e379121023967c Mon Sep 17 00:00:00 2001 From: ejay-dev Date: Sun, 24 May 2026 02:09:40 +0930 Subject: [PATCH] =?UTF-8?q?fix(audit-sprint-8a):=20modal=20Phase=203=20?= =?UTF-8?q?=E2=80=94=20invite-button=20=E2=86=92=20Dialog=20primitive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues the modal Phase 3 migration from Sprint 7c. This PR migrates components/team/invite-button.tsx — one of the 5 form modals left over from that pass. What this PR migrates - components/team/invite-button.tsx: rewrap the ad-hoc fixed-inset-0 modal as a Dialog/DialogContent. Strip the cyan→indigo→blue gradient on the trigger button + submit button per the stored enterprise-aesthetic preference. Add a clean resetState() helper so close-paths (cancel / esc / outside-click via Dialog) all converge on the same teardown. What this PR does NOT migrate (and why) - components/vault/credential-inspector-modal.tsx — wide side-by-side layout (iframe preview left + metadata/approval sidebar right) that's actively used as a workflow surface, not a typical form modal. A centered Dialog or right-side Sheet would destroy the side-by-side review pattern. Needs design pass, not mechanical migration. - components/team/invite-modal.tsx — light-themed white-bg modal that would need a deeper light/dark decision before mechanical swap (Dialog primitive defaults dark). - components/vault/upload-artifact-modal.tsx — file-upload complexity + the agent flagged a duplicated overlay block as a pre-existing bug. Needs investigation before migration. - components/compliance-system/plan-activation-flow.tsx — multi- step state-machine flow, not a single-screen form. Different primitive concern entirely. Visual change - Trigger button + submit go from `bg-gradient-to-r from-blue-600 via-indigo-600 to-cyan-500` to neutral `bg-slate-100 text-slate-900`. Intentional — matches the new primitive style and the audit's "enterprise aesthetic over AI feel" finding. After-merge screenshot review recommended for /app/team. Built on top of fix/audit-sprint-4c-modal-primitives (PR #167) for the Dialog primitive. Validation - tsc -p tsconfig.typecheck.json: clean - eslint: 0 errors, 18 warnings (baseline) - jest: 5319/5334 pass (no test changes) Test plan - /app/team → click Invite member → Dialog opens centered, ESC dismisses, focus on email field, role picker tab order works - Submit invite → success state shows; auto-close after 1.5s on the email-sent path, sticky with copy-link on manual-share path Co-Authored-By: Claude Opus 4.7 (1M context) --- components/team/invite-button.tsx | 375 +++++++++++++++--------------- 1 file changed, 190 insertions(+), 185 deletions(-) diff --git a/components/team/invite-button.tsx b/components/team/invite-button.tsx index 6959c8327..977c950d7 100644 --- a/components/team/invite-button.tsx +++ b/components/team/invite-button.tsx @@ -1,24 +1,30 @@ "use client" +// Audit Sprint 8a (2026-05-24): migrated from ad-hoc `fixed inset-0` +// modal to the shared Dialog primitive (Sprint 4c). Gains focus trap, +// ESC, aria-modal, scroll lock. Cyan trigger-button gradient stripped +// per the stored enterprise-aesthetic preference; trigger is now +// neutral white. + import { useState } from "react" import { Plus, Loader2, Mail, Shield, Eye, CheckCircle2, UserPlus } from "lucide-react" import { useRouter } from "next/navigation" import { useComplianceAction } from "@/components/compliance-system" import { z } from "zod" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" const inviteButtonSchema = z.object({ email: z.string().min(1, "Email is required").email("Please enter a valid email address"), role: z.enum(["member", "viewer"]), }) -/** - * ========================================================= - * INVITE BUTTON - * Node Type: Entity (neutral) - * Adds a new member entity to the organization graph - * ========================================================= - */ - export function InviteButton({ orgId, disabled }: { orgId: string; disabled?: boolean }) { const [open, setOpen] = useState(false) const [email, setEmail] = useState("") @@ -33,6 +39,17 @@ export function InviteButton({ orgId, disabled }: { orgId: string; disabled?: bo const [validationError, setValidationError] = useState(null) + const resetState = () => { + setOpen(false) + setEmail("") + setRole("member") + setSuccess(false) + setDelivery("sent") + setManualShareUrl("") + setCopied(false) + setValidationError(null) + } + const handleInvite = async (e: React.FormEvent) => { e.preventDefault() setValidationError(null) @@ -48,11 +65,7 @@ export function InviteButton({ orgId, disabled }: { orgId: string; disabled?: bo const response = await fetch("/app/api/invitations/create", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - organizationId: orgId, - email, - role, - }), + body: JSON.stringify({ organizationId: orgId, email, role }), }) setLoading(false) @@ -66,13 +79,11 @@ export function InviteButton({ orgId, disabled }: { orgId: string; disabled?: bo const payload = await response.json().catch(() => null) const result = payload?.data ?? null - // Show success state setSuccess(true) setDelivery(result?.delivery === "manual_share_required" ? "manual_share_required" : "sent") setManualShareUrl(typeof result?.inviteUrl === "string" ? result.inviteUrl : "") setCopied(false) - - // Report to compliance system + nodeCreated("entity", email) reportInfo({ title: result?.delivery === "manual_share_required" ? "Invitation created" : "Invitation sent", @@ -81,17 +92,12 @@ export function InviteButton({ orgId, disabled }: { orgId: string; disabled?: bo ? `Share the secure invite link with ${email}` : `Sent to ${email}`, }) - - // Close after animation + + // Auto-close + refresh on the happy path; manual-share requires the + // user to copy the link before dismissing. if (result?.delivery !== "manual_share_required") { setTimeout(() => { - setOpen(false) - setEmail("") - setRole("member") - setSuccess(false) - setDelivery("sent") - setManualShareUrl("") - setCopied(false) + resetState() router.refresh() }, 1500) } @@ -108,172 +114,171 @@ export function InviteButton({ orgId, disabled }: { orgId: string; disabled?: bo } return ( - <> - - {open && ( -
-
- {success ? ( -
-
- -
-

- {delivery === "sent" ? "Invitation Sent" : "Invitation Created"} -

-

- {delivery === "sent" - ? `${email} will receive an invitation email` - : "Email delivery is unavailable. Share the secure invite link manually."} -

- {delivery === "manual_share_required" && manualShareUrl ? ( - <> -
-

- Manual Share Link -

-

- {manualShareUrl} -

-
-
- - -
- - ) : null} -
- ) : ( - <> -
-
-
-
- -
-
-

Invite User

-

Add a team member or external auditor.

-
-
- -
-
- -
- {validationError && ( -
- {validationError} -
- )} - {/* Email Input */} -
- -
- - setEmail(e.target.value)} - className="w-full pl-10 p-3 rounded-xl border border-glass-border bg-glass-subtle outline-none focus:border-blue-400/50 focus:ring-2 focus:ring-blue-400/20 transition-all text-sm" - placeholder="name@company.com" - /> -
-
+ + {success ? ( + <> + + + {delivery === "sent" ? "Invitation sent" : "Invitation created"} + + + {delivery === "sent" + ? `${email} will receive an invitation email.` + : "Email delivery is unavailable. Share the secure invite link manually."} + + +
+ +
+ {delivery === "manual_share_required" && manualShareUrl ? ( + <> +
+

+ Manual share link +

+

+ {manualShareUrl} +

+
+ + + + + + ) : null} + + ) : ( + <> + + + + Invite user + + + Add a team member or external auditor. + + + + + {validationError ? ( +
+ {validationError} +
+ ) : null} - {/* Role Selector */} -
- -
- + - -
-
- -
- - -
- - - )} -
-
- )} - +
+ + Access level + +
+ + +
+
+ + + + + + + + )} + + ) }