/** * One attached state of a binding: the state selector, the templates it attaches * (attach-on-use — a not-yet-attached pick shows the "attached on save" hint), the subject * and scope expressions, the input injections, and the updates. */ import { type ReactNode, useMemo } from 'react'; import { Checkbox } from '../components/checkbox'; import { CodeBlock } from '../components/code-block'; import { Field } from '../components/field'; import { AlertTriangleIcon, CloseIcon } from '../components/icons'; import { Button } from '../components/primitives'; import { Select } from '../components/select'; import { templatedTextSummary } from '../components/templated-text-field'; import type { TemplateJqSuggestion } from './BindingJqField'; import { resolveTemplateJq } from './catalog'; import { InjectionList } from './InputInjectionRow'; import { SubjectScopeFields } from './SubjectScopeFields'; import type { BindingSourceSchemas, BindingStateOption, BindingTemplateOption, StateAttach, TemplatedText, TemplatedTextCatalog, } from './types'; import { UpdateList } from './UpdateRow'; const ATTACH_ON_USE_HINT = 'This template will be attached to the state when you save.'; /** The subject/scope of the target's own (inherited) binding for a state this one also names. */ export interface InheritedSubject { readonly subject_expr: TemplatedText; readonly scope_expr: TemplatedText | null; } /** The stored binding's raw jq, for the unresolved-catalog read-only fallback. */ function rawBindingLines(attach: StateAttach): string { const lines = [`subject: ${templatedTextSummary(attach.subject_expr)}`]; if (attach.scope_expr !== null) lines.push(`scope: ${templatedTextSummary(attach.scope_expr)}`); for (const injection of attach.input_injections) { lines.push( `input ${injection.into || '(unset)'} <- ${injection.template_jq ?? templatedTextSummary(injection.jq)}`, ); } for (const update of attach.updates) { const ref = update.template_jq ?? templatedTextSummary(update.jq); lines.push( `update ${ref}${update.adapter !== null ? ` adapter ${templatedTextSummary(update.adapter)}` : ''}`, ); } return lines.join('\n'); } export interface StateAttachRowProps { readonly attach: StateAttach; readonly onChange: (attach: StateAttach) => void; readonly onRemove: () => void; readonly statesCatalog: readonly BindingStateOption[]; readonly templatesCatalog: readonly BindingTemplateOption[]; readonly sources?: BindingSourceSchemas; readonly subjectError?: string; /** The target's own binding subject for this state, when it also binds it (precedence). */ readonly inherited?: InheritedSubject; /** The stored templates a templated-text field's id picker offers. */ readonly templates?: TemplatedTextCatalog; } export function StateAttachRow({ attach, onChange, onRemove, statesCatalog, templatesCatalog, sources, subjectError, inherited, templates, }: StateAttachRowProps): ReactNode { const stateOption = statesCatalog.find((option) => option.name === attach.state); const attachedTemplates = stateOption?.attachedTemplates ?? []; // A bound state or template absent on the server cannot resolve its pickers: show // the raw jq read-only rather than a blank form. const stateMissing = attach.state !== '' && stateOption === undefined; const missingTemplates = attach.templates.filter( (name) => !templatesCatalog.some((template) => template.name === name), ); const unresolved = stateMissing || missingTemplates.length > 0; const inputJq = useMemo( () => resolveTemplateJq(attach.templates, templatesCatalog, 'input'), [attach.templates, templatesCatalog], ); const updateJq = useMemo( () => resolveTemplateJq(attach.templates, templatesCatalog, 'update'), [attach.templates, templatesCatalog], ); // An `update` jq returns an op batch and is never callable from an expression (it is // picked only by an update's own select), so the `tjq_…` insert suggestions offered to // EVERY jq field are the attached templates' `input`-purpose jq only. const suggestions: TemplateJqSuggestion[] = useMemo( () => inputJq.map((entry) => ({ ref: entry.ref, purpose: entry.purpose, description: entry.description, })), [inputJq], ); const toggleTemplate = (name: string, checked: boolean): void => { const next = checked ? [...attach.templates, name] : attach.templates.filter((template) => template !== name); onChange({ ...attach, templates: next }); }; return (
{ // A new state resets the template picks (its attachments differ). onChange({ ...attach, state: next, templates: [] }); }} onRemove={onRemove} /> {inherited !== undefined && attach.state !== '' ? ( ) : null} {attach.state !== '' && unresolved ? : null} {attach.state !== '' && !unresolved ? ( ) : null}
); } /** The state selector plus the remove control. */ function AttachStateHeader({ state, statesCatalog, onSelectState, onRemove, }: { readonly state: string; readonly statesCatalog: readonly BindingStateOption[]; readonly onSelectState: (next: string) => void; readonly onRemove: () => void; }): ReactNode { return (