FEATURE: rebranded admin logos settings (#31554)

Redesigned page to update site logos. `AdminBrandingLogoFormComponent`
is attached to the old logos page and the new branding page. In the next
steps, branding will replace the logos page.

A new `AdminConfigAreaCardSection` component was added hidden and less
frequently used settings.

An image placeholder was also needed because many additional logos have
a fallback to the site logo.

Finally, `twitter_summary_large_image` was renamed to
`x_summary_large_image`.

Desktop
![localhost_4200_admin_config_branding
(4)](https://github.com/user-attachments/assets/b6ae5266-72f6-4582-b0ef-4d05545943e8)


Mobile
![localhost_4200_admin_config_branding(iPhone 12 Pro)
(3)](https://github.com/user-attachments/assets/bf329a5c-9ba0-4d88-b30d-e8f1feb02e31)
This commit is contained in:
Krzysztof Kotlarek
2025-03-04 12:51:27 +11:00
committed by GitHub
parent 76e58a55ed
commit dbba838ef4
21 changed files with 1044 additions and 20 deletions

View File

@ -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,
};
}
<template>
<ConditionalLoadingSpinner @condition={{this.loading}}>
<Form
@onSubmit={{this.save}}
@data={{this.formData}}
class="admin-logo-form"
as |form transientData|
>
<form.Field
@name="logo"
@title={{i18n "admin.config.branding.logo.form.logo.title"}}
@description={{i18n
"admin.config.branding.logo.form.logo.description"
}}
@helpText={{i18n "admin.config.branding.logo.form.logo.help_text"}}
@onSet={{fn this.handleUpload "logo"}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
<form.Field
@name="logo_dark_required"
@title={{i18n "admin.config.branding.logo.form.logo_dark.required"}}
@format="full"
as |field|
>
<field.Toggle />
</form.Field>
{{#if transientData.logo_dark_required}}
<form.Section>
<form.Field
@name="logo_dark"
@title={{i18n "admin.config.branding.logo.form.logo_dark.title"}}
@helpText={{i18n
"admin.config.branding.logo.form.logo_dark.help_text"
}}
@onSet={{fn this.handleUpload "logo_dark"}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
</form.Section>
{{/if}}
<form.Field
@name="large_icon"
@title={{i18n "admin.config.branding.logo.form.large_icon.title"}}
@description={{i18n
"admin.config.branding.logo.form.large_icon.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.large_icon.help_text"
}}
@onSet={{fn this.handleUpload "large_icon"}}
@placeholderUrl={{this.placeholders.large_icon}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
<form.Field
@name="favicon"
@title={{i18n "admin.config.branding.logo.form.favicon.title"}}
@description={{i18n
"admin.config.branding.logo.form.favicon.description"
}}
@onSet={{fn this.handleUpload "favicon"}}
@placeholderUrl={{this.placeholders.favicon}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
<form.Field
@name="logo_small"
@title={{i18n "admin.config.branding.logo.form.logo_small.title"}}
@description={{i18n
"admin.config.branding.logo.form.logo_small.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.logo_small.help_text"
}}
@onSet={{fn this.handleUpload "logo_small"}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
<form.Field
@name="logo_small_dark_required"
@title={{i18n
"admin.config.branding.logo.form.logo_small_dark.required"
}}
@format="full"
as |field|
>
<field.Toggle />
</form.Field>
{{#if transientData.logo_small_dark_required}}
<form.Section>
<form.Field
@name="logo_small_dark"
@title={{i18n
"admin.config.branding.logo.form.logo_small_dark.title"
}}
@helpText={{i18n
"admin.config.branding.logo.form.logo_small_dark.help_text"
}}
@onSet={{fn this.handleUpload "logo_small_dark"}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
</form.Section>
{{/if}}
<AdminConfigAreaCardSection
@heading={{i18n "admin.config.branding.logo.form.mobile"}}
class="admin-logo-form__mobile-section"
@collapsable={{true}}
@collapsed={{true}}
>
<:content>
<form.Field
@name="mobile_logo"
@title={{i18n
"admin.config.branding.logo.form.mobile_logo.title"
}}
@description={{i18n
"admin.config.branding.logo.form.mobile_logo.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.mobile_logo.help_text"
}}
@onSet={{fn this.handleUpload "mobile_logo"}}
@placeholderUrl={{this.placeholders.mobile_logo}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
<form.Field
@name="mobile_logo_dark_required"
@title={{i18n
"admin.config.branding.logo.form.mobile_logo_dark.required"
}}
@format="full"
as |field|
>
<field.Toggle />
</form.Field>
{{#if transientData.mobile_logo_dark_required}}
<form.Section>
<form.Field
@name="mobile_logo_dark"
@title={{i18n
"admin.config.branding.logo.form.mobile_logo_dark.title"
}}
@helpText={{i18n
"admin.config.branding.logo.form.mobile_logo_dark.help_text"
}}
@onSet={{fn this.handleUpload "mobile_logo_dark"}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
</form.Section>
{{/if}}
<form.Field
@name="manifest_icon"
@title={{i18n
"admin.config.branding.logo.form.manifest_icon.title"
}}
@description={{i18n
"admin.config.branding.logo.form.manifest_icon.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.manifest_icon.help_text"
}}
@onSet={{fn this.handleUpload "manifest_icon"}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
<form.Field
@name="manifest_screenshots"
@title={{i18n
"admin.config.branding.logo.form.manifest_screenshots.title"
}}
@description={{i18n
"admin.config.branding.logo.form.manifest_screenshots.description"
}}
@format="full"
as |field|
>
<field.Custom>
<SimpleList
@id={{field.id}}
@onChange={{fn this.updateManifestScreenshots field}}
@inputDelimiter="|"
@values={{field.value}}
@allowAny={{true}}
/>
</field.Custom>
</form.Field>
<form.Field
@name="apple_touch_icon"
@title={{i18n
"admin.config.branding.logo.form.apple_touch_icon.title"
}}
@description={{i18n
"admin.config.branding.logo.form.apple_touch_icon.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.apple_touch_icon.help_text"
}}
@onSet={{fn this.handleUpload "apple_touch_icon"}}
@placeholderUrl={{this.placeholders.apple_touch_icon}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
</:content>
</AdminConfigAreaCardSection>
<AdminConfigAreaCardSection
@heading={{i18n "admin.config.branding.logo.form.email"}}
class="admin-logo-form__email-section"
@collapsable={{true}}
@collapsed={{true}}
>
<:content>
<form.Field
@name="digest_logo"
@title={{i18n
"admin.config.branding.logo.form.digest_logo.title"
}}
@description={{i18n
"admin.config.branding.logo.form.digest_logo.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.digest_logo.help_text"
}}
@onSet={{fn this.handleUpload "digest_logo"}}
@placeholderUrl={{this.placeholders.digest_logo}}
as |field|
>
<field.Image
@type="branding"
@placeholderUrl={{this.placeholders.digest_logo}}
/>
</form.Field>
</:content>
</AdminConfigAreaCardSection>
<AdminConfigAreaCardSection
@heading={{i18n "admin.config.branding.logo.form.social_media"}}
class="admin-logo-form__social-media-section"
@collapsable={{true}}
@collapsed={{true}}
>
<:content>
<form.Field
@name="opengraph_image"
@title={{i18n
"admin.config.branding.logo.form.opengraph_image.title"
}}
@description={{i18n
"admin.config.branding.logo.form.opengraph_image.description"
}}
@onSet={{fn this.handleUpload "opengraph_image"}}
@placeholderUrl={{this.placeholders.opengraph_image}}
as |field|
>
<field.Image @type="branding" />
</form.Field>
</:content>
</AdminConfigAreaCardSection>
<form.Submit />
</Form>
</ConditionalLoadingSpinner>
</template>
}

View File

@ -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;
}
<template>
<section class="admin-config-area-card-section" ...attributes>
<div class="admin-config-area-card-section__header-wrapper">
<h4 class="admin-config-area-card-section__title">{{@heading}}</h4>
{{#if @collapsable}}
<DButton
@title="sidebar.toggle_section"
@action={{this.toggleSectionDisplay}}
class="admin-config-area-card-section__toggle-button btn-transparent"
>
{{icon this.headerCaretIcon}}
</DButton>
{{/if}}
</div>
{{#unless this.collapsed}}
<div class="admin-config-area-card-section__content">
{{yield to="content"}}
</div>
{{/unless}}
</section>
</template>
}

View File

@ -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: "/" });
});

View File

@ -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(<template>
<div class="admin-config-page">
<DPageHeader
@hideTabs={{true}}
@titleLabel={{i18n "admin.config.branding.title"}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/config/branding"
@label={{i18n "admin.config.branding.title"}}
/>
</:breadcrumbs>
</DPageHeader>
<div class="admin-config-area">
<div class="admin-config-area__primary-content">
<AdminConfigAreaCard
@heading="admin.config.branding.logo.title"
@collapsable={{true}}
class="admin-config-area-branding__logo"
>
<:content>
<AdminBrandingLogoForm />
</:content>
</AdminConfigAreaCard>
</div>
</div>
</div>
</template>);

View File

@ -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(<template>
<DPageHeader
@ -19,13 +20,13 @@ export default RouteTemplate(<template>
</:breadcrumbs>
</DPageHeader>
<div class="admin-config-page__main-area">
<AdminAreaSettings
@showBreadcrumb={{false}}
@categories="branding"
@path="/admin/config/logo"
@filter={{@controller.filter}}
@adminSettingsFilterChangedCallback={{@controller.adminSettingsFilterChangedCallback}}
/>
</div>
<AdminConfigAreaCard
@heading="admin.config.branding.logo.title"
@collapsable={{true}}
class="admin-config-area-branding__logo"
>
<:content>
<AdminBrandingLogoForm />
</:content>
</AdminConfigAreaCard>
</template>);

View File

@ -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"
/>
</template>

View File

@ -90,6 +90,7 @@ export default class FKField extends Component {
@descriptionFormat={{@descriptionFormat}}
@disabled={{@disabled}}
@parentName={{@parentName}}
@placeholderUrl={{@placeholderUrl}}
as |field|
>
<this.wrapper @size={{@size}}>

View File

@ -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(<template>
<AdminConfigAreaCardSection @heading="test heading"><:content
>test</:content></AdminConfigAreaCardSection>
</template>);
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(<template>
<AdminConfigAreaCardSection
@heading="test heading"
@collapsable={{true}}
><:content>test</:content></AdminConfigAreaCardSection>
</template>);
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(<template>
<AdminConfigAreaCardSection
@heading="test heading"
@collapsable={{true}}
@collapsed={{true}}
><:content>test</:content></AdminConfigAreaCardSection>
</template>);
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();
});
}
);

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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"

View File

@ -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 <br><a href="https://meta.discourse.org/t/76575">https://meta.discourse.org/t/76575</a>'
@ -1834,7 +1834,7 @@ en:
favicon: "A favicon for your site, see <a href='https://en.wikipedia.org/wiki/Favicon' target='_blank'>https://en.wikipedia.org/wiki/Favicon</a>. 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"

View File

@ -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"

View File

@ -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:

View File

@ -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

View File

@ -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[

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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