import { useEffect, useRef, useState } from "react"; import type { CompanySecret, EnvBinding } from "@paperclipai/shared"; import { X } from "lucide-react"; import { cn } from "../lib/utils"; const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; type Row = { key: string; source: "plain" | "secret"; plainValue: string; secretId: string; }; function toRows(rec: Record | null | undefined): Row[] { if (!rec || typeof rec !== "object") { return [{ key: "", source: "plain", plainValue: "", secretId: "" }]; } const entries = Object.entries(rec).map(([key, binding]) => { if (typeof binding === "string") { return { key, source: "plain" as const, plainValue: binding, secretId: "" }; } if ( typeof binding === "object" && binding !== null && "type" in binding && (binding as { type?: unknown }).type === "secret_ref" ) { const record = binding as { secretId?: unknown }; return { key, source: "secret" as const, plainValue: "", secretId: typeof record.secretId === "string" ? record.secretId : "", }; } if ( typeof binding === "object" && binding !== null && "type" in binding && (binding as { type?: unknown }).type === "plain" ) { const record = binding as { value?: unknown }; return { key, source: "plain" as const, plainValue: typeof record.value === "string" ? record.value : "", secretId: "", }; } return { key, source: "plain" as const, plainValue: "", secretId: "" }; }); return [...entries, { key: "", source: "plain", plainValue: "", secretId: "" }]; } export function EnvVarEditor({ value, secrets, onCreateSecret, onChange, }: { value: Record; secrets: CompanySecret[]; onCreateSecret: (name: string, value: string) => Promise; onChange: (env: Record | undefined) => void; }) { const [rows, setRows] = useState(() => toRows(value)); const [sealError, setSealError] = useState(null); const valueRef = useRef(value); const emittingRef = useRef(false); useEffect(() => { if (emittingRef.current) { emittingRef.current = false; valueRef.current = value; return; } if (value !== valueRef.current) { valueRef.current = value; setRows(toRows(value)); } }, [value]); function emit(nextRows: Row[]) { const rec: Record = {}; for (const row of nextRows) { const key = row.key.trim(); if (!key) continue; if (row.source === "secret") { if (row.secretId) { rec[key] = { type: "secret_ref", secretId: row.secretId, version: "latest" }; } else { rec[key] = { type: "plain", value: row.plainValue }; } } else { rec[key] = { type: "plain", value: row.plainValue }; } } emittingRef.current = true; onChange(Object.keys(rec).length > 0 ? rec : undefined); } function updateRow(index: number, patch: Partial) { const withPatch = rows.map((row, rowIndex) => (rowIndex === index ? { ...row, ...patch } : row)); if ( withPatch[withPatch.length - 1].key || withPatch[withPatch.length - 1].plainValue || withPatch[withPatch.length - 1].secretId ) { withPatch.push({ key: "", source: "plain", plainValue: "", secretId: "" }); } setRows(withPatch); emit(withPatch); } function removeRow(index: number) { const next = rows.filter((_, rowIndex) => rowIndex !== index); if ( next.length === 0 || next[next.length - 1].key || next[next.length - 1].plainValue || next[next.length - 1].secretId ) { next.push({ key: "", source: "plain", plainValue: "", secretId: "" }); } setRows(next); emit(next); } function defaultSecretName(key: string) { return key .trim() .toLowerCase() .replace(/[^a-z0-9_]+/g, "_") .replace(/^_+|_+$/g, "") .slice(0, 64); } async function sealRow(index: number) { const row = rows[index]; if (!row) return; const key = row.key.trim(); const plain = row.plainValue; if (!key || plain.length === 0) return; const suggested = defaultSecretName(key) || "secret"; const name = window.prompt("Secret name", suggested)?.trim(); if (!name) return; try { setSealError(null); const created = await onCreateSecret(name, plain); updateRow(index, { source: "secret", secretId: created.id }); } catch (error) { setSealError(error instanceof Error ? error.message : "Failed to create secret"); } } return (
{rows.map((row, index) => { const isTrailing = index === rows.length - 1 && !row.key && !row.plainValue && !row.secretId; return (
updateRow(index, { key: event.target.value })} /> {row.source === "secret" ? ( <> ) : ( <> updateRow(index, { plainValue: event.target.value })} /> )} {!isTrailing ? ( ) : (
)}
); })} {sealError &&

{sealError}

}

PAPERCLIP_* variables are injected automatically at runtime.

); }