summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/widgets/form_renderer
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/widgets/form_renderer
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/widgets/form_renderer')
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer.js188
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer.scss17
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js982
3 files changed, 1187 insertions, 0 deletions
diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.js b/addons/mail/static/src/widgets/form_renderer/form_renderer.js
new file mode 100644
index 00000000..cf147656
--- /dev/null
+++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.js
@@ -0,0 +1,188 @@
+odoo.define('mail/static/src/widgets/form_renderer/form_renderer.js', function (require) {
+"use strict";
+
+const components = {
+ ChatterContainer: require('mail/static/src/components/chatter_container/chatter_container.js'),
+};
+
+const FormRenderer = require('web.FormRenderer');
+const { ComponentWrapper } = require('web.OwlCompatibility');
+
+class ChatterContainerWrapperComponent extends ComponentWrapper {}
+
+/**
+ * Include the FormRenderer to instantiate the chatter area containing (a
+ * subset of) the mail widgets (mail_thread, mail_followers and mail_activity).
+ */
+FormRenderer.include({
+ /**
+ * @override
+ */
+ init(parent, state, params) {
+ this._super(...arguments);
+ this.chatterFields = params.chatterFields;
+ this.mailFields = params.mailFields;
+ this._chatterContainerComponent = undefined;
+ /**
+ * The target of chatter, if chatter has to be appended to the DOM.
+ * This is set when arch contains `div.oe_chatter`.
+ */
+ this._chatterContainerTarget = undefined;
+ // Do not load chatter in form view dialogs
+ this._isFromFormViewDialog = params.isFromFormViewDialog;
+ },
+ /**
+ * @override
+ */
+ destroy() {
+ this._super(...arguments);
+ this._chatterContainerComponent = undefined;
+ this.off('o_attachments_changed', this);
+ this.off('o_chatter_rendered', this);
+ this.off('o_message_posted', this);
+ owl.Component.env.bus.off('mail.thread:promptAddFollower-closed', this);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns whether the form renderer has a chatter to display or not.
+ * This is based on arch, which should have `div.oe_chatter`.
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _hasChatter() {
+ return !!this._chatterContainerTarget;
+ },
+ /**
+ * @private
+ */
+ _makeChatterContainerComponent() {
+ const props = this._makeChatterContainerProps();
+ this._chatterContainerComponent = new ChatterContainerWrapperComponent(
+ this,
+ components.ChatterContainer,
+ props
+ );
+ // Not in custom_events because other modules may remove this listener
+ // while attempting to extend them.
+ this.on('o_chatter_rendered', this, ev => this._onChatterRendered(ev));
+ if (this.chatterFields.hasRecordReloadOnMessagePosted) {
+ this.on('o_message_posted', this, ev => {
+ this.trigger_up('reload', { keepChanges: true });
+ });
+ }
+ if (this.chatterFields.hasRecordReloadOnAttachmentsChanged) {
+ this.on('o_attachments_changed', this, ev => this.trigger_up('reload', { keepChanges: true }));
+ }
+ if (this.chatterFields.hasRecordReloadOnFollowersUpdate) {
+ owl.Component.env.bus.on('mail.thread:promptAddFollower-closed', this, ev => this.trigger_up('reload', { keepChanges: true }));
+ }
+ },
+ /**
+ * @private
+ * @returns {Object}
+ */
+ _makeChatterContainerProps() {
+ return {
+ hasActivities: this.chatterFields.hasActivityIds,
+ hasFollowers: this.chatterFields.hasMessageFollowerIds,
+ hasMessageList: this.chatterFields.hasMessageIds,
+ isAttachmentBoxVisibleInitially: this.chatterFields.isAttachmentBoxVisibleInitially,
+ threadId: this.state.res_id,
+ threadModel: this.state.model,
+ };
+ },
+ /**
+ * Create the DOM element that will contain the chatter. This is made in
+ * a separate method so it can be overridden (like in mail_enterprise for
+ * example).
+ *
+ * @private
+ * @returns {jQuery.Element}
+ */
+ _makeChatterContainerTarget() {
+ const $el = $('<div class="o_FormRenderer_chatterContainer"/>');
+ this._chatterContainerTarget = $el[0];
+ return $el;
+ },
+ /**
+ * Mount the chatter
+ *
+ * Force re-mounting chatter component in DOM. This is necessary
+ * because each time `_renderView` is called, it puts old content
+ * in a fragment.
+ *
+ * @private
+ */
+ async _mountChatterContainerComponent() {
+ try {
+ await this._chatterContainerComponent.mount(this._chatterContainerTarget);
+ } catch (error) {
+ if (error.message !== "Mounting operation cancelled") {
+ throw error;
+ }
+ }
+ },
+ /**
+ * @override
+ */
+ _renderNode(node) {
+ if (node.tag === 'div' && node.attrs.class === 'oe_chatter') {
+ if (this._isFromFormViewDialog) {
+ return $('<div/>');
+ }
+ return this._makeChatterContainerTarget();
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * Overrides the function to render the chatter once the form view is
+ * rendered.
+ *
+ * @override
+ */
+ async __renderView() {
+ await this._super(...arguments);
+ if (this._hasChatter()) {
+ if (!this._chatterContainerComponent) {
+ this._makeChatterContainerComponent();
+ } else {
+ await this._updateChatterContainerComponent();
+ }
+ await this._mountChatterContainerComponent();
+ }
+ },
+ /**
+ * @private
+ */
+ async _updateChatterContainerComponent() {
+ const props = this._makeChatterContainerProps();
+ try {
+ await this._chatterContainerComponent.update(props);
+ } catch (error) {
+ if (error.message !== "Mounting operation cancelled") {
+ throw error;
+ }
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @abstract
+ * @private
+ * @param {OdooEvent} ev
+ * @param {Object} ev.data
+ * @param {mail.attachment[]} ev.data.attachments
+ * @param {mail.thread} ev.data.thread
+ */
+ _onChatterRendered(ev) {},
+});
+
+});
diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.scss b/addons/mail/static/src/widgets/form_renderer/form_renderer.scss
new file mode 100644
index 00000000..3092055b
--- /dev/null
+++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.scss
@@ -0,0 +1,17 @@
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+
+.o_FormRenderer_chatterContainer {
+ display: flex;
+ flex: 1 1 auto;
+ margin: 0 auto;
+ max-width: $o-form-view-sheet-max-width;
+ padding: map-get($spacers, 3) map-get($spacers, 3) map-get($spacers, 5);
+ width: 100%;
+}
+
+// FIX to hide chatter in dialogs when they are opened from an action returned by python code
+.modal .modal-dialog .o_form_view .o_FormRenderer_chatterContainer {
+ display: none;
+}
diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js b/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js
new file mode 100644
index 00000000..90cdb169
--- /dev/null
+++ b/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js
@@ -0,0 +1,982 @@
+odoo.define('mail/static/src/widgets/form_renderer/form_renderer_tests.js', function (require) {
+"use strict";
+
+const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ nextAnimationFrame,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+const config = require('web.config');
+const FormView = require('web.FormView');
+const {
+ dom: { triggerEvent },
+} = require('web.test_utils');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('widgets', {}, function () {
+QUnit.module('form_renderer', {}, function () {
+QUnit.module('form_renderer_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ // FIXME archs could be removed once task-2248306 is done
+ // The mockServer will try to get the list view
+ // of every relational fields present in the main view.
+ // In the case of mail fields, we don't really need them,
+ // but they still need to be defined.
+ this.createView = async (viewParams, ...args) => {
+ await afterNextRender(async () => {
+ const viewArgs = Object.assign(
+ {
+ archs: {
+ 'mail.activity,false,list': '<tree/>',
+ 'mail.followers,false,list': '<tree/>',
+ 'mail.message,false,list': '<tree/>',
+ },
+ },
+ viewParams,
+ );
+ const { afterEvent, env, widget } = await start(viewArgs, ...args);
+ this.afterEvent = afterEvent;
+ this.env = env;
+ this.widget = widget;
+ });
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('[technical] spinner when messaging is not created', async function (assert) {
+ /**
+ * Creation of messaging in env is async due to generation of models being
+ * async. Generation of models is async because it requires parsing of all
+ * JS modules that contain pieces of model definitions.
+ *
+ * Time of having no messaging is very short, almost imperceptible by user
+ * on UI, but the display should not crash during this critical time period.
+ */
+ assert.expect(3);
+
+ this.data['res.partner'].records.push({
+ display_name: "second partner",
+ id: 12,
+ });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation
+ waitUntilMessagingCondition: 'none',
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter"></div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterContainer',
+ "should display chatter container even when messaging is not created yet"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterContainer_noChatter',
+ "chatter container should not display any chatter when messaging not created"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatterContainer').textContent,
+ "Please wait...",
+ "chatter container should display spinner when messaging not yet created"
+ );
+});
+
+QUnit.test('[technical] keep spinner on transition from messaging non-created to messaging created (and non-initialized)', async function (assert) {
+ /**
+ * Creation of messaging in env is async due to generation of models being
+ * async. Generation of models is async because it requires parsing of all
+ * JS modules that contain pieces of model definitions.
+ *
+ * Time of having no messaging is very short, almost imperceptible by user
+ * on UI, but the display should not crash during this critical time period.
+ */
+ assert.expect(4);
+
+ const messagingBeforeCreationDeferred = makeDeferred();
+ this.data['res.partner'].records.push({
+ display_name: "second partner",
+ id: 12,
+ });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ messagingBeforeCreationDeferred,
+ async mockRPC(route, args) {
+ const _super = this._super.bind(this, ...arguments); // limitation of class.js
+ if (route === '/mail/init_messaging') {
+ await new Promise(() => {}); // simulate messaging never initialized
+ }
+ return _super();
+ },
+ waitUntilMessagingCondition: 'none',
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter"></div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.strictEqual(
+ document.querySelector('.o_ChatterContainer').textContent,
+ "Please wait...",
+ "chatter container should display spinner when messaging not yet created"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterContainer_noChatter',
+ "chatter container should not display any chatter when messaging not created"
+ );
+
+ // simulate messaging become created
+ messagingBeforeCreationDeferred.resolve();
+ await nextAnimationFrame();
+ assert.strictEqual(
+ document.querySelector('.o_ChatterContainer').textContent,
+ "Please wait...",
+ "chatter container should still display spinner when messaging is created but not initialized"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterContainer_noChatter',
+ "chatter container should still not display any chatter when messaging not initialized"
+ );
+});
+
+QUnit.test('spinner when messaging is created but not initialized', async function (assert) {
+ assert.expect(3);
+
+ this.data['res.partner'].records.push({
+ display_name: "second partner",
+ id: 12,
+ });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ async mockRPC(route, args) {
+ const _super = this._super.bind(this, ...arguments); // limitation of class.js
+ if (route === '/mail/init_messaging') {
+ await new Promise(() => {}); // simulate messaging never initialized
+ }
+ return _super();
+ },
+ waitUntilMessagingCondition: 'created',
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter"></div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterContainer',
+ "should display chatter container even when messaging is not fully initialized"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterContainer_noChatter',
+ "chatter container should not display any chatter when messaging not initialized"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatterContainer').textContent,
+ "Please wait...",
+ "chatter container should display spinner when messaging not yet initialized"
+ );
+});
+
+QUnit.test('transition non-initialized messaging to initialized messaging: display spinner then chatter', async function (assert) {
+ assert.expect(3);
+
+ const messagingBeforeInitializationDeferred = makeDeferred();
+ this.data['res.partner'].records.push({
+ display_name: "second partner",
+ id: 12,
+ });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ async mockRPC(route, args) {
+ const _super = this._super.bind(this, ...arguments); // limitation of class.js
+ if (route === '/mail/init_messaging') {
+ await messagingBeforeInitializationDeferred;
+ }
+ return _super();
+ },
+ waitUntilMessagingCondition: 'created',
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter"></div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.strictEqual(
+ document.querySelector('.o_ChatterContainer').textContent,
+ "Please wait...",
+ "chatter container should display spinner when messaging not yet initialized"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterContainer_noChatter',
+ "chatter container should not display any chatter when messaging not initialized"
+ );
+
+ // Simulate messaging becomes initialized
+ await afterNextRender(() => messagingBeforeInitializationDeferred.resolve());
+ assert.containsNone(
+ document.body,
+ '.o_ChatterContainer_noChatter',
+ "chatter container should now display chatter when messaging becomes initialized"
+ );
+});
+
+QUnit.test('basic chatter rendering', async function (assert) {
+ assert.expect(1);
+
+ this.data['res.partner'].records.push({ display_name: "second partner", id: 12, });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter"></div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Chatter`).length,
+ 1,
+ "there should be a chatter"
+ );
+});
+
+QUnit.test('basic chatter rendering without followers', async function (assert) {
+ assert.expect(6);
+
+ this.data['res.partner'].records.push({ display_name: "second partner", id: 12 });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="activity_ids"/>
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter',
+ "there should be a chatter"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar',
+ "there should be a chatter topbar"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonAttachments',
+ "there should be an attachment button"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonScheduleActivity',
+ "there should be a schedule activity button"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_FollowerListMenu',
+ "there should be no followers menu"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter_thread',
+ "there should be a thread"
+ );
+});
+
+QUnit.test('basic chatter rendering without activities', async function (assert) {
+ assert.expect(6);
+
+ this.data['res.partner'].records.push({ display_name: "second partner", id: 12 });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids"/>
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter',
+ "there should be a chatter"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar',
+ "there should be a chatter topbar"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonAttachments',
+ "there should be an attachment button"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_ChatterTopbar_buttonScheduleActivity',
+ "there should be a schedule activity button"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_FollowerListMenu',
+ "there should be a followers menu"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter_thread',
+ "there should be a thread"
+ );
+});
+
+QUnit.test('basic chatter rendering without messages', async function (assert) {
+ assert.expect(6);
+
+ this.data['res.partner'].records.push({ display_name: "second partner", id: 12 });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids"/>
+ <field name="activity_ids"/>
+ </div>
+ </form>
+ `,
+ res_id: 12,
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter',
+ "there should be a chatter"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar',
+ "there should be a chatter topbar"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonAttachments',
+ "there should be an attachment button"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonScheduleActivity',
+ "there should be a schedule activity button"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_FollowerListMenu',
+ "there should be a followers menu"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_Chatter_thread',
+ "there should be a thread"
+ );
+});
+
+QUnit.test('chatter updating', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.message'].records.push({ body: "not empty", model: 'res.partner', res_id: 12 });
+ this.data['res.partner'].records.push(
+ { display_name: "first partner", id: 11 },
+ { display_name: "second partner", id: 12 }
+ );
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ res_id: 11,
+ viewOptions: {
+ ids: [11, 12],
+ index: 0,
+ },
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ waitUntilEvent: {
+ eventName: 'o-thread-view-hint-processed',
+ message: "should wait until partner 11 thread loaded messages initially",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'res.partner' &&
+ threadViewer.thread.id === 11
+ );
+ },
+ }
+ });
+
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector('.o_pager_next').click(),
+ message: "should wait until partner 12 thread loaded messages after clicking on next",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'res.partner' &&
+ threadViewer.thread.id === 12
+ );
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "there should be a message in partner 12 thread"
+ );
+});
+
+QUnit.test('chatter should become enabled when creation done', async function (assert) {
+ assert.expect(10);
+
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter',
+ "there should be a chatter"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonSendMessage',
+ "there should be a send message button"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonLogNote',
+ "there should be a log note button"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatterTopbar_buttonLogNote',
+ "there should be an attachments button"
+ );
+ assert.ok(
+ document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled,
+ "send message button should be disabled"
+ );
+ assert.ok(
+ document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled,
+ "log note button should be disabled"
+ );
+ assert.ok(
+ document.querySelector(`.o_ChatterTopbar_buttonAttachments`).disabled,
+ "attachments button should be disabled"
+ );
+
+ document.querySelectorAll('.o_field_char')[0].focus();
+ document.execCommand('insertText', false, "hello");
+ await afterNextRender(() => {
+ document.querySelector('.o_form_button_save').click();
+ });
+ assert.notOk(
+ document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled,
+ "send message button should now be enabled"
+ );
+ assert.notOk(
+ document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled,
+ "log note button should now be enabled"
+ );
+ assert.notOk(
+ document.querySelector(`.o_ChatterTopbar_buttonAttachments`).disabled,
+ "attachments button should now be enabled"
+ );
+});
+
+QUnit.test('read more/less links are not duplicated when switching from read to edit mode', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.message'].records.push({
+ author_id: 100,
+ // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks
+ body: `
+ <div>
+ Dear Joel Willis,<br>
+ Thank you for your enquiry.<br>
+ If you have any questions, please let us know.
+ <br><br>
+ Thank you,<br>
+ <span data-o-mail-quote="1">-- <br data-o-mail-quote="1">
+ System
+ </span>
+ </div>
+ `,
+ id: 1000,
+ model: 'res.partner',
+ res_id: 2,
+ });
+ this.data['res.partner'].records.push({
+ display_name: "Someone",
+ id: 100,
+ });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ res_id: 2,
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ waitUntilEvent: {
+ eventName: 'o-component-message-read-more-less-inserted',
+ message: "should wait until read more/less is inserted initially",
+ predicate: ({ message }) => message.id === 1000,
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter',
+ "there should be a chatter"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "there should be a message"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_readMoreLess',
+ "there should be only one read more"
+ );
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-component-message-read-more-less-inserted',
+ func: () => document.querySelector('.o_form_button_edit').click(),
+ message: "should wait until read more/less is inserted after clicking on edit",
+ predicate: ({ message }) => message.id === 1000,
+ }));
+ assert.containsOnce(
+ document.body,
+ '.o_Message_readMoreLess',
+ "there should still be only one read more after switching to edit mode"
+ );
+
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-component-message-read-more-less-inserted',
+ func: () => document.querySelector('.o_form_button_cancel').click(),
+ message: "should wait until read more/less is inserted after canceling edit",
+ predicate: ({ message }) => message.id === 1000,
+ }));
+ assert.containsOnce(
+ document.body,
+ '.o_Message_readMoreLess',
+ "there should still be only one read more after switching back to read mode"
+ );
+});
+
+QUnit.test('read more links becomes read less after being clicked', async function (assert) {
+ assert.expect(6);
+
+ this.data['mail.message'].records = [{
+ author_id: 100,
+ // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks
+ body: `
+ <div>
+ Dear Joel Willis,<br>
+ Thank you for your enquiry.<br>
+ If you have any questions, please let us know.
+ <br><br>
+ Thank you,<br>
+ <span data-o-mail-quote="1">-- <br data-o-mail-quote="1">
+ System
+ </span>
+ </div>
+ `,
+ id: 1000,
+ model: 'res.partner',
+ res_id: 2,
+ }];
+ this.data['res.partner'].records.push({
+ display_name: "Someone",
+ id: 100,
+ });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ res_id: 2,
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ waitUntilEvent: {
+ eventName: 'o-component-message-read-more-less-inserted',
+ message: "should wait until read more/less is inserted initially",
+ predicate: ({ message }) => message.id === 1000,
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Chatter',
+ "there should be a chatter"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "there should be a message"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_readMoreLess',
+ "there should be a read more"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message_readMoreLess').textContent,
+ 'read more',
+ "read more/less link should contain 'read more' as text"
+ );
+
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-component-message-read-more-less-inserted',
+ func: () => document.querySelector('.o_form_button_edit').click(),
+ message: "should wait until read more/less is inserted after clicking on edit",
+ predicate: ({ message }) => message.id === 1000,
+ }));
+ assert.strictEqual(
+ document.querySelector('.o_Message_readMoreLess').textContent,
+ 'read more',
+ "read more/less link should contain 'read more' as text"
+ );
+
+ document.querySelector('.o_Message_readMoreLess').click();
+ assert.strictEqual(
+ document.querySelector('.o_Message_readMoreLess').textContent,
+ 'read less',
+ "read more/less link should contain 'read less' as text after it has been clicked"
+ );
+});
+
+QUnit.test('Form view not scrolled when switching record', async function (assert) {
+ assert.expect(6);
+
+ this.data['res.partner'].records.push(
+ {
+ id: 11,
+ display_name: "Partner 1",
+ description: [...Array(60).keys()].join('\n'),
+ },
+ {
+ id: 12,
+ display_name: "Partner 2",
+ }
+ );
+
+ const messages = [...Array(60).keys()].map(id => {
+ return {
+ model: 'res.partner',
+ res_id: id % 2 ? 11 : 12,
+ };
+ });
+ this.data['mail.message'].records = messages;
+
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ <field name="description"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ viewOptions: {
+ currentId: 11,
+ ids: [11, 12],
+ },
+ config: {
+ device: { size_class: config.device.SIZES.LG },
+ },
+ env: {
+ device: { size_class: config.device.SIZES.LG },
+ },
+ });
+
+ const controllerContentEl = document.querySelector('.o_content');
+
+ assert.strictEqual(
+ document.querySelector('.breadcrumb-item.active').textContent,
+ 'Partner 1',
+ "Form view should display partner 'Partner 1'"
+ );
+ assert.strictEqual(controllerContentEl.scrollTop, 0,
+ "The top of the form view is visible"
+ );
+
+ await afterNextRender(async () => {
+ controllerContentEl.scrollTop = controllerContentEl.scrollHeight - controllerContentEl.clientHeight;
+ await triggerEvent(
+ document.querySelector('.o_ThreadView_messageList'),
+ 'scroll'
+ );
+ });
+ assert.strictEqual(
+ controllerContentEl.scrollTop,
+ controllerContentEl.scrollHeight - controllerContentEl.clientHeight,
+ "The controller container should be scrolled to its bottom"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_pager_next').click()
+ );
+ assert.strictEqual(
+ document.querySelector('.breadcrumb-item.active').textContent,
+ 'Partner 2',
+ "The form view should display partner 'Partner 2'"
+ );
+ assert.strictEqual(controllerContentEl.scrollTop, 0,
+ "The top of the form view should be visible when switching record from pager"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_pager_previous').click()
+ );
+ assert.strictEqual(controllerContentEl.scrollTop, 0,
+ "Form view's scroll position should have been reset when switching back to first record"
+ );
+});
+
+QUnit.test('Attachments that have been unlinked from server should be visually unlinked from record', async function (assert) {
+ // Attachments that have been fetched from a record at certain time and then
+ // removed from the server should be reflected on the UI when the current
+ // partner accesses this record again.
+ assert.expect(2);
+
+ this.data['res.partner'].records.push(
+ { display_name: "Partner1", id: 11 },
+ { display_name: "Partner2", id: 12 }
+ );
+ this.data['ir.attachment'].records.push(
+ {
+ id: 11,
+ mimetype: 'text.txt',
+ res_id: 11,
+ res_model: 'res.partner',
+ },
+ {
+ id: 12,
+ mimetype: 'text.txt',
+ res_id: 11,
+ res_model: 'res.partner',
+ }
+ );
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ // View params
+ View: FormView,
+ model: 'res.partner',
+ res_id: 11,
+ viewOptions: {
+ ids: [11, 12],
+ index: 0,
+ },
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <field name="name"/>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ });
+ assert.strictEqual(
+ document.querySelector('.o_ChatterTopbar_buttonCount').textContent,
+ '2',
+ "Partner1 should have 2 attachments initially"
+ );
+
+ // The attachment links are updated on (re)load,
+ // so using pager is a way to reload the record "Partner1".
+ await afterNextRender(() =>
+ document.querySelector('.o_pager_next').click()
+ );
+ // Simulate unlinking attachment 12 from Partner 1.
+ this.data['ir.attachment'].records.find(a => a.id === 11).res_id = 0;
+ await afterNextRender(() =>
+ document.querySelector('.o_pager_previous').click()
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatterTopbar_buttonCount').textContent,
+ '1',
+ "Partner1 should now have 1 attachment after it has been unlinked from server"
+ );
+});
+
+QUnit.test('chatter just contains "creating a new record" message during the creation of a new record after having displayed a chatter for an existing record', async function (assert) {
+ assert.expect(2);
+
+ this.data['res.partner'].records.push({ id: 12 });
+ await this.createView({
+ data: this.data,
+ hasView: true,
+ View: FormView,
+ model: 'res.partner',
+ res_id: 12,
+ arch: `
+ <form>
+ <div class="oe_chatter">
+ <field name="message_ids"/>
+ </div>
+ </form>
+ `,
+ });
+
+ await afterNextRender(() => {
+ document.querySelector('.o_form_button_create').click();
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "Should have a single message when creating a new record"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Message_content').textContent,
+ 'Creating a new record...',
+ "the message content should be in accord to the creation of this record"
+ );
+});
+
+});
+});
+});
+
+});