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 %> -
-
- <%= I18n.t(:label_custom_export_cover_overlay) %> -
- - -
- <% design_color_name = "export_cover_text_color" %> - <% design_color_hex = @custom_style.export_cover_text_color.presence %> - <%= icon_for_color( - Color.new( - hexcode: design_color_hex || "#000" - ), - data: { target: "##{design_color_name}" } - ) %> -
- - <%= styled_text_field_tag design_color_name, - design_color_hex || "", - placeholder: "#000" %> - -
-
- <% instruction_key = "admin.custom_styles.instructions.#{design_color_name}" %> - <% if I18n.exists?(instruction_key, :en) %> - <%= I18n.t(instruction_key) %> - <% end %> -
-
- <%= - render( - Primer::Beta::Button.new( - type: :submit, - data: { - "test-selector": "text-color-change" - } - ) - ) do |_button| - I18n.t(:button_save) - end - %> -
-
-<% 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| %> -
-
- <%= font[:label] %> -

<%= font[:instructions] %>

- <% if font[:present] %> -

- <%= render(Primer::Beta::Octicon.new(icon: :file)) %> - <%= font[:filename] %>

- <% end %> -
-
-
-
- <%= f.file_field font[:field], required: true, class: "attachment_choose_file", accept: "font/ttf", size: "15" %> -
-
-
-
- <%= - render(Primer::Beta::Button.new( - type: :submit - )) do |button| - button.with_leading_visual_icon(icon: font[:present] ? :upload : :plus) - t(font[:present] ? :button_replace : :button_upload) - end - %> - <% if font[:present] %> - <%= render(Primer::Beta::Button.new( - tag: :a, - href: font[:delete_path], +<%= + primer_form_with( + model: @custom_style, + url: custom_style_path, + multipart: true, + class: "form -vertical", + data: { controller: "auto-submit" } + ) do |f| + render_inline_form(f) do |form| + form.file_field( + name: font[:field], + label: font[:label], + description: font[:instructions], + accept: "font/ttf", + filename: font[:filename], + required: true, + auto_submit: true, + delete_path: (font[:delete_path] if font[:present]), + delete_button_arguments: { data: { turbo_method: :delete } - )) do |button| - button.with_leading_visual_icon(icon: :trash) - t(:button_delete) - end %> - <% end %> -
-
-<% 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? %> <%= render(Primer::Alpha::ActionMenu.new) do |menu| %> <% menu.with_show_button(icon: "kebab-horizontal", "aria-label": menu_label, scheme: :invisible) %> diff --git a/lib/primer/open_project/forms/file_field.rb b/lib/primer/open_project/forms/file_field.rb index eb7c4c4906ef..d3605307ce19 100644 --- a/lib/primer/open_project/forms/file_field.rb +++ b/lib/primer/open_project/forms/file_field.rb @@ -35,7 +35,7 @@ class FileField < Primer::Forms::BaseComponent prepend WrappedInput delegate :builder, :form, :name, :label, :description, :icon, :accept, :preview_url, :preview_class, - :preview_data, :delete_path, to: :@input + :preview_data, :filename, :delete_path, to: :@input def initialize(input:) super() @@ -51,6 +51,16 @@ def preview_attributes }.compact end + # Files that cannot be previewed as an image (e.g. fonts) still need + # the replace/delete menu once one is already uploaded. + def has_attachment? + preview_url.present? || filename.present? + end + + def displayed_label + filename.presence || label + end + def field_wrap_arguments @field_wrap_arguments.merge( class: class_names( diff --git a/spec/features/custom_styles/tabs_navigation_spec.rb b/spec/features/custom_styles/tabs_navigation_spec.rb index 6ce5597a0c51..6052822c2de2 100644 --- a/spec/features/custom_styles/tabs_navigation_spec.rb +++ b/spec/features/custom_styles/tabs_navigation_spec.rb @@ -232,9 +232,11 @@ expect(page).to have_current_path custom_style_path(tab: "pdf_export_styles") # change export cover text color and redirect to the PDF export styles tab + find_test_selector("edit-design-color-export_cover_text_color").click fill_in "export_cover_text_color", with: "#333" - find_test_selector("text-color-change").click - expect(page).to have_css("#export_cover_text_color", value: "#333") + find_test_selector("save-design-color-export_cover_text_color").click + + expect(page).to have_css("[name='export_cover_text_color']", value: "#333", visible: :all) expect(page).to have_current_path custom_style_path(tab: "pdf_export_styles") end end diff --git a/spec/lib/primer/open_project/forms/file_field_spec.rb b/spec/lib/primer/open_project/forms/file_field_spec.rb index ff99d91d17cf..cadbe8ed3705 100644 --- a/spec/lib/primer/open_project/forms/file_field_spec.rb +++ b/spec/lib/primer/open_project/forms/file_field_spec.rb @@ -143,8 +143,24 @@ def render_file_field(**) end end + context "with a filename but no preview_url (e.g. a font, which cannot be previewed as an image)" do + it "shows the filename instead of the label, without marking the wrapper as having a preview" do + render_file_field(filename: "Regular.ttf") + + expect(page).to have_button("Regular.ttf", class: "FormControl-file-field-drop-zone") + expect(page).to have_css("[data-controller='file-field']:not(.FormControl-file-field-wrap--has-preview)") + end + + it "still resets to the label as the empty-state placeholder for a newly chosen file" do + render_file_field(filename: "Regular.ttf", label: "Upload file") + + label = page.find("[data-file-field-target='filename']", visible: :all) + expect(label[:"data-empty"]).to eq("Upload file") + end + end + describe "the actions menu" do - context "without a preview_url" do + context "without a preview_url or filename" do it "does not render the menu, since there is nothing uploaded yet to act on" do render_file_field(delete_path: "/uploads/logo") @@ -160,6 +176,14 @@ def render_file_field(**) end end + context "with only a filename (no preview_url)" do + it "renders the menu, since a file is already uploaded even though it cannot be previewed" do + render_file_field(filename: "Regular.ttf", delete_path: "/uploads/font") + + expect(page).to have_css(".FormControl-file-field-menu", visible: :all) + end + end + context "with a preview_url" do it "renders a Replace item that triggers the same action as clicking the drop zone" do render_file_field(preview_url: "/uploads/logo.png")