Skip to content

In-place description editing

Detail pages can let users edit a rich-text description without opening the full edit form. The UI follows the classic task detail: a "Description" heading with a pencil, an "Add description" empty state, and CKEditor opened in place with Save and Cancel. Saving updates the block over AJAX.

The shared pieces live in core:

Piece Purpose
EasyControllersConcerns::InlineDescription inline_description macro, description_edit and description_update actions
InlineDescriptionHelper element ids and the image-upload rule
easy_engines/easy_extensions/app/views/common/_inline_description.html.erb description block for the detail page
common/_inline_description_form.html.erb, common/inline_description_edit.js.erb, common/inline_description_update.js.erb in-place editor and its responses
spec/support/inline_description_requests.rb shared request examples

CRM entities (accounts, contact groups, leads, partners, personal contacts, campaigns and opportunities) are the current users. The classic task detail still has its own implementation in EasyIssuesController.

Controller

Include the concern, call inline_description with the existing finder, and define a private inline_description_entity that returns the record the finder loaded. The attribute defaults to :description.

Controller wiring
class EasyContactsController < ApplicationController
  include EasyControllersConcerns::InlineDescription

  before_action :find_contact, only: %i[show edit update]
  inline_description load_with: :find_contact, attribute: :author_note

  private

  def inline_description_entity
    @easy_contact
  end
end

inline_description runs the finder for description_edit and description_update, so the finder's own only: list stays unchanged. Call it right after the finder so the filter order is preserved. Override inline_description_saved for side effects that the controller's update action performs and the description save also needs, such as notifications sent from the controller.

By default the user can edit when safe_attribute? allows the attribute and the record's editable? returns true. A controller can override inline_description_editable?(entity) instead; the override replaces the whole rule and also decides whether the page shows the pencil and the empty state. Contact groups do this with their controller's visibility rule (contact_group_visible?), because the model has no editable?.

Responses:

  • a record the user cannot edit returns 403;
  • a missing or nested attribute returns 400;
  • a failed validation, or a save that loses an optimistic-locking race, returns 422 with { errors: [...] }, shown as a flash while the editor stays open;
  • a successful save re-renders the block, and for models with lock_version it updates window.easy_lock_version for the full edit form and inline attribute edits on the page.

Routes and permissions

config/routes.rb
1
2
3
4
5
6
resources :easy_leads do
  member do
    get :description_edit
    patch :description_update
  end
end

Add the member routes to the resource named after the model, because the views and shared specs build URLs with polymorphic_path. They may sit inside a rys_feature block like the rest of the resource. Grant description_edit and description_update in every permission that grants update, including permissions extended from other plugins with Redmine::AccessControl.update_permission.

View

Detail page
<%= render partial: "common/inline_description", locals: { entity: @easy_campaign } %>

Pass the model, not a decorator (for example @easy_lead.model when the view decorated @easy_lead). Place the block right after the header attributes or custom fields, before attachments, related lists and tabs. Nothing is rendered when the description is empty and the user cannot edit it.

Image upload is offered when Model.strips_inline_images?(attribute) is true, which matches what the full edit forms offer. Cancel keeps the CKEditor autosave draft, and reopening offers to restore it; a successful save clears the draft.

Specs

All shared examples need entity and attribute lets. "inline description editing" and "inline description parameter handling" need a record the user can edit; "inline description editing forbidden" needs one the user can see but not edit.

Request spec
let(:entity) { FactoryBot.create(:easy_lead, description: "<p>Note</p>") }
let(:attribute) { :description }

context "when user is admin", logged: :admin do
  it_behaves_like "inline description parameter handling"
end

context "when user manages leads", logged: true do
  before { Role.non_member.add_permission!(:view_easy_leads, :manage_easy_leads) }

  it_behaves_like "inline description editing"
end

context "when user can only view leads", logged: true do
  before { Role.non_member.add_permission!(:view_easy_leads) }

  it_behaves_like "inline description editing forbidden"
end