Implement attribute help text caption

This commit is contained in:
Oliver Günther
2025-11-21 09:12:40 +01:00
parent 426c19f58b
commit 3552df22a6
24 changed files with 221 additions and 145 deletions
@@ -39,7 +39,7 @@ class AttributeHelpTexts::ShowComponent < ApplicationComponent
private
def title = @attribute_help_text.attribute_caption
def title = @attribute_help_text.attribute_field_name
def has_attachments? = @attribute_help_text.attachments.any?
@@ -42,7 +42,7 @@ class AttributeHelpTexts::ShowDialogComponent < ApplicationComponent
@dialog_id = dom_id(@attribute_help_text, :dialog)
@system_arguments = system_arguments
@system_arguments[:id] = dialog_id
@system_arguments[:title] = @attribute_help_text.attribute_caption
@system_arguments[:title] = @attribute_help_text.attribute_field_name
end
private
@@ -31,6 +31,8 @@
module OpenProject
module Common
class AttributeLabelComponent < ApplicationComponent
include AttributeHelpTextsHelper
def initialize(
attribute:,
model:,
@@ -50,10 +52,7 @@ module OpenProject
)
@required = required
@help_text = ::AttributeHelpText.for(model)
&.cached(current_user)
&.[](AttributeHelpText.normalize_value_for(:attribute_name, attribute))
@help_text = help_text_for(model, attribute, current_user:)
end
end
end
@@ -2,7 +2,7 @@
<% if attribute_help_text.present? %>
<%= render Primer::Beta::Text.new(tag: :p) do %>
<%= render Primer::Beta::Octicon.new(icon: :info, color: :muted, mr: 2) %>
<%= attribute_help_text.attribute_caption %>
<%= attribute_help_text.attribute_field_name %>
<% end %>
<%= render AttributeHelpTexts::ShowComponent.new(attribute_help_text:) %>
<% else %>
@@ -38,6 +38,7 @@ module AttributeHelpTexts
attribute :type
attribute :attribute_name
attribute :caption
attribute :help_text
validate :user_allowed
@@ -41,7 +41,10 @@ class AttributeHelpTextsController < ApplicationController
authorization_checked! :show_dialog
def index
@texts_by_type = AttributeHelpText.all_by_scope
@texts_by_type = AttributeHelpText
.where(type: @attribute_scope)
.to_a
.sort_by(&:attribute_field_name)
end
def show_dialog
@@ -102,7 +105,9 @@ class AttributeHelpTextsController < ApplicationController
private
def permitted_params_with_attachments
permitted_params.attribute_help_text.merge(attachment_params)
permitted_params
.attribute_help_text
.merge(attachment_params)
end
def attachment_params
@@ -120,13 +125,13 @@ class AttributeHelpTextsController < ApplicationController
end
def find_type_scope
name = params.fetch(:name, "WorkPackage")
name = params[:tab] || params[:name] || "WorkPackage"
submodule = AttributeHelpText.available_types.find { |mod| mod == name }
if submodule.nil?
render_404
end
@attribute_scope = AttributeHelpText.const_get(submodule)
@attribute_scope = AttributeHelpText.const_get(submodule).to_s
end
end
+97
View File
@@ -0,0 +1,97 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module AttributeHelpTexts
class Form < ApplicationForm
form do |attribute_form|
attribute_form.hidden(
name: :type,
value: model.type
)
attribute_form.select_list(
name: :attribute_name,
label: AttributeHelpText.human_attribute_name(:attribute_name),
required: true,
disabled: model.persisted?
) do |list|
selectable_attributes.each do |label, value|
list.option(
label:,
value:
)
end
end
attribute_form.text_field(
name: :caption,
caption: I18n.t("attribute_help_texts.caption"),
label: AttributeHelpText.human_attribute_name(:caption),
required: false
)
attribute_form.rich_text_area(
name: :help_text,
label: AttributeHelpText.human_attribute_name(:help_text),
required: true,
rich_text_options: {
showAttachments: true,
primerized: true,
resource: ::API::V3::HelpTexts::HelpTextRepresenter.new(
model,
current_user: User.current,
embed_links: true
),
footer: render(Primer::Beta::Text.new(color: :muted)) { I18n.t("attribute_help_texts.note_public") }
}
)
attribute_form.submit(
name: :submit,
label: I18n.t(:button_save),
scheme: :primary
)
end
private
def selectable_attributes
@selectable_attributes ||= begin
available = model.class.available_attributes
used = AttributeHelpText.used_attributes(model.type)
available
.reject { |key,| used.include? key }
.map { |key, label| [label, key] }
.sort_by { |label, _key| label.downcase }
end
end
end
end
+4 -8
View File
@@ -29,13 +29,9 @@
#++
module AttributeHelpTextsHelper
def selectable_attributes(instance)
available = instance.class.available_attributes
used = AttributeHelpText.used_attributes(instance.type)
available
.reject { |key,| used.include? key }
.map { |key, label| [label, key] }
.sort_by { |label, _key| label.downcase }
def help_text_for(model, attribute_name, current_user: User.current)
AttributeHelpText.for(model)
&.cached(current_user)
&.[](AttributeHelpText.normalize_value_for(:attribute_name, attribute_name))
end
end
+3 -3
View File
@@ -33,7 +33,7 @@ class AttributeHelpText < ApplicationRecord
def self.cached(user)
RequestStore.fetch(name) do
visible(user).select(:id, :attribute_name).index_by(&:attribute_name)
visible(user).select(:id, :attribute_name, :caption).index_by(&:attribute_name)
end
end
@@ -71,8 +71,8 @@ class AttributeHelpText < ApplicationRecord
validates :help_text, presence: true
validates :attribute_name, uniqueness: { scope: :type }
def attribute_caption
@attribute_caption ||= self.class.available_attributes[attribute_name]
def attribute_field_name
@attribute_field_name ||= self.class.available_attributes[attribute_name]
end
def attribute_scope
+13 -11
View File
@@ -30,17 +30,19 @@
class AttributeHelpText::WorkPackage < AttributeHelpText
def self.available_attributes
attributes = ::Type.translated_work_package_form_attributes
RequestStore.fetch(:attribute_help_text_work_package_attributes) do
attributes = ::Type.translated_work_package_form_attributes
# Start and finish dates are joined into a single field for non-milestones
attributes.delete "start_date"
attributes.delete "due_date"
# Start and finish dates are joined into a single field for non-milestones
attributes.delete "start_date"
attributes.delete "due_date"
# Status and project are currently special attribute that we need to add
attributes["status"] = WorkPackage.human_attribute_name "status"
attributes["project"] = WorkPackage.human_attribute_name "project"
# Status and project are currently special attribute that we need to add
attributes["status"] = WorkPackage.human_attribute_name "status"
attributes["project"] = WorkPackage.human_attribute_name "project"
attributes
attributes
end
end
validates :attribute_name, inclusion: { in: ->(*) { available_attributes.keys } }
@@ -51,9 +53,9 @@ class AttributeHelpText::WorkPackage < AttributeHelpText
def self.visible_condition(user)
visible_cf_names = WorkPackageCustomField
.manageable_by_user(user)
.pluck(:id)
.map { |id| "custom_field_#{id}" }
.manageable_by_user(user)
.pluck(:id)
.map { |id| "custom_field_#{id}" }
::AttributeHelpText
.where(attribute_name: visible_cf_names)
+1
View File
@@ -460,6 +460,7 @@ class PermittedParams
type
attribute_name
help_text
caption
),
ldap_auth_source: %i(
name
@@ -1,63 +0,0 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<%= error_messages_for "attribute_help_text" %>
<section class="form--section" id="custom_field_form">
<div class="form--field -required">
<% if local_assigns[:editing] %>
<%= f.select :attribute_name,
[[@attribute_help_text.attribute_caption, @attribute_help_text.attribute_name]],
{},
disabled: true %>
<% else %>
<%= f.select :attribute_name, selectable_attributes(@attribute_help_text), container_class: "-middle" %>
<% end %>
</div>
<% resource = ::API::V3::HelpTexts::HelpTextRepresenter.new(
@attribute_help_text,
current_user: current_user,
embed_links: true
) %>
<div class="form--field -required -visible-overflow">
<%= f.text_area :help_text,
cols: 100,
rows: 20,
class: "wiki-edit",
with_text_formatting: true,
resource: resource %>
<div class="form--field-instructions">
<p>
<strong><%= t(:note) %>:</strong>
<%= t("attribute_help_texts.note_public") %>
</p>
</div>
</div>
</section>
+3 -4
View File
@@ -1,5 +1,4 @@
<% entries = @texts_by_type[tab[:name]] || [] %>
<% if entries.any? %>
<% if @texts_by_type.any? %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table" data-controller="table-highlighting">
@@ -35,10 +34,10 @@
</tr>
</thead>
<tbody>
<% entries.each do |attribute_help_text| -%>
<% @texts_by_type.each do |attribute_help_text| -%>
<tr class="attribute-help-text--entry">
<td>
<%= link_to h(attribute_help_text.attribute_caption),
<%= link_to h(attribute_help_text.attribute_field_name),
edit_attribute_help_text_path(attribute_help_text) %>
</td>
<td>
+16 -14
View File
@@ -27,28 +27,30 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), t(:"attribute_help_texts.edit", attribute_caption: @attribute_help_text.attribute_caption) %>
<% html_title t(:label_administration), t(:"attribute_help_texts.edit", attribute_field_name: @attribute_help_text.attribute_field_name) %>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { @attribute_help_text.attribute_caption }
header.with_title { @attribute_help_text.attribute_field_name }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t(:label_administration) },
{ href: attribute_help_texts_path, text: t(:"attribute_help_texts.label_plural") },
{ href: attribute_help_texts_path(tab: @attribute_help_text.attribute_scope), text: @attribute_help_text.type_caption },
@attribute_help_text.attribute_caption]
@attribute_help_text.attribute_field_name]
)
end
%>
<%= labelled_tabular_form_for @attribute_help_text,
as: "attribute_help_text",
url: { action: :update },
html: {
id: "attribute_help_text_form",
data: { turbo: false } # reload page to update previews
} do |f| %>
<%= render partial: "form", locals: { f: f, editing: true } %>
<%= hidden_field_tag "attribute_scope", @attribute_help_text.attribute_scope %>
<%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
<% end %>
<div class="op-admin-settings-form-wrapper">
<%=
primer_form_with(
model: @attribute_help_text,
scope: :attribute_help_text,
url: { action: :update },
method: :patch,
data: { turbo: false } # reload page to update previews
) do |f|
render AttributeHelpTexts::Form.new(f)
end
%>
</div>
+12 -8
View File
@@ -41,11 +41,15 @@ See COPYRIGHT and LICENSE files for more details.
end
%>
<%= labelled_tabular_form_for @attribute_help_text,
as: "attribute_help_text",
url: { action: :create },
html: { id: "attribute_help_text_form" } do |f| %>
<%= render partial: "form", locals: { f: f } %>
<%= f.hidden_field :type, value: @attribute_help_text.type %>
<%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
<% end %>
<div class="op-admin-settings-form-wrapper">
<%=
primer_form_with(
model: @attribute_help_text,
scope: :attribute_help_text,
url: { action: :create },
method: :post,
) do |f|
render AttributeHelpTexts::Form.new(f)
end
%>
</div>
+4 -2
View File
@@ -205,12 +205,13 @@ en:
error_cannot_act_self: "Cannot perform actions on your own uploaded files."
attribute_help_texts:
note_public: "Any text and images you add to this field is publicly visible to all logged in users!"
caption: "This short version will be displayed as caption of the attribute."
note_public: "Any text and images you add to this field is publicly visible to all logged in users."
text_overview: "In this view, you can create custom help texts for attributes view. When defined, these texts can be shown by clicking the help icon next to its belonging attribute."
label_plural: "Attribute help texts"
show_preview: "Preview text"
add_new: "Add help text"
edit: "Edit help text for %{attribute_caption}"
edit: "Edit help text for %{attribute_field_name}"
background_jobs:
status:
@@ -1206,6 +1207,7 @@ en:
attribute_help_text:
attribute_name: "Attribute"
help_text: "Help text"
caption: "Caption"
auth_provider:
scim_clients: "SCIM clients"
calculated_value_error:
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddCaptionToAttributeHelpTexts < ActiveRecord::Migration[8.0]
def change
add_column :attribute_help_texts, :caption, :text
end
end
@@ -86,6 +86,8 @@ export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin impl
@Input() public showAttachments = true;
@Input() public primerized = false;
@Input() public storageKey?:string;
// Output save requests (ctrl+enter and cmd+enter)
@@ -1,25 +1,35 @@
<div class="op-ckeditor--wrapper">
<op-ckeditor [context]="context"
[content]="initialContent"
(initializeDone)="setup($event)"
(contentChanged)="updateContent($event)"
(saveRequested)="saveForm()"
(editorEscape)="editorEscape.emit()"
(editorKeyup)="editorKeyup.emit()"
(editorBlur)="editorBlurred()"
(editorFocus)="editorFocused()"
/>
[content]="initialContent"
(initializeDone)="setup($event)"
(contentChanged)="updateContent($event)"
(saveRequested)="saveForm()"
(editorEscape)="editorEscape.emit()"
(editorKeyup)="editorKeyup.emit()"
(editorBlur)="editorBlurred()"
(editorFocus)="editorFocused()"
/>
</div>
@if (showAttachments && halResource?.attachments) {
<fieldset
class="form--fieldset op-ckeditor--attachments"
>
<legend class="form--fieldset-legend">
{{ text.attachments }}
</legend>
<op-attachments [resource]="halResource"
@if (primerized) {
<div class="FormControl width-full FormControl--fullWidth op-ckeditor--attachments">
<span class="FormControl-label">{{ text.attachments }}</span>
</div>
<op-attachments
[resource]="halResource"
[destroyImmediately]="false"
/>
</fieldset>
/>
} @else {
<fieldset
class="form--fieldset op-ckeditor--attachments"
>
<legend class="form--fieldset-legend">
{{ text.attachments }}
</legend>
<op-attachments [resource]="halResource"
[destroyImmediately]="false"
/>
</fieldset>
}
}
@@ -10,8 +10,9 @@
@include op-uc-table-header-styles
// Wrapper for full text element
// matches the primer form default spacing
opce-ckeditor-augmented-textarea .op-ckeditor--attachments
margin-top: 2rem
margin-top: var(--stack-gap-normal)
// Ensure same border and min-height for preview
.ck-content,
@@ -54,7 +54,7 @@ module API
getter: ->(*) {
::API::Utilities::PropertyNameConverter.from_ar_name(attribute_name)
}
property :attribute_caption
property :attribute_field_name
property :attribute_scope,
as: :scope
property :help_text,
@@ -5,6 +5,8 @@ module Primer
module Forms
module Dsl
module InputMethods
include AttributeHelpTextsHelper
def multi(**, &)
super(**decorate_options(**), &)
end
@@ -77,12 +79,18 @@ module Primer
if include_help_text && supports_help_texts?(form.model)
attribute_name = help_text_options[:attribute_name] || options[:name]
options[:label] = form.wrap_attribute_label_with_help_text(options[:label], attribute_name)
options[:caption] ||= help_text_caption_for(attribute_name)
end
options
end
private
def help_text_caption_for(attribute_name)
help_text = help_text_for(form.model, attribute_name)
help_text&.caption
end
def supports_help_texts?(model)
return @supports_help_texts if defined?(@supports_help_texts)
@@ -12,4 +12,7 @@
),
class: @input.classes,
data: @rich_text_data %>
<% if @rich_text_options[:footer] %>
<%= @rich_text_options[:footer] %>
<% end %>
<% end %>
@@ -60,7 +60,7 @@ RSpec.describe AttributeHelpTexts::ShowDialogComponent, type: :component do
describe "dialog heading" do
it "renders the heading" do
expect(subject).to have_heading attribute_help_text.attribute_caption
expect(subject).to have_heading attribute_help_text.attribute_field_name
end
end