kirche-einfach/backend/components/kirche/form_inputs.py

162 lines
5.1 KiB
Python

"""Structured form-input component for kirche-einfach flows.
A flow that starts with :class:`CustomInputForm` 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``. Field metadata maps as follows:
* ``display_name`` → ``title``, ``info`` → ``description``,
``value`` → ``default``, ``options`` → ``enum``
* ``required`` → required array, ``advanced`` → ``ui:collapsed`` hint
* ``_input_type`` → JSON type + widget (see ``app/flows/input_schema.py``)
``FileInput`` is a stub for now: it carries a string reference only, real
upload comes later.
"""
from __future__ import annotations
import json
from typing import ClassVar
from lfx.custom import Component
from lfx.field_typing.range_spec import RangeSpec
from lfx.io import (
BoolInput,
DropdownInput,
FileInput,
FloatInput,
IntInput,
MessageTextInput,
MultilineInput,
MultiselectInput,
Output,
SecretStrInput,
SliderInput,
StrInput,
)
from lfx.schema.message import Message
def _text(value) -> str:
if value is None:
return ""
if hasattr(value, "text"):
return value.text or ""
return str(value)
class CustomInputForm(Component):
display_name = "CustomInput Form"
description = "Collects structured form values into a single JSON message."
icon = "ClipboardList"
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,
),
StrInput(
name="full_name",
display_name="Full name",
info="Your first and last name",
value="",
required=True,
),
MultilineInput(
name="notes",
display_name="Notes",
info="Anything else we should know",
value="",
),
SecretStrInput(
name="password",
display_name="Password",
info="Optional access phrase",
value="",
),
IntInput(
name="age",
display_name="Age",
info="Your age in years",
value=18,
),
FloatInput(
name="amount",
display_name="Amount",
info="Amount in euros",
value=0.0,
),
BoolInput(
name="newsletter",
display_name="Newsletter",
info="Send me the parish newsletter",
value=False,
),
DropdownInput(
name="department",
display_name="Department",
info="Which team is this for",
options=["Music", "Youth", "Seniors", "Admin"],
value="Music",
),
MultiselectInput(
name="topics",
display_name="Topics",
info="What are you interested in",
options=["Worship", "Outreach", "Courses"],
value=[],
),
SliderInput(
name="satisfaction",
display_name="Satisfaction",
info="How satisfied are you (1-5)",
value=3,
range_spec=RangeSpec(min=1, max=5, step=1),
),
FileInput(
name="attachment",
display_name="Attachment",
info="String reference only for now (no upload yet)",
file_types=["pdf", "png", "jpg"],
),
]
outputs: ClassVar[list] = [
Output(display_name="Form Data", name="form_data", method="collect"),
]
def collect(self) -> Message:
data = {
"full_name": _text(self.full_name),
"notes": _text(self.notes),
"password": _text(self.password),
"age": self.age,
"amount": self.amount,
"newsletter": self.newsletter,
"department": _text(self.department),
"topics": self.topics,
"satisfaction": self.satisfaction,
"attachment": _text(self.attachment),
}
# Submitted values (via form_values) win over the field defaults.
# Accepts the raw dict or the run envelope {"payload": {...}}.
try:
override = json.loads(_text(self.form_values) or "{}")
except json.JSONDecodeError:
override = {}
if isinstance(override, dict):
if isinstance(override.get("payload"), dict):
override = override["payload"]
for key in data:
if key in override:
data[key] = override[key]
return Message(text=json.dumps(data, ensure_ascii=False))