373 lines · 12.6 KB · TypeScript
RawDownload
"use client"; import { useMemo, type ReactNode } from "react";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import type {  ProviderSettingsFormAnnotation,  ProviderSettingsFormControl,  ProviderSettingsFormSchemaAnnotation,} from "@t3tools/contracts"; import { cn } from "../../lib/utils";import { DraftInput } from "../ui/draft-input";import { Input } from "../ui/input";import { Switch } from "../ui/switch";import { Textarea } from "../ui/textarea";import type { ProviderClientDefinition } from "./providerDriverMeta"; export interface ProviderSettingsFieldModel {  readonly key: string;  readonly control: ProviderSettingsFormControl;  readonly label: string;  readonly description?: string | undefined;  readonly placeholder?: string | undefined;  readonly clearWhenEmpty: "omit" | "persist";  readonly defaultBooleanValue?: boolean | undefined;} function titleizeFieldKey(key: string): string {  return key    .replace(/([a-z0-9])([A-Z])/g, "$1 $2")    .replace(/[-_]+/g, " ")    .replace(/^./, (char) => char.toUpperCase());} function readFieldAnnotations(  fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],) {  return Schema.resolveAnnotationsKey(fieldSchema) ?? Schema.resolveAnnotations(fieldSchema);} function readFieldAnnotationString(  fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],  key: "title" | "description",): string | undefined {  const annotations = readFieldAnnotations(fieldSchema);  const value = annotations?.[key];  return typeof value === "string" ? value : undefined;} function readProviderSettingsFormAnnotation(  fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],): ProviderSettingsFormAnnotation {  const annotation = readFieldAnnotations(fieldSchema)?.providerSettingsForm;  return annotation ?? {};} function readProviderSettingsFormSchemaAnnotation(  definition: ProviderClientDefinition,): ProviderSettingsFormSchemaAnnotation {  return Schema.resolveAnnotations(definition.settingsSchema)?.providerSettingsFormSchema ?? {};} function readFieldBooleanDefault(  fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],): boolean | undefined {  const decodeDefault = Schema.decodeUnknownOption(fieldSchema as Schema.Decoder<unknown>);  const decoded = decodeDefault(undefined);  return Option.isSome(decoded) && typeof decoded.value === "boolean" ? decoded.value : undefined;} export function deriveProviderSettingsFields(  definition: ProviderClientDefinition,): ReadonlyArray<ProviderSettingsFieldModel> {  const schemaAnnotation = readProviderSettingsFormSchemaAnnotation(definition);  const orderedKeys = new Map(    (schemaAnnotation.order ?? []).map((key, index) => [key, index] as const),  );  const orderFallbackOffset = orderedKeys.size;   return Object.keys(definition.settingsSchema.fields)    .map((key, index) => ({ key, index }))    .toSorted((left, right) => {      return (        (orderedKeys.get(left.key) ?? orderFallbackOffset + left.index) -        (orderedKeys.get(right.key) ?? orderFallbackOffset + right.index)      );    })    .flatMap(({ key }) => {      const fieldSchema = definition.settingsSchema.fields[key]!;      const formAnnotation = readProviderSettingsFormAnnotation(fieldSchema);      if (formAnnotation.hidden) return [];       const annotatedTitle = readFieldAnnotationString(fieldSchema, "title");      const annotatedDescription = readFieldAnnotationString(fieldSchema, "description");      return [        {          key,          control: formAnnotation.control ?? "text",          label: annotatedTitle ?? titleizeFieldKey(key),          ...(annotatedDescription !== undefined ? { description: annotatedDescription } : {}),          ...(formAnnotation.placeholder !== undefined            ? { placeholder: formAnnotation.placeholder }            : {}),          clearWhenEmpty: formAnnotation.clearWhenEmpty ?? "omit",          ...(formAnnotation.control === "switch"            ? { defaultBooleanValue: readFieldBooleanDefault(fieldSchema) }            : {}),        } satisfies ProviderSettingsFieldModel,      ];    });} export function readProviderConfigString(config: unknown, key: string): string {  if (config === null || typeof config !== "object") return "";  const value = (config as Record<string, unknown>)[key];  return typeof value === "string" ? value : "";} export function readProviderConfigBoolean(  config: unknown,  key: string,  defaultValue = false,): boolean {  if (config === null || typeof config !== "object") return defaultValue;  const value = (config as Record<string, unknown>)[key];  return typeof value === "boolean" ? value : defaultValue;} export function nextProviderConfigWithFieldValue(  config: unknown,  field: ProviderSettingsFieldModel,  value: string | boolean,): Record<string, unknown> | undefined {  const base: Record<string, unknown> =    config !== null && typeof config === "object" ? { ...(config as Record<string, unknown>) } : {};   if (typeof value === "boolean") {    const emptyBooleanValue = field.defaultBooleanValue ?? false;    if (field.clearWhenEmpty === "omit" && value === emptyBooleanValue) {      delete base[field.key];    } else {      base[field.key] = value;    }    return Object.keys(base).length > 0 ? base : undefined;  }   const trimmed = value.trim();  if (field.clearWhenEmpty === "omit" && trimmed.length === 0) {    delete base[field.key];  } else {    base[field.key] = value;  }  return Object.keys(base).length > 0 ? base : undefined;} interface ProviderSettingsFormProps {  readonly definition: ProviderClientDefinition;  readonly value: unknown;  readonly idPrefix: string;  /**   * `card` stacks label over control, `dialog` is the compact wizard layout,   * `grid` emits a label cell and a control cell per field for a parent   * two-column grid (label column left, control right), with the description   * beside a fixed-width control so each field stays on one line.   */  readonly variant: "card" | "dialog" | "grid";  readonly onChange: (nextConfig: Record<string, unknown> | undefined) => void;} function FieldFrame(props: {  readonly variant: ProviderSettingsFormProps["variant"];  readonly children: ReactNode;}) {  if (props.variant === "card") {    return <div>{props.children}</div>;  }  return <div className="grid gap-1.5">{props.children}</div>;} interface ProviderSettingsFieldRowProps {  readonly field: ProviderSettingsFieldModel;  readonly value: unknown;  readonly idPrefix: string;  readonly variant: ProviderSettingsFormProps["variant"];  readonly onChange: ProviderSettingsFormProps["onChange"];} function ProviderSettingsFieldRow({  field,  value,  idPrefix,  variant,  onChange,}: ProviderSettingsFieldRowProps) {  const inputId = `${idPrefix}-${field.key}`;  const descriptionClassName =    variant === "dialog"      ? "text-[11px] text-muted-foreground"      : "mt-1 block text-xs text-muted-foreground";  const label = <span className="text-xs font-medium text-foreground">{field.label}</span>;  const description = field.description ? (    <span className={descriptionClassName}>{field.description}</span>  ) : null;   if (variant === "grid") {    // Label cell, then a control cell where the description sits beside a    // fixed-width control and wraps under it when the pane is narrow. The    // description is outside the label, so the control points at it instead.    const descriptionId = field.description ? `${inputId}-description` : undefined;    return (      <>        {field.control === "switch" ? (          <span className="pt-1.5 text-xs font-medium text-foreground">{field.label}</span>        ) : (          <label htmlFor={inputId} className="pt-1.5 text-xs font-medium text-foreground">            {field.label}          </label>        )}        <div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">          {field.control === "switch" ? (            <span className="flex h-8 items-center sm:h-7">              <Switch                checked={readProviderConfigBoolean(value, field.key, field.defaultBooleanValue)}                onCheckedChange={(checked) =>                  onChange(nextProviderConfigWithFieldValue(value, field, Boolean(checked)))                }                aria-label={field.label}                aria-describedby={descriptionId}              />            </span>          ) : field.control === "textarea" ? (            <Textarea              id={inputId}              aria-describedby={descriptionId}              className="w-full max-w-sm"              value={readProviderConfigString(value, field.key)}              onChange={(event) =>                onChange(nextProviderConfigWithFieldValue(value, field, event.target.value))              }              placeholder={field.placeholder}              spellCheck={false}            />          ) : (            <DraftInput              id={inputId}              aria-describedby={descriptionId}              size="sm"              className="w-56"              type={field.control === "password" ? "password" : undefined}              autoComplete={field.control === "password" ? "off" : undefined}              value={readProviderConfigString(value, field.key)}              onCommit={(next) => onChange(nextProviderConfigWithFieldValue(value, field, next))}              placeholder={field.placeholder}              spellCheck={false}            />          )}          {field.description ? (            <span id={descriptionId} className="text-[11px] leading-snug text-muted-foreground">              {field.description}            </span>          ) : null}        </div>      </>    );  }   if (field.control === "switch") {    return (      <FieldFrame variant={variant}>        <div className="flex items-center justify-between gap-3">          <div className="min-w-0">            {label}            {description}          </div>          <Switch            checked={readProviderConfigBoolean(value, field.key, field.defaultBooleanValue)}            onCheckedChange={(checked) =>              onChange(nextProviderConfigWithFieldValue(value, field, Boolean(checked)))            }            aria-label={field.label}          />        </div>      </FieldFrame>    );  }   if (field.control === "textarea") {    return (      <FieldFrame variant={variant}>        <label htmlFor={inputId} className={cn(variant === "card" && "block")}>          {label}          <Textarea            id={inputId}            className={cn(variant === "card" && "mt-1.5")}            value={readProviderConfigString(value, field.key)}            onChange={(event) =>              onChange(nextProviderConfigWithFieldValue(value, field, event.target.value))            }            placeholder={field.placeholder}            spellCheck={false}          />          {description}        </label>      </FieldFrame>    );  }   const type = field.control === "password" ? "password" : undefined;  return (    <FieldFrame variant={variant}>      <label htmlFor={inputId} className={cn(variant === "card" && "block")}>        {label}        {variant === "card" ? (          <DraftInput            id={inputId}            size="sm"            className="mt-1.5"            type={type}            autoComplete={field.control === "password" ? "off" : undefined}            value={readProviderConfigString(value, field.key)}            onCommit={(next) => onChange(nextProviderConfigWithFieldValue(value, field, next))}            placeholder={field.placeholder}            spellCheck={false}          />        ) : (          <Input            id={inputId}            className="bg-background"            type={type}            autoComplete={field.control === "password" ? "off" : undefined}            value={readProviderConfigString(value, field.key)}            onChange={(event) =>              onChange(nextProviderConfigWithFieldValue(value, field, event.target.value))            }            placeholder={field.placeholder}            spellCheck={false}          />        )}        {description}      </label>    </FieldFrame>  );} export function ProviderSettingsForm({  definition,  value,  idPrefix,  variant,  onChange,}: ProviderSettingsFormProps) {  const fields = useMemo(() => deriveProviderSettingsFields(definition), [definition]);   if (fields.length === 0) {    return null;  }   return (    <>      {fields.map((field) => (        <ProviderSettingsFieldRow          key={field.key}          field={field}          value={value}          idPrefix={idPrefix}          variant={variant}          onChange={onChange}        />      ))}    </>  );}