mirror of
https://github.com/discourse/discourse.git
synced 2025-03-14 10:33:43 +00:00
FEATURE: Introduce new color palettes config area (#31742)
As part of the theme/color palette overhaul project, we're introducing a new admin page for editing color palettes. The new page is located at `/admin/config/colors/:id`. It's linked from anywhere, but it will be linked in the sidebar as we progress more in the overhaul project. Related PRs: https://github.com/discourse/discourse/pull/30893 https://github.com/discourse/discourse/pull/30915 https://github.com/discourse/discourse/pull/31328. Internal topic: t/148628.
This commit is contained in:
@ -0,0 +1,228 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { cached, tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import Form from "discourse/components/form";
|
||||||
|
import { extractError } from "discourse/lib/ajax-error";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||||
|
import ColorPaletteEditor, {
|
||||||
|
LIGHT,
|
||||||
|
} from "admin/components/color-palette-editor";
|
||||||
|
|
||||||
|
export default class AdminConfigAreasColorPalette extends Component {
|
||||||
|
@service toasts;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
@tracked editingName = false;
|
||||||
|
@tracked editorMode = LIGHT;
|
||||||
|
@tracked hasUnsavedChanges = false;
|
||||||
|
|
||||||
|
@cached
|
||||||
|
get data() {
|
||||||
|
return {
|
||||||
|
name: this.args.colorPalette.name,
|
||||||
|
user_selectable: this.args.colorPalette.user_selectable,
|
||||||
|
colors: this.args.colorPalette.colors,
|
||||||
|
editingName: this.editingName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleEditingName() {
|
||||||
|
this.editingName = !this.editingName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onLightColorChange(name, value) {
|
||||||
|
const color = this.data.colors.find((c) => c.name === name);
|
||||||
|
color.hex = value;
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onDarkColorChange(name, value) {
|
||||||
|
const color = this.data.colors.find((c) => c.name === name);
|
||||||
|
color.dark_hex = value;
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async handleSubmit(data) {
|
||||||
|
this.args.colorPalette.name = data.name;
|
||||||
|
this.args.colorPalette.user_selectable = data.user_selectable;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.args.colorPalette.save();
|
||||||
|
this.editingName = false;
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
this.toasts.success({
|
||||||
|
data: {
|
||||||
|
message: i18n("saved"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.toasts.error({
|
||||||
|
duration: 3000,
|
||||||
|
data: {
|
||||||
|
message: extractError(error),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onEditorTabSwitch(newMode) {
|
||||||
|
this.editorMode = newMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async duplicate() {
|
||||||
|
const copy = this.args.colorPalette.copy();
|
||||||
|
copy.name = i18n("admin.config_areas.color_palettes.copy_of", {
|
||||||
|
name: this.args.colorPalette.name,
|
||||||
|
});
|
||||||
|
await copy.save();
|
||||||
|
this.router.replaceWith("adminConfig.color-palettes-show", copy);
|
||||||
|
this.toasts.success({
|
||||||
|
data: {
|
||||||
|
message: i18n("admin.config_areas.color_palettes.copy_created", {
|
||||||
|
name: this.args.colorPalette.name,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleNameChange(value, { set }) {
|
||||||
|
set("name", value);
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleUserSelectableChange(value, { set }) {
|
||||||
|
set("user_selectable", value);
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
@data={{this.data}}
|
||||||
|
@onSubmit={{this.handleSubmit}}
|
||||||
|
as |form transientData|
|
||||||
|
>
|
||||||
|
<div class="admin-config-area">
|
||||||
|
<div class="admin-config-area__primary-content">
|
||||||
|
<div class="admin-config-color-palettes__top-controls">
|
||||||
|
<form.Field
|
||||||
|
@name="name"
|
||||||
|
@showTitle={{false}}
|
||||||
|
@title={{i18n "admin.config_areas.color_palettes.palette_name"}}
|
||||||
|
@validation="required"
|
||||||
|
@format="full"
|
||||||
|
@onSet={{this.handleNameChange}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
{{#if transientData.editingName}}
|
||||||
|
<div class="admin-config-color-palettes__name-control">
|
||||||
|
<field.Input />
|
||||||
|
<DButton
|
||||||
|
class="btn-flat"
|
||||||
|
@icon="xmark"
|
||||||
|
@action={{this.toggleEditingName}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<field.Custom>
|
||||||
|
<div class="admin-config-color-palettes__name-control">
|
||||||
|
<h2>{{@colorPalette.name}}</h2>
|
||||||
|
<DButton
|
||||||
|
class="btn-flat"
|
||||||
|
@icon="pencil"
|
||||||
|
@action={{this.toggleEditingName}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</field.Custom>
|
||||||
|
{{/if}}
|
||||||
|
</form.Field>
|
||||||
|
<DButton
|
||||||
|
class="duplicate-palette"
|
||||||
|
@label="admin.customize.copy"
|
||||||
|
@action={{this.duplicate}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<form.Alert class="fonts-and-logos-hint">
|
||||||
|
<div class="admin-config-color-palettes__fonts-and-logos-hint">
|
||||||
|
<span>{{i18n
|
||||||
|
"admin.config_areas.color_palettes.fonts_and_logos_hint"
|
||||||
|
}}</span>
|
||||||
|
<LinkTo @route="adminConfig.branding">{{i18n
|
||||||
|
"admin.config_areas.color_palettes.go_to_branding"
|
||||||
|
}}</LinkTo>
|
||||||
|
</div>
|
||||||
|
</form.Alert>
|
||||||
|
<AdminConfigAreaCard
|
||||||
|
@heading="admin.config_areas.color_palettes.color_options.title"
|
||||||
|
>
|
||||||
|
<:content>
|
||||||
|
<form.Field
|
||||||
|
@name="user_selectable"
|
||||||
|
@title={{i18n
|
||||||
|
"admin.config_areas.color_palettes.color_options.toggle"
|
||||||
|
}}
|
||||||
|
@showTitle={{false}}
|
||||||
|
@description={{i18n
|
||||||
|
"admin.config_areas.color_palettes.color_options.toggle_description"
|
||||||
|
}}
|
||||||
|
@format="full"
|
||||||
|
@onSet={{this.handleUserSelectableChange}}
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Toggle />
|
||||||
|
</form.Field>
|
||||||
|
</:content>
|
||||||
|
</AdminConfigAreaCard>
|
||||||
|
<AdminConfigAreaCard
|
||||||
|
@heading="admin.config_areas.color_palettes.colors.title"
|
||||||
|
>
|
||||||
|
<:content>
|
||||||
|
<form.Field
|
||||||
|
@name="colors"
|
||||||
|
@title={{i18n "admin.config_areas.color_palettes.colors.title"}}
|
||||||
|
@showTitle={{false}}
|
||||||
|
@format="full"
|
||||||
|
as |field|
|
||||||
|
>
|
||||||
|
<field.Custom>
|
||||||
|
<ColorPaletteEditor
|
||||||
|
@initialMode={{this.editorMode}}
|
||||||
|
@colors={{transientData.colors}}
|
||||||
|
@onLightColorChange={{this.onLightColorChange}}
|
||||||
|
@onDarkColorChange={{this.onDarkColorChange}}
|
||||||
|
@onTabSwitch={{this.onEditorTabSwitch}}
|
||||||
|
/>
|
||||||
|
</field.Custom>
|
||||||
|
</form.Field>
|
||||||
|
</:content>
|
||||||
|
</AdminConfigAreaCard>
|
||||||
|
<AdminConfigAreaCard>
|
||||||
|
<:content>
|
||||||
|
<div class="admin-config-color-palettes__save-card">
|
||||||
|
{{#if this.hasUnsavedChanges}}
|
||||||
|
<span class="admin-config-color-palettes__unsaved-changes">
|
||||||
|
{{i18n "admin.config_areas.color_palettes.unsaved_changes"}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
<form.Submit
|
||||||
|
@label="admin.config_areas.color_palettes.save_changes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</:content>
|
||||||
|
</AdminConfigAreaCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
}
|
@ -7,25 +7,19 @@ import concatClass from "discourse/helpers/concat-class";
|
|||||||
import dIcon from "discourse/helpers/d-icon";
|
import dIcon from "discourse/helpers/d-icon";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
const LIGHT = "light";
|
export const LIGHT = "light";
|
||||||
const DARK = "dark";
|
export const DARK = "dark";
|
||||||
|
|
||||||
class Color {
|
class Color {
|
||||||
@tracked lightValue;
|
@tracked lightValue;
|
||||||
@tracked darkValue;
|
@tracked darkValue;
|
||||||
|
|
||||||
constructor({ name, lightValue, darkValue }) {
|
constructor({ name, lightValue, darkValue, description, translatedName }) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.lightValue = lightValue;
|
this.lightValue = lightValue;
|
||||||
this.darkValue = darkValue;
|
this.darkValue = darkValue;
|
||||||
}
|
this.displayName = translatedName;
|
||||||
|
this.description = description;
|
||||||
get displayName() {
|
|
||||||
return this.name.replaceAll("_", " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
get description() {
|
|
||||||
return i18n(`admin.customize.colors.${this.name}.description`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,26 +62,38 @@ const Picker = class extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get displayedColor() {
|
get displayedColor() {
|
||||||
|
let color;
|
||||||
if (this.args.showDark) {
|
if (this.args.showDark) {
|
||||||
return this.args.color.darkValue;
|
color = this.args.color.darkValue ?? this.args.color.lightValue;
|
||||||
} else {
|
} else {
|
||||||
return this.args.color.lightValue;
|
color = this.args.color.lightValue ?? this.args.color.darkValue;
|
||||||
}
|
}
|
||||||
|
return this.ensureSixDigitsHex(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeValue() {
|
get activeValue() {
|
||||||
let color;
|
let color;
|
||||||
if (this.args.showDark) {
|
if (this.args.showDark) {
|
||||||
color = this.args.color.darkValue;
|
color = this.args.color.darkValue ?? this.args.color.lightValue;
|
||||||
} else {
|
} else {
|
||||||
color = this.args.color.lightValue;
|
color = this.args.color.lightValue ?? this.args.color.darkValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
return `#${color}`;
|
return `#${this.ensureSixDigitsHex(color)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureSixDigitsHex(hex) {
|
||||||
|
if (hex.length === 3) {
|
||||||
|
return hex
|
||||||
|
.split("")
|
||||||
|
.map((digit) => `${digit}${digit}`)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
class="color-palette-editor__input"
|
class="color-palette-editor__input"
|
||||||
@ -124,6 +130,8 @@ export default class ColorPaletteEditor extends Component {
|
|||||||
name: color.name,
|
name: color.name,
|
||||||
lightValue: color.hex,
|
lightValue: color.hex,
|
||||||
darkValue: color.dark_hex,
|
darkValue: color.dark_hex,
|
||||||
|
description: color.description,
|
||||||
|
translatedName: color.translatedName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -134,7 +142,11 @@ export default class ColorPaletteEditor extends Component {
|
|||||||
event.type === "click" ||
|
event.type === "click" ||
|
||||||
(event.type === "keydown" && event.keyCode === 13)
|
(event.type === "keydown" && event.keyCode === 13)
|
||||||
) {
|
) {
|
||||||
this.selectedMode = newMode;
|
if (this.args.onTabSwitch) {
|
||||||
|
this.args.onTabSwitch(newMode);
|
||||||
|
} else {
|
||||||
|
this.selectedMode = newMode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { observes, on } from "@ember-decorators/object";
|
import { observes, on } from "@ember-decorators/object";
|
||||||
import { propertyNotEqual } from "discourse/lib/computed";
|
import { propertyNotEqual } from "discourse/lib/computed";
|
||||||
@ -5,26 +6,35 @@ import discourseComputed from "discourse/lib/decorators";
|
|||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
export default class ColorSchemeColor extends EmberObject {
|
export default class ColorSchemeColor extends EmberObject {
|
||||||
|
@tracked hex;
|
||||||
|
@tracked dark_hex;
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
@on("init")
|
@on("init")
|
||||||
startTrackingChanges() {
|
startTrackingChanges() {
|
||||||
this.set("originals", { hex: this.hex || "FFFFFF" });
|
this.set("originals", {
|
||||||
|
hex: this.hex || "FFFFFF",
|
||||||
|
darkHex: this.dark_hex,
|
||||||
|
});
|
||||||
|
|
||||||
// force changed property to be recalculated
|
// force changed property to be recalculated
|
||||||
this.notifyPropertyChange("hex");
|
this.notifyPropertyChange("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether value has changed since it was last saved.
|
// Whether value has changed since it was last saved.
|
||||||
@discourseComputed("hex")
|
@discourseComputed("hex", "dark_hex")
|
||||||
changed(hex) {
|
changed(hex, darkHex) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { A } from "@ember/array";
|
import { A } from "@ember/array";
|
||||||
import ArrayProxy from "@ember/array/proxy";
|
import ArrayProxy from "@ember/array/proxy";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
@ -27,6 +28,7 @@ export default class ColorScheme extends EmberObject {
|
|||||||
return ColorSchemeColor.create({
|
return ColorSchemeColor.create({
|
||||||
name: c.name,
|
name: c.name,
|
||||||
hex: c.hex,
|
hex: c.hex,
|
||||||
|
dark_hex: c.dark_hex,
|
||||||
default_hex: c.default_hex,
|
default_hex: c.default_hex,
|
||||||
is_advanced: c.is_advanced,
|
is_advanced: c.is_advanced,
|
||||||
});
|
});
|
||||||
@ -38,6 +40,31 @@ export default class ColorScheme extends EmberObject {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async find(id) {
|
||||||
|
const json = await ajax(`/admin/config/colors/${id}`);
|
||||||
|
return ColorScheme.create({
|
||||||
|
id: json.id,
|
||||||
|
name: json.name,
|
||||||
|
is_base: json.is_base,
|
||||||
|
theme_id: json.theme_id,
|
||||||
|
theme_name: json.theme_name,
|
||||||
|
base_scheme_id: json.base_scheme_id,
|
||||||
|
user_selectable: json.user_selectable,
|
||||||
|
colors: json.colors.map((c) => {
|
||||||
|
return ColorSchemeColor.create({
|
||||||
|
name: c.name,
|
||||||
|
hex: c.hex,
|
||||||
|
dark_hex: c.dark_hex,
|
||||||
|
default_hex: c.default_hex,
|
||||||
|
is_advanced: c.is_advanced,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@tracked name;
|
||||||
|
@tracked user_selectable;
|
||||||
|
|
||||||
@not("id") newRecord;
|
@not("id") newRecord;
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -75,7 +102,9 @@ export default class ColorScheme extends EmberObject {
|
|||||||
});
|
});
|
||||||
this.colors.forEach((c) => {
|
this.colors.forEach((c) => {
|
||||||
newScheme.colors.pushObject(
|
newScheme.colors.pushObject(
|
||||||
ColorSchemeColor.create(c.getProperties("name", "hex", "default_hex"))
|
ColorSchemeColor.create(
|
||||||
|
c.getProperties("name", "hex", "default_hex", "dark_hex")
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return newScheme;
|
return newScheme;
|
||||||
@ -128,20 +157,17 @@ 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"));
|
data.colors.pushObject(c.getProperties("name", "hex", "dark_hex"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ajax(
|
return ajax(`/admin/color_schemes${this.id ? `/${this.id}` : ""}.json`, {
|
||||||
"/admin/color_schemes" + (this.id ? "/" + this.id : "") + ".json",
|
data: JSON.stringify({ color_scheme: data }),
|
||||||
{
|
type: this.id ? "PUT" : "POST",
|
||||||
data: JSON.stringify({ color_scheme: data }),
|
dataType: "json",
|
||||||
type: this.id ? "PUT" : "POST",
|
contentType: "application/json",
|
||||||
dataType: "json",
|
}).then((result) => {
|
||||||
contentType: "application/json",
|
|
||||||
}
|
|
||||||
).then((result) => {
|
|
||||||
if (result.id) {
|
if (result.id) {
|
||||||
this.set("id", result.id);
|
this.set("id", result.id);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
import ColorScheme from "admin/models/color-scheme";
|
||||||
|
|
||||||
|
export default class AdminConfigColorPalettesShowRoute extends DiscourseRoute {
|
||||||
|
model(params) {
|
||||||
|
return ColorScheme.find(params.palette_id);
|
||||||
|
}
|
||||||
|
}
|
@ -335,6 +335,10 @@ export default function () {
|
|||||||
this.route("user-api", function () {
|
this.route("user-api", function () {
|
||||||
this.route("settings", { path: "/" });
|
this.route("settings", { path: "/" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route("color-palettes-show", {
|
||||||
|
path: "/colors/:palette_id",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<AdminConfigAreas::ColorPalette @colorPalette={{this.model}} />
|
@ -2,6 +2,7 @@ import { click, find, render, triggerEvent } from "@ember/test-helpers";
|
|||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
import ColorPaletteEditor from "admin/components/color-palette-editor";
|
import ColorPaletteEditor from "admin/components/color-palette-editor";
|
||||||
|
import ColorSchemeColor from "admin/models/color-scheme-color";
|
||||||
|
|
||||||
function editor() {
|
function editor() {
|
||||||
return {
|
return {
|
||||||
@ -98,7 +99,7 @@ module("Integration | Component | ColorPaletteEditor", function (hooks) {
|
|||||||
hex: "473921",
|
hex: "473921",
|
||||||
dark_hex: "f2cca9",
|
dark_hex: "f2cca9",
|
||||||
},
|
},
|
||||||
];
|
].map((data) => ColorSchemeColor.create(data));
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
<template><ColorPaletteEditor @colors={{colors}} /></template>
|
<template><ColorPaletteEditor @colors={{colors}} /></template>
|
||||||
@ -166,29 +167,19 @@ module("Integration | Component | ColorPaletteEditor", function (hooks) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("replacing underscores in color name with spaces for display", async function (assert) {
|
test("uses the i18n string for the color name", async function (assert) {
|
||||||
const colors = [
|
const colors = [
|
||||||
{
|
|
||||||
name: "my_awesome_color",
|
|
||||||
hex: "aaaaaa",
|
|
||||||
dark_hex: "1e3c8a",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "header_background",
|
name: "header_background",
|
||||||
hex: "473921",
|
hex: "473921",
|
||||||
dark_hex: "f2cca9",
|
dark_hex: "f2cca9",
|
||||||
},
|
},
|
||||||
];
|
].map((data) => ColorSchemeColor.create(data));
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
<template><ColorPaletteEditor @colors={{colors}} /></template>
|
<template><ColorPaletteEditor @colors={{colors}} /></template>
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
this.subject.color("my_awesome_color").displayName(),
|
|
||||||
"my awesome color"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
this.subject.color("header_background").displayName(),
|
this.subject.color("header_background").displayName(),
|
||||||
"header background"
|
"header background"
|
||||||
@ -207,7 +198,7 @@ module("Integration | Component | ColorPaletteEditor", function (hooks) {
|
|||||||
hex: "473921",
|
hex: "473921",
|
||||||
dark_hex: "f2cca9",
|
dark_hex: "f2cca9",
|
||||||
},
|
},
|
||||||
];
|
].map((data) => ColorSchemeColor.create(data));
|
||||||
|
|
||||||
const lightChanges = [];
|
const lightChanges = [];
|
||||||
const darkChanges = [];
|
const darkChanges = [];
|
||||||
@ -387,4 +378,42 @@ module("Integration | Component | ColorPaletteEditor", function (hooks) {
|
|||||||
"the dark color for the header_background color is remembered after switching tabs"
|
"the dark color for the header_background color is remembered after switching tabs"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("converting 3 digits hex values to 6 digits", async function (assert) {
|
||||||
|
const colors = [
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
hex: "a8c",
|
||||||
|
dark_hex: "971",
|
||||||
|
},
|
||||||
|
].map((data) => ColorSchemeColor.create(data));
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template><ColorPaletteEditor @colors={{colors}} /></template>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
this.subject.color("primary").input().value,
|
||||||
|
"#aa88cc",
|
||||||
|
"the input field has the equivalent 6 digits value"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
this.subject.color("primary").displayedValue(),
|
||||||
|
"aa88cc",
|
||||||
|
"the displayed value shows the 6 digits format"
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.subject.switchToDarkTab();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
this.subject.color("primary").input().value,
|
||||||
|
"#997711",
|
||||||
|
"the input field has the equivalent 6 digits value"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
this.subject.color("primary").displayedValue(),
|
||||||
|
"997711",
|
||||||
|
"the displayed value shows the 6 digits format"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1277,3 +1277,4 @@ a.inline-editable-field {
|
|||||||
@import "admin/customize_themes_show_schema";
|
@import "admin/customize_themes_show_schema";
|
||||||
@import "admin/admin_bulk_users_delete_modal";
|
@import "admin/admin_bulk_users_delete_modal";
|
||||||
@import "admin/color-palette-editor";
|
@import "admin/color-palette-editor";
|
||||||
|
@import "admin/admin_config_color_palettes";
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
.admin-config.color-palettes-show {
|
||||||
|
.admin-config-color-palettes {
|
||||||
|
&__name-control {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__top-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__fonts-and-logos-hint {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__unsaved-changes {
|
||||||
|
font-size: var(--font-down-1);
|
||||||
|
color: var(--primary-medium);
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__save-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,9 @@ class Admin::ColorSchemesController < Admin::AdminController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def color_scheme_params
|
def color_scheme_params
|
||||||
params.permit(color_scheme: [:base_scheme_id, :name, :user_selectable, colors: %i[name hex]])[
|
params.permit(
|
||||||
|
color_scheme: [:base_scheme_id, :name, :user_selectable, colors: %i[name hex dark_hex]],
|
||||||
|
)[
|
||||||
:color_scheme
|
:color_scheme
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
class Admin::Config::ColorPalettesController < Admin::AdminController
|
||||||
|
def show
|
||||||
|
render_serialized(ColorScheme.find(params[:id]), ColorSchemeSerializer, root: false)
|
||||||
|
end
|
||||||
|
end
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ColorSchemeColorSerializer < ApplicationSerializer
|
class ColorSchemeColorSerializer < ApplicationSerializer
|
||||||
attributes :name, :hex, :default_hex, :is_advanced
|
attributes :name, :hex, :default_hex, :is_advanced, :dark_hex
|
||||||
|
|
||||||
def hex
|
def hex
|
||||||
object.hex # otherwise something crazy is returned
|
object.hex # otherwise something crazy is returned
|
||||||
|
@ -24,7 +24,11 @@ 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)
|
||||||
else
|
else
|
||||||
@color_scheme.color_scheme_colors << ColorSchemeColor.new(name: c[:name], hex: c[:hex])
|
@color_scheme.color_scheme_colors << ColorSchemeColor.new(
|
||||||
|
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
|
||||||
|
@ -6072,6 +6072,20 @@ en:
|
|||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
delete_successful: "User field deleted."
|
delete_successful: "User field deleted."
|
||||||
save_successful: "User field saved."
|
save_successful: "User field saved."
|
||||||
|
color_palettes:
|
||||||
|
palette_name: "Name"
|
||||||
|
color_options:
|
||||||
|
title: "Color options"
|
||||||
|
toggle: "User selectable"
|
||||||
|
toggle_description: "Color palette can be selected by users"
|
||||||
|
colors:
|
||||||
|
title: "Colors"
|
||||||
|
fonts_and_logos_hint: "Looking to update your fonts and logos?"
|
||||||
|
go_to_branding: "Go to Branding"
|
||||||
|
save_changes: "Save changes"
|
||||||
|
unsaved_changes: "You have unsaved changes"
|
||||||
|
copy_of: "Copy of %{name}"
|
||||||
|
copy_created: 'A new copy of "%{name}" has been created'
|
||||||
plugins:
|
plugins:
|
||||||
title: "Plugins"
|
title: "Plugins"
|
||||||
name: "Name"
|
name: "Name"
|
||||||
|
@ -414,6 +414,7 @@ Discourse::Application.routes.draw do
|
|||||||
get "group-permissions" => "site_settings#index"
|
get "group-permissions" => "site_settings#index"
|
||||||
get "branding" => "branding#index"
|
get "branding" => "branding#index"
|
||||||
put "branding/logo" => "branding#logo"
|
put "branding/logo" => "branding#logo"
|
||||||
|
get "colors/:id" => "color_palettes#show"
|
||||||
|
|
||||||
resources :flags, only: %i[index new create update destroy] do
|
resources :flags, only: %i[index new create update destroy] do
|
||||||
put "toggle"
|
put "toggle"
|
||||||
|
@ -3577,6 +3577,7 @@ experimental:
|
|||||||
use_overhauled_theme_color_palette:
|
use_overhauled_theme_color_palette:
|
||||||
default: false
|
default: false
|
||||||
hidden: true
|
hidden: true
|
||||||
|
client: true
|
||||||
rich_editor:
|
rich_editor:
|
||||||
client: true
|
client: true
|
||||||
default: false
|
default: false
|
||||||
|
95
spec/system/admin_color_palettes_config_area_spec.rb
Normal file
95
spec/system/admin_color_palettes_config_area_spec.rb
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe "Admin Color Palettes Config Area Page", type: :system do
|
||||||
|
fab!(:admin)
|
||||||
|
fab!(:color_scheme) { Fabricate(:color_scheme, user_selectable: false, name: "A Test Palette") }
|
||||||
|
|
||||||
|
let(:config_area) { PageObjects::Pages::AdminColorPalettesConfigArea.new }
|
||||||
|
let(:toasts) { PageObjects::Components::Toasts.new }
|
||||||
|
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
it "allows editing the palette name" do
|
||||||
|
config_area.visit(color_scheme.id)
|
||||||
|
|
||||||
|
config_area.edit_name_button.click
|
||||||
|
config_area.name_field.fill_in("Changed name 2.0")
|
||||||
|
|
||||||
|
expect(config_area).to have_unsaved_changes_indicator
|
||||||
|
|
||||||
|
config_area.form.submit
|
||||||
|
|
||||||
|
expect(toasts).to have_success(I18n.t("js.saved"))
|
||||||
|
expect(config_area).to have_no_unsaved_changes_indicator
|
||||||
|
expect(color_scheme.reload.name).to eq("Changed name 2.0")
|
||||||
|
expect(config_area.name_heading.text).to eq("Changed name 2.0")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows changing the user selectable field" do
|
||||||
|
config_area.visit(color_scheme.id)
|
||||||
|
|
||||||
|
config_area.user_selectable_field.toggle
|
||||||
|
|
||||||
|
expect(config_area).to have_unsaved_changes_indicator
|
||||||
|
|
||||||
|
config_area.form.submit
|
||||||
|
|
||||||
|
expect(toasts).to have_success(I18n.t("js.saved"))
|
||||||
|
expect(config_area).to have_no_unsaved_changes_indicator
|
||||||
|
expect(config_area.user_selectable_field.value).to eq(true)
|
||||||
|
expect(color_scheme.reload.user_selectable).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows changing colors" do
|
||||||
|
config_area.visit(color_scheme.id)
|
||||||
|
|
||||||
|
expect(config_area.color_palette_editor).to have_light_tab_active
|
||||||
|
|
||||||
|
config_area.color_palette_editor.input_for_color("primary").fill_in(with: "#abcdef")
|
||||||
|
|
||||||
|
expect(config_area).to have_unsaved_changes_indicator
|
||||||
|
|
||||||
|
config_area.color_palette_editor.switch_to_dark_tab
|
||||||
|
expect(config_area.color_palette_editor).to have_dark_tab_active
|
||||||
|
|
||||||
|
config_area.color_palette_editor.input_for_color("primary").fill_in(with: "#fedcba")
|
||||||
|
config_area.color_palette_editor.input_for_color("secondary").fill_in(with: "#111222")
|
||||||
|
|
||||||
|
config_area.form.submit
|
||||||
|
|
||||||
|
expect(toasts).to have_success(I18n.t("js.saved"))
|
||||||
|
expect(config_area).to have_no_unsaved_changes_indicator
|
||||||
|
expect(config_area.color_palette_editor).to have_dark_tab_active
|
||||||
|
expect(config_area.color_palette_editor.input_for_color("primary").value).to eq("#fedcba")
|
||||||
|
expect(config_area.color_palette_editor.input_for_color("secondary").value).to eq("#111222")
|
||||||
|
|
||||||
|
config_area.color_palette_editor.switch_to_light_tab
|
||||||
|
expect(config_area.color_palette_editor).to have_light_tab_active
|
||||||
|
expect(config_area.color_palette_editor.input_for_color("primary").value).to eq("#abcdef")
|
||||||
|
|
||||||
|
expect(color_scheme.colors.find_by(name: "primary").hex).to eq("abcdef")
|
||||||
|
expect(color_scheme.colors.find_by(name: "primary").dark_hex).to eq("fedcba")
|
||||||
|
expect(color_scheme.colors.find_by(name: "secondary").dark_hex).to eq("111222")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows duplicating the color palette" do
|
||||||
|
max_id = ColorScheme.maximum(:id)
|
||||||
|
color_scheme.update!(user_selectable: true)
|
||||||
|
|
||||||
|
config_area.visit(color_scheme.id)
|
||||||
|
|
||||||
|
expect(config_area.user_selectable_field.value).to eq(true)
|
||||||
|
|
||||||
|
config_area.duplicate_button.click
|
||||||
|
|
||||||
|
expect(page).to have_current_path("/admin/config/colors/#{max_id + 1}")
|
||||||
|
expect(config_area).to have_no_unsaved_changes_indicator
|
||||||
|
expect(config_area.name_heading.text).to eq(
|
||||||
|
I18n.t("admin_js.admin.config_areas.color_palettes.copy_of", name: color_scheme.name),
|
||||||
|
)
|
||||||
|
expect(toasts).to have_success(
|
||||||
|
I18n.t("admin_js.admin.config_areas.color_palettes.copy_created", name: color_scheme.name),
|
||||||
|
)
|
||||||
|
expect(config_area.user_selectable_field.value).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
35
spec/system/page_objects/components/color_palette_editor.rb
Normal file
35
spec/system/page_objects/components/color_palette_editor.rb
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Components
|
||||||
|
class ColorPaletteEditor < PageObjects::Components::Base
|
||||||
|
attr_reader :component
|
||||||
|
|
||||||
|
def initialize(component)
|
||||||
|
@component = component
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_light_tab_active?
|
||||||
|
component.has_css?(".light-tab.active")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_dark_tab_active?
|
||||||
|
component.has_css?(".dark-tab.active")
|
||||||
|
end
|
||||||
|
|
||||||
|
def switch_to_light_tab
|
||||||
|
component.find(".light-tab").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def switch_to_dark_tab
|
||||||
|
component.find(".dark-tab").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def input_for_color(name)
|
||||||
|
component.find(
|
||||||
|
".color-palette-editor__colors-item[data-color-name=\"#{name}\"] input[type=\"color\"]",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -48,6 +48,8 @@ module PageObjects
|
|||||||
url = component.find(".uploaded-image-preview a.lightbox", wait: 10)[:href]
|
url = component.find(".uploaded-image-preview a.lightbox", wait: 10)[:href]
|
||||||
sha1 = url.match(/(\h{40})/).captures.first
|
sha1 = url.match(/(\h{40})/).captures.first
|
||||||
Upload.find_by(sha1:)
|
Upload.find_by(sha1:)
|
||||||
|
when "toggle"
|
||||||
|
component.find("button[role=\"switch\"]", visible: :all)["aria-checked"] == "true"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -91,6 +93,8 @@ module PageObjects
|
|||||||
component.find("input[type='checkbox']").click
|
component.find("input[type='checkbox']").click
|
||||||
when "password"
|
when "password"
|
||||||
component.find(".form-kit__control-password-toggle").click
|
component.find(".form-kit__control-password-toggle").click
|
||||||
|
when "toggle"
|
||||||
|
component.find("button[role=\"switch\"]", visible: :all).ancestor("label").click
|
||||||
else
|
else
|
||||||
raise "'toggle' is not supported for control type: #{control_type}"
|
raise "'toggle' is not supported for control type: #{control_type}"
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Pages
|
||||||
|
class AdminColorPalettesConfigArea < PageObjects::Pages::Base
|
||||||
|
def visit(palette_id)
|
||||||
|
page.visit("/admin/config/colors/#{palette_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def form
|
||||||
|
PageObjects::Components::FormKit.new(".admin-config.color-palettes-show .form-kit")
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit_name_button
|
||||||
|
name_field.component.find(".btn-flat")
|
||||||
|
end
|
||||||
|
|
||||||
|
def name_field
|
||||||
|
form.field("name")
|
||||||
|
end
|
||||||
|
|
||||||
|
def name_heading
|
||||||
|
name_field.find("h2")
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_selectable_field
|
||||||
|
form.field("user_selectable")
|
||||||
|
end
|
||||||
|
|
||||||
|
def color_palette_editor
|
||||||
|
PageObjects::Components::ColorPaletteEditor.new(
|
||||||
|
form.field("colors").component.find(".color-palette-editor"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def duplicate_button
|
||||||
|
page.find(".duplicate-palette")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_unsaved_changes_indicator?
|
||||||
|
page.has_text?(I18n.t("admin_js.admin.config_areas.color_palettes.unsaved_changes"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_no_unsaved_changes_indicator?
|
||||||
|
page.has_no_text?(I18n.t("admin_js.admin.config_areas.color_palettes.unsaved_changes"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue
Block a user