"""Bauvorhaben form component for kirche-einfach flows. Thin variant: shared runtime lives in ``kirche._form_core`` (imported here for the Langflow palette); ``scripts/make_flow_fixtures.py:kirche_node()`` inlines the core into the exported ``code`` so the LFX runtime stays self-contained. Copy this file as the starting point for future forms. A flow that starts with :class:`CustomInputBauvorhaben` declares the fields of its web form. The API reads the exported node template (``template`` minus ``code``) and derives ``{json_schema, ui_schema}`` from the ``lfx.io`` input definitions below — one JSON-Schema property per field, keyed by ``name``: * ``display_name`` → ``title``, ``info`` → ``description`` (``[visible_if ...]`` markers declare conditional branches, see below), ``value`` → ``default``, ``options`` → ``enum`` * ``required`` → required array (branch-required applies only while visible) ``collect()`` is fully dynamic: it emits every input except the plumbing one (``form_values``), so fields can be renamed, added, or removed in the Langflow code editor without touching this method. When the run envelope names the asked fields (``fields`` list, sent by the API from the version's form schema), defaults are emitted only for asked AND currently visible fields. Without the list (raw dict, Langflow playground), all visible declared defaults apply. ``collect()`` returns ``Data`` (the LFX JSON type), not ``Message``. """ from __future__ import annotations import json # noqa: F401 -- used by inlined _form_core at runtime import re # noqa: F401 -- used by inlined _form_core at runtime from typing import ClassVar from lfx.custom import Component from lfx.io import ( BoolInput, DropdownInput, IntInput, MessageTextInput, MultiselectInput, Output, StrInput, ) from lfx.schema.data import Data from kirche._form_core import ( PLUMBING_INPUTS, _field_conditions, _json_safe, collect_form_data, parse_envelope, ) # <-- _form_core inlined by scripts/make_flow_fixtures.py:kirche_node() # (LFX executes embedded source without the kirche package) --> class CustomInputBauvorhaben(Component): display_name = "Bauvorhaben Form" description = "Collects Bauvorhaben form values into a single JSON object." icon = "Church" inputs: ClassVar[list] = [ # Plumbing, not a form field: the API posts submitted web-form # values as the run envelope (same {"payload": {...}} shape the # other kirche components use); they arrive here through a ChatInput # entry node and override the defaults below. Hidden in the GUI and # skipped by the form parser (show=False). MessageTextInput( name="form_values", display_name="Form values (JSON envelope)", info="Submitted values as JSON; merged over the defaults", value="", show=False, ), DropdownInput( name="gebaeude", display_name="Welches Gebäude soll verändert werden?", info="", options=["Kirche", "Anderes Gebäude"], value="", required=True, ), MultiselectInput( name="bestandteil", display_name="Welcher Bestandteil der Kirche?", info="Mehrfachauswahl möglich [visible_if gebaeude=Kirche]", options=["Glocke", "Orgel", "Innenraum", "Fenster", "Fassade"], value=[], required=True, ), DropdownInput( name="glockenmassnahme", display_name="Welche Maßnahme an der Glocke?", info="[visible_if bestandteil=Glocke]", options=["Reinigung", "Reparatur", "Neuguss"], value="Reinigung", ), IntInput( name="gussjahr", display_name="Gussjahr (nur bei Neuguss)?", info="[visible_if glockenmassnahme=Neuguss]", value=0, ), DropdownInput( name="gebaeudeart", display_name="Art des anderen Gebäudes?", info='[visible_if gebaeude="Anderes Gebäude"]', options=["Wohnhaus", "Scheune", "Sonstiges"], value="Wohnhaus", ), BoolInput( name="denkmalschutz", display_name="Steht das Gebäude unter Denkmalschutz?", info="", value=False, ), StrInput( name="denkmalnummer", display_name="Aktenzeichen Denkmalschutz?", info="[visible_if denkmalschutz=true]", value="", required=True, ), ] outputs: ClassVar[list] = [ Output(display_name="Form Data", name="form_data", method="collect"), ] def _field_names(self) -> list: """Names of all form fields (every input except the plumbing ones).""" inputs = getattr(self, "_inputs", None) if isinstance(inputs, dict) and inputs: return [name for name in inputs if name not in PLUMBING_INPUTS] seen = [] declared = getattr(self, "inputs", None) or getattr(type(self), "inputs", None) or [] for inp in declared: name = getattr(inp, "name", None) if name and name not in PLUMBING_INPUTS and name not in seen: seen.append(name) return seen def _field_value(self, name: str): """Runtime value of one input. Reads ``_inputs`` directly: a field renamed to e.g. ``name`` would otherwise resolve to the component attribute instead of its value. """ inputs = getattr(self, "_inputs", None) if isinstance(inputs, dict) and name in inputs: return getattr(inputs[name], "value", None) try: return getattr(self, name, None) except (AttributeError, ValueError): return None def _is_visible(self, name: str, effective: dict) -> bool: """Whether a field's branch conditions hold. Single pass over effective values (submitted first, code defaults after): no recursion, so trigger cycles cannot hang — a stale or cyclic marker simply matches or not. Unknown triggers never match. """ inputs = getattr(self, "_inputs", None) info = "" structured = None if isinstance(inputs, dict) and name in inputs: info = getattr(inputs[name], "info", "") or "" structured = getattr(inputs[name], "visible_if", None) for trigger, expected in _field_conditions(info, structured): if trigger == name or trigger not in effective: # Self-reference or unknown trigger: fail open (visible), # mirroring the backend parser. continue actual = effective[trigger] if isinstance(actual, list): if expected not in actual: return False elif actual != expected: return False return True def collect(self) -> Data: payload, asked = parse_envelope(self.form_values) # Effective trigger values: submitted first, code defaults after. declared = self._field_names() field_values = {name: _json_safe(self._field_value(name)) for name in declared} effective, scope = collect_form_data(declared, field_values, payload, asked) # Defaults only for asked AND visible fields (hidden branch fields # stay out, like the frontend never submits them); without an asked # list, all visible declared fields (Langflow playground). data = {name: effective[name] for name in scope if self._is_visible(name, effective)} # Submitted values win over the field defaults. Unknown keys are # kept: the frontend may already know renamed fields this code # revision does not declare. for key, val in payload.items(): if isinstance(key, str) and key: data[key] = _json_safe(val) return Data(data=data)