From b341a3675e2f57eb57b326eb28585fb8e5b43a0a Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 26 Sep 2025 14:18:24 -0400 Subject: [PATCH] added user rig submission, logout | initial --- app/api/my/rigs/[id]/route.ts | 76 ++++++ app/api/my/rigs/route.ts | 83 ++++++ app/auth/sign-in/page.tsx | 2 +- app/auth/sign-up/page.tsx | 2 +- app/components/SignOutButton.tsx | 43 ++++ app/my-rigs/page.tsx | 418 ------------------------------- app/my/rigs/page.tsx | 407 ++++++++++++++++++++++++++++++ middleware.ts | 24 ++ 8 files changed, 635 insertions(+), 420 deletions(-) create mode 100644 app/api/my/rigs/[id]/route.ts create mode 100644 app/api/my/rigs/route.ts create mode 100644 app/components/SignOutButton.tsx delete mode 100644 app/my-rigs/page.tsx create mode 100644 app/my/rigs/page.tsx create mode 100644 middleware.ts diff --git a/app/api/my/rigs/[id]/route.ts b/app/api/my/rigs/[id]/route.ts new file mode 100644 index 00000000..f838a11b --- /dev/null +++ b/app/api/my/rigs/[id]/route.ts @@ -0,0 +1,76 @@ +// app/api/my/rigs/[id]/route.ts +import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +const BASE = process.env.DIRECTUS_URL!; +if (!BASE) console.warn("[my/rigs/:id] Missing DIRECTUS_URL"); + +function bearerFromCookies() { + const at = cookies().get("ma_at")?.value; + if (!at) throw new Error("Not authenticated"); + return `Bearer ${at}`; +} + +async function df(path: string, init?: RequestInit) { + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { + Accept: "application/json", + Authorization: bearerFromCookies(), + "Content-Type": "application/json", + ...(init?.headers || {}), + }, + cache: "no-store", + }); + const text = await res.text(); + let json: any = null; + try { json = text ? JSON.parse(text) : null; } catch {} + if (!res.ok) throw new Error(`Directus error ${res.status}: ${text || res.statusText}`); + return json ?? {}; +} + +export async function GET(_req: NextRequest, ctx: any) { + try { + const id = ctx?.params?.id; + const fields = [ + "id","name","rig_type","notes","meta", + "laser_source.id","laser_source.make","laser_source.model", + "laser_scan_lens.id","laser_scan_lens.field_size","laser_scan_lens.f_number", + "laser_focus_lens.id","laser_focus_lens.name", + "laser_scan_lens_apt.id","laser_scan_lens_apt.name", + "laser_scan_lens_exp.id","laser_scan_lens_exp.multiplier", + "laser_software.id","laser_software.name", + "date_created","date_updated" + ].join(","); + + const { data } = await df(`/items/rigs/${id}?fields=${encodeURIComponent(fields)}`); + return NextResponse.json({ ok: true, data }); + } catch (e: any) { + const msg = e?.message || "Load failed"; + const code = msg.includes("Not authenticated") ? 401 : (msg.includes("404") ? 404 : 500); + return NextResponse.json({ error: msg }, { status: code }); + } +} + +export async function PATCH(req: NextRequest, ctx: any) { + try { + const id = ctx?.params?.id; + const body = await req.json(); + const { data } = await df(`/items/rigs/${id}`, { method: "PATCH", body: JSON.stringify(body) }); + return NextResponse.json({ ok: true, id: data?.id ?? id }); + } catch (e: any) { + const msg = e?.message || "Update failed"; + return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 }); + } +} + +export async function DELETE(_req: NextRequest, ctx: any) { + try { + const id = ctx?.params?.id; + await df(`/items/rigs/${id}`, { method: "DELETE" }); + return NextResponse.json({ ok: true }); + } catch (e: any) { + const msg = e?.message || "Delete failed"; + return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 }); + } +} diff --git a/app/api/my/rigs/route.ts b/app/api/my/rigs/route.ts new file mode 100644 index 00000000..8c80ca3e --- /dev/null +++ b/app/api/my/rigs/route.ts @@ -0,0 +1,83 @@ +// app/api/my/rigs/route.ts +import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +const BASE = process.env.DIRECTUS_URL!; +if (!BASE) console.warn("[my/rigs] Missing DIRECTUS_URL"); + +// Pull the user's Directus access token from cookies +function bearerFromCookies() { + const at = cookies().get("ma_at")?.value; + if (!at) throw new Error("Not authenticated"); + return `Bearer ${at}`; +} + +async function df(path: string, init?: RequestInit) { + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { + Accept: "application/json", + Authorization: bearerFromCookies(), + "Content-Type": "application/json", + ...(init?.headers || {}), + }, + cache: "no-store", + }); + const text = await res.text(); + let json: any = null; + try { json = text ? JSON.parse(text) : null; } catch {} + if (!res.ok) throw new Error(`Directus error ${res.status}: ${text || res.statusText}`); + return json ?? {}; +} + +export async function GET(_req: NextRequest) { + try { + // Ownership is enforced by Directus policy (owner = $CURRENT_USER) + const fields = [ + "id","name","rig_type", + "laser_source.id","laser_source.make","laser_source.model", + "laser_scan_lens.id","laser_scan_lens.field_size","laser_scan_lens.f_number", + "laser_focus_lens.id","laser_focus_lens.name", + "laser_scan_lens_apt.id","laser_scan_lens_apt.name", + "laser_scan_lens_exp.id","laser_scan_lens_exp.multiplier", + "laser_software.id","laser_software.name", + "date_created","date_updated" + ].join(","); + + const { data } = await df(`/items/rigs?fields=${encodeURIComponent(fields)}&limit=200&sort=-date_updated`); + return NextResponse.json({ ok: true, data }); + } catch (e: any) { + const msg = e?.message || "Failed to load rigs"; + return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + + // Minimal validation + if (!body?.name) return NextResponse.json({ error: "Missing name" }, { status: 400 }); + if (!body?.rig_type) return NextResponse.json({ error: "Missing rig_type" }, { status: 400 }); + + // owner is set by Directus preset (owner: $CURRENT_USER) + const payload = { + name: body.name, + rig_type: body.rig_type, // "fiber" | "co2_galvo" | "co2_gantry" | "uv" + laser_source: body.laser_source ?? null, + laser_scan_lens: body.laser_scan_lens ?? null, + laser_focus_lens: body.laser_focus_lens ?? null, + laser_scan_lens_apt: body.laser_scan_lens_apt ?? null, + laser_scan_lens_exp: body.laser_scan_lens_exp ?? null, + laser_software: body.laser_software ?? null, + notes: body.notes ?? null, + meta: body.meta ?? null, // future: measured focal distance, spot size, etc. + }; + + const { data } = await df(`/items/rigs`, { method: "POST", body: JSON.stringify(payload) }); + return NextResponse.json({ ok: true, id: data?.id }); + } catch (e: any) { + const msg = e?.message || "Create failed"; + return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 }); + } +} diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index 2b3b42d4..af783714 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -21,7 +21,7 @@ export default function SignInPage() { }); const j = await res.json(); if (!res.ok) throw new Error(j?.error || "Login failed"); - r.replace("/my-rigs"); + r.replace("/my/rigs"); } catch (e: any) { setErr(e?.message || "Error"); } finally { diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index ee38e12a..d6ebab34 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -35,7 +35,7 @@ export default function SignUpPage() { const lj = await login.json(); if (!login.ok) throw new Error(lj?.error || "Auto login failed"); - r.replace("/my-rigs"); // or wherever you want to land + r.replace("/my/rigs"); // or wherever you want to land } catch (e: any) { setErr(e?.message || "Error"); } finally { diff --git a/app/components/SignOutButton.tsx b/app/components/SignOutButton.tsx new file mode 100644 index 00000000..2484a60e --- /dev/null +++ b/app/components/SignOutButton.tsx @@ -0,0 +1,43 @@ +// components/SignOutButton.tsx +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export default function SignOutButton({ + className, + redirectTo = "/sign-in", + children = "Sign out", +}: { + className?: string; + redirectTo?: string; + children?: React.ReactNode; +}) { + const r = useRouter(); + const [busy, setBusy] = useState(false); + + async function onClick() { + if (busy) return; + setBusy(true); + try { + await fetch("/api/auth/logout", { method: "POST" }); + r.push(redirectTo); + r.refresh(); + } catch { + // ignore; worst case user can hard-refresh + } finally { + setBusy(false); + } + } + + return ( + + ); +} diff --git a/app/my-rigs/page.tsx b/app/my-rigs/page.tsx deleted file mode 100644 index 12844699..00000000 --- a/app/my-rigs/page.tsx +++ /dev/null @@ -1,418 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { useForm, type UseFormRegister } from "react-hook-form"; - -// ───────────────────────────────────────────────────────────── -// Types -// ───────────────────────────────────────────────────────────── - -type RigKind = "fiber" | "uv" | "co2_galvo" | "co2_gantry"; - -type Rig = { - id: string; - name: string; - kind: RigKind; - - // core components - source?: { id: string; label: string } | null; - scan_lens?: { id: string; label: string } | null; // fiber/uv/co2_galvo - focus_lens?: { id: string; label: string } | null; // co2_gantry - scan_aperture?: { id: string; label: string } | null; // laser_scan_lens_apt - beam_expander?: { id: string; label: string } | null; // laser_scan_lens_exp - software?: { id: string; label: string } | null; - - created_at: number; - updated_at: number; -}; - -type Opt = { id: string; label: string }; - -// ───────────────────────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────────────────────── - -const STORAGE_KEY = "makearmy_rigs_v1"; - -const TARGET_FOR_KIND: Record = { - fiber: "settings_fiber", - uv: "settings_uv", - co2_galvo: "settings_co2gal", - co2_gantry: "settings_co2gan", -}; - -function uuid() { - // fine for local client IDs - return Math.random().toString(36).slice(2) + Date.now().toString(36); -} - -function loadRigs(): Rig[] { - try { - const raw = localStorage.getItem(STORAGE_KEY); - return raw ? (JSON.parse(raw) as Rig[]) : []; - } catch { - return []; - } -} - -function saveRigs(rigs: Rig[]) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(rigs)); -} - -function useOptions(path: string) { - const [opts, setOpts] = useState([]); - const [loading, setLoading] = useState(false); - const [q, setQ] = useState(""); - - useEffect(() => { - let alive = true; - async function run() { - setLoading(true); - try { - const url = `/api/options/${path}${path.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`; - const res = await fetch(url, { cache: "no-store" }); - const json = await res.json(); - if (!alive) return; - setOpts((json?.data as Opt[]) ?? []); - } catch { - if (!alive) return; - setOpts([]); - } finally { - if (!alive) return; - setLoading(false); - } - } - run(); - return () => { - alive = false; - }; - }, [path, q]); - - return { opts, loading, setQ }; -} - -// Reusable UI bits -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-

{title}

- {children} -
- ); -} - -function FilterableSelect({ - label, - name, - register, - options, - loading, - onQuery, - placeholder = "—", - disabled, -}: { - label: string; - name: string; - register: UseFormRegister; - options: Opt[]; - loading?: boolean; - onQuery?: (q: string) => void; - placeholder?: string; - disabled?: boolean; -}) { - const [filter, setFilter] = useState(""); - useEffect(() => { - onQuery?.(filter); - }, [filter, onQuery]); - - const filtered = useMemo(() => { - if (!filter) return options; - const f = filter.toLowerCase(); - return options.filter((o) => o.label.toLowerCase().includes(f)); - }, [options, filter]); - - return ( -
- - setFilter(e.target.value)} - disabled={disabled} - /> - -
- ); -} - -// ───────────────────────────────────────────────────────────── -// Page -// ───────────────────────────────────────────────────────────── - -export default function MyRigsPage() { - const [rigs, setRigs] = useState([]); - const [editingId, setEditingId] = useState(null); - - // form - const { register, handleSubmit, watch, reset, setValue, formState: { isSubmitting } } = useForm({ - defaultValues: { - name: "", - kind: "fiber" as RigKind, - source: "", - scan_lens: "", - focus_lens: "", - scan_aperture: "", - beam_expander: "", - software: "", - }, - }); - - // load saved rigs - useEffect(() => { - setRigs(loadRigs()); - }, []); - - // kind → target (for filtering laser source & lens APIs) - const kind = watch("kind") as RigKind; - const target = TARGET_FOR_KIND[kind]; - - const isGalvo = kind === "fiber" || kind === "uv" || kind === "co2_galvo"; - const isGantry = kind === "co2_gantry"; - - // options - const sourceOpts = useOptions(`laser_source?target=${target}`); - const lensOpts = useOptions(`lens?target=${target}`); // scan lens for galvo, focus lens for gantry - const softOpts = useOptions("laser_software"); - const aptOpts = useOptions("laser_scan_lens_apt"); - const expOpts = useOptions("laser_scan_lens_exp"); - - // helpers to resolve id→label from fetched option lists - const findLabel = (id: string | undefined, opts: Opt[]) => - id ? (opts.find((o) => o.id === id)?.label || id) : ""; - - function onEdit(rig: Rig) { - setEditingId(rig.id); - reset({ - name: rig.name, - kind: rig.kind, - source: rig.source?.id || "", - scan_lens: rig.scan_lens?.id || "", - focus_lens: rig.focus_lens?.id || "", - scan_aperture: rig.scan_aperture?.id || "", - beam_expander: rig.beam_expander?.id || "", - software: rig.software?.id || "", - }); - } - - function onDelete(id: string) { - const next = rigs.filter((r) => r.id !== id); - setRigs(next); - saveRigs(next); - if (editingId === id) { - setEditingId(null); - reset(); - } - } - - function onClearForm() { - setEditingId(null); - reset({ - name: "", - kind: "fiber", - source: "", - scan_lens: "", - focus_lens: "", - scan_aperture: "", - beam_expander: "", - software: "", - }); - } - - const onSubmit = handleSubmit((vals) => { - // basic validation - if (!vals.name?.trim()) { - alert("Please provide a name for your Rig."); - return; - } - if (!vals.source) { - alert("Please select a Laser Source."); - return; - } - if (isGalvo && !vals.scan_lens) { - alert("Please select a Scan Lens."); - return; - } - if (isGantry && !vals.focus_lens) { - alert("Please select a Focus Lens."); - return; - } - - const now = Date.now(); - const rig: Rig = { - id: editingId || uuid(), - name: String(vals.name).trim(), - kind: vals.kind as RigKind, - source: vals.source ? { id: vals.source, label: findLabel(vals.source, sourceOpts.opts) } : null, - scan_lens: vals.scan_lens ? { id: vals.scan_lens, label: findLabel(vals.scan_lens, lensOpts.opts) } : null, - focus_lens: vals.focus_lens ? { id: vals.focus_lens, label: findLabel(vals.focus_lens, lensOpts.opts) } : null, - scan_aperture: vals.scan_aperture ? { id: vals.scan_aperture, label: findLabel(vals.scan_aperture, aptOpts.opts) } : null, - beam_expander: vals.beam_expander ? { id: vals.beam_expander, label: findLabel(vals.beam_expander, expOpts.opts) } : null, - software: vals.software ? { id: vals.software, label: findLabel(vals.software, softOpts.opts) } : null, - created_at: editingId ? rigs.find((r) => r.id === editingId)?.created_at ?? now : now, - updated_at: now, - }; - - const next = editingId - ? rigs.map((r) => (r.id === editingId ? rig : r)) - : [rig, ...rigs]; - - setRigs(next); - saveRigs(next); - setEditingId(null); - reset(); - }); - - return ( -
-

My Rigs

- -
-
-
-
- - -
-
- - -
-
- -
- - - -
- -
- - - -
- -
- - -
-
-
- -
- {rigs.length === 0 ? ( -

No rigs yet. Create one above.

- ) : ( -
    - {rigs.map((r) => ( -
  • -
    -
    -
    {r.name}
    -
    - Type: {r.kind.replace("_", " ")} -
    -
    -
    Source: {r.source?.label || "—"}
    - {r.scan_lens &&
    Scan Lens: {r.scan_lens.label}
    } - {r.focus_lens &&
    Focus Lens: {r.focus_lens.label}
    } - {r.scan_aperture &&
    Scan Head Aperture: {r.scan_aperture.label}
    } - {r.beam_expander &&
    Beam Expander: {r.beam_expander.label}
    } - {r.software &&
    Software: {r.software.label}
    } -
    -
    -
    - - -
    -
    -
  • - ))} -
- )} -
- -

- Rigs are currently saved locally in your browser. We can switch this to Directus-backed storage once - auth is in place; the UI can stay the same. -

-
- ); -} diff --git a/app/my/rigs/page.tsx b/app/my/rigs/page.tsx new file mode 100644 index 00000000..b156f3b5 --- /dev/null +++ b/app/my/rigs/page.tsx @@ -0,0 +1,407 @@ +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useForm, Controller } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; + +// UI bits (shadcn-style) +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Separator } from "@/components/ui/separator"; +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "@/components/ui/use-toast"; +import { Loader2, Plus, Save, Settings2, X } from "lucide-react"; +import { SignOutButton } from "@/components/SignOutButton"; + +// simple fetch helper +async function jfetch(url: string, init?: RequestInit): Promise { + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers || {}), + }, + cache: "no-store", + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || res.statusText); + } + return (await res.json()) as T; +} + +/** ---------- Types for option endpoints ---------- */ +type Opt = { id: string; label: string }; + +// Finder for options with free-text filter +function useOptions(endpoint: string) { + const [opts, setOpts] = useState([]); + const [loading, setLoading] = useState(false); + const [q, setQ] = useState(""); + + useEffect(() => { + let alive = true; + setLoading(true); + const url = `/api/options/${endpoint}${endpoint.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`; + fetch(url, { cache: "no-store" }) + .then((r) => r.json()) + .then((j) => { + if (!alive) return; + setOpts((j?.data as Opt[]) ?? []); + }) + .catch(() => { + if (!alive) return; + setOpts([]); + }) + .finally(() => alive && setLoading(false)); + return () => { + alive = false; + }; + }, [endpoint, q]); + + return { opts, loading, setQ }; +} + +/** ---------- Form schema ---------- */ +const RigSchema = z.object({ + name: z.string().min(1, "Rig name required"), + notes: z.string().optional(), + // Relations (IDs) + laser_source: z.string().min(1, "Laser source required"), + laser_software: z.string().min(1, "Software required"), + // One of scan lens (with optional aperture & expander) OR focus lens + laser_scan_lens: z.string().optional(), + laser_scan_lens_apt: z.string().optional(), + laser_scan_lens_exp: z.string().optional(), + laser_focus_lens: z.string().optional(), +}); + +type RigForm = z.infer; + +/** ---------- Little select with filter ---------- */ +function FilterableSelect({ + label, + value, + onValueChange, + placeholder = "Select…", + endpoint, // e.g. "laser_source?target=settings_fiber" +}: { + label: string; + value?: string; + onValueChange: (v: string) => void; + placeholder?: string; + endpoint: string; +}) { + const { opts, loading, setQ } = useOptions(endpoint); + + return ( +
+ + setQ(e.target.value)} + className="mb-1" + /> + +
+ ); +} + +/** ---------- Page ---------- */ +export default function MyRigsPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [saving, setSaving] = useState(false); + const [list, setList] = useState([]); + const [loadingList, setLoadingList] = useState(true); + + // Which lens type UI is active: "scan" vs "focus" + const [lensMode, setLensMode] = useState<"scan" | "focus">("scan"); + + const form = useForm({ + resolver: zodResolver(RigSchema), + defaultValues: { + name: "", + notes: "", + laser_source: "", + laser_software: "", + laser_scan_lens: "", + laser_scan_lens_apt: "", + laser_scan_lens_exp: "", + laser_focus_lens: "", + }, + }); + + const sourceTarget = "settings_fiber"; // for now default fiber; could be dynamic later + + // Options hooks + const srcs = useOptions(`laser_source?target=${sourceTarget}`); + const soft = useOptions("laser_software"); + const scanLens = useOptions(`lens?target=${sourceTarget}`); // F-theta + const apertures = useOptions("laser_scan_lens_apt"); + const expanders = useOptions("laser_scan_lens_exp"); + const focusLens = useOptions("lens?target=settings_co2gan"); // focus lens for gantry + + // Load my rigs + useEffect(() => { + let alive = true; + (async () => { + try { + setLoadingList(true); + const res = await jfetch<{ data: any[] }>("/api/rigs/list"); + if (!alive) return; + setList(res.data || []); + } catch (e: any) { + console.warn("[my/rigs] list error", e?.message); + } finally { + alive = false ? undefined : setLoadingList(false); + } + })(); + return () => { + alive = false; + }; + }, []); + + async function onSave(data: RigForm) { + try { + setSaving(true); + + // If in focus mode, ensure scan-lens fields are cleared and vice versa. + const payload: any = { ...data }; + if (lensMode === "focus") { + payload.laser_scan_lens = null; + payload.laser_scan_lens_apt = null; + payload.laser_scan_lens_exp = null; + } else { + payload.laser_focus_lens = null; + } + + const res = await jfetch<{ ok: boolean; id: string }>("/api/rigs/save", { + method: "POST", + body: JSON.stringify(payload), + }); + + toast({ + title: "Rig saved", + description: "Your rig has been saved to your account.", + }); + + // refresh list + const updated = await jfetch<{ data: any[] }>("/api/rigs/list"); + setList(updated.data || []); + + // clear form (optional) + form.reset({ + name: "", + notes: "", + laser_source: "", + laser_software: "", + laser_scan_lens: "", + laser_scan_lens_apt: "", + laser_scan_lens_exp: "", + laser_focus_lens: lensMode === "focus" ? "" : "", + }); + } catch (e: any) { + toast({ + title: "Save failed", + description: e?.message || "Unknown error", + variant: "destructive", + }); + } finally { + setSaving(false); + } + } + + function LensModeToggle() { + return ( +
+ setLensMode("scan")} + > + Scan Lens + + setLensMode("focus")} + > + Focus Lens + +
+ ); + } + + return ( +
+
+

My Rigs

+ +
+ + + + + + Build a Rig + + + + +
+
+ + + {form.formState.errors.name && ( +

+ {form.formState.errors.name.message} +

+ )} +
+ + form.setValue("laser_source", v)} + /> + + form.setValue("laser_software", v)} + /> + + {lensMode === "scan" ? ( + <> + form.setValue("laser_scan_lens", v)} + /> + form.setValue("laser_scan_lens_apt", v)} + /> + form.setValue("laser_scan_lens_exp", v)} + /> + + ) : ( + <> + form.setValue("laser_focus_lens", v)} + /> +
+
+ Focus lenses don’t use F-numbers; just pick the lens by name. +
+
+ + )} + +
+ +