DEV: support json_schema in theme settings (#12294)
This commit is contained in:
parent
9fb9a2c098
commit
10780d2448
|
@ -1,6 +1,7 @@
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import { create } from "virtual-dom";
|
import { create } from "virtual-dom";
|
||||||
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { iconNode } from "discourse-common/lib/icon-library";
|
import { iconNode } from "discourse-common/lib/icon-library";
|
||||||
import loadScript from "discourse/lib/load-script";
|
import loadScript from "discourse/lib/load-script";
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
|
@ -32,12 +33,18 @@ export default Component.extend({
|
||||||
disable_edit_json: true,
|
disable_edit_json: true,
|
||||||
disable_properties: true,
|
disable_properties: true,
|
||||||
disable_collapse: true,
|
disable_collapse: true,
|
||||||
|
show_errors: "always",
|
||||||
startval: this.model.value ? JSON.parse(this.model.value) : null,
|
startval: this.model.value ? JSON.parse(this.model.value) : null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed("model.settingName")
|
||||||
|
settingName(name) {
|
||||||
|
return name.replace(/\_/g, " ");
|
||||||
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
saveChanges() {
|
saveChanges() {
|
||||||
const fieldValue = JSON.stringify(this.editor.getValue());
|
const fieldValue = JSON.stringify(this.editor.getValue());
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{#d-modal-body rawTitle=(i18n "admin.site_settings.json_schema.modal_title" name=model.settingName)}}
|
{{#d-modal-body rawTitle=(i18n "admin.site_settings.json_schema.modal_title" name=settingName)}}
|
||||||
|
|
||||||
<div id="json-editor-holder"></div>
|
<div id="json-editor-holder"></div>
|
||||||
{{/d-modal-body}}
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
|
|
@ -821,6 +821,28 @@
|
||||||
display: block !important;
|
display: block !important;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 1em 0;
|
||||||
|
}
|
||||||
|
td.compact {
|
||||||
|
.invalid-feedback {
|
||||||
|
margin: 0;
|
||||||
|
font-size: $font-down-1;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
margin-bottom: 0;
|
||||||
|
width: 95%;
|
||||||
|
&.is-invalid {
|
||||||
|
border-color: var(--danger);
|
||||||
|
outline: 1px solid var(--danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-invite-modal {
|
.create-invite-modal {
|
||||||
|
|
|
@ -177,6 +177,8 @@ class RemoteTheme < ActiveRecord::Base
|
||||||
updated_fields << theme.set_field(**opts.merge(value: value))
|
updated_fields << theme.set_field(**opts.merge(value: value))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
theme.convert_settings
|
||||||
|
|
||||||
# Destroy fields that no longer exist in the remote theme
|
# Destroy fields that no longer exist in the remote theme
|
||||||
field_ids_to_destroy = theme.theme_fields.pluck(:id) - updated_fields.map { |tf| tf&.id }
|
field_ids_to_destroy = theme.theme_fields.pluck(:id) - updated_fields.map { |tf| tf&.id }
|
||||||
ThemeField.where(id: field_ids_to_destroy).destroy_all
|
ThemeField.where(id: field_ids_to_destroy).destroy_all
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_dependency 'global_path'
|
require_dependency 'global_path'
|
||||||
|
require 'csv'
|
||||||
|
require 'json_schemer'
|
||||||
|
|
||||||
class Theme < ActiveRecord::Base
|
class Theme < ActiveRecord::Base
|
||||||
include GlobalPath
|
include GlobalPath
|
||||||
|
@ -622,6 +624,44 @@ class Theme < ActiveRecord::Base
|
||||||
list_baked_fields(target, name).count > 0
|
list_baked_fields(target, name).count > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def convert_settings
|
||||||
|
settings.each do |setting|
|
||||||
|
setting_row = ThemeSetting.where(theme_id: self.id, name: setting.name.to_s).first
|
||||||
|
|
||||||
|
if setting_row && setting_row.data_type != setting.type
|
||||||
|
if (setting_row.data_type == ThemeSetting.types[:list] &&
|
||||||
|
setting.type == ThemeSetting.types[:string] &&
|
||||||
|
setting.json_schema.present?)
|
||||||
|
convert_list_to_json_schema(setting_row, setting)
|
||||||
|
else
|
||||||
|
Rails.logger.warn("Theme setting type has changed but cannot be converted. \n\n #{setting.inspect}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_list_to_json_schema(setting_row, setting)
|
||||||
|
schema = setting.json_schema
|
||||||
|
return if !schema
|
||||||
|
keys = schema["items"]["properties"].keys
|
||||||
|
return if !keys
|
||||||
|
|
||||||
|
current_values = CSV.parse(setting_row.value, { col_sep: '|' }).flatten
|
||||||
|
new_values = []
|
||||||
|
current_values.each do |item|
|
||||||
|
parts = CSV.parse(item, { col_sep: ',' }).flatten
|
||||||
|
props = parts.map.with_index { |p, idx| [keys[idx], p] }.to_h
|
||||||
|
new_values << props
|
||||||
|
end
|
||||||
|
|
||||||
|
schemer = JSONSchemer.schema(schema)
|
||||||
|
raise "Schema validation failed" if !schemer.valid?(new_values)
|
||||||
|
|
||||||
|
setting_row.value = new_values.to_json
|
||||||
|
setting_row.data_type = setting.type
|
||||||
|
setting_row.save!
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def to_scss_variable(name, value)
|
def to_scss_variable(name, value)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class ThemeSettingsSerializer < ApplicationSerializer
|
class ThemeSettingsSerializer < ApplicationSerializer
|
||||||
attributes :setting, :type, :default, :value, :description, :valid_values,
|
attributes :setting, :type, :default, :value, :description, :valid_values,
|
||||||
:list_type, :textarea
|
:list_type, :textarea, :json_schema
|
||||||
|
|
||||||
def setting
|
def setting
|
||||||
object.name
|
object.name
|
||||||
|
@ -53,4 +53,11 @@ class ThemeSettingsSerializer < ApplicationSerializer
|
||||||
object.type == ThemeSetting.types[:string]
|
object.type == ThemeSetting.types[:string]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def json_schema
|
||||||
|
object.json_schema
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_json_schema?
|
||||||
|
object.type == ThemeSetting.types[:string] && object.json_schema.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5052,7 +5052,7 @@ en:
|
||||||
simple_list:
|
simple_list:
|
||||||
add_item: "Add item..."
|
add_item: "Add item..."
|
||||||
json_schema:
|
json_schema:
|
||||||
edit: Launch JSON Editor
|
edit: Launch Editor
|
||||||
modal_title: "Edit %{name}"
|
modal_title: "Edit %{name}"
|
||||||
|
|
||||||
badges:
|
badges:
|
||||||
|
|
|
@ -111,6 +111,10 @@ class ThemeSettingsManager
|
||||||
def textarea
|
def textarea
|
||||||
@opts[:textarea]
|
@opts[:textarea]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def json_schema
|
||||||
|
JSON.parse(@opts[:json_schema]) rescue false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Bool < self
|
class Bool < self
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ThemeSettingsParser
|
||||||
end
|
end
|
||||||
|
|
||||||
opts[:textarea] = !!raw_opts[:textarea]
|
opts[:textarea] = !!raw_opts[:textarea]
|
||||||
|
opts[:json_schema] = raw_opts[:json_schema]
|
||||||
|
|
||||||
opts
|
opts
|
||||||
end
|
end
|
||||||
|
|
|
@ -132,10 +132,15 @@ describe ThemeSettingsManager do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can be a textarea" do
|
it "can be a textarea" do
|
||||||
string_setting = find_by_name(:string_setting_02)
|
|
||||||
expect(find_by_name(:string_setting_02).textarea).to eq(false)
|
expect(find_by_name(:string_setting_02).textarea).to eq(false)
|
||||||
expect(find_by_name(:string_setting_03).textarea).to eq(true)
|
expect(find_by_name(:string_setting_03).textarea).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "supports json schema" do
|
||||||
|
expect(find_by_name(:string_setting_03).json_schema).to eq(false)
|
||||||
|
expect(find_by_name(:invalid_json_schema_setting).json_schema).to eq(false)
|
||||||
|
expect(find_by_name(:valid_json_schema_setting).json_schema).to be_truthy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "List" do
|
context "List" do
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
test_setting:
|
||||||
|
default: ""
|
||||||
|
description: "A JSON schema field type"
|
||||||
|
json_schema: '{ "type": "array", "uniqueItems": true, "items": { "type": "object", "properties": { "color": { "type": "string" }, "icon": { "type": "string" } }, "additionalProperties": false } }'
|
|
@ -72,3 +72,11 @@ enum_setting_03:
|
||||||
upload_setting:
|
upload_setting:
|
||||||
type: upload
|
type: upload
|
||||||
default: ""
|
default: ""
|
||||||
|
|
||||||
|
invalid_json_schema_setting:
|
||||||
|
default: ""
|
||||||
|
json_schema: '{ "type": "array", "invalid json"'
|
||||||
|
|
||||||
|
valid_json_schema_setting:
|
||||||
|
default: ""
|
||||||
|
json_schema: '{ "type": "array", "uniqueItems": true, "items": { "type": "object", "properties": { "color": { "type": "string" }, "icon": { "type": "string" } }, "additionalProperties": false } }'
|
||||||
|
|
|
@ -608,6 +608,72 @@ HTML
|
||||||
expect(json).to match(/\"boolean_setting\":false/)
|
expect(json).to match(/\"boolean_setting\":false/)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "convert_settings" do
|
||||||
|
|
||||||
|
it 'can migrate a list field to a string field with json schema' do
|
||||||
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"green,globe\"\n type: \"list\"")
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
||||||
|
setting.value = "red,globe|green,cog|brown,users"
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
expect(setting.type).to eq(ThemeSetting.types[:list])
|
||||||
|
|
||||||
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
||||||
|
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
theme.convert_settings
|
||||||
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
||||||
|
|
||||||
|
expect(JSON.parse(setting.value)).to eq(JSON.parse('[{"color":"red","icon":"globe"},{"color":"green","icon":"cog"},{"color":"brown","icon":"users"}]'))
|
||||||
|
expect(setting.type).to eq(ThemeSetting.types[:string])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update setting if data does not validate against json schema' do
|
||||||
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"green,globe\"\n type: \"list\"")
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
||||||
|
|
||||||
|
# json_schema_settings.yaml defines only two properties per object and disallows additionalProperties
|
||||||
|
setting.value = "red,globe,hey|green,cog,hey|brown,users,nay"
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
||||||
|
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
expect { theme.convert_settings }.to raise_error("Schema validation failed")
|
||||||
|
|
||||||
|
setting.value = "red,globe|green,cog|brown"
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
expect { theme.convert_settings }.not_to raise_error
|
||||||
|
|
||||||
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
||||||
|
expect(setting.type).to eq(ThemeSetting.types[:string])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'warns when the theme has modified the setting type but data cannot be converted' do
|
||||||
|
Rails.logger = FakeLogger.new
|
||||||
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"\"\n type: \"list\"")
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
||||||
|
setting.value = "red,globe"
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"\"\n type: \"string\"")
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
theme.convert_settings
|
||||||
|
expect(setting.value).to eq("red,globe")
|
||||||
|
expect(Rails.logger.warnings[0]).to include("Theme setting type has changed but cannot be converted.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "theme translations" do
|
describe "theme translations" do
|
||||||
it "can list working theme_translation_manager objects" do
|
it "can list working theme_translation_manager objects" do
|
||||||
en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML)
|
en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML)
|
||||||
|
|
Loading…
Reference in New Issue