439 lines · 14.3 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,  ProviderSettingsFormOption,  ProviderSettingsFormSchemaAnnotation,} from "@t3tools/contracts"; import { cn } from "../../lib/utils";import { DraftInput } from "../ui/draft-input";import { Input } from "../ui/input";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { Switch } from "../ui/switch";import { Textarea } from "../ui/textarea";import type { ProviderClientDefinition } from "./providerDriverMeta";import { SettingsRow } from "./settingsLayout"; 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;  /** Choices for a `select` control. The first entry is the default. */  readonly options?: ReadonlyArray<ProviderSettingsFormOption> | 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) }            : {}),          ...(formAnnotation.control === "select" && formAnnotation.options            ? { options: formAnnotation.options }            : {}),        } 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,   * and `settings` renders the shared settings row treatment.   */  readonly variant: "card" | "dialog" | "settings";  readonly onChange: (nextConfig: Record<string, unknown> | undefined) => void;} /** Stores the default choice as an omitted key so unchanged configs stay small. */function ProviderSettingsSelect({  field,  value,  inputId,  size,  className,  onChange,}: {  readonly field: ProviderSettingsFieldModel;  readonly value: unknown;  readonly inputId: string;  readonly size: "sm" | "xs";  readonly className?: string | undefined;  readonly onChange: ProviderSettingsFormProps["onChange"];}) {  const options = field.options ?? [];  const fallback = options[0]?.value ?? "";  const current = readProviderConfigString(value, field.key) || fallback;  const label = options.find((option) => option.value === current)?.label ?? current;  return (    <Select      value={current}      onValueChange={(next) => {        if (typeof next !== "string") return;        onChange(nextProviderConfigWithFieldValue(value, field, next === fallback ? "" : next));      }}    >      <SelectTrigger id={inputId} size={size} className={className} aria-label={field.label}>        <SelectValue>{label}</SelectValue>      </SelectTrigger>      <SelectPopup align="start" alignItemWithTrigger={false}>        {options.map((option) => (          <SelectItem key={option.value} value={option.value}>            {option.label}          </SelectItem>        ))}      </SelectPopup>    </Select>  );} 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 === "settings") {    const descriptionId = field.description ? `${inputId}-description` : undefined;    const control =      field.control === "switch" ? (        <Switch          checked={readProviderConfigBoolean(value, field.key, field.defaultBooleanValue)}          onCheckedChange={(checked) =>            onChange(nextProviderConfigWithFieldValue(value, field, Boolean(checked)))          }          aria-label={field.label}          aria-describedby={descriptionId}        />      ) : field.control === "select" ? (        <ProviderSettingsSelect          field={field}          value={value}          inputId={inputId}          size="sm"          className="w-full sm:w-56"          onChange={onChange}        />      ) : field.control === "textarea" ? (        <Textarea          id={inputId}          aria-describedby={descriptionId}          className="w-full sm:w-96"          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-full sm: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}        />      );     return (      <SettingsRow        title={          field.control === "switch" ? field.label : <label htmlFor={inputId}>{field.label}</label>        }        description={          field.description ? <span id={descriptionId}>{field.description}</span> : undefined        }        control={control}      />    );  }   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 === "select") {    return (      <FieldFrame variant={variant}>        <label htmlFor={inputId} className={cn(variant === "card" && "block")}>          {label}          <ProviderSettingsSelect            field={field}            value={value}            inputId={inputId}            size="sm"            className={cn("w-full", variant === "card" && "mt-1.5")}            onChange={onChange}          />          {description}        </label>      </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}        />      ))}    </>  );}