kanban: render the remaining forms, and file the gap that stops the last two - #389
Merged
Merged
Conversation
…ast two #357 converted the login form and left the rest of the showcase hand-built. This finishes it: `CreateProject`, `CreateColumn`, `CreateSwimlane`, `CreateTask` and `AddComment` now render from `morph::forms::schemaJson<A>()` through the shipped `DynamicForm`, each with the renderer's own explicit Submit button. Six forms on five screens, none with a hand-written field or submit button. `BoardBridge` grows the controller contract the renderer calls -- `schemasJson`, `submitIfValid`, `replyReceived` -- forwarding to a `BoardPresenter::submitForm` that dispatches through `executeJson` and then decodes the reply. Decoding is the load-bearing part: every board action returns the full rebuilt state, so `submitForm` re-emits `boardOpened` (and `commentAdded` for `AddComment`), which is what keeps `BoardBridge::board` and every binding over it live in the same turn as the submit rather than at the next poll tick. `ProjectAdminPresenter::submitForm` becomes the two-model routing table its own comment predicted, and re-emits `projectCreated` with the name recovered from the submitted body so the project pane keeps re-listing itself. Both bridges serve one schema document and route only their own actions; an action a controller does not serve is reported on `replyReceived`, never dropped -- checked on `MoveTaskPosition`, a registered action deliberately outside the form set, so the gate is proven rather than the JSON decoder. `createProject`, `createColumn`, `createSwimlane`, `createTask` and `addComment` are demoted from `Q_INVOKABLE` to plain methods: nothing in `gui/qml/` calls them any more, and dead QML surface is what `test_kanban_qml_surface.cpp` exists to catch. `submitIfValid` needs the opposite exemption on both bridges, its caller being the shipped `DynamicForm.qml`. Two DTO declarations carry the interaction the hand-built controls had. `CreateTask`'s column/swimlane ids and `AddComment`'s task id are context, not input -- the task goes in the column you are typing in, the comment on the task whose popup is open -- so they are `hidden` in `fieldMetadata` and seeded by the view that owns the form; no row id reaches the screen. `CreateColumn::wipLimit` joins `optionalFields`, because 0 already means unlimited and the `SpinBox` it replaced defaulted to it rather than demanding a number. Rendering these for real found what generating them could not, which is the point triage made when it said schema generation is necessary and not sufficient. `MembersView`'s role pickers and `RulesView`'s mutation-type picker are *not* converted, and now carry rule 2(a)'s written justification plus the finding rule 2 demands: `schemaJson` describes a `enum class` completely, and `DynamicForm` draws it as a free-text field that reports `ready` for `role = "Emperor"` (morph#386). Converting them would have replaced two working combo boxes with two typo-accepting text boxes. Two smaller renderer findings are filed rather than folded in: morph#387 (every form warns about `onOptionsReceived` on a controller with no `Choice` field -- invisible until a real controller is attached) and morph#388 (array-valued schema keys mistyped when `schema` is assigned rather than bound). The drag-and-drop board keeps its own rule 2(a) justification and files nothing: its schema renders fine, and a form is simply the wrong shape for a gesture whose inputs the gesture itself produces. `tests/test_gui_forms_render.cpp` is what makes "rendered" more than a claim: it loads the shipped `.qml` into a real engine with real bridges over a real backend and, per form, asserts the controls drawn, the body assembled and the row written. Mutation-checked twice -- dropping the hidden-field seeding leaves `CreateTask` permanently `!ready`, and dropping `hidden` from `CreateTask::columnId` makes the raw id column visible on screen. 151 kanban cases pass (was 140), 1672 in the whole configure; both scenario scripts still exit 0. Built with `MORPH_ENABLE_STRICT_COMPILATION=OFF` because morph#372 makes any AUTOMOC target unbuildable from a nested worktree, so the test binary was not compiled under the strict warning set; the two changed DTO headers were compiled separately under it and are clean. Closes #344. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnSnsebJ92Kti1D7m3RwYJ
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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.
Closes #344. Follows #357, which rendered the login form; this does the rest of the rung.
examples/LADDER.mddesignates kanban the single polished showcase, andexamples/IMPLEMENTATION.md's rule 2 makes hand-built input widgets forbidden by default, allowing two justifications — (a) the generated UI cannot express the interaction, or (b) pure glue — each of which must be written in the rung README. The rung had 26 hand-written controls across seven QML files, nomorph::formscode at all, and neither justification written. Triage had already refuted (a) for the enumerated inputs by compilingschemaJson<A>()against the rung's real DTOs.Converted — five forms, on top of #357's login
CreateProjectProjectListView.qmlCreateColumnBoardView.qmlCreateSwimlaneBoardView.qmlCreateTaskBoardView.qml, one per columnAddCommentTaskDetailPopup.qmlAll five render from
schemaJson<A>()through the shippedMorphFormsDynamicForm, with the renderer's own Submit button.BoardBridgegainedschemasJson/submitIfValid/replyReceived— the contractProjectAdminBridgealready had — forwarding to a newBoardPresenter::submitFormthat dispatches throughexecuteJsonand decodes the reply, soboardOpened/commentAddedstill fire. Without the decode the board property would only catch up on the next poll tick, which is a visible regression rather than a refactor.ProjectAdminPresenter::submitFormbecame the two-model routing table.CreateTask's column/swimlane ids andAddComment's task id arehiddeninfieldMetadataand seeded by the owning view;CreateColumn::wipLimitjoinedoptionalFields.Left hand-built, with the justification written
The drag-and-drop board (
MoveTaskPosition) — rule 2(a), no issue filed. Its schema renders fine; the interaction is the problem. The destination column, swimlane and position are produced by the gesture —DropArea.onDroppedcomputes them from geometry — so a schema-rendered form has no input left to collect. The README says that, rather than dressing a gesture up as a subsystem gap.MembersView.qml's role pickers andRulesView.qml's add-rule row — rule 2(a), issue filed (#386). This is a real renderer gap, measured rather than assumed: converting them would have swapped a working 3-itemComboBoxfor a text box that accepts anything.The README's old "Not exercised:
morph::forms" section, which recorded the absence and deferred the decision to this issue, is replaced by the actual state.The renderer gaps, all filed rather than worked around
DynamicFormrenders a C++enum classas a free-text field, and its gate accepts values outside the set. Reproduced inmorph_forms_qml_testswithschemaJson<SetMemberRole>()pasted verbatim:isChoice=false, the control drawn is aTextField, androle='Emperor'passes as ready. This is what stops the last two screens.DynamicFormwarnsDetected function "onOptionsReceived" … no signal of the target matchesagainst any controller with noChoicefield, which is the shape bookmarks documents as sanctioned. Invisible to every rung's rule-6 smoke test, because those attach null controllers.DynamicFormmistypes array-valued schema keys whenschemais assigned as aQVariantrather than bound, so a strong id goes out as{"taskId":"7"}. Not a live app bug — every shipped app binds declaratively, and the bound path was confirmed correct before filing.Verification
Triage was explicit that schema generation working is necessary but not sufficient, and that nothing had been rendered on screen. So:
QT_QPA_PLATFORM=offscreen ladder_kanban_guiboots with zero output — no QML warning, no "produced no root object".tests/test_gui_forms_render.cpp, which loads the shipped.qmlinto a realQQmlApplicationEnginewith real bridges over a real backend, and per form asserts the controls drawn, the body assembled, and the row written. Not Qt Quick Test and it synthesizes no mouse events (TESTING.md rule 6) — it reproduces the two calls Submit makes.CreateTaskpermanently!readywithpreviewLine == "{}"; removinghiddenfromCreateTask::columnIdflips the raw-id column tovisible == true. Both failed as expected, then were reverted.ctest -L ladder-kanban→100% tests passed out of 151(baseline before this change: 140). Whole configure →100% tests passed out of 1672.test_morph_scenario.pyandscenario_coverage.pyexit 0, kanban still 22/22 actions and 20/20 workflows.check_spec_citations.sh,check_test_type_names.sh,check_rung_filters.shall OK; clang-format clean.Not verified: the test binary was not compiled under the strict warning set —
-DMORPH_ENABLE_STRICT_COMPILATION=OFF, because #372 breaks every AUTOMOC target from a nested worktree. The two changed DTO headers were compiled standalone under the full-Weverything -Werrorset: clean. No Remote-mode GUI session was run end to end — the offscreen run was Local mode, and the driven test usesBackendRigMode::Local, so whether the desktop client emits #387's warning at runtime is inferred from the same code path, not measured. Whether other rungs (bookmarks'CreateBookmark::visibility) currently hit #386 was not checked. Nodocs/spec/file was invalidated, so none changed.🤖 Generated with Claude Code
https://claude.ai/code/session_01GnSnsebJ92Kti1D7m3RwYJ