[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:
@@ -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>
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user