From 949039135df1455a9809a788e04ccf93657dbfe3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Fri, 20 Oct 2023 13:50:29 +0200 Subject: [PATCH 1/2] [FIX] mail: fix invalid date done in the activity list popover Date done field was in camel case instead of snake case leading to undefined value and "Invalid date" in the interface. Task-3558929 Part-of: odoo/odoo#139373 --- addons/mail/static/src/core/web/activity_list_popover_item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/static/src/core/web/activity_list_popover_item.js b/addons/mail/static/src/core/web/activity_list_popover_item.js index c3a957d25ff..b3becefcf59 100644 --- a/addons/mail/static/src/core/web/activity_list_popover_item.js +++ b/addons/mail/static/src/core/web/activity_list_popover_item.js @@ -111,7 +111,7 @@ export class ActivityListPopoverItem extends Component { } get dateDoneFormatted() { - return luxon.DateTime.fromISO(this.props.activity.dateDone).toLocaleString( + return luxon.DateTime.fromISO(this.props.activity.date_done).toLocaleString( luxon.DateTime.DATE_SHORT ); } From d2f44211b98d0fc9f5e11cd9fee21a0f2b7447aa Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Fri, 20 Oct 2023 16:20:20 +0200 Subject: [PATCH 2/2] [IMP] mail: add/modify activity client tests We add a test that checks the rendering of the activity view and the activity popover with activities done. We also add a test that marks as done an activity and verify that the activity view is updated accordingly. And we do a correction in the test "activity menu widget: activity menu with 2 models" (assert.ok(selectContaining... was always true as an array is always returned and [] is true in javascript). Task-3558929 Part-of: odoo/odoo#139373 --- .../mock_server/models/mail_activity.js | 24 ++- .../test_mail/static/tests/activity_tests.js | 199 ++++++++++++++++++ .../tests/systray_activity_menu_tests.js | 16 +- 3 files changed, 227 insertions(+), 12 deletions(-) diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js index d74a24ceace..7486e468a85 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js @@ -4,7 +4,7 @@ import { Domain } from "@web/core/domain"; import { groupBy, sortBy, unique } from "@web/core/utils/arrays"; import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -import { deserializeDate } from "@web/core/l10n/dates"; +import { deserializeDate, serializeDate, today } from "@web/core/l10n/dates"; const { DateTime} = luxon; @@ -84,8 +84,7 @@ patch(MockServer.prototype, { * @returns {Object} */ _mockMailActivityActionDone(ids) { - const activities = this.getRecords("mail.activity", [["id", "in", ids]]); - this.mockUnlink("mail.activity", [activities.map((activity) => activity.id)]); + this._mockMailActivityActionFeedback(ids); }, /** * Simulates `action_feedback` on `mail.activity`. @@ -96,7 +95,24 @@ patch(MockServer.prototype, { * @returns {Object} */ _mockMailActivityActionFeedback(ids, attachment_ids= null) { - this._mockMailActivityActionDone(ids); + const activities = this.getRecords("mail.activity", [["id", "in", ids]]); + const activityTypes = this.getRecords("mail.activity.type", [ + ["id", "in", unique(activities.map((a) => a.activity_type_id))], + ]); + const activityTypeById = Object.fromEntries( + activityTypes.map((actType) => [actType.id, actType]) + ); + this.mockWrite("mail.activity", [ + activities + .filter((act) => activityTypeById[act.activity_type_id].keep_done) + .map((act) => act.id), + { active: false, date_done: serializeDate(today()), state: "done" }, + ]); + this.mockUnlink("mail.activity", [ + activities + .filter((act) => !activityTypeById[act.activity_type_id].keep_done) + .map((act) => act.id), + ]); }, /** * Simulates `action_feedback_schedule_next` on `mail.activity`. diff --git a/addons/test_mail/static/tests/activity_tests.js b/addons/test_mail/static/tests/activity_tests.js index cde4946c552..2f8ffb59401 100644 --- a/addons/test_mail/static/tests/activity_tests.js +++ b/addons/test_mail/static/tests/activity_tests.js @@ -241,6 +241,162 @@ QUnit.module("test_mail", {}, function () { ); }); + QUnit.test("activity view: Activity rendering with done activities", async function (assert) { + function containsN(domElement, selector, containings) { + return Array.from(domElement.querySelectorAll(selector)).filter((sel) => + containings.every((containing) => sel.textContent.includes(containing)) + ); + } + + const activityTypeUpload = pyEnv["mail.activity.type"].create({ + category: "upload_file", + name: "Test Upload document", + keep_done: true, + }); + pyEnv["mail.activity"].create( + Object.entries(["done", "done", "done", "done", "planned", "planned", "planned"]).map( + ([idx, state]) => ({ + active: state !== "done", + activity_type_id: activityTypeUpload, + attachment_ids: + state === "done" + ? [ + pyEnv["ir.attachment"].create({ + name: `attachment ${idx}`, + create_date: serializeDate( + DateTime.now().minus({ days: idx }) + ), + create_uid: pyEnv.currentUserId, + }), + ] + : [], + can_write: true, + date_deadline: serializeDate(DateTime.now().plus({ days: idx })), + date_done: + state === "done" + ? serializeDate(DateTime.now().minus({ days: idx })) + : false, + display_name: `Upload folders ${idx}`, + state: state, + user_id: pyEnv["res.users"].create({ display_name: `user${idx}` }), + }) + ) + ); + const [meetingRecord, officeRecord] = pyEnv["mail.test.activity"].search([]); + const uploadDoneActs = pyEnv["mail.activity"].searchRead([ + ["activity_type_id", "=", activityTypeUpload], + ["active", "=", false], + ]); + const uploadPlannedActs = pyEnv["mail.activity"].searchRead([ + ["activity_type_id", "=", activityTypeUpload], + ]); + pyEnv["mail.test.activity"].write([meetingRecord], { + activity_ids: [ + uploadPlannedActs[0].id, + uploadPlannedActs[1].id, + uploadPlannedActs[2].id, + uploadDoneActs[0].id, + ], + }); + pyEnv["mail.test.activity"].write([officeRecord], { + activity_ids: [uploadDoneActs[1].id, uploadDoneActs[2].id, uploadDoneActs[3].id], + }); + const { openView } = await start({ + serverData, + }); + await openView({ + res_model: "mail.test.activity", + views: [[false, "activity"]], + }); + const domActivity = document.querySelector(".o_activity_view"); + const domHeaderUpload = domActivity.querySelector( + "table thead tr:first-child th:nth-child(6)" + ); + const selRowMeetingCellUpload = "table tbody tr:first-child td:nth-child(6)"; + const domRowMeetingCellUpload = domActivity.querySelector(selRowMeetingCellUpload); + const selRowOfficeCellUpload = "table tbody tr:nth-child(2) td:nth-child(6)"; + const domRowOfficeCellUpload = domActivity.querySelector(selRowOfficeCellUpload); + + // Headers + await contains(".o_column_progress .progress-bar:first-child[data-tooltip='3 Planned']", { + target: domHeaderUpload, + }); + await contains(".o_animated_number", { + target: domHeaderUpload, + text: "3", + }); + await contains(".o_column_progress_aggregated_on", { + target: domHeaderUpload, + text: "7", + }); + // Cells avatars + await contains( + `.o-mail-Avatar img[data-src='/web/image/res.users/${uploadPlannedActs[0].user_id[0]}/avatar_128'`, + { target: domRowMeetingCellUpload } + ); + await contains( + `.o-mail-Avatar img[data-src='/web/image/res.users/${uploadPlannedActs[1].user_id[0]}/avatar_128'`, + { target: domRowMeetingCellUpload } + ); + await contains( + `.o-mail-Avatar img[data-src='/web/image/res.users/${uploadPlannedActs[2].user_id[0]}/avatar_128'`, + { target: domRowMeetingCellUpload, count: 0 } + ); + await contains(`.o-mail-Avatar`, { target: domRowOfficeCellUpload, count: 0 }); // all activity are done + // Cells counters + assert.ok( + containsN(domRowMeetingCellUpload, ".o-mail-ActivityCell-counter", ["3", "/", "4"]) + ); + await contains(".o-mail-ActivityCell-counter", { + text: "3", + target: domRowOfficeCellUpload, + }); + // Cells dates + await contains(".o-mail-ActivityCell-deadline", { + text: luxon.DateTime.fromISO(uploadPlannedActs[0].date_deadline).toLocaleString( + luxon.DateTime.DATE_SHORT + ), + target: domRowMeetingCellUpload, + }); + await contains(".o-mail-ActivityCell-deadline", { + text: luxon.DateTime.fromISO(uploadDoneActs[1].date_done).toLocaleString( + luxon.DateTime.DATE_SHORT + ), + target: domRowOfficeCellUpload, + }); + // Activity list popovers content + await click(domActivity, `${selRowMeetingCellUpload} > div`); + await contains(".o-mail-ActivityListPopover .badge.text-bg-success", { text: "3" }); // 3 planned + for (const actIdx of [0, 1, 2]) { + await contains(".o-mail-ActivityListPopoverItem", { + text: uploadPlannedActs[actIdx].user_id[1], + }); + } + await contains(".o-mail-ActivityListPopoverItem", { text: "Due in 4 days" }); + await contains(".o-mail-ActivityListPopoverItem", { text: "Due in 5 days" }); + await contains(".o-mail-ActivityListPopoverItem", { text: "Due in 6 days" }); + await contains(".o-mail-ActivityListPopover .badge.text-bg-secondary", { text: "1" }); // 1 done + await contains(".o-mail-ActivityListPopoverItem", { text: uploadDoneActs[0].user_id[1] }); + await contains(".o-mail-ActivityListPopoverItem", { + text: luxon.DateTime.fromISO(uploadDoneActs[0].date_done).toLocaleString( + luxon.DateTime.DATE_SHORT + ), + }); + + await click(domActivity, `${selRowOfficeCellUpload} > div`); + await contains(".o-mail-ActivityListPopover .badge.text-bg-secondary", { text: "3" }); // 3 done + for (const actIdx of [1, 2, 3]) { + await contains(".o-mail-ActivityListPopoverItem", { + text: luxon.DateTime.fromISO(uploadDoneActs[actIdx].date_done).toLocaleString( + luxon.DateTime.DATE_SHORT + ), + }); + await contains(".o-mail-ActivityListPopoverItem", { + text: uploadDoneActs[actIdx].user_id[1], + }); + } + }); + QUnit.test( "activity view: a pager can be used when there are more than the limit of 100 activities to display", async function (assert) { @@ -538,6 +694,49 @@ QUnit.module("test_mail", {}, function () { ]); }); + QUnit.test("activity view: Mark as done with keep done enabled", async function (assert) { + const emailActType = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0]; + pyEnv["mail.activity.type"].write([emailActType], { + keep_done: true, + }); + const { openView } = await start({ + serverData, + }); + await openView({ + res_model: "mail.test.activity", + views: [[false, "activity"]], + context: { active_test: false }, + }); + const domActivity = document.querySelector(".o_activity_view"); + const domHeaderEmail = domActivity.querySelector( + "table thead tr:first-child th:nth-child(2)" + ); + const selRowOfficeCellEmail = "table tbody tr:nth-child(2) td:nth-child(2)"; + + await contains(".o_animated_number", { + target: domHeaderEmail, + text: "2", + }); + await contains(".o_column_progress_aggregated_on", { + target: domHeaderEmail, + text: "2", + }); + await click(domActivity, `${selRowOfficeCellEmail} > div`); + await click( + document, + ".o-mail-ActivityListPopoverItem .o-mail-ActivityListPopoverItem-markAsDone" + ); + await click(document, ".o-mail-ActivityMarkAsDone button[aria-label='Done']"); + await contains(".o_animated_number", { + target: domHeaderEmail, + text: "1", + }); + await contains(".o_column_progress_aggregated_on", { + target: domHeaderEmail, + text: "2", + }); + }); + QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) { assert.expect(4); diff --git a/addons/test_mail/static/tests/systray_activity_menu_tests.js b/addons/test_mail/static/tests/systray_activity_menu_tests.js index 16a2638ae34..add4c736091 100644 --- a/addons/test_mail/static/tests/systray_activity_menu_tests.js +++ b/addons/test_mail/static/tests/systray_activity_menu_tests.js @@ -45,10 +45,10 @@ QUnit.test("do not show empty text when at least some future activities", async }); QUnit.test("activity menu widget: activity menu with 2 models", async (assert) => { - function selectContaining(domElement, selector, containings) { + function containsN(domElement, selector, containings) { return Array.from(domElement.querySelectorAll(selector)).filter((sel) => containings.every((containing) => sel.textContent.includes(containing)) - ); + ).length; } const tomorrow = today().plus({ days: 1 }); @@ -95,22 +95,22 @@ QUnit.test("activity menu widget: activity menu with 2 models", async (assert) = await contains(".o-mail-ActivityMenu"); await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", { count: 2 }); assert.ok( - selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "0 Late"]) + containsN(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "0 Late"]) ); assert.ok( - selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "1 Today"]) + containsN(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "1 Today"]) ); assert.ok( - selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "0 Future"]) + containsN(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "0 Future"]) ); assert.ok( - selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "1 Late"]) + containsN(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "1 Late"]) ); assert.ok( - selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "1 Today"]) + containsN(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "1 Today"]) ); assert.ok( - selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "2 Future"]) + containsN(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "2 Future"]) ); actionChecks.res_model = "res.partner"; await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "res.partner" });