summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src
diff options
context:
space:
mode:
authorAndrea Ulliana <aul@odoo.com>2019-01-22 14:48:44 +0000
committerqsm-odoo <qsm@odoo.com>2019-02-05 14:28:29 +0000
commit515e7bc50f07c983eb932825d763753978346e66 (patch)
tree65dc17a032f13dd1318c88d63fdc566fe4f6a8d4 /addons/web_editor/static/src
parent7f2ce89554e3675f553d2658fc0a04cd5facf088 (diff)
[IMP] website, *: improve CSS edition usability + add JS edition
* web_editor Currently, when an user wants to edit the css of its website, he has access to lots of files and, in debug mode, to even more files. While the feature is nice for people who know what they are doing, a lambda person can easily break its website by changing the wrong files. Also, nothing warns the user that the files that are edited will never receive updates anymore (unless if they are reset). The goal is to prevent those behavior while still allowing the add custom css. Another goal is to be able to *add* custom javascript. Custom files created for the user: SCSS: user_custom_rules.scss, user_custom_bootstrap_overridden.scss JS: user_custom_javascript.js See https://github.com/odoo/odoo/pull/29999 task-1919350
Diffstat (limited to 'addons/web_editor/static/src')
-rw-r--r--addons/web_editor/static/src/js/common/ace.js201
-rw-r--r--addons/web_editor/static/src/xml/ace.xml41
2 files changed, 159 insertions, 83 deletions
diff --git a/addons/web_editor/static/src/js/common/ace.js b/addons/web_editor/static/src/js/common/ace.js
index 6216633e424..f231bb0aac6 100644
--- a/addons/web_editor/static/src/js/common/ace.js
+++ b/addons/web_editor/static/src/js/common/ace.js
@@ -119,20 +119,22 @@ var ViewEditor = Widget.extend({
jsLibs: [
'/web/static/lib/ace/ace.js',
[
+ '/web/static/lib/ace/javascript_highlight_rules.js',
'/web/static/lib/ace/mode-xml.js',
'/web/static/lib/ace/mode-scss.js',
+ '/web/static/lib/ace/mode-js.js',
'/web/static/lib/ace/theme-monokai.js'
]
],
events: {
'click .o_ace_type_switcher_choice': '_onTypeChoice',
'change .o_res_list': '_onResChange',
- 'click .js_include_bundles': '_onIncludeBundlesChange',
- 'click .js_include_all_scss': '_onIncludeAllSCSSChange',
+ 'click .o_ace_filter': '_onFilterChange',
'click button[data-action=save]': '_onSaveClick',
'click button[data-action=reset]': '_onResetClick',
'click button[data-action=format]': '_onFormatClick',
'click button[data-action=close]': '_onCloseClick',
+ 'click #ace-view-id > .alert-warning .close': '_onCloseWarningClick'
},
/**
@@ -150,8 +152,9 @@ var ViewEditor = Widget.extend({
* @param {string} [options.position=right]
* @param {boolean} [options.doNotLoadViews=false]
* @param {boolean} [options.doNotLoadSCSS=false]
+ * @param {boolean} [options.doNotLoadJS=false]
* @param {boolean} [options.includeBundles=false]
- * @param {boolean} [options.includeAllSCSS=false]
+ * @param {string} [options.filesFilter=custom]
* @param {string[]} [options.defaultBundlesRestriction]
*/
init: function (parent, viewKey, options) {
@@ -164,18 +167,20 @@ var ViewEditor = Widget.extend({
position: 'right',
doNotLoadViews: false,
doNotLoadSCSS: false,
+ doNotLoadJS: false,
includeBundles: false,
- includeAllSCSS: false,
+ filesFilter: 'custom',
defaultBundlesRestriction: [],
});
- this.resources = {xml: {}, scss: {}};
- this.editingSessions = {xml: {}, scss: {}};
+ this.resources = {xml: {}, scss: {}, js: {}};
+ this.editingSessions = {xml: {}, scss: {}, js: {}};
this.currentType = 'xml';
// Alias
this.views = this.resources.xml;
this.scss = this.resources.scss;
+ this.js = this.resources.js;
},
/**
* Loads everything the ace library needs to work.
@@ -204,11 +209,13 @@ var ViewEditor = Widget.extend({
this.$lists = {
xml: this.$('#ace-view-list'),
- scss: this.$('#ace-scss-list')
+ scss: this.$('#ace-scss-list'),
+ js: this.$('#ace-js-list'),
};
this.$includeBundlesArea = this.$('.oe_include_bundles');
this.$includeAllSCSSArea = this.$('.o_include_all_scss');
this.$viewID = this.$('#ace-view-id > span');
+ this.$warningMessage = this.$('#ace-view-id > .alert-warning');
this.$formatButton = this.$('button[data-action=format]');
this.$resetButton = this.$('button[data-action=reset]');
@@ -228,12 +235,24 @@ var ViewEditor = Widget.extend({
var initType;
if (this.options.initialResID) {
initResID = this.options.initialResID;
- initType = (_.isString(initResID) && initResID[0] === '/') ? 'scss' : 'xml';
+ if (_.isString(initResID) && initResID[0] === '/') {
+ if (_.str.endsWith(initResID, '.scss')) {
+ initType = 'scss';
+ } else {
+ initType = 'js';
+ }
+ } else {
+ initType = 'xml';
+ }
} else {
if (!this.options.doNotLoadSCSS) {
initResID = this.sortedSCSS[0][1][0].url; // first bundle, scss files, first one
initType = 'scss';
}
+ if (!this.options.doNotLoadJS) {
+ initResID = this.sortedJS[0][1][0].url; // first bundle, js files, first one
+ initType = 'js';
+ }
if (!this.options.doNotLoadViews) {
if (typeof this.viewKey === "number") {
initResID = this.viewKey;
@@ -321,7 +340,7 @@ var ViewEditor = Widget.extend({
* Initializes a text editor for the specified resource.
*
* @private
- * @param {integer|string} resID - the ID/URL of the view/scss file
+ * @param {integer|string} resID - the ID/URL of the view/scss/js file
* @param {string} [type] (default to the currently selected one)
* @returns {ace.EditSession}
*/
@@ -341,13 +360,13 @@ var ViewEditor = Widget.extend({
return editingSession;
},
/**
- * Forces the view/scss file identified by its ID/URL to be displayed in the
+ * Forces the view/scss/js file identified by its ID/URL to be displayed in the
* editor. The method will update the resource select DOM element as well if
* necessary.
*
* @private
* @param {integer|string} resID
- * @param {string} [type] - the type of resource (either 'xml' or 'scss')
+ * @param {string} [type] - the type of resource (either 'xml', 'scss' or 'js')
*/
_displayResource: function (resID, type) {
if (type) {
@@ -360,14 +379,29 @@ var ViewEditor = Widget.extend({
}
this.aceEditor.setSession(editingSession);
+ var isCustomized = false;
if (this.currentType === 'xml') {
this.$viewID.text(_.str.sprintf(_t("Template ID: %s"), this.views[resID].key));
- } else {
+ } else if (this.currentType === 'scss') {
+ isCustomized = this.scss[resID].customized;
this.$viewID.text(_.str.sprintf(_t("SCSS file: %s"), resID));
+ } else {
+ isCustomized = this.js[resID].customized;
+ this.$viewID.text(_.str.sprintf(_t("JS file: %s"), resID));
}
this.$lists[this.currentType].select2('val', resID);
- this.$resetButton.toggleClass('d-none', this.currentType === 'xml' || !this.scss[resID].customized);
+ this.$resetButton.toggleClass('d-none', this.currentType === 'xml' || !isCustomized);
+
+ // TODO the warning message is always shown for XML templates but:
+ // 1) We have to implement a way to be able to reset XML templates
+ // otherwise the warning message is not accurate
+ // 2) We should be able to detect if the XML template is customized to
+ // not show the warning in that case
+ this.$warningMessage.toggleClass('d-none',
+ this.currentType !== 'xml' && (resID.indexOf('/user_custom_') >= 0 || isCustomized));
+
+ this.aceEditor.resize(true);
},
/**
* Formats the current resource being vizualized.
@@ -403,10 +437,11 @@ var ViewEditor = Widget.extend({
*/
_loadResources: function () {
// Reset resources
- this.resources = {xml: {}, scss: {}};
- this.editingSessions = {xml: {}, scss: {}};
+ this.resources = {xml: {}, scss: {}, js: {}};
+ this.editingSessions = {xml: {}, scss: {}, js: {}};
this.views = this.resources.xml;
this.scss = this.resources.scss;
+ this.js = this.resources.js;
// Load resources
return this._rpc({
@@ -415,12 +450,15 @@ var ViewEditor = Widget.extend({
key: this.viewKey,
get_views: !this.options.doNotLoadViews,
get_scss: !this.options.doNotLoadSCSS,
+ get_js: !this.options.doNotLoadJS,
bundles: this.options.includeBundles,
- bundles_restriction: this.options.includeAllSCSS ? [] : this.options.defaultBundlesRestriction,
+ bundles_restriction: this.options.filesFilter === 'all' ? [] : this.options.defaultBundlesRestriction,
+ only_user_custom_files: this.options.filesFilter === 'custom',
},
}).then((function (resources) {
_processViews.call(this, resources.views || []);
- _processSCSS.call(this, resources.scss || []);
+ _processJSorSCSS.call(this, resources.scss || [], 'scss');
+ _processJSorSCSS.call(this, resources.js || [], 'js');
}).bind(this));
function _processViews(views) {
@@ -461,20 +499,24 @@ var ViewEditor = Widget.extend({
});
}
- function _processSCSS(scss) {
- // The received scss data is already sorted by bundle and DOM order
- this.sortedSCSS = scss;
+ function _processJSorSCSS(data, type) {
+ // The received scss or js data is already sorted by bundle and DOM order
+ if (type === 'scss') {
+ this.sortedSCSS = data;
+ } else {
+ this.sortedJS = data;
+ }
// Store the URL ungrouped by bundle and use the URL as key (resource ID)
- var self = this;
- _.each(scss, function (bundleInfos) {
+ var resources = type === 'scss' ? this.scss : this.js;
+ _.each(data, function (bundleInfos) {
_.each(bundleInfos[1], function (info) { info.bundle_xmlid = bundleInfos[0].xmlid; });
- _.extend(self.scss, _.indexBy(bundleInfos[1], 'url'));
+ _.extend(resources, _.indexBy(bundleInfos[1], 'url'));
});
}
},
/**
- * Forces the view/scss file identified by its ID/URL to be reset to the way
+ * Forces the view/scss/js file identified by its ID/URL to be reset to the way
* it was before the user started editing it.
*
* @todo views reset is not supported yet
@@ -491,37 +533,42 @@ var ViewEditor = Widget.extend({
if (this.currentType === 'xml') {
return $.Defered().reject(_t("Reseting views is not supported yet"));
} else {
+ var resource = type === 'scss' ? this.scss[resID] : this.js[resID];
return this._rpc({
- route: '/web_editor/reset_scss',
+ route: '/web_editor/reset_scss_or_js',
params: {
url: resID,
- bundle_xmlid: this.scss[resID].bundle_xmlid,
+ bundle_xmlid: resource.bundle_xmlid,
},
});
}
},
/**
- * Saves an unique SCSS file.
+ * Saves an unique SCSS or JS file.
*
* @private
* @param {Object} session - contains the 'id' (url) and the 'text' of the
- * SCSS file to save.
+ * SCSS or JS file to save.
* @return {Deferred} status indicates if the save is finished or if an
* error occured.
*/
- _saveSCSS: function (session) {
+ _saveSCSSorJS: function (session) {
var def = $.Deferred();
var self = this;
- this._rpc({
- route: '/web_editor/save_scss',
+ var sessionIdEndsWithJS = _.string.endsWith(session.id, '.js');
+ var bundleXmlID = sessionIdEndsWithJS ? this.js[session.id].bundle_xmlid : this.scss[session.id].bundle_xmlid;
+ var fileType = sessionIdEndsWithJS ? 'js' : 'scss';
+ self._rpc({
+ route: '/web_editor/save_scss_or_js',
params: {
url: session.id,
- bundle_xmlid: this.scss[session.id].bundle_xmlid,
+ bundle_xmlid: bundleXmlID,
content: session.text,
+ file_type: fileType,
},
}).then(function () {
- self._toggleDirtyInfo(session.id, 'scss', false);
+ self._toggleDirtyInfo(session.id, fileType, false);
def.resolve();
}, function (source, error) {
def.reject(session, error);
@@ -572,7 +619,7 @@ var ViewEditor = Widget.extend({
_toSave = _.sortBy(_toSave, 'id').reverse();
_.each(_toSave, function (session) {
defs.push(mutex.exec(function () {
- return (type === 'xml' ? self._saveView(session) : self._saveSCSS(session));
+ return (type === 'xml' ? self._saveView(session) : self._saveSCSSorJS(session));
}));
});
}).bind(this));
@@ -665,11 +712,11 @@ var ViewEditor = Widget.extend({
}
},
/**
- * Switches to the SCSS or XML edition. Calling this method will adapt all
+ * Switches to the SCSS, XML or JS edition. Calling this method will adapt all
* DOM elements to keep the editor consistent.
*
* @private
- * @param {string} type - either 'xml' or 'scss'
+ * @param {string} type - either 'xml', 'scss' or 'js'
*/
_switchType: function (type) {
this.currentType = type;
@@ -677,9 +724,10 @@ var ViewEditor = Widget.extend({
_.each(this.$lists, function ($list, _type) { $list.toggleClass('d-none', type !== _type); });
this.$lists[type].change();
- this.$includeBundlesArea.toggleClass('d-none', this.currentType === 'scss' || !session.debug);
- this.$includeAllSCSSArea.toggleClass('d-none', this.currentType === 'xml' || !session.debug || this.options.defaultBundlesRestriction.length === 0);
- this.$formatButton.toggleClass('d-none', this.currentType === 'scss');
+ this.$includeBundlesArea.toggleClass('d-none', this.currentType !== 'xml' || !session.debug);
+ this.$includeAllSCSSArea.toggleClass('d-none', this.currentType !== 'scss' || !session.debug);
+ this.$includeAllSCSSArea.find('[data-value="restricted"]').toggleClass('d-none', this.options.defaultBundlesRestriction.length === 0);
+ this.$formatButton.toggleClass('d-none', this.currentType !== 'xml');
},
/**
* Updates the select option DOM element associated with a particular resID
@@ -724,21 +772,10 @@ var ViewEditor = Widget.extend({
});
this.$lists.scss.empty();
- _.each(this.sortedSCSS, function (bundleInfos) {
- var $optgroup = $('<optgroup/>', {
- label: bundleInfos[0].name,
- }).appendTo(self.$lists.scss);
- _.each(bundleInfos[1], function (scssInfo) {
- var name = scssInfo.url.substring(_.lastIndexOf(scssInfo.url, '/') + 1, scssInfo.url.length - 5);
- $optgroup.append($('<option/>', {
- value: scssInfo.url,
- text: name,
- selected: currentId === scssInfo.url,
- 'data-debug': scssInfo.url,
- 'data-customized': scssInfo.customized
- }));
- });
- });
+ _populateList(this.sortedSCSS, this.$lists.scss, 5);
+
+ this.$lists.js.empty();
+ _populateList(this.sortedJS, this.$lists.js, 3);
this.$lists.xml.select2('destroy');
this.$lists.xml.select2({
@@ -752,6 +789,30 @@ var ViewEditor = Widget.extend({
formatSelection: _formatDisplay.bind(this, true),
});
this.$lists.scss.data('select2').dropdown.addClass('o_ace_select2_dropdown');
+ this.$lists.js.select2('destroy');
+ this.$lists.js.select2({
+ formatResult: _formatDisplay.bind(this, false),
+ formatSelection: _formatDisplay.bind(this, true),
+ });
+ this.$lists.js.data('select2').dropdown.addClass('o_ace_select2_dropdown');
+
+ function _populateList(sortedData, $list, lettersToRemove) {
+ _.each(sortedData, function (bundleInfos) {
+ var $optgroup = $('<optgroup/>', {
+ label: bundleInfos[0].name,
+ }).appendTo($list);
+ _.each(bundleInfos[1], function (dataInfo) {
+ var name = dataInfo.url.substring(_.lastIndexOf(dataInfo.url, '/') + 1, dataInfo.url.length - lettersToRemove);
+ $optgroup.append($('<option/>', {
+ value: dataInfo.url,
+ text: name,
+ selected: currentId === dataInfo.url,
+ 'data-debug': dataInfo.url,
+ 'data-customized': dataInfo.customized
+ }));
+ });
+ });
+ }
function _formatDisplay(isSelected, data) {
var $elem = $(data.element);
@@ -803,25 +864,20 @@ var ViewEditor = Widget.extend({
this._formatResource();
},
/**
- * Called when the checkbox which indicates if all SCSS should be included
- * is toggled -> reloads the resources accordingly.
+ * Called when a filter dropdown item is cliked. Reload the resources
+ * according to the new filter and make it visually active.
*
* @private
* @param {Event} ev
*/
- _onIncludeAllSCSSChange: function (ev) {
- this.options.includeAllSCSS = $(ev.target).prop('checked');
- this._loadResources().then(this._updateViewSelectDOM.bind(this));
- },
- /**
- * Called when the checkbox which indicates if assets bundles should be
- * included is toggled -> reloads the resources accordingly.
- *
- * @private
- * @param {Event} ev
- */
- _onIncludeBundlesChange: function (ev) {
- this.options.includeBundles = $(ev.target).prop('checked');
+ _onFilterChange: function (ev) {
+ var $item = $(ev.target);
+ $item.addClass('active').siblings().removeClass('active');
+ if ($item.data('type') === 'xml') {
+ this.options.includeBundles = $(ev.target).data('value') === 'all';
+ } else {
+ this.options.filesFilter = $item.data('value');
+ }
this._loadResources().then(this._updateViewSelectDOM.bind(this));
},
/**
@@ -867,6 +923,13 @@ var ViewEditor = Widget.extend({
ev.preventDefault();
this._switchType($(ev.target).data('type'));
},
+ /**
+ * Allows to hide the warning message without removing it from the DOM
+ * -> by default Bootstrap removes alert from the DOM
+ */
+ _onCloseWarningClick: function () {
+ this.$warningMessage.addClass('d-none');
+ },
});
return ViewEditor;
diff --git a/addons/web_editor/static/src/xml/ace.xml b/addons/web_editor/static/src/xml/ace.xml
index 1d59a94c827..20a4f893207 100644
--- a/addons/web_editor/static/src/xml/ace.xml
+++ b/addons/web_editor/static/src/xml/ace.xml
@@ -9,26 +9,33 @@
<div class="dropdown-menu" role="menu">
<a role="menuitem" href="#" class="dropdown-item o_ace_type_switcher_choice" data-type="xml">XML (HTML)</a>
<a role="menuitem" href="#" class="dropdown-item o_ace_type_switcher_choice" data-type="scss">SCSS (CSS)</a>
+ <a role="menuitem" href="#" class="dropdown-item o_ace_type_switcher_choice" data-type="js">JS</a>
</div>
</div>
<select id="ace-view-list" class="o_res_list"/>
<select id="ace-scss-list" class="o_res_list d-none"/>
+ <select id="ace-js-list" class="o_res_list d-none"/>
<label class="o_include_option oe_include_bundles">
- <div class="custom-control custom-checkbox">
- <input type="checkbox" id="o_ace_view_editor_include_bundle"
- class="custom-control-input js_include_bundles"
- t-att-checked="widget.options.includeBundles ? 'checked' : undefined"/>
- <label for="o_ace_view_editor_include_bundle"
- class="custom-control-label">Include Asset Bundles</label>
+ <div class="dropdown">
+ <button class="btn btn-primary dropdown-toggle" type="button" id="o_dropdown_filter_views" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ Filter
+ </button>
+ <div class="dropdown-menu" aria-labelledby="o_dropdown_filter_views">
+ <a class="dropdown-item o_ace_filter active" data-type="xml" data-value="default" href="#">Only Views</a>
+ <a class="dropdown-item o_ace_filter" data-type="xml" data-value="all" href="#">Views and Assets bundles</a>
+ </div>
</div>
</label>
<label class="o_include_option o_include_all_scss d-none">
- <div class="custom-control custom-checkbox">
- <input type="checkbox" id="o_ace_view_editor_include_all_scss"
- class="custom-control-input js_include_all_scss"
- t-att-checked="widget.options.includeAllSCSS ? 'checked' : undefined"/>
- <label for="o_ace_view_editor_include_all_scss"
- class="custom-control-label">Include All SCSS Files</label>
+ <div class="dropdown">
+ <button class="btn btn-primary dropdown-toggle" type="button" id="o_dropdown_filter_assets" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ Filter
+ </button>
+ <div class="dropdown-menu" aria-labelledby="o_dropdown_filter_assets">
+ <a class="dropdown-item o_ace_filter active" data-type="scss" data-value="custom" href="#">Only Custom SCSS Files</a>
+ <a class="dropdown-item o_ace_filter" data-type="scss" data-value="restricted" href="#">Only Page SCSS Files</a>
+ <a class="dropdown-item o_ace_filter" data-type="scss" data-value="all" href="#">All SCSS Files</a>
+ </div>
</div>
</label>
<div class="o_button_section">
@@ -37,11 +44,17 @@
</div>
</div>
<div id="ace-view-id">
- <span/>
- <div class="float-right">
+ <div class="float-right mb-2">
<button data-action="reset" type="button" class="btn btn-sm btn-danger"><i class="fa fa-undo"/> Reset</button>
<button data-action="format" type="button" class="btn btn-sm btn-link">Format</button>
</div>
+ <span/>
+ <div class="alert alert-warning alert-dismissible mt-2 mb-0" role="alert">
+ If you edit and save this file, you will never receive updates anymore unless you reset your customizations.
+ <button type="button" class="close" aria-label="Close">
+ <span>&amp;times;</span>
+ </button>
+ </div>
</div>
<div id="ace-view-editor"/>
</div>