Add forms, fields, and operation-backed actions - #349
Merged
czpython merged 1 commit intoAug 30, 2026
Merged
Conversation
czpython
force-pushed
the
commonzenpython/eng-904-add-rich-data-and-layout-blocks
branch
3 times, most recently
from
August 30, 2026 10:34
71567b8 to
2000835
Compare
Base automatically changed from
commonzenpython/eng-904-add-rich-data-and-layout-blocks
to
main
August 30, 2026 10:38
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 10:44
631cc77 to
7834062
Compare
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 10:46
7834062 to
8b82f96
Compare
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 10:51
8b82f96 to
127dff4
Compare
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 10:52
127dff4 to
c7845c7
Compare
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 11:01
c7845c7 to
912b520
Compare
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 12:02
912b520 to
fd087ba
Compare
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 12:06
fd087ba to
cc662af
Compare
czpython
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 12:11
cc662af to
9ef3f0c
Compare
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
force-pushed
the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
from
August 30, 2026 12:18
9ef3f0c to
530b373
Compare
czpython
deleted the
commonzenpython/eng-905-add-forms-fields-and-operation-backed-actions
branch
August 30, 2026 12:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ENG-905
An operator writes into an app through blocks the app declared in Python. No
app JavaScript.
Fields
TextField,TextAreaField,NumberField,SelectField,MultiSelectField,RadioField, andCheckboxField, each withname,label,help_text, andis_required.nameis the key the shell sends.Option(label)takes itslabel positionally, like every type whose one required value is the thing it
shows.
Actions and operations
An
Actionnames one of the app's own routes by itsoperation_id— the idFastAPI 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 routeanswering 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 routeshaped 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
argumentswith the field values, fills theroute'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. Alinknavigates after success.confirmasks first. A 422 lands on thefield it names; anything else shows on the action. A double press sends once.
Formnames 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.pypins the wire shapes, the boot errors, thepage-read refusals, and that every action on a page is checked wherever it
nests.
backend/tests/test_ui_page_api.pyreads the proof app's form page andgist card through HTTP.
frontend/src/druksui/Form.test.tsxandFields.tsxtests cover every fieldinput, the merged payload, path filling, each refresh mode, the confirm, the
double-press guard, field-level 422s, and the success link.
AppPage.test.tsxcovers 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 testspass),
npm --prefix frontend run build.uv run pytest backend/runs in CI;the fixture-free suites were run directly and pass.