Files
SKEEN-Proyecto/wacrm/src/components/flows/forms/fields.tsx
Consultoría Alcaraz Salazar a718592291 Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind
- Frontend Homenest (versión alternativa)
- Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp)
- WACRM fork (Next.js 16 + Supabase)
- Hermes + Bridge + Skills (Qwen3.6 via Nan Builders)
- Scripts de migración y operación
- Documentación extensiva en docs/
2026-07-20 07:44:23 +00:00

135 lines
3.3 KiB
TypeScript

"use client";
/**
* Reusable field components shared across every per-node form.
*
* `NodeKeySelect` — picks a node from the flow's node list, rendered
* with the source node's icon so the dropdown reads as
* "destination = ◇ menu" rather than an opaque slug.
*
* `NextNodeRow` — wraps NodeKeySelect with a label; the most common
* per-node form row ("after this node, advance to…").
*
* `TextRow` — wraps Input or Textarea behind a label. Pure UI sugar
* to keep per-node forms uncluttered.
*
* Lives in src/components/flows/forms/ so both the list view's
* collapsed-card editor and the canvas view's side-panel editor
* (introduced in this PR) mount the exact same form components.
*/
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { NODE_META, type BuilderNode } from "../shared";
export function TextRow({
label,
value,
onChange,
rows = 1,
}: {
label: string;
value: string;
onChange: (v: string) => void;
rows?: number;
}) {
return (
<div>
<label className="mb-1 block text-xs text-muted-foreground">{label}</label>
{rows > 1 ? (
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
rows={rows}
className="bg-muted"
/>
) : (
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
className="bg-muted"
/>
)}
</div>
);
}
export function NextNodeRow({
value,
allNodes,
currentKey,
onChange,
label,
}: {
value: string;
allNodes: BuilderNode[];
currentKey: string;
onChange: (v: string) => void;
label: string;
}) {
return (
<div>
<label className="mb-1 block text-xs text-muted-foreground">{label}</label>
<NodeKeySelect
value={value || null}
nodes={allNodes}
excludeKey={currentKey}
onChange={(v) => onChange(v ?? "")}
placeholder="Pick a next node…"
/>
</div>
);
}
export function NodeKeySelect({
value,
nodes,
excludeKey,
onChange,
placeholder,
className,
}: {
value: string | null;
nodes: BuilderNode[];
excludeKey?: string;
onChange: (v: string | null) => void;
placeholder?: string;
className?: string;
}) {
const options = nodes.filter((n) => n.node_key !== excludeKey);
return (
<Select
value={value ?? "__none__"}
onValueChange={(v) => onChange(v === "__none__" ? null : v)}
>
<SelectTrigger className={cn("bg-muted", className)}>
<SelectValue placeholder={placeholder ?? "—"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">— None —</SelectItem>
{options.map((n) => {
const Icon = NODE_META[n.node_type].icon;
return (
<SelectItem key={n.node_key} value={n.node_key}>
<span className="inline-flex items-center gap-1.5">
<Icon
className={cn("h-3 w-3", NODE_META[n.node_type].color)}
/>
{n.node_key}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
);
}