mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Add tab to project custom fields settings
This commit is contained in:
@@ -62,6 +62,13 @@ module Admin
|
||||
path: custom_field_projects_path(@custom_field),
|
||||
label: t(:label_project_plural)
|
||||
}
|
||||
|
||||
tabs <<
|
||||
{
|
||||
name: "attribute_help_text",
|
||||
path: attribute_help_text_admin_settings_project_custom_field_path(@custom_field),
|
||||
label: AttributeHelpText.human_plural_model_name
|
||||
}
|
||||
end
|
||||
|
||||
tabs
|
||||
|
||||
@@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
++#%>
|
||||
<%=
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title { I18n.t(:"attribute_help_texts.label_plural") }
|
||||
header.with_title { I18n.AttributeHelpText.human_plural_model_name }
|
||||
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: :normal)
|
||||
|
||||
helpers.render_tab_header_nav(header, @tabs)
|
||||
|
||||
@@ -41,7 +41,7 @@ class AttributeHelpTexts::IndexPageHeaderComponent < ApplicationComponent
|
||||
def breadcrumb_items
|
||||
[
|
||||
{ href: admin_index_path, text: t("label_administration") },
|
||||
helpers.nested_breadcrumb_element(t(:"attribute_help_texts.label_plural"),
|
||||
helpers.nested_breadcrumb_element(AttributeHelpText.human_plural_model_name,
|
||||
I18n.t(currently_selected_tab[:label].to_s))
|
||||
]
|
||||
end
|
||||
|
||||
@@ -68,6 +68,13 @@ module Settings
|
||||
label: t(:label_project_mappings)
|
||||
}
|
||||
|
||||
tabs <<
|
||||
{
|
||||
name: "attribute_help_text",
|
||||
path: attribute_help_text_admin_settings_project_custom_field_path(@custom_field),
|
||||
label: AttributeHelpText.human_plural_model_name
|
||||
}
|
||||
|
||||
tabs
|
||||
end
|
||||
|
||||
|
||||
@@ -41,13 +41,15 @@ module Admin::Settings
|
||||
before_action :set_sections, only: %i[show index edit update move drop]
|
||||
before_action :find_custom_field,
|
||||
only: %i(show edit project_mappings new_link link unlink update destroy delete_option reorder_alphabetical
|
||||
move drop role_assignment update_role_assignment role_assignment_preview_dialog)
|
||||
move drop role_assignment update_role_assignment role_assignment_preview_dialog
|
||||
attribute_help_text update_attribute_help_text)
|
||||
before_action :prepare_custom_option_position, only: %i(update create)
|
||||
before_action :find_custom_option, only: :delete_option
|
||||
before_action :project_custom_field_mappings_query, only: %i[project_mappings unlink]
|
||||
before_action :find_custom_field_projects_to_link, only: :link
|
||||
before_action :find_unlink_project_custom_field_mapping, only: :unlink
|
||||
before_action :prepare_role_assignment_form, only: %i[role_assignment update_role_assignment]
|
||||
before_action :find_or_initialize_attribute_help_text, only: %i[attribute_help_text update_attribute_help_text]
|
||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||
|
||||
def index
|
||||
@@ -184,6 +186,24 @@ module Admin::Settings
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def attribute_help_text; end
|
||||
|
||||
def update_attribute_help_text
|
||||
service_class = @attribute_help_text.persisted? ? ::AttributeHelpTexts::UpdateService : ::AttributeHelpTexts::CreateService
|
||||
call = service_class
|
||||
.new(user: current_user, model: @attribute_help_text)
|
||||
.call(attribute_help_text_params_with_attachments)
|
||||
|
||||
if call.success?
|
||||
flash[:notice] = t(:notice_successful_update)
|
||||
redirect_to attribute_help_text_admin_settings_project_custom_field_path(@custom_field)
|
||||
else
|
||||
@attribute_help_text = call.result
|
||||
flash.now[:error] = call.message || I18n.t("notice_internal_server_error")
|
||||
render :attribute_help_text, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_role_assignment_form
|
||||
@@ -266,5 +286,35 @@ module Admin::Settings
|
||||
def role_assignment_params
|
||||
params.expect(custom_field: [:role_id])
|
||||
end
|
||||
|
||||
def find_or_initialize_attribute_help_text
|
||||
@attribute_help_text = AttributeHelpText::Project.find_or_initialize_by(
|
||||
attribute_name: "custom_field_#{@custom_field.id}"
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_help_text_params
|
||||
params
|
||||
.require(:attribute_help_text)
|
||||
.permit(:help_text, :caption, :type, :attribute_name)
|
||||
.merge(
|
||||
type: "AttributeHelpText::Project",
|
||||
attribute_name: "custom_field_#{@custom_field.id}"
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_help_text_params_with_attachments
|
||||
attribute_help_text_params.merge(attachment_params_for_help_text)
|
||||
end
|
||||
|
||||
def attachment_params_for_help_text
|
||||
attachment_params = permitted_params.attachments.to_h
|
||||
|
||||
if attachment_params.any?
|
||||
{ attachment_ids: attachment_params.values.map(&:values).flatten }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,17 +36,25 @@ module AttributeHelpTexts
|
||||
value: model.type
|
||||
)
|
||||
|
||||
attribute_form.select_list(
|
||||
name: :attribute_name,
|
||||
label: attribute_name(:attribute_name),
|
||||
required: true,
|
||||
disabled: model.persisted?
|
||||
) do |list|
|
||||
selectable_attributes.each do |label, value|
|
||||
list.option(
|
||||
label:,
|
||||
value:
|
||||
)
|
||||
if hide_attribute_name?
|
||||
attribute_form.hidden(
|
||||
name: :attribute_name,
|
||||
value: model.attribute_name
|
||||
)
|
||||
else
|
||||
attribute_form.select_list(
|
||||
name: :attribute_name,
|
||||
label: attribute_name(:attribute_name),
|
||||
required: true,
|
||||
disabled: model.persisted?
|
||||
) do |list|
|
||||
selectable_attributes.each do |label, value|
|
||||
list.option(
|
||||
label:,
|
||||
value:,
|
||||
selected: value == model.attribute_name
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -80,6 +88,14 @@ module AttributeHelpTexts
|
||||
)
|
||||
end
|
||||
|
||||
def initialize(hide_attribute_name: false)
|
||||
super()
|
||||
|
||||
@hide_attribute_name = hide_attribute_name
|
||||
end
|
||||
|
||||
def hide_attribute_name? = @hide_attribute_name
|
||||
|
||||
private
|
||||
|
||||
def selectable_attributes
|
||||
@@ -87,8 +103,10 @@ module AttributeHelpTexts
|
||||
available = model.class.available_attributes
|
||||
used = AttributeHelpText.used_attributes(model.type)
|
||||
|
||||
available
|
||||
.reject { |key,| used.include? key }
|
||||
# Always include the current attribute_name if it's set, even if it's "used"
|
||||
filtered = available.reject { |key,| used.include?(key) && key != model.attribute_name }
|
||||
|
||||
filtered
|
||||
.map { |key, label| [label, key] }
|
||||
.sort_by { |label, _key| label.downcase }
|
||||
end
|
||||
|
||||
@@ -83,6 +83,14 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
ActiveRecord::Base.connection.select_value(union_query)
|
||||
end
|
||||
|
||||
def self.human_model_name
|
||||
model_name.human
|
||||
end
|
||||
|
||||
def self.human_plural_model_name(count: 2)
|
||||
model_name.human(count:)
|
||||
end
|
||||
|
||||
# Returns all the attribute names as symbols.
|
||||
# @return [Array<Symbol>]
|
||||
def attribute_keys
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<%#-- 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.
|
||||
|
||||
++#%>
|
||||
|
||||
<% html_title t(:label_administration), t("settings.project_attributes.heading"), @custom_field.name, AttributeHelpText.human_plural_model_name %>
|
||||
|
||||
<%=
|
||||
render(
|
||||
Settings::ProjectCustomFields::EditFormHeaderComponent.new(
|
||||
custom_field: @custom_field,
|
||||
selected: :attribute_help_text
|
||||
)
|
||||
)
|
||||
%>
|
||||
|
||||
<%= error_messages_for "attribute_help_text" %>
|
||||
|
||||
<div class="op-admin-settings-form-wrapper">
|
||||
<%=
|
||||
primer_form_with(
|
||||
model: @attribute_help_text,
|
||||
scope: :attribute_help_text,
|
||||
url: update_attribute_help_text_admin_settings_project_custom_field_path(@custom_field),
|
||||
method: :put
|
||||
) do |f|
|
||||
render AttributeHelpTexts::Form.new(f, hide_attribute_name: true)
|
||||
end
|
||||
%>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
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, text: AttributeHelpText.human_plural_model_name },
|
||||
{ href: attribute_help_texts_path(tab: @attribute_help_text.attribute_scope), text: @attribute_help_text.type_caption },
|
||||
@attribute_help_text.attribute_field_name]
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
<% html_title t(:label_administration), t(:"attribute_help_texts.label_plural") %>
|
||||
<% html_title t(:label_administration), AttributeHelpText.human_plural_model_name %>
|
||||
|
||||
<% tabs = [
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
header.with_title { t(:"attribute_help_texts.add_new") }
|
||||
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, text: AttributeHelpText.human_plural_model_name },
|
||||
{ href: attribute_help_texts_path(tab: @attribute_help_text.attribute_scope), text: @attribute_help_text.type_caption },
|
||||
t(:"attribute_help_texts.add_new")]
|
||||
)
|
||||
|
||||
@@ -470,7 +470,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
|
||||
menu.push :attribute_help_texts,
|
||||
{ controller: "/attribute_help_texts" },
|
||||
caption: :"attribute_help_texts.label_plural",
|
||||
caption: AttributeHelpText.human_plural_model_name,
|
||||
icon: "question",
|
||||
if: ->(_) { User.current.allowed_globally?(:edit_attribute_help_texts) }
|
||||
|
||||
|
||||
@@ -208,7 +208,6 @@ en:
|
||||
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_field_name}"
|
||||
@@ -1932,7 +1931,9 @@ en:
|
||||
|
||||
models:
|
||||
attachment: "File"
|
||||
attribute_help_text: "Attribute help text"
|
||||
attribute_help_text:
|
||||
one: "Attribute help text"
|
||||
other: "Attribute help texts"
|
||||
auth_provider:
|
||||
one: "Authentication provider"
|
||||
other: "Authentication providers"
|
||||
|
||||
@@ -674,6 +674,9 @@ Rails.application.routes.draw do
|
||||
get :role_assignment
|
||||
post :update_role_assignment
|
||||
get :role_assignment_preview_dialog
|
||||
|
||||
get :attribute_help_text
|
||||
put :update_attribute_help_text
|
||||
end
|
||||
resources :items, controller: "/admin/settings/project_custom_fields/hierarchy/items" do
|
||||
member do
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Project custom field attribute help text", :js do
|
||||
shared_let(:admin) { create(:admin) }
|
||||
shared_let(:project_custom_field_section) { create(:project_custom_field_section) }
|
||||
shared_let(:project_custom_field) do
|
||||
create(:project_custom_field,
|
||||
name: "Project Rating",
|
||||
field_format: "text",
|
||||
project_custom_field_section:)
|
||||
end
|
||||
|
||||
let(:editor) { Components::WysiwygEditor.new }
|
||||
let(:image_fixture) { UploadedFile.load_from("spec/fixtures/files/image.png") }
|
||||
|
||||
before do
|
||||
login_as(admin)
|
||||
end
|
||||
|
||||
describe "creating attribute help text for project custom field" do
|
||||
it "allows creating help text from the custom field edit page" do
|
||||
visit edit_admin_settings_project_custom_field_path(project_custom_field)
|
||||
|
||||
# Navigate to attribute help text tab
|
||||
click_on AttributeHelpText.human_plural_model_name
|
||||
|
||||
expect(page).to have_current_path(
|
||||
attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
)
|
||||
|
||||
# Verify attribute field is hidden
|
||||
expect(page).to have_no_css("#attribute_help_text_attribute_name")
|
||||
|
||||
# Fill in caption
|
||||
fill_in "Caption", with: "Rating help"
|
||||
|
||||
# Fill in help text
|
||||
editor.set_markdown("Please rate this project from 1-5 stars")
|
||||
|
||||
# Save
|
||||
click_button "Save"
|
||||
|
||||
# Should return to the attribute help text tab
|
||||
expect(page).to have_current_path(
|
||||
attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
)
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
# Verify the help text was created
|
||||
help_text = AttributeHelpText::Project.find_by(attribute_name: "custom_field_#{project_custom_field.id}")
|
||||
expect(help_text).to be_present
|
||||
expect(help_text.caption).to eq("Rating help")
|
||||
expect(help_text.help_text).to include("Please rate this project from 1-5 stars")
|
||||
end
|
||||
|
||||
it "allows creating help text with attachments" do
|
||||
visit attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
|
||||
# Fill in caption
|
||||
fill_in "Caption", with: "Rating with image"
|
||||
|
||||
# Fill in help text
|
||||
editor.set_markdown("Rating guidelines below")
|
||||
|
||||
# Add an image
|
||||
editor.drag_attachment image_fixture.path, "Rating guideline image"
|
||||
editor.attachments_list.expect_attached("image.png")
|
||||
|
||||
# Save
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
# Verify the help text was created with attachment
|
||||
help_text = AttributeHelpText::Project.find_by(attribute_name: "custom_field_#{project_custom_field.id}")
|
||||
expect(help_text).to be_present
|
||||
expect(help_text.help_text).to include("Rating guidelines below")
|
||||
expect(help_text.help_text).to match(/\/api\/v3\/attachments\/\d+\/content/)
|
||||
expect(help_text.attachments).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe "editing attribute help text for project custom field" do
|
||||
let!(:existing_help_text) do
|
||||
create(:project_help_text,
|
||||
attribute_name: "custom_field_#{project_custom_field.id}",
|
||||
caption: "Original caption",
|
||||
help_text: "Original help text")
|
||||
end
|
||||
|
||||
it "allows editing existing help text" do
|
||||
visit attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
|
||||
# Should show existing values
|
||||
expect(page).to have_field("Caption", with: "Original caption")
|
||||
|
||||
# Update caption
|
||||
fill_in "Caption", with: "Updated caption"
|
||||
|
||||
# Update help text
|
||||
editor.clear
|
||||
editor.set_markdown("Updated help text with **bold** text")
|
||||
|
||||
# Save
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
# Verify the help text was updated
|
||||
existing_help_text.reload
|
||||
expect(existing_help_text.caption).to eq("Updated caption")
|
||||
expect(existing_help_text.help_text).to eq("Updated help text with **bold** text")
|
||||
end
|
||||
|
||||
it "shows validation errors when clearing help text" do
|
||||
visit attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
|
||||
editor.clear
|
||||
editor.set_markdown(" ")
|
||||
click_button "Save"
|
||||
expect(page).to have_text("Help text can't be blank")
|
||||
end
|
||||
|
||||
it "persists caption as optional field" do
|
||||
visit attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
|
||||
# Clear caption but keep help text
|
||||
fill_in "Caption", with: ""
|
||||
editor.clear
|
||||
editor.set_markdown("Help text without caption")
|
||||
|
||||
# Save
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_text("Successful update")
|
||||
|
||||
# Verify caption is nil but help text is saved
|
||||
existing_help_text.reload
|
||||
expect(existing_help_text.caption).to be_blank
|
||||
expect(existing_help_text.help_text).to eq("Help text without caption")
|
||||
end
|
||||
end
|
||||
|
||||
describe "navigation between tabs" do
|
||||
it "maintains tab context when navigating" do
|
||||
visit edit_admin_settings_project_custom_field_path(project_custom_field)
|
||||
|
||||
# Navigate to attribute help text tab
|
||||
click_on AttributeHelpText.human_plural_model_name
|
||||
|
||||
expect(page).to have_current_path(
|
||||
attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
)
|
||||
|
||||
# Navigate back to details tab
|
||||
click_on "Details"
|
||||
|
||||
expect(page).to have_current_path(
|
||||
edit_admin_settings_project_custom_field_path(project_custom_field)
|
||||
)
|
||||
|
||||
# Navigate back to attribute help text tab
|
||||
click_on AttributeHelpText.human_plural_model_name
|
||||
|
||||
expect(page).to have_current_path(
|
||||
attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "help text display uniqueness" do
|
||||
it "creates separate help texts for different custom fields" do
|
||||
other_custom_field = create(:project_custom_field,
|
||||
name: "Project Priority",
|
||||
field_format: "text",
|
||||
project_custom_field_section:)
|
||||
|
||||
# Create help text for first custom field
|
||||
visit attribute_help_text_admin_settings_project_custom_field_path(project_custom_field)
|
||||
fill_in "Caption", with: "Rating help"
|
||||
editor.set_markdown("Help for rating")
|
||||
click_button "Save"
|
||||
|
||||
# Create help text for second custom field
|
||||
visit attribute_help_text_admin_settings_project_custom_field_path(other_custom_field)
|
||||
fill_in "Caption", with: "Priority help"
|
||||
editor.set_markdown("Help for priority")
|
||||
click_button "Save"
|
||||
|
||||
# Verify both help texts exist and are different
|
||||
rating_help = AttributeHelpText::Project.find_by(
|
||||
attribute_name: "custom_field_#{project_custom_field.id}"
|
||||
)
|
||||
priority_help = AttributeHelpText::Project.find_by(
|
||||
attribute_name: "custom_field_#{other_custom_field.id}"
|
||||
)
|
||||
|
||||
expect(rating_help).to be_present
|
||||
expect(priority_help).to be_present
|
||||
expect(rating_help.id).not_to eq(priority_help.id)
|
||||
expect(rating_help.caption).to eq("Rating help")
|
||||
expect(priority_help.caption).to eq("Priority help")
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user