[IMP] sms, mass_mailing_sms : port sms widget field to OWL

This ports the `sms_widget` field to OWL.

closes odoo/odoo#101350

X-original-commit: 89109de6d3752388f0b2f976c8a32588f760ec48
Related: odoo/enterprise#31882
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Antoine Guenet
2022-09-28 09:38:56 +02:00
parent 57a7b6064e
commit b78385c02d
4 changed files with 109 additions and 176 deletions
@@ -120,7 +120,7 @@
<page string="SMS Content" name="sms_body" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">
<field name="body_plaintext" widget="sms_widget"
attrs="{'required': [('mailing_type', '=', 'sms')], 'readonly': [('state', 'in', ('sending', 'done'))]}"
options='{"enable_emojis": True, "dynamic_placeholder": true}'/>
options="{'dynamic_placeholder': true}" enable_emojis="true"/>
</page>
</xpath>
<xpath expr="//page[@name='settings']/group/group[@name='email_content']" position="after">
@@ -1,39 +1,33 @@
/** @odoo-module **/
import { _t } from 'web.core';
import basic_fields from 'web.basic_fields';
import fieldRegistry from 'web.field_registry';
import EmojisTextField from '@mail/views/fields/emojis_text_field';
import { patch } from "@web/core/utils/patch";
import { EmojisTextField} from '@mail/views/fields/emojis_text_field/emojis_text_field';
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
var DynamicPlaceholderFieldMixin = basic_fields.DynamicPlaceholderFieldMixin;
const DynamicPlaceholderFieldMixin = basic_fields.DynamicPlaceholderFieldMixin;
/**
* SmsWidget is a widget to display a textarea (the body) and a text representing
* the number of SMS and the number of characters. This text is computed every
* time the user changes the body.
*/
var SmsWidget = EmojisTextField.extend(DynamicPlaceholderFieldMixin, {
className: 'o_field_text',
enableEmojis: false,
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this.nbrChar = 0;
this.nbrSMS = 0;
this.encoding = 'GSM7';
this.enableEmojis = !!this.nodeOptions.enable_emojis;
},
export class SmsWidget extends EmojisTextField {
setup() {
super.setup();
this.notification = useService('notification');
}
/**
* @override
*"This will add the emoji dropdown to a target field (controlled by the "enableEmojis" attribute)
*/
on_attach_callback: function () {
if (this.enableEmojis) {
this._super.apply(this, arguments);
}
},
get encoding() {
return this._extractEncoding(this.props.value || '');
}
get nbrChar() {
const content = this.props.value || '';
return content.length + (content.match(/\n/g) || []).length;
}
get nbrSMS() {
return this._countSMS(this.nbrChar, this.encoding);
}
/**
* Open a Model Field Selector in order to select fields
@@ -45,67 +39,35 @@ var SmsWidget = EmojisTextField.extend(DynamicPlaceholderFieldMixin, {
* @param {Array} chain
*
*/
openDynamicPlaceholder: async function (baseModel, chain = []) {
async openDynamicPlaceholder(baseModel, chain = []) {
const modelSelector = await this._openNewModelSelector(baseModel, chain);
modelSelector.$el.css('margin-top', 4);
},
//--------------------------------------------------------------------------
// Private: override widget
//--------------------------------------------------------------------------
/**
* @private
* @override
*/
_renderEdit: function () {
var def = this._super.apply(this, arguments);
this._compute();
var $sms_container = $('<div class="o_sms_container"/>');
$sms_container.append(this._renderSMSInfo());
$sms_container.append(this._renderIAPButton());
this.$el = this.$el.add($sms_container);
return def;
},
}
//--------------------------------------------------------------------------
// Private: SMS
//--------------------------------------------------------------------------
/**
* Compute the number of characters and sms
* @private
*/
_compute: function () {
var content = this._getValue();
this.encoding = this._extractEncoding(content);
this.nbrChar = content.length;
this.nbrChar += (content.match(/\n/g) || []).length;
this.nbrSMS = this._countSMS(this.nbrChar, this.encoding);
},
/**
* Count the number of SMS of the content
* @private
* @returns {integer} Number of SMS
*/
_countSMS: function () {
if (this.nbrChar === 0) {
_countSMS(nbrChar, encoding) {
if (nbrChar === 0) {
return 0;
}
if (this.encoding === 'UNICODE') {
if (this.nbrChar <= 70) {
if (encoding === 'UNICODE') {
if (nbrChar <= 70) {
return 1;
}
return Math.ceil(this.nbrChar / 67);
return Math.ceil(nbrChar / 67);
}
if (this.nbrChar <= 160) {
if (nbrChar <= 160) {
return 1;
}
return Math.ceil(this.nbrChar / 153);
},
return Math.ceil(nbrChar / 153);
}
/**
* Extract the encoding depending on the characters in the content
@@ -113,49 +75,12 @@ var SmsWidget = EmojisTextField.extend(DynamicPlaceholderFieldMixin, {
* @param {String} content Content of the SMS
* @returns {String} Encoding of the content (GSM7 or UNICODE)
*/
_extractEncoding: function (content) {
_extractEncoding(content) {
if (String(content).match(RegExp("^[@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà]*$"))) {
return 'GSM7';
}
return 'UNICODE';
},
/**
* Render the IAP button to redirect to IAP pricing
* @private
*/
_renderIAPButton: function () {
return $('<a>', {
'href': 'https://iap-services.odoo.com/iap/sms/pricing',
'target': '_blank',
'title': _t('SMS Pricing'),
'aria-label': _t('SMS Pricing'),
'class': 'fa fa-lg fa-info-circle',
});
},
/**
* Render the number of characters, sms and the encoding.
* @private
*/
_renderSMSInfo: function () {
var string = _.str.sprintf(_t('%s characters, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding);
var $span = $('<span>', {
'class': 'text-muted o_sms_count',
});
$span.text(string);
return $span;
},
/**
* Update widget SMS information with re-computed info about length, ...
* @private
*/
_updateSMSInfo: function () {
this._compute();
var string = _.str.sprintf(_t('%s characters, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding);
this.$('.o_sms_count').text(string);
},
}
//--------------------------------------------------------------------------
// Handlers
@@ -165,43 +90,34 @@ var SmsWidget = EmojisTextField.extend(DynamicPlaceholderFieldMixin, {
* @override
* @private
*/
_onBlur: function () {
var content = this._getValue();
async onBlur() {
var content = this.props.value || '';
if( !content.trim().length && content.length > 0) {
this.displayNotification({ title: _t("Your SMS Text Message must include at least one non-whitespace character"), type: 'danger' });
this.$input.val(content.trim());
this._updateSMSInfo();
this.notification.add(
this.env._t("Your SMS Text Message must include at least one non-whitespace character"),
{ type: 'danger' },
)
await this.props.update(content.trim());
}
},
}
/**
* @override
* @private
*/
_onChange: function () {
this._super.apply(this, arguments);
this._updateSMSInfo();
},
/**
* @override
* @private
*/
_onInput: function (ev) {
this._super.apply(this, arguments);
async onInput(ev) {
await this.props.update(this.targetEditElement.el.value);
super.onInput(...arguments);
const key = ev.originalEvent ? ev.originalEvent.data : '';
if (this.nodeOptions &&
this.nodeOptions.dynamic_placeholder &&
key === this.DYNAMIC_PLACEHOLDER_TRIGGER_KEY) {
if (this.props.dynamicPlaceholder && key === this.DYNAMIC_PLACEHOLDER_TRIGGER_KEY) {
const baseModel = this.recordData && this.recordData.mailing_model_real ? this.recordData.mailing_model_real : undefined;
if (baseModel) {
this.openDynamicPlaceholder(baseModel);
}
}
this._updateSMSInfo();
},
});
fieldRegistry.add('sms_widget', SmsWidget);
export default SmsWidget;
}
};
SmsWidget.template = 'sms.SmsWidget';
SmsWidget.additionalClasses = [...(EmojisTextField.additionalClasses || []), 'o_field_text'];
patch(SmsWidget.prototype, 'sms_widget_dynamic_placeholder_field_mixin', DynamicPlaceholderFieldMixin);
registry.category("fields").add("sms_widget", SmsWidget);
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="sms.SmsWidget" t-inherit="mail.EmojisTextField" t-inherit-mode="primary" owl="1">
<xpath expr="//textarea[1]" position="attributes">
<attribute name="t-on-blur">onBlur</attribute>
</xpath>
<xpath expr="/*[last()]/*[last()]" position="after">
<div class="o_sms_container">
<span class="text-muted o_sms_count">
<t t-out="nbrChar"/> characters, fits in <t t-out="nbrSMS"/> SMS (<t t-out="encoding"/>)
<a href="https://iap-services.odoo.com/iap/sms/pricing" target="_blank"
title="SMS Pricing" aria-label="SMS Pricing" class="fa fa-lg fa-info-circle"/>
</span>
</div>
</xpath>
</t>
</templates>
+40 -41
View File
@@ -3,8 +3,10 @@
import FormView from 'web.FormView';
import ListView from 'web.ListView';
import testUtils from 'web.test_utils';
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { getFixture } from "@web/../tests/helpers/utils";
var createView = testUtils.createView;
const createView = testUtils.createView;
QUnit.module('fields', {
beforeEach: function () {
@@ -36,6 +38,8 @@ QUnit.module('fields', {
}]
},
};
setupViewRegistries();
this.target = getFixture();
}
}, function () {
@@ -43,84 +47,79 @@ QUnit.module('fields', {
QUnit.test('Sms widgets are correctly rendered', async function (assert) {
assert.expect(9);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><sheet><field name="message" widget="sms_widget"/></sheet></form>',
await makeView({
type: "form",
resModel: "partner",
serverData: { models: this.data },
arch: /* xml */ `<form><sheet><field name="message" widget="sms_widget"/></sheet></form>`,
});
assert.containsOnce(form, '.o_sms_count', "Should have a sms counter");
assert.strictEqual(form.$('.o_sms_count').text(), '0 characters, fits in 0 SMS (GSM7) ',
assert.containsOnce(this.target, '.o_sms_count', "Should have a sms counter");
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '0 characters, fits in 0 SMS (GSM7) ',
'Should be "0 characters, fits in 0 SMS (GSM7) " by default');
// GSM-7
await testUtils.fields.editAndTrigger(form.$('.o_input'), "Hello from Odoo", 'input');
assert.strictEqual(form.$('.o_sms_count').text(), '15 characters, fits in 1 SMS (GSM7) ',
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), "Hello from Odoo", 'input');
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '15 characters, fits in 1 SMS (GSM7) ',
'Should be "15 characters, fits in 1 SMS (GSM7) " for "Hello from Odoo"');
// GSM-7 with \n => this one count as 2 characters
form.$('.o_input').val("Hello from Odoo\n").trigger('input');
assert.strictEqual(form.$('.o_sms_count').text(), '17 characters, fits in 1 SMS (GSM7) ',
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), "Hello from Odoo\n", 'input');
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '17 characters, fits in 1 SMS (GSM7) ',
'Should be "17 characters, fits in 1 SMS (GSM7) " for "Hello from Odoo\\n"');
// Unicode => ê
form.$('.o_input').val("Hêllo from Odoo").trigger('input');
assert.strictEqual(form.$('.o_sms_count').text(), '15 characters, fits in 1 SMS (UNICODE) ',
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), "Hêllo from Odoo", 'input');
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '15 characters, fits in 1 SMS (UNICODE) ',
'Should be "15 characters, fits in 1 SMS (UNICODE) " for "Hêllo from Odoo"');
// GSM-7 with 160c
var text = Array(161).join('a');
await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
assert.strictEqual(form.$('.o_sms_count').text(), '160 characters, fits in 1 SMS (GSM7) ',
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '160 characters, fits in 1 SMS (GSM7) ',
'Should be "160 characters, fits in 1 SMS (GSM7) " for 160 x "a"');
// GSM-7 with 161c
text = Array(162).join('a');
await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
assert.strictEqual(form.$('.o_sms_count').text(), '161 characters, fits in 2 SMS (GSM7) ',
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '161 characters, fits in 2 SMS (GSM7) ',
'Should be "161 characters, fits in 2 SMS (GSM7) " for 161 x "a"');
// Unicode with 70c
text = Array(71).join('ê');
await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
assert.strictEqual(form.$('.o_sms_count').text(), '70 characters, fits in 1 SMS (UNICODE) ',
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '70 characters, fits in 1 SMS (UNICODE) ',
'Should be "70 characters, fits in 1 SMS (UNICODE) " for 70 x "ê"');
// Unicode with 71c
text = Array(72).join('ê');
await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
assert.strictEqual(form.$('.o_sms_count').text(), '71 characters, fits in 2 SMS (UNICODE) ',
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '71 characters, fits in 2 SMS (UNICODE) ',
'Should be "71 characters, fits in 2 SMS (UNICODE) " for 71 x "ê"');
form.destroy();
});
QUnit.test('Sms widgets with non-empty initial value', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'visitor',
data: this.data,
arch: `<form><sheet><field name="mobile" widget="sms_widget"/></sheet></form>`,
res_id: 1,
res_ids: [1],
await makeView({
type: "form",
resModel: "visitor",
resId: 1,
serverData: { models: this.data },
arch: /* xml */ `<form><sheet><field name="mobile" widget="sms_widget" readonly="true"/></sheet></form>`,
});
assert.strictEqual(form.$('.o_field_text').text(), '+32494444444',
assert.strictEqual(this.target.querySelector('.o_field_text span').textContent, '+32494444444',
'Should have the initial value');
form.destroy();
});
QUnit.test('Sms widgets with empty initial value', async function (assert) {
assert.expect(1);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form><sheet><field name="message" widget="sms_widget"/></sheet></form>`,
res_id: 1,
res_ids: [1],
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData: { models: this.data },
arch: /* xml */ `<form><sheet><field name="message" widget="sms_widget" readonly="true"/></sheet></form>`,
});
assert.strictEqual(form.$('.o_field_text').text(), '',
assert.strictEqual(this.target.querySelector('.o_field_text span').textContent, '',
'Should have the empty initial value');
form.destroy();
});
QUnit.module('PhoneWidget');