Skip to content

[DREAM-784] Pilot LiveComponent on documents page header (experiment, do not merge)#24442

Draft
myabc wants to merge 12 commits into
devfrom
experiment/DREAM-784-livecomponent-editable-page-headers
Draft

[DREAM-784] Pilot LiveComponent on documents page header (experiment, do not merge)#24442
myabc wants to merge 12 commits into
devfrom
experiment/DREAM-784-livecomponent-editable-page-headers

Conversation

@myabc

@myabc myabc commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Ticket

https://community.openproject.org/wp/DREAM-784

Draft / visibility only — not for merge. This is an experiment evaluating LiveComponent (client-owned component state + server re-render for ViewComponent) on a single component. It carries library workarounds in config/application.rb and a not-adoption-ready verdict; it is shared so the team can see the shape of the integration, not to land.

What are you trying to accomplish?

Pilot LiveComponent 0.4.0 on the documents page-header title toggle, replacing the current state: + turbo_stream house pattern (three controller actions whose only job is re-rendering the component) with client-owned state and a server-authoritative reflex.

The title toggle now works as:

  • Edit / Cancel — client-state flips (this.render({ state })) through an authenticated render endpoint, morphed in at the component boundary.
  • Save — a LiveComponent reflex (component.call("update_title", { title })), server-authoritative.

Net: three controller actions + their routes + OpTurbo wiring deleted, replaced by one reflex method plus a small sidecar LiveController. The full edit → blank-save (validation banner) → save → cancel cycle passes in real Chrome via Cuprite.

Screenshots

To add before/after of the title toggle if this goes further.

What approach did you choose and why?

Eight commits, built and reviewed task-by-task:

  1. Add live_component 0.4.0 (gem + npm), with a boot-crash workaround in config/application.rb (the engine's on_load(:action_controller) hook crashes on ActionController::API).
  2. Convert the component to a kwargs-only initializer (LiveComponent round-trips only keyword props).
  3. include LiveComponent::Base + re-target the title turbo streams (the custom element renders outside the OpTurbo wrapper).
  4. Authenticated render endpoint — LiveComponentsController < ApplicationController, NOT the library's Rack middleware, so User.current/session/CSRF apply. Root-component allowlist.
  5. Client-side edit/cancel toggle (commit A).
  6. Reflex save (commit B); delete update_title/edit_title/cancel_title_edit.
  7. Hardening — body-size cap, malformed-payload rejection, per-component read guard.
  8. Polish.

Why a custom endpoint instead of the shipped middleware: the library's Rack middleware bypasses the controller stack, so User.current is never set and permission-dependent UI renders as anonymous. Routing through a real controller costs the "no Rails overhead" speed claim but is the only correct option here.

Verdict: the programming model is genuinely pleasant and collapsed the boilerplate as hoped, but v0.4.0 is not adoption-ready — nine library gaps, eight filed upstream (livecomponent/livecomponent#3#10), one held for private security disclosure. Full write-up lives in the team KB. Headline blockers: the engine boot crash, a global turbo:submit-end handler that throws on every non-LiveComponent Turbo form once the library loads, and a serializer trust issue requiring a per-component read guard.

Merge checklist

Not applicable — draft for visibility, not for merge.

  • Added/updated tests — request spec (8 examples) + feature-spec coverage adapted; browser gate green
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...) — Chrome only (pilot)

myabc added 8 commits July 24, 2026 14:44
Adds the live_component gem and @camertron/live-component npm
package to pilot client-initiated component re-rendering on the
documents page header.

https://community.openproject.org/wp/DREAM-784

Registers an early action_controller load hook giving helper-less
controller bases (ActionController::API) a no-op helper, because
the gem's own load hook calls helper on every controller base and
crashes boot (library gap in 0.4.0).
Converts the component to a kwargs-only initializer because
LiveComponent round-trips only keyword parameters as client
state; positional ApplicationComponent models cannot survive a
re-render.
Wraps the page header in a live-component custom element and
re-targets the title turbo streams at the element's DOM id,
since the element renders outside the OpTurbo wrapper and
nested updates would stack stale elements.
Routes client-initiated re-renders through a regular Rails
controller instead of the library middleware so User.current is
set and component permission checks keep working. Restricts the
root component to the pilot allowlist.
Moves the show/edit flip of the documents page header into a
sidecar LiveController backed by the authenticated render
endpoint, and drops the edit_title and cancel_title_edit
actions whose only job was re-rendering the component. Saving
still posts to update_title and replaces via turbo stream.
Replaces the update_title controller action with a reflex
method on the page header component so the whole edit cycle
runs through LiveComponent. Validation errors render inline as
a banner instead of a flash.
Rejects malformed or oversized request bodies before JSON
parsing/gzip decoding runs, closing a gzip-bomb surface, and fixes
the status symbol used for that rejection (:content_too_large is
not a valid Rack status; :payload_too_large is).

Adds a render? guard to PageHeaderComponent, plus a RenderGuard
module prepended above the gem's own prepended render_in, so an
unauthorized request short-circuits before LiveComponent::Base ever
serializes props. A bare render? alone would not have been enough:
the gem always serializes full model attributes into the data-state
HTML attribute before consulting render?, so it would still leak a
document's title/description (and a freshly re-signed GID) to a
signed-in user who cannot view it. This matters because the render
endpoint performs no authorization of its own and the gem's
ModelSerializer lets a client-controlled signed flag downgrade a
reload to an unsigned GID lookup.
Attaches a no-op .catch() to every render() call in the page header
controller: the library's task queue logs failures but still
re-rejects, so the prior fire-and-forget calls left every failed
render as an unhandled promise rejection. Corrects the transport
comment that described the same behavior backwards.

Restores the after_update URL-param cleanup dropped from the
original plan, and unifies the remaining symbol-keyed I18n.t call to
match its sibling. Documents why the edit form's url: "#" is left
alone.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces an experimental LiveComponent (v0.4.0) integration to pilot client-owned state + server re-render/reflexes for the Documents page header title editing flow, replacing the existing Turbo Stream/controller-action pattern with a dedicated authenticated render endpoint and a component reflex.

Changes:

  • Added an authenticated /live_components/render endpoint (LiveComponentsController) with basic allowlisting and payload hardening for the pilot component.
  • Reworked Documents::ShowEditView::PageHeaderComponent to use LiveComponent::Base and a reflex (update_title) for server-authoritative updates, removing the prior Turbo Stream routes/actions.
  • Added frontend LiveComponent wiring (custom element + transport + Stimulus controller) to support edit/cancel client state flips and save via reflex.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
spec/requests/live_components_spec.rb Request spec coverage for the LiveComponent render endpoint (authz, reflex, allowlist).
modules/documents/spec/features/documents/project/show_edit_document_spec.rb Adapts feature spec to the LiveComponent-driven edit/save/cancel flow.
modules/documents/lib/open_project/documents/engine.rb Removes deleted controller actions from manage_documents permission mapping.
modules/documents/config/routes.rb Removes member routes for edit_title/update_title/cancel_title_edit.
modules/documents/app/controllers/documents_controller.rb Deletes the title-edit Turbo Stream actions and helper method.
modules/documents/app/components/documents/show_edit_view/page_header_component.rb Converts page header to LiveComponent with render-guard + reflex update.
modules/documents/app/components/documents/show_edit_view/page_header_component.html.erb Switches title editing UI from Primer editable slot to LiveComponent-driven form/actions.
modules/documents/app/components/documents/show_edit_view/collaboration_disabled_notice_component.html.erb Updates PageHeaderComponent invocation (kwargs + LiveComponent attributes).
modules/documents/app/components/documents/show_edit_view/block_note_editor_component.html.erb Updates PageHeaderComponent invocation (kwargs + LiveComponent attributes).
Gemfile Adds live_component gem for the pilot.
Gemfile.lock Locks live_component and transitive dependencies (use_context, weak_key_map).
frontend/src/stimulus/setup.ts Registers LiveComponent and the Documents page-header LiveController/element.
frontend/src/stimulus/live-component-transport.ts Adds a CSRF-aware transport implementation for LiveComponent render requests.
frontend/src/stimulus/controllers/dynamic/documents/page-header-live.controller.ts Implements edit/cancel client state flips and save via reflex call.
frontend/package.json Adds @camertron/live-component dependency.
frontend/package-lock.json Locks the new npm dependency.
config/routes.rb Adds the /live_components/render route.
config/application.rb Adds a workaround for the LiveComponent engine on_load(:action_controller) crash on ActionController::API.
app/controllers/live_components_controller.rb Implements the authenticated LiveComponent render endpoint with allowlisting/size checks.
Files not reviewed (1)
  • frontend/package-lock.json: Generated file

Comment on lines +77 to +82
def initialize(document:, project:, state: :show)
super()
@document = document
@project = project
@state = state.to_sym.presence_in(STATES) || :show
end
Comment on lines +146 to +157
def title_edit_form
# url: "#" is a harmless no-JS fallback (a GET to the current page) --
# submission is normally intercepted by the Stimulus #save action
# before it ever hits the network. There is no real route to point
# this at (saving goes through the reflex, not a controller action),
# so don't "fix" this into one.
form_with(model: document, url: "#",
class: "d-flex",
data: { action: "#{self.class.__lc_controller}#save" }) do |f|
safe_join([title_edit_input(f), title_edit_save_button, title_edit_cancel_button])
end
end
Comment on lines +21 to +25
const json = JSON.stringify(request);
const bytes = new TextEncoder().encode(json);
let binary = '';
bytes.forEach((b) => { binary += String.fromCharCode(b); });
const payload = btoa(binary);
myabc added 4 commits July 25, 2026 18:55
`render?` guarded only `document` while `project` arrived as its own
client-controlled prop, so a mismatched pair leaked the foreign
project and evaluated `manage_documents` against a project of the
caller's choosing. Reflexes run before RenderGuard, so `update_title`
authorizes its own read.
The endpoint accepted any prop name, any `__lc_attributes`, any reflex
and any reflex argument, and its size cap sat above an unbounded
`Zlib.gunzip`. Login is now required outright, since
`check_if_login_required` is a no-op on public instances.
Every `render()` swallowed its rejection, so a 500, an expired session
and a permission-denied empty body all presented as the button doing
nothing.
Restructures the file so contexts override `props`/`reflexes` rather
than rebuilding the body, then covers each guard -- including the
`_lc_symkeys` marker a real client round-trips, which no
`serialize_props`-built payload exercises.
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./modules/backlogs/spec/features/inbox_column_spec.rb[1:7:1]
  • rspec ./spec/features/workflows/edit_spec.rb[1:4:7]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #24442, linked for reference only):

- `rspec ./modules/backlogs/spec/features/inbox_column_spec.rb[1:7:1]`
- `rspec ./spec/features/workflows/edit_spec.rb[1:4:7]`

Treat this as a standalone task, unrelated to PR #24442. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #24442 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @myabc to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @myabc, and request a review from @myabc.
On every commit, set @myabc as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE experiment javascript Pull requests that update Javascript code ruby Pull requests that update Ruby code

Development

Successfully merging this pull request may close these issues.

2 participants