diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/composer_suggestion | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/composer_suggestion')
7 files changed, 828 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js new file mode 100644 index 00000000..da54ab54 --- /dev/null +++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js @@ -0,0 +1,143 @@ +odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion.js', function (require) { +'use strict'; + +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js'); + +const components = { + PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'), +}; + +const { Component } = owl; + +class ComposerSuggestion extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + useStore(props => { + const composer = this.env.models['mail.composer'].get(this.props.composerLocalId); + const record = this.env.models[props.modelName].get(props.recordLocalId); + return { + composerHasToScrollToActiveSuggestion: composer && composer.hasToScrollToActiveSuggestion, + record: record ? record.__state : undefined, + }; + }); + useUpdate({ func: () => this._update() }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {mail.composer} + */ + get composer() { + return this.env.models['mail.composer'].get(this.props.composerLocalId); + } + + get isCannedResponse() { + return this.props.modelName === "mail.canned_response"; + } + + get isChannel() { + return this.props.modelName === "mail.thread"; + } + + get isCommand() { + return this.props.modelName === "mail.channel_command"; + } + + get isPartner() { + return this.props.modelName === "mail.partner"; + } + + get record() { + return this.env.models[this.props.modelName].get(this.props.recordLocalId); + } + + /** + * Returns a descriptive title for this suggestion. Useful to be able to + * read both parts when they are overflowing the UI. + * + * @returns {string} + */ + title() { + if (this.isCannedResponse) { + return _.str.sprintf("%s: %s", this.record.source, this.record.substitution); + } + if (this.isChannel) { + return this.record.name; + } + if (this.isCommand) { + return _.str.sprintf("%s: %s", this.record.name, this.record.help); + } + if (this.isPartner) { + if (this.record.email) { + return _.str.sprintf("%s (%s)", this.record.nameOrDisplayName, this.record.email); + } + return this.record.nameOrDisplayName; + } + return ""; + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _update() { + if ( + this.composer && + this.composer.hasToScrollToActiveSuggestion && + this.props.isActive + ) { + this.el.scrollIntoView({ + block: 'center', + }); + this.composer.update({ hasToScrollToActiveSuggestion: false }); + } + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onClick(ev) { + ev.preventDefault(); + this.composer.update({ activeSuggestedRecord: [['link', this.record]] }); + this.composer.insertSuggestion(); + this.composer.closeSuggestions(); + this.trigger('o-composer-suggestion-clicked'); + } + +} + +Object.assign(ComposerSuggestion, { + components, + defaultProps: { + isActive: false, + }, + props: { + composerLocalId: String, + isActive: Boolean, + modelName: String, + recordLocalId: String, + }, + template: 'mail.ComposerSuggestion', +}); + +return ComposerSuggestion; + +}); diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion.scss b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.scss new file mode 100644 index 00000000..4083c149 --- /dev/null +++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.scss @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + +.o_ComposerSuggestion { + display: flex; + width: map-get($sizes, 100); + padding: map-get($spacers, 2) map-get($spacers, 4); +} + +.o_ComposerSuggestion_part1 { + // avoid shrinking part 1 because it is more important than part 2 + // because no shrink, ensure it cannot overflow with a max-width + flex: 0 0 auto; + max-width: 100%; + overflow: hidden; + padding-inline-end: map-get($spacers, 2); + text-overflow: ellipsis; +} + +.o_ComposerSuggestion_part2 { + // shrink part 2 to properly ensure it cannot overflow + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; +} + +.o_ComposerSuggestion_partnerImStatusIcon { + flex: 0 0 auto; +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_ComposerSuggestion_part1 { + font-weight: $font-weight-bold; +} + +.o_ComposerSuggestion_part2 { + font-style: italic; + color: $gray-600; +} diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml new file mode 100644 index 00000000..787b5aed --- /dev/null +++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="mail.ComposerSuggestion" owl="1"> + <a class="o_ComposerSuggestion dropdown-item" t-att-class="{ 'active': props.isActive }" href="#" t-att-title="title()" role="menuitem" t-on-click="_onClick"> + <t t-if="record"> + <t t-if="isCannedResponse"> + <span class="o_ComposerSuggestion_part1"><t t-esc="record.source"/></span> + <span class="o_ComposerSuggestion_part2"><t t-esc="record.substitution"/></span> + </t> + <t t-if="isChannel"> + <span class="o_ComposerSuggestion_part1"><t t-esc="record.name"/></span> + </t> + <t t-if="isCommand"> + <span class="o_ComposerSuggestion_part1"><t t-esc="record.name"/></span> + <span class="o_ComposerSuggestion_part2"><t t-esc="record.help"/></span> + </t> + <t t-if="isPartner"> + <PartnerImStatusIcon + class="o_ComposerSuggestion_partnerImStatusIcon" + hasBackground="false" + partnerLocalId="record.localId" + /> + <span class="o_ComposerSuggestion_part1"><t t-esc="record.nameOrDisplayName"/></span> + <t t-if="record.email"> + <span class="o_ComposerSuggestion_part2">(<t t-esc="record.email"/>)</span> + </t> + </t> + </t> + </a> + </t> + +</templates> diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js new file mode 100644 index 00000000..0e0f8685 --- /dev/null +++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js @@ -0,0 +1,154 @@ +odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js', function (require) { +'use strict'; + +const components = { + ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'), +}; +const { + afterEach, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('composer_suggestion', {}, function () { +QUnit.module('composer_suggestion_canned_response_tests.js', { + beforeEach() { + beforeEach(this); + + this.createComposerSuggestion = async props => { + await createRootComponent(this, components.ComposerSuggestion, { + props, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('canned response suggestion displayed', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const cannedResponse = this.env.models['mail.canned_response'].create({ + id: 7, + source: 'hello', + substitution: "Hello, how are you?", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.canned_response', + recordLocalId: cannedResponse.localId, + }); + + assert.containsOnce( + document.body, + `.o_ComposerSuggestion`, + "Canned response suggestion should be present" + ); +}); + +QUnit.test('canned response suggestion correct data', async function (assert) { + assert.expect(5); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const cannedResponse = this.env.models['mail.canned_response'].create({ + id: 7, + source: 'hello', + substitution: "Hello, how are you?", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.canned_response', + recordLocalId: cannedResponse.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Canned response suggestion should be present" + ); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion_part1', + "Canned response source should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestion_part1`).textContent, + "hello", + "Canned response source should be displayed" + ); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion_part2', + "Canned response substitution should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestion_part2`).textContent, + "Hello, how are you?", + "Canned response substitution should be displayed" + ); +}); + +QUnit.test('canned response suggestion active', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const cannedResponse = this.env.models['mail.canned_response'].create({ + id: 7, + source: 'hello', + substitution: "Hello, how are you?", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.canned_response', + recordLocalId: cannedResponse.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Canned response suggestion should be displayed" + ); + assert.hasClass( + document.querySelector('.o_ComposerSuggestion'), + 'active', + "should be active initially" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js new file mode 100644 index 00000000..7a211483 --- /dev/null +++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js @@ -0,0 +1,144 @@ +odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js', function (require) { +'use strict'; + +const components = { + ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'), +}; +const { + afterEach, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('composer_suggestion', {}, function () { +QUnit.module('composer_suggestion_channel_tests.js', { + beforeEach() { + beforeEach(this); + + this.createComposerSuggestion = async props => { + await createRootComponent(this, components.ComposerSuggestion, { + props, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('channel mention suggestion displayed', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const channel = this.env.models['mail.thread'].create({ + id: 7, + name: "General", + model: 'mail.channel', + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.thread', + recordLocalId: channel.localId, + }); + + assert.containsOnce( + document.body, + `.o_ComposerSuggestion`, + "Channel mention suggestion should be present" + ); +}); + +QUnit.test('channel mention suggestion correct data', async function (assert) { + assert.expect(3); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const channel = this.env.models['mail.thread'].create({ + id: 7, + name: "General", + model: 'mail.channel', + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.thread', + recordLocalId: channel.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Channel mention suggestion should be present" + ); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion_part1', + "Channel name should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestion_part1`).textContent, + "General", + "Channel name should be displayed" + ); +}); + +QUnit.test('channel mention suggestion active', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const channel = this.env.models['mail.thread'].create({ + id: 7, + name: "General", + model: 'mail.channel', + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.thread', + recordLocalId: channel.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Channel mention suggestion should be displayed" + ); + assert.hasClass( + document.querySelector('.o_ComposerSuggestion'), + 'active', + "should be active initially" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js new file mode 100644 index 00000000..8bbb3d45 --- /dev/null +++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js @@ -0,0 +1,151 @@ +odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js', function (require) { +'use strict'; + +const components = { + ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'), +}; +const { + afterEach, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('composer_suggestion', {}, function () { +QUnit.module('composer_suggestion_command_tests.js', { + beforeEach() { + beforeEach(this); + + this.createComposerSuggestion = async props => { + await createRootComponent(this, components.ComposerSuggestion, { + props, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('command suggestion displayed', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const command = this.env.models['mail.channel_command'].create({ + name: 'whois', + help: "Displays who it is", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.channel_command', + recordLocalId: command.localId, + }); + + assert.containsOnce( + document.body, + `.o_ComposerSuggestion`, + "Command suggestion should be present" + ); +}); + +QUnit.test('command suggestion correct data', async function (assert) { + assert.expect(5); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const command = this.env.models['mail.channel_command'].create({ + name: 'whois', + help: "Displays who it is", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.channel_command', + recordLocalId: command.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Command suggestion should be present" + ); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion_part1', + "Command name should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestion_part1`).textContent, + "whois", + "Command name should be displayed" + ); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion_part2', + "Command help should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestion_part2`).textContent, + "Displays who it is", + "Command help should be displayed" + ); +}); + +QUnit.test('command suggestion active', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const command = this.env.models['mail.channel_command'].create({ + name: 'whois', + help: "Displays who it is", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.channel_command', + recordLocalId: command.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Command suggestion should be displayed" + ); + assert.hasClass( + document.querySelector('.o_ComposerSuggestion'), + 'active', + "should be active initially" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js new file mode 100644 index 00000000..548fd6d7 --- /dev/null +++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js @@ -0,0 +1,160 @@ +odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js', function (require) { +'use strict'; + +const components = { + ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'), +}; +const { + afterEach, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('composer_suggestion', {}, function () { +QUnit.module('composer_suggestion_partner_tests.js', { + beforeEach() { + beforeEach(this); + + this.createComposerSuggestion = async props => { + await createRootComponent(this, components.ComposerSuggestion, { + props, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('partner mention suggestion displayed', async function (assert) { + assert.expect(1); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const partner = this.env.models['mail.partner'].create({ + id: 7, + im_status: 'online', + name: "Demo User", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.partner', + recordLocalId: partner.localId, + }); + + assert.containsOnce( + document.body, + `.o_ComposerSuggestion`, + "Partner mention suggestion should be present" + ); +}); + +QUnit.test('partner mention suggestion correct data', async function (assert) { + assert.expect(6); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const partner = this.env.models['mail.partner'].create({ + email: "demo_user@odoo.com", + id: 7, + im_status: 'online', + name: "Demo User", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.partner', + recordLocalId: partner.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Partner mention suggestion should be present" + ); + assert.strictEqual( + document.querySelectorAll(`.o_PartnerImStatusIcon`).length, + 1, + "Partner's im_status should be displayed" + ); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion_part1', + "Partner's name should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestion_part1`).textContent, + "Demo User", + "Partner's name should be displayed" + ); + assert.containsOnce( + document.body, + '.o_ComposerSuggestion_part2', + "Partner's email should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestion_part2`).textContent, + "(demo_user@odoo.com)", + "Partner's email should be displayed" + ); +}); + +QUnit.test('partner mention suggestion active', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 20 }); + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel', + }); + const partner = this.env.models['mail.partner'].create({ + id: 7, + im_status: 'online', + name: "Demo User", + }); + await this.createComposerSuggestion({ + composerLocalId: thread.composer.localId, + isActive: true, + modelName: 'mail.partner', + recordLocalId: partner.localId, + }); + + assert.containsOnce( + document.body, + '.o_ComposerSuggestion', + "Partner mention suggestion should be displayed" + ); + assert.hasClass( + document.querySelector('.o_ComposerSuggestion'), + 'active', + "should be active initially" + ); +}); + +}); +}); +}); + +}); |
