import { createContext, use, useId, useMemo, useState, type CSSProperties, type ReactNode,} from "react";import { useAtomSet } from "@effect/atom-react";import * as Exit from "effect/Exit"; import { createConnection } from "../api/atoms";import { connectionWriteKeys } from "../api/reactivity-keys";import { AuthTemplateSlug, ConnectionName, IntegrationSlug, type Owner,} from "@executor-js/sdk/shared";import { Button, type buttonVariants } from "../components/button";import { Field, FieldError, FieldLabel } from "../components/field";import { Input } from "../components/input";import type { VariantProps } from "class-variance-authority"; import { secretValueInputType } from "./secret-input";import { getUniqueSecretId, isSecretIdTaken } from "./secret-id"; // ---------------------------------------------------------------------------// Connection-create form (v2) — successor to v1's secret form.//// v1 minted a standalone secret bound to a scope. v2 mints a Connection: a// value pasted by the user, saved under an owner (Personal | Workspace) for one// integration's auth template. The compound API (`state`/`actions`/`meta`) is// kept so existing call sites compose `SecretForm.*` parts; the payload is now a// `createConnection` mutation.// --------------------------------------------------------------------------- type SubmitStatus = | { readonly kind: "idle" } | { readonly kind: "submitting" } | { readonly kind: "error"; readonly message: string }; interface SecretFormState { readonly name: string; readonly value: string; readonly idOverride: string | null; readonly provider: string; readonly revealed: boolean; readonly status: SubmitStatus;} interface SecretFormActions { readonly setName: (v: string) => void; readonly setValue: (v: string) => void; readonly setIdOverride: (v: string) => void; readonly setProvider: (v: string) => void; readonly toggleReveal: () => void; readonly submit: () => Promise<void>;} interface SecretFormMeta { readonly id: string; readonly duplicateError: string | null; readonly canSubmit: boolean;} interface SecretFormContextValue { readonly state: SecretFormState; readonly actions: SecretFormActions; readonly meta: SecretFormMeta;} const SecretFormContext = createContext<SecretFormContextValue | null>(null); function useSecretForm(): SecretFormContextValue { const ctx = use(SecretFormContext); if (!ctx) { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React context invariant surfaces programmer misuse during render throw new Error("SecretForm parts must be rendered inside <SecretForm.Provider>"); } return ctx;} // ---------------------------------------------------------------------------// Provider// --------------------------------------------------------------------------- interface SecretFormProviderProps { readonly existingSecretIds: readonly string[]; readonly suggestedName?: string; readonly fallbackId?: string; /** Force the connection name to a pre-allocated value. */ readonly initialIdOverride?: string; readonly initialProvider?: string; /** Owner the new connection is saved under (Personal | Workspace). */ readonly owner: Owner; /** The integration the connection authenticates. */ readonly integration: IntegrationSlug; /** Which of the integration's auth methods the value applies through. */ readonly template: AuthTemplateSlug; readonly onCreated: (connectionName: string) => void; readonly children: ReactNode;} function SecretFormProvider(props: SecretFormProviderProps) { const { existingSecretIds, suggestedName = "", fallbackId = "credential", initialIdOverride, initialProvider = "auto", owner, integration, template, onCreated, children, } = props; const doCreate = useAtomSet(createConnection, { mode: "promiseExit" }); const [state, setState] = useState<SecretFormState>(() => ({ name: suggestedName, value: "", idOverride: initialIdOverride ?? null, provider: initialProvider, revealed: false, status: { kind: "idle" }, })); const baseName = state.name || suggestedName; const autoId = useMemo( () => getUniqueSecretId(baseName, existingSecretIds, fallbackId), [baseName, existingSecretIds, fallbackId], ); const id = state.idOverride ?? autoId; const duplicateError = state.idOverride !== null && isSecretIdTaken(state.idOverride, existingSecretIds) ? "Name already exists" : null; const displayName = state.name.trim() || suggestedName.trim(); const canSubmit = id.trim() !== "" && state.value.trim() !== "" && duplicateError === null && state.status.kind !== "submitting"; const submit = async () => { if (!canSubmit) return; setState((s) => ({ ...s, status: { kind: "submitting" } })); const exit = await doCreate({ payload: { owner, name: ConnectionName.make(id.trim()), integration, template, identityLabel: displayName || id.trim(), value: state.value.trim(), }, reactivityKeys: connectionWriteKeys, }); if (Exit.isFailure(exit)) { setState((s) => ({ ...s, status: { kind: "error", message: "Failed to save credential", }, })); return; } onCreated(id.trim()); }; const value: SecretFormContextValue = { state, actions: { setName: (v) => setState((s) => ({ ...s, name: v })), setValue: (v) => setState((s) => ({ ...s, value: v })), setIdOverride: (v) => setState((s) => ({ ...s, idOverride: v })), setProvider: (v) => setState((s) => ({ ...s, provider: v })), toggleReveal: () => setState((s) => ({ ...s, revealed: !s.revealed })), submit, }, meta: { id, duplicateError, canSubmit }, }; return <SecretFormContext value={value}>{children}</SecretFormContext>;} // ---------------------------------------------------------------------------// Parts// --------------------------------------------------------------------------- function NameField(props: { label?: string; placeholder?: string }) { const { state, actions } = useSecretForm(); const inputId = useId(); return ( <Field> <FieldLabel htmlFor={inputId}>{props.label ?? "Name"}</FieldLabel> <Input id={inputId} value={state.name} onChange={(e) => actions.setName((e.target as HTMLInputElement).value)} placeholder={props.placeholder ?? "GitHub PAT"} /> </Field> );} function IdField(props: { placeholder?: string }) { const { actions, meta } = useSecretForm(); const inputId = useId(); return ( <Field> <FieldLabel htmlFor={inputId}>ID</FieldLabel> <Input id={inputId} value={meta.id} onChange={(e) => actions.setIdOverride((e.target as HTMLInputElement).value)} placeholder={props.placeholder ?? "github-token"} className="font-mono" /> {meta.duplicateError && <FieldError>{meta.duplicateError}</FieldError>} </Field> );} function ValueField(props: { revealable?: boolean; placeholder?: string; autoFocus?: boolean }) { const { state, actions } = useSecretForm(); const inputId = useId(); const revealable = props.revealable ?? false; const revealed = revealable && state.revealed; const errored = state.status.kind === "error"; return ( <Field> <FieldLabel htmlFor={inputId}>Value</FieldLabel> <div className="relative" data-ph-block> <Input id={inputId} type={secretValueInputType({ revealable, revealed })} value={state.value} onChange={(e) => actions.setValue((e.target as HTMLInputElement).value)} placeholder={props.placeholder ?? "ghp_xxxxxxxxxxxxxxxxxxxx"} autoFocus={props.autoFocus} autoComplete="new-password" className={revealable ? "pr-9 font-mono" : "font-mono"} style={ revealable && !revealed ? ({ WebkitTextSecurity: "disc" } as CSSProperties) : undefined } data-ph-block /> {revealable && ( <Button type="button" variant="ghost" size="icon-xs" className="absolute right-1 top-1/2 size-7 -translate-y-1/2 text-muted-foreground hover:text-foreground" onClick={actions.toggleReveal} aria-label={state.revealed ? "Hide value" : "Reveal value"} > <SecretVisibilityIcon revealed={state.revealed} /> </Button> )} </div> {errored && ( <FieldError>{state.status.kind === "error" ? state.status.message : ""}</FieldError> )} </Field> );} function ErrorBanner() { const { state } = useSecretForm(); if (state.status.kind !== "error") return null; return ( <div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2"> <p className="text-sm text-destructive">{state.status.message}</p> </div> );} type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>; function SubmitButton(props: ButtonProps & { children?: ReactNode }) { const { state, meta, actions } = useSecretForm(); const { children, disabled, onClick, ...rest } = props; const submitting = state.status.kind === "submitting"; return ( <Button {...rest} onClick={(e) => { onClick?.(e); if (!e.defaultPrevented) void actions.submit(); }} disabled={disabled || !meta.canSubmit} > {submitting ? "Saving…" : children} </Button> );} // ---------------------------------------------------------------------------// Reveal-eye icon (used by ValueField when `revealable`)// --------------------------------------------------------------------------- function SecretVisibilityIcon(props: { revealed: boolean }) { return props.revealed ? ( <svg viewBox="0 0 16 16" className="size-3.5" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" > <path d="M2 2l12 12" /> <path d="M6.5 6.5a2 2 0 0 0 3 3" /> <path d="M3.5 5.5C2.3 6.7 1.5 8 1.5 8s2.5 4.5 6.5 4.5c1 0 1.9-.3 2.7-.7" /> <path d="M10.7 10.7c2-1.4 3.3-3.2 3.8-3.7 0 0-2.5-5-6.5-5-.7 0-1.4.1-2 .4" /> </svg> ) : ( <svg viewBox="0 0 16 16" className="size-3.5" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" > <path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" /> <circle cx="8" cy="8" r="2" /> </svg> );} // ---------------------------------------------------------------------------// Compound export// --------------------------------------------------------------------------- export const SecretForm = { Provider: SecretFormProvider, NameField, IdField, ValueField, ErrorBanner, SubmitButton,}; export type { SecretFormProviderProps };
import {
createContext,
use,
useId,
useMemo,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import { useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import { createConnection } from "../api/atoms";
import { connectionWriteKeys } from "../api/reactivity-keys";
import {
AuthTemplateSlug,
ConnectionName,
IntegrationSlug,
type Owner,
} from "@executor-js/sdk/shared";
import { Button, type buttonVariants } from "../components/button";
import { Field, FieldError, FieldLabel } from "../components/field";
import { Input } from "../components/input";
import type { VariantProps } from "class-variance-authority";
import { secretValueInputType } from "./secret-input";
import { getUniqueSecretId, isSecretIdTaken } from "./secret-id";
// ---------------------------------------------------------------------------
// Connection-create form (v2) — successor to v1's secret form.
//
// v1 minted a standalone secret bound to a scope. v2 mints a Connection: a
// value pasted by the user, saved under an owner (Personal | Workspace) for one
// integration's auth template. The compound API (`state`/`actions`/`meta`) is
// kept so existing call sites compose `SecretForm.*` parts; the payload is now a
// `createConnection` mutation.
// ---------------------------------------------------------------------------
type SubmitStatus =
| { readonly kind: "idle" }
| { readonly kind: "submitting" }
| { readonly kind: "error"; readonly message: string };
interface SecretFormState {
readonly name: string;
readonly value: string;
readonly idOverride: string | null;
readonly provider: string;
readonly revealed: boolean;
readonly status: SubmitStatus;
}
interface SecretFormActions {
readonly setName: (v: string) => void;
readonly setValue: (v: string) => void;
readonly setIdOverride: (v: string) => void;
readonly setProvider: (v: string) => void;
readonly toggleReveal: () => void;
readonly submit: () => Promise<void>;
}
interface SecretFormMeta {
readonly id: string;
readonly duplicateError: string | null;
readonly canSubmit: boolean;
}
interface SecretFormContextValue {
readonly state: SecretFormState;
readonly actions: SecretFormActions;
readonly meta: SecretFormMeta;
}
const SecretFormContext = createContext<SecretFormContextValue | null>(null);
function useSecretForm(): SecretFormContextValue {
const ctx = use(SecretFormContext);
if (!ctx) {
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React context invariant surfaces programmer misuse during render
throw new Error("SecretForm parts must be rendered inside <SecretForm.Provider>");
}
return ctx;
}
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
interface SecretFormProviderProps {
readonly existingSecretIds: readonly string[];
readonly suggestedName?: string;
readonly fallbackId?: string;
/** Force the connection name to a pre-allocated value. */
readonly initialIdOverride?: string;
readonly initialProvider?: string;
/** Owner the new connection is saved under (Personal | Workspace). */
readonly owner: Owner;
/** The integration the connection authenticates. */
readonly integration: IntegrationSlug;
/** Which of the integration's auth methods the value applies through. */
readonly template: AuthTemplateSlug;
readonly onCreated: (connectionName: string) => void;
readonly children: ReactNode;
}
function SecretFormProvider(props: SecretFormProviderProps) {
const {
existingSecretIds,
suggestedName = "",
fallbackId = "credential",
initialIdOverride,
initialProvider = "auto",
owner,
integration,
template,
onCreated,
children,
} = props;
const doCreate = useAtomSet(createConnection, { mode: "promiseExit" });
const [state, setState] = useState<SecretFormState>(() => ({
name: suggestedName,
value: "",
idOverride: initialIdOverride ?? null,
provider: initialProvider,
revealed: false,
status: { kind: "idle" },
}));
const baseName = state.name || suggestedName;
const autoId = useMemo(
() => getUniqueSecretId(baseName, existingSecretIds, fallbackId),
[baseName, existingSecretIds, fallbackId],
);
const id = state.idOverride ?? autoId;
const duplicateError =
state.idOverride !== null && isSecretIdTaken(state.idOverride, existingSecretIds)
? "Name already exists"
: null;
const displayName = state.name.trim() || suggestedName.trim();
const canSubmit =
id.trim() !== "" &&
state.value.trim() !== "" &&
duplicateError === null &&
state.status.kind !== "submitting";
const submit = async () => {
if (!canSubmit) return;
setState((s) => ({ ...s, status: { kind: "submitting" } }));
const exit = await doCreate({
payload: {
owner,
name: ConnectionName.make(id.trim()),
integration,
template,
identityLabel: displayName || id.trim(),
value: state.value.trim(),
},
reactivityKeys: connectionWriteKeys,
});
if (Exit.isFailure(exit)) {
setState((s) => ({
...s,
status: {
kind: "error",
message: "Failed to save credential",
},
}));
return;
}
onCreated(id.trim());
};
const value: SecretFormContextValue = {
state,
actions: {
setName: (v) => setState((s) => ({ ...s, name: v })),
setValue: (v) => setState((s) => ({ ...s, value: v })),
setIdOverride: (v) => setState((s) => ({ ...s, idOverride: v })),
setProvider: (v) => setState((s) => ({ ...s, provider: v })),
toggleReveal: () => setState((s) => ({ ...s, revealed: !s.revealed })),
submit,
},
meta: { id, duplicateError, canSubmit },
};
return <SecretFormContext value={value}>{children}</SecretFormContext>;
}
// ---------------------------------------------------------------------------
// Parts
// ---------------------------------------------------------------------------
function NameField(props: { label?: string; placeholder?: string }) {
const { state, actions } = useSecretForm();
const inputId = useId();
return (
<Field>
<FieldLabel htmlFor={inputId}>{props.label ?? "Name"}</FieldLabel>
<Input
id={inputId}
value={state.name}
onChange={(e) => actions.setName((e.target as HTMLInputElement).value)}
placeholder={props.placeholder ?? "GitHub PAT"}
/>
</Field>
);
}
function IdField(props: { placeholder?: string }) {
const { actions, meta } = useSecretForm();
const inputId = useId();
return (
<Field>
<FieldLabel htmlFor={inputId}>ID</FieldLabel>
<Input
id={inputId}
value={meta.id}
onChange={(e) => actions.setIdOverride((e.target as HTMLInputElement).value)}
placeholder={props.placeholder ?? "github-token"}
className="font-mono"
/>
{meta.duplicateError && <FieldError>{meta.duplicateError}</FieldError>}
</Field>
);
}
function ValueField(props: { revealable?: boolean; placeholder?: string; autoFocus?: boolean }) {
const { state, actions } = useSecretForm();
const inputId = useId();
const revealable = props.revealable ?? false;
const revealed = revealable && state.revealed;
const errored = state.status.kind === "error";
return (
<Field>
<FieldLabel htmlFor={inputId}>Value</FieldLabel>
<div className="relative" data-ph-block>
<Input
id={inputId}
type={secretValueInputType({ revealable, revealed })}
value={state.value}
onChange={(e) => actions.setValue((e.target as HTMLInputElement).value)}
placeholder={props.placeholder ?? "ghp_xxxxxxxxxxxxxxxxxxxx"}
autoFocus={props.autoFocus}
autoComplete="new-password"
className={revealable ? "pr-9 font-mono" : "font-mono"}
style={
revealable && !revealed ? ({ WebkitTextSecurity: "disc" } as CSSProperties) : undefined
}
data-ph-block
/>
{revealable && (
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute right-1 top-1/2 size-7 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={actions.toggleReveal}
aria-label={state.revealed ? "Hide value" : "Reveal value"}
>
<SecretVisibilityIcon revealed={state.revealed} />
</Button>
)}
</div>
{errored && (
<FieldError>{state.status.kind === "error" ? state.status.message : ""}</FieldError>
)}
</Field>
);
}
function ErrorBanner() {
const { state } = useSecretForm();
if (state.status.kind !== "error") return null;
return (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
<p className="text-sm text-destructive">{state.status.message}</p>
</div>
);
}
type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>;
function SubmitButton(props: ButtonProps & { children?: ReactNode }) {
const { state, meta, actions } = useSecretForm();
const { children, disabled, onClick, ...rest } = props;
const submitting = state.status.kind === "submitting";
return (
<Button
{...rest}
onClick={(e) => {
onClick?.(e);
if (!e.defaultPrevented) void actions.submit();
}}
disabled={disabled || !meta.canSubmit}
>
{submitting ? "Saving…" : children}
</Button>
);
}
// ---------------------------------------------------------------------------
// Reveal-eye icon (used by ValueField when `revealable`)
// ---------------------------------------------------------------------------
function SecretVisibilityIcon(props: { revealed: boolean }) {
return props.revealed ? (
<svg
viewBox="0 0 16 16"
className="size-3.5"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 2l12 12" />
<path d="M6.5 6.5a2 2 0 0 0 3 3" />
<path d="M3.5 5.5C2.3 6.7 1.5 8 1.5 8s2.5 4.5 6.5 4.5c1 0 1.9-.3 2.7-.7" />
<path d="M10.7 10.7c2-1.4 3.3-3.2 3.8-3.7 0 0-2.5-5-6.5-5-.7 0-1.4.1-2 .4" />
</svg>
) : (
<svg
viewBox="0 0 16 16"
className="size-3.5"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" />
<circle cx="8" cy="8" r="2" />
</svg>
);
}
// ---------------------------------------------------------------------------
// Compound export
// ---------------------------------------------------------------------------
export const SecretForm = {
Provider: SecretFormProvider,
NameField,
IdField,
ValueField,
ErrorBanner,
SubmitButton,
};
export type { SecretFormProviderProps };