2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2025-10-03 17:21:20 +08:00

DEV: Remove dual mode support for palettes and drop theme-owned palettes (#34467)

We're not going to finish/release dual-mode palettes and theme-owned
palettes at this time, so we're cleaning up the code that we've already
merged for these features.

Internal topic: t/161279.
This commit is contained in:
Osama Sayegh 2025-08-26 06:24:11 +03:00 committed by GitHub
parent 5cfda47b25
commit 7ee52c8f85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 208 additions and 1107 deletions

View file

@ -186,33 +186,16 @@ export default class AdminConfigAreasColorPalette extends Component {
return; return;
} }


const tags = document.querySelectorAll(`link[data-scheme-id="${id}"]`); const tag = document.querySelector(`link[data-scheme-id="${id}"]`);


if (tags.length === 0) { if (!tag) {
return; return;
} }


let darkTag;
let lightTag;
for (const tag of tags) {
if (tag.classList.contains("dark-scheme")) {
darkTag = tag;
} else if (tag.classList.contains("light-scheme")) {
lightTag = tag;
}
}

try { try {
const data = await ajax(`/color-scheme-stylesheet/${id}.json`, { const data = await ajax(`/color-scheme-stylesheet/${id}.json`);
data: { if (data?.new_href) {
include_dark_scheme: !!darkTag, tag.href = data.new_href;
},
});
if (data?.new_href && lightTag) {
lightTag.href = data.new_href;
}
if (data?.new_dark_href && darkTag) {
darkTag.href = data.new_dark_href;
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View file

@ -1,37 +0,0 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { TrackedSet } from "@ember-compat/tracked-built-ins";

class ChangeTracker {
@tracked dirtyLightColors = new TrackedSet();
@tracked dirtyDarkColors = new TrackedSet();

addDirtyLightColor(name) {
this.dirtyLightColors.add(name);
}

addDirtyDarkColor(name) {
this.dirtyDarkColors.add(name);
}

removeDirtyLightColor(name) {
this.dirtyLightColors.delete(name);
}

removeDirtyDarkColor(name) {
this.dirtyDarkColors.delete(name);
}

clear() {
this.dirtyLightColors.clear();
this.dirtyDarkColors.clear();
}

get dirtyColorsCount() {
return this.dirtyLightColors.size + this.dirtyDarkColors.size;
}
}

export default class AdminCustomizeThemesShowColorsController extends Controller {
colorPaletteChangeTracker = new ChangeTracker();
}

View file

@ -70,12 +70,7 @@ export async function applyColorScheme(scheme, options = {}) {


const apiUrl = `/color-scheme-stylesheet/${id}.json`; const apiUrl = `/color-scheme-stylesheet/${id}.json`;


const data = await ajax(apiUrl, { const data = await ajax(apiUrl);
data: {
include_dark_scheme: !!darkTag,
},
dataType: "json",
});


if (data?.new_href && lightTag) { if (data?.new_href && lightTag) {
lightTag.href = data.new_href; lightTag.href = data.new_href;
@ -87,16 +82,6 @@ export async function applyColorScheme(scheme, options = {}) {
} }
} }


if (data?.new_dark_href && darkTag) {
darkTag.href = data.new_dark_href;

if (replace && id) {
darkTag.setAttribute("data-scheme-id", id);
} else if (replace && !id) {
darkTag.removeAttribute("data-scheme-id");
}
}

return data; return data;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View file

@ -7,10 +7,8 @@ import { i18n } from "discourse-i18n";


export default class ColorSchemeColor extends EmberObject { export default class ColorSchemeColor extends EmberObject {
@tracked hex; @tracked hex;
@tracked dark_hex;


@tracked originalHex; @tracked originalHex;
@tracked originalDarkHex;


// Whether the current value is different than Discourse's default color scheme. // Whether the current value is different than Discourse's default color scheme.
@propertyNotEqual("hex", "default_hex") overridden; @propertyNotEqual("hex", "default_hex") overridden;
@ -18,19 +16,16 @@ export default class ColorSchemeColor extends EmberObject {
init(object) { init(object) {
super.init(...arguments); super.init(...arguments);
this.originalHex = object.hex; this.originalHex = object.hex;
this.originalDarkHex = object.dark_hex;
} }


discardColorChange() { discardColorChange() {
this.hex = this.originalHex; this.hex = this.originalHex;
this.dark_hex = this.originalDarkHex;
} }


@on("init") @on("init")
startTrackingChanges() { startTrackingChanges() {
this.set("originals", { this.set("originals", {
hex: this.hex || "FFFFFF", hex: this.hex || "FFFFFF",
darkHex: this.dark_hex,
}); });


// force changed property to be recalculated // force changed property to be recalculated
@ -38,18 +33,14 @@ export default class ColorSchemeColor extends EmberObject {
} }


// Whether value has changed since it was last saved. // Whether value has changed since it was last saved.
@discourseComputed("hex", "dark_hex") @discourseComputed("hex")
changed(hex, darkHex) { changed(hex) {
if (!this.originals) { if (!this.originals) {
return false; return false;
} }
if (hex !== this.originals.hex) { if (hex !== this.originals.hex) {
return true; return true;
} }
if (darkHex !== this.originals.darkHex) {
return true;
}

return false; return false;
} }



View file

@ -11,15 +11,10 @@ import ColorSchemeColor from "admin/models/color-scheme-color";
class ColorSchemes extends ArrayProxy {} class ColorSchemes extends ArrayProxy {}


export default class ColorScheme extends EmberObject { export default class ColorScheme extends EmberObject {
static findAll({ excludeThemeOwned = false } = {}) { static findAll() {
const colorSchemes = ColorSchemes.create({ content: [], loading: true }); const colorSchemes = ColorSchemes.create({ content: [], loading: true });


const data = {}; return ajax("/admin/color_schemes").then((all) => {
if (excludeThemeOwned) {
data.exclude_theme_owned = true;
}

return ajax("/admin/color_schemes", { data }).then((all) => {
all.forEach((colorScheme) => { all.forEach((colorScheme) => {
colorSchemes.pushObject( colorSchemes.pushObject(
ColorScheme.create({ ColorScheme.create({
@ -89,14 +84,11 @@ export default class ColorScheme extends EmberObject {
} }


schemeObject() { schemeObject() {
const extractColors = (property) =>
Object.fromEntries(
this.colors.map((color) => [color.name, color[property]])
);
return { return {
name: this.name, name: this.name,
dark: extractColors("dark_hex"), light: Object.fromEntries(
light: extractColors("hex"), this.colors.map((color) => [color.name, color.hex])
),
}; };
} }


@ -115,9 +107,7 @@ export default class ColorScheme extends EmberObject {
}); });
this.colors.forEach((c) => { this.colors.forEach((c) => {
newScheme.colors.pushObject( newScheme.colors.pushObject(
ColorSchemeColor.create( ColorSchemeColor.create(c.getProperties("name", "hex", "default_hex"))
c.getProperties("name", "hex", "default_hex", "dark_hex")
)
); );
}); });
return newScheme; return newScheme;
@ -172,7 +162,7 @@ export default class ColorScheme extends EmberObject {
data.colors = []; data.colors = [];
this.colors.forEach((c) => { this.colors.forEach((c) => {
if (!this.id || c.get("changed")) { if (!this.id || c.get("changed")) {
data.colors.pushObject(c.getProperties("name", "hex", "dark_hex")); data.colors.pushObject(c.getProperties("name", "hex"));
} }
}); });
} }

View file

@ -1,13 +1,10 @@
import { tracked } from "@glimmer/tracking";
import { get } from "@ember/object"; import { get } from "@ember/object";
import { gt, or } from "@ember/object/computed"; import { gt, or } from "@ember/object/computed";
import { isBlank, isEmpty } from "@ember/utils"; import { isBlank, isEmpty } from "@ember/utils";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse/lib/decorators"; import discourseComputed from "discourse/lib/decorators";
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import ColorScheme from "admin/models/color-scheme";
import ThemeSettings from "admin/models/theme-settings"; import ThemeSettings from "admin/models/theme-settings";
import ThemeSiteSettings from "admin/models/theme-site-settings"; import ThemeSiteSettings from "admin/models/theme-site-settings";


@ -34,17 +31,9 @@ class Theme extends RestModel {
); );
} }


const palette =
json.owned_color_palette || json.color_scheme || json.base_palette;
if (palette) {
json.colorPalette = ColorScheme.create(palette);
}

return json; return json;
} }


@tracked colorPalette;

@or("default", "user_selectable") isActive; @or("default", "user_selectable") isActive;
@gt("remote_theme.commits_behind", 0) isPendingUpdates; @gt("remote_theme.commits_behind", 0) isPendingUpdates;
@gt("editedFields.length", 0) hasEditedFields; @gt("editedFields.length", 0) hasEditedFields;
@ -192,34 +181,6 @@ class Theme extends RestModel {
return field ? field.error : ""; return field ? field.error : "";
} }


async changeColors() {
const colors = [];

for (const color of this.colorPalette.colors) {
const colorPayload = {
name: color.name,
hex: color.hex,
dark_hex: color.dark_hex,
};

colors.push(colorPayload);
}

const paletteData = await ajax(`/admin/themes/${this.id}/change-colors`, {
type: "PUT",
data: JSON.stringify({ colors }),
contentType: "application/json",
});
this.owned_color_palette = paletteData;
this.colorPalette = ColorScheme.create(paletteData);
}

discardColorChanges() {
for (const color of this.colorPalette.colors) {
color.discardColorChange();
}
}

getField(target, name) { getField(target, name) {
if (target === "common" && name === "js") { if (target === "common" && name === "js") {
target = "extra_js"; target = "extra_js";

View file

@ -3,6 +3,6 @@ import ColorScheme from "admin/models/color-scheme";


export default class AdminConfigColorPalettesRoute extends DiscourseRoute { export default class AdminConfigColorPalettesRoute extends DiscourseRoute {
model() { model() {
return ColorScheme.findAll({ excludeThemeOwned: true }); return ColorScheme.findAll();
} }
} }

View file

@ -1,31 +0,0 @@
import { action } from "@ember/object";
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";

export default class AdminCustomizeThemesShowColorsRoute extends DiscourseRoute {
@service dialog;

@action
willTransition(transition) {
if (
this.controller.colorPaletteChangeTracker.dirtyColorsCount > 0 &&
transition.intent.name !== "adminCustomizeThemes.show.index"
) {
transition.abort();
this.dialog.yesNoConfirm({
message: i18n(
"admin.customize.theme.unsaved_colors_leave_route_confirmation"
),
didConfirm: () => {
this.controller.colorPaletteChangeTracker.clear();
transition.retry();
},
});
}
}

titleToken() {
return i18n("admin.customize.theme.colors_title");
}
}

View file

@ -69,7 +69,6 @@ export default function () {
function () { function () {
this.route("show", { path: "/:theme_id" }, function () { this.route("show", { path: "/:theme_id" }, function () {
this.route("schema", { path: "schema/:setting_name" }); this.route("schema", { path: "schema/:setting_name" });
this.route("colors");
}); });
this.route("edit", { path: "/:theme_id/:target/:field_name/edit" }); this.route("edit", { path: "/:theme_id/:target/:field_name/edit" });
} }

View file

@ -1,61 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import RouteTemplate from "ember-route-template";
import { gt } from "truth-helpers";
import { i18n } from "discourse-i18n";
import ChangesBanner from "admin/components/changes-banner";
import ColorPaletteEditor from "admin/components/color-palette-editor";

export default RouteTemplate(
class extends Component {
get pendingChangesBannerLabel() {
return i18n("admin.customize.theme.unsaved_colors", {
count: this.args.controller.colorPaletteChangeTracker.dirtyColorsCount,
});
}

@action
onColorChange(color, value) {
color.hex = value;
if (color.hex !== color.originalHex) {
this.args.controller.colorPaletteChangeTracker.addDirtyLightColor(
color.name
);
} else {
this.args.controller.colorPaletteChangeTracker.removeDirtyLightColor(
color.name
);
}
}

@action
async save() {
await this.args.model.changeColors();
this.args.controller.colorPaletteChangeTracker.clear();
}

@action
discard() {
this.args.model.discardColorChanges();
this.args.controller.colorPaletteChangeTracker.clear();
}

<template>
<ColorPaletteEditor
@colors={{@model.colorPalette.colors}}
@onColorChange={{this.onColorChange}}
@hideRevertButton={{true}}
@system={{@model.system}}
/>
{{#if (gt @controller.colorPaletteChangeTracker.dirtyColorsCount 0)}}
<ChangesBanner
@bannerLabel={{this.pendingChangesBannerLabel}}
@saveLabel={{i18n "admin.customize.theme.save_colors"}}
@discardLabel={{i18n "admin.customize.theme.discard_colors"}}
@save={{this.save}}
@discard={{this.discard}}
/>
{{/if}}
</template>
}
);

View file

@ -119,114 +119,112 @@ export default RouteTemplate(
{{/if}} {{/if}}


{{#unless @controller.model.component}} {{#unless @controller.model.component}}
{{#unless @controller.siteSettings.use_overhauled_theme_color_palette}} <section
<section class="form-horizontal theme settings control-unit theme-settings__light-color-scheme"
class="form-horizontal theme settings control-unit theme-settings__light-color-scheme" >
> <div class="row setting">
<div class="row setting"> <div class="setting-label">
<div class="setting-label"> {{i18n "admin.customize.theme.color_scheme"}}
{{i18n "admin.customize.theme.color_scheme"}} </div>
</div>


<div class="setting-value"> <div class="setting-value">
<div class="color-palette-input-group"> <div class="color-palette-input-group">
<ColorPalettePicker <ColorPalettePicker
@content={{@controller.colorSchemes}} @content={{@controller.colorSchemes}}
@value={{@controller.colorSchemeId}} @value={{@controller.colorSchemeId}}
@icon="paintbrush" @icon="paintbrush"
@options={{hash @options={{hash
filterable=true filterable=true
translatedNone=(i18n translatedNone=(i18n
"admin.customize.theme.default_light_scheme" "admin.customize.theme.default_light_scheme"
) )
}}
/>
</div>

<div class="desc">{{i18n
"admin.customize.theme.color_scheme_select"
}} }}

/>
{{#if @controller.colorSchemeId}}
<LinkTo
@route="adminCustomize.colors-show"
@model={{@controller.colorSchemeId}}
>
{{i18n "admin.customize.theme.edit_colors"}}
</LinkTo>
{{/if}}
</div>
</div> </div>


<div class="setting-controls"> <div class="desc">{{i18n
{{#if @controller.lightColorSchemeChanged}} "admin.customize.theme.color_scheme_select"
<DButton }}
@action={{@controller.changeLightScheme}}
@icon="check" {{#if @controller.colorSchemeId}}
class="ok submit-light-edit" <LinkTo
/> @route="adminCustomize.colors-show"
<DButton @model={{@controller.colorSchemeId}}
@action={{@controller.cancelChangeLightScheme}} >
@icon="xmark" {{i18n "admin.customize.theme.edit_colors"}}
class="cancel cancel-light-edit" </LinkTo>
/>
{{/if}} {{/if}}
</div> </div>
</div> </div>
</section>
<section <div class="setting-controls">
class="form-horizontal theme settings control-unit theme-settings__dark-color-scheme" {{#if @controller.lightColorSchemeChanged}}
> <DButton
<div class="row setting"> @action={{@controller.changeLightScheme}}
<div class="setting-label"> @icon="check"
{{i18n "admin.customize.theme.dark_color_scheme"}} class="ok submit-light-edit"
/>
<DButton
@action={{@controller.cancelChangeLightScheme}}
@icon="xmark"
class="cancel cancel-light-edit"
/>
{{/if}}
</div>
</div>
</section>
<section
class="form-horizontal theme settings control-unit theme-settings__dark-color-scheme"
>
<div class="row setting">
<div class="setting-label">
{{i18n "admin.customize.theme.dark_color_scheme"}}
</div>

<div class="setting-value">
<div class="color-palette-input-group">
<ColorPalettePicker
@content={{@controller.colorSchemes}}
@value={{@controller.darkColorSchemeId}}
@icon="paintbrush"
@options={{hash
filterable=true
translatedNone=(i18n
"admin.customize.theme.default_light_scheme"
)
}}
/>
</div> </div>


<div class="setting-value"> <div class="desc">
<div class="color-palette-input-group"> {{i18n "admin.customize.theme.dark_color_scheme_select"}}
<ColorPalettePicker
@content={{@controller.colorSchemes}}
@value={{@controller.darkColorSchemeId}}
@icon="paintbrush"
@options={{hash
filterable=true
translatedNone=(i18n
"admin.customize.theme.default_light_scheme"
)
}}
/>
</div>


<div class="desc"> {{#if @controller.darkColorSchemeId}}
{{i18n "admin.customize.theme.dark_color_scheme_select"}} <LinkTo

@route="adminCustomize.colors-show"
{{#if @controller.darkColorSchemeId}} @model={{@controller.darkColorSchemeId}}
<LinkTo >
@route="adminCustomize.colors-show" {{i18n "admin.customize.theme.edit_colors"}}
@model={{@controller.darkColorSchemeId}} </LinkTo>
>
{{i18n "admin.customize.theme.edit_colors"}}
</LinkTo>
{{/if}}
</div>
</div>
<div class="setting-controls">
{{#if @controller.darkColorSchemeChanged}}
<DButton
@action={{@controller.changeDarkScheme}}
@icon="check"
class="ok submit-dark-edit"
/>
<DButton
@action={{@controller.cancelChangeDarkScheme}}
@icon="xmark"
class="cancel cancel-dark-edit"
/>
{{/if}} {{/if}}
</div> </div>
</div> </div>
</section> <div class="setting-controls">
{{/unless}} {{#if @controller.darkColorSchemeChanged}}
<DButton
@action={{@controller.changeDarkScheme}}
@icon="check"
class="ok submit-dark-edit"
/>
<DButton
@action={{@controller.cancelChangeDarkScheme}}
@icon="xmark"
class="cancel cancel-dark-edit"
/>
{{/if}}
</div>
</div>
</section>
{{/unless}} {{/unless}}


{{#if @controller.model.component}} {{#if @controller.model.component}}

View file

@ -2,8 +2,6 @@ import { LinkTo } from "@ember/routing";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import RouteTemplate from "ember-route-template"; import RouteTemplate from "ember-route-template";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import DPageHeader from "discourse/components/d-page-header";
import NavItem from "discourse/components/nav-item";
import PluginOutlet from "discourse/components/plugin-outlet"; import PluginOutlet from "discourse/components/plugin-outlet";
import TextField from "discourse/components/text-field"; import TextField from "discourse/components/text-field";
import UserLink from "discourse/components/user-link"; import UserLink from "discourse/components/user-link";
@ -250,27 +248,6 @@ export default RouteTemplate(
</div> </div>
{{/if}} {{/if}}


{{#if @controller.siteSettings.use_overhauled_theme_color_palette}}
{{#unless @controller.model.component}}
<DPageHeader>
<:tabs>
<NavItem
class="admin-customize-theme-tabs__settings"
@route="adminCustomizeThemes.show.index"
@routeParam={{@controller.model.id}}
@label="admin.customize.theme.settings"
/>
<NavItem
class="admin-customize-theme-tabs__colors"
@route="adminCustomizeThemes.show.colors"
@routeParam={{@controller.model.id}}
@label="admin.customize.theme.colors"
/>
</:tabs>
</DPageHeader>
{{/unless}}
{{/if}}

{{outlet}} {{outlet}}
{{/if}} {{/if}}
</div> </div>

View file

@ -374,16 +374,6 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
]); ]);
} }


if (siteSettings.use_overhauled_theme_color_palette) {
this.adminNavManager.overrideSectionLink(
"appearance",
"admin_color_palettes",
{
route: "adminConfig.colorPalettes",
}
);
}

for (const [sectionName, additionalLinks] of Object.entries( for (const [sectionName, additionalLinks] of Object.entries(
additionalAdminSidebarSectionLinks additionalAdminSidebarSectionLinks
)) { )) {

View file

@ -57,7 +57,6 @@ acceptance("Admin - Customize - Themes - Show", function (needs) {
"/uploads/default/original/1X/f8a61b9a0bfac672daec9e401787812f8c5e28df.png", "/uploads/default/original/1X/f8a61b9a0bfac672daec9e401787812f8c5e28df.png",
system: true, system: true,
color_scheme: null, color_scheme: null,
owned_color_palette: null,
user: { user: {
id: -1, id: -1,
username: "system", username: "system",

View file

@ -13,328 +13,246 @@ const example_scheme = {
hex: "F0ECD7", hex: "F0ECD7",
default_hex: "F0ECD7", default_hex: "F0ECD7",
is_advanced: true, is_advanced: true,
dark_hex: "F0ECD7",
default_dark_hex: null,
}, },
{ {
name: "primary_low", name: "primary_low",
hex: "D6D8C7", hex: "D6D8C7",
default_hex: "D6D8C7", default_hex: "D6D8C7",
is_advanced: true, is_advanced: true,
dark_hex: "D6D8C7",
default_dark_hex: null,
}, },
{ {
name: "primary_low_mid", name: "primary_low_mid",
hex: "A4AFA5", hex: "A4AFA5",
default_hex: "A4AFA5", default_hex: "A4AFA5",
is_advanced: true, is_advanced: true,
dark_hex: "A4AFA5",
default_dark_hex: null,
}, },
{ {
name: "primary_medium", name: "primary_medium",
hex: "7E918C", hex: "7E918C",
default_hex: "7E918C", default_hex: "7E918C",
is_advanced: true, is_advanced: true,
dark_hex: "7E918C",
default_dark_hex: null,
}, },
{ {
name: "primary_high", name: "primary_high",
hex: "4C6869", hex: "4C6869",
default_hex: "4C6869", default_hex: "4C6869",
is_advanced: true, is_advanced: true,
dark_hex: "4C6869",
default_dark_hex: null,
}, },
{ {
name: "primary", name: "primary",
hex: "002B36", hex: "002B36",
default_hex: "002B36", default_hex: "002B36",
is_advanced: false, is_advanced: false,
dark_hex: "002B36",
default_dark_hex: null,
}, },
{ {
name: "primary-50", name: "primary-50",
hex: "F0EBDA", hex: "F0EBDA",
default_hex: "F0EBDA", default_hex: "F0EBDA",
is_advanced: true, is_advanced: true,
dark_hex: "F0EBDA",
default_dark_hex: null,
}, },
{ {
name: "primary-100", name: "primary-100",
hex: "DAD8CA", hex: "DAD8CA",
default_hex: "DAD8CA", default_hex: "DAD8CA",
is_advanced: true, is_advanced: true,
dark_hex: "DAD8CA",
default_dark_hex: null,
}, },
{ {
name: "primary-200", name: "primary-200",
hex: "B2B9B3", hex: "B2B9B3",
default_hex: "B2B9B3", default_hex: "B2B9B3",
is_advanced: true, is_advanced: true,
dark_hex: "B2B9B3",
default_dark_hex: null,
}, },
{ {
name: "primary-300", name: "primary-300",
hex: "839496", hex: "839496",
default_hex: "839496", default_hex: "839496",
is_advanced: true, is_advanced: true,
dark_hex: "839496",
default_dark_hex: null,
}, },
{ {
name: "primary-400", name: "primary-400",
hex: "76898C", hex: "76898C",
default_hex: "76898C", default_hex: "76898C",
is_advanced: true, is_advanced: true,
dark_hex: "76898C",
default_dark_hex: null,
}, },
{ {
name: "primary-500", name: "primary-500",
hex: "697F83", hex: "697F83",
default_hex: "697F83", default_hex: "697F83",
is_advanced: true, is_advanced: true,
dark_hex: "697F83",
default_dark_hex: null,
}, },
{ {
name: "primary-600", name: "primary-600",
hex: "627A7E", hex: "627A7E",
default_hex: "627A7E", default_hex: "627A7E",
is_advanced: true, is_advanced: true,
dark_hex: "627A7E",
default_dark_hex: null,
}, },
{ {
name: "primary-700", name: "primary-700",
hex: "556F74", hex: "556F74",
default_hex: "556F74", default_hex: "556F74",
is_advanced: true, is_advanced: true,
dark_hex: "556F74",
default_dark_hex: null,
}, },
{ {
name: "primary-800", name: "primary-800",
hex: "415F66", hex: "415F66",
default_hex: "415F66", default_hex: "415F66",
is_advanced: true, is_advanced: true,
dark_hex: "415F66",
default_dark_hex: null,
}, },
{ {
name: "primary-900", name: "primary-900",
hex: "21454E", hex: "21454E",
default_hex: "21454E", default_hex: "21454E",
is_advanced: true, is_advanced: true,
dark_hex: "21454E",
default_dark_hex: null,
}, },
{ {
name: "secondary_low", name: "secondary_low",
hex: "325458", hex: "325458",
default_hex: "325458", default_hex: "325458",
is_advanced: true, is_advanced: true,
dark_hex: "325458",
default_dark_hex: null,
}, },
{ {
name: "secondary_medium", name: "secondary_medium",
hex: "6C8280", hex: "6C8280",
default_hex: "6C8280", default_hex: "6C8280",
is_advanced: true, is_advanced: true,
dark_hex: "6C8280",
default_dark_hex: null,
}, },
{ {
name: "secondary_high", name: "secondary_high",
hex: "97A59D", hex: "97A59D",
default_hex: "97A59D", default_hex: "97A59D",
is_advanced: true, is_advanced: true,
dark_hex: "97A59D",
default_dark_hex: null,
}, },
{ {
name: "secondary_very_high", name: "secondary_very_high",
hex: "E8E6D3", hex: "E8E6D3",
default_hex: "E8E6D3", default_hex: "E8E6D3",
is_advanced: true, is_advanced: true,
dark_hex: "E8E6D3",
default_dark_hex: null,
}, },
{ {
name: "secondary", name: "secondary",
hex: "FCF6E1", hex: "FCF6E1",
default_hex: "FCF6E1", default_hex: "FCF6E1",
is_advanced: false, is_advanced: false,
dark_hex: "FCF6E1",
default_dark_hex: null,
}, },
{ {
name: "tertiary_low", name: "tertiary_low",
hex: "D6E6DE", hex: "D6E6DE",
default_hex: "D6E6DE", default_hex: "D6E6DE",
is_advanced: true, is_advanced: true,
dark_hex: "D6E6DE",
default_dark_hex: null,
}, },
{ {
name: "tertiary_medium", name: "tertiary_medium",
hex: "7EBFD7", hex: "7EBFD7",
default_hex: "7EBFD7", default_hex: "7EBFD7",
is_advanced: true, is_advanced: true,
dark_hex: "7EBFD7",
default_dark_hex: null,
}, },
{ {
name: "tertiary", name: "tertiary",
hex: "0088cc", hex: "0088cc",
default_hex: "0088cc", default_hex: "0088cc",
is_advanced: false, is_advanced: false,
dark_hex: "0088cc",
default_dark_hex: null,
}, },
{ {
name: "tertiary_high", name: "tertiary_high",
hex: "329ED0", hex: "329ED0",
default_hex: "329ED0", default_hex: "329ED0",
is_advanced: true, is_advanced: true,
dark_hex: "329ED0",
default_dark_hex: null,
}, },
{ {
name: "quaternary", name: "quaternary",
hex: "e45735", hex: "e45735",
default_hex: "e45735", default_hex: "e45735",
is_advanced: false, is_advanced: false,
dark_hex: "e45735",
default_dark_hex: null,
}, },
{ {
name: "header_background", name: "header_background",
hex: "FCF6E1", hex: "FCF6E1",
default_hex: "FCF6E1", default_hex: "FCF6E1",
is_advanced: false, is_advanced: false,
dark_hex: "FCF6E1",
default_dark_hex: null,
}, },
{ {
name: "header_primary", name: "header_primary",
hex: "002B36", hex: "002B36",
default_hex: "002B36", default_hex: "002B36",
is_advanced: false, is_advanced: false,
dark_hex: "002B36",
default_dark_hex: null,
}, },
{ {
name: "highlight_low", name: "highlight_low",
hex: "FDF9AD", hex: "FDF9AD",
default_hex: "FDF9AD", default_hex: "FDF9AD",
is_advanced: true, is_advanced: true,
dark_hex: "FDF9AD",
default_dark_hex: null,
}, },
{ {
name: "highlight_medium", name: "highlight_medium",
hex: "E3D0A3", hex: "E3D0A3",
default_hex: "E3D0A3", default_hex: "E3D0A3",
is_advanced: true, is_advanced: true,
dark_hex: "E3D0A3",
default_dark_hex: null,
}, },
{ {
name: "highlight", name: "highlight",
hex: "F2F481", hex: "F2F481",
default_hex: "F2F481", default_hex: "F2F481",
is_advanced: false, is_advanced: false,
dark_hex: "F2F481",
default_dark_hex: null,
}, },
{ {
name: "highlight_high", name: "highlight_high",
hex: "BCAA7F", hex: "BCAA7F",
default_hex: "BCAA7F", default_hex: "BCAA7F",
is_advanced: true, is_advanced: true,
dark_hex: "BCAA7F",
default_dark_hex: null,
}, },
{ {
name: "selected", name: "selected",
hex: "E8E6D3", hex: "E8E6D3",
default_hex: "E8E6D3", default_hex: "E8E6D3",
is_advanced: false, is_advanced: false,
dark_hex: "E8E6D3",
default_dark_hex: null,
}, },
{ {
name: "hover", name: "hover",
hex: "F0EBDA", hex: "F0EBDA",
default_hex: "F0EBDA", default_hex: "F0EBDA",
is_advanced: false, is_advanced: false,
dark_hex: "F0EBDA",
default_dark_hex: null,
}, },
{ {
name: "danger_low", name: "danger_low",
hex: "F8D9C2", hex: "F8D9C2",
default_hex: "F8D9C2", default_hex: "F8D9C2",
is_advanced: true, is_advanced: true,
dark_hex: "F8D9C2",
default_dark_hex: null,
}, },
{ {
name: "danger", name: "danger",
hex: "e45735", hex: "e45735",
default_hex: "e45735", default_hex: "e45735",
is_advanced: false, is_advanced: false,
dark_hex: "e45735",
default_dark_hex: null,
}, },
{ {
name: "success_low", name: "success_low",
hex: "CFE5B9", hex: "CFE5B9",
default_hex: "CFE5B9", default_hex: "CFE5B9",
is_advanced: true, is_advanced: true,
dark_hex: "CFE5B9",
default_dark_hex: null,
}, },
{ {
name: "success_medium", name: "success_medium",
hex: "4CB544", hex: "4CB544",
default_hex: "4CB544", default_hex: "4CB544",
is_advanced: true, is_advanced: true,
dark_hex: "4CB544",
default_dark_hex: null,
}, },
{ {
name: "success", name: "success",
hex: "009900", hex: "009900",
default_hex: "009900", default_hex: "009900",
is_advanced: false, is_advanced: false,
dark_hex: "009900",
default_dark_hex: null,
}, },
{ {
name: "love_low", name: "love_low",
hex: "FCDDD2", hex: "FCDDD2",
default_hex: "FCDDD2", default_hex: "FCDDD2",
is_advanced: true, is_advanced: true,
dark_hex: "FCDDD2",
default_dark_hex: null,
}, },
{ {
name: "love", name: "love",
hex: "fa6c8d", hex: "fa6c8d",
default_hex: "fa6c8d", default_hex: "fa6c8d",
is_advanced: false, is_advanced: false,
dark_hex: "fa6c8d",
default_dark_hex: null,
}, },
], ],
is_dark: false, is_dark: false,

View file

@ -17,24 +17,18 @@ const DEFAULT_CONTENT = [
hex: "1a1a1a", hex: "1a1a1a",
default_hex: "222", default_hex: "222",
is_advanced: false, is_advanced: false,
dark_hex: "1a1a1a",
default_dark_hex: null,
}, },
{ {
name: "secondary", name: "secondary",
hex: "ffffff", hex: "ffffff",
default_hex: "fff", default_hex: "fff",
is_advanced: false, is_advanced: false,
dark_hex: "ffffff",
default_dark_hex: null,
}, },
{ {
name: "tertiary", name: "tertiary",
hex: "595bca", hex: "595bca",
default_hex: "08c", default_hex: "08c",
is_advanced: false, is_advanced: false,
dark_hex: "595bca",
default_dark_hex: null,
}, },
], ],
is_dark: false, is_dark: false,
@ -49,24 +43,18 @@ const DEFAULT_CONTENT = [
hex: "dddddd", hex: "dddddd",
default_hex: "dddddd", default_hex: "dddddd",
is_advanced: false, is_advanced: false,
dark_hex: "dddddd",
default_dark_hex: null,
}, },
{ {
name: "secondary", name: "secondary",
hex: "222222", hex: "222222",
default_hex: "222222", default_hex: "222222",
is_advanced: false, is_advanced: false,
dark_hex: "222222",
default_dark_hex: null,
}, },
{ {
name: "tertiary", name: "tertiary",
hex: "099dd7", hex: "099dd7",
default_hex: "099dd7", default_hex: "099dd7",
is_advanced: false, is_advanced: false,
dark_hex: "099dd7",
default_dark_hex: null,
}, },
], ],
is_dark: true, is_dark: true,

View file

@ -4,10 +4,7 @@ class Admin::ColorSchemesController < Admin::AdminController
before_action :fetch_color_scheme, only: %i[update destroy] before_action :fetch_color_scheme, only: %i[update destroy]


def index def index
schemes = schemes = ColorScheme.includes(:base_scheme).order("color_schemes.id ASC")
ColorScheme.without_theme_owned_palettes.includes(:base_scheme).order("color_schemes.id ASC")

schemes = schemes.where(theme_id: nil) if params[:exclude_theme_owned]


render_serialized(ColorScheme.base_color_schemes + schemes.to_a, ColorSchemeSerializer) render_serialized(ColorScheme.base_color_schemes + schemes.to_a, ColorSchemeSerializer)
end end
@ -38,13 +35,11 @@ class Admin::ColorSchemesController < Admin::AdminController
private private


def fetch_color_scheme def fetch_color_scheme
@color_scheme = ColorScheme.without_theme_owned_palettes.find(params[:id]) @color_scheme = ColorScheme.find(params[:id])
end end


def color_scheme_params def color_scheme_params
params.permit( params.permit(color_scheme: [:base_scheme_id, :name, :user_selectable, colors: %i[name hex]])[
color_scheme: [:base_scheme_id, :name, :user_selectable, colors: %i[name hex dark_hex]],
)[
:color_scheme :color_scheme
] ]
end end

View file

@ -2,7 +2,7 @@


class Admin::Config::ColorPalettesController < Admin::AdminController class Admin::Config::ColorPalettesController < Admin::AdminController
def index def index
palettes = ColorScheme.without_theme_owned_palettes.to_a palettes = ColorScheme.all.to_a
palettes.unshift(ColorScheme.base) palettes.unshift(ColorScheme.base)
default_theme = Theme.find_default default_theme = Theme.find_default
default_light_palette = default_theme&.color_scheme_id default_light_palette = default_theme&.color_scheme_id
@ -55,10 +55,6 @@ class Admin::Config::ColorPalettesController < Admin::AdminController
end end


def show def show
render_serialized( render_serialized(ColorScheme.find(params[:id]), ColorSchemeSerializer, root: false)
ColorScheme.without_theme_owned_palettes.find(params[:id]),
ColorSchemeSerializer,
root: false,
)
end end
end end

View file

@ -176,7 +176,6 @@ class Admin::ThemesController < Admin::AdminController
ColorScheme ColorScheme
.strict_loading .strict_loading
.all .all
.without_theme_owned_palettes
.includes( .includes(
:theme, :theme,
:base_scheme, :base_scheme,
@ -402,20 +401,6 @@ class Admin::ThemesController < Admin::AdminController
render_serialized(theme_setting, ThemeObjectsSettingMetadataSerializer, root: false) render_serialized(theme_setting, ThemeObjectsSettingMetadataSerializer, root: false)
end end


def change_colors
raise Discourse::InvalidAccess if params[:id].to_i.negative?
theme = Theme.find_by(id: params[:id], component: false)
raise Discourse::NotFound if !theme

palette = theme.find_or_create_owned_color_palette

colors = params.permit(colors: %i[name hex dark_hex])

ColorSchemeRevisor.revise_existing_colors_only(palette, colors)

render_serialized(palette, ColorSchemeSerializer, root: false)
end

private private


def ban_in_allowlist_mode! def ban_in_allowlist_mode!

View file

@ -23,15 +23,9 @@ class StylesheetsController < ApplicationController
def color_scheme def color_scheme
params.require("id") params.require("id")
params.permit("theme_id") params.permit("theme_id")
params.permit("include_dark_scheme")


manager = Stylesheet::Manager.new(theme_id: params[:theme_id]) manager = Stylesheet::Manager.new(theme_id: params[:theme_id])
stylesheet = stylesheet = manager.color_scheme_stylesheet_details(params[:id], fallback_to_base: true)
manager.color_scheme_stylesheet_details(
params[:id],
fallback_to_base: true,
include_dark_scheme: !!params[:include_dark_scheme],
)


render json: stylesheet render json: stylesheet
end end

View file

@ -575,10 +575,7 @@ module ApplicationHelper
return user_scheme_id if user_scheme_id return user_scheme_id if user_scheme_id
return if theme_id.blank? return if theme_id.blank?


if SiteSetting.use_overhauled_theme_color_palette @scheme_id = Theme.where(id: theme_id).pick(:color_scheme_id)
@scheme_id = ThemeColorScheme.where(theme_id: theme_id).pick(:color_scheme_id)
end
@scheme_id ||= Theme.where(id: theme_id).pick(:color_scheme_id)
end end


def user_dark_scheme_id def user_dark_scheme_id
@ -588,12 +585,8 @@ module ApplicationHelper
end end


def dark_scheme_id def dark_scheme_id
if SiteSetting.use_overhauled_theme_color_palette user_dark_scheme_id ||
scheme_id (theme_id ? Theme.find_by_id(theme_id) : Theme.find_default)&.dark_color_scheme_id || -1
else
user_dark_scheme_id ||
(theme_id ? Theme.find_by_id(theme_id) : Theme.find_default)&.dark_color_scheme_id || -1
end
end end


def current_homepage def current_homepage
@ -684,7 +677,6 @@ module ApplicationHelper
if dark_scheme_id != -1 if dark_scheme_id != -1
result << stylesheet_manager.color_scheme_stylesheet_preload_tag( result << stylesheet_manager.color_scheme_stylesheet_preload_tag(
dark_scheme_id, dark_scheme_id,
dark: SiteSetting.use_overhauled_theme_color_palette,
fallback_to_base: false, fallback_to_base: false,
) )
end end
@ -702,7 +694,6 @@ module ApplicationHelper
dark_href = dark_href =
stylesheet_manager.color_scheme_stylesheet_link_tag_href( stylesheet_manager.color_scheme_stylesheet_link_tag_href(
dark_scheme_id, dark_scheme_id,
dark: SiteSetting.use_overhauled_theme_color_palette,
fallback_to_base: false, fallback_to_base: false,
) )
end end
@ -786,11 +777,7 @@ module ApplicationHelper
end end


def dark_color_hex_for_name(name) def dark_color_hex_for_name(name)
ColorScheme.hex_for_name( ColorScheme.hex_for_name(name, dark_scheme_id)
name,
dark_scheme_id,
dark: SiteSetting.use_overhauled_theme_color_palette,
)
end end


def dark_elements_media_query def dark_elements_media_query

View file

@ -346,12 +346,6 @@ class ColorScheme < ActiveRecord::Base
belongs_to :theme belongs_to :theme
belongs_to :base_scheme, class_name: "ColorScheme" belongs_to :base_scheme, class_name: "ColorScheme"


has_one :theme_color_scheme, dependent: :destroy
has_one :owning_theme, class_name: "Theme", through: :theme_color_scheme, source: :theme

scope :without_theme_owned_palettes,
-> { where("color_schemes.id NOT IN (SELECT color_scheme_id FROM theme_color_schemes)") }

validates_associated :color_scheme_colors validates_associated :color_scheme_colors


BASE_COLORS_FILE = "#{Rails.root}/app/assets/stylesheets/common/foundation/colors.scss" BASE_COLORS_FILE = "#{Rails.root}/app/assets/stylesheets/common/foundation/colors.scss"
@ -450,18 +444,17 @@ class ColorScheme < ActiveRecord::Base
new_color_scheme new_color_scheme
end end


def self.lookup_hex_for_name(name, scheme_id = nil, dark: false) def self.lookup_hex_for_name(name, scheme_id = nil)
enabled_color_scheme = find_by(id: scheme_id) if scheme_id enabled_color_scheme = find_by(id: scheme_id) if scheme_id
enabled_color_scheme ||= Theme.where(id: SiteSetting.default_theme_id).first&.color_scheme enabled_color_scheme ||= Theme.where(id: SiteSetting.default_theme_id).first&.color_scheme
color_record = (enabled_color_scheme || base).colors.find { |c| c.name == name } color_record = (enabled_color_scheme || base).colors.find { |c| c.name == name }
return if !color_record return if !color_record
dark ? color_record.dark_hex || color_record.hex : color_record.hex color_record.hex
end end


def self.hex_for_name(name, scheme_id = nil, dark: false) def self.hex_for_name(name, scheme_id = nil)
cache_key = scheme_id ? "#{name}_#{scheme_id}" : name cache_key = scheme_id ? "#{name}_#{scheme_id}" : name
cache_key += "_dark" if dark hex_cache.defer_get_set(cache_key) { lookup_hex_for_name(name, scheme_id) }
hex_cache.defer_get_set(cache_key) { lookup_hex_for_name(name, scheme_id, dark:) }
end end


def colors=(arr) def colors=(arr)
@ -501,16 +494,10 @@ class ColorScheme < ActiveRecord::Base
colors || (base_scheme_id ? {} : ColorScheme.base_colors) colors || (base_scheme_id ? {} : ColorScheme.base_colors)
end end


def resolved_colors(dark: false) def resolved_colors
from_base = ColorScheme.base_colors from_base = ColorScheme.base_colors
from_custom_scheme = base_colors from_custom_scheme = base_colors
from_db = from_db = colors.map { |c| [c.name, c.hex] }.to_h
colors
.map do |c|
hex = dark ? (c.dark_hex || c.hex) : c.hex
[c.name, hex]
end
.to_h


resolved = from_base.merge(from_custom_scheme).except("hover", "selected").merge(from_db) resolved = from_base.merge(from_custom_scheme).except("hover", "selected").merge(from_db)



View file

@ -1,10 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true


class ColorSchemeColor < ActiveRecord::Base class ColorSchemeColor < ActiveRecord::Base
self.ignored_columns = [
"dark_hex", # TODO: Remove when 20250821155127_drop_dark_hex_from_color_scheme_color has been promoted to pre-deploy
]

belongs_to :color_scheme belongs_to :color_scheme


validates :hex, format: { with: /\A([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\z/ } validates :hex, format: { with: /\A([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\z/ }
validates :dark_hex, format: { with: /\A([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\z/ }, allow_nil: true


def hex_with_hash def hex_with_hash
"##{hex}" "##{hex}"
@ -16,12 +19,11 @@ end
# Table name: color_scheme_colors # Table name: color_scheme_colors
# #
# id :integer not null, primary key # id :integer not null, primary key
# name :string not null
# hex :string not null # hex :string not null
# color_scheme_id :integer not null # name :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# dark_hex :string(6) # color_scheme_id :integer not null
# #
# Indexes # Indexes
# #

View file

@ -2,12 +2,12 @@


class ColorSchemeSetting < EnumSiteSetting class ColorSchemeSetting < EnumSiteSetting
def self.valid_value?(val) def self.valid_value?(val)
val == -1 || ColorScheme.without_theme_owned_palettes.find_by_id(val) val == -1 || ColorScheme.find_by_id(val)
end end


def self.values def self.values
values = [{ name: I18n.t("site_settings.dark_mode_none"), value: -1 }] values = [{ name: I18n.t("site_settings.dark_mode_none"), value: -1 }]
ColorScheme.all.without_theme_owned_palettes.map { |c| values << { name: c.name, value: c.id } } ColorScheme.all.map { |c| values << { name: c.name, value: c.id } }
values values
end end
end end

View file

@ -66,13 +66,6 @@ class Theme < ActiveRecord::Base
-> { where(target_id: Theme.targets[:settings], name: "yaml") }, -> { where(target_id: Theme.targets[:settings], name: "yaml") },
class_name: "ThemeField" class_name: "ThemeField"
has_one :javascript_cache, dependent: :destroy has_one :javascript_cache, dependent: :destroy
has_one :theme_color_scheme, dependent: :destroy
has_one :owned_color_scheme,
class_name: "ColorScheme",
through: :theme_color_scheme,
source: :color_scheme
alias_method :owned_color_palette, :owned_color_scheme
alias_method :owned_color_palette=, :owned_color_scheme=


has_many :locale_fields, has_many :locale_fields,
-> { filter_locale_fields(I18n.fallbacks[I18n.locale]) }, -> { filter_locale_fields(I18n.fallbacks[I18n.locale]) },
@ -129,7 +122,6 @@ class Theme < ActiveRecord::Base
:locale_fields, :locale_fields,
:theme_translation_overrides, :theme_translation_overrides,
color_scheme: %i[theme color_scheme_colors base_scheme], color_scheme: %i[theme color_scheme_colors base_scheme],
owned_color_scheme: %i[theme color_scheme_colors base_scheme],
parent_themes: %i[color_scheme locale_fields theme_translation_overrides], parent_themes: %i[color_scheme locale_fields theme_translation_overrides],
) )
end end
@ -1089,40 +1081,6 @@ class Theme < ActiveRecord::Base
ThemeSiteSettingResolver.new(theme: self).resolved_theme_site_settings ThemeSiteSettingResolver.new(theme: self).resolved_theme_site_settings
end end


def find_or_create_owned_color_palette
Theme.transaction do
next self.owned_color_palette if self.owned_color_palette

palette = self.color_palette || ColorScheme.base

copy = palette.dup
copy.theme_id = self.id
copy.base_scheme_id = nil
copy.user_selectable = false
copy.via_wizard = false
copy.save!

result =
ThemeColorScheme.insert_all(
[{ theme_id: self.id, color_scheme_id: copy.id }],
unique_by: :index_theme_color_schemes_on_theme_id,
)

if result.rows.size == 0
# race condition, a palette has already been associated with this theme
copy.destroy!
self.reload.owned_color_palette
else
ColorSchemeColor.insert_all(
palette.colors.map do |color|
{ color_scheme_id: copy.id, name: color.name, hex: color.hex, dark_hex: color.dark_hex }
end,
)
copy.reload
end
end
end

private private


attr_accessor :theme_setting_requests_refresh attr_accessor :theme_setting_requests_refresh

View file

@ -1,22 +0,0 @@
# frozen_string_literal: true

class ThemeColorScheme < ActiveRecord::Base
belongs_to :theme
belongs_to :color_scheme, dependent: :destroy
end

# == Schema Information
#
# Table name: theme_color_schemes
#
# id :bigint not null, primary key
# theme_id :integer not null
# color_scheme_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_theme_color_schemes_on_color_scheme_id (color_scheme_id) UNIQUE
# index_theme_color_schemes_on_theme_id (theme_id) UNIQUE
#

View file

@ -1,16 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true


class ColorSchemeColorSerializer < ApplicationSerializer class ColorSchemeColorSerializer < ApplicationSerializer
attributes :name, :hex, :default_hex, :is_advanced, :dark_hex, :default_dark_hex attributes :name, :hex, :default_hex, :is_advanced


def hex def hex
object.hex object.hex
end end


def dark_hex
object.dark_hex || object.hex
end

def default_hex def default_hex
# return the hex value of the color when it is already a base color or no base_scheme is set # return the hex value of the color when it is already a base color or no base_scheme is set
if !object.color_scheme || object.color_scheme.base_scheme_id == 0 if !object.color_scheme || object.color_scheme.base_scheme_id == 0
@ -20,12 +16,6 @@ class ColorSchemeColorSerializer < ApplicationSerializer
end end
end end


def default_dark_hex
# TODO(osama) implement this when we add dark mode colors for built-in
# palettes
nil
end

def is_advanced def is_advanced
!ColorScheme.base_colors.keys.include?(object.name) !ColorScheme.base_colors.keys.include?(object.name)
end end

View file

@ -19,8 +19,6 @@ class ThemeSerializer < BasicThemeSerializer
:system :system


has_one :color_scheme, serializer: ColorSchemeSerializer, embed: :object has_one :color_scheme, serializer: ColorSchemeSerializer, embed: :object
has_one :owned_color_palette, serializer: ColorSchemeSerializer, embed: :object
has_one :base_palette, serializer: ColorSchemeSerializer, embed: :object
has_one :user, serializer: UserNameSerializer, embed: :object has_one :user, serializer: UserNameSerializer, embed: :object
has_one :disabled_by, serializer: UserNameSerializer, embed: :object has_one :disabled_by, serializer: UserNameSerializer, embed: :object


@ -60,14 +58,6 @@ class ThemeSerializer < BasicThemeSerializer
object.parent_themes object.parent_themes
end end


def base_palette
ColorScheme.base
end

def include_base_palette?
object.color_scheme_id.blank? && object.owned_color_palette.blank?
end

def settings def settings
object.settings.map do |_name, setting| object.settings.map do |_name, setting|
ThemeSettingsSerializer.new(setting, scope:, root: false) ThemeSettingsSerializer.new(setting, scope:, root: false)

View file

@ -28,11 +28,7 @@ class ColorSchemeRevisor
if existing = @color_scheme.colors_by_name[c[:name]] if existing = @color_scheme.colors_by_name[c[:name]]
existing.update(c) existing.update(c)
elsif !update_existing_colors_only elsif !update_existing_colors_only
@color_scheme.color_scheme_colors << ColorSchemeColor.new( @color_scheme.color_scheme_colors << ColorSchemeColor.new(name: c[:name], hex: c[:hex])
name: c[:name],
hex: c[:hex],
dark_hex: c[:dark_hex],
)
end end
end end
@color_scheme.clear_colors_cache @color_scheme.clear_colors_cache

View file

@ -7051,15 +7051,6 @@ en:
repo_unreachable: "Couldn't contact the Git repository of this theme. Error message:" repo_unreachable: "Couldn't contact the Git repository of this theme. Error message:"
built_in_description: "This theme is preinstalled and can not be deleted or customized" built_in_description: "This theme is preinstalled and can not be deleted or customized"
imported_from_archive: "This theme was imported from a .zip file" imported_from_archive: "This theme was imported from a .zip file"
settings: "Settings"
colors: "Colors"
unsaved_colors:
one: "You have %{count} unsaved color"
other: "You have %{count} unsaved colors"
unsaved_colors_leave_route_confirmation: "You haven't saved your color palette changes yet. Are you sure you want to leave?"
save_colors: "Save colors"
discard_colors: "Discard"
colors_title: "Colors"
scss: scss:
text: "CSS" text: "CSS"
title: "Custom CSS rules with support for SCSS syntax" title: "Custom CSS rules with support for SCSS syntax"

View file

@ -254,7 +254,6 @@ Discourse::Application.routes.draw do
put "setting" => "themes#update_single_setting" put "setting" => "themes#update_single_setting"
put "site-setting" => "themes#update_theme_site_setting" put "site-setting" => "themes#update_theme_site_setting"
get "objects_setting_metadata/:setting_name" => "themes#objects_setting_metadata" get "objects_setting_metadata/:setting_name" => "themes#objects_setting_metadata"
put "change-colors" => "themes#change_colors"
end end


collection do collection do
@ -275,7 +274,6 @@ Discourse::Application.routes.draw do
get "components/:id" => "themes#index" get "components/:id" => "themes#index"
get "components/:id/:target/:field_name/edit" => "themes#index" get "components/:id/:target/:field_name/edit" => "themes#index"
get "themes/:id/export" => "themes#export" get "themes/:id/export" => "themes#export"
get "themes/:id/colors" => "themes#index"
get "themes/:id/schema/:setting_name" => "themes#schema" get "themes/:id/schema/:setting_name" => "themes#schema"
get "components/:id/schema/:setting_name" => "themes#schema" get "components/:id/schema/:setting_name" => "themes#schema"



View file

@ -4150,11 +4150,6 @@ experimental:
type: group_list type: group_list
list_type: compact list_type: compact
area: "group_permissions|experimental" area: "group_permissions|experimental"
use_overhauled_theme_color_palette:
default: false
hidden: true
client: true
area: "experimental"
reviewable_ui_refresh: reviewable_ui_refresh:
client: true client: true
type: group_list type: group_list

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true

class RemoveUseOverhauledThemeColorPaletteSetting < ActiveRecord::Migration[8.0]
def up
execute "DELETE FROM site_settings WHERE name = 'use_overhauled_theme_color_palette'"
end

def down
raise ActiveRecord::IrreversibleMigration
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true

class DropDarkHexFromColorSchemeColor < ActiveRecord::Migration[8.0]
DROPPED_COLUMNS = { color_scheme_colors: %i[dark_hex] }

def up
DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) }
end

def down
raise ActiveRecord::IrreversibleMigration
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true

class DropThemeColorScheme < ActiveRecord::Migration[8.0]
DROPPED_TABLES = %i[theme_color_schemes]

def up
delete_ids = DB.query_single("SELECT color_scheme_id FROM theme_color_schemes")

if delete_ids.length > 0
execute("DELETE FROM color_schemes WHERE id IN (#{delete_ids.join(",")})")
execute("DELETE FROM color_scheme_colors WHERE color_scheme_id IN (#{delete_ids.join(",")})")
end

DROPPED_TABLES.each { |table| Migration::TableDropper.execute_drop(table) }
end

def down
raise ActiveRecord::IrreversibleMigration
end
end

View file

@ -160,21 +160,19 @@ module Stylesheet
if @color_scheme_id if @color_scheme_id
colors = colors =
begin begin
ColorScheme.find(@color_scheme_id).resolved_colors(dark: @dark) ColorScheme.find(@color_scheme_id).resolved_colors
rescue StandardError rescue StandardError
ColorScheme.base_colors ColorScheme.base_colors
end end
elsif (@theme_id && !theme.component) elsif (@theme_id && !theme.component)
colors = theme&.color_scheme&.resolved_colors(dark: @dark) || ColorScheme.base_colors colors = theme&.color_scheme&.resolved_colors || ColorScheme.base_colors
else else
# this is a slightly ugly backwards compatibility fix, # this is a slightly ugly backwards compatibility fix,
# we shouldn't be using the default theme color scheme for components # we shouldn't be using the default theme color scheme for components
# (most components use CSS custom properties which work fine without this) # (most components use CSS custom properties which work fine without this)
colors = colors =
Theme Theme.find_by_id(SiteSetting.default_theme_id)&.color_scheme&.resolved_colors ||
.find_by_id(SiteSetting.default_theme_id) ColorScheme.base_colors
&.color_scheme
&.resolved_colors(dark: @dark) || ColorScheme.base_colors
end end


colors.each { |n, hex| contents << "$#{n}: ##{hex} !default; " } colors.each { |n, hex| contents << "$#{n}: ##{hex} !default; " }
@ -195,7 +193,6 @@ module Stylesheet
@theme = options[:theme] @theme = options[:theme]
@theme_id = options[:theme_id] @theme_id = options[:theme_id]
@color_scheme_id = options[:color_scheme_id] @color_scheme_id = options[:color_scheme_id]
@dark = options[:dark]


if @theme && !@theme_id if @theme && !@theme_id
# make up an id so other stuff does not bail out # make up an id so other stuff does not bail out

View file

@ -42,11 +42,10 @@ class Stylesheet::Manager
cache.clear_regex(/#{plugin}/) cache.clear_regex(/#{plugin}/)
end end


def self.color_scheme_cache_key(color_scheme, theme_id = nil, dark: false) def self.color_scheme_cache_key(color_scheme, theme_id = nil)
color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s
theme_string = theme_id ? "_theme#{theme_id}" : "" theme_string = theme_id ? "_theme#{theme_id}" : ""
dark_string = dark ? "_dark" : "" "#{COLOR_SCHEME_STYLESHEET}_#{color_scheme_name}_#{theme_string}_#{Discourse.current_hostname}_#{GlobalSetting.relative_url_root}"
"#{COLOR_SCHEME_STYLESHEET}_#{color_scheme_name}_#{theme_string}_#{Discourse.current_hostname}_#{GlobalSetting.relative_url_root}_#{dark_string}"
end end


def self.precompile_css def self.precompile_css
@ -117,17 +116,13 @@ class Stylesheet::Manager
theme_dark_color_scheme = ColorScheme.find_by_id(dark_color_scheme_id) theme_dark_color_scheme = ColorScheme.find_by_id(dark_color_scheme_id)
theme = manager.get_theme(theme_id) theme = manager.get_theme(theme_id)
[theme_color_scheme, theme_dark_color_scheme, *color_schemes].compact.uniq.each do |scheme| [theme_color_scheme, theme_dark_color_scheme, *color_schemes].compact.uniq.each do |scheme|
[true, false].each do |dark| $stderr.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{theme.name} (#{scheme.name})"
mode = dark ? "dark" : "light" Stylesheet::Manager::Builder.new(
$stderr.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{theme.name} (#{scheme.name}) (#{mode})" target: COLOR_SCHEME_STYLESHEET,
Stylesheet::Manager::Builder.new( theme: theme,
target: COLOR_SCHEME_STYLESHEET, color_scheme: scheme,
theme: theme, manager: manager,
color_scheme: scheme, ).compile(force: true)
manager: manager,
dark:,
).compile(force: true)
end
end end


clear_color_scheme_cache! clear_color_scheme_cache!
@ -344,12 +339,7 @@ class Stylesheet::Manager
end end
end end


def color_scheme_stylesheet_details( def color_scheme_stylesheet_details(color_scheme_id = nil, fallback_to_base: true)
color_scheme_id = nil,
dark: false,
fallback_to_base: true,
include_dark_scheme: false
)
theme_id = @theme_id || SiteSetting.default_theme_id theme_id = @theme_id || SiteSetting.default_theme_id


color_scheme = ColorScheme.find_by(id: color_scheme_id) color_scheme = ColorScheme.find_by(id: color_scheme_id)
@ -361,10 +351,10 @@ class Stylesheet::Manager


target = COLOR_SCHEME_STYLESHEET.to_sym target = COLOR_SCHEME_STYLESHEET.to_sym
current_hostname = Discourse.current_hostname current_hostname = Discourse.current_hostname
cache_key = self.class.color_scheme_cache_key(color_scheme, theme_id, dark:) cache_key = self.class.color_scheme_cache_key(color_scheme, theme_id)


cache.defer_get_set(cache_key) do cache.defer_get_set(cache_key) do
stylesheet = { color_scheme_id: color_scheme.id, dark: } stylesheet = { color_scheme_id: color_scheme.id }


theme = get_theme(theme_id) theme = get_theme(theme_id)


@ -374,7 +364,6 @@ class Stylesheet::Manager
theme: get_theme(theme_id), theme: get_theme(theme_id),
color_scheme: color_scheme, color_scheme: color_scheme,
manager: self, manager: self,
dark:,
) )


builder.compile unless File.exist?(builder.stylesheet_fullpath) builder.compile unless File.exist?(builder.stylesheet_fullpath)
@ -382,27 +371,12 @@ class Stylesheet::Manager
href = builder.stylesheet_absolute_url href = builder.stylesheet_absolute_url
stylesheet[:new_href] = href stylesheet[:new_href] = href


if include_dark_scheme
dark_href =
self.color_scheme_stylesheet_link_tag_href(
color_scheme_id,
dark: true,
fallback_to_base: false,
)

stylesheet[:new_dark_href] = dark_href if dark_href
end

stylesheet.freeze stylesheet.freeze
end end
end end


def color_scheme_stylesheet_preload_tag( def color_scheme_stylesheet_preload_tag(color_scheme_id = nil, fallback_to_base: true)
color_scheme_id = nil, stylesheet = color_scheme_stylesheet_details(color_scheme_id, fallback_to_base:)
dark: false,
fallback_to_base: true
)
stylesheet = color_scheme_stylesheet_details(color_scheme_id, dark:, fallback_to_base:)


return "" if !stylesheet return "" if !stylesheet


@ -411,12 +385,8 @@ class Stylesheet::Manager
%[<link href="#{href}" rel="preload" as="style"/>].html_safe %[<link href="#{href}" rel="preload" as="style"/>].html_safe
end end


def color_scheme_stylesheet_link_tag_href( def color_scheme_stylesheet_link_tag_href(color_scheme_id = nil, fallback_to_base: true)
color_scheme_id = nil, stylesheet = color_scheme_stylesheet_details(color_scheme_id, fallback_to_base:)
dark: false,
fallback_to_base: true
)
stylesheet = color_scheme_stylesheet_details(color_scheme_id, dark:, fallback_to_base:)


return if !stylesheet return if !stylesheet



View file

@ -3,12 +3,11 @@
class Stylesheet::Manager::Builder class Stylesheet::Manager::Builder
attr_reader :theme attr_reader :theme


def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:, dark: false) def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:)
@target = target @target = target
@theme = theme @theme = theme
@color_scheme = color_scheme @color_scheme = color_scheme
@manager = manager @manager = manager
@dark = dark
end end


def compile(opts = {}) def compile(opts = {})
@ -47,7 +46,6 @@ class Stylesheet::Manager::Builder
source_map_file: source_map_url_relative_from_stylesheet, source_map_file: source_map_url_relative_from_stylesheet,
color_scheme_id: @color_scheme&.id, color_scheme_id: @color_scheme&.id,
load_paths: load_paths, load_paths: load_paths,
dark: @dark,
strict_deprecations: %i[desktop mobile admin wizard].include?(@target), strict_deprecations: %i[desktop mobile admin wizard].include?(@target),
) )
rescue SassC::SyntaxError, SassC::NotRenderedError, DiscourseJsProcessor::TranspileError => e rescue SassC::SyntaxError, SassC::NotRenderedError, DiscourseJsProcessor::TranspileError => e
@ -122,14 +120,13 @@ class Stylesheet::Manager::Builder
end end


def qualified_target def qualified_target
dark_string = @dark ? "_dark" : ""
if is_theme? if is_theme?
"#{@target}_#{theme&.id}" "#{@target}_#{theme&.id}"
elsif @color_scheme elsif @color_scheme
"#{@target}_#{scheme_slug}_#{@color_scheme&.id}_#{@theme&.id}#{dark_string}" "#{@target}_#{scheme_slug}_#{@color_scheme&.id}_#{@theme&.id}"
else else
scheme_string = theme&.color_scheme ? "_#{theme.color_scheme.id}" : "" scheme_string = theme&.color_scheme ? "_#{theme.color_scheme.id}" : ""
"#{@target}#{scheme_string}#{dark_string}" "#{@target}#{scheme_string}"
end end
end end


@ -250,9 +247,8 @@ class Stylesheet::Manager::Builder
digest_string = "#{current_hostname}-" digest_string = "#{current_hostname}-"
if cs if cs
theme_color_defs = resolve_baked_field(:common, :color_definitions) theme_color_defs = resolve_baked_field(:common, :color_definitions)
dark_string = @dark ? "-dark" : ""
digest_string += digest_string +=
"#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}#{dark_string}" "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"
else else
digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}" digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"



View file

@ -956,20 +956,12 @@ RSpec.describe ApplicationHelper do
describe "#discourse_theme_color_meta_tags" do describe "#discourse_theme_color_meta_tags" do
before do before do
light = Fabricate(:color_scheme) light = Fabricate(:color_scheme)
light.color_scheme_colors << ColorSchemeColor.new( light.color_scheme_colors << ColorSchemeColor.new(name: "header_background", hex: "abcdef")
name: "header_background",
hex: "abcdef",
dark_hex: "fedcba",
)
light.save! light.save!
helper.request.cookies["color_scheme_id"] = light.id helper.request.cookies["color_scheme_id"] = light.id


dark = Fabricate(:color_scheme) dark = Fabricate(:color_scheme)
dark.color_scheme_colors << ColorSchemeColor.new( dark.color_scheme_colors << ColorSchemeColor.new(name: "header_background", hex: "defabc")
name: "header_background",
hex: "defabc",
dark_hex: "cbafed",
)
dark.save! dark.save!
helper.request.cookies["dark_scheme_id"] = dark.id helper.request.cookies["dark_scheme_id"] = dark.id
end end
@ -989,17 +981,6 @@ RSpec.describe ApplicationHelper do
<meta name="theme-color" media="all" content="#abcdef"> <meta name="theme-color" media="all" content="#abcdef">
HTML HTML
end end

context "when use_overhauled_theme_color_palette setting is true" do
before { SiteSetting.use_overhauled_theme_color_palette = true }

it "renders a light and dark theme-color meta tag using the light and dark palettes of the same color scheme record" do
expect(helper.discourse_theme_color_meta_tags).to eq(<<~HTML)
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#abcdef">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#fedcba">
HTML
end
end
end end


describe "#discourse_color_scheme_meta_tag" do describe "#discourse_color_scheme_meta_tag" do
@ -1055,15 +1036,6 @@ RSpec.describe ApplicationHelper do
it "returns the value set in the dark_scheme_id cookie" do it "returns the value set in the dark_scheme_id cookie" do
expect(helper.dark_scheme_id).to eq(dark_scheme.id) expect(helper.dark_scheme_id).to eq(dark_scheme.id)
end end

context "when use_overhauled_theme_color_palette is true" do
before { SiteSetting.use_overhauled_theme_color_palette = true }

it "returns the same value as #scheme_id" do
expect(helper.dark_scheme_id).to eq(helper.scheme_id)
expect(helper.scheme_id).to eq(light_scheme.id)
end
end
end end


describe "#forced_light_mode?" do describe "#forced_light_mode?" do

View file

@ -688,81 +688,6 @@ RSpec.describe Stylesheet::Manager do
expect(href).to include("/stylesheets/color_definitions_funky-bunch_#{cs.id}_") expect(href).to include("/stylesheets/color_definitions_funky-bunch_#{cs.id}_")
end end


it "generates the dark mode of a color scheme when the dark option is specified" do
scheme =
ColorScheme.create_from_base(
name: "Neutral",
base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Neutral"],
)
ColorSchemeRevisor.revise(
scheme,
colors: [{ name: "primary", hex: "CABFAF", dark_hex: "FAFCAB" }],
)
theme = Fabricate(:theme)
manager = manager(theme.id)

dark_stylesheet =
Stylesheet::Manager::Builder.new(
target: :color_definitions,
theme: theme,
color_scheme: scheme,
manager: manager,
dark: true,
).compile
light_stylesheet =
Stylesheet::Manager::Builder.new(
target: :color_definitions,
theme: theme,
color_scheme: scheme,
manager: manager,
).compile

expect(light_stylesheet).to include("--primary: #CABFAF;")
expect(light_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}")
expect(light_stylesheet).not_to include(
"color_definitions_neutral_#{scheme.id}_#{theme.id}_dark",
)

expect(dark_stylesheet).to include("--primary: #FAFCAB;")
expect(dark_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}_dark")
end

it "uses the light colors as fallback if the dark scheme doesn't define them" do
scheme =
ColorScheme.create_from_base(
name: "Neutral",
base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Neutral"],
)
ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "BACFAB", dark_hex: nil }])
theme = Fabricate(:theme)
manager = manager(theme.id)

dark_stylesheet =
Stylesheet::Manager::Builder.new(
target: :color_definitions,
theme: theme,
color_scheme: scheme,
manager: manager,
dark: true,
).compile
light_stylesheet =
Stylesheet::Manager::Builder.new(
target: :color_definitions,
theme: theme,
color_scheme: scheme,
manager: manager,
).compile

expect(light_stylesheet).to include("--primary: #BACFAB;")
expect(light_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}")
expect(light_stylesheet).not_to include(
"color_definitions_neutral_#{scheme.id}_#{theme.id}_dark",
)

expect(dark_stylesheet).to include("--primary: #BACFAB;")
expect(dark_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}_dark")
end

it "updates outputted colors when updating a color scheme" do it "updates outputted colors when updating a color scheme" do
scheme = scheme =
ColorScheme.create_from_base( ColorScheme.create_from_base(
@ -1004,7 +929,7 @@ RSpec.describe Stylesheet::Manager do


# Ensure we force compile each theme only once # Ensure we force compile each theme only once
expect(output.scan(/#{child_theme_with_css.name}/).length).to eq(2) # ltr/rtl expect(output.scan(/#{child_theme_with_css.name}/).length).to eq(2) # ltr/rtl
expect(StylesheetCache.count).to eq(10) # (2 theme with rtl/ltr) + 8 color schemes (2 themes * 2 color schemes (1 base light palette + 1 theme scheme) * 2 (light and dark mode per scheme)) expect(StylesheetCache.count).to eq(6) # (2 theme with rtl/ltr) + 4 color schemes (2 themes * 2 color schemes (1 base light palette + 1 theme scheme))
end end


it "generates precompiled CSS - core and themes" do it "generates precompiled CSS - core and themes" do
@ -1012,7 +937,7 @@ RSpec.describe Stylesheet::Manager do
Stylesheet::Manager.precompile_theme_css Stylesheet::Manager.precompile_theme_css


results = StylesheetCache.pluck(:target) results = StylesheetCache.pluck(:target)
expect(results.size).to eq(20) # 10 core targets + 2 theme (ltr/rtl) + 8 color schemes (light and dark mode per scheme) expect(results.size).to eq(16) # 10 core targets + 2 theme (ltr/rtl) + 4 color schemes


expect(results.count { |target| target =~ /^common_theme_/ }).to eq(2) # ltr/rtl expect(results.count { |target| target =~ /^common_theme_/ }).to eq(2) # ltr/rtl
end end
@ -1024,7 +949,7 @@ RSpec.describe Stylesheet::Manager do
Stylesheet::Manager.precompile_theme_css Stylesheet::Manager.precompile_theme_css


results = StylesheetCache.pluck(:target) results = StylesheetCache.pluck(:target)
expect(results.size).to eq(24) # 10 core targets + 2 theme rtl/ltr + 12 color schemes (light and dark mode per scheme) expect(results.size).to eq(18) # 10 core targets + 2 theme rtl/ltr + 6 color schemes


expect(results).to include("color_definitions_#{scheme1.name}_#{scheme1.id}_#{user_theme.id}") expect(results).to include("color_definitions_#{scheme1.name}_#{scheme1.id}_#{user_theme.id}")
expect(results).to include( expect(results).to include(

View file

@ -1575,17 +1575,6 @@ HTML
end end
end end


describe "#owned_color_scheme" do
it "is destroyed when the theme is destroyed" do
scheme = Fabricate(:color_scheme, owning_theme: theme)

theme.destroy!

expect(ThemeColorScheme.exists?(color_scheme_id: scheme.id)).to eq(false)
expect(ColorScheme.unscoped.exists?(id: scheme.id)).to eq(false)
end
end

describe ".include_basic_relations" do describe ".include_basic_relations" do
fab!(:parent_theme_1) do fab!(:parent_theme_1) do
Fabricate( Fabricate(
@ -1705,51 +1694,6 @@ HTML
end end
end end


describe "#find_or_create_owned_color_palette" do
it "correctly associates a theme with its owned color palette" do
palette = theme.find_or_create_owned_color_palette

expect(palette.owning_theme).to eq(theme)
expect(theme.reload.owned_color_palette).to eq(palette)
end

it "ensures owned color palette is not user selectable" do
palette = theme.find_or_create_owned_color_palette

expect(palette.user_selectable).to eq(false)
end

it "copies colors from base or theme color scheme" do
theme_without_scheme = Fabricate(:theme, color_scheme: nil)
base_palette = theme_without_scheme.find_or_create_owned_color_palette

expect(base_palette.colors.length).to be > 0
expect(base_palette.colors.map(&:name).sort).to eq(ColorScheme.base.colors.map(&:name).sort)

custom_palette =
Fabricate(
:color_scheme,
colors: [ColorSchemeColor.new(name: "custom", hex: "11ccff", dark_hex: "ee9955")],
)
theme_with_scheme = Fabricate(:theme, color_scheme: custom_palette)
custom_palette = theme_with_scheme.find_or_create_owned_color_palette

expect(custom_palette.colors.length).to be > 0
expect(custom_palette.colors.map(&:name).sort).to eq(custom_palette.colors.map(&:name).sort)
end

it "returns the existing palette if a race condition occurs and a theme-owned palette is created while it's executing" do
expect(theme.owned_color_palette).to eq(nil)

palette = Fabricate(:color_scheme)
ThemeColorScheme.create!(theme_id: theme.id, color_scheme_id: palette.id)

expect(theme.owned_color_palette).to eq(nil)
expect(theme.find_or_create_owned_color_palette.id).to eq(palette.id)
expect(theme.owned_color_palette).to eq(palette)
end
end

it "checks if fields can be updated for system themes" do it "checks if fields can be updated for system themes" do
foundation_theme.update!(user_selectable: true) foundation_theme.update!(user_selectable: true)
expect(foundation_theme.user_selectable).to be true expect(foundation_theme.user_selectable).to be true

View file

@ -45,45 +45,6 @@ RSpec.describe Admin::ColorSchemesController do
expect(scheme_colors[0]["name"]).to eq("primary") expect(scheme_colors[0]["name"]).to eq("primary")
expect(scheme_colors[0]["hex"]).to eq(scheme.resolved_colors["primary"]) expect(scheme_colors[0]["hex"]).to eq(scheme.resolved_colors["primary"])
end end

it "doesn't list theme-owned color schemes" do
owned_scheme = Fabricate(:color_scheme, owning_theme: Fabricate(:theme))
scheme = Fabricate(:color_scheme)

get "/admin/color_schemes.json"
expect(response.status).to eq(200)

ids = response.parsed_body.map { |obj| obj["id"] }
expect(ids).to include(scheme.id)
expect(ids).not_to include(owned_scheme.id)
end

it "filters out theme-owned color schemes when exclude_theme_owned is true" do
theme = Fabricate(:theme)
theme_owned_scheme = Fabricate(:color_scheme, name: "Theme Scheme")

ThemeColorScheme.create!(theme: theme, color_scheme: theme_owned_scheme)

owned_scheme = Fabricate(:color_scheme, name: "Directly Owned", theme: theme)
regular_scheme = Fabricate(:color_scheme, name: "Regular Scheme")

get "/admin/color_schemes.json", params: { exclude_theme_owned: true }

expect(response.status).to eq(200)

scheme_names = response.parsed_body.map { |scheme| scheme["name"] }
expect(scheme_names).to include("Regular Scheme")
expect(scheme_names).not_to include("Theme Scheme")
expect(scheme_names).not_to include("Directly Owned")

get "/admin/color_schemes.json"

expect(response.status).to eq(200)
scheme_names = response.parsed_body.map { |scheme| scheme["name"] }
expect(scheme_names).to include("Regular Scheme")
expect(scheme_names).to include("Directly Owned")
expect(scheme_names).not_to include("Theme Scheme")
end
end end


shared_examples "color schemes inaccessible" do shared_examples "color schemes inaccessible" do
@ -187,15 +148,6 @@ RSpec.describe Admin::ColorSchemesController do
expect(response.status).to eq(422) expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to be_present expect(response.parsed_body["errors"]).to be_present
end end

it "doesn't allow editing theme-owned schemes" do
color_scheme = Fabricate(:color_scheme, owning_theme: Fabricate(:theme))

put "/admin/color_schemes/#{color_scheme.id}.json", params: valid_params
expect(response.status).to eq(404)
color_scheme.reload
expect(color_scheme.name).not_to eq(valid_params[:color_scheme][:name])
end
end end


shared_examples "color scheme update not allowed" do shared_examples "color scheme update not allowed" do
@ -232,14 +184,6 @@ RSpec.describe Admin::ColorSchemesController do
}.by(-1) }.by(-1)
expect(response.status).to eq(200) expect(response.status).to eq(200)
end end

it "doesn't allow deleting theme-owned schemes" do
color_scheme = Fabricate(:color_scheme, owning_theme: Fabricate(:theme))

delete "/admin/color_schemes/#{color_scheme.id}.json"
expect(response.status).to eq(404)
expect(color_scheme.reload).to be_persisted
end
end end


shared_examples "color scheme deletion not allowed" do shared_examples "color scheme deletion not allowed" do

View file

@ -1690,98 +1690,6 @@ RSpec.describe Admin::ThemesController do
end end
end end


describe "#change_colors" do
fab!(:theme)

before { sign_in(admin) }

context "with valid parameters" do
it "creates a theme-owned color palette if one doesn't exist" do
expect(theme.owned_color_palette).to be_nil

put "/admin/themes/#{theme.id}/change-colors.json",
params: {
colors: [{ name: "primary", hex: "ff0000", dark_hex: "0000ff" }],
}

expect(response.status).to eq(200)

theme.reload
expect(theme.owned_color_palette.id).to eq(response.parsed_body["id"])

color = theme.owned_color_palette.colors.find_by(name: "primary")
expect(color.hex).to eq("ff0000")
expect(color.dark_hex).to eq("0000ff")
end

it "updates an existing theme-owned color palette" do
palette = theme.find_or_create_owned_color_palette
primary_color = palette.colors.find_by(name: "primary")
secondary_color = palette.colors.find_by(name: "secondary")

original_secondary_hex = secondary_color.hex
original_secondary_dark_hex = secondary_color.dark_hex

put "/admin/themes/#{theme.id}/change-colors.json",
params: {
colors: [{ name: "primary", hex: "aabbcc", dark_hex: "ccddee" }],
}

expect(response.status).to eq(200)

primary_color.reload
secondary_color.reload

expect(primary_color.hex).to eq("aabbcc")
expect(primary_color.dark_hex).to eq("ccddee")

expect(secondary_color.hex).to eq(original_secondary_hex)
expect(secondary_color.dark_hex).to eq(original_secondary_dark_hex)
end

it "returns the updated palette in the response" do
put "/admin/themes/#{theme.id}/change-colors.json",
params: {
colors: [{ name: "primary", hex: "abcdef", dark_hex: "fedcba" }],
}

expect(response.status).to eq(200)
json = response.parsed_body

expect(json["colors"]).to be_present
primary_color = json["colors"].find { |c| c["name"] == "primary" }
expect(primary_color["hex"]).to eq("abcdef")
expect(primary_color["dark_hex"]).to eq("fedcba")
end
end

context "with invalid parameters" do
it "returns 404 for non-existent theme" do
max_id = (Theme.maximum(:id) || 0) + 1

put "/admin/themes/#{max_id}/change-colors.json",
params: {
colors: [{ name: "primary", hex: "ff0000", dark_hex: "0000ff" }],
}

expect(response.status).to eq(404)
end
end

context "when system theme" do
before { theme.update_columns(id: -10) }

it "returns invalid access" do
put "/admin/themes/#{theme.id}/change-colors.json",
params: {
colors: [{ name: "primary", hex: "ff0000", dark_hex: "0000ff" }],
}

expect(response.status).to eq(403)
end
end
end

describe "#show" do describe "#show" do
let(:theme) { Fabricate(:theme) } let(:theme) { Fabricate(:theme) }



View file

@ -189,8 +189,6 @@ describe "Admin Color Palette Config Area Page", type: :system do


color_scheme.colors.each do |color| color_scheme.colors.each do |color|
expect(color.hex).to eq(clipboard_scheme["light"][color.name]) expect(color.hex).to eq(clipboard_scheme["light"][color.name])
next if color.dark_hex.nil?
expect(color.dark_hex).to eq(clipboard_scheme["dark"][color.name])
end end
end end
end end

View file

@ -268,95 +268,6 @@ describe "Admin Customize Themes", type: :system do
end end
end end


describe "theme color palette editor" do
before { SiteSetting.use_overhauled_theme_color_palette = true }

it "allows editing colors of theme-owned palette" do
theme_page.visit(theme.id)
theme_page.colors_tab.click

expect(theme_page).to have_current_path("/admin/customize/themes/#{theme.id}/colors")

theme_page.color_palette_editor.change_color("primary", "#ff000e")

expect(theme_page.changes_banner).to be_visible
theme_page.changes_banner.click_save

page.refresh
expect(theme_page).to have_colors_tab_active

updated_color = theme_page.color_palette_editor.get_color_value("primary")
expect(updated_color).to eq("#ff000e")
end

it "allows discarding unsaved color changes" do
theme_page.visit(theme.id)
theme_page.colors_tab.click

original_hex = theme_page.color_palette_editor.get_color_value("primary")

theme_page.color_palette_editor.change_color("primary", "#10ff00")

theme_page.changes_banner.click_discard

expect(theme_page.changes_banner).to be_hidden

updated_color = theme_page.color_palette_editor.get_color_value("primary")
expect(updated_color).to eq(original_hex)
end

it "shows count of unsaved colors" do
theme_page.visit(theme.id)
theme_page.colors_tab.click

theme_page.color_palette_editor.change_color("primary", "#eeff80")

expect(theme_page.changes_banner).to have_label(
I18n.t("admin_js.admin.customize.theme.unsaved_colors", count: 1),
)

theme_page.color_palette_editor.change_color("secondary", "#ee30ab")
expect(theme_page.changes_banner).to have_label(
I18n.t("admin_js.admin.customize.theme.unsaved_colors", count: 2),
)
end

it "doesn't show colors tab or DPageHeader for components" do
component = Fabricate(:theme, component: true)
theme_page.visit(component.id)
expect(theme_page.header).to be_hidden

expect(theme_page).to have_no_color_scheme_selector
end

it "shows a confirmation dialog when leaving the page with unsaved changes" do
theme_page.visit(theme.id)
theme_page.colors_tab.click

theme_page.color_palette_editor.change_color("primary", "#eeff80")

expect(theme_page.changes_banner).to be_visible

find("#site-logo").click

expect(dialog).to be_open
expect(page).to have_content(
I18n.t("admin_js.admin.customize.theme.unsaved_colors_leave_route_confirmation"),
)

dialog.click_no

expect(dialog).to be_closed
expect(page).to have_current_path("/admin/customize/themes/#{theme.id}/colors")

find("#site-logo").click
expect(dialog).to be_open

dialog.click_yes
expect(page).to have_current_path("/")
end
end

describe "editing theme site settings" do describe "editing theme site settings" do
it "shows all themeable site settings and allows editing values" do it "shows all themeable site settings and allows editing values" do
theme_page.visit(theme.id) theme_page.visit(theme.id)