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/website_livechat/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website_livechat/static')
26 files changed, 1396 insertions, 0 deletions
diff --git a/addons/website_livechat/static/description/icon.png b/addons/website_livechat/static/description/icon.png Binary files differnew file mode 100644 index 00000000..7c2a6ea8 --- /dev/null +++ b/addons/website_livechat/static/description/icon.png diff --git a/addons/website_livechat/static/description/icon.svg b/addons/website_livechat/static/description/icon.svg new file mode 100644 index 00000000..f608435b --- /dev/null +++ b/addons/website_livechat/static/description/icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#CD7690"/><stop offset="100%" stop-color="#CA5377"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M42.103 69H4c-2 0-4-.145-4-4.07V41.395L20 19.14C23.333 15.07 28.333 11 35 11s11.667 7.801 17 17.298v8.14h2V48.65l-2.635 1.09L51 54.754 42.103 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" d="M35.096 13C25.103 13 18 21.103 18 31.096V35c-1.333 0-2 .667-2 2v8.171C16 48.51 18.662 51 22 51h2c1.333 0 2-.667 2-2V37c0-1.333-.667-2-2-2h-2.979v-3.904c0-7.781 6.294-14.075 14.075-14.075C42.878 17.021 49 23.315 49 31.096V35h-3c-1.333 0-2 .667-2 2v12c0 1.333.667 2 2 2h3c.667 1.333.667 2.667 0 4h-9c0-.667-.328-1-.984-1H36c-.667 0-1 .333-1 1v1c.064.667.398 1 1 1h13c2 0 3-2 3-6 2 0 3-2 3-6v-8c0-1.333-1-2-3-2v-3.904C52 21.103 45.09 13 35.096 13z" opacity=".3"/><path fill="#FFF" d="M35.096 11C25.103 11 18 19.103 18 29.096V33c-1.333 0-2 .667-2 2v8.171C16 46.51 18.662 49 22 49h2c1.333 0 2-.667 2-2V35c0-1.333-.667-2-2-2h-2.979v-3.904c0-7.781 6.294-14.075 14.075-14.075C42.878 15.021 49 21.315 49 29.096V33h-3c-1.333 0-2 .667-2 2v12c0 1.333.667 2 2 2h3c.667 1.333.667 2.667 0 4h-9c0-.667-.328-1-.984-1H36c-.667 0-1 .333-1 1v1c.064.667.398 1 1 1h13c2 0 3-2 3-6 2 0 3-2 3-6v-8c0-1.333-1-2-3-2v-3.904C52 19.103 45.09 11 35.096 11z"/></g></g></svg>
\ No newline at end of file diff --git a/addons/website_livechat/static/src/bugfix/bugfix.js b/addons/website_livechat/static/src/bugfix/bugfix.js new file mode 100644 index 00000000..b2f0f5e8 --- /dev/null +++ b/addons/website_livechat/static/src/bugfix/bugfix.js @@ -0,0 +1,10 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('website_livechat/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +}); diff --git a/addons/website_livechat/static/src/bugfix/bugfix.scss b/addons/website_livechat/static/src/bugfix/bugfix.scss new file mode 100644 index 00000000..c4272e52 --- /dev/null +++ b/addons/website_livechat/static/src/bugfix/bugfix.scss @@ -0,0 +1,6 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ diff --git a/addons/website_livechat/static/src/bugfix/bugfix.xml b/addons/website_livechat/static/src/bugfix/bugfix.xml new file mode 100644 index 00000000..c17906f7 --- /dev/null +++ b/addons/website_livechat/static/src/bugfix/bugfix.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + +<!-- + This file allows introducing new static templates without contaminating other files. + This is useful when bug fixing requires adding new components for instance in stable + versions of Odoo. Any template that is defined in this file should be isolated + in its own file in master. +--> + +</templates> diff --git a/addons/website_livechat/static/src/bugfix/bugfix_tests.js b/addons/website_livechat/static/src/bugfix/bugfix_tests.js new file mode 100644 index 00000000..24179c31 --- /dev/null +++ b/addons/website_livechat/static/src/bugfix/bugfix_tests.js @@ -0,0 +1,18 @@ +odoo.define('website_livechat/static/src/bugfix/bugfix_tests.js', function (require) { +'use strict'; + +/** + * This file allows introducing new QUnit test modules without contaminating + * other test files. This is useful when bug fixing requires adding new + * components for instance in stable versions of Odoo. Any test that is defined + * in this file should be isolated in its own file in master. + */ +QUnit.module('website_livechat', {}, function () { +QUnit.module('bugfix', {}, function () { +QUnit.module('bugfix_tests.js', { + +}); +}); +}); + +}); diff --git a/addons/website_livechat/static/src/bugfix/public_bugfix.js b/addons/website_livechat/static/src/bugfix/public_bugfix.js new file mode 100644 index 00000000..759fe019 --- /dev/null +++ b/addons/website_livechat/static/src/bugfix/public_bugfix.js @@ -0,0 +1,25 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('website_livechat/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +const { LivechatButton } = require('im_livechat.legacy.im_livechat.im_livechat'); + +LivechatButton.include({ + className: `${LivechatButton.prototype.className} o_bottom_fixed_element`, + + /** + * @override + */ + start() { + // We trigger a resize to launch the event that checks if this element hides + // a button when the page is loaded. + $(window).trigger('resize'); + return this._super(...arguments); + }, +}); +}); diff --git a/addons/website_livechat/static/src/bugfix/public_bugfix.scss b/addons/website_livechat/static/src/bugfix/public_bugfix.scss new file mode 100644 index 00000000..fff015c2 --- /dev/null +++ b/addons/website_livechat/static/src/bugfix/public_bugfix.scss @@ -0,0 +1,14 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ + +.editor_has_snippets { + .o_livechat_button, .o_thread_window { + // TODO add this in an edit-mode only file in master (in 14.0 that asset + // would be website.assets_wysiwyg...) + right: $o-we-sidebar-width !important; + } +} diff --git a/addons/website_livechat/static/src/bugfix/public_bugfix.xml b/addons/website_livechat/static/src/bugfix/public_bugfix.xml new file mode 100644 index 00000000..c17906f7 --- /dev/null +++ b/addons/website_livechat/static/src/bugfix/public_bugfix.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + +<!-- + This file allows introducing new static templates without contaminating other files. + This is useful when bug fixing requires adding new components for instance in stable + versions of Odoo. Any template that is defined in this file should be isolated + in its own file in master. +--> + +</templates> diff --git a/addons/website_livechat/static/src/components/discuss/discuss.js b/addons/website_livechat/static/src/components/discuss/discuss.js new file mode 100644 index 00000000..778b1513 --- /dev/null +++ b/addons/website_livechat/static/src/components/discuss/discuss.js @@ -0,0 +1,31 @@ +odoo.define('website_livechat/static/src/components/discuss/discuss.js', function (require) { +'use strict'; + +const components = { + Discuss: require('mail/static/src/components/discuss/discuss.js'), + VisitorBanner: require('website_livechat/static/src/components/visitor_banner/visitor_banner.js'), +}; + +components.Discuss.patch('website_livechat/static/src/components/discuss/discuss.js', T => + class extends T { + + /** + * @override + */ + _useStoreSelector(props) { + const res = super._useStoreSelector(...arguments); + const thread = res.thread; + const visitor = thread && thread.visitor; + return Object.assign({}, res, { + visitor, + }); + } + + } +); + +Object.assign(components.Discuss.components, { + VisitorBanner: components.VisitorBanner, +}); + +}); diff --git a/addons/website_livechat/static/src/components/discuss/discuss.xml b/addons/website_livechat/static/src/components/discuss/discuss.xml new file mode 100644 index 00000000..af5a1469 --- /dev/null +++ b/addons/website_livechat/static/src/components/discuss/discuss.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-inherit="mail.Discuss.content" t-inherit-mode="extension"> + <xpath expr="//*[hasclass('o_Discuss_thread')]" position="before"> + <t t-if="discuss.thread.visitor"> + <VisitorBanner + visitorLocalId="discuss.thread.visitor.localId" + /> + </t> + </xpath> + </t> +</templates> diff --git a/addons/website_livechat/static/src/components/discuss/discuss_tests.js b/addons/website_livechat/static/src/components/discuss/discuss_tests.js new file mode 100644 index 00000000..153e55e4 --- /dev/null +++ b/addons/website_livechat/static/src/components/discuss/discuss_tests.js @@ -0,0 +1,269 @@ +odoo.define('website_livechat/static/src/components/discuss/discuss_tests.js', function (require) { +'use strict'; + +const { + afterEach, + beforeEach, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('website_livechat', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('rendering of visitor banner', async function (assert) { + assert.expect(13); + + this.data['res.country'].records.push({ + id: 11, + code: 'FAKE', + }); + this.data['website.visitor'].records.push({ + id: 11, + country_id: 11, + display_name: 'Visitor #11', + history: 'Home → Contact', + is_connected: true, + lang: "English", + website: "General website", + }); + this.data['mail.channel'].records.push({ + channel_type: 'livechat', + id: 11, + livechat_operator_id: this.data.currentPartnerId, + livechat_visitor_id: 11, + members: [this.data.currentPartnerId, this.data.publicPartnerId], + }); + await this.start({ + discuss: { + context: { + active_id: 'mail.channel_11', + }, + }, + }); + assert.containsOnce( + document.body, + '.o_VisitorBanner', + "should have a visitor banner", + ); + assert.containsOnce( + document.body, + '.o_VisitorBanner_avatar', + "should show the visitor avatar in the banner", + ); + assert.strictEqual( + document.querySelector('.o_VisitorBanner_avatar').dataset.src, + "/mail/static/src/img/smiley/avatar.jpg", + "should show the default avatar", + ); + assert.containsOnce( + document.body, + '.o_VisitorBanner_onlineStatusIcon', + "should show the visitor online status icon on the avatar", + ); + assert.strictEqual( + document.querySelector('.o_VisitorBanner_country').dataset.src, + "/base/static/img/country_flags/FAKE.png", + "should show the flag of the country of the visitor", + ); + assert.containsOnce( + document.body, + '.o_VisitorBanner_visitor', + "should show the visitor name in the banner", + ); + assert.strictEqual( + document.querySelector('.o_VisitorBanner_visitor').textContent, + "Visitor #11", + "should have 'Visitor #11' as visitor name", + ); + assert.containsOnce( + document.body, + '.o_VisitorBanner_language', + "should show the visitor language in the banner", + ); + assert.strictEqual( + document.querySelector('.o_VisitorBanner_language').textContent, + "English", + "should have 'English' as language of the visitor", + ); + assert.containsOnce( + document.body, + '.o_VisitorBanner_website', + "should show the visitor website in the banner", + ); + assert.strictEqual( + document.querySelector('.o_VisitorBanner_website').textContent, + "General website", + "should have 'General website' as website of the visitor", + ); + assert.containsOnce( + document.body, + '.o_VisitorBanner_history', + "should show the visitor history in the banner", + ); + assert.strictEqual( + document.querySelector('.o_VisitorBanner_history').textContent, + "Home → Contact", + "should have 'Home → Contact' as history of the visitor", + ); +}); + +QUnit.test('livechat with non-logged visitor should show visitor banner', async function (assert) { + assert.expect(1); + + this.data['res.country'].records.push({ + id: 11, + code: 'FAKE', + }); + this.data['website.visitor'].records.push({ + id: 11, + country_id: 11, + display_name: 'Visitor #11', + history: 'Home → Contact', + is_connected: true, + lang: "English", + website: "General website", + }); + this.data['mail.channel'].records.push({ + channel_type: 'livechat', + id: 11, + livechat_operator_id: this.data.currentPartnerId, + livechat_visitor_id: 11, + members: [this.data.currentPartnerId, this.data.publicPartnerId], + }); + await this.start({ + discuss: { + context: { + active_id: 'mail.channel_11', + }, + }, + }); + assert.containsOnce( + document.body, + '.o_VisitorBanner', + "should have a visitor banner", + ); +}); + +QUnit.test('livechat with logged visitor should show visitor banner', async function (assert) { + assert.expect(2); + + this.data['res.country'].records.push({ + id: 11, + code: 'FAKE', + }); + this.data['res.partner'].records.push({ + id: 12, + name: 'Partner Visitor', + }); + this.data['website.visitor'].records.push({ + id: 11, + country_id: 11, + display_name: 'Visitor #11', + history: 'Home → Contact', + is_connected: true, + lang: "English", + partner_id: 12, + website: "General website", + }); + this.data['mail.channel'].records.push({ + channel_type: 'livechat', + id: 11, + livechat_operator_id: this.data.currentPartnerId, + livechat_visitor_id: 11, + members: [this.data.currentPartnerId, 12], + }); + await this.start({ + discuss: { + context: { + active_id: 'mail.channel_11', + }, + }, + }); + assert.containsOnce( + document.body, + '.o_VisitorBanner', + "should have a visitor banner", + ); + assert.strictEqual( + document.querySelector('.o_VisitorBanner_visitor').textContent, + "Partner Visitor", + "should have partner name as display name of logged visitor on the visitor banner" + ); +}); + +QUnit.test('livechat without visitor should not show visitor banner', async function (assert) { + assert.expect(2); + + this.data['res.partner'].records.push({ id: 11 }); + this.data['mail.channel'].records.push({ + channel_type: 'livechat', + id: 11, + livechat_operator_id: this.data.currentPartnerId, + members: [this.data.currentPartnerId, 11], + }); + await this.start({ + discuss: { + context: { + active_id: 'mail.channel_11', + }, + }, + }); + assert.containsOnce( + document.body, + '.o_MessageList', + "should have a message list", + ); + assert.containsNone( + document.body, + '.o_VisitorBanner', + "should not have any visitor banner", + ); +}); + +QUnit.test('non-livechat channel should not show visitor banner', async function (assert) { + assert.expect(2); + + this.data['mail.channel'].records.push({ id: 11, name: "General" }); + await this.start({ + discuss: { + context: { + active_id: 'mail.channel_11', + }, + }, + }); + assert.containsOnce( + document.body, + '.o_MessageList', + "should have a message list", + ); + assert.containsNone( + document.body, + '.o_VisitorBanner', + "should not have any visitor banner", + ); +}); + +}); +}); +}); + +}); diff --git a/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.js b/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.js new file mode 100644 index 00000000..4d0b95a8 --- /dev/null +++ b/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.js @@ -0,0 +1,47 @@ +odoo.define('website_livechat/static/src/components/visitor_banner/visitor_banner.js', function (require) { +'use strict'; + +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; + +class VisitorBanner extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useStore(props => { + const visitor = this.env.models['website_livechat.visitor'].get(props.visitorLocalId); + const country = visitor && visitor.country; + return { + country: country && country.__state, + visitor: visitor ? visitor.__state : undefined, + }; + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {website_livechat.visitor} + */ + get visitor() { + return this.env.models['website_livechat.visitor'].get(this.props.visitorLocalId); + } + +} + +Object.assign(VisitorBanner, { + props: { + visitorLocalId: String, + }, + template: 'website_livechat.VisitorBanner', +}); + +return VisitorBanner; + +}); diff --git a/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.scss b/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.scss new file mode 100644 index 00000000..9f042d2a --- /dev/null +++ b/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.scss @@ -0,0 +1,90 @@ +// ----------------------------------------------------------------------------- +// Layout +// ----------------------------------------------------------------------------- + +.o_VisitorBanner { + border-bottom-width: $border-width; + border-bottom-style: solid; + display: flex; + flex: 0 0 auto; + padding: map-get($spacers, 4) map-get($spacers, 2); +} + +.o_VisitorBanner_avatar { + height: map-get($sizes, 100); + width: map-get($sizes, 100); + object-fit: cover; +} + +.o_VisitorBanner_avatarContainer { + height: $o-mail-thread-avatar-size; + width: $o-mail-thread-avatar-size; + margin-left: map-get($spacers, 1); + margin-right: map-get($spacers, 1); + position: relative; +} + +.o_VisitorBanner_country { + margin-inline-end: map-get($spacers, 1); +} + +.o_VisitorBanner_history { + margin-top: map-get($spacers, 1); +} + +.o_VisitorBanner_historyIcon { + margin-inline-end: map-get($spacers, 1); +} + +.o_VisitorBanner_language { + margin-inline-end: map-get($spacers, 3); +} + +.o_VisitorBanner_languageIcon { + margin-inline-end: map-get($spacers, 1); +} + +.o_VisitorBanner_onlineStatusIcon { + @include o-position-absolute($bottom: 0, $right: 0); + display: flex; + align-items: center; + justify-content: center; + flex-flow: column; + width: 1.2em; + height: 1.2em; + line-height: 1.3em; + font-size: x-small; +} + +.o_VisitorBanner_sidebar { + display: flex; + flex: 0 0 $o-mail-message-sidebar-width; + justify-content: center; + margin-inline-end: map-get($spacers, 2); + max-width: $o-mail-message-sidebar-width; +} + +.o_VisitorBanner_visitor { + margin-inline-end: map-get($spacers, 3); +} + +.o_VisitorBanner_websiteIcon { + margin-inline-end: map-get($spacers, 1); +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_VisitorBanner { + background: $white; + border-bottom-color: gray('400'); +} + +.o_VisitorBanner_onlineStatusIcon { + color: $o-enterprise-primary-color; +} + +.o_VisitorBanner_visitor { + font-weight: $font-weight-bold; +} diff --git a/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.xml b/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.xml new file mode 100644 index 00000000..b1dc4bd3 --- /dev/null +++ b/addons/website_livechat/static/src/components/visitor_banner/visitor_banner.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="website_livechat.VisitorBanner" owl="1"> + <div class="o_VisitorBanner"> + <div class="o_VisitorBanner_sidebar"> + <div class="o_VisitorBanner_avatarContainer"> + <img class="o_VisitorBanner_avatar rounded-circle" t-att-src="visitor.avatarUrl" alt="Avatar"/> + <t t-if="visitor.is_connected"> + <i class="o_VisitorBanner_onlineStatusIcon fa fa-circle" title="Online" role="img" aria-label="Visitor is online"/> + </t> + </div> + </div> + <div class="o_VisitorBanner_content"> + <t t-if="visitor.country"> + <img class="o_VisitorBanner_country o_country_flag" t-att-src="visitor.country.flagUrl" t-att-alt="visitor.country.code or visitor.country.name"/> + </t> + <span class="o_VisitorBanner_visitor" t-esc="visitor.nameOrDisplayName"/> + <span class="o_VisitorBanner_language"> + <i class="o_VisitorBanner_languageIcon fa fa-comment-o" aria-label="Lang"/> + <t t-esc="visitor.lang"/> + </span> + <t t-if="visitor.website"> + <span class="o_VisitorBanner_website"> + <i class="o_VisitorBanner_websiteIcon fa fa-globe" aria-label="Website"/> + <span t-esc="visitor.website"/> + </span> + </t> + <div class="o_VisitorBanner_history"> + <i class="o_VisitorBanner_historyIcon fa fa-history" aria-label="History"/> + <span t-esc="visitor.history"/> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/website_livechat/static/src/js/website_livechat.editor.js b/addons/website_livechat/static/src/js/website_livechat.editor.js new file mode 100644 index 00000000..288f2af0 --- /dev/null +++ b/addons/website_livechat/static/src/js/website_livechat.editor.js @@ -0,0 +1,51 @@ +odoo.define('website_livechat.editor', function (require) { +'use strict'; + +var core = require('web.core'); +var wUtils = require('website.utils'); +var WebsiteNewMenu = require('website.newMenu'); + +var _t = core._t; + +WebsiteNewMenu.include({ + actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { + new_channel: '_createNewChannel', + }), + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Asks the user information about a new channel to create, then creates it + * and redirects the user to this new channel. + * + * @private + * @returns {Promise} Unresolved if there is a redirection + */ + _createNewChannel: function () { + var self = this; + return wUtils.prompt({ + window_title: _t("New Channel"), + input: _t("Name"), + }).then(function (result) { + var name = result.val; + if (!name) { + return; + } + return self._rpc({ + model: 'im_livechat.channel', + method: 'create_and_get_website_url', + args: [[]], + kwargs: { + name: name, + }, + }).then(function (url) { + window.location.href = url; + return new Promise(function () {}); + }); + }); + }, +}); + +}); diff --git a/addons/website_livechat/static/src/legacy/public_livechat.js b/addons/website_livechat/static/src/legacy/public_livechat.js new file mode 100644 index 00000000..6bb15735 --- /dev/null +++ b/addons/website_livechat/static/src/legacy/public_livechat.js @@ -0,0 +1,47 @@ +odoo.define('website_livechat.legacy.website_livechat.livechat_request', function (require) { +"use strict"; + +var utils = require('web.utils'); +var session = require('web.session'); +var LivechatButton = require('im_livechat.legacy.im_livechat.im_livechat').LivechatButton; + + +LivechatButton.include({ + + /** + * @override + * Check if a chat request is opened for this visitor + * if yes, replace the session cookie and start the conversation immediately. + * Do this before calling super to have everything ready before executing existing start logic. + * This is used for chat request mechanism, when an operator send a chat request + * from backend to a website visitor. + */ + willStart: function () { + if (this.options.chat_request_session) { + utils.set_cookie('im_livechat_session', JSON.stringify(this.options.chat_request_session), 60*60); + } + return this._super(); + }, + + /** + * @override + * Called when the visitor closes the livechat chatter the first time (first click on X button) + * this will deactivate the mail_channel, clean the chat request if any + * and allow the operators to send the visitor a new chat request + */ + _onCloseChatWindow: function (ev) { + this._super(ev); + var cookie = utils.get_cookie('im_livechat_session'); + if (cookie) { + var channel = JSON.parse(cookie); + session.rpc('/im_livechat/visitor_leave_session', {uuid: channel.uuid}); + utils.set_cookie('im_livechat_session', "", -1); // remove cookie + } + }, +}); + +return { + LivechatButton: LivechatButton, +}; + +}); diff --git a/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js new file mode 100644 index 00000000..d43957ba --- /dev/null +++ b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js @@ -0,0 +1,32 @@ +odoo.define('website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js', function (require) { +'use strict'; + +const { registerInstancePatchModel } = require('mail/static/src/model/model_core.js'); + +registerInstancePatchModel('mail.messaging_notification_handler', 'website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js', { + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @override + */ + _handleNotificationPartner(data) { + const { info } = data; + if (info === 'send_chat_request') { + this._handleNotificationPartnerChannel(data); + const channel = this.env.models['mail.thread'].findFromIdentifyingData({ + id: data.id, + model: 'mail.channel', + }); + this.env.messaging.chatWindowManager.openThread(channel, { + makeActive: true, + }); + return; + } + return this._super(data); + }, +}); + +}); diff --git a/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js new file mode 100644 index 00000000..4130af64 --- /dev/null +++ b/addons/website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js @@ -0,0 +1,98 @@ +odoo.define('website_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler_tests.js', function (require) { +'use strict'; + +const { + afterEach, + afterNextRender, + beforeEach, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const FormView = require('web.FormView'); +const { + mock: { + intercept, + }, +} = require('web.test_utils'); + +QUnit.module('website_livechat', {}, function () { +QUnit.module('models', {}, function () { +QUnit.module('messaging_notification_handler', {}, function () { +QUnit.module('messaging_notification_handler_tests.js', { + beforeEach() { + beforeEach(this); + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, { + data: this.data, + }, params)); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('should open chat window on send chat request to website visitor', async function (assert) { + assert.expect(3); + + this.data['website.visitor'].records.push({ + id: 11, + name: "Visitor #11", + }); + await this.start({ + data: this.data, + hasChatWindow: true, + hasView: true, + // View params + View: FormView, + model: 'website.visitor', + arch: ` + <form> + <header> + <button name="action_send_chat_request" string="Send chat request" class="btn btn-primary" type="button"/> + </header> + <field name="name"/> + </form> + `, + res_id: 11, + }); + intercept(this.widget, 'execute_action', payload => { + this.env.services.rpc({ + route: '/web/dataset/call_button', + params: { + args: [payload.data.env.resIDs], + kwargs: { context: payload.data.env.context }, + method: payload.data.action_data.name, + model: payload.data.env.model, + } + }); + }); + + await afterNextRender(() => + document.querySelector('button[name="action_send_chat_request"]').click() + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "should have a chat window open after sending chat request to website visitor" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow'), + 'o-focused', + "chat window of livechat should be focused on open" + ); + assert.strictEqual( + document.querySelector('.o_ChatWindowHeader_name').textContent, + "Visitor #11", + "chat window of livechat should have name of visitor in the name" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/website_livechat/static/src/models/thread/thread.js b/addons/website_livechat/static/src/models/thread/thread.js new file mode 100644 index 00000000..46d80ca7 --- /dev/null +++ b/addons/website_livechat/static/src/models/thread/thread.js @@ -0,0 +1,45 @@ +odoo.define('website_livechat/static/src/models/thread/thread.js', function (require) { +'use strict'; + +const { + registerClassPatchModel, + registerFieldPatchModel, +} = require('mail/static/src/model/model_core.js'); +const { many2one } = require('mail/static/src/model/model_field.js'); + +registerClassPatchModel('mail.thread', 'website_livechat/static/src/models/thread/thread.js', { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + */ + convertData(data) { + const data2 = this._super(data); + if ('visitor' in data) { + if (data.visitor) { + data2.visitor = [[ + 'insert', + this.env.models['website_livechat.visitor'].convertData(data.visitor) + ]]; + } else { + data2.visitor = [['unlink']]; + } + } + return data2; + }, + +}); + +registerFieldPatchModel('mail.thread', 'website_livechat/static/src/models/thread/thread.js', { + /** + * Visitor connected to the livechat. + */ + visitor: many2one('website_livechat.visitor', { + inverse: 'threads', + }), +}); + +}); diff --git a/addons/website_livechat/static/src/models/visitor/visitor.js b/addons/website_livechat/static/src/models/visitor/visitor.js new file mode 100644 index 00000000..8c1b417e --- /dev/null +++ b/addons/website_livechat/static/src/models/visitor/visitor.js @@ -0,0 +1,169 @@ +odoo.define('website_livechat/static/src/models/partner/partner.js', function (require) { +'use strict'; + +const { registerNewModel } = require('mail/static/src/model/model_core.js'); +const { attr, many2one, one2many } = require('mail/static/src/model/model_field.js'); + +function factory(dependencies) { + + class Visitor extends dependencies['mail.model'] { + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + */ + static convertData(data) { + const data2 = {}; + if ('country_id' in data) { + if (data.country_id) { + data2.country = [['insert', { + id: data.country_id, + code: data.country_code, + }]]; + } else { + data2.country = [['unlink']]; + } + } + if ('history' in data) { + data2.history = data.history; + } + if ('is_connected' in data) { + data2.is_connected = data.is_connected; + } + if ('lang' in data) { + data2.lang = data.lang; + } + if ('name' in data) { + data2.name = data.name; + } + if ('partner_id' in data) { + if (data.partner_id) { + data2.partner = [['insert', { id: data.partner_id }]]; + } else { + data2.partner = [['unlink']]; + } + } + if ('website' in data) { + data2.website = data.website; + } + return data2; + } + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @private + * @returns {string} + */ + _computeAvatarUrl() { + if (!this.partner) { + return '/mail/static/src/img/smiley/avatar.jpg'; + } + return this.partner.avatarUrl; + } + + /** + * @private + * @returns {mail.country} + */ + _computeCountry() { + if (this.partner && this.partner.country) { + return [['link', this.partner.country]]; + } + if (this.country) { + return [['link', this.country]]; + } + return [['unlink']]; + } + + /** + * @private + * @returns {string} + */ + _computeNameOrDisplayName() { + if (this.partner) { + return this.partner.nameOrDisplayName; + } + return this.name; + } + } + + Visitor.fields = { + /** + * Url to the avatar of the visitor. + */ + avatarUrl: attr({ + compute: '_computeAvatarUrl', + dependencies: [ + 'partner', + 'partnerAvatarUrl', + ], + }), + /** + * Country of the visitor. + */ + country: many2one('mail.country', { + compute: '_computeCountry', + dependencies: [ + 'country', + 'partnerCountry', + ], + }), + /** + * Browsing history of the visitor as a string. + */ + history: attr(), + /** + * Determine whether the visitor is connected or not. + */ + is_connected: attr(), + /** + * Name of the language of the visitor. (Ex: "English") + */ + lang: attr(), + /** + * Name of the visitor. + */ + name: attr(), + nameOrDisplayName: attr({ + compute: '_computeNameOrDisplayName', + dependencies: [ + 'name', + 'partnerNameOrDisplayName', + ], + }), + /** + * Partner linked to this visitor, if any. + */ + partner: many2one('mail.partner'), + partnerAvatarUrl: attr({ + related: 'partner.avatarUrl', + }), + partnerCountry: many2one('mail.country',{ + related: 'partner.country', + }), + partnerNameOrDisplayName: attr({related: 'partner.nameOrDisplayName'}), + /** + * Threads with this visitor as member + */ + threads: one2many('mail.thread', { + inverse: 'visitor', + }), + /** + * Name of the website on which the visitor is connected. (Ex: "Website 1") + */ + website: attr(), + }; + + Visitor.modelName = 'website_livechat.visitor'; + + return Visitor; +} + +registerNewModel('website_livechat.visitor', factory); + +}); diff --git a/addons/website_livechat/static/tests/helpers/mock_models.js b/addons/website_livechat/static/tests/helpers/mock_models.js new file mode 100644 index 00000000..bc407959 --- /dev/null +++ b/addons/website_livechat/static/tests/helpers/mock_models.js @@ -0,0 +1,45 @@ +odoo.define('website_livechat/static/tests/helpers/mock_models.js', function (require) { +'use strict'; + +const MockModels = require('mail/static/tests/helpers/mock_models.js'); + +MockModels.patch('website_livechat/static/tests/helpers/mock_models.js', T => + class extends T { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + */ + static generateData() { + const data = super.generateData(...arguments); + Object.assign(data, { + 'website.visitor': { + fields: { + country_id: { string: "Country", type: 'many2one', relation: 'res.country' }, + display_name: { string: "Display name", type: 'string' }, + // Represent the browsing history of the visitor as a string. + // To ease testing this allows tests to set it directly instead + // of implementing the computation made on server. + // This should normally not be a field. + history: { string: "History", type: 'string'}, + is_connected: { string: "Is connected", type: 'boolean' }, + lang: { string: "Language", type: 'string'}, + partner_id: {string: "partner", type: "many2one", relation: 'res.partner'}, + website: { string: "Website", type: 'string' }, + }, + records: [], + }, + }); + Object.assign(data['mail.channel'].fields, { + livechat_visitor_id: { string: "Visitor", type: 'many2one', relation: 'website.visitor' }, + }); + return data; + } + + } +); + +}); diff --git a/addons/website_livechat/static/tests/helpers/mock_server.js b/addons/website_livechat/static/tests/helpers/mock_server.js new file mode 100644 index 00000000..1383e521 --- /dev/null +++ b/addons/website_livechat/static/tests/helpers/mock_server.js @@ -0,0 +1,78 @@ +odoo.define('website_livechat/static/tests/helpers/mock_server.js', function (require) { +'use strict'; + +require('im_livechat/static/tests/helpers/mock_server.js'); // ensure mail overrides are applied first + +const MockServer = require('web.MockServer'); + +MockServer.include({ + /** + * Simulate a 'call_button' operation from a view. + * + * @override + */ + _mockCallButton({ args, kwargs, method, model }) { + if (model === 'website.visitor' && method === 'action_send_chat_request') { + return this._mockWebsiteVisitorActionSendChatRequest(args[0]); + } + return this._super(...arguments); + }, + /** + * Overrides to add visitor information to livechat channels. + * + * @override + */ + _mockMailChannelChannelInfo(ids, extra_info) { + const channelInfos = this._super(...arguments); + for (const channelInfo of channelInfos) { + const channel = this._getRecords('mail.channel', [['id', '=', channelInfo.id]])[0]; + if (channel.channel_type === 'livechat' && channelInfo.livechat_visitor_id) { + const visitor = this._getRecords('website.visitor', [['id', '=', channelInfo.livechat_visitor_id]])[0]; + const country = this._getRecords('res.country', [['id', '=', visitor.country_id]])[0]; + channelInfo.visitor = { + name: visitor.display_name, + country_code: country && country.code, + country_id: country && country.id, + is_connected: visitor.is_connected, + history: visitor.history, // TODO should be computed + website: visitor.website, + lang: visitor.lang, + partner_id: visitor.partner_id, + } + } + } + return channelInfos; + }, + /** + * @private + * @param {integer[]} ids + */ + _mockWebsiteVisitorActionSendChatRequest(ids) { + const visitors = this._getRecords('website.visitor', [['id', 'in', ids]]); + for (const visitor of visitors) { + const country = visitor.country_id + ? this._getRecords('res.country', [['id', '=', visitor.country_id]]) + : undefined; + const visitor_name = `${visitor.display_name}${country ? `(${country.name})` : ''}`; + const members = [this.currentPartnerId]; + if (visitor.partner_id) { + members.push(visitor.partner_id); + } else { + members.push(this.publicPartnerId); + } + const livechatId = this._mockCreate('mail.channel', { + anonymous_name: visitor_name, + channel_type: 'livechat', + livechat_operator_id: this.currentPartnerId, + members, + public: 'private', + }); + // notify operator + const channelInfo = this._mockMailChannelChannelInfo([livechatId], 'send_chat_request')[0]; + const notification = [[false, 'res.partner', this.currentPartnerId], channelInfo]; + this._widget.call('bus_service', 'trigger', 'notification', [notification]); + } + }, +}); + +}); diff --git a/addons/website_livechat/static/tests/tours/website_livechat_common.js b/addons/website_livechat/static/tests/tours/website_livechat_common.js new file mode 100644 index 00000000..6568a82f --- /dev/null +++ b/addons/website_livechat/static/tests/tours/website_livechat_common.js @@ -0,0 +1,165 @@ +odoo.define('website_livechat.tour_common', function(require) { +'use strict'; + +var session = require('web.session'); +var LivechatButton = require('im_livechat.legacy.im_livechat.im_livechat').LivechatButton; + +/** + * Alter this method for test purposes. + * + * Fake the notification after sending message + * As bus is not available, it's necessary to add the message in the chatter + in livechat.messages + * + * Add a class to the chatter window after sendFeedback is done + * to force the test to wait until feedback is really done + * (to check afterwards if the livechat session is set to inactive) + * + * Note : this asset is loaded for tests only (rpc call done only during tests) + */ +LivechatButton.include({ + _sendMessage: function (message) { + var self = this; + return this._super.apply(this, arguments).then(function () { + if (message.isFeedback) { + $('div.o_thread_window_header').addClass('feedback_sent'); + } + else { + session.rpc('/bus/test_mode_activated', {}).then(function (in_test_mode) { + if (in_test_mode) { + var notification = [ + self._livechat.getUUID(), + { + 'id': -1, + 'author_id': [0, 'Website Visitor Test'], + 'email_from': 'Website Visitor Test', + 'body': '<p>' + message.content + '</p>', + 'is_discussion': true, + 'subtype_id': [1, "Discussions"], + 'date': moment().format('YYYY-MM-DD HH:mm:ss'), + } + ] + self._handleNotification(notification); + } + }); + } + }); + }, +}); + +/******************************* +* Common Steps +*******************************/ + +var startStep = [{ + content: "click on livechat widget", + trigger: "div.o_livechat_button" +}, { + content: "Say hello!", + trigger: "input.o_composer_text_field", + run: "text Hello Sir!" +}, { + content: "Send the message", + trigger: "input.o_composer_text_field", + run: function() { + $('input.o_composer_text_field').trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); + } +}, { + content: "Verify your message has been typed", + trigger: "div.o_thread_message_content>p:contains('Hello Sir!')" +}, { + content: "Verify there is no duplicates", + trigger: "body", + run: function () { + if ($("div.o_thread_message_content p:contains('Hello Sir!')").length === 1) { + $('body').addClass('no_duplicated_message'); + } + } +}, { + content: "Is your message correctly sent ?", + trigger: 'body.no_duplicated_message' +}]; + +var endDiscussionStep = [{ + content: "Close the chatter", + trigger: "a.o_thread_window_close", + run: function() { + $('a.o_thread_window_close').click(); + } +}]; + +var feedbackStep = [{ + content: "Type a feedback", + trigger: "div.o_livechat_rating_reason > textarea", + run: "text ;-) This was really helpful. Thanks ;-)!" +}, { + content: "Send the feedback", + trigger: "input[type='button'].o_rating_submit_button", +}, { + content: "Check if feedback has been sent", + trigger: "div.o_thread_window_header.feedback_sent", +}, { + content: "Thanks for your feedback", + trigger: "div.o_livechat_rating_box:has(div:contains('Thank you for your feedback'))", +}]; + +var transcriptStep = [{ + content: "Type your email", + trigger: "input[id='o_email']", + run: "text deboul@onner.com" +}, { + content: "Send the conversation to your email address", + trigger: "button.o_email_chat_button", +}, { + content: "Type your email", + trigger: "div.o_livechat_email:has(strong:contains('Conversation Sent'))", +}]; + +var closeStep = [{ + content: "Close the conversation with the x button", + trigger: "a.o_thread_window_close", +}, { + content: "Check that the chat window is closed", + trigger: 'body', + run: function () { + if ($('div.o_livechat_button').length === 1 && !$('div.o_livechat_button').is(':visible')) { + $('body').addClass('tour_success'); + } + } +}, { + content: "Is the Test succeded ?", + trigger: 'body.tour_success' +}]; + +var goodRatingStep = [{ + content: "Send Good Rating", + trigger: "div.o_livechat_rating_choices > img[data-value=5]", +}, { + content: "Check if feedback has been sent", + trigger: "div.o_thread_window_header.feedback_sent", +}, { + content: "Thanks for your feedback", + trigger: "div.o_livechat_rating_box:has(div:contains('Thank you for your feedback'))" +}]; + +var okRatingStep = [{ + content: "Send ok Rating", + trigger: "div.o_livechat_rating_choices > img[data-value=3]", +}]; + +var sadRatingStep = [{ + content: "Send bad Rating", + trigger: "div.o_livechat_rating_choices > img[data-value=1]", +}]; + +return { + 'startStep': startStep, + 'endDiscussionStep': endDiscussionStep, + 'transcriptStep': transcriptStep, + 'feedbackStep': feedbackStep, + 'closeStep': closeStep, + 'goodRatingStep': goodRatingStep, + 'okRatingStep': okRatingStep, + 'sadRatingStep': sadRatingStep, +}; + +}); diff --git a/addons/website_livechat/static/tests/tours/website_livechat_rating.js b/addons/website_livechat/static/tests/tours/website_livechat_rating.js new file mode 100644 index 00000000..d3339f2b --- /dev/null +++ b/addons/website_livechat/static/tests/tours/website_livechat_rating.js @@ -0,0 +1,38 @@ +odoo.define('website_livechat.tour', function(require) { +'use strict'; + +var commonSteps = require("website_livechat.tour_common"); +var tour = require("web_tour.tour"); + +tour.register('website_livechat_complete_flow_tour', { + test: true, + url: '/', +}, [].concat(commonSteps.startStep, commonSteps.endDiscussionStep, commonSteps.okRatingStep, commonSteps.feedbackStep, commonSteps.transcriptStep, commonSteps.closeStep)); + +tour.register('website_livechat_happy_rating_tour', { + test: true, + url: '/', +}, [].concat(commonSteps.startStep, commonSteps.endDiscussionStep, commonSteps.goodRatingStep)); + +tour.register('website_livechat_ok_rating_tour', { + test: true, + url: '/', +}, [].concat(commonSteps.startStep, commonSteps.endDiscussionStep, commonSteps.okRatingStep, commonSteps.feedbackStep)); + +tour.register('website_livechat_sad_rating_tour', { + test: true, + url: '/', +}, [].concat(commonSteps.startStep, commonSteps.endDiscussionStep, commonSteps.sadRatingStep, commonSteps.feedbackStep)); + +tour.register('website_livechat_no_rating_tour', { + test: true, + url: '/', +}, [].concat(commonSteps.startStep, commonSteps.endDiscussionStep, commonSteps.transcriptStep, commonSteps.closeStep)); + +tour.register('website_livechat_no_rating_no_close_tour', { + test: true, + url: '/', +}, [].concat(commonSteps.startStep)); + +return {}; +}); diff --git a/addons/website_livechat/static/tests/tours/website_livechat_request.js b/addons/website_livechat/static/tests/tours/website_livechat_request.js new file mode 100644 index 00000000..8c4042e3 --- /dev/null +++ b/addons/website_livechat/static/tests/tours/website_livechat_request.js @@ -0,0 +1,46 @@ +odoo.define('website_livechat.chat_request_tour', function(require) { +'use strict'; + +var commonSteps = require("website_livechat.tour_common"); +var tour = require("web_tour.tour"); + + +var stepWithChatRequestStep = [{ + content: "Answer the chat request!", + trigger: "input.o_composer_text_field", + run: "text Hi ! What a coincidence! I need your help indeed." +}, { + content: "Send the message", + trigger: "input.o_composer_text_field", + run: function() { + $('input.o_composer_text_field').trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); + } +}, { + content: "Verify your message has been typed", + trigger: "div.o_thread_message_content>p:contains('Hi ! What a coincidence! I need your help indeed.')" +}, { + content: "Verify there is no duplicates", + trigger: "body", + run: function () { + if ($("div.o_thread_message_content p:contains('Hi ! What a coincidence! I need your help indeed.')").length === 1) { + $('body').addClass('no_duplicated_message'); + } + } +}, { + content: "Is your message correctly sent ?", + trigger: 'body.no_duplicated_message' +}]; + + +tour.register('website_livechat_chat_request_part_1_no_close_tour', { + test: true, + url: '/', +}, [].concat(stepWithChatRequestStep)); + +tour.register('website_livechat_chat_request_part_2_end_session_tour', { + test: true, + url: '/', +}, [].concat(commonSteps.endDiscussionStep, commonSteps.okRatingStep, commonSteps.feedbackStep, commonSteps.transcriptStep, commonSteps.closeStep)); + +return {}; +}); |
