Repository source: docs/plans/archive/forte_board_reply_step3_development_plan.md
Forte Board Reply — Step 3: Development Plan
Stage 1
Goal: Add a hidden, board-appropriate compose panel and a disabled toolbar Reply button to the board view (markup only).
Dependencies: none (Step 2 approved); builds on
forte_reply's already-shippedreply_form.php/forte.cssreuseExpected changes: new
templates/partials/paned_board_compose_panel.php(generic, not tied to a single$thread— initialthread_id/parent_idrender empty since no thread is selected by default), included once fromtemplates/partials/paned_board_content_pane.phpafter the per-thread loop;templates/pages/forte_board.phptoolbar gets a newdisabled"Reply" button with adata-paned-board-replyhook (mirrors the placeholderforte.phporiginally had)Verification approach: load
/forte, inspect DOM for the hidden panel with emptythread_id/parent_idand the disabled toolbar button; confirm single-thread/threads/{id}/forteview is unaffectedRisks or open questions: none identified
Canonical components/API contracts touched:
partials/reply_form.php(reused unchanged),paned_board_content_pane.php/forte_board.php(extended)
Stage 2
Goal: Wire the Reply button to toggle the panel, and make it enabled/disabled based on whether a thread is currently selected.
Dependencies: Stage 1
Expected changes:
public/assets/paned_board_reader.jsgets a click handler on[data-paned-board-reply]toggling the panel'shiddenattribute;selectThread()enables the button,resetContentPane()(the existing no-selection path) disables it and hides the panelVerification approach: headless-browser test on
/forte— confirm Reply starts disabled with nothing selected, becomes enabled after selecting a thread, and toggles the panel open/closed; visually compare the opened panel against the single-thread reader's composer to confirm the reusedforte.cssclasses render identically with no new rules neededRisks or open questions: none identified
Canonical components/API contracts touched:
public/assets/paned_board_reader.js(extended)
Stage 3
Goal: Keep the compose panel's target thread in sync with board selection, including the no-selection case.
Dependencies: Stage 2
Expected changes:
selectThread()updates the panel's hiddenthread_id/parent_idinputs to the selected thread'sroot_post_id;resetContentPane()clears both fields back to empty (paired with disabling Reply/hiding the panel from Stage 2)Verification approach: headless-browser test — select thread A, confirm fields match; select thread B, confirm fields update; trigger a folder/tag filter that hides the selected thread (existing
resetContentPanepath) and confirm fields clear and Reply disablesRisks or open questions: none identified
Canonical components/API contracts touched:
public/assets/paned_board_reader.js(extended)
Stage 4
Goal: Return to the board view after replying from it, reusing (not duplicating) the redirect guard built in
forte_reply.Dependencies: Stage 3
Expected changes: extend
Application.php::resolveComposeReplyReturnTo(string $requestedReturnTo, string $threadId): stringto also accept the literal/forteboard-view path (in addition to the existing/threads/{threadId}/fortepattern) as a valid return target;paned_board_compose_panel.phppassesreturnTo = '/forte'Verification approach:
curl -i -X POST /compose/replywithreturn_to=/forte→ redirect Location targets/forte?...; same request with noreturn_to→ unchanged classic fallback; same request with a malicious/unrecognizedreturn_to→ falls back to classic location (regression check on the existing guard); confirm the new reply appears on/forteafter submitting from thereRisks or open questions:
Board view's tag filter/sort state isn't part of the redirect target, so returning to
/fortealways lands on the unfiltered view — acceptable per Step 2 (not a stated requirement), but worth confirming with the user isn't surprising
Canonical components/API contracts touched:
Application.php::resolveComposeReplyReturnTo(extended),partials/reply_form.php(return_tofield reused unchanged)