From c84dd850fa5af4865585efcecd0df4ff6e1075aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 9 Sep 2025 08:51:14 +0200 Subject: [PATCH] Refactor static links to ensure users have to go through url_for This ensures links will be translated if they are part of the website --- .../welcome_dialog_component.html.erb | 2 +- .../progress/base_modal_component.rb | 2 +- app/controllers/help_controller.rb | 4 +- app/forms/my/look_and_feel_form.rb | 3 +- app/helpers/static_links_helper.rb | 14 +- app/views/admin/info.html.erb | 2 +- .../admin/settings/api_settings/show.html.erb | 2 +- .../date_format_settings/show.html.erb | 2 +- .../settings/general_settings/show.html.erb | 2 +- .../settings/icalendar_settings/show.html.erb | 2 +- app/views/enterprise_tokens/_info.html.erb | 2 +- .../blocks/_administration.html.erb | 2 +- .../homescreen/blocks/_community.html.erb | 53 ++++- app/views/homescreen/index.html.erb | 17 +- app/views/ldap_auth_sources/_form.html.erb | 2 +- app/views/user_mailer/user_signed_up.html.erb | 6 +- app/views/user_mailer/user_signed_up.text.erb | 6 +- app/views/users/_preferences.html.erb | 2 +- config/initializers/homescreen.rb | 23 +-- config/static_links.yml | 2 + lib/open_project/journal_formatter/cause.rb | 4 +- lib/open_project/static/links.rb | 52 +++-- .../menu_manager/top_menu/help_menu.rb | 32 ++- .../forms/general_info_form_component.rb | 2 +- .../forms/oauth_client_form_component.rb | 4 +- .../open_project_storage_modal_component.rb | 2 +- .../admin/provider_drive_id_input_form.rb | 2 +- .../admin/provider_tenant_id_input_form.rb | 2 +- .../storages/admin/storages/new.html.erb | 2 +- .../_health_status_notification.html.erb | 2 +- .../settings.html.erb | 4 +- .../webhooks/outgoing/admin/_form.html.erb | 2 +- .../banner_component_spec.rb | 2 +- .../progress/shared_modal_examples.rb | 2 +- .../journal_formatter/cause_spec.rb | 6 +- spec/lib/open_project/static/links_spec.rb | 190 ++++++++++++++---- spec/support/support_links.rb | 4 +- 37 files changed, 323 insertions(+), 141 deletions(-) diff --git a/app/components/enterprise_trials/welcome_dialog_component.html.erb b/app/components/enterprise_trials/welcome_dialog_component.html.erb index 3c7029043b5..630c3ec6a6c 100644 --- a/app/components/enterprise_trials/welcome_dialog_component.html.erb +++ b/app/components/enterprise_trials/welcome_dialog_component.html.erb @@ -22,7 +22,7 @@ frameborder: "0", height: "400", width: "100%", - src: OpenProject::Static::Links.links[:enterprise_welcome_video][:href], + src: OpenProject::Static::Links.url_for(:enterprise_welcome_video), allowfullscreen: true) end end diff --git a/app/components/work_packages/progress/base_modal_component.rb b/app/components/work_packages/progress/base_modal_component.rb index 6ba3656a1e6..94103d635f1 100644 --- a/app/components/work_packages/progress/base_modal_component.rb +++ b/app/components/work_packages/progress/base_modal_component.rb @@ -79,7 +79,7 @@ module WorkPackages end def learn_more_href - OpenProject::Static::Links.links[:progress_tracking_docs][:href] + OpenProject::Static::Links.url_for(:progress_tracking_docs) end private diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 661ec8f20e5..17c4276d307 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -32,11 +32,11 @@ class HelpController < ApplicationController no_authorization_required! :keyboard_shortcuts, :text_formatting def keyboard_shortcuts - redirect_to OpenProject::Static::Links[:shortcuts][:href], allow_other_host: true + redirect_to OpenProject::Static::Links.url_for(:shortcuts), allow_other_host: true end def text_formatting - default_link = OpenProject::Static::Links[:text_formatting][:href] + default_link = OpenProject::Static::Links.url_for(:text_formatting) help_link = OpenProject::Configuration.force_formatting_help_link.presence || default_link redirect_to help_link, allow_other_host: true diff --git a/app/forms/my/look_and_feel_form.rb b/app/forms/my/look_and_feel_form.rb index df827a3c6c1..c0439de7e7c 100644 --- a/app/forms/my/look_and_feel_form.rb +++ b/app/forms/my/look_and_feel_form.rb @@ -30,6 +30,7 @@ class My::LookAndFeelForm < ApplicationForm include ApplicationHelper + form do |f| f.select_list( name: :theme, @@ -65,7 +66,7 @@ class My::LookAndFeelForm < ApplicationForm f.check_box name: :disable_keyboard_shortcuts, label: I18n.t("activerecord.attributes.user_preference.disable_keyboard_shortcuts"), caption: I18n.t("activerecord.attributes.user_preference.disable_keyboard_shortcuts_caption_html", - href: OpenProject::Static::Links.links[:shortcuts][:href]).html_safe + href: OpenProject::Static::Links.url_for(:shortcuts)).html_safe f.submit(name: :submit, label: I18n.t("activerecord.attributes.user_preference.button_update_look_and_feel"), diff --git a/app/helpers/static_links_helper.rb b/app/helpers/static_links_helper.rb index 0a6b1ff952e..36b5f6283b3 100644 --- a/app/helpers/static_links_helper.rb +++ b/app/helpers/static_links_helper.rb @@ -31,11 +31,12 @@ module StaticLinksHelper ## # Create a static link to the given key entry - def static_link_to(key, label: nil) - item = OpenProject::Static::Links.links.fetch key + def static_link_to(*path, label: nil) + href = OpenProject::Static::Links.url_for(*path) + label_text = label || OpenProject::Static::Links.label_for(*path) - link_to label || t(item[:label]), - item[:href], + link_to label_text, + href, class: "openproject--static-link", target: "_blank", rel: "noopener" end @@ -44,8 +45,7 @@ module StaticLinksHelper # Link to the correct installation guides for the current selected method def installation_guide_link val = OpenProject::Configuration.installation_type - link = OpenProject::Static::Links.links[:"#{val}_installation"] || OpenProject::Static::Links.links[:installation_guides] - - link[:href] + # Try specific installation type first, fallback to general installation guides + OpenProject::Static::Links.url_for(:"#{val}_installation") || OpenProject::Static::Links.url_for(:installation_guides) end end diff --git a/app/views/admin/info.html.erb b/app/views/admin/info.html.erb index 50e35fe2b7c..21d2e9821d2 100644 --- a/app/views/admin/info.html.erb +++ b/app/views/admin/info.html.erb @@ -73,7 +73,7 @@ See COPYRIGHT and LICENSE files for more details. if display_security_badge_graphic? content = content_tag :object, nil, data: security_badge_url, type: "image/svg+xml" content += link_to "", - ::OpenProject::Static::Links[:security_badge_documentation][:href], + ::OpenProject::Static::Links.url_for(:security_badge_documentation), title: t(:label_what_is_this), class: "security-badge--help-icon icon-context icon-help1", target: "_blank" diff --git a/app/views/admin/settings/api_settings/show.html.erb b/app/views/admin/settings/api_settings/show.html.erb index 1eeeba35ca0..e1167f136f2 100644 --- a/app/views/admin/settings/api_settings/show.html.erb +++ b/app/views/admin/settings/api_settings/show.html.erb @@ -84,7 +84,7 @@ See COPYRIGHT and LICENSE files for more details.

<%= t(:text_line_separated) %>

<%= t( :setting_apiv3_cors_origins_text_html, - origin_link: ::OpenProject::Static::Links[:origin_mdn_documentation][:href] + origin_link: ::OpenProject::Static::Links.url_for(:origin_mdn_documentation) ) %>

diff --git a/app/views/admin/settings/date_format_settings/show.html.erb b/app/views/admin/settings/date_format_settings/show.html.erb index 9e6b5b9fe52..66a3ed5395b 100644 --- a/app/views/admin/settings/date_format_settings/show.html.erb +++ b/app/views/admin/settings/date_format_settings/show.html.erb @@ -69,7 +69,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= setting_select :first_week_of_year, [[day_name(1), "1"], [day_name(4), "4"]], blank: :label_language_based, container_class: "-wide" %>
-

<%= t("settings.date_format.first_week_of_year_text_html", link: OpenProject::Static::Links[:date_format_settings_documentation][:href]) %>

+

<%= t("settings.date_format.first_week_of_year_text_html", link: OpenProject::Static::Links.url_for(:date_format_settings_documentation)) %>

diff --git a/app/views/admin/settings/general_settings/show.html.erb b/app/views/admin/settings/general_settings/show.html.erb index 6137683473a..81ac683fcb1 100644 --- a/app/views/admin/settings/general_settings/show.html.erb +++ b/app/views/admin/settings/general_settings/show.html.erb @@ -96,7 +96,7 @@ See COPYRIGHT and LICENSE files for more details. <%= t( :text_notice_security_badge_displayed_html, information_panel_label: t(:label_information), - more_info_url: ::OpenProject::Static::Links[:security_badge_documentation][:href], + more_info_url: ::OpenProject::Static::Links.url_for(:security_badge_documentation), information_panel_path: info_admin_index_path ) %> diff --git a/app/views/admin/settings/icalendar_settings/show.html.erb b/app/views/admin/settings/icalendar_settings/show.html.erb index bd1c6cc6623..ac1885f1b22 100644 --- a/app/views/admin/settings/icalendar_settings/show.html.erb +++ b/app/views/admin/settings/icalendar_settings/show.html.erb @@ -50,7 +50,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= setting_check_box :ical_enabled, size: 6 %>
-

<%= t("settings.icalendar.enable_subscriptions_text_html", link: OpenProject::Static::Links[:ical_docs][:href]) %>

+

<%= t("settings.icalendar.enable_subscriptions_text_html", link: OpenProject::Static::Links.url_for(:ical_docs)) %>

diff --git a/app/views/enterprise_tokens/_info.html.erb b/app/views/enterprise_tokens/_info.html.erb index 442f7494d26..2e93bb6bcea 100644 --- a/app/views/enterprise_tokens/_info.html.erb +++ b/app/views/enterprise_tokens/_info.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% content_for :header_tags do %> - <%= nonced_javascript_include_tag OpenProject::Static::Links.links[:chargebee][:href], + <%= nonced_javascript_include_tag OpenProject::Static::Links.url_for(:chargebee), "data-cb-site": OpenProject::Configuration.enterprise_chargebee_site %> <% end %> diff --git a/app/views/homescreen/blocks/_administration.html.erb b/app/views/homescreen/blocks/_administration.html.erb index 37d1414d433..4d6eefbf5f8 100644 --- a/app/views/homescreen/blocks/_administration.html.erb +++ b/app/views/homescreen/blocks/_administration.html.erb @@ -51,7 +51,7 @@ <%= content_tag :div, class: "security-badge--container" do %> <%= content_tag :object, nil, data: security_badge_url, type: "image/svg+xml" %> <%= link_to "", - ::OpenProject::Static::Links[:security_badge_documentation][:href], + ::OpenProject::Static::Links.url_for(:security_badge_documentation), title: t(:label_what_is_this), class: "security-badge--help-icon icon-context icon-help1", target: "_blank", rel: "noopener" %> diff --git a/app/views/homescreen/blocks/_community.html.erb b/app/views/homescreen/blocks/_community.html.erb index fa2974ce694..d0b385c9c07 100644 --- a/app/views/homescreen/blocks/_community.html.erb +++ b/app/views/homescreen/blocks/_community.html.erb @@ -11,30 +11,63 @@
  • <%= static_link_to :forums %>
  • - <%= static_link_to (EnterpriseToken.active? ? :enterprise_support : :enterprise_support_as_community) %> + <%= static_link_to(EnterpriseToken.active? ? :enterprise_support : :enterprise_support_as_community) %>
  • <%= link_to( - t("label_openproject_website"), "#{OpenProject::Static::Links.links[:website][:href]}/?utm_source=unknown&utm_medium=op-instance&utm_campaign=website-home-screen", - { aria: { label: t("label_openproject_website") }, + t("label_openproject_website"), + OpenProject::Static::Links.url_for( + :website, + url_params: { + utm_source: "unknown", + utm_medium: "op-instance", + utm_campaign: "website-home-screen" + } + ), + { + aria: { label: t("label_openproject_website") }, target: "_blank", - title: t("label_openproject_website"), rel: "noopener" } + title: t("label_openproject_website"), + rel: "noopener" + } ) %>
  • <%= link_to( - t("homescreen.links.security_alerts"), "#{OpenProject::Static::Links.links[:security_alerts][:href]}/?utm_source=unknown&utm_medium=op-instance&utm_campaign=security-alerts-home-screen", - { aria: { label: t("homescreen.links.security_alerts") }, + t("homescreen.links.security_alerts"), + OpenProject::Static::Links.url_for( + :security_alerts, + url_params: { + utm_source: "unknown", + utm_medium: "op-instance", + utm_campaign: "security-alerts-home-screen" + } + ), + { + aria: { label: t("homescreen.links.security_alerts") }, target: "_blank", - title: t("homescreen.links.security_alerts"), rel: "noopener" } + title: t("homescreen.links.security_alerts"), + rel: "noopener" + } ) %>
  • <%= link_to( - t("homescreen.links.newsletter"), "#{OpenProject::Static::Links.links[:newsletter][:href]}/?utm_source=unknown&utm_medium=op-instance&utm_campaign=newsletter-home-screen", - { aria: { label: t("homescreen.links.newsletter") }, + t("homescreen.links.newsletter"), + OpenProject::Static::Links.url_for( + :newsletter, + url_params: { + utm_source: "unknown", + utm_medium: "op-instance", + utm_campaign: "newsletter-home-screen" + } + ), + { + aria: { label: t("homescreen.links.newsletter") }, target: "_blank", - title: t("homescreen.links.newsletter"), rel: "noopener" } + title: t("homescreen.links.newsletter"), + rel: "noopener" + } ) %>
  • diff --git a/app/views/homescreen/index.html.erb b/app/views/homescreen/index.html.erb index 285d42e9ca2..90bfc0324f6 100644 --- a/app/views/homescreen/index.html.erb +++ b/app/views/homescreen/index.html.erb @@ -51,11 +51,18 @@ See COPYRIGHT and LICENSE files for more details. <% end %> diff --git a/app/views/ldap_auth_sources/_form.html.erb b/app/views/ldap_auth_sources/_form.html.erb index 8ffca274b2c..1abac258bed 100644 --- a/app/views/ldap_auth_sources/_form.html.erb +++ b/app/views/ldap_auth_sources/_form.html.erb @@ -73,7 +73,7 @@ See COPYRIGHT and LICENSE files for more details. <%= t("ldap_auth_sources.connection_encryption") %>

    <%= t "ldap_auth_sources.tls_mode.section_more_info_link_html", - link: OpenProject::Static::Links[:ldap_encryption_documentation][:href] %> + link: OpenProject::Static::Links.url_for(:ldap_encryption_documentation) %>

    <%= f.radio_button :tls_mode, diff --git a/app/views/user_mailer/user_signed_up.html.erb b/app/views/user_mailer/user_signed_up.html.erb index 299c41c34e7..13bbbe9e2db 100644 --- a/app/views/user_mailer/user_signed_up.html.erb +++ b/app/views/user_mailer/user_signed_up.html.erb @@ -40,10 +40,10 @@ See COPYRIGHT and LICENSE files for more details.

    <%= t( :mail_body_register_links_html, - webinar_link: link_to("Webinar Link", OpenProject::Static::Links.links[:webinar_videos][:href]), + webinar_link: link_to("Webinar Link", OpenProject::Static::Links.url_for(:webinar_videos)), youtube_link: link_to("Youtube Link", OpenProject::Configuration.youtube_channel), - get_started_link: link_to("Get Started Videos Link", OpenProject::Static::Links.links[:get_started_videos][:href]), - documentation_link: link_to("Documentation Link", OpenProject::Static::Links.links[:openproject_docs][:href]) + get_started_link: link_to("Get Started Videos Link", OpenProject::Static::Links.url_for(:get_started_videos)), + documentation_link: link_to("Documentation Link", OpenProject::Static::Links.url_for(:openproject_docs)) ) %>

    diff --git a/app/views/user_mailer/user_signed_up.text.erb b/app/views/user_mailer/user_signed_up.text.erb index ebf9bedce67..d24cb8b8ae1 100644 --- a/app/views/user_mailer/user_signed_up.text.erb +++ b/app/views/user_mailer/user_signed_up.text.erb @@ -33,10 +33,10 @@ See COPYRIGHT and LICENSE files for more details. <%= strip_tags t( :mail_body_register_links_html, - webinar_link: OpenProject::Static::Links.links[:webinar_videos][:href], + webinar_link: OpenProject::Static::Links.url_for(:webinar_videos), youtube_link: OpenProject::Configuration.youtube_channel, - get_started_link: OpenProject::Static::Links.links[:get_started_videos][:href], - documentation_link: OpenProject::Static::Links.links[:openproject_docs][:href] + get_started_link: OpenProject::Static::Links.url_for(:get_started_videos), + documentation_link: OpenProject::Static::Links.url_for(:openproject_docs) ) %> diff --git a/app/views/users/_preferences.html.erb b/app/views/users/_preferences.html.erb index ec3d9447877..0968a94229c 100644 --- a/app/views/users/_preferences.html.erb +++ b/app/views/users/_preferences.html.erb @@ -46,7 +46,7 @@ See COPYRIGHT and LICENSE files for more details. <%= I18n.t( "activerecord.attributes.user_preference.disable_keyboard_shortcuts_caption_html", - href: OpenProject::Static::Links.links[:shortcuts][:href] + href: OpenProject::Static::Links.url_for(:shortcuts) ).html_safe %>
    diff --git a/config/initializers/homescreen.rb b/config/initializers/homescreen.rb index 83db6d1c19d..9a5f20f9224 100644 --- a/config/initializers/homescreen.rb +++ b/config/initializers/homescreen.rb @@ -72,36 +72,31 @@ OpenProject::Static::Homescreen.manage :blocks do |blocks| end OpenProject::Static::Homescreen.manage :links do |links| - link_hash = OpenProject::Static::Links.links - links.push( { label: :user_guides, icon: "icon-context icon-rename", - url: link_hash[:user_guides][:href] + url_key: :user_guides }, { label: :glossary, icon: "icon-context icon-glossar", - url: link_hash[:glossary][:href] + url_key: :glossary }, { label: :shortcuts, icon: "icon-context icon-shortcuts", - url: link_hash[:shortcuts][:href] + url_key: :shortcuts }, { label: :forums, icon: "icon-context icon-forums", - url: link_hash[:forums][:href] + url_key: :forums + }, + { + label: :impressum, + icon: "icon-context icon-info1", + url_key: :impressum } ) - - if impressum_link = link_hash[:impressum] - links.push({ - label: :impressum, - url: impressum_link[:href], - icon: "icon-context icon-info1" - }) - end end diff --git a/config/static_links.yml b/config/static_links.yml index 1b43258b354..5cbae86fe57 100644 --- a/config/static_links.yml +++ b/config/static_links.yml @@ -81,6 +81,8 @@ get_started_videos: glossary: href: https://www.openproject.org/docs/glossary/ label: homescreen.links.glossary +github: + href: https://github.com/opf/openproject ical_docs: href: https://www.openproject.org/docs/user-guide/calendar/#subscribe-to-a-calendar installation_guides: diff --git a/lib/open_project/journal_formatter/cause.rb b/lib/open_project/journal_formatter/cause.rb index 6148cbf82f9..1728ebe119f 100644 --- a/lib/open_project/journal_formatter/cause.rb +++ b/lib/open_project/journal_formatter/cause.rb @@ -94,9 +94,9 @@ class OpenProject::JournalFormatter::Cause < JournalFormatter::Base case feature when "progress_calculation_adjusted_from_disabled_mode", "progress_calculation_adjusted" - { href: OpenProject::Static::Links.links[:blog_article_progress_changes][:href] } + { href: OpenProject::Static::Links.url_for(:blog_article_progress_changes) } when "totals_removed_from_childless_work_packages" - { href: OpenProject::Static::Links.links[:release_notes_14_0_1][:href] } + { href: OpenProject::Static::Links.url_for(:release_notes_14_0_1) } else {} end diff --git a/lib/open_project/static/links.rb b/lib/open_project/static/links.rb index 47b4d4f60ea..07cff8577c6 100644 --- a/lib/open_project/static/links.rb +++ b/lib/open_project/static/links.rb @@ -37,22 +37,28 @@ module OpenProject end def help_link - OpenProject::Configuration.force_help_link.presence || static_links[:user_guides] + OpenProject::Configuration.force_help_link.presence || static_links[:user_guides][:href] end - delegate :[], to: :links - - def links - @links ||= static_links.merge(dynamic_links) + def cache_key + @cache_key ||= OpenProject::Cache::CacheKey.expand(links) end - def url_for(*items, localize_url: true) - href = links.dig(*items, :href) + def label_for(*path) + key = links.dig(*path, :label) + return if key.nil? - if localize_url && docs_url?(href) - with_locale_param(href) + I18n.t(key) + end + + def url_for(*path, localize_url: true, url_params: {}) + href = links.dig(*path, :href) + return if href.nil? + + if localize_url && website_link?(href) + url_with_query(href, **url_params, go_to_locale: I18n.locale) else - href + url_with_query(href, **url_params) end end @@ -60,22 +66,28 @@ module OpenProject @links.key? name end - def docs_url?(url) - url&.start_with?(docs_url) + def website_link?(url) + url&.start_with?(website_url) end - def docs_url - links[:openproject_docs][:href] - end - - def with_locale_param(href) - url = Addressable::URI.parse(href) - url.query_values = (url.query_values || {}).merge(go_to_locale: I18n.locale) - url.to_s + def website_url + links[:website][:href] end private + def links + @links ||= static_links.merge(dynamic_links) + end + + def url_with_query(href, **params) + return href if params.empty? + + url = Addressable::URI.parse(href) + url.query_values = (url.query_values || {}).merge(params) + url.to_s + end + def dynamic_links dynamic = { help: { diff --git a/lib/redmine/menu_manager/top_menu/help_menu.rb b/lib/redmine/menu_manager/top_menu/help_menu.rb index e1bb6557a01..4926bf8ea75 100644 --- a/lib/redmine/menu_manager/top_menu/help_menu.rb +++ b/lib/redmine/menu_manager/top_menu/help_menu.rb @@ -31,7 +31,7 @@ module Redmine::MenuManager::TopMenu::HelpMenu def render_help_top_menu_node(item = help_menu_item) cache_key = ["help_top_menu_node", - OpenProject::Static::Links.links, + OpenProject::Static::Links.cache_key, I18n.locale, OpenProject::Static::Links.help_link, EnterpriseToken.active?] @@ -100,7 +100,11 @@ module Redmine::MenuManager::TopMenu::HelpMenu unless EnterpriseToken.hide_banners? && EnterpriseToken.active? menu_group.with_item( **link_options_for(:upsell, - href_suffix: "/?utm_source=unknown&utm_medium=op-instance&utm_campaign=ee-upsell-help-menu") + url_params: { + utm_source: "unknown", + utm_medium: "op-instance", + utm_campaign: "ee-upsell-help-menu" + }) ) end menu_group.with_item(**link_options_for(:user_guides)) @@ -131,15 +135,27 @@ module Redmine::MenuManager::TopMenu::HelpMenu menu_group.with_item(**link_options_for(:digital_accessibility)) menu_group.with_item(**link_options_for( :website, - href_suffix: "/?utm_source=unknown&utm_medium=op-instance&utm_campaign=website-help-menu" + url_params: { + utm_source: "unknown", + utm_medium: "op-instance", + utm_campaign: "website-help-menu" + } )) menu_group.with_item(**link_options_for( :security_alerts, - href_suffix: "/?utm_source=unknown&utm_medium=op-instance&utm_campaign=security-help-menu" + url_params: { + utm_source: "unknown", + utm_medium: "op-instance", + utm_campaign: "security-help-menu" + } )) menu_group.with_item(**link_options_for( :newsletter, - href_suffix: "/?utm_source=unknown&utm_medium=op-instance&utm_campaign=newsletter-help-menu" + url_params: { + utm_source: "unknown", + utm_medium: "op-instance", + utm_campaign: "newsletter-help-menu" + } )) menu_group.with_item(**link_options_for(:blog)) menu_group.with_item(**link_options_for(:release_notes)) @@ -151,11 +167,11 @@ module Redmine::MenuManager::TopMenu::HelpMenu end def link_options_for(key, options = {}) - link = OpenProject::Static::Links.links[key] - label = I18n.t(link[:label]) + href = OpenProject::Static::Links.url_for(key, url_params: options[:url_params] || {}) + label = OpenProject::Static::Links.label_for(key) { - href: "#{link[:href]}#{options[:href_suffix]}", + href: href, label: label, content_arguments: { target: "_blank", diff --git a/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb b/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb index 5ef187c7fba..bf087c78006 100644 --- a/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb +++ b/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb @@ -87,7 +87,7 @@ module Storages::Admin::Forms I18n.t( "storages.instructions.#{provider_type}.provider_configuration", application_link_text: application_link_text_for( - ::OpenProject::Static::Links[:storage_docs][:"#{provider_type}_oauth_application"][:href], + ::OpenProject::Static::Links.url_for(:storage_docs, :"#{provider_type}_oauth_application"), I18n.t("storages.instructions.#{provider_type}.application_link_text") ) ).html_safe diff --git a/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb b/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb index 71a8bc01232..c090e03e5dd 100644 --- a/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb +++ b/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb @@ -57,12 +57,12 @@ module Storages::Admin::Forms end def one_drive_integration_link(target: "_blank") - href = ::OpenProject::Static::Links[:storage_docs][:one_drive_oauth_application][:href] + href = ::OpenProject::Static::Links.url_for(:storage_docs, :one_drive_oauth_application) render(Primer::Beta::Link.new(href:, underline: true, target:)) { I18n.t("storages.instructions.one_drive.application_link_text") } end def sharepoint_integration_link(target: "_blank") - href = ::OpenProject::Static::Links[:storage_docs][:sharepoint_oauth_application][:href] + href = ::OpenProject::Static::Links.url_for(:storage_docs, :sharepoint_oauth_application) render(Primer::Beta::Link.new(href:, underline: true, target:)) { I18n.t("storages.instructions.sharepoint.application_link_text") } end diff --git a/modules/storages/app/components/storages/open_project_storage_modal_component.rb b/modules/storages/app/components/storages/open_project_storage_modal_component.rb index 70266653e56..ec12fcad1fb 100644 --- a/modules/storages/app/components/storages/open_project_storage_modal_component.rb +++ b/modules/storages/app/components/storages/open_project_storage_modal_component.rb @@ -57,7 +57,7 @@ class Storages::OpenProjectStorageModalComponent < ViewComponent::Base end def subtitle_timeout_text - href = OpenProject::Static::Links[:storage_docs][:health_status][:href] + href = OpenProject::Static::Links.url_for(:storage_docs, :health_status) I18n.t( "storages.open_project_storage_modal.timeout.subtitle", storages_health_link: render(Primer::Beta::Link.new(href:, target: "_blank", underline: true)) do diff --git a/modules/storages/app/forms/storages/admin/provider_drive_id_input_form.rb b/modules/storages/app/forms/storages/admin/provider_drive_id_input_form.rb index 9f9b46c21fe..9a46f35a297 100644 --- a/modules/storages/app/forms/storages/admin/provider_drive_id_input_form.rb +++ b/modules/storages/app/forms/storages/admin/provider_drive_id_input_form.rb @@ -44,7 +44,7 @@ module Storages::Admin private def caption - href = ::OpenProject::Static::Links[:storage_docs][:one_drive_drive_id_guide][:href] + href = ::OpenProject::Static::Links.url_for(:storage_docs, :one_drive_drive_id_guide) I18n.t("storages.instructions.one_drive.drive_id", drive_id_link_text: render(Primer::Beta::Link.new(href:, underline: true, target: "_blank")) do I18n.t("storages.instructions.one_drive.documentation_link_text") diff --git a/modules/storages/app/forms/storages/admin/provider_tenant_id_input_form.rb b/modules/storages/app/forms/storages/admin/provider_tenant_id_input_form.rb index 82e383eef7b..8d3f09e88e4 100644 --- a/modules/storages/app/forms/storages/admin/provider_tenant_id_input_form.rb +++ b/modules/storages/app/forms/storages/admin/provider_tenant_id_input_form.rb @@ -45,7 +45,7 @@ module Storages::Admin private def caption - href = ::OpenProject::Static::Links[:storage_docs][:one_drive_oauth_application][:href] + href = ::OpenProject::Static::Links.url_for(:storage_docs, :one_drive_oauth_application) I18n.t("storages.instructions.one_drive.tenant_id", application_link_text: render(Primer::Beta::Link.new(href:, underline: true, target: "_blank")) do I18n.t("storages.instructions.one_drive.application_link_text") diff --git a/modules/storages/app/views/storages/admin/storages/new.html.erb b/modules/storages/app/views/storages/admin/storages/new.html.erb index 0c1a6243ac7..9178b323b94 100644 --- a/modules/storages/app/views/storages/admin/storages/new.html.erb +++ b/modules/storages/app/views/storages/admin/storages/new.html.erb @@ -18,7 +18,7 @@ <% header.with_description(test_selector: 'storage-new-page-header--description') do %> <%= t("storages.instructions.new_storage", - provider_link: ::OpenProject::Static::Links[:storage_docs][:"#{@storage}_setup"][:href].html_safe, + provider_link: ::OpenProject::Static::Links.url_for(:storage_docs, :"#{@storage}_setup"), provider_name: I18n.t("storages.provider_types.#{@storage}.name") ).html_safe %> diff --git a/modules/storages/app/views/storages/storages_mailer/_health_status_notification.html.erb b/modules/storages/app/views/storages/storages_mailer/_health_status_notification.html.erb index c8f76f7c9a1..6d8d499ca97 100644 --- a/modules/storages/app/views/storages/storages_mailer/_health_status_notification.html.erb +++ b/modules/storages/app/views/storages/storages_mailer/_health_status_notification.html.erb @@ -87,7 +87,7 @@ See COPYRIGHT and LICENSE files for more details. <%= I18n.t("mail.storages.health.unhealthy.troubleshooting.text") %> - <%= I18n.t("mail.storages.health.unhealthy.troubleshooting.link_text") %>. + <%= I18n.t("mail.storages.health.unhealthy.troubleshooting.link_text") %>. <%= placeholder_cell("12px", vertical: true) %> diff --git a/modules/two_factor_authentication/app/views/two_factor_authentication/settings.html.erb b/modules/two_factor_authentication/app/views/two_factor_authentication/settings.html.erb index b586dfc047e..97db0435e12 100644 --- a/modules/two_factor_authentication/app/views/two_factor_authentication/settings.html.erb +++ b/modules/two_factor_authentication/app/views/two_factor_authentication/settings.html.erb @@ -22,9 +22,9 @@

    <%= t("two_factor_authentication.settings.text_configuration") %>
    - <% configuration_link = OpenProject::Static::Links.links.fetch :configuration_guide %> + <% configuration_link = OpenProject::Static::Links.url_for :configuration_guide %> <%= link_to t("two_factor_authentication.settings.text_configuration_guide"), - configuration_link[:href], + configuration_link, target: "_blank" %>

    <%= render(AttributeGroups::AttributeGroupComponent.new) do |component| diff --git a/modules/webhooks/app/views/webhooks/outgoing/admin/_form.html.erb b/modules/webhooks/app/views/webhooks/outgoing/admin/_form.html.erb index 0058b573046..99b507d8202 100644 --- a/modules/webhooks/app/views/webhooks/outgoing/admin/_form.html.erb +++ b/modules/webhooks/app/views/webhooks/outgoing/admin/_form.html.erb @@ -2,7 +2,7 @@

    <%= t "webhooks.outgoing.form.introduction" %>
    - <%= link_to t("webhooks.outgoing.form.apiv3_doc_url"), OpenProject::Static::Links.links[:api_docs][:href] %> + <%= link_to t("webhooks.outgoing.form.apiv3_doc_url"), OpenProject::Static::Links.url_for(:api_docs) %>

    diff --git a/spec/components/enterprise_edition/banner_component_spec.rb b/spec/components/enterprise_edition/banner_component_spec.rb index 3c66f44d5d3..25383683334 100644 --- a/spec/components/enterprise_edition/banner_component_spec.rb +++ b/spec/components/enterprise_edition/banner_component_spec.rb @@ -257,7 +257,7 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do expect(component).to have_text(expected_title) expect(component).to have_text(expected_description) - expect(component).to have_link("More information", href: "https://www.openproject.org/enterprise-edition") + expect(component).to have_link("More information", href: "https://www.openproject.org/enterprise-edition?go_to_locale=mo") end end diff --git a/spec/components/work_packages/progress/shared_modal_examples.rb b/spec/components/work_packages/progress/shared_modal_examples.rb index 398c3ebd155..bd81d07658b 100644 --- a/spec/components/work_packages/progress/shared_modal_examples.rb +++ b/spec/components/work_packages/progress/shared_modal_examples.rb @@ -74,7 +74,7 @@ RSpec.shared_examples_for "progress modal help links" do expect(page) .to have_link("Learn more", - href: OpenProject::Static::Links.links[:progress_tracking_docs][:href]) + href: OpenProject::Static::Links.url_for(:progress_tracking_docs)) end end end diff --git a/spec/lib/open_project/journal_formatter/cause_spec.rb b/spec/lib/open_project/journal_formatter/cause_spec.rb index fdf249ad827..059471fd9ad 100644 --- a/spec/lib/open_project/journal_formatter/cause_spec.rb +++ b/spec/lib/open_project/journal_formatter/cause_spec.rb @@ -450,7 +450,7 @@ RSpec.describe OpenProject::JournalFormatter::Cause do end it do - href = OpenProject::Static::Links.links[:blog_article_progress_changes][:href] + href = OpenProject::Static::Links.url_for(:blog_article_progress_changes) expect(cause).to render_html_variant( "OpenProject system update: Progress calculation automatically " \ "set to work-based mode and adjusted with version update." @@ -474,7 +474,7 @@ RSpec.describe OpenProject::JournalFormatter::Cause do end it do - href = OpenProject::Static::Links.links[:blog_article_progress_changes][:href] + href = OpenProject::Static::Links.url_for(:blog_article_progress_changes) expect(cause).to render_html_variant( "OpenProject system update: Progress calculation automatically " \ "adjusted with version update." @@ -509,7 +509,7 @@ RSpec.describe OpenProject::JournalFormatter::Cause do end it do - href = OpenProject::Static::Links.links[:release_notes_14_0_1][:href] + href = OpenProject::Static::Links.url_for(:release_notes_14_0_1) expect(cause).to render_html_variant( "OpenProject system update: Work and progress totals " \ "automatically removed for non-parent work packages with " \ diff --git a/spec/lib/open_project/static/links_spec.rb b/spec/lib/open_project/static/links_spec.rb index 6b35e1aedd8..86f4825c568 100644 --- a/spec/lib/open_project/static/links_spec.rb +++ b/spec/lib/open_project/static/links_spec.rb @@ -61,18 +61,123 @@ RSpec.describe OpenProject::Static::Links do end end - context "with non-docs URLs" do + context "with website URL" do let(:args) { %i[website] } - it "does not add locale parameter to non-docs URLs" do - expect(subject).to eq("https://www.openproject.org") + it "adds locale parameter to website URL" do + expect(subject).to eq("https://www.openproject.org?go_to_locale=en") + end + end + + context "with other URLs" do + let(:args) { %i[github] } + + it "does not add a parameter" do + expect(subject).to eq("https://github.com/opf/openproject") expect(subject).not_to include("go_to_locale=") end end + + context "with additional URL parameters" do + let(:args) { %i[website] } + + it "adds custom URL parameters" do + result = described_class.url_for(*args, url_params: { utm_source: "test", utm_medium: "spec" }) + expect(result).to include("utm_source=test") + expect(result).to include("utm_medium=spec") + end + end + + context "with non-existent path" do + let(:args) { %i[non_existent_key] } + + it "returns nil for non-existent paths" do + expect(subject).to be_nil + end + end + + context "with localize_url disabled" do + let(:args) { %i[enterprise_features board_view] } + + it "does not add locale parameter when localize_url is false" do + result = described_class.url_for(*args, localize_url: false) + expect(result).not_to include("go_to_locale=") + expect(result).to eq("https://www.openproject.org/docs/user-guide/agile-boards/#action-boards-enterprise-add-on") + end + end end - describe ".docs_url?" do - subject { described_class.docs_url?(url) } + describe ".label_for" do + subject { described_class.label_for(*args) } + + let(:args) { %i[website] } + + it "returns the translated label for the given path" do + expect(subject).to eq(I18n.t("label_openproject_website")) + end + + context "with single key" do + let(:args) { %i[shortcuts] } + + it "returns the translated label for a single key" do + expect(subject).to eq(I18n.t("homescreen.links.shortcuts")) + end + end + + context "with non-existent path" do + let(:args) { %i[non_existent_key] } + + it "returns nil for non-existent paths" do + expect(subject).to be_nil + end + end + end + + describe ".cache_key" do + subject { described_class.cache_key } + + it "returns a cache key based on the links" do + expect(subject).to be_a(String) + expect(subject).not_to be_empty + end + + it "returns the same key for multiple calls" do + first_call = described_class.cache_key + second_call = described_class.cache_key + expect(first_call).to eq(second_call) + end + end + + describe ".has?" do + subject { described_class.has?(key) } + + context "with existing key" do + let(:key) { :website } + + it "returns true for existing keys" do + expect(subject).to be true + end + end + + context "with non-existing key" do + let(:key) { :non_existent_key } + + it "returns false for non-existing keys" do + expect(subject).to be false + end + end + end + + describe ".website_url" do + subject { described_class.website_url } + + it "returns the website URL" do + expect(subject).to eq("https://www.openproject.org") + end + end + + describe ".website_link?" do + subject { described_class.website_link?(url) } context "with docs URLs" do let(:url) { "https://www.openproject.org/docs/user-guide/agile-boards/" } @@ -83,56 +188,67 @@ RSpec.describe OpenProject::Static::Links do end context "with non-docs URLs" do - let(:url) { "https://www.openproject.org/enterprise-edition" } + let(:url) { "https://foo.example.com" } it "returns false for URLs that do not start with the docs base URL" do expect(subject).to be false end end - end - describe ".with_locale_param" do - subject { described_class.with_locale_param(href) } + context "with nil URL" do + let(:url) { nil } - let(:href) { "https://www.openproject.org/docs/system-admin-guide/authentication/openid-providers/" } - - before do - allow(I18n).to receive(:locale).and_return(:en) - end - - it "adds go_to_locale parameter to the URL" do - expect(subject).to include("go_to_locale=en") - end - - it "preserves the original URL structure" do - expect(subject).to start_with(href) - end - - context "with URL that already has query parameters" do - let(:href) { "https://www.openproject.org/docs/user-guide/agile-boards/?section=boards" } - - it "adds go_to_locale parameter while preserving existing parameters" do - expect(subject).to include("section=boards") - expect(subject).to include("go_to_locale=en") + it "returns false for nil URLs" do + expect(subject).to be_falsy end end + end - context "with different locale" do + describe ".help_link_overridden?" do + subject { described_class.help_link_overridden? } + + context "when help link is not overridden" do before do - allow(I18n).to receive(:locale).and_return(:de) + allow(OpenProject::Configuration).to receive(:force_help_link).and_return(nil) end - it "uses the current I18n locale" do - expect(subject).to include("go_to_locale=de") + it "returns false" do + expect(subject).to be false + end + end + + context "when help link is overridden" do + before do + allow(OpenProject::Configuration).to receive(:force_help_link).and_return("https://custom.help.com") + end + + it "returns true" do + expect(subject).to be true end end end - describe ".docs_url" do - subject { described_class.docs_url } + describe ".help_link" do + subject { described_class.help_link } - it "returns the base docs URL" do - expect(subject).to eq("https://www.openproject.org/docs/") + context "when help link is not overridden" do + before do + allow(OpenProject::Configuration).to receive(:force_help_link).and_return(nil) + end + + it "returns the default user guides link" do + expect(subject).to eq("https://www.openproject.org/docs/user-guide/") + end + end + + context "when help link is overridden" do + before do + allow(OpenProject::Configuration).to receive(:force_help_link).and_return("https://custom.help.com") + end + + it "returns the overridden help link" do + expect(subject).to eq("https://custom.help.com") + end end end end diff --git a/spec/support/support_links.rb b/spec/support/support_links.rb index f3715449984..4423716c747 100644 --- a/spec/support/support_links.rb +++ b/spec/support/support_links.rb @@ -30,7 +30,7 @@ # rubocop:disable RSpec/ContextWording RSpec.shared_context "support links" do - let(:support_link_as_community) { "https://www.openproject.org/pricing/#support" } - let(:support_link_as_enterprise) { "https://www.openproject.org/docs/enterprise-guide/support/" } + let(:support_link_as_community) { "https://www.openproject.org/pricing/?go_to_locale=en#support" } + let(:support_link_as_enterprise) { "https://www.openproject.org/docs/enterprise-guide/support/?go_to_locale=en" } end # rubocop:enable RSpec/ContextWording