DEV: Change category type to categories type for theme object schema (#26339)
Why this change?
This is a follow-up to 86b2e3aa3e
.
Basically, we want to allow people to select more than 1 category as well.
What does this change do?
1. Change `type: category` to `type: categories` and support `min` and `max`
validations for `type: categories`.
2. Fix the `<SchemaThemeSetting::Types::Categories>` component to support the
`min` and `max` validations and switch it to use the `<CategorySelector>` component
instead of the `<CategoryChooser>` component which only supports selecting one category.
This commit is contained in:
parent
0df50a7e5d
commit
476d91d233
|
@ -377,6 +377,7 @@ export default class SchemaThemeSettingEditor extends Component {
|
||||||
@spec={{field.spec}}
|
@spec={{field.spec}}
|
||||||
@onValueChange={{fn this.inputFieldChanged field}}
|
@onValueChange={{fn this.inputFieldChanged field}}
|
||||||
@description={{field.description}}
|
@description={{field.description}}
|
||||||
|
@setting={{@setting}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if (gt this.fields.length 0)}}
|
{{#if (gt this.fields.length 0)}}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Component from "@glimmer/component";
|
||||||
import { cached } from "@glimmer/tracking";
|
import { cached } from "@glimmer/tracking";
|
||||||
import htmlSafe from "discourse-common/helpers/html-safe";
|
import htmlSafe from "discourse-common/helpers/html-safe";
|
||||||
import BooleanField from "./types/boolean";
|
import BooleanField from "./types/boolean";
|
||||||
import CategoryField from "./types/category";
|
import CategoriesField from "./types/categories";
|
||||||
import EnumField from "./types/enum";
|
import EnumField from "./types/enum";
|
||||||
import FloatField from "./types/float";
|
import FloatField from "./types/float";
|
||||||
import GroupField from "./types/group";
|
import GroupField from "./types/group";
|
||||||
|
@ -25,8 +25,8 @@ export default class SchemaThemeSettingField extends Component {
|
||||||
return BooleanField;
|
return BooleanField;
|
||||||
case "enum":
|
case "enum":
|
||||||
return EnumField;
|
return EnumField;
|
||||||
case "category":
|
case "categories":
|
||||||
return CategoryField;
|
return CategoriesField;
|
||||||
case "tags":
|
case "tags":
|
||||||
return TagsField;
|
return TagsField;
|
||||||
case "group":
|
case "group":
|
||||||
|
@ -58,6 +58,7 @@ export default class SchemaThemeSettingField extends Component {
|
||||||
@spec={{@spec}}
|
@spec={{@spec}}
|
||||||
@onChange={{@onValueChange}}
|
@onChange={{@onValueChange}}
|
||||||
@description={{this.description}}
|
@description={{this.description}}
|
||||||
|
@setting={{@setting}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { hash } from "@ember/helper";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { and, not } from "truth-helpers";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||||
|
import CategorySelector from "select-kit/components/category-selector";
|
||||||
|
|
||||||
|
export default class SchemaThemeSettingTypeCategories extends Component {
|
||||||
|
@tracked touched = false;
|
||||||
|
|
||||||
|
@tracked
|
||||||
|
value =
|
||||||
|
this.args.value?.map((categoryId) => {
|
||||||
|
return this.args.setting.metadata.categories[categoryId];
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
required = this.args.spec.required;
|
||||||
|
min = this.args.spec.validations?.min;
|
||||||
|
max = this.args.spec.validations?.max;
|
||||||
|
|
||||||
|
@action
|
||||||
|
onInput(categories) {
|
||||||
|
this.touched = true;
|
||||||
|
this.value = categories;
|
||||||
|
this.args.onChange(categories.map((category) => category.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
get validationErrorMessage() {
|
||||||
|
if (!this.touched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(this.min && this.value.length < this.min) ||
|
||||||
|
(this.required && (!this.value || this.value.length === 0))
|
||||||
|
) {
|
||||||
|
return I18n.t("admin.customize.theme.schema.fields.categories.at_least", {
|
||||||
|
count: this.min || 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CategorySelector
|
||||||
|
@categories={{this.value}}
|
||||||
|
@onChange={{this.onInput}}
|
||||||
|
@options={{hash allowUncategorized=false maximum=this.max}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
import Component from "@glimmer/component";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
|
||||||
import CategoryChooser from "select-kit/components/category-chooser";
|
|
||||||
|
|
||||||
export default class SchemaThemeSettingTypeCategory extends Component {
|
|
||||||
@tracked value = this.args.value;
|
|
||||||
required = this.args.spec.required;
|
|
||||||
|
|
||||||
@action
|
|
||||||
onInput(newVal) {
|
|
||||||
this.value = newVal;
|
|
||||||
this.args.onChange(newVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
get categoryChooserOptions() {
|
|
||||||
return {
|
|
||||||
allowUncategorized: false,
|
|
||||||
none: !this.required,
|
|
||||||
clearable: !this.required,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CategoryChooser
|
|
||||||
@value={{this.value}}
|
|
||||||
@onChange={{this.onInput}}
|
|
||||||
@options={{this.categoryChooserOptions}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FieldInputDescription @description={{@description}} />
|
|
||||||
</template>
|
|
||||||
}
|
|
|
@ -184,7 +184,7 @@ export default function schemaAndData(version = 1) {
|
||||||
choices: ["nice", "awesome", "cool"]
|
choices: ["nice", "awesome", "cool"]
|
||||||
},
|
},
|
||||||
category_field: {
|
category_field: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
},
|
},
|
||||||
group_field: {
|
group_field: {
|
||||||
type: "group",
|
type: "group",
|
||||||
|
|
|
@ -229,6 +229,10 @@ export default function selectKit(selector) {
|
||||||
return filterHelper(query(selector).querySelector(".select-kit-filter"));
|
return filterHelper(query(selector).querySelector(".select-kit-filter"));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
error() {
|
||||||
|
return query(selector).querySelector(".select-kit-error");
|
||||||
|
},
|
||||||
|
|
||||||
rows() {
|
rows() {
|
||||||
return query(selector).querySelectorAll(".select-kit-row");
|
return query(selector).querySelectorAll(".select-kit-row");
|
||||||
},
|
},
|
||||||
|
|
|
@ -667,7 +667,7 @@ module(
|
||||||
assert.strictEqual(enumSelector.header().value(), "nice");
|
assert.strictEqual(enumSelector.header().value(), "nice");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("input fields of type category", async function (assert) {
|
test("input fields of type categories", async function (assert) {
|
||||||
const setting = ThemeSettings.create({
|
const setting = ThemeSettings.create({
|
||||||
setting: "objects_setting",
|
setting: "objects_setting",
|
||||||
objects_schema: {
|
objects_schema: {
|
||||||
|
@ -675,17 +675,29 @@ module(
|
||||||
identifier: "id",
|
identifier: "id",
|
||||||
properties: {
|
properties: {
|
||||||
required_category: {
|
required_category: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
not_required_category: {
|
not_required_category: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
|
validations: {
|
||||||
|
min: 2,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
categories: {
|
||||||
|
6: {
|
||||||
|
id: 6,
|
||||||
|
name: "some category",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
required_category: 6,
|
required_category: [6],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -706,9 +718,17 @@ module(
|
||||||
|
|
||||||
assert.strictEqual(categorySelector.header().value(), "6");
|
assert.strictEqual(categorySelector.header().value(), "6");
|
||||||
|
|
||||||
assert
|
await categorySelector.expand();
|
||||||
.dom(categorySelector.clearButton())
|
await categorySelector.deselectItemByValue("6");
|
||||||
.doesNotExist("is not clearable");
|
await categorySelector.collapse();
|
||||||
|
|
||||||
|
inputFields.refresh();
|
||||||
|
|
||||||
|
assert.dom(inputFields.fields.required_category.errorElement).hasText(
|
||||||
|
I18n.t("admin.customize.theme.schema.fields.categories.at_least", {
|
||||||
|
count: 1,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(inputFields.fields.not_required_category.labelElement)
|
.dom(inputFields.fields.not_required_category.labelElement)
|
||||||
|
@ -722,8 +742,24 @@ module(
|
||||||
|
|
||||||
await categorySelector.expand();
|
await categorySelector.expand();
|
||||||
await categorySelector.selectRowByIndex(1);
|
await categorySelector.selectRowByIndex(1);
|
||||||
|
await categorySelector.collapse();
|
||||||
|
|
||||||
assert.dom(categorySelector.clearButton()).exists("is clearable");
|
inputFields.refresh();
|
||||||
|
|
||||||
|
assert.dom(inputFields.fields.not_required_category.errorElement).hasText(
|
||||||
|
I18n.t("admin.customize.theme.schema.fields.categories.at_least", {
|
||||||
|
count: 2,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await categorySelector.expand();
|
||||||
|
await categorySelector.selectRowByIndex(2);
|
||||||
|
await categorySelector.selectRowByIndex(3);
|
||||||
|
await categorySelector.selectRowByIndex(4);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(categorySelector.error())
|
||||||
|
.hasText("You can only select 3 items.");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("input fields of type tags which is required", async function (assert) {
|
test("input fields of type tags which is required", async function (assert) {
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ThemeObjectsSettingMetadataSerializer < ApplicationSerializer
|
class ThemeObjectsSettingMetadataSerializer < ApplicationSerializer
|
||||||
attributes :property_descriptions
|
attributes :categories, :property_descriptions
|
||||||
|
|
||||||
|
def categories
|
||||||
|
object
|
||||||
|
.categories(scope)
|
||||||
|
.reduce({}) do |acc, category|
|
||||||
|
acc[category.id] = BasicCategorySerializer.new(category, scope: scope, root: false).as_json
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def property_descriptions
|
def property_descriptions
|
||||||
locales = {}
|
locales = {}
|
||||||
|
|
|
@ -5652,6 +5652,10 @@ en:
|
||||||
back_button: "Back to %{name}"
|
back_button: "Back to %{name}"
|
||||||
fields:
|
fields:
|
||||||
required: "*required"
|
required: "*required"
|
||||||
|
categories:
|
||||||
|
at_least:
|
||||||
|
one: "at least %{count} category is required"
|
||||||
|
other: "at least %{count} categories are required"
|
||||||
tags:
|
tags:
|
||||||
at_least:
|
at_least:
|
||||||
one: "at least %{count} tag is required"
|
one: "at least %{count} tag is required"
|
||||||
|
|
|
@ -141,40 +141,58 @@ en:
|
||||||
required: "must be present"
|
required: "must be present"
|
||||||
humanize_invalid_type: "The property at JSON Pointer '%{property_json_pointer}' must be of type %{type}."
|
humanize_invalid_type: "The property at JSON Pointer '%{property_json_pointer}' must be of type %{type}."
|
||||||
invalid_type: "%{type} is not a valid type"
|
invalid_type: "%{type} is not a valid type"
|
||||||
|
|
||||||
humanize_not_valid_string_value: "The property at JSON Pointer '%{property_json_pointer}' must be a string."
|
humanize_not_valid_string_value: "The property at JSON Pointer '%{property_json_pointer}' must be a string."
|
||||||
not_valid_string_value: "must be a string"
|
not_valid_string_value: "must be a string"
|
||||||
|
|
||||||
humanize_not_valid_integer_value: "The property at JSON Pointer '%{property_json_pointer}' must be an integer."
|
humanize_not_valid_integer_value: "The property at JSON Pointer '%{property_json_pointer}' must be an integer."
|
||||||
not_valid_integer_value: "must be an integer"
|
not_valid_integer_value: "must be an integer"
|
||||||
|
|
||||||
humanize_not_valid_float_value: "The property at JSON Pointer '%{property_json_pointer}' must be a float."
|
humanize_not_valid_float_value: "The property at JSON Pointer '%{property_json_pointer}' must be a float."
|
||||||
not_valid_float_value: "must be a float"
|
not_valid_float_value: "must be a float"
|
||||||
|
|
||||||
humanize_not_valid_boolean_value: "The property at JSON Pointer '%{property_json_pointer}' must be a boolean."
|
humanize_not_valid_boolean_value: "The property at JSON Pointer '%{property_json_pointer}' must be a boolean."
|
||||||
not_valid_boolean_value: "must be a boolean"
|
not_valid_boolean_value: "must be a boolean"
|
||||||
|
|
||||||
humanize_not_valid_enum_value: "The property at JSON Pointer '%{property_json_pointer}' must be one of the following %{choices}."
|
humanize_not_valid_enum_value: "The property at JSON Pointer '%{property_json_pointer}' must be one of the following %{choices}."
|
||||||
not_valid_enum_value: "must be one of the following: %{choices}"
|
not_valid_enum_value: "must be one of the following: %{choices}"
|
||||||
humanize_not_valid_category_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid category id."
|
|
||||||
not_valid_category_value: "must be a valid category id"
|
humanize_not_valid_categories_value: "The property at JSON Pointer '%{property_json_pointer}' must be an array of valid category ids."
|
||||||
|
not_valid_categories_value: "must be an array of valid category ids"
|
||||||
|
humanize_categories_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must have at least %{min} category ids."
|
||||||
|
categories_value_not_valid_min: "must have at least %{min} category ids"
|
||||||
|
humanize_categories_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must have at most %{max} category ids."
|
||||||
|
categories_value_not_valid_max: "must have at most %{max} category ids"
|
||||||
|
|
||||||
humanize_not_valid_topic_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid topic id."
|
humanize_not_valid_topic_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid topic id."
|
||||||
not_valid_topic_value: "must be a valid topic id"
|
not_valid_topic_value: "must be a valid topic id"
|
||||||
|
|
||||||
humanize_not_valid_post_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid post id."
|
humanize_not_valid_post_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid post id."
|
||||||
not_valid_post_value: "must be a valid post id"
|
not_valid_post_value: "must be a valid post id"
|
||||||
|
|
||||||
humanize_not_valid_group_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid group id."
|
humanize_not_valid_group_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid group id."
|
||||||
not_valid_group_value: "must be a valid group id"
|
not_valid_group_value: "must be a valid group id"
|
||||||
|
|
||||||
humanize_not_valid_tags_value: "The property at JSON Pointer '%{property_json_pointer}' must be an array of valid tag names."
|
humanize_not_valid_tags_value: "The property at JSON Pointer '%{property_json_pointer}' must be an array of valid tag names."
|
||||||
not_valid_tags_value: "must be an array of valid tag names"
|
not_valid_tags_value: "must be an array of valid tag names"
|
||||||
humanize_tags_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must have at least %{min} tag names."
|
humanize_tags_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must have at least %{min} tag names."
|
||||||
tags_value_not_valid_min: "must have at least %{min} tag names"
|
tags_value_not_valid_min: "must have at least %{min} tag names"
|
||||||
humanize_tags_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must have at most %{max} tag names."
|
humanize_tags_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must have at most %{max} tag names."
|
||||||
tags_value_not_valid_max: "must have at most %{max} tag names"
|
tags_value_not_valid_max: "must have at most %{max} tag names"
|
||||||
|
|
||||||
humanize_not_valid_upload_value: "The property at JSON Pointer '%{property_json_pointer}' must be a valid upload id."
|
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"
|
not_valid_upload_value: "must be a valid upload id"
|
||||||
|
|
||||||
humanize_string_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must be at least %{min} characters long."
|
humanize_string_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must be at least %{min} characters long."
|
||||||
string_value_not_valid_min: "must be at least %{min} characters long"
|
string_value_not_valid_min: "must be at least %{min} characters long"
|
||||||
humanize_string_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must be at most %{max} characters long."
|
humanize_string_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must be at most %{max} characters long."
|
||||||
string_value_not_valid_max: "must be at most %{max} characters long"
|
string_value_not_valid_max: "must be at most %{max} characters long"
|
||||||
|
|
||||||
humanize_number_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must be larger than or equal to %{min}."
|
humanize_number_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must be larger than or equal to %{min}."
|
||||||
number_value_not_valid_min: "must be larger than or equal to %{min}"
|
number_value_not_valid_min: "must be larger than or equal to %{min}"
|
||||||
humanize_number_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must be smaller than or equal to %{max}."
|
humanize_number_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must be smaller than or equal to %{max}."
|
||||||
number_value_not_valid_max: "must be smaller than or equal to %{max}"
|
number_value_not_valid_max: "must be smaller than or equal to %{max}"
|
||||||
|
|
||||||
humanize_string_value_not_valid_url: "The property at JSON Pointer '%{property_json_pointer}' must be a valid URL."
|
humanize_string_value_not_valid_url: "The property at JSON Pointer '%{property_json_pointer}' must be a valid URL."
|
||||||
string_value_not_valid_url: "must be a valid URL"
|
string_value_not_valid_url: "must be a valid URL"
|
||||||
locale_errors:
|
locale_errors:
|
||||||
|
|
|
@ -16,4 +16,21 @@ class ThemeSettingsManager::Objects < ThemeSettingsManager
|
||||||
def schema
|
def schema
|
||||||
@opts[:schema]
|
@opts[:schema]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def categories(guardian)
|
||||||
|
category_ids = Set.new
|
||||||
|
|
||||||
|
value.each do |theme_setting_object|
|
||||||
|
category_ids.merge(
|
||||||
|
ThemeSettingsObjectValidator.new(
|
||||||
|
schema:,
|
||||||
|
object: theme_setting_object,
|
||||||
|
).property_values_of_type("categories"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return [] if category_ids.empty?
|
||||||
|
|
||||||
|
Category.secured(guardian).where(id: category_ids)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,6 +85,10 @@ class ThemeSettingsObjectValidator
|
||||||
@errors
|
@errors
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def property_values_of_type(type)
|
||||||
|
fetch_property_values_of_type(@properties, @object, type)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_child_objects(objects, property_name:, schema:)
|
def validate_child_objects(objects, property_name:, schema:)
|
||||||
|
@ -120,7 +124,7 @@ class ThemeSettingsObjectValidator
|
||||||
case type
|
case type
|
||||||
when "string"
|
when "string"
|
||||||
value.is_a?(String)
|
value.is_a?(String)
|
||||||
when "integer", "category", "topic", "post", "group", "upload"
|
when "integer", "topic", "post", "group", "upload"
|
||||||
value.is_a?(Integer)
|
value.is_a?(Integer)
|
||||||
when "float"
|
when "float"
|
||||||
value.is_a?(Float) || value.is_a?(Integer)
|
value.is_a?(Float) || value.is_a?(Integer)
|
||||||
|
@ -128,6 +132,8 @@ class ThemeSettingsObjectValidator
|
||||||
[true, false].include?(value)
|
[true, false].include?(value)
|
||||||
when "enum"
|
when "enum"
|
||||||
property_attributes[:choices].include?(value)
|
property_attributes[:choices].include?(value)
|
||||||
|
when "categories"
|
||||||
|
value.is_a?(Array) && value.all? { |id| id.is_a?(Integer) }
|
||||||
when "tags"
|
when "tags"
|
||||||
value.is_a?(Array) && value.all? { |tag| tag.is_a?(String) }
|
value.is_a?(Array) && value.all? { |tag| tag.is_a?(String) }
|
||||||
else
|
else
|
||||||
|
@ -151,24 +157,24 @@ class ThemeSettingsObjectValidator
|
||||||
return true if value.nil?
|
return true if value.nil?
|
||||||
|
|
||||||
case type
|
case type
|
||||||
when "topic", "category", "upload", "post", "group"
|
when "topic", "upload", "post", "group"
|
||||||
if !valid_ids(type).include?(value)
|
if !valid_ids(type).include?(value)
|
||||||
add_error(property_name, :"not_valid_#{type}_value")
|
add_error(property_name, :"not_valid_#{type}_value")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
when "tags"
|
when "tags", "categories"
|
||||||
if !Array(value).to_set.subset?(valid_ids(type))
|
if !Array(value).to_set.subset?(valid_ids(type))
|
||||||
add_error(property_name, :"not_valid_#{type}_value")
|
add_error(property_name, :"not_valid_#{type}_value")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if (min = validations&.dig(:min)) && value.length < min
|
if (min = validations&.dig(:min)) && value.length < min
|
||||||
add_error(property_name, :tags_value_not_valid_min, min:)
|
add_error(property_name, :"#{type}_value_not_valid_min", min:)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if (max = validations&.dig(:max)) && value.length > max
|
if (max = validations&.dig(:max)) && value.length > max
|
||||||
add_error(property_name, :tags_value_not_valid_max, max:)
|
add_error(property_name, :"#{type}_value_not_valid_max", max:)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
when "string"
|
when "string"
|
||||||
|
@ -225,7 +231,7 @@ class ThemeSettingsObjectValidator
|
||||||
end
|
end
|
||||||
|
|
||||||
TYPE_TO_MODEL_MAP = {
|
TYPE_TO_MODEL_MAP = {
|
||||||
"category" => {
|
"categories" => {
|
||||||
klass: Category,
|
klass: Category,
|
||||||
},
|
},
|
||||||
"topic" => {
|
"topic" => {
|
||||||
|
|
|
@ -32,4 +32,20 @@ objects_setting:
|
||||||
validations:
|
validations:
|
||||||
max_length: 20
|
max_length: 20
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
objects_with_categories:
|
||||||
|
type: objects
|
||||||
|
default: []
|
||||||
|
schema:
|
||||||
|
name: categories
|
||||||
|
properties:
|
||||||
|
category_ids:
|
||||||
|
type: categories
|
||||||
|
child_categories:
|
||||||
|
type: objects
|
||||||
|
schema:
|
||||||
|
name: child category
|
||||||
|
properties:
|
||||||
|
category_ids:
|
||||||
|
type: categories
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
RSpec.describe ThemeSettingsManager::Objects do
|
RSpec.describe ThemeSettingsManager::Objects do
|
||||||
fab!(:theme)
|
fab!(:theme)
|
||||||
|
|
||||||
let(:objects_setting) do
|
let(:theme_setting) do
|
||||||
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml")
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml")
|
||||||
field = theme.set_field(target: :settings, name: "yaml", value: yaml)
|
field = theme.set_field(target: :settings, name: "yaml", value: yaml)
|
||||||
theme.save!
|
theme.save!
|
||||||
theme.settings[:objects_setting]
|
theme.settings
|
||||||
end
|
end
|
||||||
|
|
||||||
before { SiteSetting.experimental_objects_type_for_theme_settings = true }
|
before { SiteSetting.experimental_objects_type_for_theme_settings = true }
|
||||||
|
@ -27,7 +27,7 @@ RSpec.describe ThemeSettingsManager::Objects do
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
objects_setting.value = new_value
|
theme_setting[:objects_setting].value = new_value
|
||||||
|
|
||||||
expect(theme.reload.settings[:objects_setting].value).to eq(new_value)
|
expect(theme.reload.settings[:objects_setting].value).to eq(new_value)
|
||||||
end
|
end
|
||||||
|
@ -45,9 +45,42 @@ RSpec.describe ThemeSettingsManager::Objects do
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect { objects_setting.value = new_value }.to raise_error(
|
expect { theme_setting[:objects_setting].value = new_value }.to raise_error(
|
||||||
Discourse::InvalidParameters,
|
Discourse::InvalidParameters,
|
||||||
"The property at JSON Pointer '/0/links/0/name' must be present. The property at JSON Pointer '/1/name' must be present. The property at JSON Pointer '/1/links/0/name' must be at most 20 characters long.",
|
"The property at JSON Pointer '/0/links/0/name' must be present. The property at JSON Pointer '/1/name' must be present. The property at JSON Pointer '/1/links/0/name' must be at most 20 characters long.",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#categories" do
|
||||||
|
fab!(:category_1) { Fabricate(:category) }
|
||||||
|
fab!(:category_2) { Fabricate(:category) }
|
||||||
|
fab!(:category_3) { Fabricate(:private_category, group: Fabricate(:group)) }
|
||||||
|
fab!(:admin)
|
||||||
|
|
||||||
|
it "returns an empty array when there are no properties of `categories` type" do
|
||||||
|
expect(theme_setting[:objects_setting].categories(Guardian.new)).to eq([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the categories record for all the properties of `categories` type in a flat array" do
|
||||||
|
new_value = [
|
||||||
|
{
|
||||||
|
"category_ids" => [category_1.id, category_2.id],
|
||||||
|
"child_categories" => [{ "category_ids" => [category_3.id] }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
theme_setting[:objects_with_categories].value = new_value
|
||||||
|
|
||||||
|
expect(theme.reload.settings[:objects_with_categories].value).to eq(new_value)
|
||||||
|
|
||||||
|
expect(theme.settings[:objects_with_categories].categories(Guardian.new)).to contain_exactly(
|
||||||
|
category_1,
|
||||||
|
category_2,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
theme.settings[:objects_with_categories].categories(Guardian.new(admin)),
|
||||||
|
).to contain_exactly(category_1, category_2, category_3)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,7 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category_property: {
|
category_property: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
links: {
|
links: {
|
||||||
|
@ -49,10 +49,10 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
objects: [
|
objects: [
|
||||||
{
|
{
|
||||||
title: "1234",
|
title: "1234",
|
||||||
category_property: category.id,
|
category_property: [category.id],
|
||||||
links: [{ position: 1, float: 4.5 }, { position: "string", float: 12 }],
|
links: [{ position: 1, float: 4.5 }, { position: "string", float: 12 }],
|
||||||
},
|
},
|
||||||
{ title: "12345678910", category_property: 99_999_999, links: [{ float: 5 }] },
|
{ title: "12345678910", category_property: [99_999_999], links: [{ float: 5 }] },
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
"The property at JSON Pointer '/0/links/1/position' must be an integer.",
|
"The property at JSON Pointer '/0/links/1/position' must be an integer.",
|
||||||
"The property at JSON Pointer '/0/links/1/float' must be smaller than or equal to 11.5.",
|
"The property at JSON Pointer '/0/links/1/float' must be smaller than or equal to 11.5.",
|
||||||
"The property at JSON Pointer '/1/title' must be at most 10 characters long.",
|
"The property at JSON Pointer '/1/title' must be at most 10 characters long.",
|
||||||
"The property at JSON Pointer '/1/category_property' must be a valid category id.",
|
"The property at JSON Pointer '/1/category_property' must be an array of valid category ids.",
|
||||||
"The property at JSON Pointer '/1/links/0/position' must be present.",
|
"The property at JSON Pointer '/1/links/0/position' must be present.",
|
||||||
"The property at JSON Pointer '/1/links/0/float' must be larger than or equal to 5.5.",
|
"The property at JSON Pointer '/1/links/0/float' must be larger than or equal to 5.5.",
|
||||||
],
|
],
|
||||||
|
@ -1028,18 +1028,24 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "for category properties" do
|
context "for category properties" do
|
||||||
it "should not return any error message when the value of the property is a valid id of a category record" do
|
fab!(:category_1) { Fabricate(:category) }
|
||||||
category = Fabricate(:category)
|
fab!(:category_2) { Fabricate(:category) }
|
||||||
|
|
||||||
schema = { name: "section", properties: { category_property: { type: "category" } } }
|
it "should not return any error message when the value of the property is an array of valid category ids" do
|
||||||
|
schema = { name: "section", properties: { category_property: { type: "categories" } } }
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
described_class.new(schema: schema, object: { category_property: category.id }).validate,
|
described_class.new(
|
||||||
|
schema: schema,
|
||||||
|
object: {
|
||||||
|
category_property: [category_1.id, category_2.id],
|
||||||
|
},
|
||||||
|
).validate,
|
||||||
).to eq({})
|
).to eq({})
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should not return any error messages when the value is not present and it's not required in the schema" do
|
it "should not return any error messages when the value is not present and it's not required in the schema" do
|
||||||
schema = { name: "section", properties: { category_property: { type: "category" } } }
|
schema = { name: "section", properties: { category_property: { type: "categories" } } }
|
||||||
expect(described_class.new(schema: schema, object: {}).validate).to eq({})
|
expect(described_class.new(schema: schema, object: {}).validate).to eq({})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1048,7 +1054,7 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
name: "section",
|
name: "section",
|
||||||
properties: {
|
properties: {
|
||||||
category_property: {
|
category_property: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1059,30 +1065,51 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
expect(errors["/category_property"].full_messages).to contain_exactly("must be present")
|
expect(errors["/category_property"].full_messages).to contain_exactly("must be present")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should return the right hash of error messages when value of property is not an integer" do
|
it "should return the right hash of error messages when value of property contains an array where not all values are integers" do
|
||||||
schema = { name: "section", properties: { category_property: { type: "category" } } }
|
schema = { name: "section", properties: { category_property: { type: "categories" } } }
|
||||||
|
|
||||||
errors =
|
errors =
|
||||||
described_class.new(schema: schema, object: { category_property: "string" }).validate
|
described_class.new(schema: schema, object: { category_property: ["string"] }).validate
|
||||||
|
|
||||||
expect(errors.keys).to eq(["/category_property"])
|
expect(errors.keys).to eq(["/category_property"])
|
||||||
|
|
||||||
expect(errors["/category_property"].full_messages).to contain_exactly(
|
expect(errors["/category_property"].full_messages).to contain_exactly(
|
||||||
"must be a valid category id",
|
"must be an array of valid category ids",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should return the right hash of error messages when value of property is not a valid id of a category record" do
|
it "should return the right hash of error messages when number of category ids does not satisfy min or max validations" do
|
||||||
category = Fabricate(:category)
|
|
||||||
|
|
||||||
schema = {
|
schema = {
|
||||||
name: "section",
|
name: "section",
|
||||||
properties: {
|
properties: {
|
||||||
category_property: {
|
category_property: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
|
validations: {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = described_class.new(schema: schema, object: { category_property: [] }).validate
|
||||||
|
|
||||||
|
expect(errors.keys).to eq(["/category_property"])
|
||||||
|
|
||||||
|
expect(errors["/category_property"].full_messages).to contain_exactly(
|
||||||
|
"must have at least 1 category ids",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return the right hash of error messages when value of property is not an array of valid category ids" do
|
||||||
|
schema = {
|
||||||
|
name: "section",
|
||||||
|
properties: {
|
||||||
|
category_property: {
|
||||||
|
type: "categories",
|
||||||
},
|
},
|
||||||
category_property_2: {
|
category_property_2: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
},
|
},
|
||||||
child_categories: {
|
child_categories: {
|
||||||
type: "objects",
|
type: "objects",
|
||||||
|
@ -1090,7 +1117,7 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
name: "child_category",
|
name: "child_category",
|
||||||
properties: {
|
properties: {
|
||||||
category_property_3: {
|
category_property_3: {
|
||||||
type: "category",
|
type: "categories",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1098,36 +1125,34 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object = {
|
||||||
|
category_property: [99_999_999, category_1.id],
|
||||||
|
category_property_2: [99_999_999],
|
||||||
|
child_categories: [
|
||||||
|
{ category_property_3: [99_999_999, category_2.id] },
|
||||||
|
{ category_property_3: [category_2.id] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
queries =
|
queries =
|
||||||
track_sql_queries do
|
track_sql_queries do
|
||||||
errors =
|
errors = described_class.new(schema:, object:).validate
|
||||||
described_class.new(
|
|
||||||
schema: schema,
|
|
||||||
object: {
|
|
||||||
category_property: 99_999_999,
|
|
||||||
category_property_2: 99_999_999,
|
|
||||||
child_categories: [
|
|
||||||
{ category_property_3: 99_999_999 },
|
|
||||||
{ category_property_3: category.id },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
).validate
|
|
||||||
|
|
||||||
expect(errors.keys).to eq(
|
expect(errors.keys).to eq(
|
||||||
%w[/category_property /category_property_2 /child_categories/0/category_property_3],
|
%w[/category_property /category_property_2 /child_categories/0/category_property_3],
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(errors["/category_property"].full_messages).to contain_exactly(
|
expect(errors["/category_property"].full_messages).to contain_exactly(
|
||||||
"must be a valid category id",
|
"must be an array of valid category ids",
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(errors["/category_property_2"].full_messages).to contain_exactly(
|
expect(errors["/category_property_2"].full_messages).to contain_exactly(
|
||||||
"must be a valid category id",
|
"must be an array of valid category ids",
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
errors["/child_categories/0/category_property_3"].full_messages,
|
errors["/child_categories/0/category_property_3"].full_messages,
|
||||||
).to contain_exactly("must be a valid category id")
|
).to contain_exactly("must be an array of valid category ids")
|
||||||
end
|
end
|
||||||
|
|
||||||
# only 1 SQL query should be executed to check if category ids are valid
|
# only 1 SQL query should be executed to check if category ids are valid
|
||||||
|
|
|
@ -1390,6 +1390,35 @@ RSpec.describe Admin::ThemesController do
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "returns 200 with the right `categories` attribute for a theme setting with categories propertoes" do
|
||||||
|
category_1 = Fabricate(:category)
|
||||||
|
category_2 = Fabricate(:category)
|
||||||
|
category_3 = Fabricate(:category)
|
||||||
|
|
||||||
|
theme_setting[:objects_with_categories].value = [
|
||||||
|
{
|
||||||
|
"category_ids" => [category_1.id, category_2.id],
|
||||||
|
"child_categories" => [{ "category_ids" => [category_3.id] }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
get "/admin/themes/#{theme.id}/objects_setting_metadata/objects_with_categories.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
categories = response.parsed_body["categories"]
|
||||||
|
|
||||||
|
expect(categories.keys.map(&:to_i)).to contain_exactly(
|
||||||
|
category_1.id,
|
||||||
|
category_2.id,
|
||||||
|
category_3.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(categories[category_1.id.to_s]["name"]).to eq(category_1.name)
|
||||||
|
expect(categories[category_2.id.to_s]["name"]).to eq(category_2.name)
|
||||||
|
expect(categories[category_3.id.to_s]["name"]).to eq(category_3.name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,4 +37,58 @@ RSpec.describe ThemeObjectsSettingMetadataSerializer do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#categories" do
|
||||||
|
fab!(:category_1) { Fabricate(:category) }
|
||||||
|
fab!(:category_2) { Fabricate(:category) }
|
||||||
|
fab!(:category_3) { Fabricate(:private_category, group: Fabricate(:group)) }
|
||||||
|
fab!(:admin)
|
||||||
|
|
||||||
|
it "should return a hash of serialized categories" do
|
||||||
|
theme_setting[:objects_with_categories].value = [
|
||||||
|
{
|
||||||
|
"category_ids" => [category_1.id, category_2.id],
|
||||||
|
"child_categories" => [{ "category_ids" => [category_3.id] }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
scope = Guardian.new
|
||||||
|
|
||||||
|
payload =
|
||||||
|
described_class.new(theme_setting[:objects_with_categories], scope:, root: false).as_json
|
||||||
|
|
||||||
|
categories = payload[:categories]
|
||||||
|
|
||||||
|
expect(categories.keys).to contain_exactly(category_1.id, category_2.id)
|
||||||
|
|
||||||
|
expect(categories[category_1.id]).to eq(
|
||||||
|
BasicCategorySerializer.new(category_1, scope:, root: false).as_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(categories[category_2.id]).to eq(
|
||||||
|
BasicCategorySerializer.new(category_2, scope:, root: false).as_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
scope = Guardian.new(admin)
|
||||||
|
|
||||||
|
payload =
|
||||||
|
described_class.new(theme_setting[:objects_with_categories], scope:, root: false).as_json
|
||||||
|
|
||||||
|
categories = payload[:categories]
|
||||||
|
|
||||||
|
expect(categories.keys).to contain_exactly(category_1.id, category_2.id, category_3.id)
|
||||||
|
|
||||||
|
expect(categories[category_1.id]).to eq(
|
||||||
|
BasicCategorySerializer.new(category_1, scope:, root: false).as_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(categories[category_2.id]).to eq(
|
||||||
|
BasicCategorySerializer.new(category_2, scope:, root: false).as_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(categories[category_3.id]).to eq(
|
||||||
|
BasicCategorySerializer.new(category_3, scope:, root: false).as_json,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,18 +3,18 @@
|
||||||
RSpec.describe ThemeSettingsSerializer do
|
RSpec.describe ThemeSettingsSerializer do
|
||||||
fab!(:theme)
|
fab!(:theme)
|
||||||
|
|
||||||
let(:objects_setting) do
|
let(:theme_setting) do
|
||||||
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml")
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml")
|
||||||
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
||||||
theme.save!
|
theme.save!
|
||||||
theme.settings[:objects_setting]
|
theme.settings
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#objects_schema" do
|
before { SiteSetting.experimental_objects_type_for_theme_settings = true }
|
||||||
before { SiteSetting.experimental_objects_type_for_theme_settings = true }
|
|
||||||
|
|
||||||
|
describe "#objects_schema" do
|
||||||
it "should include the attribute when theme setting is typed objects" do
|
it "should include the attribute when theme setting is typed objects" do
|
||||||
payload = ThemeSettingsSerializer.new(objects_setting).as_json
|
payload = ThemeSettingsSerializer.new(theme_setting[:objects_setting]).as_json
|
||||||
|
|
||||||
expect(payload[:theme_settings][:objects_schema][:name]).to eq("section")
|
expect(payload[:theme_settings][:objects_schema][:name]).to eq("section")
|
||||||
end
|
end
|
||||||
|
|
|
@ -113,6 +113,10 @@ RSpec.describe "Admin editing objects type theme setting", type: :system do
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"setting": "objects_with_categories",
|
||||||
|
"value": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
SETTING
|
SETTING
|
||||||
|
|
Loading…
Reference in New Issue