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/pad/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/pad/static')
| -rw-r--r-- | addons/pad/static/plugin/ep_disable_init_focus/README.md | 28 | ||||
| -rw-r--r-- | addons/pad/static/plugin/ep_disable_init_focus/ep.json | 10 | ||||
| -rw-r--r-- | addons/pad/static/plugin/ep_disable_init_focus/package.json | 15 | ||||
| -rw-r--r-- | addons/pad/static/plugin/ep_disable_init_focus/static/js/disable_init_focus.js | 8 | ||||
| -rw-r--r-- | addons/pad/static/src/css/etherpad.css | 129 | ||||
| -rw-r--r-- | addons/pad/static/src/img/pad_link_companies.jpeg | bin | 0 -> 70544 bytes | |||
| -rw-r--r-- | addons/pad/static/src/js/pad.js | 193 | ||||
| -rw-r--r-- | addons/pad/static/src/xml/pad.xml | 21 | ||||
| -rw-r--r-- | addons/pad/static/tests/pad_tests.js | 310 |
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 Binary files differnew file mode 100644 index 00000000..1bce3a56 --- /dev/null +++ b/addons/pad/static/src/img/pad_link_companies.jpeg 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; + }); + +}); |
