diff --git a/addons/web/static/src/xml/search_panel.xml b/addons/web/static/src/xml/search_panel.xml
new file mode 100644
index 00000000000..4b6dc4d7a60
--- /dev/null
+++ b/addons/web/static/src/xml/search_panel.xml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/tests/control_panel/comparison_menu_tests.js b/addons/web/static/tests/control_panel/comparison_menu_tests.js
index 141250c3bb4..e376904f503 100644
--- a/addons/web/static/tests/control_panel/comparison_menu_tests.js
+++ b/addons/web/static/tests/control_panel/comparison_menu_tests.js
@@ -18,13 +18,14 @@ odoo.define('web.comparison_menu_tests', function (require) {
float_field: { string: "Float", type: "float", group_operator: 'sum' },
foo: { string: "Foo", type: "char", store: true, sortable: true },
};
- this.viewInfo = {
+ this.cpModelConfig = {
arch: `
`,
fields: this.fields,
+ searchMenuTypes,
};
},
}, function () {
@@ -36,7 +37,7 @@ odoo.define('web.comparison_menu_tests', function (require) {
const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0);
const params = {
- cpStoreConfig: { viewInfo: this.viewInfo, searchMenuTypes },
+ cpModelConfig: this.cpModelConfig,
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -71,7 +72,7 @@ odoo.define('web.comparison_menu_tests', function (require) {
const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0);
const params = {
- cpStoreConfig: { viewInfo: this.viewInfo, searchMenuTypes },
+ cpModelConfig: this.cpModelConfig,
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -125,10 +126,9 @@ odoo.define('web.comparison_menu_tests', function (require) {
QUnit.test('no timeRanges key in search query if "comparison" not in searchMenuTypes', async function (assert) {
assert.expect(1);
+ this.cpModelConfig.searchMenuTypes = ['filter'];
const params = {
- cpStoreConfig: {
- viewInfo: this.viewInfo, searchMenuTypes: ['filter'],
- },
+ cpModelConfig: this.cpModelConfig,
cpProps: { fields: this.fields, searchMenuTypes: ['filter'] },
};
const controlPanel = await createControlPanel(params);
diff --git a/addons/web/static/tests/control_panel/control_panel_model_tests.js b/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js
similarity index 67%
rename from addons/web/static/tests/control_panel/control_panel_model_tests.js
rename to addons/web/static/tests/control_panel/control_panel_model_extension_tests.js
index 211912d8ac2..3bf2f58d306 100644
--- a/addons/web/static/tests/control_panel/control_panel_model_tests.js
+++ b/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js
@@ -1,17 +1,31 @@
-odoo.define('web.control_panel_model_tests', function (require) {
+odoo.define("web/static/tests/control_panel/control_panel_model_extension_tests.js", function (require) {
"use strict";
- const ControlPanelModel = require('web.ControlPanelModel');
+ const ActionModel = require("web/static/src/js/views/action_model.js");
const makeTestEnvironment = require('web.test_env');
- function createControlPanelModel(config = {}, env, rpc) {
- return new ControlPanelModel(Object.assign(
- { env: makeTestEnvironment(env, rpc) },
- config
- ));
+ function createModel(params = {}) {
+ const archs = (params.arch && { search: params.arch, }) || {};
+ const { ControlPanel: controlPanelInfo, } = ActionModel.extractArchInfo(archs);
+ const extensions = {
+ ControlPanel: {
+ context: params.context,
+ archNodes: controlPanelInfo.children,
+ dynamicFilters: params.dynamicFilters,
+ favoriteFilters: params.favoriteFilters,
+ env: makeTestEnvironment(),
+ fields: params.fields,
+ },
+ };
+ const model = new ActionModel(extensions);
+ return model;
}
function sanitizeFilters(model) {
- return Object.values(model.state.filters).map(filter => {
+ const cpme = model.extensions[0].find(
+ (ext) => ext.constructor.name === "ControlPanelModelExtension"
+ );
+ const filters = Object.values(cpme.state.filters);
+ return filters.map(filter => {
const copy = Object.assign({}, filter);
delete copy.groupId;
delete copy.groupNumber;
@@ -20,7 +34,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
});
}
- QUnit.module('ControlPanelModel', {
+ QUnit.module('ControlPanelModelExtension', {
beforeEach() {
this.fields = {
display_name: { string: "Displayed name", type: 'char' },
@@ -33,21 +47,20 @@ odoo.define('web.control_panel_model_tests', function (require) {
}, function () {
QUnit.module('Arch parsing');
- QUnit.test('empty arch', function (assert) {
+ QUnit.test('empty arch', async function (assert) {
assert.expect(1);
-
- const model = createControlPanelModel();
+ const model = createModel();
assert.deepEqual(sanitizeFilters(model), []);
});
- QUnit.test('one field tag', function (assert) {
+ QUnit.test('one field tag', async function (assert) {
assert.expect(1);
const arch = `
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
description: "Bar",
@@ -58,18 +71,18 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('one separator tag', function (assert) {
+ QUnit.test('one separator tag', async function (assert) {
assert.expect(1);
const arch = `
-
-
- `;
+
+
+ `;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), []);
});
- QUnit.test('one separator tag and one field tag', function (assert) {
+ QUnit.test('one separator tag and one field tag', async function (assert) {
assert.expect(1);
const arch = `
@@ -77,7 +90,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
description: "Bar",
@@ -88,14 +101,14 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('one filter tag', function (assert) {
+ QUnit.test('one filter tag', async function (assert) {
assert.expect(1);
const arch = `
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
description: "Hello",
@@ -105,15 +118,15 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('one filter tag with date attribute', function (assert) {
+ QUnit.test('one filter tag with date attribute', async function (assert) {
assert.expect(1);
const arch = `
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
- const dateFilterId = Object.values(model.state.filters)[0].id;
+ const model = createModel({ arch, fields, });
+ const dateFilterId = Object.values(model.get('filters'))[0].id;
assert.deepEqual(sanitizeFilters(model), [
{
defaultOptionId: "this_month",
@@ -139,14 +152,14 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('one groupBy tag', function (assert) {
+ QUnit.test('one groupBy tag', async function (assert) {
assert.expect(1);
const arch = `
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
defaultOptionId: "day",
@@ -159,7 +172,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('two filter tags', function (assert) {
+ QUnit.test('two filter tags', async function (assert) {
assert.expect(1);
const arch = `
@@ -167,7 +180,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
description: "Hello One",
@@ -182,7 +195,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('two filter tags separated by a separator', function (assert) {
+ QUnit.test('two filter tags separated by a separator', async function (assert) {
assert.expect(1);
const arch = `
@@ -191,7 +204,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
description: "Hello One",
@@ -206,7 +219,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('one filter tag and one field', function (assert) {
+ QUnit.test('one filter tag and one field', async function (assert) {
assert.expect(1);
const arch = `
@@ -214,7 +227,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
description: "Hello",
@@ -230,7 +243,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
]);
});
- QUnit.test('two field tags', function (assert) {
+ QUnit.test('two field tags', async function (assert) {
assert.expect(1);
const arch = `
@@ -238,7 +251,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields } });
+ const model = createModel({ arch, fields, });
assert.deepEqual(sanitizeFilters(model), [
{
description: "Foo",
@@ -270,7 +283,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
domain: "[('user_id', '=', uid)]",
}];
- const model = createControlPanelModel({ viewInfo: { favoriteFilters } });
+ const model = createModel({ favoriteFilters });
assert.deepEqual(sanitizeFilters(model), [
{
context: {},
@@ -303,7 +316,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
domain: [['id', 'in', [1, 3, 4]]]
}];
- const model = createControlPanelModel({ dynamicFilters });
+ const model = createModel({ dynamicFilters });
assert.deepEqual(sanitizeFilters(model), [
{
description: 'Quick search',
@@ -318,7 +331,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
QUnit.test('falsy search defaults are not activated', async function (assert) {
assert.expect(1);
- const actionContext = {
+ const context = {
search_default_filter: false,
search_default_bar: 0,
search_default_groupby: 2,
@@ -330,7 +343,7 @@ odoo.define('web.control_panel_model_tests', function (require) {
`;
const fields = this.fields;
- const model = createControlPanelModel({ viewInfo: { arch, fields }, actionContext });
+ const model = createModel({ arch, fields, context });
// only the truthy filter 'groupby' has isDefault true
assert.deepEqual(sanitizeFilters(model), [
{
@@ -356,72 +369,5 @@ odoo.define('web.control_panel_model_tests', function (require) {
});
- QUnit.test('search default field many2one with filter domain', async function (assert) {
- assert.expect(3);
-
- const actionContext = {
- search_default_bar: 20,
- };
- const arch = `
-
-
- `;
- const fields = this.fields;
- const model = createControlPanelModel(
- { viewInfo: { arch, fields }, actionContext }, {},
- async function mockRPC(route, params) {
- assert.deepEqual(params, {
- args: [20],
- kwargs: {},
- method: "name_get",
- model: "partner",
- });
- return [[20, "Gandalf"]];
- }
- );
-
- // Labels not fetched: self = undefined
- assert.throws(() => model.getQuery());
-
- await model.isReady;
-
- // Labels fetched: domain should be ready
- assert.deepEqual(model.getQuery().domain, [["bar", "ilike", "Gandalf"]]);
- });
-
- QUnit.test('search default field many2one with filter domain and default favorite', async function (assert) {
- assert.expect(2);
-
- const actionContext = {
- search_default_bar: 20,
- };
- const arch = `
-
-
- `;
- const favoriteFilters = [{
- domain: "[('user_id', '=', uid)]",
- id: 5,
- is_default: true,
- name: 'Sorted filter',
- user_id: [2, "Mitchell Admin"],
- }];
- const fields = this.fields;
- const model = createControlPanelModel(
- { viewInfo: { arch, favoriteFilters, fields }, actionContext },
- { session: { user_context: { uid: 2 } } },
- async function mockRPC() {
- throw new Error("There should be no name_get");
- }
- );
-
- // Labels not fetched, but favorite overrides search_default
- assert.deepEqual(model.getQuery().domain, [["user_id", "=", 2]]);
-
- await model.isReady;
-
- // domain should be the same
- assert.deepEqual(model.getQuery().domain, [["user_id", "=", 2]]);
- });
});
});
diff --git a/addons/web/static/tests/control_panel/control_panel_tests.js b/addons/web/static/tests/control_panel/control_panel_tests.js
index c2dce2eb16f..24744291cc9 100644
--- a/addons/web/static/tests/control_panel/control_panel_tests.js
+++ b/addons/web/static/tests/control_panel/control_panel_tests.js
@@ -24,12 +24,11 @@ odoo.define('web.control_panel_tests', function (require) {
assert.expect(2);
const params = {
- cpStoreConfig: {
- viewInfo: {
- arch: `
`,
- fields: this.fields,
- },
- actionContext: { search_default_foo: "a" },
+ cpModelConfig: {
+ arch: `
`,
+ fields: this.fields,
+ context: { search_default_foo: "a" },
+ searchMenuTypes: ['filter'],
},
cpProps: { fields: this.fields },
};
@@ -67,9 +66,10 @@ odoo.define('web.control_panel_tests', function (require) {
`;
const searchMenuTypes = ['filter', 'groupBy'];
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
- actionContext: {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ context: {
show_filterC: true,
search_default_display_name: 'value',
search_default_filterB: true,
@@ -179,9 +179,10 @@ odoo.define('web.control_panel_tests', function (require) {
const arch = `
`;
const searchMenuTypes = ['filter'];
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
- searchMenuTypes
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
},
cpProps: { fields: this.fields, searchMenuTypes },
};
diff --git a/addons/web/static/tests/control_panel/custom_filter_item_tests.js b/addons/web/static/tests/control_panel/custom_filter_item_tests.js
index 7602f56aae0..2fa4c5e4e57 100644
--- a/addons/web/static/tests/control_panel/custom_filter_item_tests.js
+++ b/addons/web/static/tests/control_panel/custom_filter_item_tests.js
@@ -3,7 +3,7 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
const Domain = require('web.Domain');
const CustomFilterItem = require('web.CustomFilterItem');
- const { Model } = require('web.model');
+ const ActionModel = require('web/static/src/js/views/action_model.js');
const pyUtils = require('web.py_utils');
const testUtils = require('web.test_utils');
@@ -33,7 +33,7 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
fields: this.fields,
},
env: {
- controlPanelModel: new Model(),
+ searchModel: new ActionModel(),
},
});
@@ -71,20 +71,22 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
});
QUnit.test('selection field: default and updated value', async function (assert) {
- assert.expect(2);
+ assert.expect(4);
let expectedFilters;
- class MockedControlPanelModel extends Model {
- createNewFilters(preFilters) {
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
assert.deepEqual(preFilters, expectedFilters);
}
}
- const controlPanelModel = new MockedControlPanelModel();
+ const searchModel = new MockedSearchModel();
const cfi = await createComponent(CustomFilterItem, {
props: {
fields: this.fields,
},
- env: { controlPanelModel },
+ env: { searchModel },
});
// Default value
@@ -112,23 +114,26 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
});
QUnit.test('adding a simple filter works', async function (assert) {
- assert.expect(5);
+ assert.expect(6);
delete this.fields.date_field;
- class MockedControlPanelModel extends Model {
- createNewFilters(preFilters) {
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
const preFilter = preFilters[0];
assert.strictEqual(preFilter.type, 'filter');
assert.strictEqual(preFilter.description, 'Boolean Field is true');
assert.strictEqual(preFilter.domain, '[["boolean_field","=",True]]');
}
}
- const controlPanelModel = new MockedControlPanelModel();
+ const searchModel = new MockedSearchModel();
const cfi = await createComponent(CustomFilterItem, {
props: {
fields: this.fields,
},
- env: { controlPanelModel },
+ env: { searchModel },
});
await cpHelpers.toggleAddCustomFilter(cfi);
@@ -142,7 +147,7 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
});
QUnit.test('commit search with an extended proposition with field char does not cause a crash', async function (assert) {
- assert.expect(6);
+ assert.expect(12);
this.fields.many2one_field = { name: 'many2one_field', string: "Trululu", type: "many2one", searchable: true };
const expectedDomains = [
@@ -154,8 +159,11 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
[['many2one_field', 'ilike', `\\`]],
];
const testedValues = [`a`, `"a"`, `'a'`, `'`, `"`, `\\`];
- class MockedControlPanelModel extends Model {
- createNewFilters(preFilters) {
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
const preFilter = preFilters[0];
// this step combine a tokenization/parsing followed by a string formatting
let domain = pyUtils.assembleDomains([preFilter.domain]);
@@ -163,12 +171,12 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
assert.deepEqual(domain, expectedDomains.shift());
}
}
- const controlPanelModel = new MockedControlPanelModel();
+ const searchModel = new MockedSearchModel();
const cfi = await createComponent(CustomFilterItem, {
props: {
fields: this.fields,
},
- env: { controlPanelModel },
+ env: { searchModel },
});
async function testValue(value) {
@@ -186,14 +194,17 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
await testValue(value);
}
+ delete ActionModel.registry.map.testExtension;
cfi.destroy();
});
QUnit.test('custom filter datetime with equal operator', async function (assert) {
- assert.expect(4);
+ assert.expect(5);
- class MockedControlPanelModel extends Model {
- createNewFilters(preFilters) {
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
const preFilter = preFilters[0];
assert.strictEqual(preFilter.description,
'DateTime is equal to "02/22/2017 11:00:00"',
@@ -201,9 +212,9 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
assert.deepEqual(preFilter.domain,
'[["date_time_field","=","2017-02-22 15:00:00"]]',
"domain should be in UTC format");
+ }
}
- }
- const controlPanelModel = new MockedControlPanelModel();
+ const searchModel = new MockedSearchModel();
const cfi = await createComponent(CustomFilterItem, {
props: {
fields: this.fields,
@@ -213,7 +224,7 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
return -240;
},
},
- env: { controlPanelModel },
+ env: { searchModel },
});
await cpHelpers.toggleAddCustomFilter(cfi);
@@ -230,10 +241,12 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
});
QUnit.test('custom filter datetime between operator', async function (assert) {
- assert.expect(4);
+ assert.expect(5);
- class MockedControlPanelModel extends Model {
- createNewFilters(preFilters) {
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
const preFilter = preFilters[0];
assert.strictEqual(preFilter.description,
'DateTime is between "02/22/2017 11:00:00 and 02/22/2017 17:00:00"',
@@ -244,7 +257,7 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
"domain should be in UTC format");
}
}
- const controlPanelModel = new MockedControlPanelModel();
+ const searchModel = new MockedSearchModel();
const cfi = await createComponent(CustomFilterItem, {
props: {
fields: this.fields,
@@ -254,7 +267,7 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
return -240;
},
},
- env: { controlPanelModel },
+ env: { searchModel },
});
await cpHelpers.toggleAddCustomFilter(cfi);
@@ -279,7 +292,7 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
fields: this.fields,
},
env: {
- controlPanelModel: new Model(),
+ searchModel: new ActionModel(),
},
});
@@ -312,10 +325,12 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
});
QUnit.test('add custom filter with multiple values', async function (assert) {
- assert.expect(1);
+ assert.expect(2);
- class MockedControlPanelModel extends Model {
- createNewFilters(preFilters) {
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
const expected = [
{
description: 'A date is equal to "01/09/1997"',
@@ -342,12 +357,12 @@ odoo.define('web.filter_menu_generator_tests', function (require) {
"Conditions should be in the correct order witht the right values.");
}
}
- const controlPanelModel = new MockedControlPanelModel();
+ const searchModel = new MockedSearchModel();
const cfi = await createComponent(CustomFilterItem, {
props: {
fields: this.fields,
},
- env: { controlPanelModel },
+ env: { searchModel },
});
await cpHelpers.toggleAddCustomFilter(cfi);
diff --git a/addons/web/static/tests/control_panel/custom_group_by_item_tests.js b/addons/web/static/tests/control_panel/custom_group_by_item_tests.js
index 957668a2dae..ee8d0331d67 100644
--- a/addons/web/static/tests/control_panel/custom_group_by_item_tests.js
+++ b/addons/web/static/tests/control_panel/custom_group_by_item_tests.js
@@ -2,7 +2,7 @@ odoo.define('web.groupby_menu_generator_tests', function (require) {
"use strict";
const CustomGroupByItem = require('web.CustomGroupByItem');
- const { Model } = require('web.model');
+ const ActionModel = require('web/static/src/js/views/action_model.js');
const testUtils = require('web.test_utils');
const { createComponent } = testUtils;
@@ -21,7 +21,7 @@ odoo.define('web.groupby_menu_generator_tests', function (require) {
],
},
env: {
- controlPanelModel: new Model(),
+ searchModel: new ActionModel(),
},
});
@@ -43,20 +43,22 @@ odoo.define('web.groupby_menu_generator_tests', function (require) {
});
QUnit.test('select a field name in Add Custom Group menu properly trigger the corresponding field', async function (assert) {
- assert.expect(3);
+ assert.expect(4);
const fields = [
{ sortable: true, name: 'candlelight', string: 'Candlelight', type: 'boolean' },
];
- class MockedControlPanelModel extends Model {
- createNewGroupBy(field) {
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewGroupBy');
+ const field = args[0];
assert.deepEqual(field, fields[0]);
}
}
- const controlPanelModel = new MockedControlPanelModel();
+ const searchModel = new MockedSearchModel();
const cgi = await createComponent(CustomGroupByItem, {
props: { fields },
- env: { controlPanelModel },
+ env: { searchModel },
});
await testUtils.dom.click(cgi.el.querySelector('.o_generator_menu button.o_add_custom_group_by'));
diff --git a/addons/web/static/tests/control_panel/favorite_menu_tests.js b/addons/web/static/tests/control_panel/favorite_menu_tests.js
index 9c404d0eb17..d01afdfb063 100644
--- a/addons/web/static/tests/control_panel/favorite_menu_tests.js
+++ b/addons/web/static/tests/control_panel/favorite_menu_tests.js
@@ -28,7 +28,7 @@ odoo.define('web.favorite_menu_tests', function (require) {
assert.expect(8);
const params = {
- cpStoreConfig: { searchMenuTypes },
+ cpModelConfig: { searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes, action: { name: "Action Name" } },
};
const controlPanel = await createControlPanel(params);
@@ -61,7 +61,7 @@ odoo.define('web.favorite_menu_tests', function (require) {
assert.expect(11);
const params = {
- cpStoreConfig: {
+ cpModelConfig: {
viewInfo: { fields: this.fields },
searchMenuTypes
},
@@ -109,8 +109,8 @@ odoo.define('web.favorite_menu_tests', function (require) {
assert.expect(1);
const params = {
- cpStoreConfig: {
- viewInfo: { fields: this.fields },
+ cpModelConfig: {
+ fields: this.fields,
searchMenuTypes
},
cpProps: {
@@ -156,10 +156,11 @@ odoo.define('web.favorite_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { fields: {}, arch },
+ cpModelConfig: {
+ fields: {},
+ arch ,
searchMenuTypes,
- actionContext: {
+ context: {
search_default_positive: true,
}
},
@@ -202,8 +203,9 @@ odoo.define('web.favorite_menu_tests', function (require) {
const arch = `
`;
const params = {
- cpStoreConfig: {
- viewInfo: { fields: this.fields, arch },
+ cpModelConfig: {
+ fields: this.fields,
+ arch ,
searchMenuTypes,
},
cpProps: {
@@ -258,7 +260,7 @@ odoo.define('web.favorite_menu_tests', function (require) {
user_id: [2, "Mitchell Admin"],
}];
const params = {
- cpStoreConfig: { viewInfo: { favoriteFilters }, searchMenuTypes },
+ cpModelConfig: { favoriteFilters, searchMenuTypes },
cpProps: { searchMenuTypes, action: {} },
search: function (searchQuery) {
const { domain } = searchQuery;
@@ -305,10 +307,10 @@ odoo.define('web.favorite_menu_tests', function (require) {
user_id: [2, "Mitchell Admin"],
}];
const params = {
- cpStoreConfig: {
- viewInfo: { favoriteFilters },
+ cpModelConfig: {
+ favoriteFilters,
searchMenuTypes,
- actionContext: { search_disable_custom_filters: true }
+ context: { search_disable_custom_filters: true }
},
cpProps: { searchMenuTypes, action: {} },
};
@@ -354,10 +356,12 @@ odoo.define('web.favorite_menu_tests', function (require) {
`;
const searchMenuTypes = ['filter', 'groupBy', 'comparison', 'favorite'];
const params = {
- cpStoreConfig: {
- viewInfo: { favoriteFilters, arch, fields: this.fields },
+ cpModelConfig: {
+ favoriteFilters,
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: {
+ context: {
search_default_positive: true,
search_default_coolName: true,
search_default_foo: "a",
@@ -442,7 +446,7 @@ odoo.define('web.favorite_menu_tests', function (require) {
user_id: [2, "Mitchell Admin"],
}];
const params = {
- cpStoreConfig: { viewInfo: { favoriteFilters }, searchMenuTypes },
+ cpModelConfig: { favoriteFilters, searchMenuTypes },
cpProps: { searchMenuTypes, action: {} },
'get-controller-query-params': function (callback) {
callback();
diff --git a/addons/web/static/tests/control_panel/filter_menu_tests.js b/addons/web/static/tests/control_panel/filter_menu_tests.js
index 15a41a21aea..baba1d39840 100644
--- a/addons/web/static/tests/control_panel/filter_menu_tests.js
+++ b/addons/web/static/tests/control_panel/filter_menu_tests.js
@@ -23,7 +23,7 @@ odoo.define('web.filter_menu_tests', function (require) {
assert.expect(2);
const params = {
- cpStoreConfig: { searchMenuTypes },
+ cpModelConfig: { searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -43,7 +43,7 @@ odoo.define('web.filter_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: { viewInfo: { arch, fields: this.fields }, searchMenuTypes },
+ cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -60,7 +60,7 @@ odoo.define('web.filter_menu_tests', function (require) {
assert.expect(2);
const params = {
- cpStoreConfig: { viewInfo: { fields: this.fields }, searchMenuTypes },
+ cpModelConfig: { fields: this.fields, searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -86,7 +86,7 @@ odoo.define('web.filter_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: { viewInfo: { arch }, searchMenuTypes },
+ cpModelConfig: { arch, searchMenuTypes },
cpProps: { fields: {}, searchMenuTypes },
search: function (searchQuery) {
const { domain } = searchQuery;
@@ -118,7 +118,7 @@ odoo.define('web.filter_menu_tests', function (require) {
assert.expect(1);
const params = {
- cpStoreConfig: { viewInfo: { fields: this.fields }, searchMenuTypes },
+ cpModelConfig: { fields: this.fields, searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -144,7 +144,7 @@ odoo.define('web.filter_menu_tests', function (require) {
const unpatchDate = patchDate(2020, 1, 5, 12, 20, 0);
const params = {
- cpStoreConfig: { viewInfo: { fields: this.fields }, searchMenuTypes },
+ cpModelConfig: { fields: this.fields, searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -207,10 +207,11 @@ odoo.define('web.filter_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: { search_default_date_field: 1 },
+ context: { search_default_date_field: 1 },
},
cpProps: { fields: this.fields, searchMenuTypes },
search: function (searchQuery) {
@@ -291,10 +292,11 @@ odoo.define('web.filter_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: { search_default_some_filter: 1 },
+ context: { search_default_some_filter: 1 },
},
cpProps: { fields: this.fields, searchMenuTypes },
};
@@ -328,8 +330,9 @@ odoo.define('web.filter_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes
},
cpProps: { fields: this.fields, searchMenuTypes },
@@ -358,8 +361,9 @@ odoo.define('web.filter_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
},
cpProps: { fields: this.fields, searchMenuTypes },
@@ -388,10 +392,11 @@ odoo.define('web.filter_menu_tests', function (require) {
`,
params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: {
+ context: {
search_default_date_field: true
}
},
@@ -416,10 +421,11 @@ odoo.define('web.filter_menu_tests', function (require) {
`,
params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: {
+ context: {
search_default_f_1_g1: true,
search_default_f1_g2: true,
search_default_f2_g2: true,
@@ -474,8 +480,9 @@ odoo.define('web.filter_menu_tests', function (require) {
`,
params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
},
cpProps: { fields: this.fields, searchMenuTypes },
@@ -493,4 +500,4 @@ odoo.define('web.filter_menu_tests', function (require) {
controlPanel.destroy();
});
});
-});
\ No newline at end of file
+});
diff --git a/addons/web/static/tests/control_panel/groupby_menu_tests.js b/addons/web/static/tests/control_panel/groupby_menu_tests.js
index 9d68db30158..1239f0885fb 100644
--- a/addons/web/static/tests/control_panel/groupby_menu_tests.js
+++ b/addons/web/static/tests/control_panel/groupby_menu_tests.js
@@ -26,7 +26,7 @@ odoo.define('web.groupby_menu_tests', function (require) {
assert.expect(1);
const params = {
- cpStoreConfig: { searchMenuTypes },
+ cpModelConfig: { searchMenuTypes },
cpProps: { fields: {}, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -40,7 +40,7 @@ odoo.define('web.groupby_menu_tests', function (require) {
assert.expect(5);
const params = {
- cpStoreConfig: { searchMenuTypes },
+ cpModelConfig: { searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -67,7 +67,7 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: { viewInfo: { arch, fields: this.fields }, searchMenuTypes },
+ cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -90,7 +90,7 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: { viewInfo: { arch, fields: this.fields }, searchMenuTypes },
+ cpModelConfig: {arch, fields: this.fields, searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
search: function (searchQuery) {
const { groupBy } = searchQuery;
@@ -125,7 +125,7 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: { viewInfo: { arch, fields: this.fields }, searchMenuTypes },
+ cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
cpProps: { fields: this.fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -147,10 +147,11 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: { search_default_gb_foo: 1 }
+ context: { search_default_gb_foo: 1 }
},
cpProps: { fields: this.fields, searchMenuTypes },
search: function (searchQuery) {
@@ -189,10 +190,11 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: { search_default_date: 1 }
+ context: { search_default_date: 1 }
},
cpProps: { fields: this.fields, searchMenuTypes },
search: function (searchQuery) {
@@ -251,10 +253,11 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: { search_default_bar: 1 }
+ context: { search_default_bar: 1 }
},
cpProps: { fields: this.fields, searchMenuTypes },
};
@@ -309,7 +312,7 @@ odoo.define('web.groupby_menu_tests', function (require) {
id: { sortable: true, string: 'ID', type: 'integer' }
};
const params = {
- cpStoreConfig: { searchMenuTypes },
+ cpModelConfig: { searchMenuTypes },
cpProps: { fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
@@ -332,7 +335,7 @@ odoo.define('web.groupby_menu_tests', function (require) {
id: { sortable: true, string: 'ID', type: 'integer' }
};
const params = {
- cpStoreConfig: { viewInfo: { fields }, searchMenuTypes },
+ cpModelConfig: { fields, searchMenuTypes },
cpProps: { fields, searchMenuTypes },
search: function (searchQuery) {
const { groupBy } = searchQuery;
@@ -363,10 +366,11 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: { search_default_birthday: 2, search_default_date: 1 }
+ context: { search_default_birthday: 2, search_default_date: 1 }
},
cpProps: { fields: this.fields, searchMenuTypes },
};
@@ -391,8 +395,9 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
},
cpProps: { fields: this.fields, searchMenuTypes },
@@ -453,10 +458,11 @@ odoo.define('web.groupby_menu_tests', function (require) {
`;
const params = {
- cpStoreConfig: {
- viewInfo: { arch, fields: this.fields },
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
searchMenuTypes,
- actionContext: { search_default_birthday: false, search_default_foo: 0 }
+ context: { search_default_birthday: false, search_default_foo: 0 }
},
cpProps: { fields: this.fields, searchMenuTypes },
};
diff --git a/addons/web/static/tests/control_panel/search_bar_tests.js b/addons/web/static/tests/control_panel/search_bar_tests.js
index 412688aa52c..b7f5fc8956d 100644
--- a/addons/web/static/tests/control_panel/search_bar_tests.js
+++ b/addons/web/static/tests/control_panel/search_bar_tests.js
@@ -1,7 +1,8 @@
odoo.define('web.search_bar_tests', function (require) {
"use strict";
- const { Model } = require('web.model');
+ const { Model } = require('web/static/src/js/model.js');
+ const Registry = require("web.Registry");
const SearchBar = require('web.SearchBar');
const testUtils = require('web.test_utils');
@@ -173,22 +174,30 @@ odoo.define('web.search_bar_tests', function (require) {
assert.expect(9);
const fields = this.data.partner.fields;
- class MockedControlPanelModel extends Model {
- getFacets() {
- return [];
- }
- getFiltersOfType() {
- return Object.keys(fields).map((fname, index) => Object.assign({
- description: fields[fname].string,
- fieldName: fname,
- fieldType: fields[fname].type,
- id: index,
- }, fields[fname]));
+
+ class TestModelExtension extends Model.Extension {
+ get(property) {
+ switch (property) {
+ case 'facets':
+ return [];
+ case 'filters':
+ return Object.keys(fields).map((fname, index) => Object.assign({
+ description: fields[fname].string,
+ fieldName: fname,
+ fieldType: fields[fname].type,
+ id: index,
+ }, fields[fname]));
+ default:
+ break;
+ }
}
}
+ class MockedModel extends Model { }
+ MockedModel.registry = new Registry({ Test: TestModelExtension, });
+ const searchModel = new MockedModel({ Test: {} });
const searchBar = await createComponent(SearchBar, {
data: this.data,
- env: { controlPanelModel: new MockedControlPanelModel() },
+ env: { searchModel },
props: { fields },
});
const input = searchBar.el.querySelector('.o_searchview_input');
@@ -477,7 +486,7 @@ odoo.define('web.search_bar_tests', function (require) {
const searchInput = actionManager.el.querySelector('.o_searchview_input');
searchInput.value = "ABC";
await testUtils.dom.triggerEvent(searchInput, 'input',
- { inputType: 'insertFromPaste' });
+ { inputType: 'insertFromPaste' });
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_searchview_autocomplete',
"should display autocomplete dropdown menu on paste in search view");
diff --git a/addons/web/static/tests/core/registry_tests.js b/addons/web/static/tests/core/registry_tests.js
index fdc437c23ee..eb3389a4383 100644
--- a/addons/web/static/tests/core/registry_tests.js
+++ b/addons/web/static/tests/core/registry_tests.js
@@ -70,7 +70,21 @@ QUnit.module('core', {}, function () {
assert.deepEqual(registry.values(), ['bar', 'foo', 'qux']);
});
+ QUnit.test("predicate prevents invalid values", function (assert) {
+ assert.expect(5);
+
+ const predicate = value => typeof value === "number";
+ const registry = new Registry(null, predicate);
+ registry.onAdd((key) => assert.step(key));
+
+ assert.ok(registry.add("age", 23));
+ assert.throws(
+ () => registry.add("name", "Fred"),
+ new Error(`Value of key "name" does not pass the addition predicate.`)
+ );
+ assert.deepEqual(registry.entries(), { age: 23 });
+ assert.verifySteps(["age"]);
+ });
});
});
-
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index 1a1f38f7f09..1aa2e1f0fcf 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -177,6 +177,7 @@ odoo.define('web.test_utils', async function (require) {
triggerMouseEvent: testUtilsDom.triggerMouseEvent,
triggerPositionalMouseEvent: testUtilsDom.triggerPositionalMouseEvent,
dragAndDrop: testUtilsDom.dragAndDrop,
+ find: testUtilsDom.findItem,
getNode: testUtilsDom.getNode,
openDatepicker: testUtilsDom.openDatepicker,
click: testUtilsDom.click,
diff --git a/addons/web/static/tests/helpers/test_utils_control_panel.js b/addons/web/static/tests/helpers/test_utils_control_panel.js
index 1f5bff32fd6..2dda198017e 100644
--- a/addons/web/static/tests/helpers/test_utils_control_panel.js
+++ b/addons/web/static/tests/helpers/test_utils_control_panel.js
@@ -1,47 +1,9 @@
odoo.define('web.test_utils_control_panel', function (require) {
"use strict";
- const { click, triggerEvent, getNode } = require('web.test_utils_dom');
+ const { click, findItem, getNode, triggerEvent } = require('web.test_utils_dom');
const { editInput, editSelect, editAndTrigger } = require('web.test_utils_fields');
- //-------------------------------------------------------------------------
- // Private functions
- //-------------------------------------------------------------------------
-
- /**
- * Helper method to retrieve a distinct item from a collection of elements defined
- * by the given 'selector' string. It can either be the index of the item or its
- * inner text.
- * @private
- * @param {HTMLElement} el
- * @param {string} selector
- * @param {(number|string)} elFinder
- */
- function findItem(el, selector, elFinder) {
- const elements = [...el.querySelectorAll(selector)];
- if (!elements.length) {
- throw new Error(`No element found with selector "${selector}".`);
- }
- switch (typeof elFinder) {
- case 'number':
- const element = elements[elFinder];
- if (!element) {
- throw new Error(`No element with selector "${selector}" at index ${elFinder}.`);
- }
- return element;
- case 'string':
- const stringMatch = elements.find(
- e => e.innerText.trim().toLowerCase() === elFinder.toLowerCase()
- );
- if (!stringMatch) {
- throw new Error(`No element with selector "${selector}" containing "${elFinder}".`);
- }
- return stringMatch;
- default:
- throw new Error(`Invalid provided element finder: must be a number|string|function.`);
- }
- }
-
//-------------------------------------------------------------------------
// Exported functions
//-------------------------------------------------------------------------
@@ -52,7 +14,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {Promise}
*/
async function toggleMenu(el, menuFinder) {
- const menu = findItem(getNode(el), `.o_dropdown > button`, menuFinder);
+ const menu = findItem(el, `.o_dropdown > button`, menuFinder);
await click(menu);
}
@@ -62,7 +24,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {Promise}
*/
async function toggleMenuItem(el, itemFinder) {
- const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ const item = findItem(el, `.o_menu_item > a`, itemFinder);
await click(item);
}
@@ -73,7 +35,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {Promise}
*/
async function toggleMenuItemOption(el, itemFinder, optionFinder) {
- const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ const item = findItem(el, `.o_menu_item > a`, itemFinder);
const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
await click(option);
}
@@ -84,7 +46,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {boolean}
*/
function isItemSelected(el, itemFinder) {
- const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ const item = findItem(el, `.o_menu_item > a`, itemFinder);
return item.classList.contains('selected');
}
@@ -95,7 +57,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {boolean}
*/
function isOptionSelected(el, itemFinder, optionFinder) {
- const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ const item = findItem(el, `.o_menu_item > a`, itemFinder);
const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
return option.classList.contains('selected');
}
@@ -217,7 +179,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {Promise}
*/
async function deleteFavorite(el, favoriteFinder) {
- const favorite = findItem(getNode(el), `.o_favorite_menu .o_menu_item`, favoriteFinder);
+ const favorite = findItem(el, `.o_favorite_menu .o_menu_item`, favoriteFinder);
await click(favorite.querySelector('i.fa-trash-o'));
}
@@ -245,7 +207,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {Promise}
*/
async function removeFacet(el, facetFinder = 0) {
- const facet = findItem(getNode(el), `.o_searchview .o_searchview_facet`, facetFinder);
+ const facet = findItem(el, `.o_searchview .o_searchview_facet`, facetFinder);
await click(facet.querySelector('.o_facet_remove'));
}
@@ -275,7 +237,7 @@ odoo.define('web.test_utils_control_panel', function (require) {
* @returns {Promise}
*/
async function toggleActionMenu(el, menuFinder = "Action") {
- const dropdown = findItem(getNode(el), `.o_cp_action_menus button`, menuFinder);
+ const dropdown = findItem(el, `.o_cp_action_menus button`, menuFinder);
await click(dropdown);
}
diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js
index 17debd413d0..86fc01b5a4c 100644
--- a/addons/web/static/tests/helpers/test_utils_create.js
+++ b/addons/web/static/tests/helpers/test_utils_create.js
@@ -15,11 +15,11 @@ odoo.define('web.test_utils_create', function (require) {
const concurrency = require('web.concurrency');
const config = require('web.config');
const ControlPanel = require('web.ControlPanel');
- const ControlPanelModel = require('web.ControlPanelModel');
const customHooks = require('web.custom_hooks');
const DebugManager = require('web.DebugManager.Backend');
const dom = require('web.dom');
const makeTestEnvironment = require('web.test_env');
+ const ActionModel = require('web/static/src/js/views/action_model.js');
const Registry = require('web.Registry');
const testUtilsMock = require('web.test_utils_mock');
const Widget = require('web.Widget');
@@ -173,7 +173,7 @@ odoo.define('web.test_utils_create', function (require) {
* available event handlers respectively.
* @param {Object} [params={}]
* @param {Object} [params.cpProps]
- * @param {Object} [params.cpStoreConfig]
+ * @param {Object} [params.cpModelConfig]
* @param {boolean} [params.debug]
* @param {Object} [params.env]
* @returns {Object} useful control panel testing elements:
@@ -183,61 +183,70 @@ odoo.define('web.test_utils_create', function (require) {
* available helpers)
*/
async function createControlPanel(params = {}) {
- const config = params.cpStoreConfig || {};
const debug = params.debug || false;
- const env = params.env || {};
+ const env = makeTestEnvironment(params.env || {});
const props = Object.assign({
action: {},
fields: {},
}, params.cpProps);
+ const globalConfig = Object.assign({
+ context: {},
+ domain: [],
+ }, params.cpModelConfig);
- if (config.viewInfo && config.viewInfo.arch && config.viewInfo.fields) {
- const { arch, fields } = config.viewInfo;
+ if (globalConfig.arch && globalConfig.fields) {
const model = "__mockmodel__";
const serverParams = {
model,
- data: { [model]: { fields, records: [] } },
+ data: { [model]: { fields: globalConfig.fields, records: [] } },
};
const mockServer = await testUtilsMock.addMockEnvironment(
new Widget(),
serverParams,
);
- Object.assign(config.viewInfo, testUtilsMock.fieldsViewGet(mockServer, {
- arch,
- fields,
+ const { arch, fields } = testUtilsMock.fieldsViewGet(mockServer, {
+ arch: globalConfig.arch,
+ fields: globalConfig.fields,
model,
- viewOptions: { context: config.actionContext },
- }));
+ viewOptions: { context: globalConfig.context },
+ });
+ Object.assign(globalConfig, { arch, fields });
}
+ globalConfig.env = env;
+ const archs = (globalConfig.arch && { search: globalConfig.arch, }) || {};
+ const { ControlPanel: controlPanelInfo, } = ActionModel.extractArchInfo(archs);
+ const extensions = {
+ ControlPanel: { archNodes: controlPanelInfo.children, },
+ };
+
class Parent extends Component {
constructor() {
super();
- config.env = this.env;
- this._controlPanelModel = new ControlPanelModel(config);
+ this.searchModel = new ActionModel(extensions, globalConfig);
this.state = useState(props);
this.controlPanel = useRef("controlPanel");
}
async willStart() {
- await this._controlPanelModel.isReady;
+ await this.searchModel.load();
}
mounted() {
if (params['get-controller-query-params']) {
- this._controlPanelModel.on('get-controller-query-params', this,
+ this.searchModel.on('get-controller-query-params', this,
params['get-controller-query-params']);
}
if (params.search) {
- this._controlPanelModel.on('search', this, params.search);
+ this.searchModel.on('search', this, params.search);
}
}
}
Parent.components = { ControlPanel };
- Parent.env = makeTestEnvironment(env);
+ Parent.env = env;
Parent.template = xml`
`;
const parent = new Parent();
@@ -249,7 +258,7 @@ odoo.define('web.test_utils_create', function (require) {
controlPanel.destroy = destroy;
parent.destroy();
};
- controlPanel.getQuery = () => parent._controlPanelModel.getQuery();
+ controlPanel.getQuery = () => parent.searchModel.get('query');
return controlPanel;
}
diff --git a/addons/web/static/tests/helpers/test_utils_dom.js b/addons/web/static/tests/helpers/test_utils_dom.js
index 8a7d3553d9e..eedbcbe5155 100644
--- a/addons/web/static/tests/helpers/test_utils_dom.js
+++ b/addons/web/static/tests/helpers/test_utils_dom.js
@@ -290,6 +290,47 @@ odoo.define('web.test_utils_dom', function (require) {
return returnAfterNextAnimationFrame();
}
+ /**
+ * Helper method to retrieve a distinct item from a collection of elements defined
+ * by the given "selector" string. It can either be the index of the item or its
+ * inner text.
+ * @param {Element} el
+ * @param {string} selector
+ * @param {number | string} [elFinder=0]
+ * @returns {Element | null}
+ */
+ function findItem(el, selector, elFinder = 0) {
+ const elements = [...getNode(el).querySelectorAll(selector)];
+ if (!elements.length) {
+ throw new Error(`No element found with selector "${selector}".`);
+ }
+ switch (typeof elFinder) {
+ case "number": {
+ const match = elements[elFinder];
+ if (!match) {
+ throw new Error(
+ `No element with selector "${selector}" at index ${elFinder}.`
+ );
+ }
+ return match;
+ }
+ case "string": {
+ const match = elements.find(
+ (el) => el.innerText.trim().toLowerCase() === elFinder.toLowerCase()
+ );
+ if (!match) {
+ throw new Error(
+ `No element with selector "${selector}" containing "${elFinder}".
+ `);
+ }
+ return match;
+ }
+ default: throw new Error(
+ `Invalid provided element finder: must be a number|string|function.`
+ );
+ }
+ }
+
/**
* Helper function used to extract an HTML EventTarget element from a given
* target. The extracted element will depend on the target type:
@@ -497,6 +538,7 @@ odoo.define('web.test_utils_dom', function (require) {
clickFirst,
clickLast,
dragAndDrop,
+ findItem,
getNode,
openDatepicker,
returnAfterNextAnimationFrame,
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
index 5a47465d642..bae159e4975 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -2369,7 +2369,7 @@ QUnit.module('Views', {
if (route === '/web/dataset/search_read') {
assert.step('search_read');
const domain = args.domain;
- assert.deepEqual(domain, [['customer', '=', 1], ['foo', '=', 12]],
+ assert.deepEqual(domain, ['&', ['customer', '=', 1], ['foo', '=', 12]],
'list domain is correct');
}
return this._super.apply(this, arguments);
@@ -2444,8 +2444,8 @@ QUnit.module('Views', {
"2", "0", "-100%", "2", "0", "-100%",
"0", "12", "100%", "0", "12", "100%",
"0", "1", "100%", "0", "1", "100%",
-
-
+
+
];
assert.strictEqual(getCurrentValues(pivot), values.join());
diff --git a/addons/web/static/tests/views/search_panel_tests.js b/addons/web/static/tests/views/search_panel_tests.js
index e91a6a49f74..f9004d562c4 100644
--- a/addons/web/static/tests/views/search_panel_tests.js
+++ b/addons/web/static/tests/views/search_panel_tests.js
@@ -1,15 +1,16 @@
-odoo.define('web.search_panel_tests', function (require) {
+odoo.define("web/static/tests/views/search_panel_tests.js", function (require) {
"use strict";
-var AbstractStorageService = require('web.AbstractStorageService');
-var FormView = require('web.FormView');
-var KanbanView = require('web.KanbanView');
-var RamStorage = require('web.RamStorage');
-var testUtils = require('web.test_utils');
+const AbstractStorageService = require('web.AbstractStorageService');
+const FormView = require('web.FormView');
+const KanbanView = require('web.KanbanView');
+const RamStorage = require('web.RamStorage');
+const testUtils = require('web.test_utils');
+const SearchPanel = require("web/static/src/js/views/search_panel.js");
const cpHelpers = testUtils.controlPanel;
-var createActionManager = testUtils.createActionManager;
-var createView = testUtils.createView;
+const createActionManager = testUtils.createActionManager;
+const createView = testUtils.createView;
/**
* Return the list of counters displayed in the search panel (if any).
@@ -412,8 +413,8 @@ QUnit.module('Views', {
assert.verifySteps([
'[["bar","=",true]]',
- '[["bar","=",true],["company_id","child_of",3]]',
- '[["bar","=",true],["company_id","child_of",5]]',
+ '["&",["bar","=",true],["company_id","child_of",3]]',
+ '["&",["bar","=",true],["company_id","child_of",5]]',
'[["bar","=",true]]',
]);
@@ -490,11 +491,12 @@ QUnit.module('Views', {
var Storage = RamStorage.extend({
getItem: function (key) {
- assert.step('getItem ' + key);
- return this._super(...arguments) || 3; // default: 'asustek'
+ assert.step(`getItem ${key}`);
+ const item = this._super(...arguments);
+ return item || 3; // default: 'asustek'
},
setItem: function (key, value) {
- assert.step('setItem ' + key + ' to ' + value);
+ assert.step(`setItem ${key} to ${value}`);
this._super(...arguments);
},
});
@@ -522,11 +524,11 @@ QUnit.module('Views', {
`,
archs: {
'partner,false,search': `
-
+
- `,
+ `,
},
mockRPC: function (route, args) {
if (route === '/web/dataset/search_read') {
@@ -713,15 +715,30 @@ QUnit.module('Views', {
assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 3);
// select 'asustek'
- await testUtils.dom.click(kanban.$('.o_search_panel_category_value[data-id=3] header'));
+ await testUtils.dom.click(
+ [
+ ...kanban.el.querySelectorAll('.o_search_panel_category_value header .o_search_panel_label_title')
+ ]
+ .filter(el => el.innerText === 'asustek')
+ );
assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 2);
// select 'abc'
- await testUtils.dom.click(kanban.$('.o_search_panel_category_value[data-id=abc] header'));
+ await testUtils.dom.click(
+ [
+ ...kanban.el.querySelectorAll('.o_search_panel_category_value header .o_search_panel_label_title')
+ ]
+ .filter(el => el.innerText === 'ABC')
+ );
assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 1);
// select 'ghi'
- await testUtils.dom.click(kanban.$('.o_search_panel_category_value[data-id=ghi] header'));
+ await testUtils.dom.click(
+ [
+ ...kanban.el.querySelectorAll('.o_search_panel_category_value header .o_search_panel_label_title')
+ ]
+ .filter(el => el.innerText === 'GHI')
+ );
assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 1);
// select 'All' in first category (company_id)
@@ -734,10 +751,10 @@ QUnit.module('Views', {
assert.verifySteps([
'[["bar","=",true]]',
- '[["bar","=",true],["company_id","child_of",3]]',
- '[["bar","=",true],["company_id","child_of",3],["state","=","abc"]]',
- '[["bar","=",true],["company_id","child_of",3],["state","=","ghi"]]',
- '[["bar","=",true],["state","=","ghi"]]',
+ '["&",["bar","=",true],["company_id","child_of",3]]',
+ '["&",["bar","=",true],"&",["company_id","child_of",3],["state","=","abc"]]',
+ '["&",["bar","=",true],"&",["company_id","child_of",3],["state","=","ghi"]]',
+ '["&",["bar","=",true],["state","=","ghi"]]',
'[["bar","=",true]]',
]);
@@ -1080,8 +1097,8 @@ QUnit.module('Views', {
assert.verifySteps([
'[["bar","=",true]]',
- '[["bar","=",true],["company_id","child_of",3]]',
- '[["bar","=",true],["company_id","child_of",5]]',
+ '["&",["bar","=",true],["company_id","child_of",3]]',
+ '["&",["bar","=",true],["company_id","child_of",5]]',
]);
kanban.destroy();
@@ -1136,7 +1153,7 @@ QUnit.module('Views', {
// 'All' should still be selected, and there should still be 4 records
assert.containsOnce(kanban, '.o_search_panel_category_value .active');
- assert.containsOnce(kanban, '.o_search_panel_category_value:first .active');
+ assert.containsOnce(kanban, '.o_search_panel_category_value:nth(1) .active');
assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 4);
// select 'ghi' (delay the reload)
@@ -1146,7 +1163,7 @@ QUnit.module('Views', {
// 'All' should still be selected, and there should still be 4 records
assert.containsOnce(kanban, '.o_search_panel_category_value .active');
- assert.containsOnce(kanban, '.o_search_panel_category_value:first .active');
+ assert.containsOnce(kanban, '.o_search_panel_category_value:nth(3) .active');
assert.containsN(kanban, '.o_kanban_record:not(.o_kanban_ghost)', 4);
// unlock ghi search
@@ -1195,6 +1212,7 @@ QUnit.module('Views', {
archs: {
'partner,false,search': `
+
@@ -1206,7 +1224,8 @@ QUnit.module('Views', {
// trigger a reload and delay the get_filter
def = testUtils.makeTestPromise();
- kanban.reload({domain: [['id', '=', 1]]});
+ await cpHelpers.toggleFilterMenu(kanban);
+ await cpHelpers.toggleMenuItem(kanban, 0);
await testUtils.nextTick();
assert.containsN(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 4);
@@ -1311,13 +1330,13 @@ QUnit.module('Views', {
'[["bar","=",true]]',
// 'asustek' checked
'[]',
- '[["bar","=",true],["company_id","in",[3]]]',
+ '["&",["bar","=",true],["company_id","in",[3]]]',
// 'asustek' and 'agrolait' checked
'[]',
- '[["bar","=",true],["company_id","in",[3,5]]]',
+ '["&",["bar","=",true],["company_id","in",[3,5]]]',
// 'agrolait' checked
'[]',
- '[["bar","=",true],["company_id","in",[5]]]',
+ '["&",["bar","=",true],["company_id","in",[5]]]',
// nothing checked
'[]',
'[["bar","=",true]]',
@@ -1418,13 +1437,13 @@ QUnit.module('Views', {
'[["bar","=",true]]',
// 'asustek' checked
'[]',
- '[["bar","=",true],["state","in",["abc"]]]',
+ '["&",["bar","=",true],["state","in",["abc"]]]',
// 'asustek' and 'agrolait' checked
'[]',
- '[["bar","=",true],["state","in",["abc","def"]]]',
+ '["&",["bar","=",true],["state","in",["abc","def"]]]',
// 'agrolait' checked
'[]',
- '[["bar","=",true],["state","in",["def"]]]',
+ '["&",["bar","=",true],["state","in",["def"]]]',
// nothing checked
'[]',
'[["bar","=",true]]',
@@ -1458,6 +1477,7 @@ QUnit.module('Views', {
archs: {
'partner,false,search': `
+
@@ -1483,7 +1503,8 @@ QUnit.module('Views', {
]);
// reload with another domain, so the filters should be reloaded
- await kanban.reload({domain: [['id', '<', 5]]});
+ await cpHelpers.toggleFilterMenu(kanban);
+ await cpHelpers.toggleMenuItem(kanban, 0);
assert.verifySteps([
'search_panel_select_multi_range',
@@ -1526,6 +1547,7 @@ QUnit.module('Views', {
archs: {
'partner,false,search': `
+
@@ -1551,7 +1573,8 @@ QUnit.module('Views', {
]);
// reload with another domain, so the filters should be reloaded
- await kanban.reload({domain: [['id', '<', 5]]});
+ await cpHelpers.toggleFilterMenu(kanban);
+ await cpHelpers.toggleMenuItem(kanban, 0);
assert.verifySteps([
'search_panel_select_multi_range',
@@ -1597,6 +1620,7 @@ QUnit.module('Views', {
archs: {
'partner,false,search': `
+
@@ -1637,8 +1661,9 @@ QUnit.module('Views', {
[ "All", "ABC1", "DEF1", "GHI2", "All", "asustek", "agrolait"]
);
- // reload with another domain, so the category 'state' should be reloaded
- await kanban.reload({domain: [['id', '<', 3]]});
+ // reload with another domain, so the categories 'state' and 'company_id' should be reloaded
+ await cpHelpers.toggleFilterMenu(kanban);
+ await cpHelpers.toggleMenuItem(kanban, 0);
assert.verifySteps([
'search_panel_select_range',
@@ -1700,6 +1725,7 @@ QUnit.module('Views', {
archs: {
'partner,false,search': `
+
@@ -1738,7 +1764,8 @@ QUnit.module('Views', {
);
// reload with another domain, so the category 'state' should be reloaded
- await kanban.reload({domain: [['id', '<', 3]]});
+ await cpHelpers.toggleFilterMenu(kanban);
+ await cpHelpers.toggleMenuItem(kanban, 0);
assert.verifySteps([
'/web/dataset/search_read',
@@ -1884,16 +1911,16 @@ QUnit.module('Views', {
'[["bar","=",true]]',
// 'asustek' checked
'[]',
- '[["bar","=",true],["company_id","in",[3]]]',
+ '["&",["bar","=",true],["company_id","in",[3]]]',
// 'asustek' and 'agrolait' checked
'[]',
- '[["bar","=",true],["company_id","in",[3]],["company_id","in",[5]]]',
+ '["&",["bar","=",true],"&",["company_id","in",[3]],["company_id","in",[5]]]',
// 'asustek', 'agrolait' and 'camptocamp' checked
'[]',
- '[["bar","=",true],["company_id","in",[3]],["company_id","in",[5,11]]]',
+ '["&",["bar","=",true],"&",["company_id","in",[3]],["company_id","in",[5,11]]]',
// 'asustek' checked
'[]',
- '[["bar","=",true],["company_id","in",[3]]]',
+ '["&",["bar","=",true],["company_id","in",[3]]]',
]);
kanban.destroy();
@@ -2038,6 +2065,7 @@ QUnit.module('Views', {
assert.expect(4);
var expectedDomain = [
+ "&",
['company_id', 'in', [5]],
['state', 'in', ['abc', 'ghi']],
];
@@ -2548,6 +2576,9 @@ QUnit.module('Views', {
QUnit.test('scroll position is kept when switching between controllers', async function (assert) {
assert.expect(6);
+ const originalDebounce = SearchPanel.scrollDebounce;
+ SearchPanel.scrollDebounce = 0;
+
for (var i = 10; i < 20; i++) {
this.data.category.records.push({id: i, name: "Cat " + i});
}
@@ -2560,24 +2591,30 @@ QUnit.module('Views', {
});
actionManager.$el.css('max-height', 300);
+ async function scroll(top) {
+ actionManager.el.querySelector(".o_search_panel").scrollTop = top;
+ await testUtils.nextTick();
+ }
+
await actionManager.doAction(1);
assert.containsOnce(actionManager, '.o_content .o_kanban_view');
assert.strictEqual(actionManager.$('.o_search_panel').scrollTop(), 0);
// simulate a scroll in the search panel and switch into list
- actionManager.$('.o_search_panel').scrollTop(50);
+ await scroll(50);
await cpHelpers.switchView(actionManager, 'list');
assert.containsOnce(actionManager, '.o_content .o_list_view');
assert.strictEqual(actionManager.$('.o_search_panel').scrollTop(), 50);
// simulate another scroll and switch back to kanban
- actionManager.$('.o_search_panel').scrollTop(30);
+ await scroll(30);
await cpHelpers.switchView(actionManager, 'kanban');
assert.containsOnce(actionManager, '.o_content .o_kanban_view');
assert.strictEqual(actionManager.$('.o_search_panel').scrollTop(), 30);
actionManager.destroy();
+ SearchPanel.scrollDebounce = originalDebounce;
});
QUnit.test('search panel is not instantiated in dialogs', async function (assert) {
@@ -3949,7 +3986,7 @@ QUnit.module('Views', {
assert.containsOnce(kanban, '.o_search_panel_section');
assert.containsOnce(kanban, '.o_search_panel_section_header');
- assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, " COMPANY");
+ assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, "COMPANY");
assert.containsOnce(kanban, 'section div.alert.alert-warning');
assert.strictEqual(kanban.el.querySelector('section div.alert.alert-warning').innerText, "Too many items to display.");
assert.containsNone(kanban, '.o_search_panel_category_value');
@@ -3987,7 +4024,7 @@ QUnit.module('Views', {
assert.containsOnce(kanban, '.o_search_panel_section');
assert.containsOnce(kanban, '.o_search_panel_section_header');
- assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, " COMPANY");
+ assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, "COMPANY");
assert.containsOnce(kanban, 'section div.alert.alert-warning');
assert.strictEqual(kanban.el.querySelector('section div.alert.alert-warning').innerText, "Too many items to display.");
assert.containsNone(kanban, '.o_search_panel_filter_value');
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 7d1554dc78d..046fb8738c2 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -148,8 +148,11 @@ QUnit.module('Views', {
mockRPC: function (route, args) {
if (args.method === 'web_read_group') {
assert.deepEqual(args.kwargs, {
- context: {},
- domain: [["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
+ context: {
+ search_default_foo: "piou",
+ search_default_groupby_bar: true,
+ },
+ domain: ["&", ["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
fields: ["display_name", "foo", "bar"],
groupby: ["bar"],
orderby: '',
@@ -160,8 +163,12 @@ QUnit.module('Views', {
if (search === 0 && route === '/web/dataset/search_read') {
search++;
assert.deepEqual(args, {
- context: {'bin_size': true}, // not part of the test, may change
- domain: [["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
+ context: {
+ search_default_foo: "piou",
+ search_default_groupby_bar: true,
+ bin_size: true
+ }, // not part of the test, may change
+ domain: ["&", ["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
fields: ["display_name", "foo"],
model: "partner",
limit: 80,
@@ -169,7 +176,11 @@ QUnit.module('Views', {
}, "should search with the complete domain (domain + search)");
} else if (search === 1 && route === '/web/dataset/search_read') {
assert.deepEqual(args, {
- context: {'bin_size': true}, // not part of the test, may change
+ context: {
+ search_default_foo: "piou",
+ search_default_groupby_bar: true,
+ bin_size: true
+ }, // not part of the test, may change
domain: [["display_name", "like", "a"]],
fields: ["display_name", "foo"],
model: "partner",
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index c0dc0097a38..c0812ace9da 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -323,7 +323,7 @@
-
+
@@ -333,7 +333,9 @@
+
+
@@ -749,7 +751,7 @@
-
+