DEV: migrate from Advanced Search banner to Welcome banner (#34534)

1. Changes translation to a more meaningful: `welcome_banner.search` =>
`welcome_banner.search_placeholder`.
* and passes along translation key, rather then `i18n` helper in
`<SearchMenu>` - to make translation handling more consistent

2. Transitions communities from the **Advanced Search Banner** (ASB)
theme component to the **core welcome banner** (CWB). It handles the
transition in three major steps:
* migrates site settings: task
"themes:advanced_search_banner:migrate_settings_to_welcome_banner"
* migrates translations: task
"themes:advanced_search_banner:migrate_translations_to_welcome_banner"
* excludes ASB from themes using it, enables CWB, disables ASB: task
"themes:advanced_search_banner:exclude_and_disable"

### Settings migration
1. Happy path
<img width="1029" height="192" alt="Screenshot 2025-10-29 at 12 40 51"
src="https://github.com/user-attachments/assets/5d29ef48-ce60-4222-b32a-217390967b8d"
/>

### Translations migration
1. Migrates overridden and default texts
<img width="1062" height="291" alt="Screenshot 2025-10-29 at 12 44 26"
src="https://github.com/user-attachments/assets/c43d51a4-59f8-4751-8abb-ae8824a7100f"
/>

### Exclude ASB, enable CWB, disable ASB

2. When ASB is not included in any of themes, all migrations are
skipped, except for disabling ASB
<img width="807" height="458" alt="Screenshot 2025-10-30 at 11 48 43"
src="https://github.com/user-attachments/assets/34e7bd5f-b334-4833-89a1-00d02faac23b"
/>
This commit is contained in:
Yuriy Kurant
2025-10-30 12:00:42 +08:00
committed by GitHub
parent 687cab906a
commit 21447cdaa4
7 changed files with 554 additions and 8 deletions
+1 -1
View File
@@ -324,7 +324,7 @@ en:
subheader:
logged_in_members: ""
anonymous_members: ""
search: "Search"
search_placeholder: "Search"
impersonation:
notice: "You are impersonating %{username}"
@@ -52,6 +52,7 @@ export default class SearchMenu extends Component {
@tracked menuPanelOpen = false;
searchInputId = this.args.searchInputId ?? "search-term";
searchInputPlaceholder = this.args.searchInputPlaceholder || "search.title";
_debouncer = null;
_activeSearch = null;
@@ -454,7 +455,7 @@ export default class SearchMenu extends Component {
@openSearchMenu={{this.open}}
@autofocus={{@autofocusInput}}
@inputId={{this.searchInputId}}
@placeholder={{@placeholder}}
@inputPlaceholder={{this.searchInputPlaceholder}}
/>
{{#if this.loading}}
@@ -121,8 +121,8 @@ export default class SearchTerm extends Component {
autocomplete="off"
enterkeyhint="search"
value={{this.search.activeGlobalSearchTerm}}
placeholder={{this.placeholderText}}
aria-label={{this.placeholderText}}
placeholder={{i18n @inputPlaceholder}}
aria-label={{i18n @inputPlaceholder}}
{{on "keyup" this.onKeyup}}
{{on "keydown" this.onKeydown}}
{{on "input" this.updateSearchTerm}}
@@ -189,7 +189,7 @@ export default class WelcomeBanner extends Component {
<SearchMenu
@location="welcome-banner"
@searchInputId="welcome-banner-search-input"
@placeholder={{i18n "welcome_banner.search"}}
@searchInputPlaceholder="welcome_banner.search_placeholder"
/>
</div>
<PluginOutlet @name="welcome-banner-below-input" />
@@ -61,15 +61,15 @@ module(
assert.dom(".welcome-banner .welcome-banner__title input").doesNotExist();
});
test("uses the welcome_banner.search translation for placeholder", async function (assert) {
test("uses the welcome_banner.search_placeholder translation for placeholder", async function (assert) {
await render(<template><WelcomeBanner /></template>);
assert
.dom("#welcome-banner-search-input")
.hasAttribute(
"placeholder",
i18n("welcome_banner.search"),
"search input uses the welcome_banner.search translation as placeholder"
i18n("welcome_banner.search_placeholder"),
"search input uses the welcome_banner.search_placeholder translation as placeholder"
);
});
}
@@ -0,0 +1,343 @@
# frozen_string_literal: true
THEME_GIT_URL = "https://github.com/discourse/discourse-search-banner.git" unless defined?(
THEME_GIT_URL
)
REQUIRED_TRANSLATION_KEYS = %w[search_banner.headline search_banner.subhead] unless defined?(
REQUIRED_TRANSLATION_KEYS
)
desc "Migrate settings from Advanced Search Banner to core welcome banner"
task "themes:advanced_search_banner:migrate_settings_to_welcome_banner" => :environment do
components = find_all_components([:theme_settings])
if components.empty?
puts "\n\e[33m✗ No Advanced Search Banner theme components found\e[0m"
next
end
components.each { |entry| process_theme_component_settings(entry[:theme]) }
end
desc "Migrate translations from Advanced Search Banner to core welcome banner"
task "themes:advanced_search_banner:migrate_translations_to_welcome_banner" => :environment do
components = find_all_components([:theme_translation_overrides])
if components.empty?
puts "\n\e[33m✗ No Advanced Search Banner theme components found\e[0m"
next
end
components.each { |entry| process_theme_component_translations(entry[:theme]) }
end
desc "Exclude and disable Advanced Search Banner theme component"
task "themes:advanced_search_banner:exclude_and_disable" => :environment do
components = find_all_components
if components.empty?
puts "\n\e[33m✗ No Advanced Search Banner theme components found\e[0m"
next
end
components.each { |entry| process_theme_component(entry[:theme]) }
end
# Common helper methods
def find_all_components(includes = [])
if ENV["RAILS_DB"].present?
db = validate_and_get_db(ENV["RAILS_DB"])
RailsMultisite::ConnectionManagement.establish_connection(db: db)
wrap_themes_with_db(find_components_in_db(db, includes), db)
else
components = []
RailsMultisite::ConnectionManagement.each_connection do |db|
components.concat(wrap_themes_with_db(find_components_in_db(db, includes), db))
end
components
end
end
def validate_and_get_db(db)
return db if RailsMultisite::ConnectionManagement.has_db?(db)
default_db = RailsMultisite::ConnectionManagement::DEFAULT
puts "\e[31m✗ Database \e[1;101m[#{db}]\e[0m \e[31mnot found\e[0m"
puts "Using default database instead: \e[1;104m[#{default_db}]\e[0m\n\n"
default_db
end
def wrap_themes_with_db(themes, db)
themes.map { |theme| { db: db, theme: theme } }
end
def find_components_in_db(db, additional_includes)
puts "Accessing database: \e[1;104m[#{db}]\e[0m"
puts " Searching for Advanced Search Banner components..."
includes = [{ parent_theme_relation: :parent_theme }] + Array(additional_includes)
themes = RemoteTheme.where(remote_url: THEME_GIT_URL).includes(theme: includes).map(&:theme)
themes.each { |theme| puts " \e[1;34mFound: #{theme_identifier(theme)}" }
themes
end
def theme_identifier(theme)
"\e[1m#{theme.name} (ID: #{theme.id})\e[0m"
end
def not_included_in_any_theme?(theme)
return false if theme.parent_theme_relation.exists?
puts " \e[33m#{theme_identifier(theme)} is not included in any of your themes. Skipping\e[0m"
true
end
# Settings migration methods
unless defined?(SETTINGS_MAPPING)
SETTINGS_MAPPING = {
"show_on" => {
site_setting: "welcome_banner_page_visibility",
value_mapping: {
"top_menu" => "top_menu_pages",
"all" => "all_pages",
},
},
"plugin_outlet" => {
site_setting: "welcome_banner_location",
value_mapping: {
"above-main-container" => "above_topic_content",
"below-site-header" => "below_site_header",
},
},
"background_image_light" => {
site_setting: "welcome_banner_image",
value_mapping: nil,
},
}
end
def process_theme_component_settings(theme)
return if not_included_in_any_theme?(theme)
migration_errors = []
puts "\n Migrating settings for #{theme_identifier(theme)}..."
migrated_count = migrate_theme_settings_to_site_settings(theme.theme_settings, migration_errors)
if migrated_count == theme.theme_settings.size
puts " \e[1;32m✓ Migrated #{migrated_count} setting#{"s" if migrated_count != 1}\e[0m"
else
puts " \e[33mMigrated #{migrated_count} out of #{theme.theme_settings.size} setting#{"s" if theme.theme_settings.size != 1}\e[0m"
end
if migration_errors.any?
puts "\n\e[1;31mMigration completed with errors\e[0m"
else
puts "\n\e[1;34mMigration completed successfully!\e[0m"
end
end
def migrate_theme_settings_to_site_settings(theme_settings, errors)
migrated_count = 0
theme_settings.each do |ts|
mapping = SETTINGS_MAPPING[ts.name]
next unless mapping
site_setting_name = mapping[:site_setting]
if ts.value.blank?
puts " - skipping '#{ts.name}' as it has no value"
next
end
if mapping[:value_mapping]
new_value = mapping[:value_mapping][ts.value] || ts.value
else
new_value = ts.value.to_i
end
begin
SiteSetting.set_and_log(
site_setting_name,
new_value,
Discourse.system_user,
"Migrated from the deprecated Advanced Search Banner",
)
old_text = "\e[0;31m#{ts.name}: #{ts.value}\e[0m"
arrow = "\e[0m=>\e[0m"
new_text = "\e[0;32m#{site_setting_name}: #{new_value}\e[0m"
puts " - #{old_text} #{arrow} #{new_text}"
migrated_count += 1
rescue StandardError => e
errors << e
puts " \e[31m- failed to migrate '#{ts.name}': \e[1m#{e.message}\e[0m"
end
end
migrated_count
end
# Translations migration methods
def process_theme_component_translations(theme)
return if not_included_in_any_theme?(theme)
puts "\n Migrating translation overrides for #{theme_identifier(theme)}..."
migrated_count = 0
if theme.theme_translation_overrides.any?
processed_keys_by_locale = Hash.new { |h, k| h[k] = Set.new }
theme.theme_translation_overrides.each do |override|
count =
migrate_translations(
locale: override.locale,
key: override.translation_key,
value: override.value,
)
migrated_count += count
processed_keys_by_locale[override.locale].add(override.translation_key)
end
shown = false
processed_keys_by_locale.each do |locale, processed_keys|
missing_keys = REQUIRED_TRANSLATION_KEYS - processed_keys.to_a
if missing_keys.any?
unless shown
puts " Migrating Advanced Search Banner's default translations..."
shown = true
end
missing_keys.each do |missing_key|
count = migrate_translations(locale: locale, key: missing_key)
migrated_count += count
end
end
end
else
puts " \e[33m✗ No translation overrides found\e[0m"
puts " Migrating Advanced Search Banner's default translations..."
REQUIRED_TRANSLATION_KEYS.each do |required_key|
count = migrate_translations(key: required_key)
migrated_count += count
end
end
puts " \e[1;32m✓ Migrated #{migrated_count} translation#{"s" if migrated_count != 1}\e[0m"
puts "\n\e[1;34mMigration completed successfully!\e[0m"
end
def migrate_translations(locale: "en", key:, value: nil)
default_translations = {
"js.welcome_banner.header.anonymous_members" => "Welcome to our community",
"js.welcome_banner.header.logged_in_members" => "Welcome to our community",
"js.welcome_banner.subheader.anonymous_members" =>
"We're happy to have you here. If you need help, please search before you post.",
"js.welcome_banner.subheader.logged_in_members" =>
"We're happy to have you here. If you need help, please search before you post.",
}
mapped_keys = map_translation_keys(key)
# Print the value once before processing all keys
first_key = mapped_keys.first
new_value = value || default_translations[first_key]
puts " \e[1;32m✓\e[0m \e[1;94m\"#{new_value}\"\e[0m"
mapped_keys.each do |new_key|
actual_value = value || default_translations[new_key]
TranslationOverride.upsert!(locale, new_key, actual_value)
old_text = "\e[0;31m#{key}\e[0m"
arrow = "\e[0m=>\e[0m"
new_text = "\e[0;32m#{locale}.#{new_key}\e[0m"
puts " - #{old_text} #{arrow} #{new_text}"
end
mapped_keys.count
end
def map_translation_keys(translation_key)
translations_mapping = {
"search_banner.headline" => %w[
js.welcome_banner.header.anonymous_members
js.welcome_banner.header.logged_in_members
],
"search_banner.subhead" => %w[
js.welcome_banner.subheader.anonymous_members
js.welcome_banner.subheader.logged_in_members
],
"search_banner.search_button_text" => ["js.welcome_banner.search_placeholder"],
}
translations_mapping[translation_key] || []
end
# Exclude and disable methods
def process_theme_component(theme)
exclude_theme_component(theme)
enable_welcome_banner(theme)
disable_theme_component(theme)
puts "\n\e[1;34mTask completed successfully!\e[0m"
end
def exclude_theme_component(theme)
puts "\n Executing exclude step..."
return if not_included_in_any_theme?(theme)
parent_relations = theme.parent_theme_relation.to_a
total_relations = parent_relations.size
puts "\n Excluding #{theme_identifier(theme)} from..."
parent_relations.each do |relation|
puts " - #{relation.parent_theme.name} (ID: #{relation.parent_theme_id})"
relation.destroy!
end
puts " \e[1;32m✓ Excluded from #{total_relations} theme#{"s" if total_relations > 1}\e[0m"
end
def enable_welcome_banner(theme)
puts "\n Executing enable core welcome banner step..."
if !theme.enabled
puts " \e[33m#{theme_identifier(theme)} is disabled, thus no need to enable core welcome banner. Skipping\e[0m"
return
end
return unless theme.enabled
return if not_included_in_any_theme?(theme)
puts "\n Enabling \e[1mcore welcome banner\e[0m for..."
enabled_count = 0
theme.parent_theme_relation.each do |relation|
parent_theme = relation.parent_theme
site_setting =
ThemeSiteSetting.find_by(theme_id: parent_theme.id, name: "enable_welcome_banner")
if site_setting.value == "f"
site_setting.update!(value: "t")
puts " - #{parent_theme.name} (ID: #{parent_theme.id}) \e[32m- enabled\e[0m"
enabled_count += 1
else
puts " - #{parent_theme.name} (ID: #{parent_theme.id}) \e[33m- it was already enabled. Skipping\e[0m"
end
end
puts " \e[1;32m✓ Enabled for #{enabled_count} theme#{"s" unless enabled_count == 1}\e[0m"
end
def disable_theme_component(theme)
puts "\n Executing disable component step..."
if !theme.enabled
puts " \e[33m#{theme_identifier(theme)} was already disabled. Skipping\e[0m"
return
end
puts "\n Disabling #{theme_identifier(theme)}..."
theme.update!(enabled: false)
puts " \e[1;32m✓ Disabled\e[0m"
end
@@ -0,0 +1,202 @@
# frozen_string_literal: true
RSpec.describe "tasks/migrate_advanced_search_banner_to_welcome_banner" do
before do
Rake::Task.clear
load Rails.root.join("lib/tasks/migrate_advanced_search_banner_to_welcome_banner.rake")
end
describe "#validate_and_get_db" do
it "returns the database name if it exists" do
db = "default"
RailsMultisite::ConnectionManagement.stubs(:has_db?).with(db).returns(true)
result = validate_and_get_db(db)
expect(result).to eq(db)
end
it "returns default database when provided database does not exist" do
db = "nonexistent"
default_db = RailsMultisite::ConnectionManagement::DEFAULT
RailsMultisite::ConnectionManagement.stubs(:has_db?).with(db).returns(false)
result = validate_and_get_db(db)
expect(result).to eq(default_db)
end
end
describe "#wrap_themes_with_db" do
it "wraps themes with database information" do
theme1 = Fabricate(:theme)
theme2 = Fabricate(:theme)
db = "default"
result = wrap_themes_with_db([theme1, theme2], db)
expect(result).to eq([{ db: db, theme: theme1 }, { db: db, theme: theme2 }])
end
it "returns empty array when no themes provided" do
result = wrap_themes_with_db([], "default")
expect(result).to eq([])
end
end
describe "#theme_identifier" do
it "returns formatted theme identifier" do
theme = Fabricate(:theme, name: "Test Theme")
result = theme_identifier(theme)
expect(result).to include("Test Theme")
expect(result).to include(theme.id.to_s)
end
end
describe "#not_included_in_any_theme?" do
fab!(:parent_theme, :theme)
fab!(:child_theme) { Fabricate(:theme, component: true) }
it "returns false when theme is included in a parent theme" do
ChildTheme.create!(parent_theme: parent_theme, child_theme: child_theme)
expect(not_included_in_any_theme?(child_theme)).to eq(false)
end
it "returns true and prints message when theme is not included in any parent theme" do
orphan_theme = Fabricate(:theme, component: true)
expect {
result = not_included_in_any_theme?(orphan_theme)
expect(result).to eq(true)
}.to output(/is not included in any of your themes/).to_stdout
end
end
describe "#map_translation_keys" do
it "returns mapped keys for headline" do
result = map_translation_keys("search_banner.headline")
expect(result).to eq(
%w[js.welcome_banner.header.anonymous_members js.welcome_banner.header.logged_in_members],
)
end
it "returns mapped keys for subhead" do
result = map_translation_keys("search_banner.subhead")
expect(result).to eq(
%w[
js.welcome_banner.subheader.anonymous_members
js.welcome_banner.subheader.logged_in_members
],
)
end
it "returns empty array for unknown translation key" do
result = map_translation_keys("unknown.key")
expect(result).to eq([])
end
end
describe "SETTINGS_MAPPING" do
it "maps show_on setting correctly" do
expect(SETTINGS_MAPPING["show_on"][:site_setting]).to eq("welcome_banner_page_visibility")
expect(SETTINGS_MAPPING["show_on"][:value_mapping]["top_menu"]).to eq("top_menu_pages")
expect(SETTINGS_MAPPING["show_on"][:value_mapping]["all"]).to eq("all_pages")
end
it "maps plugin_outlet setting correctly" do
expect(SETTINGS_MAPPING["plugin_outlet"][:site_setting]).to eq("welcome_banner_location")
expect(SETTINGS_MAPPING["plugin_outlet"][:value_mapping]["above-main-container"]).to eq(
"above_topic_content",
)
expect(SETTINGS_MAPPING["plugin_outlet"][:value_mapping]["below-site-header"]).to eq(
"below_site_header",
)
end
it "maps background_image_light setting correctly" do
expect(SETTINGS_MAPPING["background_image_light"][:site_setting]).to eq(
"welcome_banner_image",
)
expect(SETTINGS_MAPPING["background_image_light"][:value_mapping]).to be_nil
end
end
describe "#exclude_theme_component" do
fab!(:parent_theme, :theme)
fab!(:child_theme) { Fabricate(:theme, component: true) }
it "excludes theme from parent themes when relations exist" do
ChildTheme.create!(parent_theme: parent_theme, child_theme: child_theme)
expect { exclude_theme_component(child_theme) }.to output(/Excluding.*from/).to_stdout
end
it "handles theme with no parent relations" do
orphan_theme = Fabricate(:theme, component: true)
expect { exclude_theme_component(orphan_theme) }.to output(
/is not included in any of your themes/,
).to_stdout
end
end
describe "#disable_theme_component" do
it "disables an enabled theme" do
theme = Fabricate(:theme, enabled: true)
expect { disable_theme_component(theme) }.to output(/Disabled/).to_stdout
expect(theme.reload.enabled).to eq(false)
end
it "skips disabling an already disabled theme" do
theme = Fabricate(:theme, enabled: false)
expect { disable_theme_component(theme) }.to output(/already disabled/).to_stdout
expect(theme.reload.enabled).to eq(false)
end
end
describe "#enable_welcome_banner" do
fab!(:parent_theme, :theme)
fab!(:child_theme) { Fabricate(:theme, component: true, enabled: true) }
before { ChildTheme.create!(parent_theme: parent_theme, child_theme: child_theme) }
it "enables welcome banner when theme is enabled and setting is false" do
ThemeSiteSetting.create!(
theme: parent_theme,
name: "enable_welcome_banner",
data_type: 5,
value: "f",
)
expect { enable_welcome_banner(child_theme) }.to output(/enabled/).to_stdout
expect(
ThemeSiteSetting.find_by(theme: parent_theme, name: "enable_welcome_banner").value,
).to eq("t")
end
it "does nothing when theme is disabled" do
disabled_theme = Fabricate(:theme, component: true, enabled: false)
result = enable_welcome_banner(disabled_theme)
expect(result).to be_nil
end
it "skips when theme has no parent relations" do
orphan_theme = Fabricate(:theme, component: true, enabled: true)
result = enable_welcome_banner(orphan_theme)
expect(result).to be_nil
end
end
end