"use client" /** * Sign-in flow builder: canvas on the left, properties for the selected node on * the right, one toolbar across the top. * * The layout is the argument. A flow is a sequence with branches, so it is drawn * as one (`flow-canvas.tsx`) instead of being implied by nested cards, and only * the node you selected is editable (`flow-properties.tsx`) instead of every field * in every flow being open at once. Those two changes are what the previous * version got wrong. * * The tester earns its place for the same reason. Conditions are worth nothing if * you cannot tell whether one fires, and you cannot look at a sign-in page while * signed in, so typing an email here marks the canvas with what would run. * * Edits are held as a draft and written on Save. Live-saving a flow you are * halfway through renaming would leave the sign-in page running a half-built one, * and sign-in is the surface you cannot recover from inside the app. * * **Which flow runs is not part of that draft**, and it is not a second picker * either. One selector says which flow you are looking at, and `Try this flow` * saves it, points the sign-in page at it, and ends the session, because seeing a * flow run is the reason to open this page and you cannot see a sign-in page while * signed in. A badge next to the selector says whether the flow on screen is the * one that currently runs. * * Both halves of that were wrong before. A separate "Runs on the sign-in page" * picker held its value in the draft, so choosing a role applied nothing until * Save and was discarded by any other way out of the page; and once two pickers * could disagree, the primary action had to silently overrule one of them. */ import * as React from "react" import { useNavigate } from "react-router" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select" import { StatusBadge } from "@/components/ui/status-badge" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { FlowCanvas, type CanvasSelection } from "@/components/builder/flow-canvas" import { FlowInspector } from "@/components/builder/flow-inspector" import { useLogOut } from "@/hooks/use-log-out" import { alwaysAuthenticates, createAuthStep, createChoiceStep, createFlow, describeFlow, dryRun, duplicateFlow, getActiveFlowId, getLoginFlows, saveLoginFlows, setActiveFlowId, type LoginFlowDefinition, } from "@/lib/login-flow" /** * The flow list, split into the shapes and the roles. * * Two groups rather than one list of nine. The role flows are all the same shape * and differ only in what they hand the session, so reading them as a set is the * point; mixed in with "Straight in" and "Pick an app" they looked like six more * variations on sign-in, which is the opposite of what they are. */ function FlowOptions({ flows }: { flows: LoginFlowDefinition[] }) { const roles = flows.filter(flow => flow.id.startsWith("role-")) const rest = flows.filter(flow => !flow.id.startsWith("role-")) if (roles.length === 0) { return ( <> {rest.map(item => ( {item.name} ))} ) } return ( <> Shapes {rest.map(item => ( {item.name} ))} User roles {roles.map(item => ( {item.name} ))} ) } export function SignInFlowsClient() { const navigate = useNavigate() const logOut = useLogOut() const [flows, setFlows] = React.useState(getLoginFlows) const [activeId, setActiveId] = React.useState(getActiveFlowId) const [editingId, setEditingId] = React.useState(getActiveFlowId) const [selection, setSelection] = React.useState({ kind: "flow" }) const [testIdentifier, setTestIdentifier] = React.useState("") const [dirty, setDirty] = React.useState(false) const [saved, setSaved] = React.useState(false) const flow = flows.find(current => current.id === editingId) ?? flows[0] function markDirty() { setDirty(true) setSaved(false) } const updateFlow = React.useCallback((next: LoginFlowDefinition) => { setFlows(current => current.map(item => (item.id === next.id ? next : item))) setDirty(true) setSaved(false) }, []) const persist = React.useCallback(() => { saveLoginFlows(flows) setDirty(false) setSaved(true) }, [flows]) /** * Point the sign-in page at a flow, now. * * Not deferred to Save, because this is a pointer rather than an edit: there is * no half-written state to protect, and the only thing deferring it bought was a * choice that looked applied and was not. */ const chooseActive = React.useCallback((id: string) => { setActiveId(id) setActiveFlowId(id) }, []) /** Only run the tester when there is something to test. */ const statuses = testIdentifier.trim() ? dryRun(flow, testIdentifier) : null function openFlow(id: string) { setEditingId(id) setSelection({ kind: "flow" }) } return (
{/* ── Toolbar ──────────────────────────────────────────── */}
{/* Whether the flow on screen is the one sign-in runs. The badge, and not a second selector: two pickers meant the page could hold two answers at once, and the primary action had to silently pick one. */} {flow.id === activeId ? ( ) : null}