diff --git a/addons/web/static/src/core/pager/pager.js b/addons/web/static/src/core/pager/pager.js index adcd5878aac..25f83d903ba 100644 --- a/addons/web/static/src/core/pager/pager.js +++ b/addons/web/static/src/core/pager/pager.js @@ -51,6 +51,8 @@ export class Pager extends Component { return parts.join("-"); } /** + * Note: returns false if we received the props "updateTotal", as in this case we don't know + * the real total so we can't assert that there's a single page. * @returns {boolean} true if there is only one page */ get isSinglePage() { @@ -62,11 +64,12 @@ export class Pager extends Component { async navigate(direction) { let minimum = this.props.offset + this.props.limit * direction; let total = this.props.total; - if (this.props.updateTotal && minimum + this.props.limit > total) { - total = await this.props.updateTotal(); - } if (minimum >= total) { - minimum = 0; + if (!this.props.updateTotal) { + // only loop forward if we know the real total, otherwise let the minimum + // go out of range + minimum = 0; + } } else if (minimum < 0 && this.props.limit === 1) { minimum = total - 1; } else if (minimum < 0 && this.props.limit > 1) { @@ -82,13 +85,13 @@ export class Pager extends Component { let [minimum, maximum] = value.trim().split(/\s*[-\s,;]\s*/); minimum = parseInt(minimum, 10); maximum = maximum ? parseInt(maximum, 10) : minimum; - let total = this.props.total; - if (this.props.updateTotal && maximum > total) { - total = await this.props.updateTotal(); + if (this.props.updateTotal) { + // we don't know the real total, so we can't clamp + return { minimum: minimum - 1, maximum }; } return { - minimum: clamp(minimum, 1, total) - 1, - maximum: clamp(maximum, 1, total), + minimum: clamp(minimum, 1, this.props.total) - 1, + maximum: clamp(maximum, 1, this.props.total), }; } /** diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index a6e47d45980..a830beaeccd 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -1795,13 +1795,14 @@ class DynamicList extends DataPoint { DynamicList.DEFAULT_LIMIT = 80; export class DynamicRecordList extends DynamicList { - setup(params) { + setup(params, state) { super.setup(...arguments); /** @type {Record[]} */ this.records = []; this.data = params.data; - this.countLimit = params.countLimit || this.constructor.WEB_SEARCH_READ_COUNT_LIMIT; + this.countLimit = + state.countLimit || params.countLimit || this.constructor.WEB_SEARCH_READ_COUNT_LIMIT; this.hasLimitedCount = false; } @@ -1936,6 +1937,7 @@ export class DynamicRecordList extends DynamicList { return { ...super.exportState(), offset: this.offset, + countLimit: this.countLimit, }; } @@ -1952,6 +1954,7 @@ export class DynamicRecordList extends DynamicList { this.countLimit = Number.MAX_SAFE_INTEGER; this.hasLimitedCount = false; this.model.notify(); + return this.count; } async load(params = {}) { @@ -2026,6 +2029,9 @@ export class DynamicRecordList extends DynamicList { * @returns {Promise} */ async _loadRecords() { + if (this.countLimit < this.offset + this.limit) { + this.countLimit = this.offset + this.limit; + } const kwargs = { limit: this.limit, offset: this.offset, @@ -2065,6 +2071,7 @@ export class DynamicRecordList extends DynamicList { this.hasLimitedCount = true; this.count = length - 1; } else { + this.hasLimitedCount = false; this.count = length; } diff --git a/addons/web/static/tests/core/pager_tests.js b/addons/web/static/tests/core/pager_tests.js index 7584e503f8a..154f57729c4 100644 --- a/addons/web/static/tests/core/pager_tests.js +++ b/addons/web/static/tests/core/pager_tests.js @@ -284,18 +284,21 @@ QUnit.module("Components", ({ beforeEach }) => { }); QUnit.test("updateTotal props: click next", async function (assert) { + let tempTotal = 10; + const realTotal = 18; const pager = await makePager({ offset: 0, limit: 5, - total: 10, + total: tempTotal, onUpdate(data) { - pager.updateProps(Object.assign({}, data)); - }, - async updateTotal() { - const total = 25; - pager.updateProps({ total, updateTotal: undefined }); - return total; + tempTotal = Math.min(realTotal, Math.max(tempTotal, data.offset + data.limit)); + const nextProps = { ...data, total: tempTotal }; + if (tempTotal === realTotal) { + nextProps.updateTotal = undefined; + } + pager.updateProps(nextProps); }, + async updateTotal() {}, }); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5"); @@ -309,23 +312,31 @@ QUnit.module("Components", ({ beforeEach }) => { await click(target, ".o_pager_next"); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "11-15"); - assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "25"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "15+"); + assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch"); + + await click(target, ".o_pager_next"); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "16-18"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "18"); assert.doesNotHaveClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch"); }); QUnit.test("updateTotal props: edit input", async function (assert) { + let tempTotal = 10; + const realTotal = 18; const pager = await makePager({ offset: 0, limit: 5, - total: 10, + total: tempTotal, onUpdate(data) { - pager.updateProps(Object.assign({}, data)); - }, - async updateTotal() { - const total = 25; - pager.updateProps({ total, updateTotal: undefined }); - return total; + tempTotal = Math.min(realTotal, Math.max(tempTotal, data.offset + data.limit)); + const nextProps = { ...data, total: tempTotal }; + if (tempTotal === realTotal) { + nextProps.updateTotal = undefined; + } + pager.updateProps(nextProps); }, + async updateTotal() {}, }); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5"); @@ -345,24 +356,20 @@ QUnit.module("Components", ({ beforeEach }) => { input = target.querySelector(".o_pager_counter input.o_pager_value"); input.value = "3-20"; await triggerEvents(input, null, ["change", "blur"]); - assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-20"); - assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "25"); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-18"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "18"); assert.doesNotHaveClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch"); }); - QUnit.test("updateTotal props: can use buttons even if single page", async function (assert) { + QUnit.test("updateTotal props: can use next even if single page", async function (assert) { const pager = await makePager({ offset: 0, limit: 5, total: 5, onUpdate(data) { - pager.updateProps(Object.assign({}, data)); - }, - async updateTotal() { - const total = 25; - pager.updateProps({ total, updateTotal: undefined }); - return total; + pager.updateProps({ ...data, total: 10 }); }, + async updateTotal() {}, }); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5"); @@ -372,7 +379,7 @@ QUnit.module("Components", ({ beforeEach }) => { await click(target, ".o_pager_next"); assert.strictEqual(target.querySelector(".o_pager_value").innerText, "6-10"); - assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "25"); - assert.doesNotHaveClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+"); + assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch"); }); }); diff --git a/addons/web/static/tests/views/kanban_view_tests.js b/addons/web/static/tests/views/kanban_view_tests.js index 4b96c32cd06..125d9506d70 100644 --- a/addons/web/static/tests/views/kanban_view_tests.js +++ b/addons/web/static/tests/views/kanban_view_tests.js @@ -1167,6 +1167,151 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["search_count"]); }); + QUnit.test("pager, ungrouped, with count limit reached, click next", async (assert) => { + patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); + + await makeView({ + type: "kanban", + resModel: "partner", + serverData, + arch: ` + + + +
+
+
+
`, + async mockRPC(route, { method }) { + assert.step(method); + }, + }); + + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); + assert.verifySteps(["get_views", "web_search_read"]); + + await click(target.querySelector(".o_pager_next")); + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-4"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4"); + assert.verifySteps(["web_search_read"]); + }); + + QUnit.test("pager, ungrouped, with count limit reached, click next (2)", async (assert) => { + patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); + serverData.models.partner.records.push({ id: 5, foo: "xxx" }); + + await makeView({ + type: "kanban", + resModel: "partner", + serverData, + arch: ` + + + +
+
+
+
`, + async mockRPC(route, { method }) { + assert.step(method); + }, + }); + + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); + assert.verifySteps(["get_views", "web_search_read"]); + + await click(target.querySelector(".o_pager_next")); + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-4"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4+"); + assert.verifySteps(["web_search_read"]); + + await click(target.querySelector(".o_pager_next")); + assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "5-5"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); + assert.verifySteps(["web_search_read"]); + }); + + QUnit.test("pager, ungrouped, with count limit reached, click previous", async (assert) => { + patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); + serverData.models.partner.records.push({ id: 5, foo: "xxx" }); + + await makeView({ + type: "kanban", + resModel: "partner", + serverData, + arch: ` + + + +
+
+
+
`, + async mockRPC(route, { method }) { + assert.step(method); + }, + }); + + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); + assert.verifySteps(["get_views", "web_search_read"]); + + await click(target.querySelector(".o_pager_previous")); + assert.containsOnce(target, ".o_kanban_record:not(.o_kanban_ghost)"); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "5-5"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); + assert.verifySteps(["search_count", "web_search_read"]); + }); + + QUnit.test("pager, ungrouped, with count limit reached, edit pager", async (assert) => { + patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); + serverData.models.partner.records.push({ id: 5, foo: "xxx" }); + + await makeView({ + type: "kanban", + resModel: "partner", + serverData, + arch: ` + + + +
+
+
+
`, + async mockRPC(route, { method }) { + assert.step(method); + }, + }); + + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); + assert.verifySteps(["get_views", "web_search_read"]); + + await click(target, ".o_pager_value"); + await editInput(target, "input.o_pager_value", "2-4"); + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 3); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "2-4"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4+"); + assert.verifySteps(["web_search_read"]); + + await click(target, ".o_pager_value"); + await editInput(target, "input.o_pager_value", "2-14"); + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "2-5"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); + assert.verifySteps(["web_search_read"]); + }); + QUnit.test("count_limit attrs set in arch", async (assert) => { await makeView({ type: "kanban", @@ -1197,47 +1342,6 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["search_count"]); }); - QUnit.test( - "pager, ungrouped, deleting all records from last page should move to previous page", - async (assert) => { - patchDialog((_cls, props) => { - assert.step("open-dialog"); - props.confirm(); - }); - - await makeView({ - type: "kanban", - resModel: "partner", - serverData, - arch: ` - - -
- - -
-
-
-
`, - }); - - assert.deepEqual(getPagerValue(target), [1, 3]); - assert.strictEqual(getPagerLimit(target), 4); - - // move to next page - await pagerNext(target); - - assert.deepEqual(getPagerValue(target), [4, 4]); - - // delete a record - await click(target, ".o_kanban_record a"); - - assert.verifySteps(["open-dialog"]); - assert.deepEqual(getPagerValue(target), [1, 3]); - assert.strictEqual(getPagerLimit(target), 3); - } - ); - QUnit.test("pager, update calls onUpdatedPager before the render", async (assert) => { assert.expect(8); diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index d09de3b8697..b6aba0e2cb6 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -5684,6 +5684,114 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["web_search_read"]); }); + QUnit.test("pager, ungrouped, with count limit reached, click next", async function (assert) { + patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); + + let expectedCountLimit = 4; + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + mockRPC(route, args) { + assert.step(args.method); + if (args.method === "web_search_read") { + assert.strictEqual(args.kwargs.count_limit, expectedCountLimit); + } + }, + }); + + assert.containsN(target, ".o_data_row", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); + assert.verifySteps(["get_views", "web_search_read"]); + + expectedCountLimit = 5; + await click(target.querySelector(".o_pager_next")); + assert.containsN(target, ".o_data_row", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-4"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4"); + assert.verifySteps(["web_search_read"]); + }); + + QUnit.test("pager, ungrouped, with count limit reached, click next (2)", async (assert) => { + patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); + serverData.models.foo.records.push({ id: 5, bar: true, foo: "xxx" }); + + let expectedCountLimit = 4; + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + mockRPC(route, args) { + assert.step(args.method); + if (args.method === "web_search_read") { + assert.strictEqual(args.kwargs.count_limit, expectedCountLimit); + } + }, + }); + + assert.containsN(target, ".o_data_row", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); + assert.verifySteps(["get_views", "web_search_read"]); + + expectedCountLimit = 5; + await click(target.querySelector(".o_pager_next")); + assert.containsN(target, ".o_data_row", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-4"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4+"); + assert.verifySteps(["web_search_read"]); + + expectedCountLimit = 7; + await click(target.querySelector(".o_pager_next")); + assert.containsOnce(target, ".o_data_row"); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "5-5"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); + assert.verifySteps(["web_search_read"]); + }); + + QUnit.test("pager, ungrouped, with count limit reached, edit pager", async (assert) => { + patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 3 }); + serverData.models.foo.records.push({ id: 5, bar: true, foo: "xxx" }); + + let expectedCountLimit = 4; + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + mockRPC(route, args) { + assert.step(args.method); + if (args.method === "web_search_read") { + assert.strictEqual(args.kwargs.count_limit, expectedCountLimit); + } + }, + }); + + assert.containsN(target, ".o_data_row", 2); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-2"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "3+"); + assert.verifySteps(["get_views", "web_search_read"]); + + expectedCountLimit = 5; + await click(target, ".o_pager_value"); + await editInput(target, "input.o_pager_value", "2-4"); + assert.containsN(target, ".o_data_row", 3); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "2-4"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "4+"); + assert.verifySteps(["web_search_read"]); + + expectedCountLimit = 15; + await click(target, ".o_pager_value"); + await editInput(target, "input.o_pager_value", "2-14"); + assert.containsN(target, ".o_data_row", 4); + assert.strictEqual(target.querySelector(".o_pager_value").innerText, "2-5"); + assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5"); + assert.verifySteps(["web_search_read"]); + }); + QUnit.test("pager, ungrouped, with count equals count limit", async function (assert) { patchWithCleanup(DynamicRecordList, { WEB_SEARCH_READ_COUNT_LIMIT: 4 });