mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 06:29:30 +08:00
DEV: Change group type to groups type for theme object schema (#26417)
Why this change?
This is a follow-up to 86b2e3a
.
Basically, we want to allow people to select more than 1 group as well.
What does this change do?
1. Change `type: group` to `type: groups` and support `min` and `max`
validations for `type: groups`.
2. Fix the `<SchemaThemeSetting::Types::Groups>` component to support the
`min` and `max` validations and switch it to use the `<GroupChooser>` component
instead of the `<ComboBoxComponent>` component which previously only supported
selecting a single group.
This commit is contained in:
parent
186d6e4996
commit
a670d6d4af
|
@ -1,14 +1,14 @@
|
||||||
import Component from "@glimmer/component";
|
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 "admin/components/schema-theme-setting/types/boolean";
|
||||||
import CategoriesField from "./types/categories";
|
import CategoriesField from "admin/components/schema-theme-setting/types/categories";
|
||||||
import EnumField from "./types/enum";
|
import EnumField from "admin/components/schema-theme-setting/types/enum";
|
||||||
import FloatField from "./types/float";
|
import FloatField from "admin/components/schema-theme-setting/types/float";
|
||||||
import GroupField from "./types/group";
|
import GroupsField from "admin/components/schema-theme-setting/types/groups";
|
||||||
import IntegerField from "./types/integer";
|
import IntegerField from "admin/components/schema-theme-setting/types/integer";
|
||||||
import StringField from "./types/string";
|
import StringField from "admin/components/schema-theme-setting/types/string";
|
||||||
import TagsField from "./types/tags";
|
import TagsField from "admin/components/schema-theme-setting/types/tags";
|
||||||
|
|
||||||
export default class SchemaThemeSettingField extends Component {
|
export default class SchemaThemeSettingField extends Component {
|
||||||
get component() {
|
get component() {
|
||||||
|
@ -29,8 +29,8 @@ export default class SchemaThemeSettingField extends Component {
|
||||||
return CategoriesField;
|
return CategoriesField;
|
||||||
case "tags":
|
case "tags":
|
||||||
return TagsField;
|
return TagsField;
|
||||||
case "group":
|
case "groups":
|
||||||
return GroupField;
|
return GroupsField;
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown type ${type}`);
|
throw new Error(`unknown type ${type}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import Component from "@glimmer/component";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
|
||||||
import ComboBoxComponent from "select-kit/components/combo-box";
|
|
||||||
|
|
||||||
export default class SchemaThemeSettingTypeGroup extends Component {
|
|
||||||
@service site;
|
|
||||||
@tracked value = this.args.value;
|
|
||||||
|
|
||||||
required = this.args.spec.required;
|
|
||||||
|
|
||||||
@action
|
|
||||||
onInput(newVal) {
|
|
||||||
this.value = newVal;
|
|
||||||
this.args.onChange(newVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
get groupChooserOptions() {
|
|
||||||
return {
|
|
||||||
clearable: !this.required,
|
|
||||||
filterable: true,
|
|
||||||
none: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ComboBoxComponent
|
|
||||||
@content={{this.site.groups}}
|
|
||||||
@value={{this.value}}
|
|
||||||
@onChange={{this.onInput}}
|
|
||||||
@options={{this.groupChooserOptions}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FieldInputDescription @description={{@description}} />
|
|
||||||
</template>
|
|
||||||
}
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { and, not } from "truth-helpers";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||||
|
import GroupChooser from "select-kit/components/group-chooser";
|
||||||
|
|
||||||
|
export default class SchemaThemeSettingTypeGroups extends Component {
|
||||||
|
@service site;
|
||||||
|
@tracked touched = false;
|
||||||
|
@tracked value = this.args.value;
|
||||||
|
|
||||||
|
required = this.args.spec.required;
|
||||||
|
min = this.args.spec.validations?.min;
|
||||||
|
max = this.args.spec.validations?.max;
|
||||||
|
|
||||||
|
@action
|
||||||
|
onInput(newVal) {
|
||||||
|
this.touched = true;
|
||||||
|
this.value = newVal;
|
||||||
|
this.args.onChange(newVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.groups.at_least", {
|
||||||
|
count: this.min || 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get groupChooserOptions() {
|
||||||
|
return {
|
||||||
|
clearable: !this.required,
|
||||||
|
filterable: true,
|
||||||
|
maximum: this.max,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GroupChooser
|
||||||
|
@content={{this.site.groups}}
|
||||||
|
@value={{this.value}}
|
||||||
|
@onChange={{this.onInput}}
|
||||||
|
@options={{this.groupChooserOptions}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
}
|
|
@ -187,7 +187,7 @@ export default function schemaAndData(version = 1) {
|
||||||
type: "categories",
|
type: "categories",
|
||||||
},
|
},
|
||||||
group_field: {
|
group_field: {
|
||||||
type: "group",
|
type: "groups",
|
||||||
},
|
},
|
||||||
tags_field: {
|
tags_field: {
|
||||||
type: "tags",
|
type: "tags",
|
||||||
|
|
|
@ -859,25 +859,28 @@ module(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("input fields of type group", async function (assert) {
|
test("input fields of type groups", async function (assert) {
|
||||||
const setting = ThemeSettings.create({
|
const setting = ThemeSettings.create({
|
||||||
setting: "objects_setting",
|
setting: "objects_setting",
|
||||||
objects_schema: {
|
objects_schema: {
|
||||||
name: "something",
|
name: "something",
|
||||||
identifier: "id",
|
|
||||||
properties: {
|
properties: {
|
||||||
required_group: {
|
required_groups: {
|
||||||
type: "group",
|
type: "groups",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
not_required_group: {
|
groups_with_validations: {
|
||||||
type: "group",
|
type: "groups",
|
||||||
|
validations: {
|
||||||
|
min: 2,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
required_group: 6,
|
required_groups: [0, 1],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -888,29 +891,59 @@ module(
|
||||||
|
|
||||||
const inputFields = new InputFieldsFromDOM();
|
const inputFields = new InputFieldsFromDOM();
|
||||||
|
|
||||||
assert
|
let groupsSelector = selectKit(
|
||||||
.dom(inputFields.fields.required_group.labelElement)
|
`${inputFields.fields.required_groups.selector} .select-kit`
|
||||||
.hasText("required_group*");
|
|
||||||
|
|
||||||
let groupSelector = selectKit(
|
|
||||||
`${inputFields.fields.required_group.selector} .select-kit`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(groupSelector.header().value(), "6");
|
assert.strictEqual(groupsSelector.header().value(), "0,1");
|
||||||
assert.dom(groupSelector.clearButton()).doesNotExist("is not clearable");
|
|
||||||
|
|
||||||
assert
|
await groupsSelector.expand();
|
||||||
.dom(inputFields.fields.not_required_group.labelElement)
|
await groupsSelector.deselectItemByValue("0");
|
||||||
.hasText("not_required_group");
|
await groupsSelector.deselectItemByValue("1");
|
||||||
|
await groupsSelector.collapse();
|
||||||
|
|
||||||
groupSelector = selectKit(
|
inputFields.refresh();
|
||||||
`${inputFields.fields.not_required_group.selector} .select-kit`
|
|
||||||
|
assert.dom(inputFields.fields.required_groups.errorElement).hasText(
|
||||||
|
I18n.t("admin.customize.theme.schema.fields.groups.at_least", {
|
||||||
|
count: 1,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await groupSelector.expand();
|
assert
|
||||||
await groupSelector.selectRowByIndex(1);
|
.dom(inputFields.fields.groups_with_validations.labelElement)
|
||||||
|
.hasText("groups_with_validations");
|
||||||
|
|
||||||
assert.dom(groupSelector.clearButton()).exists("is clearable");
|
groupsSelector = selectKit(
|
||||||
|
`${inputFields.fields.groups_with_validations.selector} .select-kit`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(groupsSelector.header().value(), null);
|
||||||
|
|
||||||
|
await groupsSelector.expand();
|
||||||
|
await groupsSelector.selectRowByIndex(1);
|
||||||
|
await groupsSelector.collapse();
|
||||||
|
|
||||||
|
assert.strictEqual(groupsSelector.header().value(), "1");
|
||||||
|
|
||||||
|
inputFields.refresh();
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(inputFields.fields.groups_with_validations.errorElement)
|
||||||
|
.hasText(
|
||||||
|
I18n.t("admin.customize.theme.schema.fields.groups.at_least", {
|
||||||
|
count: 2,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await groupsSelector.expand();
|
||||||
|
await groupsSelector.selectRowByIndex(2);
|
||||||
|
await groupsSelector.selectRowByIndex(3);
|
||||||
|
await groupsSelector.selectRowByIndex(4);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(groupsSelector.error())
|
||||||
|
.hasText("You can only select 3 items.");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generic identifier is used when identifier is not specified in the schema", async function (assert) {
|
test("generic identifier is used when identifier is not specified in the schema", async function (assert) {
|
||||||
|
|
|
@ -5654,6 +5654,10 @@ en:
|
||||||
back_button: "Back to %{name}"
|
back_button: "Back to %{name}"
|
||||||
fields:
|
fields:
|
||||||
required: "*required"
|
required: "*required"
|
||||||
|
groups:
|
||||||
|
at_least:
|
||||||
|
one: "at least %{count} group is required"
|
||||||
|
other: "at least %{count} groups are required"
|
||||||
categories:
|
categories:
|
||||||
at_least:
|
at_least:
|
||||||
one: "at least %{count} category is required"
|
one: "at least %{count} category is required"
|
||||||
|
|
|
@ -170,8 +170,12 @@ en:
|
||||||
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_groups_value: "The property at JSON Pointer '%{property_json_pointer}' must be an array of valid group ids."
|
||||||
not_valid_group_value: "must be a valid group id"
|
not_valid_groups_value: "must be an array of valid group ids"
|
||||||
|
humanize_groups_value_not_valid_min: "The property at JSON Pointer '%{property_json_pointer}' must have at least %{min} group ids."
|
||||||
|
groups_value_not_valid_min: "must have at least %{min} group ids"
|
||||||
|
humanize_groups_value_not_valid_max: "The property at JSON Pointer '%{property_json_pointer}' must have at most %{max} group ids."
|
||||||
|
groups_value_not_valid_max: "must have at most %{max} group ids"
|
||||||
|
|
||||||
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"
|
||||||
|
|
|
@ -124,7 +124,7 @@ class ThemeSettingsObjectValidator
|
||||||
case type
|
case type
|
||||||
when "string"
|
when "string"
|
||||||
value.is_a?(String)
|
value.is_a?(String)
|
||||||
when "integer", "topic", "post", "group", "upload"
|
when "integer", "topic", "post", "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)
|
||||||
|
@ -132,7 +132,7 @@ 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"
|
when "categories", "groups"
|
||||||
value.is_a?(Array) && value.all? { |id| id.is_a?(Integer) }
|
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) }
|
||||||
|
@ -157,12 +157,12 @@ class ThemeSettingsObjectValidator
|
||||||
return true if value.nil?
|
return true if value.nil?
|
||||||
|
|
||||||
case type
|
case type
|
||||||
when "topic", "upload", "post", "group"
|
when "topic", "upload", "post"
|
||||||
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", "categories"
|
when "tags", "categories", "groups"
|
||||||
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
|
||||||
|
@ -240,7 +240,7 @@ class ThemeSettingsObjectValidator
|
||||||
"post" => {
|
"post" => {
|
||||||
klass: Post,
|
klass: Post,
|
||||||
},
|
},
|
||||||
"group" => {
|
"groups" => {
|
||||||
klass: Group,
|
klass: Group,
|
||||||
},
|
},
|
||||||
"upload" => {
|
"upload" => {
|
||||||
|
|
|
@ -845,19 +845,19 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "for group properties" do
|
context "for groups properties" do
|
||||||
it "should not return any error message when the value of the property is a valid id of a group record" do
|
it "should not return any error message when the value of the property is an array of valid group record ids" do
|
||||||
group = Fabricate(:group)
|
group = Fabricate(:group)
|
||||||
|
|
||||||
schema = { name: "section", properties: { group_property: { type: "group" } } }
|
schema = { name: "section", properties: { groups_property: { type: "groups" } } }
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
described_class.new(schema: schema, object: { group_property: group.id }).validate,
|
described_class.new(schema: schema, object: { groups_property: [group.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: { group_property: { type: "group" } } }
|
schema = { name: "section", properties: { groups_property: { type: "groups" } } }
|
||||||
expect(described_class.new(schema: schema, object: {}).validate).to eq({})
|
expect(described_class.new(schema: schema, object: {}).validate).to eq({})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -865,44 +865,85 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
schema = {
|
schema = {
|
||||||
name: "section",
|
name: "section",
|
||||||
properties: {
|
properties: {
|
||||||
group_property: {
|
groups_property: {
|
||||||
type: "group",
|
type: "groups",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
errors = described_class.new(schema: schema, object: {}).validate
|
errors = described_class.new(schema: schema, object: {}).validate
|
||||||
|
|
||||||
expect(errors.keys).to eq(["/group_property"])
|
expect(errors.keys).to eq(["/groups_property"])
|
||||||
expect(errors["/group_property"].full_messages).to contain_exactly("must be present")
|
expect(errors["/groups_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 is not an array of valid group ids" do
|
||||||
schema = { name: "section", properties: { group_property: { type: "group" } } }
|
schema = { name: "section", properties: { groups_property: { type: "groups" } } }
|
||||||
|
|
||||||
errors = described_class.new(schema: schema, object: { group_property: "string" }).validate
|
errors = described_class.new(schema: schema, object: { groups_property: "string" }).validate
|
||||||
|
|
||||||
expect(errors.keys).to eq(["/group_property"])
|
expect(errors.keys).to eq(["/groups_property"])
|
||||||
|
|
||||||
expect(errors["/group_property"].full_messages).to contain_exactly(
|
expect(errors["/groups_property"].full_messages).to contain_exactly(
|
||||||
"must be a valid group id",
|
"must be an array of valid group ids",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should return the right hash of error messages when value of property is not a valid id of a group record" do
|
it "should return the right hash of error messages when number of groups ids does not satisfy min or max validations" do
|
||||||
|
group_1 = Fabricate(:group)
|
||||||
|
group_2 = Fabricate(:group)
|
||||||
|
group_3 = Fabricate(:group)
|
||||||
|
|
||||||
schema = {
|
schema = {
|
||||||
name: "section",
|
name: "section",
|
||||||
properties: {
|
properties: {
|
||||||
group_property: {
|
group_property: {
|
||||||
type: "group",
|
type: "groups",
|
||||||
|
validations: {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = described_class.new(schema: schema, object: { group_property: [] }).validate
|
||||||
|
|
||||||
|
expect(errors.keys).to eq(["/group_property"])
|
||||||
|
|
||||||
|
expect(errors["/group_property"].full_messages).to contain_exactly(
|
||||||
|
"must have at least 1 group ids",
|
||||||
|
)
|
||||||
|
|
||||||
|
errors =
|
||||||
|
described_class.new(
|
||||||
|
schema: schema,
|
||||||
|
object: {
|
||||||
|
group_property: [group_1.id, group_2.id, group_3.id],
|
||||||
|
},
|
||||||
|
).validate
|
||||||
|
|
||||||
|
expect(errors.keys).to eq(["/group_property"])
|
||||||
|
|
||||||
|
expect(errors["/group_property"].full_messages).to contain_exactly(
|
||||||
|
"must have at most 2 group ids",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return the right hash of error messages when value of property is an array containing invalid group ids" do
|
||||||
|
schema = {
|
||||||
|
name: "section",
|
||||||
|
properties: {
|
||||||
|
groups_property: {
|
||||||
|
type: "groups",
|
||||||
},
|
},
|
||||||
child_groups: {
|
child_groups: {
|
||||||
type: "objects",
|
type: "objects",
|
||||||
schema: {
|
schema: {
|
||||||
name: "child_group",
|
name: "child_group",
|
||||||
properties: {
|
properties: {
|
||||||
group_property_2: {
|
groups_property_2: {
|
||||||
type: "group",
|
type: "groups",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -916,19 +957,19 @@ RSpec.describe ThemeSettingsObjectValidator do
|
||||||
described_class.new(
|
described_class.new(
|
||||||
schema:,
|
schema:,
|
||||||
object: {
|
object: {
|
||||||
group_property: 99_999_999,
|
groups_property: [99_999_999],
|
||||||
child_groups: [{ group_property_2: 99_999_999 }],
|
child_groups: [{ groups_property_2: [99_999_999] }],
|
||||||
},
|
},
|
||||||
).validate
|
).validate
|
||||||
|
|
||||||
expect(errors.keys).to eq(%w[/group_property /child_groups/0/group_property_2])
|
expect(errors.keys).to eq(%w[/groups_property /child_groups/0/groups_property_2])
|
||||||
|
|
||||||
expect(errors["/group_property"].full_messages).to contain_exactly(
|
expect(errors["/groups_property"].full_messages).to contain_exactly(
|
||||||
"must be a valid group id",
|
"must be an array of valid group ids",
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(errors["/child_groups/0/group_property_2"].full_messages).to contain_exactly(
|
expect(errors["/child_groups/0/groups_property_2"].full_messages).to contain_exactly(
|
||||||
"must be a valid group id",
|
"must be an array of valid group ids",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user