144 lines · 5.2 KB · TypeScript
RawDownload
import { useCallback, useState } from "react";import { parse } from "tldts"; import {  CardStack,  CardStackContent,  CardStackEntryField,} from "../components/card-stack";import { Input } from "../components/input";import { normalizeNamespaceInput, slugifyNamespace } from "./namespace";export { normalizeNamespaceInput, slugifyNamespace } from "./namespace"; /** * Derives a display-name candidate from a URL by extracting its apex domain * label (e.g. `https://api.shopify.com/graphql` → `"Shopify"`) and * title-casing it. Returns `null` if the URL has no parseable domain. */export function displayNameFromUrl(url: string): string | null {  const trimmed = url.trim();  if (!trimmed) return null;  const parsed = parse(trimmed);  const label = parsed.domainWithoutSuffix;  if (!label) return null;  return label.charAt(0).toUpperCase() + label.slice(1);} // ---------------------------------------------------------------------------// Hook — owns the name + namespace state with namespace auto-derivation// --------------------------------------------------------------------------- export interface SourceIdentity {  /** Display name — the user's override if they've typed one, otherwise the fallback. */  readonly name: string;  /** Namespace — the user's override if they've typed one, otherwise slugified from `name`. */  readonly namespace: string;  readonly setName: (name: string) => void;  readonly setNamespace: (namespace: string) => void;  /** Clears any user overrides so both fields return to deriving from the fallback. */  readonly reset: () => void;} export interface UseSourceIdentityOptions {  /**   * Fallback display name — used when the user hasn't typed one. Pass a   * value computed from the caller's reactive state (probe result, URL   * apex domain, template default, etc.) and it'll flow through to `name`   * automatically.   */  readonly fallbackName?: string;  /** Fallback namespace — defaults to `slugifyNamespace(fallbackName ?? "")`. */  readonly fallbackNamespace?: string;} /** * Manages a display name and a derived namespace. Both fields are pure * derived state: the user's `setName` / `setNamespace` call stores an * override, otherwise the hook returns the caller-supplied fallback * (passed fresh on every render). Call `reset()` to drop overrides. */export function useSourceIdentity(options?: UseSourceIdentityOptions): SourceIdentity {  const [nameOverride, setNameOverride] = useState<string | null>(null);  const [namespaceOverride, setNamespaceOverride] = useState<string | null>(null);   const fallbackName = options?.fallbackName ?? "";  const name = nameOverride ?? fallbackName;  const fallbackNamespace = options?.fallbackNamespace ?? slugifyNamespace(name);  const namespace = namespaceOverride ?? fallbackNamespace;   const setName = useCallback((next: string) => {    setNameOverride(next);  }, []);   const setNamespace = useCallback((next: string) => {    setNamespaceOverride(normalizeNamespaceInput(next));  }, []);   const reset = useCallback(() => {    setNameOverride(null);    setNamespaceOverride(null);  }, []);   return { name, namespace, setName, setNamespace, reset };} // ---------------------------------------------------------------------------// UI — two fields, wrapped in a shared CardStack// --------------------------------------------------------------------------- export interface SourceIdentityFieldsProps {  readonly identity: SourceIdentity;  readonly namePlaceholder?: string;  readonly namespacePlaceholder?: string;  readonly nameLabel?: string;  readonly namespaceHint?: string;  /**   * When true, the namespace field is rendered disabled — useful on Edit   * forms, where the namespace is the source's identity and changing it   * would require a delete + recreate flow.   */  readonly namespaceReadOnly?: boolean;} export function SourceIdentityFields({  identity,  namePlaceholder = "e.g. Sentry API",  namespacePlaceholder = "sentry_api",  nameLabel = "Display Name",  namespaceHint,  namespaceReadOnly = false,}: SourceIdentityFieldsProps) {  const effectiveNamespaceHint =    namespaceHint ??    (namespaceReadOnly      ? "The namespace is part of the source's identity and cannot be changed."      : "Prefix for the tool names. Auto-derived from the display name.");   return (    <CardStack>      <CardStackContent className="border-t-0">        <CardStackEntryField label={nameLabel}>          <Input            value={identity.name}            onChange={(e) => identity.setName((e.target as HTMLInputElement).value)}            placeholder={namePlaceholder}            className="text-sm"          />        </CardStackEntryField>        <CardStackEntryField          label="Namespace"          description={namespaceReadOnly ? undefined : "(optional)"}          hint={effectiveNamespaceHint}        >          <Input            value={identity.namespace}            onChange={(e) => identity.setNamespace((e.target as HTMLInputElement).value)}            placeholder={namespacePlaceholder}            className="font-mono text-sm"            disabled={namespaceReadOnly}          />        </CardStackEntryField>      </CardStackContent>    </CardStack>  );}