diff --git a/app/assets/javascripts/admin/addon/components/admin-branding-logo-form.gjs b/app/assets/javascripts/admin/addon/components/admin-branding-logo-form.gjs new file mode 100644 index 00000000000..e64a88fac99 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-branding-logo-form.gjs @@ -0,0 +1,396 @@ +import Component from "@glimmer/component"; +import { cached, tracked } from "@glimmer/tracking"; +import { fn } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner"; +import Form from "discourse/components/form"; +import { ajax } from "discourse/lib/ajax"; +import { bind } from "discourse/lib/decorators"; +import getURL from "discourse/lib/get-url"; +import { i18n } from "discourse-i18n"; +import AdminConfigAreaCardSection from "admin/components/admin-config-area-card-section"; +import SimpleList from "admin/components/simple-list"; + +export default class AdminBrandingLogoForm extends Component { + @service siteSettings; + @service toasts; + @tracked placeholders = {}; + @tracked loading = false; + + constructor() { + super(...arguments); + this.#loadPlaceholders(); + } + + @bind + async #loadPlaceholders() { + this.loading = true; + try { + const result = await ajax("/admin/config/site_settings.json", { + data: { + categories: ["branding"], + }, + }); + + result.site_settings.forEach((setting) => { + if (setting.placeholder) { + this.placeholders[setting.setting] = setting.placeholder; + } + }); + } finally { + this.loading = false; + } + } + + @action + handleUpload(type, upload, { set }) { + if (upload) { + set(type, getURL(upload.url)); + } else { + set(type, undefined); + } + } + + @action + async save(data) { + try { + await ajax("/admin/config/branding/logo.json", { + type: "PUT", + data: { + logo: data.logo, + logo_dark: data.logo_dark, + large_icon: data.large_icon, + favicon: data.favicon, + logo_small: data.logo_small, + logo_small_dark: data.logo_small_dark, + mobile_logo: data.mobile_logo, + mobile_logo_dark: data.mobile_logo_dark, + manifest_icon: data.manifest_icon, + manifest_screenshots: data.manifest_screenshots, + apple_touch_icon: data.apple_touch_icon, + digest_logo: data.digest_logo, + opengraph_image: data.opengraph_image, + x_summary_large_image: data.x_summary_large_image, + }, + }); + this.toasts.success({ + duration: 3000, + data: { + message: i18n("admin.config.branding.logo.form.saved"), + }, + }); + } catch (err) { + this.toasts.error({ + duration: 3000, + data: { + message: err.jqXHR.responseJSON.errors[0], + }, + }); + } + } + + @action + updateManifestScreenshots(field, selected) { + field.set(selected.join("|")); + } + + @cached + get formData() { + return { + logo: this.siteSettings.logo, + logo_dark_required: !!this.siteSettings.logo_dark, + logo_dark: this.siteSettings.logo_dark, + large_icon: this.siteSettings.large_icon, + favicon: this.siteSettings.favicon, + logo_small: this.siteSettings.logo_small, + logo_small_dark_required: !!this.siteSettings.logo_small_dark, + logo_small_dark: this.siteSettings.logo_small_dark, + mobile_logo: this.siteSettings.mobile_logo, + mobile_logo_dark_required: !!this.siteSettings.mobile_logo_dark, + mobile_logo_dark: this.siteSettings.mobile_logo_dark, + manifest_icon: this.siteSettings.manifest_icon, + manifest_screenshots: this.siteSettings.manifest_screenshots, + apple_touch_icon: this.siteSettings.apple_touch_icon, + digest_logo: this.siteSettings.digest_logo, + opengraph_image: this.siteSettings.opengraph_image, + x_summary_large_image: this.siteSettings.x_summary_large_image, + }; + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/admin-config-area-card-section.gjs b/app/assets/javascripts/admin/addon/components/admin-config-area-card-section.gjs new file mode 100644 index 00000000000..96e1e5470f6 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-config-area-card-section.gjs @@ -0,0 +1,40 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import DButton from "discourse/components/d-button"; +import icon from "discourse/helpers/d-icon"; + +export default class AdminConfigAreaCardSection extends Component { + @tracked collapsed = this.args.collapsed; + + get headerCaretIcon() { + return this.collapsed ? "plus" : "minus"; + } + + @action + toggleSectionDisplay() { + this.collapsed = !this.collapsed; + } + + +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index 9034a5352cf..7f909c1a5be 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -316,6 +316,7 @@ export default function () { this.route("logo", function () { this.route("settings", { path: "/" }); }); + this.route("branding"); this.route("navigation", function () { this.route("settings", { path: "/" }); }); diff --git a/app/assets/javascripts/admin/addon/templates/config-branding.gjs b/app/assets/javascripts/admin/addon/templates/config-branding.gjs new file mode 100644 index 00000000000..5e521a8e901 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/config-branding.gjs @@ -0,0 +1,36 @@ +import RouteTemplate from "ember-route-template"; +import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; +import DPageHeader from "discourse/components/d-page-header"; +import { i18n } from "discourse-i18n"; +import AdminBrandingLogoForm from "admin/components/admin-branding-logo-form"; +import AdminConfigAreaCard from "admin/components/admin-config-area-card"; + +export default RouteTemplate(); diff --git a/app/assets/javascripts/admin/addon/templates/config-logo-settings.gjs b/app/assets/javascripts/admin/addon/templates/config-logo-settings.gjs index d4931b8a074..4a803a4a307 100644 --- a/app/assets/javascripts/admin/addon/templates/config-logo-settings.gjs +++ b/app/assets/javascripts/admin/addon/templates/config-logo-settings.gjs @@ -2,7 +2,8 @@ import RouteTemplate from "ember-route-template"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; import DPageHeader from "discourse/components/d-page-header"; import { i18n } from "discourse-i18n"; -import AdminAreaSettings from "admin/components/admin-area-settings"; +import AdminBrandingLogoForm from "admin/components/admin-branding-logo-form"; +import AdminConfigAreaCard from "admin/components/admin-config-area-card"; export default RouteTemplate(); diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs index db2a911befa..4f378c731e8 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/image.gjs @@ -28,6 +28,7 @@ export default class FKControlImage extends Component { @onUploadDeleted={{this.removeImage}} @type={{@type}} @disabled={{@field.disabled}} + @placeholderUrl={{@field.args.placeholderUrl}} class="form-kit__control-image no-repeat contain-image" /> diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs index dff10f875aa..89c89d5a4f2 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/field.gjs @@ -90,6 +90,7 @@ export default class FKField extends Component { @descriptionFormat={{@descriptionFormat}} @disabled={{@disabled}} @parentName={{@parentName}} + @placeholderUrl={{@placeholderUrl}} as |field| > diff --git a/app/assets/javascripts/discourse/tests/integration/components/admin-config-area-card-section-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/admin-config-area-card-section-test.gjs new file mode 100644 index 00000000000..4aa85277b84 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/admin-config-area-card-section-test.gjs @@ -0,0 +1,61 @@ +import { click, render } from "@ember/test-helpers"; +import { module, test } from "qunit"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import AdminConfigAreaCardSection from "admin/components/admin-config-area-card-section"; + +module( + "Integration | Component | AdminConfigAreaCardSection", + function (hooks) { + setupRenderingTest(hooks); + + test("renders admin config area card section without toggle button", async function (assert) { + await render(); + + assert + .dom(".admin-config-area-card-section__title") + .hasText("test heading"); + assert.dom(".admin-config-area-card-section__content").exists(); + assert + .dom(".admin-config-area-card-section__toggle-button") + .doesNotExist(); + }); + + test("renders admin config area card section with toggle button", async function (assert) { + await render(); + + assert + .dom(".admin-config-area-card-section__title") + .hasText("test heading"); + assert.dom(".admin-config-area-card-section__content").hasText("test"); + assert.dom(".admin-config-area-card-section__toggle-button").exists(); + + await click(".admin-config-area-card-section__toggle-button"); + assert.dom(".admin-config-area-card-section__content").doesNotExist(); + + await click(".admin-config-area-card-section__toggle-button"); + assert.dom(".admin-config-area-card-section__content").exists(); + }); + + test("renders admin config area card section with toggle button and collapsed by default", async function (assert) { + await render(); + + assert.dom(".admin-config-area-card-section__title").exists(); + assert.dom(".admin-config-area-card-section__toggle-button").exists(); + assert.dom(".admin-config-area-card-section__content").doesNotExist(); + }); + } +); diff --git a/app/assets/stylesheets/admin/admin_config_area.scss b/app/assets/stylesheets/admin/admin_config_area.scss index bcd9d7f7bb7..62750cc83de 100644 --- a/app/assets/stylesheets/admin/admin_config_area.scss +++ b/app/assets/stylesheets/admin/admin_config_area.scss @@ -123,3 +123,46 @@ margin-top: 1em; } } + +.admin-config-area-card-section { + display: flex; + flex-wrap: nowrap; + gap: var(--space-4); + flex-direction: column; + width: 100%; + border: 1px solid var(--primary-low); + + @media (max-width: $mobile-breakpoint) { + flex-direction: column; + } + + &__header-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + padding-left: 0.5em; + } + + &__title { + margin-bottom: 0; + } + + &__content { + margin-top: 0.5rem; + padding-right: 1em; + padding-left: 0.5em; + padding-bottom: 1em; + gap: 1.5em; + display: flex; + flex-direction: column; + } +} + +.admin-logo-form { + .form-kit__section { + .form-kit__container { + padding-left: 1em; + width: calc(100% - 1em) !important; + } + } +} diff --git a/app/controllers/admin/config/branding_controller.rb b/app/controllers/admin/config/branding_controller.rb new file mode 100644 index 00000000000..4602d65b9a8 --- /dev/null +++ b/app/controllers/admin/config/branding_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +class Admin::Config::BrandingController < Admin::AdminController + def index + end + + def logo + SiteSetting::Update.call( + guardian:, + params: { + settings: { + logo: params[:logo], + logo_dark: params[:logo_dark], + large_icon: params[:large_icon], + favicon: params[:favicon], + logo_small: params[:logo_small], + logo_small_dark: params[:logo_small_dark], + mobile_logo: params[:mobile_logo], + mobile_logo_dark: params[:mobile_logo_dark], + manifest_icon: params[:manifest_icon], + manifest_screenshots: params[:manifest_screenshots], + apple_touch_icon: params[:apple_touch_icon], + digest_logo: params[:digest_logo], + opengraph_image: params[:opengraph_image], + x_summary_large_image: params[:x_summary_large_image], + }, + }, + ) do + on_success { render json: success_json } + on_failed_policy(:settings_are_visible) do |policy| + raise Discourse::InvalidParameters, policy.reason + end + on_failed_policy(:settings_are_unshadowed_globally) do |policy| + raise Discourse::InvalidParameters, policy.reason + end + on_failed_policy(:settings_are_configurable) do |policy| + raise Discourse::InvalidParameters, policy.reason + end + on_failed_policy(:values_are_valid) do |policy| + raise Discourse::InvalidParameters, policy.reason + end + end + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 76458e933ff..624780d4a6e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -5232,6 +5232,72 @@ en: logo: title: "Site logo" header_description: "Customize the variations of your site logo" + branding: + title: "Branding" + logo: + title: "Logo" + form: + saved: "Logo settings are saved." + logo: + title: "Primary logo" + description: "Appears on the site's top navigation, as well as the top of the site's Email Notifications." + help_text: "Recommended size is 600 x 80 pixels." + logo_dark: + required: "Use a different logo for dark mode?" + title: "Primary logo dark" + help_text: "Recommended size is 600 x 80 pixels." + large_icon: + title: "Square icon" + description: "A squared version of the logo image appears at the top of the administration and is also the mobile home screen app logo." + help_text: "Recommended size is 512 x 512 pixels." + square_icon_dark: + required: "Use a different square icon for dark mode?" + title: "Square icon dark" + help_text: "Recommended size is 512 x 512 pixels." + favicon: + title: "Favicon" + description: "The logo will appear as the icon in the browser tab and the browser favorites/bookmarks." + logo_small: + title: "Small logo" + description: "The small logo image at the top left of your site, seen when scrolling down. If left blank, a home glyph will be shown." + help_text: "Recommended size is 120 x 120 pixels." + logo_small_dark: + required: "Use a different small logo for dark mode?" + title: "Small logo dark" + help_text: "Recommended size is 120 x 120 pixels." + mobile: "Mobile" + email: "Email" + social_media: "Social media" + mobile_logo: + title: "Mobile logo" + description: "The logo used on mobile version of your site. If left blank, the image from the `logo` setting will be used." + help_text: "Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1." + mobile_logo_dark: + required: "Use a different mobile logo for dark mode?" + title: "Mobile logo dark" + help_text: "Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1." + manifest_icon: + title: "Manifest icon" + description: "Image used as logo/splash image on Android. If left blank, large_icon will be used." + help_text: "Recommended size is 512 x 512 pixels." + manifest_screenshots: + title: "Manifest screenshots" + description: "Screenshots that showcase your instance features and functionality on its install prompt page. All images should be local uploads and of the same dimensions." + apple_touch_icon: + title: "Apple touch icon" + description: "Icon used for Apple touch devices. If left blank, large_icon will be used." + help_text: "Recommended size is 180 x 180 pixels. A transparent background is not recommended." + digest_logo: + title: "Digest logo" + description: "The alternate logo image used at the top of your site's email summary. If left blank, the image from the `logo` setting will be used." + help_text: "Use a wide rectangle image. Don't use an SVG image." + opengraph_image: + title: "OpenGraph image" + description: "Default opengraph image, used when the page has no other suitable image. If left blank, large_icon will be used." + x_summary_large_image: + title: "X summary large image" + description: "X card 'summary large image'. If left blank, regular card metadata is generated using the OpenGraph_image, as long as that is not also a .svg." + help_text: "recommended size is at least 280 x 150 pixels. Don't use an SVG image." navigation: title: "Navigation" header_description: "Configure the navigation links and menu items for your site. This includes the location and behavior of the primary navigation menu, the quick links at the top of the homepage, as well as the admin sidebar" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 50ff4d9550c..9868dff8f54 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -365,7 +365,7 @@ en: slow_down_crawler_user_agent_must_be_at_least_3_characters: "User agents must be at least 3 characters long to avoid accidentally rate-limiting legitimate users." slow_down_crawler_user_agent_cannot_be_popular_browsers: "You cannot add any of the following values to the setting: %{values}." strip_image_metadata_cannot_be_disabled_if_composer_media_optimization_image_enabled: "You cannot disable strip image metadata if 'composer media optimization image enabled' is enabled. Disable 'composer media optimization image enabled' before disabling strip image metadata." - twitter_summary_large_image_no_svg: "Twitter summary images used for twitter:image metadata cannot be an .svg image." + x_summary_large_image_no_svg: "Twitter summary images used for twitter:image metadata cannot be an .svg image." tl0_and_anonymous_flag: "Either 'site contact email' or 'email address to report illegal content' must be provided for anonymous users." conflicting_google_user_id: 'The Google Account ID for this account has changed; staff intervention is required for security reasons. Please contact staff and point them to
https://meta.discourse.org/t/76575' @@ -1834,7 +1834,7 @@ en: favicon: "A favicon for your site, see https://en.wikipedia.org/wiki/Favicon. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, large_icon will be used." apple_touch_icon: "Icon used for Apple touch devices. A transparent background is not reccomended. Will be automatically resized to 180x180. If left blank, large_icon will be used." opengraph_image: "Default opengraph image, used when the page has no other suitable image. If left blank, large_icon will be used" - twitter_summary_large_image: "Twitter card 'summary large image' (should be at least 280 in width, and at least 150 in height, cannot be .svg). If left blank, regular card metadata is generated using the opengraph_image, as long as that is not also a .svg" + x_summary_large_image: "Twitter card 'summary large image' (should be at least 280 in width, and at least 150 in height, cannot be .svg). If left blank, regular card metadata is generated using the opengraph_image, as long as that is not also a .svg" notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive." email_custom_headers: "A pipe-delimited list of custom email headers" diff --git a/config/routes.rb b/config/routes.rb index b12358876c9..a98fc44c3fe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -412,6 +412,8 @@ Discourse::Application.routes.draw do get "experimental" => "site_settings#index" get "trust-levels" => "site_settings#index" get "group-permissions" => "site_settings#index" + get "branding" => "branding#index" + put "branding/logo" => "branding#logo" resources :flags, only: %i[index new create update destroy] do put "toggle" diff --git a/config/site_settings.yml b/config/site_settings.yml index b077bcd2865..db6a48789a8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -125,10 +125,12 @@ branding: type: upload manifest_icon: default: "" + client: true type: upload manifest_screenshots: type: list list_type: simple + client: true default: "" favicon: default: "" @@ -140,10 +142,16 @@ branding: type: upload opengraph_image: default: "" + client: true type: upload twitter_summary_large_image: default: "" type: upload + hidden: true + x_summary_large_image: + default: "" + type: upload + client: true basic: display_local_time_in_user_card: diff --git a/db/migrate/20250217003916_x_summary_large_image_based_on_deprecated_setting.rb b/db/migrate/20250217003916_x_summary_large_image_based_on_deprecated_setting.rb new file mode 100644 index 00000000000..13b3f224ad6 --- /dev/null +++ b/db/migrate/20250217003916_x_summary_large_image_based_on_deprecated_setting.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class XSummaryLargeImageBasedOnDeprecatedSetting < ActiveRecord::Migration[7.2] + def up + old_setting = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'twitter_summary_large_image' LIMIT 1", + ).first + + if old_setting.present? + DB.exec( + "INSERT INTO site_settings(name, value, data_type, created_at, updated_at) + VALUES('x_summary_large_image', :setting, '18', NOW(), NOW())", + setting: old_setting, + ) + end + end + + def down + old_setting = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'x_summary_large_image' LIMIT 1", + ).first + + if old_setting.present? + DB.exec( + "INSERT INTO site_settings(name, value, data_type, created_at, updated_at) + VALUES('twitter_summary_large_image', :setting, '18', NOW(), NOW()) + ON CONFLICT DO UPDATE", + setting: old_setting, + ) + end + end +end diff --git a/lib/site_settings/deprecated_settings.rb b/lib/site_settings/deprecated_settings.rb index 69c6d471a15..a469aa0130e 100644 --- a/lib/site_settings/deprecated_settings.rb +++ b/lib/site_settings/deprecated_settings.rb @@ -45,7 +45,7 @@ module SiteSettings::DeprecatedSettings false, "3.3", ], - ["min_first_post_typing_time", "fast_typing_threshold", false, "3.4"], + ["twitter_summary_large_image", "x_summary_large_image", false, "3.4"], ] OVERRIDE_TL_GROUP_SETTINGS = %w[ diff --git a/lib/site_settings/validations.rb b/lib/site_settings/validations.rb index cbd5b7f36af..e20a895df87 100644 --- a/lib/site_settings/validations.rb +++ b/lib/site_settings/validations.rb @@ -261,10 +261,10 @@ module SiteSettings::Validations validate_error :strip_image_metadata_cannot_be_disabled_if_composer_media_optimization_image_enabled end - def validate_twitter_summary_large_image(new_val) + def validate_x_summary_large_image(new_val) return if new_val.blank? return if !Upload.exists?(id: new_val, extension: "svg") - validate_error :twitter_summary_large_image_no_svg + validate_error :x_summary_large_image_no_svg end def validate_allow_all_users_to_flag_illegal_content(new_val) diff --git a/spec/lib/site_settings/validations_spec.rb b/spec/lib/site_settings/validations_spec.rb index f8d0c911e44..47d9f4bc4d2 100644 --- a/spec/lib/site_settings/validations_spec.rb +++ b/spec/lib/site_settings/validations_spec.rb @@ -462,16 +462,16 @@ RSpec.describe SiteSettings::Validations do end end - describe "#twitter_summary_large_image" do + describe "#x_summary_large_image" do it "does not allow SVG image files" do upload = Fabricate(:upload, url: "/images/logo-dark.svg", extension: "svg") - expect { validations.validate_twitter_summary_large_image(upload.id) }.to raise_error( + expect { validations.validate_x_summary_large_image(upload.id) }.to raise_error( Discourse::InvalidParameters, - I18n.t("errors.site_settings.twitter_summary_large_image_no_svg"), + I18n.t("errors.site_settings.x_summary_large_image_no_svg"), ) upload.update!(url: "/images/logo-dark.png", extension: "png") - expect { validations.validate_twitter_summary_large_image(upload.id) }.not_to raise_error - expect { validations.validate_twitter_summary_large_image(nil) }.not_to raise_error + expect { validations.validate_x_summary_large_image(upload.id) }.not_to raise_error + expect { validations.validate_x_summary_large_image(nil) }.not_to raise_error end end diff --git a/spec/system/admin_branding_spec.rb b/spec/system/admin_branding_spec.rb new file mode 100644 index 00000000000..43d705f9d4d --- /dev/null +++ b/spec/system/admin_branding_spec.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +describe "Admin Branding Page", type: :system do + fab!(:admin) + fab!(:image_upload) + + let(:branding_page) { PageObjects::Pages::AdminBranding.new } + let(:image_file) { file_from_fixtures("logo.png", "images") } + + before { sign_in(admin) } + + describe "primary section" do + let(:primary_section_logos) { %i[logo logo_dark large_icon favicon logo_small logo_small_dark] } + it "can upload images and dark versions" do + branding_page.visit + + expect(branding_page.logo_form).to have_no_form_field(:logo_dark) + branding_page.logo_form.toggle_dark_mode(:logo_dark_required) + expect(branding_page.logo_form).to have_form_field(:logo_dark) + + expect(branding_page.logo_form).to have_no_form_field(:logo_small_dark) + branding_page.logo_form.toggle_dark_mode(:logo_small_dark_required) + expect(branding_page.logo_form).to have_form_field(:logo_small_dark) + + primary_section_logos.each do |image_type| + branding_page.logo_form.upload_image(image_type, image_file) + end + + primary_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + visit("/") + branding_page.visit + + expect(branding_page.logo_form).to have_form_field(:logo_dark) + expect(branding_page.logo_form).to have_form_field(:logo_small_dark) + + primary_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + end + + it "can remove images" do + primary_section_logos.each { |image_type| SiteSetting.send("#{image_type}=", image_upload) } + + branding_page.visit + + primary_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + primary_section_logos.each { |image_type| branding_page.logo_form.remove_image(image_type) } + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + primary_section_logos.each { |image_type| expect(SiteSetting.send(image_type)).to eq(nil) } + end + end + + describe "mobile section" do + let(:mobile_section_logos) { %i[mobile_logo mobile_logo_dark manifest_icon apple_touch_icon] } + it "can upload images and dark versions" do + branding_page.visit + branding_page.logo_form.expand_mobile_section + + expect(branding_page.logo_form).to have_no_form_field(:mobile_logo_dark) + branding_page.logo_form.toggle_dark_mode(:mobile_logo_dark_required) + expect(branding_page.logo_form).to have_form_field(:mobile_logo_dark) + + mobile_section_logos.each do |image_type| + branding_page.logo_form.upload_image(image_type, image_file) + end + + mobile_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + visit("/") + branding_page.visit + branding_page.logo_form.expand_mobile_section + + expect(branding_page.logo_form).to have_form_field(:mobile_logo_dark) + + mobile_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + end + + it "can remove images" do + mobile_section_logos.each { |image_type| SiteSetting.send("#{image_type}=", image_upload) } + + branding_page.visit + branding_page.logo_form.expand_mobile_section + + mobile_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + mobile_section_logos.each { |image_type| branding_page.logo_form.remove_image(image_type) } + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + mobile_section_logos.each { |image_type| expect(SiteSetting.send(image_type)).to eq(nil) } + end + end + + describe "email section" do + let(:email_section_logos) { %i[digest_logo] } + it "can upload images" do + branding_page.visit + branding_page.logo_form.expand_email_section + + email_section_logos.each do |image_type| + branding_page.logo_form.upload_image(image_type, image_file) + end + + email_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + visit("/") + branding_page.visit + branding_page.logo_form.expand_email_section + + email_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + end + + it "can remove images" do + email_section_logos.each { |image_type| SiteSetting.send("#{image_type}=", image_upload) } + + branding_page.visit + branding_page.logo_form.expand_email_section + + email_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + email_section_logos.each { |image_type| branding_page.logo_form.remove_image(image_type) } + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + email_section_logos.each { |image_type| expect(SiteSetting.send(image_type)).to eq(nil) } + end + end + + describe "social media section" do + let(:social_media_section_logos) { %i[opengraph_image] } + it "can upload images" do + branding_page.visit + branding_page.logo_form.expand_social_media_section + + social_media_section_logos.each do |image_type| + branding_page.logo_form.upload_image(image_type, image_file) + end + + social_media_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + visit("/") + branding_page.visit + branding_page.logo_form.expand_social_media_section + + social_media_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + end + + it "can remove images" do + social_media_section_logos.each do |image_type| + SiteSetting.send("#{image_type}=", image_upload) + end + + branding_page.visit + branding_page.logo_form.expand_social_media_section + + social_media_section_logos.each do |image_type| + expect(branding_page.logo_form.image_uploader(image_type)).to have_uploaded_image + end + + social_media_section_logos.each do |image_type| + branding_page.logo_form.remove_image(image_type) + end + + branding_page.logo_form.submit + expect(branding_page.logo_form).to have_saved_successfully + + social_media_section_logos.each do |image_type| + expect(SiteSetting.send(image_type)).to eq(nil) + end + end + end +end diff --git a/spec/system/page_objects/components/admin_branding_logo_form.rb b/spec/system/page_objects/components/admin_branding_logo_form.rb new file mode 100644 index 00000000000..c1ce73e8799 --- /dev/null +++ b/spec/system/page_objects/components/admin_branding_logo_form.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module PageObjects + module Components + class AdminBrandingLogoForm < PageObjects::Components::Base + def image_uploader(image_type) + PageObjects::Components::UppyImageUploader.new( + find(".form-kit__container[data-name='#{image_type}']"), + ) + end + + def upload_image(image_type, image_file) + image_uploader(image_type).select_image_with_keyboard(image_file.path) + end + + def remove_image(image_type) + image_uploader(image_type).remove_image + end + + def has_no_form_field?(field) + page.has_no_css?("#control-#{field}") + end + + def has_form_field?(field) + page.has_css?("#control-#{field}") + end + + def toggle_dark_mode(field) + PageObjects::Components::DToggleSwitch.new( + ".form-kit__field-toggle[data-name='#{field}'] .d-toggle-switch button", + ).toggle + end + + def expand_mobile_section + find( + ".admin-logo-form__mobile-section .admin-config-area-card-section__toggle-button", + ).click + end + + def expand_email_section + find(".admin-logo-form__email-section .admin-config-area-card-section__toggle-button").click + end + + def expand_social_media_section + find( + ".admin-logo-form__social-media-section .admin-config-area-card-section__toggle-button", + ).click + end + + def submit + form.submit + end + + def has_saved_successfully? + PageObjects::Components::Toasts.new.has_success?( + I18n.t("admin_js.admin.config.branding.logo.form.saved"), + ) + end + + def form + @form ||= PageObjects::Components::FormKit.new(".admin-logo-form") + end + end + end +end diff --git a/spec/system/page_objects/pages/admin_branding.rb b/spec/system/page_objects/pages/admin_branding.rb new file mode 100644 index 00000000000..972ebe4d9dd --- /dev/null +++ b/spec/system/page_objects/pages/admin_branding.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module PageObjects + module Pages + class AdminBranding < PageObjects::Pages::Base + def visit + page.visit("/admin/config/branding") + end + + def logo_form + @logo_form ||= PageObjects::Components::AdminBrandingLogoForm.new + end + end + end +end