mirror of
https://github.com/discourse/discourse.git
synced 2025-10-03 17:21:20 +08:00
FEATURE: Themeable site settings (#32233)
This commit introduces the concept of themeable site settings, which is a new tool for theme authors that lives alongside theme modifiers and theme settings. Here is a quick summary: * Theme settings - These are custom settings used to control UI and functionality within your theme or component and provide configuration options. These cannot change core Discourse functionality. * Theme modifiers - Allows a theme or a component to modify selected server-side functionality of core Discourse as an alternative to building a plugin. * Themeable site settings (new) - Allows a theme (not components) to override a small subset of core site settings, which generally control parts of the UI and other minor functionality. This allows themes to have a greater control over the full site experience. Themeable site settings will be shown for all themes, whether the theme changes the value or not, and have a similar UI to custom theme settings. We are also introducing a new page at `/admin/config/theme-site-settings` that allows admins to see all possible themeable site settings, and which themes are changing the value from the default. ### Configuration Theme authors can configure initial values themeable site settings using a section in the `about.json` file like so: ```json "theme_site_settings": { "search_experience": "search_field" } ``` These values will not change when the theme updates, because we cannot know if admins have manually changed them. ### Limitations Themeable site settings are only really intended to control elements of the UI, and when retrieving their value we require a theme ID, so these limitations apply: - Themeable site settings cannot be used in Sidekiq jobs - Themeable site settings cannot be used in markdown rules - Themeable site settings will be cached separately to client site settings using theme ID as a key - Themeable site settings will override keys on the `siteSettings` service on the client using the application preloader - `SiteSetting.client_settings_json` will not include themeable site settings, instead you can call `SiteSetting.theme_site_settings_json` with a theme ID ### Initial settings There are only two site settings that will be themeable to begin with: * `enable_welcome_banner` * `search_experience` And our new Horizon theme will take advantage of both. Over time, more settings that control elements of the UI will be exposed this way.
This commit is contained in:
parent
22f061f36e
commit
19af83d39e
63 changed files with 2191 additions and 126 deletions
|
@ -26,6 +26,7 @@ export default class AdminConfigAreaEmptyList extends Component {
|
|||
}}
|
||||
@action={{@ctaAction}}
|
||||
@route={{@ctaRoute}}
|
||||
@routeModels={{@ctaRouteModels}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{yield}}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { service } from "@ember/service";
|
|||
import { isPresent } from "@ember/utils";
|
||||
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import getURL from "discourse/helpers/get-url";
|
||||
import lazyHash from "discourse/helpers/lazy-hash";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import InstallThemeModal from "admin/components/modal/install-theme";
|
||||
|
@ -90,6 +91,7 @@ export default class AdminConfigAreasThemes extends Component {
|
|||
}}
|
||||
@descriptionLabel={{i18n
|
||||
"admin.config_areas.themes_and_components.themes.description"
|
||||
themeSiteSettingsUrl=(getURL "/admin/config/theme-site-settings")
|
||||
}}
|
||||
>
|
||||
<:actions as |actions|>
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
import { service } from "@ember/service";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import SiteSettingComponent from "./site-setting";
|
||||
|
||||
export default class extends SiteSettingComponent {
|
||||
export default class ThemeSettingEditor extends SiteSettingComponent {
|
||||
@service toasts;
|
||||
|
||||
_save() {
|
||||
return this.setting.updateSetting(
|
||||
this.args.model.id,
|
||||
this.buffered.get("value")
|
||||
);
|
||||
return this.setting
|
||||
.updateSetting(this.args.model.id, this.buffered.get("value"))
|
||||
.then(() => {
|
||||
this.toasts.success({
|
||||
data: {
|
||||
message: i18n("admin.customize.theme.theme_setting_saved"),
|
||||
},
|
||||
duration: "short",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { service } from "@ember/service";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import SiteSettingComponent from "./site-setting";
|
||||
|
||||
export default class ThemeSiteSettingEditor extends SiteSettingComponent {
|
||||
@service toasts;
|
||||
|
||||
_save() {
|
||||
return this.setting
|
||||
.updateSetting(this.args.model.id, this.buffered.get("value"))
|
||||
.then(() => {
|
||||
this.toasts.success({
|
||||
data: {
|
||||
message: i18n("admin.customize.theme.theme_site_setting_saved"),
|
||||
},
|
||||
duration: "short",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { array } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import { eq } from "truth-helpers";
|
||||
import AsyncContent from "discourse/components/async-content";
|
||||
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||
import basePath from "discourse/helpers/base-path";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { currentThemeId, listThemes } from "discourse/lib/theme-selector";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
|
||||
export default class ThemeSiteSettings extends Component {
|
||||
@service site;
|
||||
@service router;
|
||||
|
||||
@tracked themesWithSiteSettingOverrides = null;
|
||||
@tracked themeableSiteSettings = null;
|
||||
|
||||
get themes() {
|
||||
return listThemes(this.site);
|
||||
}
|
||||
|
||||
get currentThemeIdValue() {
|
||||
return currentThemeId();
|
||||
}
|
||||
|
||||
get currentTheme() {
|
||||
return this.themes.find((theme) => {
|
||||
return eq(theme.id, this.currentThemeIdValue);
|
||||
});
|
||||
}
|
||||
|
||||
isLastThemeSettingOverride(overrides, theme) {
|
||||
return theme === overrides.themes.at(-1);
|
||||
}
|
||||
|
||||
@action
|
||||
async loadThemeSiteSettings() {
|
||||
let url = "/admin/config/theme-site-settings.json";
|
||||
const response = await ajax(url, {
|
||||
method: "GET",
|
||||
});
|
||||
this.themeableSiteSettings = response.themeable_site_settings.map(
|
||||
(setting) => {
|
||||
return {
|
||||
name: setting.humanized_name,
|
||||
value: setting,
|
||||
};
|
||||
}
|
||||
);
|
||||
this.themesWithSiteSettingOverrides =
|
||||
response.themes_with_site_setting_overrides;
|
||||
return this.themesWithSiteSettingOverrides;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="theme-site-settings">
|
||||
<AsyncContent @asyncData={{this.loadThemeSiteSettings}}>
|
||||
<:content as |content|>
|
||||
<DPageSubheader
|
||||
@descriptionLabel={{i18n
|
||||
"admin.theme_site_settings.help"
|
||||
currentTheme=this.currentTheme.name
|
||||
basePath=basePath
|
||||
currentThemeId=this.currentThemeIdValue
|
||||
}}
|
||||
/>
|
||||
<table class="d-admin-table admin-theme-site-settings">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "admin.theme_site_settings.setting"}}</th>
|
||||
<th>{{i18n "admin.theme_site_settings.default_value"}}</th>
|
||||
<th>{{i18n "admin.theme_site_settings.overridden_by"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each-in content as |settingName overrides|}}
|
||||
<tr
|
||||
class="admin-theme-site-settings-row d-admin-row__content"
|
||||
data-setting-name={{settingName}}
|
||||
>
|
||||
<td class="admin-theme-site-settings-row__setting">
|
||||
<p class="setting-label">{{overrides.humanized_name}}</p>
|
||||
<div
|
||||
class="setting-description"
|
||||
>{{overrides.description}}</div>
|
||||
</td>
|
||||
<td class="admin-theme-site-settings-row__default">
|
||||
{{overrides.default}}
|
||||
</td>
|
||||
<td class="admin-theme-site-settings-row__overridden">
|
||||
{{#each overrides.themes as |theme|}}
|
||||
<DTooltip>
|
||||
<:trigger>
|
||||
<LinkTo
|
||||
@route="adminCustomizeThemes.show"
|
||||
@models={{array "themes" theme.theme_id}}
|
||||
class="theme-link"
|
||||
data-theme-id={{theme.theme_id}}
|
||||
>
|
||||
{{theme.theme_name}}
|
||||
</LinkTo>
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{i18n
|
||||
"admin.theme_site_settings.overridden_value"
|
||||
value=theme.value
|
||||
}}
|
||||
</:content>
|
||||
</DTooltip>
|
||||
{{#unless
|
||||
(this.isLastThemeSettingOverride overrides theme)
|
||||
}},{{/unless}}
|
||||
{{/each}}
|
||||
{{#unless overrides.themes}}
|
||||
-
|
||||
{{/unless}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each-in}}
|
||||
</tbody>
|
||||
</table>
|
||||
</:content>
|
||||
<:empty>
|
||||
</:empty>
|
||||
</AsyncContent>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -47,7 +47,9 @@ export default class AdminCustomizeThemesShowIndexController extends Controller
|
|||
@filterBy("model.theme_fields", "target", "extra_js") extraFiles;
|
||||
@notEmpty("settings") hasSettings;
|
||||
@notEmpty("translations") hasTranslations;
|
||||
@notEmpty("model.themeable_site_settings") hasThemeableSiteSettings;
|
||||
@readOnly("model.settings") settings;
|
||||
@readOnly("model.themeable_site_settings") themeSiteSettings;
|
||||
|
||||
@discourseComputed("model.component", "model.remote_theme")
|
||||
showCheckboxes() {
|
||||
|
|
|
@ -3,18 +3,22 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
|||
import SiteSetting from "admin/models/site-setting";
|
||||
|
||||
export default class ThemeSettings extends SiteSetting {
|
||||
updateSetting(themeId, newValue) {
|
||||
async updateSetting(themeId, newValue) {
|
||||
if (this.objects_schema) {
|
||||
newValue = JSON.stringify(newValue);
|
||||
}
|
||||
|
||||
return ajax(`/admin/themes/${themeId}/setting`, {
|
||||
type: "PUT",
|
||||
data: {
|
||||
name: this.setting,
|
||||
value: newValue,
|
||||
},
|
||||
});
|
||||
try {
|
||||
return ajax(`/admin/themes/${themeId}/setting`, {
|
||||
type: "PUT",
|
||||
data: {
|
||||
name: this.setting,
|
||||
value: newValue,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
loadMetadata(themeId) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import SiteSetting from "admin/models/site-setting";
|
||||
|
||||
export default class ThemeSiteSettings extends SiteSetting {
|
||||
async updateSetting(themeId, newValue) {
|
||||
try {
|
||||
return ajax(`/admin/themes/${themeId}/site-setting`, {
|
||||
type: "PUT",
|
||||
data: {
|
||||
name: this.setting,
|
||||
value: newValue,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import RestModel from "discourse/models/rest";
|
|||
import { i18n } from "discourse-i18n";
|
||||
import ColorScheme from "admin/models/color-scheme";
|
||||
import ThemeSettings from "admin/models/theme-settings";
|
||||
import ThemeSiteSettings from "admin/models/theme-site-settings";
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
const FIELDS_IDS = [0, 1, 5, 6];
|
||||
|
@ -27,6 +28,12 @@ class Theme extends RestModel {
|
|||
);
|
||||
}
|
||||
|
||||
if (json.themeable_site_settings) {
|
||||
json.themeable_site_settings = json.themeable_site_settings.map(
|
||||
(setting) => ThemeSiteSettings.create(setting)
|
||||
);
|
||||
}
|
||||
|
||||
const palette =
|
||||
json.owned_color_palette || json.color_scheme || json.base_palette;
|
||||
if (palette) {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class AdminConfigThemeSiteSettings extends DiscourseRoute {
|
||||
titleToken() {
|
||||
return i18n("admin.config.theme_site_settings.title");
|
||||
}
|
||||
}
|
|
@ -381,6 +381,7 @@ export default function () {
|
|||
this.route("spam", function () {
|
||||
this.route("settings", { path: "/" });
|
||||
});
|
||||
this.route("theme-site-settings");
|
||||
|
||||
this.route(
|
||||
"colorPalettes",
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
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 ThemeSiteSettings from "admin/components/theme-site-settings";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<DPageHeader
|
||||
@hideTabs={{true}}
|
||||
@titleLabel={{i18n "admin.config.theme_site_settings.title"}}
|
||||
@descriptionLabel={{i18n
|
||||
"admin.config.theme_site_settings.header_description"
|
||||
}}
|
||||
>
|
||||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/config/theme-site-settings"
|
||||
@label={{i18n "admin.config.theme_site_settings.title"}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</DPageHeader>
|
||||
|
||||
<div class="admin-config-page__main-area">
|
||||
<ThemeSiteSettings />
|
||||
</div>
|
||||
</template>
|
||||
);
|
|
@ -1,4 +1,5 @@
|
|||
import { fn, hash } from "@ember/helper";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import { and, not } from "truth-helpers";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
|
@ -9,10 +10,12 @@ import icon from "discourse/helpers/d-icon";
|
|||
import formatDate from "discourse/helpers/format-date";
|
||||
import formatUsername from "discourse/helpers/format-username";
|
||||
import lazyHash from "discourse/helpers/lazy-hash";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import InlineEditCheckbox from "admin/components/inline-edit-checkbox";
|
||||
import ThemeSettingEditor from "admin/components/theme-setting-editor";
|
||||
import ThemeSettingRelativesSelector from "admin/components/theme-setting-relatives-selector";
|
||||
import ThemeSiteSettingEditor from "admin/components/theme-site-setting-editor";
|
||||
import ThemeTranslation from "admin/components/theme-translation";
|
||||
import ColorPalettes from "select-kit/components/color-palettes";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
@ -289,6 +292,33 @@ export default RouteTemplate(
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.hasThemeableSiteSettings}}
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n
|
||||
"admin.customize.theme.theme_site_settings"
|
||||
}}</div>
|
||||
<p><i>{{htmlSafe
|
||||
(i18n
|
||||
"admin.customize.theme.overriden_site_settings_explanation"
|
||||
themeSiteSettingsConfigUrl=(getURL
|
||||
"/admin/config/theme-site-settings"
|
||||
)
|
||||
)
|
||||
}}</i></p>
|
||||
<section
|
||||
class="form-horizontal theme settings theme-site-settings control-unit"
|
||||
>
|
||||
{{#each @controller.themeSiteSettings as |setting|}}
|
||||
<ThemeSiteSettingEditor
|
||||
@setting={{setting}}
|
||||
@model={{@controller.model}}
|
||||
class="theme-site-setting control-unit"
|
||||
/>
|
||||
{{/each}}
|
||||
</section>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.hasSettings}}
|
||||
<div class="control-unit theme-settings">
|
||||
<div class="mini-title">{{i18n
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
register as registerPushNotifications,
|
||||
unsubscribe as unsubscribePushNotifications,
|
||||
} from "discourse/lib/push-notifications";
|
||||
import { currentThemeId } from "discourse/lib/theme-selector";
|
||||
import Notification from "discourse/models/notification";
|
||||
|
||||
class SubscribeUserNotificationsInit {
|
||||
|
@ -255,6 +256,14 @@ class SubscribeUserNotificationsInit {
|
|||
|
||||
@bind
|
||||
onClientSettings(data) {
|
||||
// Theme site setting changes for client settings should only affect users
|
||||
// currently using the same theme.
|
||||
if (data.scoped_to?.theme_id) {
|
||||
if (currentThemeId() !== data.scoped_to.theme_id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.siteSettings[data.name] = data.value;
|
||||
}
|
||||
|
||||
|
|
|
@ -272,6 +272,15 @@ export const ADMIN_NAV_MAP = [
|
|||
description: "admin.config.themes_and_components.header_description",
|
||||
icon: "paintbrush",
|
||||
keywords: "admin.config.themes_and_components.keywords",
|
||||
links: [
|
||||
{
|
||||
name: "admin_theme_site_settings",
|
||||
route: "adminConfig.theme-site-settings",
|
||||
label: "admin.config.theme_site_settings.title",
|
||||
description: "admin.config.theme_site_settings.header_description",
|
||||
icon: "gear",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "admin_customize_site_texts",
|
||||
|
|
|
@ -2,8 +2,22 @@ import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
|||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
|
||||
export function createSiteSettingsFromPreloaded(data) {
|
||||
const settings = new TrackedObject(data);
|
||||
export function createSiteSettingsFromPreloaded(
|
||||
siteSettings,
|
||||
themeSiteSettingOverrides
|
||||
) {
|
||||
const settings = new TrackedObject(siteSettings);
|
||||
|
||||
if (themeSiteSettingOverrides) {
|
||||
for (const [key, value] of Object.entries(themeSiteSettingOverrides)) {
|
||||
settings[key] = value;
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(
|
||||
`[Discourse] Overriding site setting ${key} with theme site setting value: ${value}`
|
||||
);
|
||||
}
|
||||
settings.themeSiteSettingOverrides = themeSiteSettingOverrides;
|
||||
}
|
||||
|
||||
settings.groupSettingArray = (groupSetting) => {
|
||||
const setting = settings[groupSetting];
|
||||
|
@ -26,6 +40,9 @@ export default class SiteSettingsService {
|
|||
static isServiceFactory = true;
|
||||
|
||||
static create() {
|
||||
return createSiteSettingsFromPreloaded(PreloadStore.get("siteSettings"));
|
||||
return createSiteSettingsFromPreloaded(
|
||||
PreloadStore.get("siteSettings"),
|
||||
PreloadStore.get("themeSiteSettingOverrides")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,3 +53,30 @@
|
|||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-theme-site-settings-row {
|
||||
&__setting {
|
||||
.setting-label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
color: var(--primary-medium);
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
}
|
||||
|
||||
@include viewport.from(sm) {
|
||||
&__setting {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
&__default {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
&__overridden {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::Config::ThemeSiteSettingsController < Admin::AdminController
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
themes_with_site_setting_overrides = {}
|
||||
|
||||
SiteSetting.themeable_site_settings.each do |setting_name|
|
||||
themes_with_site_setting_overrides[setting_name] = SiteSetting.setting_metadata_hash(
|
||||
setting_name,
|
||||
).merge(themes: [])
|
||||
end
|
||||
|
||||
ThemeSiteSetting.themes_with_overridden_settings.each do |row|
|
||||
themes_with_site_setting_overrides[row.setting_name][:themes] << {
|
||||
theme_id: row.theme_id,
|
||||
theme_name: row.theme_name,
|
||||
value: row.value,
|
||||
}
|
||||
end
|
||||
|
||||
render_json_dump(
|
||||
themeable_site_settings: SiteSetting.themeable_site_settings,
|
||||
themes_with_site_setting_overrides: themes_with_site_setting_overrides,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -350,6 +350,30 @@ class Admin::ThemesController < Admin::AdminController
|
|||
render json: updated_setting, status: :ok
|
||||
end
|
||||
|
||||
def update_theme_site_setting
|
||||
Themes::ThemeSiteSettingManager.call(
|
||||
params: {
|
||||
theme_id: params[:id],
|
||||
name: params[:name],
|
||||
value: params[:value],
|
||||
},
|
||||
guardian:,
|
||||
) do
|
||||
on_success do |theme_site_setting:|
|
||||
if theme_site_setting.present?
|
||||
render json: success_json.merge(theme_site_setting.as_json(only: %i[name value theme_id]))
|
||||
else
|
||||
render json: success_json
|
||||
end
|
||||
end
|
||||
on_failed_policy(:current_user_is_admin) { raise Discourse::InvalidAccess }
|
||||
on_failed_policy(:ensure_setting_is_themeable) do
|
||||
render_json_error(I18n.t("themes.setting_not_themeable", name: params[:name]), status: 400)
|
||||
end
|
||||
on_model_not_found(:theme) { raise Discourse::NotFound }
|
||||
end
|
||||
end
|
||||
|
||||
def schema
|
||||
end
|
||||
|
||||
|
|
|
@ -386,6 +386,8 @@ class RemoteTheme < ActiveRecord::Base
|
|||
raise ActiveRecord::Rollback if !theme.save
|
||||
end
|
||||
|
||||
create_theme_site_settings(theme, theme_info["theme_site_settings"])
|
||||
|
||||
theme.migrate_settings(start_transaction: false) if run_migrations
|
||||
end
|
||||
|
||||
|
@ -457,6 +459,35 @@ class RemoteTheme < ActiveRecord::Base
|
|||
theme.color_scheme = ordered_schemes.first if theme.new_record?
|
||||
end
|
||||
|
||||
def create_theme_site_settings(theme, theme_site_settings)
|
||||
theme_site_settings ||= {}
|
||||
|
||||
existing_theme_site_settings =
|
||||
theme.theme_site_settings.where(name: theme_site_settings.keys).to_a
|
||||
theme_site_settings.each do |setting, value|
|
||||
next if !SiteSetting.themeable[setting.to_sym]
|
||||
|
||||
# If there is an existing theme site setting, then don't touch it,
|
||||
# we don't want to mess with site owner's changes.
|
||||
existing_theme_site_setting =
|
||||
existing_theme_site_settings.find do |theme_site_setting|
|
||||
theme_site_setting.name == setting
|
||||
end
|
||||
next if existing_theme_site_setting.present?
|
||||
|
||||
# The manager handles creating the theme site setting record
|
||||
# if it does not exist.
|
||||
Themes::ThemeSiteSettingManager.call(
|
||||
params: {
|
||||
theme_id: theme.id,
|
||||
name: setting,
|
||||
value: value,
|
||||
},
|
||||
guardian: Discourse.system_user.guardian,
|
||||
) { |result| }
|
||||
end
|
||||
end
|
||||
|
||||
def github_diff_link
|
||||
if github_repo_url.present? && local_version != remote_version
|
||||
"#{github_repo_url.gsub(/\.git\z/, "")}/compare/#{local_version}...#{remote_version}"
|
||||
|
|
|
@ -337,8 +337,8 @@ class SiteSetting < ActiveRecord::Base
|
|||
|
||||
protected
|
||||
|
||||
def self.clear_cache!
|
||||
super
|
||||
def self.clear_cache!(expire_theme_site_setting_cache: false)
|
||||
super(expire_theme_site_setting_cache:)
|
||||
|
||||
@blocked_attachment_content_types_regex = nil
|
||||
@blocked_attachment_filenames_regex = nil
|
||||
|
|
|
@ -93,6 +93,7 @@ class Theme < ActiveRecord::Base
|
|||
has_many :migration_fields,
|
||||
-> { where(target_id: Theme.targets[:migrations]) },
|
||||
class_name: "ThemeField"
|
||||
has_many :theme_site_settings, dependent: :destroy
|
||||
|
||||
validate :component_validations
|
||||
validate :validate_theme_fields
|
||||
|
@ -107,6 +108,7 @@ class Theme < ActiveRecord::Base
|
|||
-> do
|
||||
include_basic_relations.includes(
|
||||
:theme_settings,
|
||||
:theme_site_settings,
|
||||
:settings_field,
|
||||
theme_fields: %i[upload theme_settings_migration],
|
||||
child_themes: %i[color_scheme locale_fields theme_translation_overrides],
|
||||
|
@ -126,6 +128,7 @@ class Theme < ActiveRecord::Base
|
|||
)
|
||||
end
|
||||
|
||||
scope :not_components, -> { where(component: false) }
|
||||
scope :not_system, -> { where("id > 0") }
|
||||
scope :system, -> { where("id < 0") }
|
||||
|
||||
|
@ -317,6 +320,15 @@ class Theme < ActiveRecord::Base
|
|||
SvgSprite.expire_cache
|
||||
end
|
||||
|
||||
def self.expire_site_setting_cache!
|
||||
Theme
|
||||
.not_components
|
||||
.pluck(:id)
|
||||
.each do |theme_id|
|
||||
Discourse.cache.delete(SiteSettingExtension.theme_site_settings_cache_key(theme_id))
|
||||
end
|
||||
end
|
||||
|
||||
def self.clear_default!
|
||||
SiteSetting.default_theme_id = -1
|
||||
expire_site_cache!
|
||||
|
@ -1063,7 +1075,12 @@ class Theme < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def user_selectable_count
|
||||
UserOption.where(theme_ids: [id]).count
|
||||
UserOption.where(theme_ids: [self.id]).count
|
||||
end
|
||||
|
||||
def themeable_site_settings
|
||||
return [] if self.component?
|
||||
ThemeSiteSettingResolver.new(theme: self).resolved_theme_site_settings
|
||||
end
|
||||
|
||||
def find_or_create_owned_color_palette
|
||||
|
|
123
app/models/theme_site_setting.rb
Normal file
123
app/models/theme_site_setting.rb
Normal file
|
@ -0,0 +1,123 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Copies the same schema as SiteSetting, since the values and
|
||||
# data types are identical. This table is used as a way for themes
|
||||
# to overrride specific site settings that we make available in
|
||||
# core based on the `themeable` designation on a site setting.
|
||||
#
|
||||
# Creation, updating, and deletion of theme site settings is done
|
||||
# via the `Themes::ThemeSiteSettingManager` service.
|
||||
class ThemeSiteSetting < ActiveRecord::Base
|
||||
belongs_to :theme
|
||||
|
||||
# Gets a list of themes that have theme site setting records
|
||||
# and the associated values for those settings, where the
|
||||
# value is different from the default site setting value.
|
||||
#
|
||||
# @return [Array<Hash>] an array of hashes where each hash contains:
|
||||
# - :theme_id [Integer] the ID of the theme
|
||||
# - :theme_name [String] the name of the theme
|
||||
# - :setting_name [Symbol] the name of the setting
|
||||
# - :value [String] the value of the setting
|
||||
# - :data_type [Integer] the data type of the setting
|
||||
def self.themes_with_overridden_settings
|
||||
sql = <<~SQL
|
||||
SELECT theme.id AS theme_id, theme.name AS theme_name,
|
||||
tss.name AS setting_name, tss.value, tss.data_type
|
||||
FROM themes theme
|
||||
INNER JOIN theme_site_settings tss ON theme.id = tss.theme_id
|
||||
WHERE theme.component = false AND tss.name IN (:setting_names)
|
||||
ORDER BY tss.name, theme.name
|
||||
SQL
|
||||
|
||||
DB
|
||||
.query(sql, setting_names: SiteSetting.themeable_site_settings)
|
||||
.each { |row| row.setting_name = row.setting_name.to_sym }
|
||||
.select do |row|
|
||||
# Do not consider this as an "overridden" setting if the value
|
||||
# is the same as the default site setting value.
|
||||
row.value !=
|
||||
SiteSetting
|
||||
.type_supervisor
|
||||
.to_db_value(row.setting_name, SiteSetting.defaults[row.setting_name])
|
||||
.first
|
||||
end
|
||||
.each do |row|
|
||||
row.value =
|
||||
SiteSetting.type_supervisor.to_rb_value(row.setting_name, row.value, row.data_type)
|
||||
end
|
||||
end
|
||||
|
||||
def self.can_access_db?
|
||||
!GlobalSetting.skip_redis? && !GlobalSetting.skip_db? &&
|
||||
ActiveRecord::Base.connection.table_exists?(self.table_name)
|
||||
end
|
||||
|
||||
# Generates a map of theme IDs to their site setting values. When
|
||||
# there is no theme site setting for a given theme, the default
|
||||
# site setting value is used.
|
||||
#
|
||||
# @return [Hash] a map where keys are theme IDs and values are hashes:
|
||||
#
|
||||
# {
|
||||
# 123 => {
|
||||
# setting_name_1 => value_1,
|
||||
# setting_name_2 => value_2,
|
||||
# ...
|
||||
# }
|
||||
# }
|
||||
def self.generate_theme_map
|
||||
# Similar to what SiteSettings::DbProvider and SiteSettings::LocalProcessProvider do
|
||||
# for their #all method, we can't try to load settings if the DB is not available,
|
||||
# since this method is called within SiteSetting.refresh! which is called on boot.
|
||||
return {} if !can_access_db?
|
||||
|
||||
theme_site_setting_values_map = {}
|
||||
Theme
|
||||
.includes(:theme_site_settings)
|
||||
.not_components
|
||||
.each do |theme|
|
||||
SiteSetting.themeable_site_settings.each do |setting_name|
|
||||
setting = theme.theme_site_settings.find { |s| s.name == setting_name.to_s }
|
||||
|
||||
value =
|
||||
if setting.nil?
|
||||
SiteSetting.defaults[setting_name]
|
||||
else
|
||||
SiteSetting.type_supervisor.to_rb_value(
|
||||
setting.name.to_sym,
|
||||
setting.value,
|
||||
setting.data_type,
|
||||
)
|
||||
end
|
||||
|
||||
theme_site_setting_values_map[theme.id] ||= {}
|
||||
theme_site_setting_values_map[theme.id][setting_name] = value
|
||||
end
|
||||
end
|
||||
|
||||
theme_site_setting_values_map
|
||||
end
|
||||
|
||||
def setting_rb_value
|
||||
SiteSetting.type_supervisor.to_rb_value(self.name, self.value, self.data_type)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: theme_site_settings
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# theme_id :integer not null
|
||||
# name :string not null
|
||||
# data_type :integer not null
|
||||
# value :text
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_theme_site_settings_on_theme_id (theme_id)
|
||||
# index_theme_site_settings_on_theme_id_and_name (theme_id,name) UNIQUE
|
||||
#
|
|
@ -156,6 +156,7 @@ class UserHistory < ActiveRecord::Base
|
|||
tag_group_destroy: 117,
|
||||
tag_group_change: 118,
|
||||
delete_associated_accounts: 119,
|
||||
change_theme_site_setting: 120,
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -278,6 +279,7 @@ class UserHistory < ActiveRecord::Base
|
|||
delete_flag
|
||||
update_flag
|
||||
create_flag
|
||||
change_theme_site_setting
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ class ThemeSerializer < BasicThemeSerializer
|
|||
:auto_update,
|
||||
:remote_theme_id,
|
||||
:settings,
|
||||
:themeable_site_settings,
|
||||
:errors,
|
||||
:supported?,
|
||||
:enabled?,
|
||||
|
@ -75,6 +76,15 @@ class ThemeSerializer < BasicThemeSerializer
|
|||
nil
|
||||
end
|
||||
|
||||
# Components always return an empty array here
|
||||
def themeable_site_settings
|
||||
object.themeable_site_settings
|
||||
end
|
||||
|
||||
def include_themeable_site_settings?
|
||||
!object.component?
|
||||
end
|
||||
|
||||
def include_child_themes?
|
||||
!object.component?
|
||||
end
|
||||
|
|
|
@ -340,6 +340,19 @@ class StaffActionLogger
|
|||
)
|
||||
end
|
||||
|
||||
def log_theme_site_setting_change(setting_name, previous_value, new_value, theme, opts = {})
|
||||
raise Discourse::InvalidParameters.new(:theme) if !theme
|
||||
|
||||
UserHistory.create!(
|
||||
params(opts).merge(
|
||||
action: UserHistory.actions[:change_theme_site_setting],
|
||||
subject: "#{theme.name}: #{setting_name}",
|
||||
previous_value: previous_value,
|
||||
new_value: new_value,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def log_site_text_change(subject, new_text = nil, old_text = nil, opts = {})
|
||||
raise Discourse::InvalidParameters.new(:subject) if subject.blank?
|
||||
UserHistory.create!(
|
||||
|
|
|
@ -63,6 +63,8 @@ class Themes::Create
|
|||
step :log_theme_change
|
||||
end
|
||||
|
||||
step :refresh_site_setting_cache
|
||||
|
||||
private
|
||||
|
||||
def ensure_remote_themes_are_not_allowlisted
|
||||
|
@ -82,4 +84,8 @@ class Themes::Create
|
|||
def log_theme_change(theme:, guardian:)
|
||||
StaffActionLogger.new(guardian.user).log_theme_change(nil, theme)
|
||||
end
|
||||
|
||||
def refresh_site_setting_cache
|
||||
SiteSetting.refresh!(refresh_site_settings: false, refresh_theme_site_settings: true)
|
||||
end
|
||||
end
|
||||
|
|
148
app/services/themes/theme_site_setting_manager.rb
Normal file
148
app/services/themes/theme_site_setting_manager.rb
Normal file
|
@ -0,0 +1,148 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for creating or updating theme site settings, and in the case
|
||||
# where the value is set to nil or is the same as the site setting default,
|
||||
# deleting the theme site setting override.
|
||||
#
|
||||
# Theme site settings are used to override specific site settings that are
|
||||
# marked as themeable in site_settings.yml. This allows themes to have a greater
|
||||
# control over the full site experience.
|
||||
#
|
||||
# Theme site settings have an identical schema to SiteSetting.
|
||||
class Themes::ThemeSiteSettingManager
|
||||
include Service::Base
|
||||
|
||||
params do
|
||||
attribute :theme_id, :integer
|
||||
attribute :name
|
||||
attribute :value
|
||||
|
||||
validates :theme_id, presence: true
|
||||
validates :name, presence: true
|
||||
|
||||
after_validation { self.name = self.name.to_sym if self.name.present? }
|
||||
end
|
||||
|
||||
policy :current_user_is_admin
|
||||
policy :ensure_setting_is_themeable
|
||||
model :theme
|
||||
model :existing_theme_site_setting, optional: true
|
||||
|
||||
transaction do
|
||||
step :convert_new_value_to_site_setting_values
|
||||
step :upsert
|
||||
step :log_change
|
||||
end
|
||||
|
||||
step :update_site_setting_cache
|
||||
|
||||
private
|
||||
|
||||
def current_user_is_admin(guardian:)
|
||||
guardian.is_admin?
|
||||
end
|
||||
|
||||
def ensure_setting_is_themeable(params:)
|
||||
SiteSetting.themeable[params.name]
|
||||
end
|
||||
|
||||
def fetch_theme(params:)
|
||||
Theme.find_by(id: params.theme_id)
|
||||
end
|
||||
|
||||
def fetch_existing_theme_site_setting(params:, theme:)
|
||||
theme.theme_site_settings.find_by(name: params.name)
|
||||
end
|
||||
|
||||
def convert_new_value_to_site_setting_values(params:)
|
||||
if params.value.nil?
|
||||
context[:setting_db_value] = nil
|
||||
context[:setting_data_type] = nil
|
||||
context[:setting_ruby_value] = nil
|
||||
return
|
||||
end
|
||||
|
||||
# This must be done because we want the schema and data of ThemeSiteSetting to reflect
|
||||
# that of SiteSetting, since they are the same data types and values.
|
||||
setting_db_value, setting_data_type =
|
||||
SiteSetting.type_supervisor.to_db_value(params.name, params.value)
|
||||
setting_ruby_value =
|
||||
SiteSetting.type_supervisor.to_rb_value(params.name, params.value, setting_data_type)
|
||||
|
||||
context[:setting_db_value] = setting_db_value
|
||||
context[:setting_data_type] = setting_data_type
|
||||
context[:setting_ruby_value] = setting_ruby_value
|
||||
end
|
||||
|
||||
def upsert(
|
||||
params:,
|
||||
existing_theme_site_setting:,
|
||||
theme:,
|
||||
setting_db_value:,
|
||||
setting_data_type:,
|
||||
setting_ruby_value:
|
||||
)
|
||||
setting_record = nil
|
||||
context[:previous_value] = nil
|
||||
context[:new_value] = nil
|
||||
|
||||
if existing_theme_site_setting
|
||||
context[:previous_value] = existing_theme_site_setting.setting_rb_value
|
||||
setting_record = existing_theme_site_setting
|
||||
|
||||
# If the setting is nil or matches the site setting default,
|
||||
# then we just update the existing theme site setting to reflect
|
||||
# this, as insurance against further changes to the site setting
|
||||
# default value.
|
||||
if params.value.nil? || setting_ruby_value == SiteSetting.defaults[params.name]
|
||||
new_db_value, _ =
|
||||
SiteSetting.type_supervisor.to_db_value(params.name, SiteSetting.defaults[params.name])
|
||||
new_ruby_value = SiteSetting.defaults[params.name]
|
||||
else
|
||||
new_db_value = setting_db_value
|
||||
new_ruby_value = setting_ruby_value
|
||||
end
|
||||
|
||||
existing_theme_site_setting.update!(value: new_db_value)
|
||||
context[:new_value] = new_ruby_value
|
||||
else
|
||||
# If the setting is nil or matches the site setting default,
|
||||
# then we make a record using the site setting default as
|
||||
# insurance against further changes to the default value for
|
||||
# the site setting.
|
||||
if params.value.nil? || setting_ruby_value == SiteSetting.defaults[params.name]
|
||||
new_db_value, _ =
|
||||
SiteSetting.type_supervisor.to_db_value(params.name, SiteSetting.defaults[params.name])
|
||||
new_ruby_value = SiteSetting.defaults[params.name]
|
||||
else
|
||||
new_db_value = setting_db_value
|
||||
new_ruby_value = setting_ruby_value
|
||||
end
|
||||
|
||||
setting_record =
|
||||
theme.theme_site_settings.create!(
|
||||
name: params.name,
|
||||
value: new_db_value,
|
||||
data_type: setting_data_type,
|
||||
)
|
||||
context[:new_value] = new_ruby_value
|
||||
end
|
||||
|
||||
context[:theme_site_setting] = setting_record
|
||||
end
|
||||
|
||||
def log_change(params:, new_value:, previous_value:, theme:, guardian:)
|
||||
StaffActionLogger.new(guardian.user).log_theme_site_setting_change(
|
||||
params.name,
|
||||
previous_value,
|
||||
new_value,
|
||||
theme,
|
||||
)
|
||||
end
|
||||
|
||||
def update_site_setting_cache(theme:, params:, new_value:)
|
||||
# This also sends a MessageBus message to the client for client site settings,
|
||||
# and a DiscourseEvent for the change.
|
||||
SiteSetting.change_themeable_site_setting(theme.id, params.name, new_value)
|
||||
end
|
||||
end
|
|
@ -5510,6 +5510,9 @@ en:
|
|||
legal:
|
||||
title: "Legal"
|
||||
header_description: "Configure legal settings, such as terms of service, privacy policy, contact details, and EU-specific considerations"
|
||||
theme_site_settings:
|
||||
title: "Theme site settings"
|
||||
header_description: "Shows all theme site settings and the themes that are currently overriding them"
|
||||
localization:
|
||||
title: "Localization"
|
||||
header_description: "Configure your community’s interface language and other localization options for your members"
|
||||
|
@ -5800,6 +5803,15 @@ en:
|
|||
title: "Embedding"
|
||||
header_description: "Discourse has the ability to embed the comments from a topic in a remote site using a Javascript API that creates an IFRAME"
|
||||
|
||||
theme_site_settings:
|
||||
setting: "Setting"
|
||||
overridden_by: "Overridden by"
|
||||
default_value: "Default"
|
||||
overridden_value: "Overridden value: %{value}"
|
||||
select_setting: "Select a site setting to see current theme values"
|
||||
add: "Go to theme site settings"
|
||||
help: "The theme you are currently using is <a href='%{basePath}/admin/customize/themes/%{currentThemeId}'>%{currentTheme}</a>. Go to the <a href='%{basePath}/admin/customize/themes/%{currentThemeId}'>theme config page</a> to alter theme site settings, or click on a linked theme in the table below to edit its settings."
|
||||
|
||||
search:
|
||||
modal_title: "Search everything in admin"
|
||||
title: "Search"
|
||||
|
@ -6485,7 +6497,7 @@ en:
|
|||
install: "Install"
|
||||
themes:
|
||||
title: "Themes"
|
||||
description: "Site-wide themes that change the overall look and feel of your site for all users."
|
||||
description: "Site-wide themes that change the overall look and feel of your site for all users. Want to check which site settings your themes override? <a href=\"%{themeSiteSettingsUrl}\">View theme site settings</a>"
|
||||
themes_intro: "Install a new theme to get started, or create your own from scratch using these resources."
|
||||
new_theme: "New theme"
|
||||
back: "Back to themes"
|
||||
|
@ -6848,11 +6860,13 @@ en:
|
|||
up_to_date: "Theme is up-to-date, last checked:"
|
||||
has_overwritten_history: "Current theme version no longer exists because the Git history has been overwritten by a force push."
|
||||
add: "Add"
|
||||
theme_settings: "Theme Settings"
|
||||
edit_objects_theme_setting: "Objects Setting Editor"
|
||||
overriden_settings_explanation: "Overridden settings are marked with a dot and have a highlighted color. To reset these settings to the default value, press the reset button next to them."
|
||||
theme_settings: "Custom theme settings"
|
||||
theme_site_settings: "Settings the theme can override"
|
||||
edit_objects_theme_setting: "Objects setting editor"
|
||||
overriden_settings_explanation: "Changes from the default are marked with a dot and highlight. Click Reset to reset to the default."
|
||||
overriden_site_settings_explanation: "Site settings the theme can customize. Click Reset to restore the theme’s default. If none is set, the site setting default is used. You can see all site settings being overridden by themes at <a href='%{themeSiteSettingsConfigUrl}'>the theme site settings config page</a>."
|
||||
no_settings: "This theme has no settings."
|
||||
theme_translations: "Theme Translations"
|
||||
theme_translations: "Theme translations"
|
||||
empty: "No items"
|
||||
commits_behind:
|
||||
one: "Theme is %{count} commit behind!"
|
||||
|
@ -6916,6 +6930,8 @@ en:
|
|||
active_filter: "Active"
|
||||
inactive_filter: "Inactive"
|
||||
updates_available_filter: "Updates Available"
|
||||
theme_setting_saved: "Theme setting saved!"
|
||||
theme_site_setting_saved: "Theme site setting saved!"
|
||||
|
||||
schema:
|
||||
title: "Edit %{name} setting"
|
||||
|
@ -7265,6 +7281,7 @@ en:
|
|||
tag_group_destroy: "delete tag group"
|
||||
tag_group_change: "change tag group"
|
||||
delete_associated_accounts: "delete associated accounts"
|
||||
change_theme_site_setting: "change theme site setting"
|
||||
screened_emails:
|
||||
title: "Screened emails"
|
||||
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
||||
|
|
|
@ -84,6 +84,7 @@ en:
|
|||
bad_color_scheme: "Can not update theme, invalid color palette"
|
||||
other_error: "Something went wrong updating theme"
|
||||
ember_selector_error: "Sorry – using #ember or .ember-view CSS selectors is not permitted, because these names are dynamically generated at runtime and will change over time, eventually resulting in broken CSS. Try a different selector."
|
||||
setting_not_themeable: "The setting '%{name}' is not themeable."
|
||||
import_error:
|
||||
generic: An error occurred while importing that theme
|
||||
upload: "Error creating upload asset: %{name}. %{errors}"
|
||||
|
|
|
@ -249,6 +249,7 @@ Discourse::Application.routes.draw do
|
|||
get "preview" => "themes#preview"
|
||||
get "translations/:locale" => "themes#get_translations"
|
||||
put "setting" => "themes#update_single_setting"
|
||||
put "site-setting" => "themes#update_theme_site_setting"
|
||||
get "objects_setting_metadata/:setting_name" => "themes#objects_setting_metadata"
|
||||
put "change-colors" => "themes#change_colors"
|
||||
end
|
||||
|
@ -437,6 +438,7 @@ Discourse::Application.routes.draw do
|
|||
put "/logo" => "logo#update"
|
||||
put "/fonts" => "fonts#update"
|
||||
get "colors/:id" => "color_palettes#show"
|
||||
get "theme-site-settings" => "theme_site_settings#index"
|
||||
get "colors" => "color_palettes#index"
|
||||
|
||||
resources :flags, only: %i[index new create update destroy] do
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
# Available options:
|
||||
#
|
||||
# default - The default value of the setting. For upload site settings, use the id of the upload seeded in db/fixtures/010_uploads.rb.
|
||||
# client - Set to true if the javascript should have access to this setting's value.
|
||||
# refresh - Set to true if clients should refresh when the setting is changed.
|
||||
# min - For a string setting, the minimum length. For an integer setting, the minimum value.
|
||||
# max - For a string setting, the maximum length. For an integer setting, the maximum value.
|
||||
# regex - A regex that the value must match.
|
||||
# validator - The name of the class that will be use to validate the value of the setting.
|
||||
# allow_any - For choice settings allow items not specified in the choice list (default true)
|
||||
# secret - Set to true if input type should be password and value needs to be scrubbed from logs (default false).
|
||||
# enum - The setting has a fixed set of allowed values, and only one can be chosen.
|
||||
# Set to the class name that defines the set.
|
||||
# locale_default - A hash which overrides according to `SiteSetting.default_locale`.
|
||||
# The key should be as the same as possible value of default_locale.
|
||||
# default - The default value of the setting. For upload site settings, use the id of the upload seeded
|
||||
# in db/fixtures/010_uploads.rb.
|
||||
# client - Set to true if the javascript should have access to this setting's value.
|
||||
# refresh - Set to true if clients should refresh when the setting is changed.
|
||||
# min - For a string setting, the minimum length. For an integer setting, the minimum value.
|
||||
# max - For a string setting, the maximum length. For an integer setting, the maximum value.
|
||||
# regex - A regex that the value must match.
|
||||
# validator - The name of the class that will be use to validate the value of the setting.
|
||||
# allow_any - For choice settings allow items not specified in the choice list (default true)
|
||||
# secret - Set to true if input type should be password and value needs to be scrubbed from logs (default false).
|
||||
# enum - The setting has a fixed set of allowed values, and only one can be chosen.
|
||||
# Set to the class name that defines the set.
|
||||
# locale_default - A hash which overrides according to `SiteSetting.default_locale`.
|
||||
# The key should be as the same as possible value of default_locale.
|
||||
# mandatory_values - A list of mandatory values that must be included in the setting, these cannot be changed or removed
|
||||
# in the UI.
|
||||
# requires_confirmation - A string that indicates if the setting requires confirmation before it can be changed.
|
||||
# Only valid value here is "simple" which will display a confirmation dialog when the setting
|
||||
# is changed.
|
||||
# themeable - Set to true if themes can override this site setting. Generally, only client side affecting settings
|
||||
# that change the UI should be themeable, and try limit to simple setting types like bool, list, integer, enum.
|
||||
#
|
||||
#
|
||||
# type: email - Must be a valid email address.
|
||||
|
@ -3218,6 +3226,7 @@ search:
|
|||
client: true
|
||||
type: enum
|
||||
enum: "SearchExperienceSiteSetting"
|
||||
themeable: true
|
||||
|
||||
uncategorized:
|
||||
version_checks:
|
||||
|
@ -3636,6 +3645,7 @@ uncategorized:
|
|||
enable_welcome_banner:
|
||||
client: true
|
||||
default: true
|
||||
themeable: true
|
||||
area: "interface"
|
||||
|
||||
user_preferences:
|
||||
|
|
15
db/migrate/20250409035119_add_theme_site_setting_table.rb
Normal file
15
db/migrate/20250409035119_add_theme_site_setting_table.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
class AddThemeSiteSettingTable < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :theme_site_settings do |t|
|
||||
t.integer :theme_id, null: false
|
||||
t.string :name, null: false
|
||||
t.integer :data_type, null: false
|
||||
t.text :value
|
||||
t.timestamps
|
||||
|
||||
t.index :theme_id
|
||||
t.index %i[theme_id name], unique: true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
class BackfillThemeableSiteSettings < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
initial_themeable_site_settings = %w[enable_welcome_banner search_experience]
|
||||
|
||||
initial_themeable_site_settings.each do |setting|
|
||||
db_data_type, db_value =
|
||||
DB.query_single("SELECT data_type, value FROM site_settings WHERE name = ?", setting)
|
||||
|
||||
# If there is no value in the DB, it means the admin hasn't changed it from the default,
|
||||
# and theme site settings will just use the default value.
|
||||
next if db_value.nil?
|
||||
|
||||
theme_ids = DB.query_single("SELECT id FROM themes WHERE NOT component")
|
||||
|
||||
theme_ids.each do |theme_id|
|
||||
# ThemeSiteSetting has an identical schema to SiteSetting, so we can use the same values
|
||||
# and data types.
|
||||
DB.exec(
|
||||
"INSERT INTO theme_site_settings (name, data_type, value, theme_id, created_at, updated_at)
|
||||
VALUES (:setting, :data_type, :value, :theme_id, NOW(), NOW())
|
||||
ON CONFLICT (name, theme_id) DO NOTHING",
|
||||
setting:,
|
||||
data_type: db_data_type,
|
||||
value: db_value,
|
||||
theme_id:,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -112,6 +112,7 @@ class ApplicationLayoutPreloader
|
|||
check_readonly_mode if @readonly_mode.nil?
|
||||
@preloaded["site"] = Site.json_for(@guardian)
|
||||
@preloaded["siteSettings"] = SiteSetting.client_settings_json
|
||||
@preloaded["themeSiteSettingOverrides"] = SiteSetting.theme_site_settings_json(@theme_id)
|
||||
@preloaded["customHTML"] = custom_html_json
|
||||
@preloaded["banner"] = banner_json
|
||||
@preloaded["customEmoji"] = custom_emoji
|
||||
|
|
|
@ -5,6 +5,7 @@ module SiteSettingExtension
|
|||
include HasSanitizableFields
|
||||
|
||||
SiteSettingChangeResult = Struct.new(:previous_value, :new_value)
|
||||
InvalidSettingAccess = Class.new(StandardError)
|
||||
|
||||
delegate :description, :keywords, :placeholder, :humanized_name, to: SiteSettings::LabelFormatter
|
||||
|
||||
|
@ -79,6 +80,11 @@ module SiteSettingExtension
|
|||
@containers[provider.current_site] ||= {}
|
||||
end
|
||||
|
||||
def theme_site_settings
|
||||
@theme_site_settings ||= {}
|
||||
@theme_site_settings[provider.current_site] ||= {}
|
||||
end
|
||||
|
||||
def defaults
|
||||
@defaults ||= SiteSettings::DefaultsProvider.new(self)
|
||||
end
|
||||
|
@ -91,6 +97,10 @@ module SiteSettingExtension
|
|||
@categories ||= {}
|
||||
end
|
||||
|
||||
def themeable
|
||||
@themeable ||= {}
|
||||
end
|
||||
|
||||
def areas
|
||||
@areas ||= {}
|
||||
end
|
||||
|
@ -153,18 +163,30 @@ module SiteSettingExtension
|
|||
&.first
|
||||
end
|
||||
|
||||
def settings_hash
|
||||
result = {}
|
||||
def theme_site_settings_json(theme_id)
|
||||
key = SiteSettingExtension.theme_site_settings_cache_key(theme_id)
|
||||
json =
|
||||
Discourse
|
||||
.cache
|
||||
.fetch(key, expires_in: 30.minutes) { theme_site_settings_json_uncached(theme_id) }
|
||||
Rails.logger.error("Nil theme_site_settings_json from the cache for '#{key}'") if json.nil?
|
||||
json || ""
|
||||
rescue => e
|
||||
Rails.logger.error("Error while retrieving theme_site_settings_json: #{e.message}")
|
||||
""
|
||||
end
|
||||
|
||||
defaults.all.keys.each do |s|
|
||||
result[s] = if deprecated_settings.include?(s.to_s)
|
||||
public_send(s, warn: false).to_s
|
||||
else
|
||||
public_send(s).to_s
|
||||
end
|
||||
end
|
||||
def setting_metadata_hash(setting)
|
||||
setting_hash = {
|
||||
setting:,
|
||||
default: SiteSetting.defaults[setting],
|
||||
description: SiteSetting.description(setting),
|
||||
humanized_name: SiteSetting.humanized_name(setting),
|
||||
}.merge(type_supervisor.type_hash(setting))
|
||||
end
|
||||
|
||||
result
|
||||
def themeable_site_settings
|
||||
themeable.select { |_, value| value }.keys
|
||||
end
|
||||
|
||||
def client_settings_json
|
||||
|
@ -180,19 +202,30 @@ module SiteSettingExtension
|
|||
def client_settings_json_uncached
|
||||
MultiJson.dump(
|
||||
Hash[
|
||||
*@client_settings.flat_map do |name|
|
||||
value =
|
||||
if deprecated_settings.include?(name.to_s)
|
||||
public_send(name, warn: false)
|
||||
else
|
||||
public_send(name)
|
||||
end
|
||||
type = type_supervisor.get_type(name)
|
||||
value = value.to_s if type == :upload
|
||||
value = value.map(&:to_s).join("|") if type == :uploaded_image_list
|
||||
*@client_settings
|
||||
.flat_map do |name|
|
||||
# Themeable site settings require a theme ID, which we do not always
|
||||
# have when loading client site settings. They are excluded here,
|
||||
# to get them use theme_site_settings_json(:theme_id)
|
||||
next if themeable[name]
|
||||
|
||||
[name, value]
|
||||
end
|
||||
value =
|
||||
if deprecated_settings.include?(name.to_s)
|
||||
public_send(name, warn: false)
|
||||
else
|
||||
public_send(name)
|
||||
end
|
||||
|
||||
type = type_supervisor.get_type(name)
|
||||
if type == :upload
|
||||
value = value.to_s
|
||||
elsif type == :uploaded_image_list
|
||||
value = value.map(&:to_s).join("|")
|
||||
end
|
||||
|
||||
[name, value]
|
||||
end
|
||||
.compact
|
||||
],
|
||||
)
|
||||
rescue => e
|
||||
|
@ -200,6 +233,10 @@ module SiteSettingExtension
|
|||
nil
|
||||
end
|
||||
|
||||
def theme_site_settings_json_uncached(theme_id)
|
||||
MultiJson.dump(theme_site_settings[theme_id])
|
||||
end
|
||||
|
||||
# Retrieve all settings
|
||||
def all_settings(
|
||||
include_hidden: false,
|
||||
|
@ -262,6 +299,11 @@ module SiteSettingExtension
|
|||
true
|
||||
end
|
||||
end
|
||||
.reject do |setting_name, _|
|
||||
# Do not show themeable site settings all_settings list or in the UI, they
|
||||
# are managed separately via the ThemeSiteSetting model.
|
||||
themeable[setting_name]
|
||||
end
|
||||
.map do |s, v|
|
||||
type_hash = type_supervisor.type_hash(s)
|
||||
default = defaults.get(s, default_locale).to_s
|
||||
|
@ -291,6 +333,7 @@ module SiteSettingExtension
|
|||
placeholder: placeholder(s),
|
||||
mandatory_values: mandatory_values[s],
|
||||
requires_confirmation: requires_confirmation_settings[s],
|
||||
themeable: themeable[s],
|
||||
)
|
||||
opts.merge!(type_hash)
|
||||
end
|
||||
|
@ -324,39 +367,63 @@ module SiteSettingExtension
|
|||
"client_settings_json_#{Discourse.git_version}"
|
||||
end
|
||||
|
||||
# refresh all the site settings
|
||||
def refresh!
|
||||
def self.theme_site_settings_cache_key(theme_id)
|
||||
# NOTE: we use the git version in the key to ensure
|
||||
# that we don't end up caching the incorrect version
|
||||
# in cases where we are cycling unicorns
|
||||
"theme_site_settings_json_#{theme_id}__#{Discourse.git_version}"
|
||||
end
|
||||
|
||||
# Refresh all the site settings and theme site settings
|
||||
def refresh!(refresh_site_settings: true, refresh_theme_site_settings: true)
|
||||
mutex.synchronize do
|
||||
ensure_listen_for_changes
|
||||
|
||||
new_hash =
|
||||
Hash[
|
||||
*(
|
||||
defaults
|
||||
.db_all
|
||||
.map do |s|
|
||||
[s.name.to_sym, type_supervisor.to_rb_value(s.name, s.value, s.data_type)]
|
||||
end
|
||||
.to_a
|
||||
.flatten
|
||||
)
|
||||
]
|
||||
if refresh_site_settings
|
||||
new_hash =
|
||||
Hash[
|
||||
*(
|
||||
provider
|
||||
.all
|
||||
.map do |s|
|
||||
[s.name.to_sym, type_supervisor.to_rb_value(s.name, s.value, s.data_type)]
|
||||
end
|
||||
.to_a
|
||||
.flatten
|
||||
)
|
||||
]
|
||||
|
||||
defaults_view = defaults.all(new_hash[:default_locale])
|
||||
defaults_view = defaults.all(new_hash[:default_locale])
|
||||
|
||||
# add locale default and defaults based on default_locale, cause they are cached
|
||||
new_hash = defaults_view.merge!(new_hash)
|
||||
# add locale default and defaults based on default_locale, cause they are cached
|
||||
new_hash = defaults_view.merge!(new_hash)
|
||||
|
||||
# add shadowed
|
||||
shadowed_settings.each { |ss| new_hash[ss] = GlobalSetting.public_send(ss) }
|
||||
# add shadowed
|
||||
shadowed_settings.each { |ss| new_hash[ss] = GlobalSetting.public_send(ss) }
|
||||
|
||||
changes, deletions = diff_hash(new_hash, current)
|
||||
changes, deletions = diff_hash(new_hash, current)
|
||||
|
||||
changes.each { |name, val| current[name] = val }
|
||||
deletions.each { |name, _| current[name] = defaults_view[name] }
|
||||
uploads.clear
|
||||
changes.each { |name, val| current[name] = val }
|
||||
deletions.each { |name, _| current[name] = defaults_view[name] }
|
||||
uploads.clear
|
||||
end
|
||||
|
||||
clear_cache!
|
||||
if refresh_theme_site_settings
|
||||
new_theme_site_settings = ThemeSiteSetting.generate_theme_map
|
||||
|
||||
theme_site_setting_changes, theme_site_setting_deletions =
|
||||
diff_hash(new_theme_site_settings, theme_site_settings)
|
||||
theme_site_setting_changes.each do |theme_id, settings|
|
||||
theme_site_settings[theme_id] ||= {}
|
||||
theme_site_settings[theme_id].merge!(settings)
|
||||
end
|
||||
theme_site_setting_deletions.each { |theme_id, _| theme_site_settings.delete(theme_id) }
|
||||
end
|
||||
|
||||
clear_cache!(
|
||||
expire_theme_site_setting_cache:
|
||||
ThemeSiteSetting.can_access_db? && refresh_theme_site_settings,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -390,7 +457,29 @@ module SiteSettingExtension
|
|||
ensure_listen_for_changes
|
||||
end
|
||||
|
||||
def raise_invalid_setting_access(setting_name)
|
||||
raise SiteSettingExtension::InvalidSettingAccess.new(
|
||||
"#{setting_name} cannot be changed like this because it is a themeable setting. Instead, use the ThemeSiteSettingManager service to manage themeable site settings.",
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Removes an override for a setting, reverting it to the default value.
|
||||
# This method is only called manually usually, more often than not
|
||||
# setting overrides are removed in database migrations.
|
||||
#
|
||||
# Here we also handle notifying the UI of the change in the case
|
||||
# of theme site settings and clearing relevant caches, and triggering
|
||||
# server-side events for changed settings.
|
||||
#
|
||||
# Themeable site settings cannot be removed this way, they must be
|
||||
# changed via the ThemeSiteSetting model.
|
||||
#
|
||||
# @param name [Symbol] the name of the setting
|
||||
# @param val [Any] the value to set
|
||||
def remove_override!(name)
|
||||
raise_invalid_setting_access(name) if themeable[name]
|
||||
|
||||
old_val = current[name]
|
||||
provider.destroy(name)
|
||||
current[name] = defaults.get(name, default_locale)
|
||||
|
@ -404,7 +493,36 @@ module SiteSettingExtension
|
|||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Adds an override, which is to say a database entry for the setting
|
||||
# instead of using the default.
|
||||
#
|
||||
# The `set`, `set_and_log`, and `setting_name=` methods all call
|
||||
# this method. Its opposite is remove_override!.
|
||||
#
|
||||
# Here we also handle notifying the UI of the change in the case
|
||||
# of theme site settings and clearing relevant caches, and triggering
|
||||
# server-side events for changed settings.
|
||||
#
|
||||
# Themeable site settings cannot be changed this way, they must be
|
||||
# changed via the ThemeSiteSetting model.
|
||||
#
|
||||
# @param name [Symbol] the name of the setting
|
||||
# @param val [Any] the value to set
|
||||
#
|
||||
# @example
|
||||
# SiteSetting.add_override!(:site_description, "My awesome forum")
|
||||
#
|
||||
# @raise [SiteSettingExtension::InvalidSettingAccess] if the setting is themeable
|
||||
# (themeable settings must be changed via ThemeSiteSetting model)
|
||||
#
|
||||
# @note When called from the Rails console, this method automatically logs the change
|
||||
# with the system user.
|
||||
#
|
||||
# @see remove_override! for removing an override and reverting to default value
|
||||
def add_override!(name, val)
|
||||
raise_invalid_setting_access(name) if themeable[name]
|
||||
|
||||
old_val = current[name]
|
||||
val, type = type_supervisor.to_db_value(name, val)
|
||||
|
||||
|
@ -423,7 +541,7 @@ module SiteSettingExtension
|
|||
return if current[name] == old_val
|
||||
|
||||
clear_uploads_cache(name)
|
||||
notify_clients!(name) if client_settings.include? name
|
||||
notify_clients!(name) if client_settings.include?(name)
|
||||
clear_cache!
|
||||
|
||||
if defined?(Rails::Console)
|
||||
|
@ -435,12 +553,52 @@ module SiteSettingExtension
|
|||
DiscourseEvent.trigger(:site_setting_changed, name, old_val, current[name])
|
||||
end
|
||||
|
||||
# Updates a theme-specific site setting value in memory and notifies observers.
|
||||
#
|
||||
# This method is used to change site settings that are marked as "themeable",
|
||||
# which means they can have different values per theme. Unlike `add_override!`,
|
||||
# the database isn't touched here.
|
||||
#
|
||||
# @param theme_id [Integer] The ID of the theme to update the setting for
|
||||
# @param name [String, Symbol] The name of the site setting to change
|
||||
# @param val [Object] The new "ruby" value for the site setting
|
||||
#
|
||||
# @example
|
||||
# SiteSetting.change_themeable_site_setting(5, "enable_welcome_banner", false)
|
||||
#
|
||||
# @note Unlike regular site settings which use add_override!, themeable settings
|
||||
# should be changed via the ThemeSiteSettingManager service.
|
||||
#
|
||||
# @see ThemeSiteSettingManager service for the higher-level implementation that handles
|
||||
# database persistence and logging.
|
||||
def change_themeable_site_setting(theme_id, name, val)
|
||||
name = name.to_sym
|
||||
|
||||
theme_site_settings[theme_id] ||= {}
|
||||
old_val = theme_site_settings[theme_id][name]
|
||||
theme_site_settings[theme_id][name] = val
|
||||
|
||||
notify_clients!(name, theme_id: theme_id) if client_settings.include?(name)
|
||||
|
||||
clear_cache!(expire_theme_site_setting_cache: true)
|
||||
|
||||
DiscourseEvent.trigger(:theme_site_setting_changed, name, old_val, val)
|
||||
end
|
||||
|
||||
def notify_changed!
|
||||
MessageBus.publish("/site_settings", process: process_id)
|
||||
end
|
||||
|
||||
def notify_clients!(name)
|
||||
MessageBus.publish("/client_settings", name: name, value: self.public_send(name))
|
||||
def notify_clients!(name, scoped_to = nil)
|
||||
MessageBus.publish(
|
||||
"/client_settings",
|
||||
name: name,
|
||||
# default_locale is a special case, it is not themeable and we define
|
||||
# a custom getter for it, so we can just use the normal getter
|
||||
value:
|
||||
name.to_s == "default_locale" ? self.public_send(name) : self.public_send(name, scoped_to),
|
||||
scoped_to: scoped_to,
|
||||
)
|
||||
end
|
||||
|
||||
def requires_refresh?(name)
|
||||
|
@ -474,6 +632,8 @@ module SiteSettingExtension
|
|||
|
||||
def set(name, value, options = nil)
|
||||
if has_setting?(name)
|
||||
raise_invalid_setting_access(name) if themeable[name]
|
||||
|
||||
value = filter_value(name, value)
|
||||
if options
|
||||
self.public_send("#{name}=", value, options)
|
||||
|
@ -490,6 +650,8 @@ module SiteSettingExtension
|
|||
|
||||
def set_and_log(name, value, user = Discourse.system_user, detailed_message = nil)
|
||||
if has_setting?(name)
|
||||
raise_invalid_setting_access(name) if themeable[name]
|
||||
|
||||
prev_value = public_send(name)
|
||||
return if prev_value == value
|
||||
set(name, value)
|
||||
|
@ -503,9 +665,19 @@ module SiteSettingExtension
|
|||
end
|
||||
end
|
||||
|
||||
def get(name)
|
||||
def get(name, scoped_to = nil)
|
||||
if has_setting?(name)
|
||||
self.public_send(name)
|
||||
if themeable[name]
|
||||
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
||||
raise SiteSettingExtension::InvalidSettingAccess.new(
|
||||
"#{name} requires a theme_id because it is themeable",
|
||||
)
|
||||
else
|
||||
self.public_send(name, scoped_to)
|
||||
end
|
||||
else
|
||||
self.public_send(name)
|
||||
end
|
||||
else
|
||||
raise Discourse::InvalidParameters.new(
|
||||
I18n.t("errors.site_settings.invalid_site_setting", name: name),
|
||||
|
@ -535,8 +707,9 @@ module SiteSettingExtension
|
|||
|
||||
protected
|
||||
|
||||
def clear_cache!
|
||||
def clear_cache!(expire_theme_site_setting_cache: false)
|
||||
Discourse.cache.delete(SiteSettingExtension.client_settings_cache_key)
|
||||
Theme.expire_site_setting_cache! if expire_theme_site_setting_cache
|
||||
Site.clear_anon_cache!
|
||||
end
|
||||
|
||||
|
@ -578,7 +751,15 @@ module SiteSettingExtension
|
|||
clean_name = name.to_s.sub("?", "").to_sym
|
||||
|
||||
if type_supervisor.get_type(name) == :uploaded_image_list
|
||||
define_singleton_method clean_name do
|
||||
define_singleton_method clean_name do |scoped_to = nil|
|
||||
if themeable[clean_name]
|
||||
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
||||
raise SiteSettingExtension::InvalidSettingAccess.new(
|
||||
"#{clean_name} requires a theme_id because it is themeable",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
uploads_list = uploads[name]
|
||||
return uploads_list if uploads_list
|
||||
|
||||
|
@ -594,7 +775,15 @@ module SiteSettingExtension
|
|||
uploads[name] = uploads_list if uploads_list
|
||||
end
|
||||
elsif type_supervisor.get_type(name) == :upload
|
||||
define_singleton_method clean_name do
|
||||
define_singleton_method clean_name do |scoped_to = nil|
|
||||
if themeable[clean_name]
|
||||
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
||||
raise SiteSettingExtension::InvalidSettingAccess.new(
|
||||
"#{clean_name} requires a theme_id because it is themeable",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
upload = uploads[name]
|
||||
return upload if upload
|
||||
|
||||
|
@ -611,13 +800,29 @@ module SiteSettingExtension
|
|||
end
|
||||
end
|
||||
else
|
||||
define_singleton_method clean_name do
|
||||
define_singleton_method clean_name do |scoped_to = nil|
|
||||
if themeable[clean_name]
|
||||
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
||||
raise SiteSettingExtension::InvalidSettingAccess.new(
|
||||
"#{clean_name} requires a theme_id because it is themeable",
|
||||
)
|
||||
end
|
||||
|
||||
# If the theme hasn't overridden any theme site settings (or changed defaults)
|
||||
# then we will just fall back further down bellow to the current site setting value.
|
||||
settings_overriden_for_theme = theme_site_settings[scoped_to[:theme_id]]
|
||||
if settings_overriden_for_theme && settings_overriden_for_theme.key?(clean_name)
|
||||
return settings_overriden_for_theme[clean_name]
|
||||
end
|
||||
end
|
||||
|
||||
if plugins[name]
|
||||
plugin = Discourse.plugins_by_name[plugins[name]]
|
||||
return false if !plugin.configurable? && plugin.enabled_site_setting == name
|
||||
end
|
||||
|
||||
refresh! if current[name].nil?
|
||||
|
||||
value = current[name]
|
||||
|
||||
if mandatory_values[name]
|
||||
|
@ -642,17 +847,19 @@ module SiteSettingExtension
|
|||
list_type = type_supervisor.get_list_type(name)
|
||||
|
||||
if %w[simple compact].include?(list_type) || list_type.nil?
|
||||
define_singleton_method("#{clean_name}_map") do
|
||||
self.public_send(clean_name).to_s.split("|")
|
||||
define_singleton_method("#{clean_name}_map") do |scoped_to = nil|
|
||||
self.public_send(clean_name, scoped_to).to_s.split("|")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
define_singleton_method "#{clean_name}?" do
|
||||
self.public_send clean_name
|
||||
define_singleton_method "#{clean_name}?" do |scoped_to = nil|
|
||||
self.public_send(clean_name, scoped_to)
|
||||
end
|
||||
|
||||
define_singleton_method "#{clean_name}=" do |val|
|
||||
raise_invalid_setting_access(clean_name) if themeable[clean_name]
|
||||
|
||||
add_override!(name, val)
|
||||
end
|
||||
end
|
||||
|
@ -703,6 +910,8 @@ module SiteSettingExtension
|
|||
|
||||
categories[name] = opts[:category] || :uncategorized
|
||||
|
||||
themeable[name] = opts[:themeable] ? true : false
|
||||
|
||||
if opts[:area]
|
||||
split_areas = opts[:area].split("|")
|
||||
if split_areas.any? { |area| !SiteSetting.valid_areas.include?(area) }
|
||||
|
|
|
@ -142,7 +142,7 @@ module SiteSettings::DeprecatedSettings
|
|||
end
|
||||
end
|
||||
|
||||
define_singleton_method old_setting do |warn: true|
|
||||
define_singleton_method old_setting do |scoped_to = nil, warn: true|
|
||||
if warn
|
||||
Discourse.deprecate(
|
||||
"`SiteSetting.#{old_setting}` has been deprecated. Please use `SiteSetting.#{new_setting}` instead.",
|
||||
|
@ -153,13 +153,13 @@ module SiteSettings::DeprecatedSettings
|
|||
if OVERRIDE_TL_GROUP_SETTINGS.include?(old_setting)
|
||||
self.public_send("_group_to_tl_#{old_setting}")
|
||||
else
|
||||
self.public_send(override ? new_setting : "_#{old_setting}")
|
||||
self.public_send(override ? new_setting : "_#{old_setting}", scoped_to)
|
||||
end
|
||||
end
|
||||
|
||||
SiteSetting.singleton_class.alias_method(:"_#{old_setting}?", :"#{old_setting}?") if !override
|
||||
|
||||
define_singleton_method "#{old_setting}?" do |warn: true|
|
||||
define_singleton_method "#{old_setting}?" do |scoped_to = nil, warn: true|
|
||||
if warn
|
||||
Discourse.deprecate(
|
||||
"`SiteSetting.#{old_setting}?` has been deprecated. Please use `SiteSetting.#{new_setting}?` instead.",
|
||||
|
@ -167,7 +167,7 @@ module SiteSettings::DeprecatedSettings
|
|||
)
|
||||
end
|
||||
|
||||
self.public_send("#{override ? new_setting : "_" + old_setting}?")
|
||||
self.public_send("#{override ? new_setting : "_" + old_setting}?", scoped_to)
|
||||
end
|
||||
|
||||
SiteSetting.singleton_class.alias_method(:"_#{old_setting}=", :"#{old_setting}=") if !override
|
||||
|
|
|
@ -488,9 +488,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
|
|||
# includes svg_icon_subset and any settings containing _icon (incl. plugin settings)
|
||||
site_setting_icons = []
|
||||
|
||||
SiteSetting.settings_hash.select do |key, value|
|
||||
site_setting_icons |= value.split("|") if key.to_s.include?("_icon") && String === value
|
||||
end
|
||||
SiteSetting
|
||||
.all_settings(include_locale_setting: false)
|
||||
.select do |setting|
|
||||
site_setting_icons |= setting[:value].split("|") if setting[:setting].to_s.include?(
|
||||
"_icon",
|
||||
) && String === setting[:value]
|
||||
end
|
||||
|
||||
site_setting_icons
|
||||
end
|
||||
|
|
62
lib/theme_site_setting_resolver.rb
Normal file
62
lib/theme_site_setting_resolver.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for resolving all possible theme site settings for
|
||||
# a given theme, including details about the setting type, valid values,
|
||||
# and description, for use in the theme editor.
|
||||
#
|
||||
# The value of the setting will either be the value stored in the
|
||||
# ThemeSiteSetting table, or the default value of the site setting.
|
||||
#
|
||||
# Example output for a single setting, there will be an array of these:
|
||||
#
|
||||
# {
|
||||
# :setting=>:search_experience,
|
||||
# :default=>"search_icon",
|
||||
# :description=>"The default position and appearance of search on desktop devices",
|
||||
# :type=>"enum",
|
||||
# :valid_values=>[
|
||||
# {
|
||||
# :name=>"search.experience.search_field", :value=>"search_field"
|
||||
# }, {
|
||||
# :name=>"search.experience.search_icon", :value=>"search_icon"
|
||||
# }
|
||||
# ],
|
||||
# :translate_names=>true,
|
||||
# :value=>"search_icon"
|
||||
# }
|
||||
class ThemeSiteSettingResolver
|
||||
attr_reader :theme
|
||||
|
||||
def initialize(theme:)
|
||||
@theme = theme
|
||||
end
|
||||
|
||||
def resolved_theme_site_settings
|
||||
stored_theme_site_settings =
|
||||
theme.theme_site_settings.to_a.select do |setting|
|
||||
setting.name.to_sym.in?(SiteSetting.themeable_site_settings)
|
||||
end
|
||||
|
||||
SiteSetting
|
||||
.themeable_site_settings
|
||||
.each_with_object([]) do |setting, settings|
|
||||
setting_hash = SiteSetting.setting_metadata_hash(setting)
|
||||
|
||||
# If the setting has been saved in the DB, it means the theme has changed
|
||||
# it in about.json on import, or the admin has changed it manually later on.
|
||||
stored_setting =
|
||||
stored_theme_site_settings.find { |db_setting| db_setting.name == setting.to_s }
|
||||
if !stored_setting.nil?
|
||||
setting_hash[:value] = stored_setting.setting_rb_value
|
||||
else
|
||||
# Otherwise if there is no value in the DB, we use the default value of
|
||||
# the site setting, since we do not insert a DB value if the about.json
|
||||
# value is the same as the default site setting value.
|
||||
setting_hash[:value] = setting_hash[:default]
|
||||
end
|
||||
|
||||
settings << setting_hash
|
||||
end
|
||||
.sort_by { |setting_hash| setting_hash[:setting] }
|
||||
end
|
||||
end
|
0
spec/db/migrate/.gitkeep
Normal file
0
spec/db/migrate/.gitkeep
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require Rails.root.join("db/migrate/20250714010001_backfill_themeable_site_settings.rb")
|
||||
|
||||
RSpec.describe BackfillThemeableSiteSettings do
|
||||
fab!(:theme_1) { Fabricate(:theme) }
|
||||
fab!(:theme_2) { Fabricate(:theme) }
|
||||
fab!(:theme_3) { Fabricate(:theme, component: true) }
|
||||
|
||||
before do
|
||||
@original_verbose = ActiveRecord::Migration.verbose
|
||||
ActiveRecord::Migration.verbose = false
|
||||
end
|
||||
|
||||
after { ActiveRecord::Migration.verbose = @original_verbose }
|
||||
|
||||
it "works" do
|
||||
DB.exec(
|
||||
"INSERT INTO site_settings (name, data_type, value, created_at, updated_at)
|
||||
VALUES ('enable_welcome_banner', :data_type, :value, NOW(), NOW())",
|
||||
data_type: SiteSettings::TypeSupervisor.types[:bool],
|
||||
value: "t",
|
||||
)
|
||||
|
||||
BackfillThemeableSiteSettings.new.up
|
||||
|
||||
# This count includes the system themes theme, but not the component.
|
||||
expect(ThemeSiteSetting.where(name: "enable_welcome_banner").count).to eq(4)
|
||||
|
||||
# Don't insert any record if the site setting was never changed from the default.
|
||||
expect(ThemeSiteSetting.where(name: "search_experience").count).to eq(0)
|
||||
|
||||
# Make sure the data type + value are the same as the site setting.
|
||||
expect(
|
||||
ThemeSiteSetting.find_by(name: "enable_welcome_banner", theme_id: theme_1.id),
|
||||
).to have_attributes(data_type: SiteSettings::TypeSupervisor.types[:bool], value: "t")
|
||||
end
|
||||
end
|
48
spec/fabricators/theme_site_setting_fabricator.rb
Normal file
48
spec/fabricators/theme_site_setting_fabricator.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# NOTE: For most cases, :theme_site_setting_with_service is the better choice to use,
|
||||
# since it updates SiteSetting and other things properly.
|
||||
Fabricator(:theme_site_setting) do
|
||||
theme { Theme.find_default }
|
||||
|
||||
# NOTE: Only site settings with `themeable: true` are valid here.
|
||||
name { "enable_welcome_banner" }
|
||||
value { false }
|
||||
|
||||
before_create do |theme_site_setting|
|
||||
setting_db_value, setting_data_type =
|
||||
SiteSetting.type_supervisor.to_db_value(
|
||||
theme_site_setting.name.to_sym,
|
||||
theme_site_setting.value,
|
||||
)
|
||||
theme_site_setting.value = setting_db_value
|
||||
theme_site_setting.data_type = setting_data_type
|
||||
end
|
||||
end
|
||||
|
||||
Fabricator(:theme_site_setting_with_service, class_name: "Themes::ThemeSiteSettingManager") do
|
||||
# NOTE: Only site settings with `themeable: true` are valid here.
|
||||
transient :theme, :name, :value
|
||||
|
||||
initialize_with do |transients|
|
||||
theme = transients[:theme] || Theme.find_default
|
||||
result =
|
||||
resolved_class.call(
|
||||
params: {
|
||||
theme_id: theme.id,
|
||||
name: transients[:name],
|
||||
value: transients[:value],
|
||||
},
|
||||
guardian: Discourse.system_user.guardian,
|
||||
)
|
||||
|
||||
if result.failure?
|
||||
raise RSpec::Expectations::ExpectationNotMetError.new(
|
||||
"Service `#{resolved_class}` failed, see below for step details:\n\n" +
|
||||
result.inspect_steps,
|
||||
)
|
||||
end
|
||||
|
||||
result.theme_site_setting
|
||||
end
|
||||
end
|
|
@ -984,6 +984,65 @@ RSpec.describe SiteSettingExtension do
|
|||
end
|
||||
end
|
||||
|
||||
describe "themeable settings" do
|
||||
fab!(:theme_1) { Fabricate(:theme) }
|
||||
fab!(:theme_2) { Fabricate(:theme) }
|
||||
fab!(:tss_1) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
name: "enable_welcome_banner",
|
||||
value: false,
|
||||
theme: theme_1,
|
||||
)
|
||||
end
|
||||
fab!(:tss_2) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
name: "search_experience",
|
||||
value: "search_field",
|
||||
theme: theme_2,
|
||||
)
|
||||
end
|
||||
|
||||
it "has the site setting default values when there are no theme site settings for the theme" do
|
||||
SiteSetting.refresh!
|
||||
expect(SiteSetting.theme_site_settings[theme_1.id][:search_experience]).to eq("search_icon")
|
||||
expect(SiteSetting.theme_site_settings[theme_2.id][:enable_welcome_banner]).to eq(true)
|
||||
end
|
||||
|
||||
it "returns true for settings that are themeable" do
|
||||
expect(SiteSetting.themeable[:enable_welcome_banner]).to eq(true)
|
||||
end
|
||||
|
||||
it "returns false for settings that are not themeable" do
|
||||
expect(SiteSetting.themeable[:title]).to eq(false)
|
||||
end
|
||||
|
||||
it "caches the theme site setting values on a per theme basis" do
|
||||
SiteSetting.refresh!
|
||||
expect(SiteSetting.theme_site_settings[theme_1.id][:enable_welcome_banner]).to eq(false)
|
||||
expect(SiteSetting.theme_site_settings[theme_2.id][:search_experience]).to eq("search_field")
|
||||
end
|
||||
|
||||
it "overrides the site setting value with the theme site setting" do
|
||||
SiteSetting.create!(
|
||||
name: "enable_welcome_banner",
|
||||
data_type: SiteSettings::TypeSupervisor.types[:bool],
|
||||
value: "t",
|
||||
)
|
||||
SiteSetting.create!(
|
||||
name: "search_experience",
|
||||
data_type: SiteSettings::TypeSupervisor.types[:enum],
|
||||
value: SiteSetting.type_supervisor.to_db_value(:search_experience, "search_icon"),
|
||||
)
|
||||
SiteSetting.refresh!
|
||||
expect(SiteSetting.enable_welcome_banner(theme_id: theme_1.id)).to eq(false)
|
||||
expect(SiteSetting.enable_welcome_banner(theme_id: theme_2.id)).to eq(true)
|
||||
expect(SiteSetting.search_experience(theme_id: theme_1.id)).to eq("search_icon")
|
||||
expect(SiteSetting.search_experience(theme_id: theme_2.id)).to eq("search_field")
|
||||
end
|
||||
end
|
||||
|
||||
describe "_map extension for list settings" do
|
||||
it "handles splitting group_list settings" do
|
||||
SiteSetting.personal_message_enabled_groups = "1|2"
|
||||
|
|
71
spec/lib/theme_site_setting_resolver_spec.rb
Normal file
71
spec/lib/theme_site_setting_resolver_spec.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ThemeSiteSettingResolver do
|
||||
fab!(:theme)
|
||||
|
||||
subject(:resolver) { described_class.new(theme: theme) }
|
||||
|
||||
describe "#resolved_theme_site_settings" do
|
||||
let(:themeable_setting) { :enable_welcome_banner }
|
||||
let(:default_value) { SiteSetting.defaults[themeable_setting] }
|
||||
|
||||
it "returns themeable settings" do
|
||||
result = resolver.resolved_theme_site_settings
|
||||
expect(result.map { |s| s[:setting] }).to match_array(SiteSetting.themeable_site_settings)
|
||||
end
|
||||
|
||||
it "returns settings in alphabetical order" do
|
||||
result = resolver.resolved_theme_site_settings
|
||||
expect(result.map { |r| r[:setting] }).to eq(SiteSetting.themeable_site_settings.sort)
|
||||
end
|
||||
|
||||
it "includes metadata for each setting" do
|
||||
result = resolver.resolved_theme_site_settings.first
|
||||
expect(result).to include(
|
||||
setting: themeable_setting,
|
||||
default: default_value,
|
||||
description: I18n.t("site_settings.#{themeable_setting}"),
|
||||
type: "bool",
|
||||
)
|
||||
end
|
||||
|
||||
context "when theme has not overridden any settings" do
|
||||
it "uses the default site setting value" do
|
||||
result = resolver.resolved_theme_site_settings.find { |s| s[:setting] == themeable_setting }
|
||||
expect(result[:value]).to eq(default_value)
|
||||
expect(result[:default]).to eq(default_value)
|
||||
end
|
||||
end
|
||||
|
||||
context "when theme has overridden settings" do
|
||||
let(:overridden_value) { false }
|
||||
|
||||
before do
|
||||
# Create a theme site setting override with a different value than default
|
||||
Fabricate(
|
||||
:theme_site_setting,
|
||||
theme: theme,
|
||||
name: themeable_setting.to_s,
|
||||
value: overridden_value,
|
||||
data_type: SiteSetting.types[:enum],
|
||||
)
|
||||
end
|
||||
|
||||
it "uses the overridden value" do
|
||||
result = resolver.resolved_theme_site_settings.find { |s| s[:setting] == themeable_setting }
|
||||
expect(result[:value]).to eq(overridden_value)
|
||||
expect(result[:default]).to eq(default_value)
|
||||
end
|
||||
end
|
||||
|
||||
context "with enum type settings" do
|
||||
let(:themeable_setting) { :search_experience }
|
||||
|
||||
it "includes valid_values and translate_names" do
|
||||
result = resolver.resolved_theme_site_settings.find { |s| s[:setting] == themeable_setting }
|
||||
expect(result[:valid_values]).to be_an(Array)
|
||||
expect(result).to include(:translate_names)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -467,6 +467,140 @@ RSpec.describe RemoteTheme do
|
|||
expect(screenshot_2.upload.original_filename).to eq("dark.jpeg")
|
||||
end
|
||||
end
|
||||
|
||||
describe "theme site settings" do
|
||||
it "creates theme site settings defined in about.json" do
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge("theme_site_settings" => { "enable_welcome_banner" => false })
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme = RemoteTheme.import_theme(initial_repo_url)
|
||||
expect(theme.theme_site_settings.count).to eq(1)
|
||||
expect(theme.theme_site_settings.first.name).to eq("enable_welcome_banner")
|
||||
expect(theme.theme_site_settings.first.value).to eq("f")
|
||||
expect(theme.theme_site_settings.first.data_type).to eq(SiteSetting.types[:bool])
|
||||
end
|
||||
|
||||
it "does not remove theme site settings that are no longer in about.json" do
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge(
|
||||
"theme_site_settings" => {
|
||||
"enable_welcome_banner" => false,
|
||||
"search_experience" => "search_field",
|
||||
},
|
||||
)
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme = RemoteTheme.import_theme(initial_repo_url)
|
||||
expect(theme.theme_site_settings.count).to eq(2)
|
||||
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge("theme_site_settings" => { "enable_welcome_banner" => false })
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme.remote_theme.update_from_remote
|
||||
theme.reload
|
||||
|
||||
expect(theme.theme_site_settings.count).to eq(2)
|
||||
expect(theme.theme_site_settings.first.name).to eq("enable_welcome_banner")
|
||||
expect(theme.theme_site_settings.first.value).to eq("f")
|
||||
expect(theme.theme_site_settings.second.name).to eq("search_experience")
|
||||
expect(theme.theme_site_settings.second.value).to eq("search_field")
|
||||
end
|
||||
|
||||
it "ignores non-themeable site settings" do
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge("theme_site_settings" => { "min_admin_password_length" => 20 })
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme = nil
|
||||
|
||||
theme = RemoteTheme.import_theme(initial_repo_url)
|
||||
|
||||
expect(theme.theme_site_settings.count).to eq(0)
|
||||
end
|
||||
|
||||
# TODO (martin) Hard to test this without a better example...we don't have any
|
||||
# theme site settings that are an enum with > 2 values.
|
||||
xit "does not override user modified theme site settings" do
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge("theme_site_settings" => { "search_experience" => "search_field" })
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme = RemoteTheme.import_theme(initial_repo_url)
|
||||
expect(theme.theme_site_settings.first.value).to eq("search_field")
|
||||
|
||||
theme.theme_site_settings.first.update!(value: "search_icon")
|
||||
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge("theme_site_settings" => { "search_experience" => "search_field" })
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme.remote_theme.update_from_remote
|
||||
theme.reload
|
||||
|
||||
expect(theme.theme_site_settings.first.value).to eq("search_icon")
|
||||
end
|
||||
|
||||
it "does not update the existing theme site setting" do
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge("theme_site_settings" => { "search_experience" => "search_field" })
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme = RemoteTheme.import_theme(initial_repo_url)
|
||||
expect(theme.theme_site_settings.first.name).to eq("search_experience")
|
||||
expect(theme.theme_site_settings.first.value).to eq("search_field")
|
||||
|
||||
add_to_git_repo(
|
||||
initial_repo,
|
||||
"about.json" =>
|
||||
JSON
|
||||
.parse(about_json)
|
||||
.merge("theme_site_settings" => { "search_experience" => "search_icon" })
|
||||
.to_json,
|
||||
)
|
||||
|
||||
theme.remote_theme.update_from_remote
|
||||
theme.reload
|
||||
|
||||
expect(theme.theme_site_settings.first.value).to eq("search_field")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:github_repo) do
|
||||
|
|
71
spec/models/theme_site_setting_spec.rb
Normal file
71
spec/models/theme_site_setting_spec.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ThemeSiteSetting do
|
||||
fab!(:theme_1) { Fabricate(:theme) }
|
||||
fab!(:theme_2) { Fabricate(:theme) }
|
||||
fab!(:theme_site_setting_1) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme_1,
|
||||
name: "enable_welcome_banner",
|
||||
value: false,
|
||||
)
|
||||
end
|
||||
fab!(:theme_site_setting_2) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme_1,
|
||||
name: "search_experience",
|
||||
value: "search_field",
|
||||
)
|
||||
end
|
||||
|
||||
describe ".generate_theme_map" do
|
||||
it "returns a map of theme ids mapped to theme site settings, using site setting defaults if the setting records do not exist" do
|
||||
expect(ThemeSiteSetting.generate_theme_map).to include(
|
||||
{
|
||||
theme_1.id => {
|
||||
enable_welcome_banner: false,
|
||||
search_experience: "search_field",
|
||||
},
|
||||
theme_2.id => {
|
||||
enable_welcome_banner: true,
|
||||
search_experience: "search_icon",
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
context "when skipping redis" do
|
||||
before { GlobalSetting.skip_redis = true }
|
||||
after { GlobalSetting.skip_redis = false }
|
||||
|
||||
it "returns {}" do
|
||||
expect(ThemeSiteSetting.generate_theme_map).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context "when skipping db" do
|
||||
before { GlobalSetting.skip_db = true }
|
||||
after { GlobalSetting.skip_db = false }
|
||||
|
||||
it "returns {}" do
|
||||
expect(ThemeSiteSetting.generate_theme_map).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the table doesn't exist yet, in case of migrations" do
|
||||
before do
|
||||
ActiveRecord::Base
|
||||
.connection
|
||||
.stubs(:table_exists?)
|
||||
.with(ThemeSiteSetting.table_name)
|
||||
.returns(false)
|
||||
end
|
||||
|
||||
it "returns {}" do
|
||||
expect(ThemeSiteSetting.generate_theme_map).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,4 +62,24 @@ RSpec.describe "Multisite SiteSettings", type: :multisite do
|
|||
test_multisite_connection("second") { SiteSetting.refresh! }
|
||||
end
|
||||
end
|
||||
|
||||
describe "themeable site settings" do
|
||||
describe "#enable_welcome_banner" do
|
||||
it "should return the right value" do
|
||||
test_multisite_connection("default") do
|
||||
expect(SiteSetting.enable_welcome_banner(theme_id: Theme.find_default.id)).to eq(true)
|
||||
end
|
||||
|
||||
test_multisite_connection("second") do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
|
||||
expect(SiteSetting.enable_welcome_banner(theme_id: Theme.find_default.id)).to eq(false)
|
||||
end
|
||||
|
||||
test_multisite_connection("default") do
|
||||
expect(SiteSetting.enable_welcome_banner(theme_id: Theme.find_default.id)).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -319,6 +319,9 @@ RSpec.configure do |config|
|
|||
Discourse.current_user_provider = TestCurrentUserProvider
|
||||
Discourse::Application.load_tasks
|
||||
|
||||
ThemeField.delete_all
|
||||
ThemeSiteSetting.delete_all
|
||||
Theme.expire_site_setting_cache!
|
||||
SiteSetting.refresh!
|
||||
|
||||
# Rebase defaults
|
||||
|
|
82
spec/requests/admin/theme_site_settings_controller_spec.rb
Normal file
82
spec/requests/admin/theme_site_settings_controller_spec.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Admin::Config::ThemeSiteSettingsController do
|
||||
fab!(:admin)
|
||||
fab!(:theme_1) { Fabricate(:theme) }
|
||||
fab!(:theme_2) { Fabricate(:theme) }
|
||||
fab!(:theme_3) { Fabricate(:theme) }
|
||||
fab!(:theme_1_theme_site_setting) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme_1,
|
||||
name: "search_experience",
|
||||
value: "search_field",
|
||||
)
|
||||
end
|
||||
fab!(:theme_2_theme_site_setting) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme_2,
|
||||
name: "enable_welcome_banner",
|
||||
value: false,
|
||||
)
|
||||
end
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
describe "#index" do
|
||||
it "gets all theme site settings and the themes which have overridden values for these settings" do
|
||||
get "/admin/config/theme-site-settings.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
json = response.parsed_body
|
||||
|
||||
expect(json["themeable_site_settings"]).to include(
|
||||
"search_experience",
|
||||
"enable_welcome_banner",
|
||||
)
|
||||
|
||||
search_setting =
|
||||
json["themes_with_site_setting_overrides"]["search_experience"].deep_symbolize_keys
|
||||
expect(search_setting[:setting]).to eq("search_experience")
|
||||
expect(search_setting[:default]).to eq("search_icon")
|
||||
expect(search_setting[:description]).to eq(I18n.t("site_settings.search_experience"))
|
||||
expect(search_setting[:type]).to eq("enum")
|
||||
expect(search_setting[:themes].find { |t| t[:theme_id] == theme_1.id }).to include(
|
||||
theme_name: theme_1.name,
|
||||
theme_id: theme_1.id,
|
||||
value: "search_field",
|
||||
)
|
||||
|
||||
welcome_banner_setting =
|
||||
json["themes_with_site_setting_overrides"]["enable_welcome_banner"].deep_symbolize_keys
|
||||
expect(welcome_banner_setting[:setting]).to eq("enable_welcome_banner")
|
||||
expect(welcome_banner_setting[:default]).to eq(true)
|
||||
expect(welcome_banner_setting[:description]).to eq(
|
||||
I18n.t("site_settings.enable_welcome_banner"),
|
||||
)
|
||||
expect(welcome_banner_setting[:type]).to eq("bool")
|
||||
expect(welcome_banner_setting[:themes].find { |t| t[:theme_id] == theme_2.id }).to include(
|
||||
theme_name: theme_2.name,
|
||||
theme_id: theme_2.id,
|
||||
value: false,
|
||||
)
|
||||
end
|
||||
|
||||
it "does not count theme site settings with same value as site setting default as overridden" do
|
||||
theme_2_theme_site_setting.update!(
|
||||
value: SiteSetting.type_supervisor.to_db_value(:enable_welcome_banner, true).first,
|
||||
)
|
||||
|
||||
get "/admin/config/theme-site-settings.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
json = response.parsed_body
|
||||
|
||||
welcome_banner_setting =
|
||||
json["themes_with_site_setting_overrides"]["enable_welcome_banner"].deep_symbolize_keys
|
||||
|
||||
expect(welcome_banner_setting[:themes].find { |t| t[:theme_id] == theme_2.id }).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1504,6 +1504,7 @@ RSpec.describe ApplicationController do
|
|||
"isStaffWritesOnly",
|
||||
"activatedThemes",
|
||||
"#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}",
|
||||
"themeSiteSettingOverrides",
|
||||
],
|
||||
)
|
||||
end
|
||||
|
@ -1529,6 +1530,7 @@ RSpec.describe ApplicationController do
|
|||
"activatedThemes",
|
||||
"#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}",
|
||||
"currentUser",
|
||||
"themeSiteSettingOverrides",
|
||||
"topicTrackingStates",
|
||||
"topicTrackingStateMeta",
|
||||
],
|
||||
|
@ -1556,6 +1558,7 @@ RSpec.describe ApplicationController do
|
|||
"activatedThemes",
|
||||
"#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}",
|
||||
"currentUser",
|
||||
"themeSiteSettingOverrides",
|
||||
"topicTrackingStates",
|
||||
"topicTrackingStateMeta",
|
||||
"fontMap",
|
||||
|
|
|
@ -113,6 +113,13 @@ RSpec.describe Themes::Create do
|
|||
result
|
||||
end
|
||||
|
||||
it "updates the SiteSetting.theme_site_settings cache for the theme" do
|
||||
new_theme = result.theme
|
||||
expect(SiteSetting.theme_site_settings[new_theme.id]).to eq(
|
||||
ThemeSiteSetting.generate_theme_map[new_theme.id],
|
||||
)
|
||||
end
|
||||
|
||||
context "with component param" do
|
||||
let(:params) do
|
||||
theme_params.merge(component: true, user_selectable: false, color_scheme_id: nil)
|
||||
|
|
213
spec/services/themes/theme_site_setting_manager_spec.rb
Normal file
213
spec/services/themes/theme_site_setting_manager_spec.rb
Normal file
|
@ -0,0 +1,213 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Themes::ThemeSiteSettingManager do
|
||||
describe described_class::Contract, type: :model do
|
||||
it { is_expected.to validate_presence_of(:theme_id) }
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
end
|
||||
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(guardian:, params:) }
|
||||
|
||||
fab!(:admin)
|
||||
fab!(:theme)
|
||||
|
||||
let(:guardian) { admin.guardian }
|
||||
let(:params) { { theme_id: theme.id, name: "enable_welcome_banner", value: false } }
|
||||
|
||||
before { SiteSetting.refresh! }
|
||||
|
||||
context "when data is invalid" do
|
||||
let(:params) { {} }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when a non-admin user tries to change a setting" do
|
||||
let(:guardian) { Guardian.new }
|
||||
|
||||
it { is_expected.to fail_a_policy(:current_user_is_admin) }
|
||||
end
|
||||
|
||||
context "when the site setting is not a themeable one" do
|
||||
let(:params) { { theme_id: theme.id, name: "title", value: "New Title" } }
|
||||
|
||||
it { is_expected.to fail_a_policy(:ensure_setting_is_themeable) }
|
||||
end
|
||||
|
||||
context "when theme doesn't exist" do
|
||||
before { theme.destroy! }
|
||||
|
||||
it "fails to find the theme" do
|
||||
expect(result).to fail_to_find_a_model(:theme)
|
||||
end
|
||||
end
|
||||
|
||||
context "when creating a new theme site setting" do
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "creates a new theme site setting" do
|
||||
expect { result }.to change { ThemeSiteSetting.count }.by(1)
|
||||
|
||||
theme_site_setting = ThemeSiteSetting.last
|
||||
expect(theme_site_setting).to have_attributes(
|
||||
theme_id: theme.id,
|
||||
name: "enable_welcome_banner",
|
||||
value: "f",
|
||||
data_type: SiteSetting.types[:bool],
|
||||
)
|
||||
end
|
||||
|
||||
it "logs the creation in staff action log" do
|
||||
StaffActionLogger
|
||||
.any_instance
|
||||
.expects(:log_theme_site_setting_change)
|
||||
.with(:enable_welcome_banner, nil, false, theme)
|
||||
result
|
||||
end
|
||||
|
||||
it "refreshes the value in the SiteSetting cache" do
|
||||
expect { result }.to change { SiteSetting.enable_welcome_banner(theme_id: theme.id) }.from(
|
||||
true,
|
||||
).to(false)
|
||||
end
|
||||
|
||||
it "should publish changes to clients for client site settings" do
|
||||
message = MessageBus.track_publish("/client_settings") { result }.first
|
||||
expect(message.data).to eq(
|
||||
{ name: :enable_welcome_banner, scoped_to: { theme_id: theme.id }, value: false },
|
||||
)
|
||||
end
|
||||
|
||||
it "sends a DiscourseEvent for the change" do
|
||||
event =
|
||||
DiscourseEvent
|
||||
.track_events { messages = MessageBus.track_publish { result } }
|
||||
.find { |e| e[:event_name] == :theme_site_setting_changed }
|
||||
|
||||
expect(event).to be_present
|
||||
expect(event[:params]).to eq([:enable_welcome_banner, true, false])
|
||||
end
|
||||
end
|
||||
|
||||
context "when updating an existing theme site setting" do
|
||||
fab!(:theme_site_setting) do
|
||||
# NOTE: This example is a little contrived, because `true` is the same as the site setting default,
|
||||
# it would usually not ever be inserted into ThemeSiteSetting.
|
||||
#
|
||||
# However, we don't have any theme site settings yet with an enum with > 2 choices,
|
||||
# so we have to fake things a bit here to make sure the update behaviour works.
|
||||
Fabricate(:theme_site_setting, theme: theme, name: "enable_welcome_banner", value: true)
|
||||
end
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "updates the existing theme site setting" do
|
||||
expect { result }.not_to change { ThemeSiteSetting.count }
|
||||
expect(theme_site_setting.reload.value).to eq("f")
|
||||
end
|
||||
|
||||
it "logs the creation in staff action log" do
|
||||
StaffActionLogger
|
||||
.any_instance
|
||||
.expects(:log_theme_site_setting_change)
|
||||
.with(:enable_welcome_banner, true, false, theme)
|
||||
result
|
||||
end
|
||||
|
||||
it "refreshes the value in the SiteSetting cache" do
|
||||
expect { result }.to change { SiteSetting.enable_welcome_banner(theme_id: theme.id) }.from(
|
||||
true,
|
||||
).to(false)
|
||||
end
|
||||
|
||||
it "should publish changes to clients for client site settings" do
|
||||
message = MessageBus.track_publish("/client_settings") { result }.first
|
||||
expect(message.data).to eq(
|
||||
{ name: :enable_welcome_banner, scoped_to: { theme_id: theme.id }, value: false },
|
||||
)
|
||||
end
|
||||
|
||||
it "sends a DiscourseEvent for the change" do
|
||||
event =
|
||||
DiscourseEvent
|
||||
.track_events { messages = MessageBus.track_publish { result } }
|
||||
.find { |e| e[:event_name] == :theme_site_setting_changed }
|
||||
|
||||
expect(event[:params]).to eq([:enable_welcome_banner, true, false])
|
||||
end
|
||||
end
|
||||
|
||||
context "when removing a theme site setting by ommitting the value" do
|
||||
let!(:theme_site_setting) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme,
|
||||
name: "enable_welcome_banner",
|
||||
value: false,
|
||||
)
|
||||
end
|
||||
|
||||
before { SiteSetting.refresh! }
|
||||
|
||||
let(:params) { { theme_id: theme.id, name: "enable_welcome_banner", value: nil } }
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "updates the theme site setting to the site setting default value" do
|
||||
result
|
||||
expect(theme_site_setting.reload.value).to eq("t")
|
||||
end
|
||||
|
||||
it "logs the removal in staff action log" do
|
||||
StaffActionLogger
|
||||
.any_instance
|
||||
.expects(:log_theme_site_setting_change)
|
||||
.with(:enable_welcome_banner, false, true, theme)
|
||||
result
|
||||
end
|
||||
|
||||
it "refreshes the value in the SiteSetting cache" do
|
||||
expect { result }.to change { SiteSetting.enable_welcome_banner(theme_id: theme.id) }.from(
|
||||
false,
|
||||
).to(true)
|
||||
end
|
||||
|
||||
it "should publish changes to clients for client site settings" do
|
||||
message = MessageBus.track_publish("/client_settings") { result }.first
|
||||
expect(message.data).to eq(
|
||||
{ name: :enable_welcome_banner, scoped_to: { theme_id: theme.id }, value: true },
|
||||
)
|
||||
end
|
||||
|
||||
it "sends a DiscourseEvent for the change" do
|
||||
event =
|
||||
DiscourseEvent
|
||||
.track_events { messages = MessageBus.track_publish { result } }
|
||||
.find { |e| e[:event_name] == :theme_site_setting_changed }
|
||||
|
||||
expect(event[:params]).to eq([:enable_welcome_banner, false, true])
|
||||
end
|
||||
end
|
||||
|
||||
context "when changing value to the same as the site setting default" do
|
||||
let!(:theme_site_setting) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme,
|
||||
name: "enable_welcome_banner",
|
||||
value: false,
|
||||
)
|
||||
end
|
||||
|
||||
let(:params) { { theme_id: theme.id, name: "enable_welcome_banner", value: true } }
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "updates theme site setting when value matches default" do
|
||||
result
|
||||
expect(theme_site_setting.reload.value).to eq("t")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -160,7 +160,7 @@ RSpec.shared_examples_for "having working core features" do |skip_examples: []|
|
|||
before do
|
||||
SearchIndexer.enable
|
||||
topics.each { SearchIndexer.index(_1, force: true) }
|
||||
SiteSetting.enable_welcome_banner = false
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
after { SearchIndexer.disable }
|
||||
|
|
47
spec/system/admin_config_theme_site_settings_spec.rb
Normal file
47
spec/system/admin_config_theme_site_settings_spec.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "Admin Theme Site Settings", type: :system do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
fab!(:theme_1) { Fabricate(:theme, name: "Blue Steel") }
|
||||
fab!(:theme_2) { Fabricate(:theme, name: "Derelicte") }
|
||||
fab!(:theme_site_setting_1) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme_1,
|
||||
name: "enable_welcome_banner",
|
||||
value: false,
|
||||
)
|
||||
end
|
||||
fab!(:theme_site_setting_2) do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme_2,
|
||||
name: "search_experience",
|
||||
value: "search_field",
|
||||
)
|
||||
end
|
||||
|
||||
let(:theme_site_settings_page) { PageObjects::Pages::AdminThemeSiteSettings.new }
|
||||
|
||||
before { sign_in(current_user) }
|
||||
|
||||
it "shows the themeable site settings and their name, description, and default value" do
|
||||
visit "/admin/config/theme-site-settings"
|
||||
expect(theme_site_settings_page).to have_setting_with_default("enable_welcome_banner")
|
||||
expect(theme_site_settings_page).to have_setting_with_default("search_experience")
|
||||
end
|
||||
|
||||
it "shows links to each theme that overrides the default and overridden values" do
|
||||
visit "/admin/config/theme-site-settings"
|
||||
expect(theme_site_settings_page).to have_theme_overriding(
|
||||
"enable_welcome_banner",
|
||||
theme_1,
|
||||
"false",
|
||||
)
|
||||
expect(theme_site_settings_page).to have_theme_overriding(
|
||||
"search_experience",
|
||||
theme_2,
|
||||
"search_field",
|
||||
)
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
describe "Admin Customize Themes", type: :system do
|
||||
fab!(:color_scheme)
|
||||
fab!(:theme) { Fabricate(:theme, name: "Cool theme 1") }
|
||||
fab!(:theme) { Fabricate(:theme, name: "Cool theme 1", user_selectable: true) }
|
||||
fab!(:admin) { Fabricate(:admin, locale: "en") }
|
||||
|
||||
let(:theme_page) { PageObjects::Pages::AdminCustomizeThemes.new }
|
||||
|
@ -323,4 +323,69 @@ describe "Admin Customize Themes", type: :system do
|
|||
expect(page).to have_current_path("/")
|
||||
end
|
||||
end
|
||||
|
||||
describe "editing theme site settings" do
|
||||
it "shows all themeable site settings and allows editing values" do
|
||||
theme_page.visit(theme.id)
|
||||
SiteSetting.themeable_site_settings.each do |setting_name|
|
||||
expect(theme_page).to have_theme_site_setting(setting_name)
|
||||
end
|
||||
theme_page.toggle_theme_site_setting("enable_welcome_banner")
|
||||
expect(theme_page).to have_overridden_theme_site_setting("enable_welcome_banner")
|
||||
expect(page).to have_content(
|
||||
I18n.t("admin_js.admin.customize.theme.theme_site_setting_saved"),
|
||||
)
|
||||
expect(
|
||||
ThemeSiteSetting.exists?(theme: theme, name: "enable_welcome_banner", value: "f"),
|
||||
).to be_truthy
|
||||
end
|
||||
|
||||
it "allows resetting themeable site setting values back to site setting default" do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme,
|
||||
name: "enable_welcome_banner",
|
||||
value: false,
|
||||
)
|
||||
theme_page.visit(theme.id)
|
||||
expect(theme_page).to have_overridden_theme_site_setting("enable_welcome_banner")
|
||||
theme_page.reset_overridden_theme_site_setting("enable_welcome_banner")
|
||||
expect(page).to have_content(
|
||||
I18n.t("admin_js.admin.customize.theme.theme_site_setting_saved"),
|
||||
)
|
||||
expect(
|
||||
ThemeSiteSetting.exists?(theme: theme, name: "enable_welcome_banner", value: "f"),
|
||||
).to be_falsey
|
||||
end
|
||||
|
||||
it "does not show the overridden indicator if the theme site setting value in the DB is the same as the default" do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
theme: theme,
|
||||
name: "enable_welcome_banner",
|
||||
value: true,
|
||||
)
|
||||
theme_page.visit(theme.id)
|
||||
expect(theme_page).to have_theme_site_setting("enable_welcome_banner")
|
||||
expect(theme_page).to have_no_overridden_theme_site_setting("enable_welcome_banner")
|
||||
end
|
||||
|
||||
it "alters the UI via MessageBus when a theme site setting changes" do
|
||||
SiteSetting.refresh!(refresh_site_settings: false, refresh_theme_site_settings: true)
|
||||
banner = PageObjects::Components::WelcomeBanner.new
|
||||
other_user = Fabricate(:user)
|
||||
other_user.user_option.update!(theme_ids: [theme.id])
|
||||
sign_in(other_user)
|
||||
visit("/")
|
||||
expect(banner).to be_visible
|
||||
|
||||
using_session(:admin) do
|
||||
sign_in(admin)
|
||||
theme_page.visit(theme.id)
|
||||
theme_page.toggle_theme_site_setting("enable_welcome_banner")
|
||||
end
|
||||
|
||||
expect(banner).to be_hidden
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,11 +4,13 @@ RSpec.describe "Header Search - Responsive Behavior", type: :system do
|
|||
fab!(:current_user, :user)
|
||||
let(:search_page) { PageObjects::Pages::Search.new }
|
||||
|
||||
before { SiteSetting.search_experience = "search_field" }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "search_experience", value: "search_field")
|
||||
end
|
||||
|
||||
context "when welcome banner is enabled" do
|
||||
it "appears based on scroll & screen width with search banner enabled" do
|
||||
SiteSetting.enable_welcome_banner = true
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: true)
|
||||
sign_in(current_user)
|
||||
visit "/"
|
||||
|
||||
|
@ -31,7 +33,7 @@ RSpec.describe "Header Search - Responsive Behavior", type: :system do
|
|||
end
|
||||
|
||||
it "appears when search banner is not enabled & shows / hides based on viewport width" do
|
||||
SiteSetting.enable_welcome_banner = false
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
sign_in(current_user)
|
||||
visit "/"
|
||||
|
||||
|
@ -50,7 +52,8 @@ RSpec.describe "Header Search - Responsive Behavior", type: :system do
|
|||
end
|
||||
|
||||
it "does not appear when search setting is set to icon" do
|
||||
SiteSetting.search_experience = "search_icon"
|
||||
Fabricate(:theme_site_setting_with_service, name: "search_experience", value: "search_icon")
|
||||
|
||||
sign_in(current_user)
|
||||
visit "/"
|
||||
|
||||
|
|
|
@ -227,7 +227,7 @@ RSpec.describe "Glimmer Header", type: :system do
|
|||
|
||||
it "displays current user when logged in and login required" do
|
||||
SiteSetting.login_required = true
|
||||
SiteSetting.enable_welcome_banner = false
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
sign_in(current_user)
|
||||
|
||||
visit "/"
|
||||
|
|
|
@ -99,6 +99,39 @@ module PageObjects
|
|||
has_css?("#{setting_selector(setting_name)} .desc", exact_text: description)
|
||||
end
|
||||
|
||||
def has_theme_site_setting?(setting_name)
|
||||
find(theme_site_setting_selector(setting_name)).has_css?(
|
||||
".setting-label",
|
||||
text: SiteSetting.humanized_name(setting_name),
|
||||
)
|
||||
find(theme_site_setting_selector(setting_name)).has_css?(
|
||||
".setting-value",
|
||||
text: SiteSetting.description(setting_name),
|
||||
)
|
||||
end
|
||||
|
||||
def has_overridden_theme_site_setting?(setting_name)
|
||||
has_css?(theme_site_setting_selector(setting_name, overridden: true))
|
||||
end
|
||||
|
||||
def has_no_overridden_theme_site_setting?(setting_name)
|
||||
has_no_css?(theme_site_setting_selector(setting_name, overridden: true))
|
||||
end
|
||||
|
||||
def toggle_theme_site_setting(setting_name)
|
||||
find(theme_site_setting_selector(setting_name)).find(
|
||||
".setting-value input[type='checkbox']",
|
||||
).click
|
||||
find(theme_site_setting_selector(setting_name)).find(".setting-controls .ok").click
|
||||
end
|
||||
|
||||
def reset_overridden_theme_site_setting(setting_name)
|
||||
find(theme_site_setting_selector(setting_name, overridden: true)).find(
|
||||
".setting-controls__undo",
|
||||
).click
|
||||
find(theme_site_setting_selector(setting_name)).find(".setting-controls .ok").click
|
||||
end
|
||||
|
||||
def has_no_themes_list?
|
||||
has_no_css?(".themes-list-header")
|
||||
end
|
||||
|
@ -210,6 +243,10 @@ module PageObjects
|
|||
def setting_selector(setting_name, overridden: false)
|
||||
"section.theme.settings .setting#{overridden ? ".overridden" : ""}[data-setting=\"#{setting_name}\"]"
|
||||
end
|
||||
|
||||
def theme_site_setting_selector(setting_name, overridden: false)
|
||||
"section.theme.theme-site-settings .setting#{overridden ? ".overridden" : ""}.theme-site-setting[data-setting=\"#{setting_name}\"]"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
40
spec/system/page_objects/pages/admin_theme_site_settings.rb
Normal file
40
spec/system/page_objects/pages/admin_theme_site_settings.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminThemeSiteSettings < PageObjects::Pages::AdminBase
|
||||
def has_setting_with_default?(setting_name)
|
||||
setting_row(setting_name).has_css?(
|
||||
".admin-theme-site-settings-row__setting .setting-label",
|
||||
text: SiteSetting.humanized_name(setting_name),
|
||||
)
|
||||
setting_row(setting_name).has_css?(
|
||||
".admin-theme-site-settings-row__setting .setting-description",
|
||||
text: SiteSetting.description(setting_name),
|
||||
)
|
||||
setting_row(setting_name).has_css?(
|
||||
".admin-theme-site-settings-row__default",
|
||||
text: SiteSetting.defaults[setting_name],
|
||||
)
|
||||
end
|
||||
|
||||
def has_theme_overriding?(setting_name, theme, overrride_value)
|
||||
setting_row(setting_name).has_css?(theme_overriding_css(theme), text: theme.name)
|
||||
find(theme_overriding_css(theme)).hover
|
||||
find(".fk-d-tooltip__content.-content.-expanded").has_content?(
|
||||
I18n.t("admin_js.admin.theme_site_settings.overridden_value", value: overrride_value),
|
||||
)
|
||||
end
|
||||
|
||||
def theme_overriding_css(theme)
|
||||
".admin-theme-site-settings-row__overridden .theme-link[data-theme-id='#{theme.id}']"
|
||||
end
|
||||
|
||||
def setting_row(setting_name)
|
||||
page.find(
|
||||
".d-admin-row__content.admin-theme-site-settings-row[data-setting-name='#{setting_name}']",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,10 +9,14 @@ describe "Search | Shortcuts for variations of search input", type: :system do
|
|||
before { sign_in(current_user) }
|
||||
|
||||
context "when search_experience is search_field" do
|
||||
before { SiteSetting.search_experience = "search_field" }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "search_experience", value: "search_field")
|
||||
end
|
||||
|
||||
context "when enable_welcome_banner is true" do
|
||||
before { SiteSetting.enable_welcome_banner = true }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: true)
|
||||
end
|
||||
|
||||
it "displays and focuses welcome banner search when / is pressed and hides it when Escape is pressed" do
|
||||
visit("/")
|
||||
|
@ -41,7 +45,9 @@ describe "Search | Shortcuts for variations of search input", type: :system do
|
|||
end
|
||||
|
||||
context "when enable_welcome_banner is false" do
|
||||
before { SiteSetting.enable_welcome_banner = false }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
it "displays and focuses header search when / is pressed and hides it when Escape is pressed" do
|
||||
visit("/")
|
||||
|
@ -56,10 +62,14 @@ describe "Search | Shortcuts for variations of search input", type: :system do
|
|||
end
|
||||
|
||||
context "when search_experience is search_icon" do
|
||||
before { SiteSetting.search_experience = "search_icon" }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "search_experience", value: "search_icon")
|
||||
end
|
||||
|
||||
context "when enable_welcome_banner is true" do
|
||||
before { SiteSetting.enable_welcome_banner = true }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: true)
|
||||
end
|
||||
|
||||
it "displays and focuses welcome banner search when / is pressed and hides it when Escape is pressed" do
|
||||
visit("/")
|
||||
|
@ -88,7 +98,9 @@ describe "Search | Shortcuts for variations of search input", type: :system do
|
|||
end
|
||||
|
||||
context "when enable_welcome_banner is false" do
|
||||
before { SiteSetting.enable_welcome_banner = false }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
it "displays and focuses search icon search when / is pressed and hides it when Escape is pressed" do
|
||||
visit("/")
|
||||
|
|
|
@ -14,7 +14,7 @@ describe "Search", type: :system do
|
|||
SearchIndexer.enable
|
||||
SearchIndexer.index(topic, force: true)
|
||||
SearchIndexer.index(topic2, force: true)
|
||||
SiteSetting.enable_welcome_banner = false
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
after { SearchIndexer.disable }
|
||||
|
@ -90,7 +90,7 @@ describe "Search", type: :system do
|
|||
SearchIndexer.index(topic, force: true)
|
||||
SiteSetting.rate_limit_search_anon_user_per_minute = 4
|
||||
RateLimiter.enable
|
||||
SiteSetting.enable_welcome_banner = false
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
after { SearchIndexer.disable }
|
||||
|
@ -116,7 +116,7 @@ describe "Search", type: :system do
|
|||
SearchIndexer.enable
|
||||
SearchIndexer.index(topic, force: true)
|
||||
SearchIndexer.index(topic2, force: true)
|
||||
SiteSetting.enable_welcome_banner = false
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
after { SearchIndexer.disable }
|
||||
|
@ -153,7 +153,9 @@ describe "Search", type: :system do
|
|||
end
|
||||
|
||||
describe "with search icon in header" do
|
||||
before { SiteSetting.search_experience = "search_icon" }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "search_experience", value: "search_icon")
|
||||
end
|
||||
|
||||
it "displays the correct search mode" do
|
||||
visit("/")
|
||||
|
@ -163,7 +165,13 @@ describe "Search", type: :system do
|
|||
end
|
||||
|
||||
describe "with search field in header" do
|
||||
before { SiteSetting.search_experience = "search_field" }
|
||||
before do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
name: "search_experience",
|
||||
value: "search_field",
|
||||
)
|
||||
end
|
||||
|
||||
it "displays the correct search mode" do
|
||||
visit("/")
|
||||
|
@ -219,7 +227,7 @@ describe "Search", type: :system do
|
|||
SearchIndexer.enable
|
||||
SearchIndexer.index(topic, force: true)
|
||||
SearchIndexer.index(topic2, force: true)
|
||||
SiteSetting.enable_welcome_banner = false
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@ describe "User page search", type: :system do
|
|||
fab!(:user)
|
||||
let(:search_page) { PageObjects::Pages::Search.new }
|
||||
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
it "filters down to the user" do
|
||||
sign_in(user)
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@ describe "Welcome banner", type: :system do
|
|||
let(:search_page) { PageObjects::Pages::Search.new }
|
||||
|
||||
context "when welcome banner is enabled" do
|
||||
before { SiteSetting.enable_welcome_banner = true }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: true)
|
||||
end
|
||||
|
||||
it "shows for logged in and anonymous users" do
|
||||
visit "/"
|
||||
|
@ -60,7 +62,13 @@ describe "Welcome banner", type: :system do
|
|||
end
|
||||
|
||||
context "when using search_field search_experience" do
|
||||
before { SiteSetting.search_experience = "search_field" }
|
||||
before do
|
||||
Fabricate(
|
||||
:theme_site_setting_with_service,
|
||||
name: "search_experience",
|
||||
value: "search_field",
|
||||
)
|
||||
end
|
||||
|
||||
it "hides welcome banner and shows header search on scroll, and vice-versa" do
|
||||
Fabricate(:topic)
|
||||
|
@ -82,7 +90,9 @@ describe "Welcome banner", type: :system do
|
|||
end
|
||||
|
||||
context "when using search_icon search_experience" do
|
||||
before { SiteSetting.search_experience = "search_icon" }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "search_experience", value: "search_icon")
|
||||
end
|
||||
|
||||
it "hides welcome banner and shows header search on scroll, and vice-versa" do
|
||||
Fabricate(:topic)
|
||||
|
@ -105,7 +115,9 @@ describe "Welcome banner", type: :system do
|
|||
end
|
||||
|
||||
context "when welcome banner is not enabled" do
|
||||
before { SiteSetting.enable_welcome_banner = false }
|
||||
before do
|
||||
Fabricate(:theme_site_setting_with_service, name: "enable_welcome_banner", value: false)
|
||||
end
|
||||
|
||||
it "does not show the welcome banner for logged in and anonymous users" do
|
||||
visit "/"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue