Skip to content

Add forms, fields, and operation-backed actions - #349

Merged
czpython merged 1 commit into
mainfrom
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
Aug 30, 2026
Merged

Add forms, fields, and operation-backed actions#349
czpython merged 1 commit into
mainfrom
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions

Conversation

@czpython

@czpython czpython commented Aug 29, 2026

Copy link
Copy Markdown
Owner

ENG-905

An operator writes into an app through blocks the app declared in Python. No
app JavaScript.

ui.Form(
    title="New note",
    fields=[ui.TextAreaField(name="body", label="Note", is_required=True, rows=3)],
    action=ui.Action(label="Save", operation="write_note", tone="primary",
                     link=ui.Link("Notes", page="notes")),
)

Fields

TextField, TextAreaField, NumberField, SelectField, MultiSelectField,
RadioField, and CheckboxField, each with name, label, help_text, and
is_required. name is the key the shell sends. Option(label) takes its
label positionally, like every type whose one required value is the thing it
shows.

Actions and operations

An Action names one of the app's own routes by its operation_id — the id
FastAPI already has — and the shell resolves it to a method and URL, so an
author never writes one. The app roster carries that table as
{id, method, path}, non-GET only.

Two boot errors: two routes sharing one operation_id, and one route
answering two methods. Two page-read errors, found the moment a page is
built: an action naming an operation no route declares, and an action naming
a GET. The route takes its values the way the shell sends them — path
parameters and a flat JSON body, Body(embed=True) or a model — and a route
shaped otherwise answers 422 when the action runs. Deeper request
introspection was deliberately left out: actions are headed for a declared
surface like pages, which removes those route shapes entirely.

What pressing a button does

The shell merges the action's arguments with the field values, fills the
route's path parameters, and posts the rest as the body. Then, by the
action's refresh: nothing, the region it sits in, or the whole page. A
link navigates after success. confirm asks first. A 422 lands on the
field it names; anything else shows on the action. A double press sends once.

Form names every argument — its required value is the action that sends it,
not something it shows — and a field validator refuses a field named like an
argument the action already carries, so every value is sent exactly once.

How it is verified

backend/tests/test_ui_actions.py pins the wire shapes, the boot errors, the
page-read refusals, and that every action on a page is checked wherever it
nests. backend/tests/test_ui_page_api.py reads the proof app's form page and
gist card through HTTP.

frontend/src/druksui/Form.test.tsx and Fields.tsx tests cover every field
input, the merged payload, path filling, each refresh mode, the confirm, the
double-press guard, field-level 422s, and the success link.
AppPage.test.tsx covers region refresh finding its section.

Gates run: uv run ruff check backend, uv run ruff format --check backend,
npm --prefix frontend run lint, npm --prefix frontend test (203 tests
pass), npm --prefix frontend run build. uv run pytest backend/ runs in CI;
the fixture-free suites were run directly and pass.

@czpython
czpython force-pushed the commonzenpython/eng-904-add-rich-data-and-layout-blocks branch 3 times, most recently from 71567b8 to 2000835 Compare August 30, 2026 10:34
Base automatically changed from commonzenpython/eng-904-add-rich-data-and-layout-blocks to main August 30, 2026 10:38
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from 631cc77 to 7834062 Compare August 30, 2026 10:44
@mintlify

mintlify Bot commented Aug 30, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
druks 🟢 Ready View Preview Aug 30, 2026, 10:44 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from 7834062 to 8b82f96 Compare August 30, 2026 10:46
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from 8b82f96 to 127dff4 Compare August 30, 2026 10:51
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from 127dff4 to c7845c7 Compare August 30, 2026 10:52
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from c7845c7 to 912b520 Compare August 30, 2026 11:01
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from 912b520 to fd087ba Compare August 30, 2026 12:02
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from fd087ba to cc662af Compare August 30, 2026 12:06
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from cc662af to 9ef3f0c Compare August 30, 2026 12:11
An Action names one of the app's own routes by its operation_id. Druks indexes
those routes at boot, and two on one name fail the load. The app roster carries
the non-GET ones with their method and path, and the shell resolves an action
against that table, so no author repeats a URL and no app code sees it.

A page read checks every action it carries. One that names an operation the app
does not declare, a GET route, or a route taking a query parameter fails with
the operation named: a GET is a read, and an action fills path parameters and a
JSON body.

Form holds the V1 fields — text, text area, number, select, multi-select, radio,
and checkbox — and the action that submits them. Two fields cannot share a name,
and a field cannot shadow an argument: each value is sent once.

The shell builds one object from the action's arguments and the submitted
values, fills the operation's path from it, and sends the rest as the body. It
shows a pending state and refuses a second press while the first runs. A server
validation error lands on the field its loc names, and anything else reads as a
form error. Then the action does what it declared: confirm first, refresh the
page or the region, or navigate through its link.
@czpython
czpython force-pushed the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch from 9ef3f0c to 530b373 Compare August 30, 2026 12:18
@czpython
czpython merged commit 5b85657 into main Aug 30, 2026
3 checks passed
@czpython
czpython deleted the commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions branch August 30, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant