mirror of
https://github.com/discourse/discourse.git
synced 2026-09-05 04:40:41 -05:00
FEATURE: Support icon properties in objects settings schemas (#42632)
This commit is contained in:
@@ -39,7 +39,10 @@ DiscourseEvent.on(:site_setting_changed) do |name, old_value, new_value|
|
||||
Scheduler::Defer.later("Null topic slug") { Topic.update_all(slug: nil) }
|
||||
end
|
||||
|
||||
SvgSprite.expire_cache if name.to_s.include?("_icon")
|
||||
if name.to_s.include?("_icon") ||
|
||||
%i[icon objects].include?(SiteSetting.type_supervisor.get_type(name))
|
||||
SvgSprite.expire_cache
|
||||
end
|
||||
|
||||
SiteIconManager.ensure_optimized! if SiteIconManager::WATCHED_SETTINGS.include?(name)
|
||||
|
||||
|
||||
@@ -238,6 +238,9 @@ en:
|
||||
humanize_not_valid_upload_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid upload id."
|
||||
not_valid_upload_value: "must be a valid upload id"
|
||||
|
||||
humanize_not_valid_icon_value: "The property at JSON Pointer '%{property_json_pointer}' must be an icon name."
|
||||
not_valid_icon_value: "must be an icon name"
|
||||
|
||||
humanize_string_value_not_valid_min:
|
||||
one: "The property at JSON Pointer '%{property_json_pointer}' must be at least %{count} character long."
|
||||
other: "The property at JSON Pointer '%{property_json_pointer}' must be at least %{count} characters long."
|
||||
|
||||
@@ -85,6 +85,7 @@ The above schema definition states that the `link` object has a `name` property
|
||||
- `categories`: Value of property is an array of valid category ids.
|
||||
- `groups`: Value of property is an array of valid group ids.
|
||||
- `tags`: Value of property is an array of valid tag names.
|
||||
- `icon`: Value of property is the name of a single icon from the Discourse icon set. Selected icons are automatically added to the sprite sheet, so they can be rendered without being registered separately.
|
||||
|
||||
With the schema defined, the default value of the setting can now be set by defining a array in yaml like so:
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import DatetimeField from "discourse/admin/components/schema-setting/types/datet
|
||||
import EnumField from "discourse/admin/components/schema-setting/types/enum";
|
||||
import FloatField from "discourse/admin/components/schema-setting/types/float";
|
||||
import GroupsField from "discourse/admin/components/schema-setting/types/groups";
|
||||
import IconField from "discourse/admin/components/schema-setting/types/icon";
|
||||
import IntegerField from "discourse/admin/components/schema-setting/types/integer";
|
||||
import StringField from "discourse/admin/components/schema-setting/types/string";
|
||||
import TagsField from "discourse/admin/components/schema-setting/types/tags";
|
||||
@@ -37,6 +38,8 @@ export default class SchemaSettingField extends Component {
|
||||
return UploadField;
|
||||
case "datetime":
|
||||
return DatetimeField;
|
||||
case "icon":
|
||||
return IconField;
|
||||
default:
|
||||
throw new Error(`unknown type ${type}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* eslint-disable ember/no-tracked-properties-from-args */
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "discourse/admin/components/schema-setting/field-input-description";
|
||||
import { and, not } from "discourse/truth-helpers";
|
||||
import DIconGridPicker from "discourse/ui-kit/d-icon-grid-picker";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class SchemaSettingTypeIcon extends Component {
|
||||
@tracked touched = false;
|
||||
@tracked value = this.args.value;
|
||||
required = this.args.spec.required;
|
||||
|
||||
@action
|
||||
onChange(newValue) {
|
||||
this.touched = true;
|
||||
this.value = newValue;
|
||||
this.args.onChange(newValue);
|
||||
}
|
||||
|
||||
get validationErrorMessage() {
|
||||
if (!this.touched) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.value && this.required) {
|
||||
return i18n("admin.customize.schema.fields.required");
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<DIconGridPicker
|
||||
@value={{this.value}}
|
||||
@onChange={{this.onChange}}
|
||||
@allowClear={{not this.required}}
|
||||
@showCaret={{true}}
|
||||
@showSelectedName={{true}}
|
||||
/>
|
||||
|
||||
<div class="schema-field__input-supporting-text">
|
||||
{{#if (and @description (not this.validationErrorMessage))}}
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-field__input-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
+64
-1
@@ -1,4 +1,4 @@
|
||||
import { click, fillIn, findAll, render } from "@ember/test-helpers";
|
||||
import { click, fillIn, findAll, render, waitFor } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import AdminSchemaSettingEditor from "discourse/admin/components/schema-setting/editor";
|
||||
import SiteSetting from "discourse/admin/models/site-setting";
|
||||
@@ -8,6 +8,7 @@ import schemaAndData, {
|
||||
SCHEMA_MODES,
|
||||
} from "discourse/tests/fixtures/theme-setting-schema-data";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import pretender, { response } from "discourse/tests/helpers/create-pretender";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@@ -802,6 +803,68 @@ module(
|
||||
assert.strictEqual(requiredEnumSelector.header().value(), "awesome");
|
||||
});
|
||||
|
||||
test("input fields of type icon", async function (assert) {
|
||||
pretender.get("/svg-sprite/picker-search", () =>
|
||||
response(200, {
|
||||
icons: [
|
||||
{ id: "gamepad", name: "gamepad" },
|
||||
{ id: "heart", name: "heart" },
|
||||
],
|
||||
has_more: false,
|
||||
})
|
||||
);
|
||||
|
||||
const setting = ThemeSettings.create({
|
||||
setting: "objects_setting",
|
||||
objects_schema: {
|
||||
name: "something",
|
||||
properties: {
|
||||
icon_field: {
|
||||
type: "icon",
|
||||
},
|
||||
required_icon_field: {
|
||||
type: "icon",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
value: [{ required_icon_field: "heart" }],
|
||||
});
|
||||
|
||||
await render(
|
||||
<template>
|
||||
<AdminSchemaSettingEditor
|
||||
@id="1"
|
||||
@setting={{setting}}
|
||||
@schema={{setting.objects_schema}}
|
||||
@routeToRedirect="adminCustomizeThemes.show"
|
||||
/>
|
||||
</template>
|
||||
);
|
||||
|
||||
const inputFields = new InputFieldsFromDOM();
|
||||
|
||||
assert
|
||||
.dom(
|
||||
`${inputFields.fields.required_icon_field.selector} .d-icon-grid-picker`
|
||||
)
|
||||
.hasAttribute("data-value", "heart");
|
||||
|
||||
assert
|
||||
.dom(`${inputFields.fields.icon_field.selector} .d-icon-grid-picker`)
|
||||
.doesNotHaveAttribute("data-value");
|
||||
|
||||
await click(
|
||||
`${inputFields.fields.icon_field.selector} .d-icon-grid-picker-trigger`
|
||||
);
|
||||
await waitFor("[data-icon-id='gamepad']");
|
||||
await click("[data-icon-id='gamepad']");
|
||||
|
||||
assert
|
||||
.dom(`${inputFields.fields.icon_field.selector} .d-icon-grid-picker`)
|
||||
.hasAttribute("data-value", "gamepad");
|
||||
});
|
||||
|
||||
test("input fields of type categories that is not required with min and max validations", async function (assert) {
|
||||
const setting = ThemeSettings.create({
|
||||
setting: "objects_setting",
|
||||
|
||||
@@ -208,7 +208,7 @@ class SchemaSettingsObjectValidator
|
||||
|
||||
is_value_valid =
|
||||
case type
|
||||
when "string", "datetime"
|
||||
when "string", "datetime", "icon"
|
||||
value.is_a?(String)
|
||||
when "integer", "topic", "post"
|
||||
value.is_a?(Integer)
|
||||
|
||||
+18
-6
@@ -534,8 +534,8 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
|
||||
|
||||
def self.settings_icons
|
||||
get_set_cache("settings_icons") do
|
||||
# includes svg_icon_subset, icon type settings, and any settings containing
|
||||
# _icon (incl. plugin settings)
|
||||
# includes svg_icon_subset, icon type settings, icon properties of objects
|
||||
# type settings, and any settings containing _icon (incl. plugin settings)
|
||||
site_setting_icons = []
|
||||
|
||||
SiteSetting.settings_hash.each do |key, value|
|
||||
@@ -543,6 +543,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
|
||||
|
||||
if key.to_s.include?("_icon") || SiteSetting.type_supervisor.get_type(key) == :icon
|
||||
site_setting_icons |= value.split("|")
|
||||
elsif SiteSetting.type_supervisor.get_type(key) == :objects && value.present?
|
||||
schema = SiteSetting.type_supervisor.type_hash(key)[:schema]
|
||||
site_setting_icons |= objects_setting_icons(schema, JSON.parse(value)) if schema
|
||||
end
|
||||
end
|
||||
|
||||
@@ -576,10 +579,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
|
||||
type_info = settings["theme_setting_type_info"] || {}
|
||||
|
||||
settings.each do |key, value|
|
||||
next unless String === value
|
||||
|
||||
if key.to_s.include?("_icon") || type_info.dig(key, :type) == "icon"
|
||||
theme_icon_settings |= value.split("|")
|
||||
if String === value
|
||||
if key.to_s.include?("_icon") || type_info.dig(key, :type) == "icon"
|
||||
theme_icon_settings |= value.split("|")
|
||||
end
|
||||
elsif type_info.dig(key, :type) == "objects" && value.is_a?(Array)
|
||||
schema = type_info.dig(key, :schema)
|
||||
theme_icon_settings |= objects_setting_icons(schema, value) if schema
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -589,6 +595,12 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
|
||||
theme_icon_settings
|
||||
end
|
||||
|
||||
def self.objects_setting_icons(schema, objects)
|
||||
SchemaSettingsObjectValidator.property_values_of_type(schema:, objects:, type: "icon").grep(
|
||||
String,
|
||||
)
|
||||
end
|
||||
|
||||
def self.custom_icons(theme_id)
|
||||
# Automatically register icons in sprites added via themes or plugins
|
||||
custom_svgs(theme_id).keys
|
||||
|
||||
+10
@@ -2,3 +2,13 @@ category:
|
||||
reaction_flair:
|
||||
type: icon
|
||||
default: ""
|
||||
reaction_list:
|
||||
type: objects
|
||||
default: []
|
||||
schema:
|
||||
name: reaction
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
icon:
|
||||
type: icon
|
||||
|
||||
@@ -1410,5 +1410,42 @@ RSpec.describe SchemaSettingsObjectValidator do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "for icon properties" do
|
||||
let(:schema) { { name: "section", properties: { icon_property: { type: "icon" } } } }
|
||||
|
||||
it "should not return any error messages when the value of the property is of type string" do
|
||||
expect(
|
||||
described_class.new(schema: schema, object: { icon_property: "heart" }).validate,
|
||||
).to eq({})
|
||||
end
|
||||
|
||||
it "should not return any error messages when the value is not present and it's not required in the schema" do
|
||||
expect(described_class.new(schema: schema, object: {}).validate).to eq({})
|
||||
end
|
||||
|
||||
it "should return the right hash of error messages when value of property is not present and it's required" do
|
||||
schema = {
|
||||
name: "section",
|
||||
properties: {
|
||||
icon_property: {
|
||||
type: "icon",
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
errors = described_class.new(schema: schema, object: {}).validate
|
||||
|
||||
expect(errors.keys).to eq(["/icon_property"])
|
||||
expect(errors["/icon_property"].full_messages).to contain_exactly("must be present")
|
||||
end
|
||||
|
||||
it "should return the right hash of error messages when value of property is not of type icon" do
|
||||
errors = described_class.new(schema: schema, object: { icon_property: 1 }).validate
|
||||
|
||||
expect(errors.keys).to eq(["/icon_property"])
|
||||
expect(errors["/icon_property"].full_messages).to contain_exactly("must be an icon name")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -177,6 +177,41 @@ RSpec.describe SvgSprite do
|
||||
expect(SvgSprite.all_icons).to include("dragon")
|
||||
end
|
||||
|
||||
it "includes icons defined in icon properties of objects type theme settings" do
|
||||
theme.set_field(target: :settings, name: :yaml, value: <<~YAML)
|
||||
featured_links:
|
||||
type: objects
|
||||
default:
|
||||
- title: link
|
||||
icon: dragon
|
||||
schema:
|
||||
name: link
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
icon:
|
||||
type: icon
|
||||
YAML
|
||||
theme.save!
|
||||
|
||||
expect(SvgSprite.all_icons(theme.id)).to include("dragon")
|
||||
|
||||
theme.update_setting(:featured_links, [{ "title" => "link", "icon" => "gas-pump" }])
|
||||
theme.save!
|
||||
|
||||
expect(SvgSprite.all_icons(theme.id)).to include("gas-pump")
|
||||
expect(SvgSprite.all_icons(theme.id)).not_to include("dragon")
|
||||
end
|
||||
|
||||
it "includes icons defined in icon properties of objects type site settings" do
|
||||
SiteSetting.load_settings(Rails.root.join("spec/fixtures/site_settings/icon_settings.yml").to_s)
|
||||
|
||||
SiteSetting.reaction_list = [{ name: "party", icon: "dragon" }].to_json
|
||||
SvgSprite.expire_cache
|
||||
|
||||
expect(SvgSprite.all_icons).to include("dragon")
|
||||
end
|
||||
|
||||
it "includes icons defined in theme modifiers" do
|
||||
child_theme = Fabricate(:theme, component: true)
|
||||
theme.add_relative_theme!(:child, child_theme)
|
||||
|
||||
@@ -118,6 +118,45 @@ RSpec.describe "Admin editing objects type" do
|
||||
)
|
||||
end
|
||||
|
||||
it "allows an admin to pick an icon for an icon type property" do
|
||||
SiteSetting.svg_icon_subset = "gamepad"
|
||||
|
||||
theme.set_field(target: :settings, name: "yaml", value: <<~YAML)
|
||||
links_setting:
|
||||
type: objects
|
||||
default:
|
||||
- title: link
|
||||
icon: heart
|
||||
schema:
|
||||
name: link
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
icon:
|
||||
type: icon
|
||||
YAML
|
||||
theme.save!
|
||||
|
||||
visit("/admin/customize/themes/#{theme.id}")
|
||||
|
||||
admin_objects_theme_setting_editor =
|
||||
admin_customize_themes_page.click_edit_objects_setting_button("links_setting")
|
||||
|
||||
icon_picker = PageObjects::Components::DIconGridPicker.new(".schema-field[data-name='icon']")
|
||||
|
||||
expect(icon_picker).to have_selected_icon("heart")
|
||||
|
||||
icon_picker.expand
|
||||
icon_picker.filter("gamepad")
|
||||
icon_picker.select_icon("gamepad")
|
||||
|
||||
admin_objects_theme_setting_editor.save
|
||||
|
||||
expect(theme.reload.settings[:links_setting].value).to eq(
|
||||
[{ "title" => "link", "icon" => "gamepad" }],
|
||||
)
|
||||
end
|
||||
|
||||
it "allows an admin to edit a theme setting of objects type via the settings editor" do
|
||||
visit "/admin/customize/themes/#{theme.id}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user