mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[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:
committed by
GitHub
parent
2fd7fdf9bf
commit
701103240a
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
%>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user