summaryrefslogtreecommitdiff
path: root/addons/pad/static
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/pad/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/pad/static')
-rw-r--r--addons/pad/static/plugin/ep_disable_init_focus/README.md28
-rw-r--r--addons/pad/static/plugin/ep_disable_init_focus/ep.json10
-rw-r--r--addons/pad/static/plugin/ep_disable_init_focus/package.json15
-rw-r--r--addons/pad/static/plugin/ep_disable_init_focus/static/js/disable_init_focus.js8
-rw-r--r--addons/pad/static/src/css/etherpad.css129
-rw-r--r--addons/pad/static/src/img/pad_link_companies.jpegbin0 -> 70544 bytes
-rw-r--r--addons/pad/static/src/js/pad.js193
-rw-r--r--addons/pad/static/src/xml/pad.xml21
-rw-r--r--addons/pad/static/tests/pad_tests.js310
9 files changed, 714 insertions, 0 deletions
diff --git a/addons/pad/static/plugin/ep_disable_init_focus/README.md b/addons/pad/static/plugin/ep_disable_init_focus/README.md
new file mode 100644
index 00000000..49a904a2
--- /dev/null
+++ b/addons/pad/static/plugin/ep_disable_init_focus/README.md
@@ -0,0 +1,28 @@
+Readme
+======
+
+`ep_disable_init_focus` is a very simple
+[Etherpad-lite](https://github.com/ether/etherpad-lite) plugin, which disable
+the focus on the pad content after its loading.
+
+Rationale
+---------
+
+Etherpad-lite autofocus can be annoying to end-users when it is used in Odoo's
+"pad" widget, because it will override web client focus rules. This plugin is
+design to get rid of this behavior.
+
+
+Installation instructions
+-------------------------
+
+1. Stop your Etherpad-lite server.
+2. Copy the `ep_disabl_init_focus` folder into the `node_modules` folder of
+ your Etherpad-lite installation.
+3. in the folder of your Etherpad-lite installation, run this command to
+ install the plugin:
+
+ ```sh
+ npm install node_modules/ep_disable_init_focus/
+ ```
+4. Restart the Etherpad-lite server.
diff --git a/addons/pad/static/plugin/ep_disable_init_focus/ep.json b/addons/pad/static/plugin/ep_disable_init_focus/ep.json
new file mode 100644
index 00000000..547cd8a2
--- /dev/null
+++ b/addons/pad/static/plugin/ep_disable_init_focus/ep.json
@@ -0,0 +1,10 @@
+{
+ "parts":[
+ {
+ "name":"ep_disable_init_focus",
+ "client_hooks":{
+ "aceEditEvent":"ep_disable_init_focus/static/js/disable_init_focus"
+ }
+ }
+ ]
+}
diff --git a/addons/pad/static/plugin/ep_disable_init_focus/package.json b/addons/pad/static/plugin/ep_disable_init_focus/package.json
new file mode 100644
index 00000000..e6f307d1
--- /dev/null
+++ b/addons/pad/static/plugin/ep_disable_init_focus/package.json
@@ -0,0 +1,15 @@
+{
+ "name":"ep_disable_init_focus",
+ "version":"0.0.1",
+ "description":"Disables init focus in etherpad-lite.",
+ "dependencies":{
+
+ },
+ "engines":{
+ "node":"*"
+ },
+ "author":{
+ "name":"Odoo S.A. - Hitesh Trivedi",
+ "email":"thiteshm155@gmail.com"
+ }
+}
diff --git a/addons/pad/static/plugin/ep_disable_init_focus/static/js/disable_init_focus.js b/addons/pad/static/plugin/ep_disable_init_focus/static/js/disable_init_focus.js
new file mode 100644
index 00000000..62f6a553
--- /dev/null
+++ b/addons/pad/static/plugin/ep_disable_init_focus/static/js/disable_init_focus.js
@@ -0,0 +1,8 @@
+exports.aceEditEvent = function(hook, call, editorInfo, rep, documentAttributeManager){
+
+ call.editorInfo.ace_focus = focus;
+ function focus(){
+ // Simple hook to disable the focus on the pad
+ }
+
+};
diff --git a/addons/pad/static/src/css/etherpad.css b/addons/pad/static/src/css/etherpad.css
new file mode 100644
index 00000000..a0157bb3
--- /dev/null
+++ b/addons/pad/static/src/css/etherpad.css
@@ -0,0 +1,129 @@
+
+.oe_pad_switch_positioner {
+ position: relative;
+}
+
+.oe_pad_switch {
+ position: absolute;
+ top: 5px;
+ left: 383px;
+ width: 28px;
+ height: 28px;
+ background-image: -webkit-linear-gradient(top, white, #f0f0f0);
+ border: solid 1px #ccc;
+ border-radius:3px;
+ text-align: center;
+ line-height: 28px;
+ overflow: hidden;
+ -webkit-box-sizing: border-box;
+ color: #666666;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+@media (max-width: 767px) {
+ .oe_pad_switch {
+ left: auto;
+ right: 5px;
+ }
+ html .o_scroll_hidden {
+ overflow: hidden;
+ }
+}
+
+.oe_pad_switch:hover{
+ background-image: -webkit-linear-gradient(top, #f4f4f4, #e4e4e4);
+}
+
+.oe_pad_fullscreen .oe_pad_switch {
+ top:4px;
+}
+
+.oe_pad_fullscreen {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ background-color: white;
+ margin:0;
+ padding:0;
+ border:none;
+ z-index: 1001;
+}
+
+.oe_pad .oe_pad_content.oe_editing{
+ border: solid 1px #c4c4c4;
+ height:500px;
+ -webkit-box-shadow: 0 5px 10px rgba(0,0,0,0.1);
+ -moz-box-shadow: 0 5px 10px rgba(0,0,0,0.1);
+ -ms-box-shadow: 0 5px 10px rgba(0,0,0,0.1);
+ -o-box-shadow: 0 5px 10px rgba(0,0,0,0.1);
+ box-shadow: 0 5px 10px rgba(0,0,0,0.1);
+}
+
+.oe_pad.oe_pad_fullscreen .oe_pad_content {
+ height: 100%;
+ border: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ -ms-box-shadow: none;
+ -o-box-shadow: none;
+ box-shadow: none;
+}
+
+.oe_pad .oe_unconfigured {
+ text-align: center;
+ opacity: 0.75;
+}
+
+.oe_pad_loading{
+ text-align: center;
+ opacity: 0.75;
+ font-style: italic;
+}
+
+.etherpad_readonly ul, .etherpad_readonly ol {
+ margin: 0;
+ margin-left: 1.5em;
+ padding: 0;
+}
+.etherpad_readonly ul li{
+ list-style-type: disc;
+}
+.etherpad_readonly ol li{
+ list-style-type: decimal;
+}
+.etherpad_readonly .indent li{
+ list-style-type: none !important;
+}
+
+.etherpad_readonly{
+ font-family: arial, sans-serif;
+ font-size: 15px;
+ line-height: 19px;
+ word-wrap: break-word;
+}
+
+.openerp .oe_form_nomargin .etherpad_readonly{
+ padding: 10px;
+}
+
+.etherpad_readonly ul.indent { list-style-type: none !important; }
+.etherpad_readonly ol li{ list-style-type: decimal !important; }
+.etherpad_readonly ol ol li{ list-style-type: lower-latin !important; }
+.etherpad_readonly ol ol ol li{ list-style-type: lower-roman !important; }
+.etherpad_readonly ol ol ol ol li{ list-style-type: decimal !important; }
+.etherpad_readonly ol ol ol ol ol li{ list-style-type: lower-latin !important; }
+.etherpad_readonly ol ol ol ol ol ol li{ list-style-type: lower-roman !important; }
+.etherpad_readonly ol ol ol ol ol ol ol li{ list-style-type: decimal !important; }
+.etherpad_readonly ol ol ol ol ol ol ol ol li{ list-style-type: lower-latin !important; }
+.etherpad_readonly ul li { list-style-type: disc !important; }
+.etherpad_readonly ul ul li { list-style-type: circle !important; }
+.etherpad_readonly ul ul ul li { list-style-type: square !important; }
+.etherpad_readonly ul ul ul ul li { list-style-type: disc !important; }
+.etherpad_readonly ul ul ul ul ul li { list-style-type: circle !important; }
+.etherpad_readonly ul ul ul ul ul ul li { list-style-type: square !important; }
+.etherpad_readonly ul ul ul ul ul ul ul li { list-style-type: disc !important; }
+.etherpad_readonly ul ul ul ul ul ul ul ul li { list-style-type: circle !important; }
+.etherpad_readonly ul ul ul ul ul ul ul ul ul li { list-style-type: square !important; }
diff --git a/addons/pad/static/src/img/pad_link_companies.jpeg b/addons/pad/static/src/img/pad_link_companies.jpeg
new file mode 100644
index 00000000..1bce3a56
--- /dev/null
+++ b/addons/pad/static/src/img/pad_link_companies.jpeg
Binary files differ
diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js
new file mode 100644
index 00000000..7107c2d6
--- /dev/null
+++ b/addons/pad/static/src/js/pad.js
@@ -0,0 +1,193 @@
+odoo.define('pad.pad', function (require) {
+"use strict";
+
+var AbstractField = require('web.AbstractField');
+var core = require('web.core');
+var fieldRegistry = require('web.field_registry');
+
+var _t = core._t;
+
+var FieldPad = AbstractField.extend({
+ template: 'FieldPad',
+ content: "",
+ events: {
+ 'click .oe_pad_switch': '_onToggleFullScreen',
+ },
+
+ /**
+ * @override
+ */
+ willStart: function () {
+ if (this.isPadConfigured === undefined) {
+ return this._rpc({
+ method: 'pad_is_configured',
+ model: this.model,
+ }).then(function (result) {
+ // we write on the prototype to share the information between
+ // all pad widgets instances, across all actions
+ FieldPad.prototype.isPadConfigured = result;
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ if (!this.isPadConfigured) {
+ this.$(".oe_unconfigured").removeClass('d-none');
+ this.$(".oe_configured").addClass('d-none');
+ return Promise.resolve();
+ }
+ if (this.mode === 'edit' && typeof(this.value) === 'object') {
+ this.value = this.value.toJSON();
+ }
+ if (this.mode === 'edit' && _.str.startsWith(this.value, 'http')) {
+ this.url = this.value;
+ // please close your eyes and look elsewhere...
+ // Since the pad value (the url) will not change during the edition
+ // process, we have a problem: the description field will not be
+ // properly updated. We need to explicitely write the value each
+ // time someone edit the record in order to force the server to read
+ // the updated value of the pad and put it in the description field.
+ //
+ // However, the basic model optimizes away the changes if they are
+ // not really different from the current value. So, we need to
+ // either add special configuration options to the basic model, or
+ // to trick him into accepting the same value as being different...
+ // Guess what we decided...
+ var url = {};
+ url.toJSON = _.constant(this.url);
+ this._setValue(url, {doNotSetDirty: true});
+ }
+
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * If we had to generate an url, we wait for the generation to be completed,
+ * so the current record will be associated with the correct pad url.
+ *
+ * @override
+ */
+ commitChanges: function () {
+ return this.urlDef;
+ },
+ /**
+ * @override
+ */
+ isSet: function () {
+ return true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Note that this method has some serious side effects: performing rpcs and
+ * setting the value of this field. This is not conventional and should not
+ * be copied in other code, unless really necessary.
+ *
+ * @override
+ * @private
+ */
+ _renderEdit: function () {
+ if (this.url) {
+ // here, we have a valid url, so we can simply display an iframe
+ // with the correct src attribute
+ var userName = encodeURIComponent(this.getSession().name);
+ var url = this.url + '?showChat=false&userName=' + userName;
+ var content = '<iframe width="100%" height="100%" frameborder="0" src="' + url + '"></iframe>';
+ this.$('.oe_pad_content').html(content);
+ } else if (this.value) {
+ // it looks like the field does not contain a valid url, so we just
+ // display it (it cannot be edited in that case)
+ this.$('.oe_pad_content').text(this.value);
+ } else {
+ // It is totally discouraged to have a render method that does
+ // non-rendering work, especially since the work in question
+ // involves doing RPCs and changing the value of the field.
+ // However, this is kind of necessary in this case, because the
+ // value of the field is actually only the url of the pad. The
+ // actual content will be loaded in an iframe. We could do this
+ // work in the basic model, but the basic model does not know that
+ // this widget is in edit or readonly, and we really do not want to
+ // create a pad url everytime a task without a pad is viewed.
+ var self = this;
+ this.urlDef = this._rpc({
+ method: 'pad_generate_url',
+ model: this.model,
+ context: {
+ model: this.model,
+ field_name: this.name,
+ object_id: this.res_id,
+ record: this.recordData,
+ },
+ }, {
+ shadow: true
+ }).then(function (result) {
+ // We need to write the url of the pad to trigger
+ // the write function which updates the actual value
+ // of the field to the value of the pad content
+ self.url = result.url;
+ self._setValue(result.url, {doNotSetDirty: true});
+ });
+ }
+ },
+ /**
+ * @override
+ * @private
+ */
+ _renderReadonly: function () {
+ if (_.str.startsWith(this.value, 'http')) {
+ var self = this;
+ this.$('.oe_pad_content')
+ .addClass('oe_pad_loading')
+ .text(_t("Loading"));
+ this._rpc({
+ method: 'pad_get_content',
+ model: this.model,
+ args: [this.value]
+ }, {
+ shadow: true
+ }).then(function (data) {
+ self.$('.oe_pad_content')
+ .removeClass('oe_pad_loading')
+ .html('<div class="oe_pad_readonly"><div>');
+ self.$('.oe_pad_readonly').html(data);
+ }).guardedCatch(function () {
+ self.$('.oe_pad_content').text(_t('Unable to load pad'));
+ });
+ } else {
+ this.$('.oe_pad_content')
+ .addClass('oe_pad_loading')
+ .show()
+ .text(_t("This pad will be initialized on first edit"));
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _onToggleFullScreen: function () {
+ this.$el.toggleClass('oe_pad_fullscreen mb0');
+ this.$('.oe_pad_switch').toggleClass('fa-expand fa-compress');
+ this.$el.parents('.o_touch_device').toggleClass('o_scroll_hidden');
+ },
+});
+
+fieldRegistry.add('pad', FieldPad);
+
+return FieldPad;
+
+});
diff --git a/addons/pad/static/src/xml/pad.xml b/addons/pad/static/src/xml/pad.xml
new file mode 100644
index 00000000..44eb11c8
--- /dev/null
+++ b/addons/pad/static/src/xml/pad.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="FieldPad">
+ <div class="oe_form_field_text oe_pad">
+ <p class="oe_unconfigured d-none">
+ Please, enter your Etherpad credentials through the Settings.
+ </p>
+ <t t-if="widget.mode === 'readonly'">
+ <div class="oe_pad_content etherpad_readonly oe_configured" />
+ </t>
+ <t t-if="widget.mode === 'edit'">
+ <div class="oe_pad_switch_positioner oe_configured">
+ <span class="fa fa-expand oe_pad_switch" role="img" aria-label="Switch pad" title="Switch pad"/>
+ </div>
+ <div class="oe_pad_content oe_editing oe_configured" />
+ </t>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js
new file mode 100644
index 00000000..c1195001
--- /dev/null
+++ b/addons/pad/static/tests/pad_tests.js
@@ -0,0 +1,310 @@
+odoo.define('pad.pad_tests', function (require) {
+"use strict";
+
+var FieldPad = require('pad.pad');
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('pad widget', {
+ beforeEach: function () {
+ this.data = {
+ task: {
+ fields: {
+ description: {string: "Description", type: "char"},
+ use_pad: {string: "Use pad", type: "boolean"},
+ },
+ records: [
+ {id: 1, description: false},
+ {id: 2, description: "https://pad.odoo.pad/p/test-03AK6RCJT"},
+ ],
+ pad_is_configured: function () {
+ return true;
+ },
+ pad_generate_url: function (route, args) {
+ return {
+ url:'https://pad.odoo.pad/p/test/' + args.context.object_id
+ };
+ },
+ pad_get_content: function () {
+ return "we should rewrite this server in haskell";
+ },
+ },
+ };
+ },
+});
+
+ QUnit.test('pad widget display help if server not configured', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'pad_is_configured') {
+ return Promise.resolve(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.isVisible(form.$('p.oe_unconfigured'),
+ "help message should be visible");
+ assert.containsNone(form, 'p.oe_pad_content',
+ "content should not be displayed");
+ await testUtils.form.clickEdit(form);
+ assert.isVisible(form.$('p.oe_unconfigured'),
+ "help message should be visible");
+ assert.containsNone(form, 'p.oe_pad_content',
+ "content should not be displayed");
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
+ QUnit.test('pad widget works, basic case', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === 'https://pad.odoo.pad/p/test/1?showChat=false&userName=batman') {
+ assert.ok(true, "should have an iframe with correct src");
+ return Promise.resolve(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ name: "batman",
+ },
+ });
+ assert.isNotVisible(form.$('p.oe_unconfigured'),
+ "help message should not be visible");
+ assert.isVisible(form.$('.oe_pad_content'),
+ "content should be visible");
+ assert.containsOnce(form, '.oe_pad_content:contains(This pad will be)',
+ "content should display a message when not initialized");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.oe_pad_content iframe',
+ "should have an iframe");
+
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
+ QUnit.test('pad widget works, with existing data', async function (assert) {
+ assert.expect(3);
+
+ var contentDef = testUtils.makeTestPromise();
+
+ var form = await createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (_.str.startsWith(route, 'http')) {
+ return Promise.resolve(true);
+ }
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'pad_get_content') {
+ return contentDef.then(_.constant(result));
+ }
+ if (args.method === 'write') {
+ assert.ok('description' in args.args[1],
+ "should always send the description value");
+ }
+ return result;
+ },
+ session: {
+ name: "batman",
+ },
+ });
+ assert.strictEqual(form.$('.oe_pad_content').text(), "Loading",
+ "should display loading message");
+ contentDef.resolve();
+ await testUtils.nextTick();
+ assert.strictEqual(form.$('.oe_pad_content').text(), "we should rewrite this server in haskell",
+ "should display proper value");
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
+ QUnit.test('pad widget is not considered dirty at creation', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (!args.method) {
+ return Promise.resolve(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ name: "batman",
+ },
+ });
+ var def = form.canBeDiscarded();
+ var defState = 'unresolved';
+ def.then(function () {
+ defState = 'resolved';
+ });
+
+ assert.strictEqual($('.modal').length, 0,
+ "should have no confirmation modal opened");
+ await testUtils.nextTick();
+ assert.strictEqual(defState, 'resolved',
+ "can be discarded was successfully resolved");
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
+ QUnit.test('pad widget is not considered dirty at edition', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (!args.method) {
+ return Promise.resolve(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ name: "batman",
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ var def = form.canBeDiscarded();
+ var defState = 'unresolved';
+ def.then(function () {
+ defState = 'resolved';
+ });
+
+ assert.strictEqual($('.modal').length, 0,
+ "should have no confirmation modal opened");
+ await testUtils.nextTick();
+ assert.strictEqual(defState, 'resolved',
+ "can be discarded was successfully resolved");
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
+ QUnit.test('record should be discarded properly even if only pad has changed', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (!args.method) {
+ return Promise.resolve(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ name: "batman",
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ await testUtils.form.clickDiscard(form);
+ assert.strictEqual(form.$('.oe_pad_readonly').text(), this.data.task.pad_get_content(),
+ "pad content should not have changed");
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
+ QUnit.test('no pad deadlock on form change modifying pad readonly modifier', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="use_pad" widget="toggle_button"/>' +
+ '<field name="description" widget="pad" attrs="{\'readonly\': [(\'use_pad\', \'=\', False)]}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (!args.method) {
+ return Promise.resolve(true);
+ }
+ if (args.method === "write") {
+ assert.strictEqual(args.args[1].description,
+ "https://pad.odoo.pad/p/test-03AK6RCJT");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_widget[name="use_pad"]'));
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
+
+});