DEV: support json_schema in theme settings (#12294)

This commit is contained in:
Penar Musaraj 2021-03-10 20:15:04 -05:00 committed by GitHub
parent 9fb9a2c098
commit 10780d2448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 171 additions and 4 deletions

View File

@ -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());

View File

@ -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}}

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 } }'

View File

@ -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 } }'

View File

@ -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)