diff --git a/app/views/custom_styles/_pdf_export_font.html.erb b/app/components/custom_styles/color_list_item_component.html.erb
similarity index 64%
rename from app/views/custom_styles/_pdf_export_font.html.erb
rename to app/components/custom_styles/color_list_item_component.html.erb
index f66f397b558c..0b239556dbf8 100644
--- a/app/views/custom_styles/_pdf_export_font.html.erb
+++ b/app/components/custom_styles/color_list_item_component.html.erb
@@ -26,19 +26,19 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-
<%= I18n.t(:label_custom_export_font_instructions) %>
-
-<%=
- render(Primer::Beta::Button.new(
- target: "_blank",
- data: { turbo: false },
- tag: :a,
- href: export_demo_pdf_download_custom_style_path
- )) do |_c|
- t("export.demo.button_text")
- end
-%>
-
-<% export_fonts_fields(@custom_style).each do |font| %>
- <%= render partial: "custom_styles/uploads/font", locals: { font: } %>
+<%= flex_layout(align_items: :center, flex_wrap: :wrap, justify_content: :space_between) do |row| %>
+ <% row.with_column(flex: 1, mr: 3) do %>
+ <%= render(Primer::Beta::Text.new(font_weight: :bold)) { color_label } %>
+ <% if instruction.present? %>
+ <%= render(Primer::Beta::Text.new(font_size: :small, color: :subtle, ml: 1)) { instruction } %>
+ <% end %>
+ <% end %>
+
+ <% row.with_column do %>
+ <%= render(Colors::SwatchComponent.new(hexcode:, show_value:)) %>
+ <% end %>
+
+ <% row.with_column(ml: 2) do %>
+ <%= render(CustomStyles::DesignColorDialogComponent.new(**dialog_arguments)) %>
+ <% end %>
<% end %>
diff --git a/app/components/custom_styles/color_list_item_component.rb b/app/components/custom_styles/color_list_item_component.rb
new file mode 100644
index 000000000000..a4fe036ff268
--- /dev/null
+++ b/app/components/custom_styles/color_list_item_component.rb
@@ -0,0 +1,48 @@
+# 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, see .
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module CustomStyles
+ # A BorderBoxList row showing a color's label/instruction, its current
+ # swatch, and an edit dialog (see DesignColorDialogComponent).
+ class ColorListItemComponent < ApplicationComponent
+ include OpPrimer::ComponentHelpers
+
+ attr_reader :color_label, :instruction, :hexcode, :show_value, :dialog_arguments
+
+ def initialize(label:, instruction:, hexcode:, dialog_arguments:, show_value: hexcode.present?)
+ super()
+
+ @color_label = label
+ @instruction = instruction
+ @hexcode = hexcode
+ @show_value = show_value
+ @dialog_arguments = dialog_arguments
+ end
+ end
+end
diff --git a/app/components/custom_styles/design_color_dialog_component.html.erb b/app/components/custom_styles/design_color_dialog_component.html.erb
index dac73d62e641..331f723caddc 100644
--- a/app/components/custom_styles/design_color_dialog_component.html.erb
+++ b/app/components/custom_styles/design_color_dialog_component.html.erb
@@ -41,7 +41,7 @@ See COPYRIGHT and LICENSE files for more details.
dialog.with_body do
primer_form_with(**form_arguments) do |form_builder|
render_inline_form(form_builder) do |form|
- form.text_field(**color_field_arguments)
+ form.color_field(**color_field_arguments)
end
end
end
diff --git a/app/components/custom_styles/design_color_dialog_component.rb b/app/components/custom_styles/design_color_dialog_component.rb
index ddb02ee0de24..584ae4ff2556 100644
--- a/app/components/custom_styles/design_color_dialog_component.rb
+++ b/app/components/custom_styles/design_color_dialog_component.rb
@@ -31,29 +31,35 @@ module CustomStyles
class DesignColorDialogComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
- attr_reader :design_color, :color_label, :instruction, :hexcode
+ attr_reader :variable, :color_label, :instruction, :value, :placeholder,
+ :form_url, :form_method, :param_name, :scope_name_to_model, :field_data
- def initialize(design_color:, label:, instruction:, hexcode:)
+ def initialize(variable:, label:, instruction:, value:, placeholder:, form_url:, param_name:,
+ form_method: :post, scope_name_to_model: false, field_data: {})
super()
- @design_color = design_color
+ @variable = variable
@color_label = label
@instruction = instruction
- @hexcode = hexcode
+ @value = value
+ @placeholder = placeholder
+ @form_url = form_url
+ @form_method = form_method
+ @param_name = param_name
+ @scope_name_to_model = scope_name_to_model
+ @field_data = field_data
end
private
- def variable = design_color.variable
-
def dialog_id = "design-color-#{variable}-dialog"
def form_id = "design-color-#{variable}-form"
def form_arguments
{
- url: url_helpers.update_design_colors_path,
- method: :post,
+ url: form_url,
+ method: form_method,
id: form_id,
data: { turbo: false }
}
@@ -61,16 +67,16 @@ def form_arguments
def field_arguments
{
- name: "design_colors[]#{variable}",
+ name: param_name,
id: "design_colors_#{variable}",
label: color_label,
visually_hide_label: true,
caption: instruction,
- value: design_color.hexcode,
- placeholder: hexcode,
+ value:,
+ placeholder:,
input_width: :medium,
- scope_name_to_model: false,
- data: { variable_name: variable }
+ scope_name_to_model:,
+ data: field_data
}
end
end
diff --git a/app/forms/_index.sass b/app/forms/_index.sass
index 1f0f29ffeefd..870d9a00dbda 100644
--- a/app/forms/_index.sass
+++ b/app/forms/_index.sass
@@ -1,2 +1 @@
-@import "admin/settings/color_form"
@import "projects/life_cycle/form"
diff --git a/app/forms/admin/settings/color_form.rb b/app/forms/admin/settings/color_form.rb
index 54b9ef0139b3..032026c5b167 100644
--- a/app/forms/admin/settings/color_form.rb
+++ b/app/forms/admin/settings/color_form.rb
@@ -38,21 +38,13 @@ class ColorForm < ApplicationForm
input_width: :medium
)
- color_form.group(layout: :horizontal, classes: "color-form--hexcode-row") do |hexcode_group|
- hexcode_group.text_field(
- name: :hexcode,
- label: attribute_name(:hexcode),
- required: true,
- autocomplete: "off",
- input_width: :small
- )
-
- if show_swatch?
- hexcode_group.html_content do
- render(Colors::SwatchComponent.new(hexcode: model.hexcode, classes: "color-form--hexcode-row-swatch"))
- end
- end
- end
+ color_form.color_field(
+ name: :hexcode,
+ label: attribute_name(:hexcode),
+ required: true,
+ autocomplete: "off",
+ input_width: :small
+ )
color_form.group(layout: :horizontal) do |button_group|
button_group.button(
@@ -76,10 +68,6 @@ class ColorForm < ApplicationForm
def cancel_path
url_helpers.custom_style_path(tab: :default_colors)
end
-
- def show_swatch?
- model.hexcode.present? && model.errors.messages_for(:hexcode).empty?
- end
end
end
end
diff --git a/app/forms/admin/settings/color_form.sass b/app/forms/admin/settings/color_form.sass
deleted file mode 100644
index df9ba55c0bb5..000000000000
--- a/app/forms/admin/settings/color_form.sass
+++ /dev/null
@@ -1,8 +0,0 @@
-.color-form--hexcode-row
- align-items: flex-end
- width: fit-content
-
- // Needed for proper alignment of the swatch next to the input. Due to the label
- // being part of the input, we need to do that manually.
- &-swatch
- margin-bottom: 10px
diff --git a/app/helpers/custom_styles_helper.rb b/app/helpers/custom_styles_helper.rb
index 46bb574bd009..a740d05b8315 100644
--- a/app/helpers/custom_styles_helper.rb
+++ b/app/helpers/custom_styles_helper.rb
@@ -93,13 +93,6 @@ def design_tabs
path: custom_style_path(tab: :pdf_export_styles),
label: t(:"admin.custom_styles.tab_pdf_export_styles"),
pdf: true
- },
- {
- name: "pdf_export_font",
- partial: "custom_styles/pdf_export_font",
- path: custom_style_path(tab: :pdf_export_font),
- label: t(:"admin.custom_styles.tab_pdf_export_font"),
- pdf: true
}
]
end
@@ -201,11 +194,12 @@ def export_fonts_fields(custom_style)
%i[regular bold italic bold_italic].map do |variant|
field = :"export_font_#{variant}"
font = custom_style.public_send(field)
+ present = custom_style.id && font.present?
{
field: field,
label: I18n.t("label_custom_export_font_#{variant}"),
- present: font.present?,
- filename: custom_style.id && font.present? ? File.basename(font.file.path) : nil,
+ present:,
+ filename: present ? File.basename(font.file.path) : nil,
delete_path: public_send(:"custom_style_export_font_#{variant}_delete_path"),
instructions: I18n.t("text_custom_export_font_#{variant}_instructions")
}
diff --git a/app/views/custom_styles/_interface.html.erb b/app/views/custom_styles/_interface.html.erb
index e459c941fb44..f1a4767bb6cf 100644
--- a/app/views/custom_styles/_interface.html.erb
+++ b/app/views/custom_styles/_interface.html.erb
@@ -48,33 +48,24 @@ See COPYRIGHT and LICENSE files for more details.
<% hexcode = effective_design_color(design_color, current_theme: @current_theme) %>
<% list.with_item do %>
- <%= flex_layout(
- align_items: :center,
- flex_wrap: :wrap,
- justify_content: :space_between
- ) do |row| %>
- <% row.with_column(flex: 1, mr: 3) do %>
- <%= render(Primer::Beta::Text.new(font_weight: :bold)) { label } %>
- <% if instruction.present? %>
- <%= render(Primer::Beta::Text.new(font_size: :small, color: :subtle, ml: 1)) { instruction } %>
- <% end %>
- <% end %>
-
- <% row.with_column do %>
- <%= render(Colors::SwatchComponent.new(hexcode:, show_value: true)) %>
- <% end %>
-
- <% row.with_column(ml: 2) do %>
- <%= render(
- CustomStyles::DesignColorDialogComponent.new(
- design_color:,
- label:,
- instruction:,
- hexcode:
- )
- ) %>
- <% end %>
- <% end %>
+ <%= render(
+ CustomStyles::ColorListItemComponent.new(
+ label:,
+ instruction:,
+ hexcode:,
+ show_value: true,
+ dialog_arguments: {
+ variable:,
+ label:,
+ instruction:,
+ value: design_color.hexcode,
+ placeholder: hexcode,
+ form_url: update_design_colors_path,
+ param_name: "design_colors[]#{variable}",
+ field_data: { variable_name: variable }
+ }
+ )
+ ) %>
<% end %>
<% end %>
<% end %>
diff --git a/app/views/custom_styles/_pdf_export_styles.html.erb b/app/views/custom_styles/_pdf_export_styles.html.erb
index 70278415c970..425909fa6676 100644
--- a/app/views/custom_styles/_pdf_export_styles.html.erb
+++ b/app/views/custom_styles/_pdf_export_styles.html.erb
@@ -26,7 +26,102 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-<%= I18n.t("label_custom_export_images_instructions") %>
+<%= render(Primer::Beta::Subhead.new(spacious: false)) do |subhead| %>
+ <% subhead.with_heading(tag: :h3, size: :medium) { t("admin.custom_styles.export.title") } %>
+ <% subhead.with_description { t("admin.custom_styles.export.description") } %>
+<% end %>
+
+<%= flex_layout(flex_wrap: :wrap, classes: "gap-3") do |flex| %>
+ <% flex.with_column(flex: 1, classes: "custom-style--growing-logo-form") do %>
+ <%= render partial: "custom_styles/uploads/image", locals: { image: {
+ field: :export_logo,
+ label: I18n.t(:label_custom_export_logo),
+ present: @custom_style.id && @custom_style.export_logo.present?,
+ source: if @custom_style.id && @custom_style.export_logo.present?
+ custom_style_export_logo_path(digest: @custom_style.digest, filename: @custom_style.export_logo_identifier)
+ end,
+ img_class: "custom-export-logo-preview",
+ accept: "image/png, image/jpeg",
+ delete_path: custom_style_export_logo_delete_path,
+ instructions: I18n.t("text_custom_export_logo_instructions")
+ } } %>
+ <% end %>
+
+ <% flex.with_column(flex: 1, classes: "custom-style--growing-logo-form") do %>
+ <%= render partial: "custom_styles/uploads/image", locals: { image: {
+ field: :export_cover,
+ label: I18n.t(:label_custom_export_cover),
+ present: @custom_style.id && @custom_style.export_cover.present?,
+ source: if @custom_style.id && @custom_style.export_cover.present?
+ custom_style_export_cover_path(digest: @custom_style.digest, filename: @custom_style.export_cover_identifier)
+ end,
+ img_class: "custom-export-cover-preview",
+ accept: "image/png, image/jpeg",
+ delete_path: custom_style_export_cover_delete_path,
+ instructions: I18n.t("text_custom_export_cover_instructions")
+ } } %>
+ <% end %>
+
+ <% if OpenProject::FeatureDecisions.minutes_styling_meeting_pdf_active? %>
+ <% flex.with_column(flex: 1, classes: "custom-style--growing-logo-form") do %>
+ <%= render partial: "custom_styles/uploads/image", locals: { image: {
+ field: :export_footer,
+ label: I18n.t(:label_custom_export_footer),
+ present: @custom_style.id && @custom_style.export_footer.present?,
+ source: if @custom_style.id && @custom_style.export_footer.present?
+ custom_style_export_footer_path(digest: @custom_style.digest, filename: @custom_style.export_footer_identifier)
+ end,
+ img_class: "custom-export-footer-preview",
+ accept: "image/png, image/jpeg",
+ delete_path: custom_style_export_footer_delete_path,
+ instructions: I18n.t("text_custom_export_footer_instructions")
+ } } %>
+ <% end %>
+ <% end %>
+<% end %>
+
+<%= render(Primer::Beta::Subhead.new(spacious: false)) do |subhead| %>
+ <% subhead.with_heading(tag: :h3, size: :medium, mt: 4) { t("label_custom_export_font") } %>
+<% end %>
+
+<%= flex_layout(flex_wrap: :wrap, classes: "gap-3", mb: 3) do |flex| %>
+ <% export_fonts_fields(@custom_style).each do |font| %>
+ <% flex.with_column(flex: 1, classes: "custom-style--growing-logo-form") do %>
+ <%= render partial: "custom_styles/uploads/font", locals: { font: } %>
+ <% end %>
+ <% end %>
+<% end %>
+
+<%
+ design_color_hex = @custom_style.export_cover_text_color.presence
+ instruction_key = "admin.custom_styles.instructions.export_cover_text_color"
+ text_color_label = I18n.t(:label_custom_export_cover_text_color)
+ text_color_instruction = I18n.t(instruction_key) if I18n.exists?(instruction_key, :en)
+%>
+
+<%= render(OpenProject::Common::BorderBoxListComponent.new(container: "export-cover-text-color", mb: 3)) do |list| %>
+ <% list.with_header(title: I18n.t(:label_custom_export_font_colors)) %>
+
+ <% list.with_item do %>
+ <%= render(
+ CustomStyles::ColorListItemComponent.new(
+ label: text_color_label,
+ instruction: text_color_instruction,
+ hexcode: design_color_hex || "#000000",
+ show_value: design_color_hex.present?,
+ dialog_arguments: {
+ variable: "export_cover_text_color",
+ label: text_color_label,
+ instruction: text_color_instruction,
+ value: design_color_hex,
+ placeholder: "#000",
+ form_url: update_custom_style_export_cover_text_color_path,
+ param_name: :export_cover_text_color
+ }
+ )
+ ) %>
+ <% end %>
+<% end %>
<%=
render(
@@ -34,96 +129,10 @@ See COPYRIGHT and LICENSE files for more details.
target: "_blank",
data: { turbo: false },
tag: :a,
+ mt: 3,
href: export_demo_pdf_download_custom_style_path
)
) do |_c|
t("export.demo.button_text")
end
%>
-
-<%= render partial: "custom_styles/uploads/image", locals: { image: {
- field: :export_logo,
- label: I18n.t(:label_custom_export_logo),
- present: @custom_style.id && @custom_style.export_logo.present?,
- source: if @custom_style.id && @custom_style.export_logo.present?
- custom_style_export_logo_path(digest: @custom_style.digest, filename: @custom_style.export_logo_identifier)
- end,
- img_class: "custom-export-logo-preview",
- accept: "image/png, image/jpeg",
- delete_path: custom_style_export_logo_delete_path,
- instructions: I18n.t("text_custom_export_logo_instructions")
- } } %>
-
-<%= render partial: "custom_styles/uploads/image", locals: { image: {
- field: :export_cover,
- label: I18n.t(:label_custom_export_cover),
- present: @custom_style.id && @custom_style.export_cover.present?,
- source: if @custom_style.id && @custom_style.export_cover.present?
- custom_style_export_cover_path(digest: @custom_style.digest, filename: @custom_style.export_cover_identifier)
- end,
- img_class: "custom-export-cover-preview",
- accept: "image/png, image/jpeg",
- delete_path: custom_style_export_cover_delete_path,
- instructions: I18n.t("text_custom_export_cover_instructions")
- } } %>
-
-<%= form_tag update_custom_style_export_cover_text_color_path, html: { multipart: true, class: "form -vertical" } do %>
-
-<% end %>
-
-<% if OpenProject::FeatureDecisions.minutes_styling_meeting_pdf_active? %>
- <%= render partial: "custom_styles/uploads/image", locals: { image: {
- field: :export_footer,
- label: I18n.t(:label_custom_export_footer),
- present: @custom_style.id && @custom_style.export_footer.present?,
- source: if @custom_style.id && @custom_style.export_footer.present?
- custom_style_export_footer_path(digest: @custom_style.digest, filename: @custom_style.export_footer_identifier)
- end,
- img_class: "custom-export-footer-preview",
- accept: "image/png, image/jpeg",
- delete_path: custom_style_export_footer_delete_path,
- instructions: I18n.t("text_custom_export_footer_instructions")
- } } %>
-<% end %>
diff --git a/app/views/custom_styles/uploads/_font.html.erb b/app/views/custom_styles/uploads/_font.html.erb
index 7a28f70897f0..1fc8c8e1f9d1 100644
--- a/app/views/custom_styles/uploads/_font.html.erb
+++ b/app/views/custom_styles/uploads/_font.html.erb
@@ -26,43 +26,28 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-<%= form_for @custom_style, url: custom_style_path, html: { multipart: true, class: "form -vertical" } do |f| %>
-
-<% end %>
+ }
+ )
+ end
+ end
+%>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 27f4b543a22b..0fbb57997d02 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1532,11 +1532,15 @@ en:
default_colors:
description: "Define a palette of colors to use elsewhere in the application. These will be available, for example, when choosing a color to represent a status or a type."
edit_color: "Edit %{color}"
+ export:
+ description: "Upload and manage custom image files used in your PDF exports."
+ title: "Custom export"
fonts:
file_is_invalid: "is not a valid TTF font file."
file_too_large: "is too large (maximum size is %{count} MB)."
instructions:
accent-color: "Color for links and other decently highlighted elements."
+ export_cover_text_color: "Color of the text on top of the cover background to ensure readability."
header-bg-color: "Default background color of the app header."
main-menu-bg-color: "Left side menu's background color."
main-menu-bg-selected-background: "Background color of menu items when selected."
@@ -1545,7 +1549,6 @@ en:
tab_branding: "Branding"
tab_default_colors: "Default colors"
tab_interface: "Interface"
- tab_pdf_export_font: "PDF export font"
tab_pdf_export_styles: "PDF export styles"
theme_warning:
confirmation: "I understand that this action cannot be reversed"
@@ -4041,20 +4044,16 @@ en:
label_current_status: "Current status"
label_current_version: "Current version"
label_custom_comment: "%{name} comment"
- label_custom_export_cover: "Custom export cover background"
- label_custom_export_cover_overlay: "Custom export cover background overlay"
- label_custom_export_cover_text_color: "Text color"
+ label_custom_export_cover: "Cover background"
+ label_custom_export_cover_text_color: "Text color for cover background"
+ label_custom_export_font: "Custom Font"
label_custom_export_font_bold: "Bold"
label_custom_export_font_bold_italic: "Bold Italic"
- label_custom_export_font_instructions: >
- Upload and manage custom TrueType (.ttf) fonts used in your PDF exports.
- For best results, use matching files from the same font family. If no font is provided, the default NotoSans font will be used.
+ label_custom_export_font_colors: "Custom font colors"
label_custom_export_font_italic: "Italic"
label_custom_export_font_regular: "Regular"
- label_custom_export_footer: "Custom export footer image"
- label_custom_export_images_instructions: >
- Upload and manage custom image files used in your PDF exports.
- label_custom_export_logo: "Custom export logo"
+ label_custom_export_footer: "Footer image"
+ label_custom_export_logo: "Logo"
label_custom_favicon: "Custom favicon"
label_custom_field_add_no_type: "Add this field to a work package type"
label_custom_field_default_type: "Empty type"
diff --git a/lib/primer/open_project/forms/dsl/file_field_input.rb b/lib/primer/open_project/forms/dsl/file_field_input.rb
index dfa3bf6b9d2e..9e9c20908f5f 100644
--- a/lib/primer/open_project/forms/dsl/file_field_input.rb
+++ b/lib/primer/open_project/forms/dsl/file_field_input.rb
@@ -34,10 +34,10 @@ module Dsl
# :nodoc:
class FileFieldInput < Primer::Forms::Dsl::Input
attr_reader :name, :label, :description, :icon, :accept, :preview_url, :preview_class, :preview_data,
- :delete_path, :delete_button_arguments, :auto_submit
+ :filename, :delete_path, :delete_button_arguments, :auto_submit
def initialize(name:, label:, description: nil, icon: :upload, accept: nil, preview_url: nil,
- preview_class: nil, preview_data: {}, delete_path: nil, delete_button_arguments: {},
+ preview_class: nil, preview_data: {}, filename: nil, delete_path: nil, delete_button_arguments: {},
auto_submit: false, **system_arguments)
@name = name
@label = label
@@ -47,6 +47,7 @@ def initialize(name:, label:, description: nil, icon: :upload, accept: nil, prev
@preview_url = preview_url
@preview_class = preview_class
@preview_data = preview_data
+ @filename = filename
@delete_path = delete_path
@delete_button_arguments = delete_button_arguments
@auto_submit = auto_submit
diff --git a/lib/primer/open_project/forms/file_field.html.erb b/lib/primer/open_project/forms/file_field.html.erb
index 97dff62db028..32351de7b319 100644
--- a/lib/primer/open_project/forms/file_field.html.erb
+++ b/lib/primer/open_project/forms/file_field.html.erb
@@ -62,14 +62,14 @@ See COPYRIGHT and LICENSE files for more details.
<%= render(Primer::Beta::Octicon.new(icon:, classes: "FormControl-file-field-icon")) %>
<%= label %>
+ data-empty="<%= label %>"><%= displayed_label %>
<% if description.present? %>
<%= description %>
<% end %>
<% end %>
- <% if preview_url.present? && !@input.disabled? %>
+ <% if has_attachment? && !@input.disabled? %>