[68323] Custom logo for mobile (#21059)

* add a migration to upload custom mobile logo

* add a new route for uploading nd removing mobile logo

* show custom logo in header

* Add a feature spec

* Update custom_style.rb

* Show mobile icon for desktop when there is no desktop logo

* show icon logo in waffle menu modal

* Show logo icon when a custom mobile logo exists or when no custom desktop logo is uploaded
This commit is contained in:
Behrokh Satarnejad
2025-11-21 11:25:57 +01:00
committed by GitHub
parent 2fd7fdf9bf
commit 701103240a
13 changed files with 186 additions and 19 deletions
@@ -36,6 +36,7 @@ class CustomStylesController < ApplicationController
menu_item :custom_style
UNGUARDED_ACTIONS = %i[logo_download
logo_mobile_download
favicon_download
touch_icon_download].freeze
@@ -102,6 +103,10 @@ class CustomStylesController < ApplicationController
file_download(:logo_path)
end
def logo_mobile_download
file_download(:logo_mobile_path)
end
def export_logo_download
file_download(:export_logo_path)
end
@@ -126,6 +131,10 @@ class CustomStylesController < ApplicationController
file_delete(:remove_logo)
end
def logo_mobile_delete
file_delete(:remove_logo_mobile)
end
def export_logo_delete
file_delete(:remove_export_logo)
end
@@ -224,6 +233,7 @@ class CustomStylesController < ApplicationController
def custom_style_params
params.expect(custom_style: %i[
logo remove_logo
logo_mobile remove_logo_mobile
export_logo remove_export_logo
export_cover remove_export_cover
export_footer remove_export_footer
+28
View File
@@ -78,6 +78,34 @@ module CustomStylesHelper
(CustomStyle.current.logo.present? || CustomStyle.current.theme_logo.present?)
end
def desktop_logo_present?
style = CustomStyle.current
return false unless style
style.logo.present? || style.theme_logo.present?
end
def mobile_logo_present?
style = CustomStyle.current
return false unless style
style.logo_mobile.present?
end
def show_waffle_icon?
# Both logos → show icon (mobile logo will be applied by CSS)
return true if desktop_logo_present? && mobile_logo_present?
# Only mobile → show icon
return true if mobile_logo_present?
# Only desktop → hide icon on mobile
return false if desktop_logo_present?
# No logos → show fallback icon
true
end
# The default favicon and touch icons are both the same for normal OP and BIM.
def apply_custom_favicon?
apply_custom_styles?(skip_ee_check: false) && CustomStyle.current.favicon.present?
+2 -1
View File
@@ -32,6 +32,7 @@ require "ttfunk"
class CustomStyle < ApplicationRecord
mount_uploader :logo, OpenProject::Configuration.file_uploader
mount_uploader :logo_mobile, OpenProject::Configuration.file_uploader
mount_uploader :export_logo, OpenProject::Configuration.file_uploader
mount_uploader :export_cover, OpenProject::Configuration.file_uploader
mount_uploader :export_footer, OpenProject::Configuration.file_uploader
@@ -59,7 +60,7 @@ class CustomStyle < ApplicationRecord
updated_at.to_i
end
%i(favicon touch_icon export_logo export_cover export_footer logo
%i(favicon touch_icon export_logo export_cover export_footer logo logo_mobile
export_font_regular export_font_bold export_font_italic export_font_bold_italic).each do |name|
define_method :"#{name}_path" do
attachment = send(name)
@@ -40,6 +40,18 @@ See COPYRIGHT and LICENSE files for more details.
instructions: I18n.t("text_custom_logo_instructions")
} } %>
<%= render partial: "custom_styles/uploads/image", locals: { image: {
field: :logo_mobile,
label: I18n.t(:label_custom_logo_mobile),
present: @custom_style.id && @custom_style.logo_mobile.present?,
source: @custom_style.id && @custom_style.logo_mobile.present? ?
custom_style_logo_mobile_path(digest: @custom_style.digest, filename: @custom_style.logo_mobile_identifier) :
nil,
img_class: "custom-logo-mobile-preview",
accept: "image/*",
delete_path: custom_style_logo_mobile_delete_path
} } %>
<%= render partial: "custom_styles/uploads/image", locals: { image:{
field: :favicon,
label: I18n.t(:label_custom_favicon),
+27 -13
View File
@@ -29,36 +29,50 @@ See COPYRIGHT and LICENSE files for more details.
<style type="text/css">
<%
isRu = I18n.locale == :ru
is_ru = I18n.locale == :ru
cs = apply_custom_styles? ? CustomStyle.current : nil
logo_url = if isRu
logo_url = if is_ru
asset_path("logo-white-bg-ua.png")
else
asset_path("logo_openproject_white_big.png")
end
logo_icon_url = asset_path("icon_logo.svg")
logo_icon_white_url = asset_path("icon_logo_white.svg")
high_contrast_logo_url = if isRu
high_contrast_logo_url = if is_ru
asset_path("logo-black-bg-ua.png")
else
asset_path("logo_openproject.png")
end
logo_icon_url = asset_path("icon_logo.svg")
logo_icon_white_url = asset_path("icon_logo_white.svg")
high_contrast_bim_logo_url = asset_path("bim/logo_openproject_bim_big_coloured.png")
if apply_custom_styles?
if CustomStyle.current.logo.present?
logo_url = custom_style_logo_path(digest: CustomStyle.current.digest, filename: CustomStyle.current.logo_identifier)
elsif CustomStyle.current.theme_logo.present?
logo_url = asset_path(CustomStyle.current.theme_logo)
if cs
has_logo = cs.logo.present?
has_logo_mobile = cs.logo_mobile.present?
if has_logo
logo_url = high_contrast_logo_url = custom_style_logo_path(digest: cs.digest, filename: cs.logo_identifier)
elsif has_logo_mobile
logo_url = high_contrast_logo_url = custom_style_logo_mobile_path(digest: cs.digest, filename: cs.logo_mobile_identifier)
elsif cs.theme_logo.present?
logo_url = asset_path(cs.theme_logo)
end
if isRu && logo_url == asset_path("logo_openproject.png")
if is_ru && logo_url == asset_path("logo_openproject.png")
logo_url = asset_path("logo-black-bg-ua.png")
end
if has_logo_mobile
mobile_logo_url = custom_style_logo_mobile_path(
digest: cs.digest,
filename: cs.logo_mobile_identifier
)
logo_icon_url = mobile_logo_url
logo_icon_white_url = mobile_logo_url
end
end
%>
+2 -1
View File
@@ -3323,7 +3323,8 @@ en:
label_lock_user: "Lock user"
label_logged_as: "Logged in as"
label_login: "Sign in"
label_custom_logo: "Custom logo"
label_custom_logo: "Custom logo desktop"
label_custom_logo_mobile: "Custom logo mobile"
label_custom_export_logo: "Custom export logo"
label_custom_export_cover: "Custom export cover background"
label_custom_export_footer: "Custom export footer image"
+5
View File
@@ -176,6 +176,10 @@ Rails.application.routes.draw do
as: "custom_style_logo",
constraints: { filename: /[^\/]*/ }
get "custom_style/:digest/logo_mobile/:filename" => "custom_styles#logo_mobile_download",
as: "custom_style_logo_mobile",
constraints: { filename: /[^\/]*/ }
get "custom_style/:digest/export_logo/:filename" => "custom_styles#export_logo_download",
as: "custom_style_export_logo",
constraints: { filename: /[^\/]*/ }
@@ -553,6 +557,7 @@ Rails.application.routes.draw do
end
delete "design/logo" => "custom_styles#logo_delete", as: "custom_style_logo_delete"
delete "design/logo_mobile" => "custom_styles#logo_mobile_delete", as: "custom_style_logo_mobile_delete"
delete "design/export_logo" => "custom_styles#export_logo_delete", as: "custom_style_export_logo_delete"
delete "design/export_cover" => "custom_styles#export_cover_delete", as: "custom_style_export_cover_delete"
delete "design/export_footer" => "custom_styles#export_footer_delete", as: "custom_style_export_footer_delete"
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddLogoMobileToCustomStyles < ActiveRecord::Migration[8.0]
def change
add_column :custom_styles, :logo_mobile, :string
end
end
@@ -1,4 +1,5 @@
img.custom-logo-preview,
img.custom-logo-mobile-preview,
img.custom-favicon-preview,
img.custom-touch-icon-preview
box-shadow: 0 0 3px lightgrey
@@ -10,6 +11,7 @@ img.custom-export-cover-preview
background-color: #ffffff // Do not theme (PDF is white, so is the preview)
img.custom-logo-preview,
img.custom-logo-mobile-preview,
img.custom-export-logo-preview
height: 42px
@@ -43,7 +43,7 @@ module Redmine::MenuManager::TopMenu::ModuleMenu
"aria-controls": "op-app-header--modules-menu-list",
"aria-label": I18n.t("label_global_modules"))
dialog.with_header(classes: "op-app-header--modules-menu-header") do
render_waffle_menu_logo_icon unless custom_logo?
render_waffle_menu_logo_icon if show_waffle_icon?
end
item_groups.each do |item_group|
+15 -3
View File
@@ -46,7 +46,12 @@ module Redmine::MenuManager::TopMenuHelper
render_module_top_menu_node,
render_logo
]
items << render_logo_icon unless custom_logo?
cs = CustomStyle.current
if cs&.logo_mobile.present? || !custom_logo?
items << render_logo_icon
end
items
end
@@ -73,8 +78,15 @@ module Redmine::MenuManager::TopMenuHelper
end
def render_waffle_menu_logo_icon
mode_class = User.current.pref.theme === "dark" ? "op-logo--icon_white" : "op-logo--icon"
render Primer::BaseComponent.new(tag: :div, classes: ["op-logo", mode_class])
style = CustomStyle.current
classes = ["op-logo"]
if style&.logo_mobile.present?
classes << "op-logo--icon"
else
mode_class = User.current.pref.theme == "dark" ? "op-logo--icon_white" : "op-logo--icon"
classes << mode_class
end
render Primer::BaseComponent.new(tag: :div, classes:)
end
def render_top_menu_search
@@ -233,6 +233,73 @@ RSpec.describe CustomStylesController do
end
end
describe "#logo_mobile_download" do
before do
allow(CustomStyle).to receive(:current).and_return(custom_style)
allow(controller).to receive(:send_file) { controller.head 200 }
get :logo_mobile_download, params: {
digest: "1234",
filename: "logo_mobile_image.png"
}
end
context "when mobile logo is present" do
let(:custom_style) { build(:custom_style_with_logo_mobile) }
it "sends a file" do
expect(response).to have_http_status(:ok)
end
end
context "when no custom style is present" do
let(:custom_style) { nil }
it "renders with error" do
expect(controller).not_to have_received(:send_file)
expect(response).to have_http_status(:not_found)
end
end
context "when no mobile logo is present" do
let(:custom_style) { build_stubbed(:custom_style) }
it "renders with error" do
expect(controller).not_to have_received(:send_file)
expect(response).to have_http_status(:not_found)
end
end
end
describe "#logo_mobile_delete", with_ee: %i[define_custom_style] do
let(:custom_style) { create(:custom_style_with_logo_mobile) }
context "if it exists" do
before do
allow(CustomStyle).to receive(:current).and_return(custom_style)
allow(custom_style).to receive(:remove_logo_mobile).and_call_original
delete :logo_mobile_delete
end
it "removes the mobile logo from custom_style" do
expect(response).to redirect_to(action: :show)
expect(response).to have_http_status(:see_other)
end
end
context "if it does not exist" do
before do
allow(CustomStyle).to receive(:current).and_return(nil)
delete :logo_mobile_delete
end
it "renders 404" do
expect(response).to have_http_status :not_found
end
end
end
describe "#export_logo_download", with_ee: %i[define_custom_style] do
before do
allow(CustomStyle).to receive(:current).and_return(custom_style)
+8
View File
@@ -110,4 +110,12 @@ FactoryBot.define do
)
end
end
factory :custom_style_with_logo_mobile, class: "CustomStyle" do
logo_mobile do
Rack::Test::UploadedFile.new(
Rails.root.join("spec/support/custom_styles/logos/logo_image.png")
)
end
end
end