// --------------------------------------------------------------------------- // Auth-method LIST editor — the add-flow's "How does this API authenticate?" // section, as a list. Every plugin registers EVERY declared method (P6: add // without auth, connect later), so the add flow edits a list of generic // `AuthTemplateEditorValue` rows seeded from detection (spec analysis, endpoint // probe, …) with add/remove and a per-row `AuthTemplateEditor`. // // Composition: `useAuthMethodList` is the headless row state (seeding, // edit/add/remove); `AuthMethodListEditor` is the presentation. Plugins own // only the codec at the edges — seeds in (detection → editor values) and // submit out (editor values → wire templates). // --------------------------------------------------------------------------- import { useCallback, useEffect, useRef, useState } from "react"; import { PlusIcon, XIcon } from "lucide-react"; import { Button } from "./button"; import { FieldLabel } from "./field"; import { AuthTemplateEditor, emptyApiKeyValue, type AuthTemplateEditorKind, type AuthTemplateEditorPreset, type AuthTemplateEditorValue, } from "./auth-template-editor"; export interface AuthMethodSeed { readonly value: AuthTemplateEditorValue; /** The detected method's stable slug — an unedited seeded row submits with * its EXACT original slug (preserving connections bound against it). */ readonly slug?: string; /** Detection label (e.g. the spec's security-scheme name) shown on the row. */ readonly label?: string; } export interface AuthMethodRow { readonly value: AuthTemplateEditorValue; readonly seedSlug?: string; readonly seedLabel?: string; } export interface AuthMethodListState { readonly rows: readonly AuthMethodRow[]; readonly setRowAt: (index: number, next: AuthTemplateEditorValue) => void; readonly removeRowAt: (index: number) => void; readonly addRow: () => void; } /** Headless row state for the method list. Re-seeds whenever `seeds` changes * identity (detection results are stable per analysis), discarding edits — * fresh detection means a fresh starting set. */ export function useAuthMethodList(seeds: readonly AuthMethodSeed[]): AuthMethodListState { const [rows, setRows] = useState([]); const seededFromRef = useRef(null); useEffect(() => { if (seededFromRef.current === seeds) return; seededFromRef.current = seeds; setRows( seeds.map( (seed: AuthMethodSeed): AuthMethodRow => ({ value: seed.value, ...(seed.slug !== undefined ? { seedSlug: seed.slug } : {}), ...(seed.label !== undefined ? { seedLabel: seed.label } : {}), }), ), ); }, [seeds]); const setRowAt = useCallback((index: number, next: AuthTemplateEditorValue) => { setRows((current: readonly AuthMethodRow[]) => current.map((row: AuthMethodRow, i: number) => (i === index ? { ...row, value: next } : row)), ); }, []); const removeRowAt = useCallback((index: number) => { setRows((current: readonly AuthMethodRow[]) => current.filter((_row: AuthMethodRow, i: number) => i !== index), ); }, []); const addRow = useCallback(() => { setRows((current: readonly AuthMethodRow[]) => [...current, { value: emptyApiKeyValue() }]); }, []); return { rows, setRowAt, removeRowAt, addRow }; } export interface AuthMethodListEditorProps { readonly list: AuthMethodListState; readonly title?: string; /** Shown when the list is empty (e.g. "No authentication detected. …"). */ readonly emptyHint?: string; /** Shown under the list when at least one row exists. */ readonly footerHint?: string; /** Per-row editor restrictions — see `AuthTemplateEditorProps`. */ readonly allowedKinds?: readonly AuthTemplateEditorKind[]; readonly presets?: readonly AuthTemplateEditorPreset[]; readonly oauthMetadata?: "editable" | "discovered"; } export function AuthMethodListEditor(props: AuthMethodListEditorProps) { const { list, allowedKinds, presets, oauthMetadata } = props; return (
{props.title ?? "How does this API authenticate?"}
{list.rows.length === 0 ? ( props.emptyHint ? (

{props.emptyHint}

) : null ) : (
{list.rows.map((row: AuthMethodRow, index: number) => (
Method {index + 1} {row.seedLabel ? ` · ${row.seedLabel}` : ""}
list.setRowAt(index, next)} {...(allowedKinds ? { allowedKinds } : {})} {...(presets ? { presets } : {})} {...(oauthMetadata ? { oauthMetadata } : {})} />
))}
)} {list.rows.length > 0 && props.footerHint ? (

{props.footerHint}

) : null}
); }