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 { i18n } from "discourse-i18n";
|
||||
|
||||
const LIGHT = "light";
|
||||
const DARK = "dark";
|
||||
export const LIGHT = "light";
|
||||
export const DARK = "dark";
|
||||
|
||||
class Color {
|
||||
@tracked lightValue;
|
||||
@tracked darkValue;
|
||||
|
||||
constructor({ name, lightValue, darkValue }) {
|
||||
constructor({ name, lightValue, darkValue, description, translatedName }) {
|
||||
this.name = name;
|
||||
this.lightValue = lightValue;
|
||||
this.darkValue = darkValue;
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
return this.name.replaceAll("_", " ");
|
||||
}
|
||||
|
||||
get description() {
|
||||
return i18n(`admin.customize.colors.${this.name}.description`);
|
||||
this.displayName = translatedName;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,26 +62,38 @@ const Picker = class extends Component {
|
||||
}
|
||||
|
||||
get displayedColor() {
|
||||
let color;
|
||||
if (this.args.showDark) {
|
||||
return this.args.color.darkValue;
|
||||
color = this.args.color.darkValue ?? this.args.color.lightValue;
|
||||
} else {
|
||||
return this.args.color.lightValue;
|
||||
color = this.args.color.lightValue ?? this.args.color.darkValue;
|
||||
}
|
||||
return this.ensureSixDigitsHex(color);
|
||||
}
|
||||
|
||||
get activeValue() {
|
||||
let color;
|
||||
if (this.args.showDark) {
|
||||
color = this.args.color.darkValue;
|
||||
color = this.args.color.darkValue ?? this.args.color.lightValue;
|
||||
} else {
|
||||
color = this.args.color.lightValue;
|
||||
color = this.args.color.lightValue ?? this.args.color.darkValue;
|
||||
}
|
||||
|
||||
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>
|
||||
<input
|
||||
class="color-palette-editor__input"
|
||||
@ -124,6 +130,8 @@ export default class ColorPaletteEditor extends Component {
|
||||
name: color.name,
|
||||
lightValue: color.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 === "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 { observes, on } from "@ember-decorators/object";
|
||||
import { propertyNotEqual } from "discourse/lib/computed";
|
||||
@ -5,26 +6,35 @@ import discourseComputed from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class ColorSchemeColor extends EmberObject {
|
||||
@tracked hex;
|
||||
@tracked dark_hex;
|
||||
|
||||
// Whether the current value is different than Discourse's default color scheme.
|
||||
@propertyNotEqual("hex", "default_hex") overridden;
|
||||
|
||||
@on("init")
|
||||
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
|
||||
this.notifyPropertyChange("hex");
|
||||
}
|
||||
|
||||
// Whether value has changed since it was last saved.
|
||||
@discourseComputed("hex")
|
||||
changed(hex) {
|
||||
@discourseComputed("hex", "dark_hex")
|
||||
changed(hex, darkHex) {
|
||||
if (!this.originals) {
|
||||
return false;
|
||||
}
|
||||
if (hex !== this.originals.hex) {
|
||||
return true;
|
||||
}
|
||||
if (darkHex !== this.originals.darkHex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { A } from "@ember/array";
|
||||
import ArrayProxy from "@ember/array/proxy";
|
||||
import EmberObject from "@ember/object";
|
||||
@ -27,6 +28,7 @@ export default class ColorScheme extends EmberObject {
|
||||
return ColorSchemeColor.create({
|
||||
name: c.name,
|
||||
hex: c.hex,
|
||||
dark_hex: c.dark_hex,
|
||||
default_hex: c.default_hex,
|
||||
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;
|
||||
|
||||
init() {
|
||||
@ -75,7 +102,9 @@ export default class ColorScheme extends EmberObject {
|
||||
});
|
||||
this.colors.forEach((c) => {
|
||||
newScheme.colors.pushObject(
|
||||
ColorSchemeColor.create(c.getProperties("name", "hex", "default_hex"))
|
||||
ColorSchemeColor.create(
|
||||
c.getProperties("name", "hex", "default_hex", "dark_hex")
|
||||
)
|
||||
);
|
||||
});
|
||||
return newScheme;
|
||||
@ -128,20 +157,17 @@ export default class ColorScheme extends EmberObject {
|
||||
data.colors = [];
|
||||
this.colors.forEach((c) => {
|
||||
if (!this.id || c.get("changed")) {
|
||||
data.colors.pushObject(c.getProperties("name", "hex"));
|
||||
data.colors.pushObject(c.getProperties("name", "hex", "dark_hex"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ajax(
|
||||
"/admin/color_schemes" + (this.id ? "/" + this.id : "") + ".json",
|
||||
{
|
||||
data: JSON.stringify({ color_scheme: data }),
|
||||
type: this.id ? "PUT" : "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
}
|
||||
).then((result) => {
|
||||
return ajax(`/admin/color_schemes${this.id ? `/${this.id}` : ""}.json`, {
|
||||
data: JSON.stringify({ color_scheme: data }),
|
||||
type: this.id ? "PUT" : "POST",
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
}).then((result) => {
|
||||
if (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("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 { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import ColorPaletteEditor from "admin/components/color-palette-editor";
|
||||
import ColorSchemeColor from "admin/models/color-scheme-color";
|
||||
|
||||
function editor() {
|
||||
return {
|
||||
@ -98,7 +99,7 @@ module("Integration | Component | ColorPaletteEditor", function (hooks) {
|
||||
hex: "473921",
|
||||
dark_hex: "f2cca9",
|
||||
},
|
||||
];
|
||||
].map((data) => ColorSchemeColor.create(data));
|
||||
|
||||
await render(
|
||||
<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 = [
|
||||
{
|
||||
name: "my_awesome_color",
|
||||
hex: "aaaaaa",
|
||||
dark_hex: "1e3c8a",
|
||||
},
|
||||
{
|
||||
name: "header_background",
|
||||
hex: "473921",
|
||||
dark_hex: "f2cca9",
|
||||
},
|
||||
];
|
||||
].map((data) => ColorSchemeColor.create(data));
|
||||
|
||||
await render(
|
||||
<template><ColorPaletteEditor @colors={{colors}} /></template>
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
this.subject.color("my_awesome_color").displayName(),
|
||||
"my awesome color"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
this.subject.color("header_background").displayName(),
|
||||
"header background"
|
||||
@ -207,7 +198,7 @@ module("Integration | Component | ColorPaletteEditor", function (hooks) {
|
||||
hex: "473921",
|
||||
dark_hex: "f2cca9",
|
||||
},
|
||||
];
|
||||
].map((data) => ColorSchemeColor.create(data));
|
||||
|
||||
const lightChanges = [];
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
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/admin_bulk_users_delete_modal";
|
||||
@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
|
||||
|
||||
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
|
||||
]
|
||||
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
|
||||
|
||||
class ColorSchemeColorSerializer < ApplicationSerializer
|
||||
attributes :name, :hex, :default_hex, :is_advanced
|
||||
attributes :name, :hex, :default_hex, :is_advanced, :dark_hex
|
||||
|
||||
def hex
|
||||
object.hex # otherwise something crazy is returned
|
||||
|
@ -24,7 +24,11 @@ class ColorSchemeRevisor
|
||||
if existing = @color_scheme.colors_by_name[c[:name]]
|
||||
existing.update(c)
|
||||
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
|
||||
@color_scheme.clear_colors_cache
|
||||
|
@ -6072,6 +6072,20 @@ en:
|
||||
delete: "Delete"
|
||||
delete_successful: "User field deleted."
|
||||
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:
|
||||
title: "Plugins"
|
||||
name: "Name"
|
||||
|
@ -414,6 +414,7 @@ Discourse::Application.routes.draw do
|
||||
get "group-permissions" => "site_settings#index"
|
||||
get "branding" => "branding#index"
|
||||
put "branding/logo" => "branding#logo"
|
||||
get "colors/:id" => "color_palettes#show"
|
||||
|
||||
resources :flags, only: %i[index new create update destroy] do
|
||||
put "toggle"
|
||||
|
@ -3577,6 +3577,7 @@ experimental:
|
||||
use_overhauled_theme_color_palette:
|
||||
default: false
|
||||
hidden: true
|
||||
client: true
|
||||
rich_editor:
|
||||
client: true
|
||||
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]
|
||||
sha1 = url.match(/(\h{40})/).captures.first
|
||||
Upload.find_by(sha1:)
|
||||
when "toggle"
|
||||
component.find("button[role=\"switch\"]", visible: :all)["aria-checked"] == "true"
|
||||
end
|
||||
end
|
||||
|
||||
@ -91,6 +93,8 @@ module PageObjects
|
||||
component.find("input[type='checkbox']").click
|
||||
when "password"
|
||||
component.find(".form-kit__control-password-toggle").click
|
||||
when "toggle"
|
||||
component.find("button[role=\"switch\"]", visible: :all).ancestor("label").click
|
||||
else
|
||||
raise "'toggle' is not supported for control type: #{control_type}"
|
||||
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