diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 9f6b28179db..d4ff41de09d 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -304,130 +304,149 @@ class Web_Editor(http.Controller): ## @param key - the xml_id or id of the view the resources are related to ## @param get_views - True if the views must be fetched (default to True) ## @param get_scss - True if the style must be fetched (default to True) + ## @param get_js - True if the javascript must be fetched (default to True) ## @param bundles - True if the bundles views must be fetched (default to False) ## @param bundles_restriction - Names of the bundle in which to look for scss files (if empty, search in all of them) + ## @param only_user_custom_files - True if only user custom files are needed (default to True) ## @returns a dictionary with views info in the views key and style info in the scss key @http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True) - def get_assets_editor_resources(self, key, get_views=True, get_scss=True, bundles=False, bundles_restriction=[]): + def get_assets_editor_resources(self, key, get_views=True, get_scss=True, get_js=True, bundles=False, bundles_restriction=[], only_user_custom_files=True): # Related views must be fetched if the user wants the views and/or the style views = request.env["ir.ui.view"].get_related_views(key, bundles=bundles) views = views.read(self._get_view_fields_to_read()) + js_files_data_by_bundle = [] scss_files_data_by_bundle = [] - # Load scss only if asked by the user + if get_js: + js_files_data_by_bundle = self.load_resources('js', views, bundles_restriction, only_user_custom_files) if get_scss: - # Compile regex outside of the loop - # This will used to exclude library scss files from the result - excluded_url_matcher = re.compile("^(.+/lib/.+)|(.+import_bootstrap.+\.scss)$") - - # Load already customized scss files attachments - custom_url = self._make_custom_scss_file_url("%%.%%", "%%") - custom_attachments = self.get_custom_attachment(custom_url, op='=like') - - # First check the t-call-assets used in the related views - url_infos = dict() - for v in views: - for asset_call_node in etree.fromstring(v["arch"]).xpath("//t[@t-call-assets]"): - if asset_call_node.get("t-css") == "false": - continue - asset_name = asset_call_node.get("t-call-assets") - - # Loop through bundle files to search for scss file info - scss_files_data = [] - for file_info in request.env["ir.qweb"]._get_asset_content(asset_name, {})[0]: - if file_info["atype"] != "text/scss": - continue - url = file_info["url"] - - # Exclude library files (see regex above) - if excluded_url_matcher.match(url): - continue - - # Check if the file is customized and get bundle/path info - scss_file_data = self._match_scss_file_url(url) - if not scss_file_data: - continue - - # Save info (arch will be fetched later) - url_infos[url] = scss_file_data - scss_files_data.append(url) - - # scss data is returned sorted by bundle, with the bundles names and xmlids - if len(scss_files_data): - scss_files_data_by_bundle.append([dict(xmlid=asset_name, name=request.env.ref(asset_name).name), scss_files_data]) - - # Filter bundles/files: - # - A file which appears in multiple bundles only appears in the first one (the first in the DOM) - # - Only keep bundles with files which appears in the asked bundles and only keep those files - for i in range(0, len(scss_files_data_by_bundle)): - bundle_1 = scss_files_data_by_bundle[i] - for j in range(0, len(scss_files_data_by_bundle)): - bundle_2 = scss_files_data_by_bundle[j] - # In unwanted bundles, keep only the files which are in wanted bundles too (_assets_helpers) - if bundle_1[0]["xmlid"] not in bundles_restriction and bundle_2[0]["xmlid"] in bundles_restriction: - bundle_1[1] = [item_1 for item_1 in bundle_1[1] if item_1 in bundle_2[1]] - for i in range(0, len(scss_files_data_by_bundle)): - bundle_1 = scss_files_data_by_bundle[i] - for j in range(i+1, len(scss_files_data_by_bundle)): - bundle_2 = scss_files_data_by_bundle[j] - # In every bundle, keep only the files which were not found in previous bundles - bundle_2[1] = [item_2 for item_2 in bundle_2[1] if item_2 not in bundle_1[1]] - - # Only keep bundles which still have files and that were requested - scss_files_data_by_bundle = [ - data for data in scss_files_data_by_bundle - if (len(data[1]) > 0 and (not bundles_restriction or data[0]["xmlid"] in bundles_restriction)) - ] - - # Fetch the arch of each kept file, in each bundle - for bundle_data in scss_files_data_by_bundle: - for i in range(0, len(bundle_data[1])): - url = bundle_data[1][i] - url_info = url_infos[url] - - content = None - if url_info["customized"]: - # If the file is already customized, the content is found in the corresponding attachment - content = base64.b64decode(custom_attachments.filtered(lambda a: a.url == url).datas) - else: - # If the file is not yet customized, the content is found by reading the local scss file - module = url_info["module"] - module_path = get_module_path(module) - module_resource_path = get_resource_path(module, url_info["resource_path"]) - if module_path and module_resource_path: - module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' - module_resource_path = os.path.normpath(module_resource_path) - if module_resource_path.startswith(module_path): - with open(module_resource_path, "rb") as f: - content = f.read() - - bundle_data[1][i] = dict( - url = "/%s/%s" % (url_info["module"], url_info["resource_path"]), - arch = content, - customized = url_info["customized"], - ) + scss_files_data_by_bundle = self.load_resources('scss', views, bundles_restriction, only_user_custom_files) return dict( - views = get_views and views or [], - scss = get_scss and scss_files_data_by_bundle or [], + views=get_views and views or [], + scss=get_scss and scss_files_data_by_bundle or [], + js=get_js and js_files_data_by_bundle or [], ) - def save_scss_view_hook(self): + def load_resources(self, file_type, views, bundles_restriction, only_user_custom_files): + files_data_by_bundle = [] + resources_type_info = {'t_call_assets_attribute': 't-js', 'mimetype': 'text/javascript'} + if file_type == 'scss': + resources_type_info = {'t_call_assets_attribute': 't-css', 'mimetype': 'text/scss'} + + # Compile regex outside of the loop + # This will used to exclude library scss files from the result + excluded_url_matcher = re.compile("^(.+/lib/.+)|(.+import_bootstrap.+\.scss)$") + + # Load already customized files attachments + custom_url = self._make_custom_scss_or_js_file_url("%%.%%", "%%") + custom_attachments = self.get_custom_attachment(custom_url, op='=like') + + # First check the t-call-assets used in the related views + url_infos = dict() + for v in views: + for asset_call_node in etree.fromstring(v["arch"]).xpath("//t[@t-call-assets]"): + if asset_call_node.get(resources_type_info['t_call_assets_attribute']) == "false": + continue + asset_name = asset_call_node.get("t-call-assets") + + # Loop through bundle files to search for file info + files_data = [] + for file_info in request.env["ir.qweb"]._get_asset_content(asset_name, {})[0]: + if file_info["atype"] != resources_type_info['mimetype']: + continue + url = file_info["url"] + + # Exclude library files (see regex above) + if excluded_url_matcher.match(url): + continue + + # Check if the file is customized and get bundle/path info + file_data = self._match_scss_or_js_file_url(url) + if not file_data: + continue + + # Save info according to the filter (arch will be fetched later) + url_infos[url] = file_data + + if '/user_custom_' in url \ + or file_data['customized'] \ + or file_type == 'scss' and not only_user_custom_files: + files_data.append(url) + + # File data is returned sorted by bundle, with the bundles names and xmlids + if len(files_data): + files_data_by_bundle.append([dict(xmlid=asset_name, name=request.env.ref(asset_name).name), files_data]) + + # Filter bundles/files: + # - A file which appears in multiple bundles only appears in the first one (the first in the DOM) + # - Only keep bundles with files which appears in the asked bundles and only keep those files + for i in range(0, len(files_data_by_bundle)): + bundle_1 = files_data_by_bundle[i] + for j in range(0, len(files_data_by_bundle)): + bundle_2 = files_data_by_bundle[j] + # In unwanted bundles, keep only the files which are in wanted bundles too (_assets_helpers) + if bundle_1[0]["xmlid"] not in bundles_restriction and bundle_2[0]["xmlid"] in bundles_restriction: + bundle_1[1] = [item_1 for item_1 in bundle_1[1] if item_1 in bundle_2[1]] + for i in range(0, len(files_data_by_bundle)): + bundle_1 = files_data_by_bundle[i] + for j in range(i + 1, len(files_data_by_bundle)): + bundle_2 = files_data_by_bundle[j] + # In every bundle, keep only the files which were not found in previous bundles + bundle_2[1] = [item_2 for item_2 in bundle_2[1] if item_2 not in bundle_1[1]] + + # Only keep bundles which still have files and that were requested + files_data_by_bundle = [ + data for data in files_data_by_bundle + if (len(data[1]) > 0 and (not bundles_restriction or data[0]["xmlid"] in bundles_restriction)) + ] + + # Fetch the arch of each kept file, in each bundle + for bundle_data in files_data_by_bundle: + for i in range(0, len(bundle_data[1])): + url = bundle_data[1][i] + url_info = url_infos[url] + + content = None + if url_info["customized"]: + # If the file is already customized, the content is found in the corresponding attachment + content = base64.b64decode(custom_attachments.filtered(lambda a: a.url == url).datas) + else: + # If the file is not yet customized, the content is found by reading the local scss file + module = url_info["module"] + module_path = get_module_path(module) + module_resource_path = get_resource_path(module, url_info["resource_path"]) + if module_path and module_resource_path: + module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' + module_resource_path = os.path.normpath(module_resource_path) + if module_resource_path.startswith(module_path): + with open(module_resource_path, "rb") as f: + content = f.read() + + bundle_data[1][i] = dict( + url="/%s/%s" % (url_info["module"], url_info["resource_path"]), + arch=content, + customized=url_info["customized"], + ) + return files_data_by_bundle + + def save_scss_or_js_view_hook(self): return {} - def save_scss_attachment_hook(self): + def save_scss_or_js_attachment_hook(self): return {} - ## The save_scss route is in charge of saving a given modification of a scss file. - ## @param url - the original url of the scss file which has to be modified - ## @param bundle_xmlid - the xmlid of the bundle in which the scss file addition can be found - ## @param content - the new content of the scss file - @http.route("/web_editor/save_scss", type="json", auth="user", website=True) - def save_scss(self, url, bundle_xmlid, content): + ## The save_scss_or_js route is in charge of saving a given modification of a scss/js file. + ## @param url - the original url of the scss/js file which has to be modified + ## @param bundle_xmlid - the xmlid of the bundle in which the scss/js file addition can be found + ## @param content - the new content of the scss/js file + ## @param file_type - scss or js + @http.route("/web_editor/save_scss_or_js", type="json", auth="user", website=True) + def save_scss_or_js(self, url, bundle_xmlid, content, file_type): IrAttachment = request.env["ir.attachment"] - custom_url = self._make_custom_scss_file_url(url, bundle_xmlid) + custom_url = self._make_custom_scss_or_js_file_url(url, bundle_xmlid) # Check if the file to save had already been modified custom_attachment = self.get_custom_attachment(custom_url) @@ -436,20 +455,24 @@ class Web_Editor(http.Controller): # If it was already modified, simply override the corresponding attachment content custom_attachment.write({"datas": datas}) else: - # If not, create a new attachment to copy the original scss file content, with its modifications + # If not, create a new attachment to copy the original scss/js file content, with its modifications new_attach = { 'name': custom_url, 'type': "binary", - 'mimetype': "text/scss", + 'mimetype': (file_type == 'js' and 'text/javascript' or 'text/scss'), 'datas': datas, 'datas_fname': url.split("/")[-1], 'url': custom_url, } - new_attach.update(self.save_scss_attachment_hook()) + new_attach.update(self.save_scss_or_js_attachment_hook()) IrAttachment.create(new_attach) # Create a view to extend the template which adds the original file to link the new modified version instead IrUiView = request.env["ir.ui.view"] + file_type_info = { + 'tag': 'link' if file_type == 'scss' else 'script', + 'attribute': 'href' if file_type == 'scss' else 'src', + } def views_linking_url(view): """ @@ -457,19 +480,22 @@ class Web_Editor(http.Controller): (note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.) """ - return bool(etree.XML(view.arch).xpath("//link[@href='{}']".format(url))) + tree = etree.XML(view.arch) + return bool(tree.xpath("//%%(tag)s[@%%(attribute)s='%(url)s']" % { + 'url': url, + } % file_type_info)) view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url) new_view = { 'name': custom_url, - 'key': 'web_editor.scss_%s' % str(uuid.uuid4())[:6], + 'key': 'web_editor.%s_%s' % (file_type, str(uuid.uuid4())[:6]), 'mode': "extension", 'inherit_id': view_to_xpath.id, 'arch': """ - - %(new_url)s + + %(new_url)s """ % { @@ -477,21 +503,21 @@ class Web_Editor(http.Controller): 'name': custom_url, 'url_to_replace': url, 'new_url': custom_url, - } + } % file_type_info } - new_view.update(self.save_scss_view_hook()) + new_view.update(self.save_scss_or_js_view_hook()) IrUiView.create(new_view) request.env["ir.qweb"].clear_caches() - ## The reset_scss route is in charge of reverting all the changes that were done to a scss file. + ## The reset_scss_or_js route is in charge of reverting all the changes that were done to a scss/js file. ## @param url - the original URL of the scss file to reset ## @param bundle_xmlid - the xmlid of the bundle in which the scss file addition can be found - @http.route("/web_editor/reset_scss", type="json", auth="user", website=True) - def reset_scss(self, url, bundle_xmlid): - custom_url = self._make_custom_scss_file_url(url, bundle_xmlid) + @http.route("/web_editor/reset_scss_or_js", type="json", auth="user", website=True) + def reset_scss_or_js(self, url, bundle_xmlid): + custom_url = self._make_custom_scss_or_js_file_url(url, bundle_xmlid) - # Simply delete the attachement which contains the modified scss file and the xpath view which links it + # Simply delete the attachement which contains the modified scss/js file and the xpath view which links it self.get_custom_attachment(custom_url).unlink() self.get_custom_view(custom_url).unlink() @@ -505,13 +531,13 @@ class Web_Editor(http.Controller): IrUiView = request.env["ir.ui.view"] return IrUiView.search([("name", op, custom_url)]) - def _make_custom_scss_file_url(self, url, bundle): + def _make_custom_scss_or_js_file_url(self, url, bundle): parts = url.rsplit(".", 1) return "%s.custom.%s.%s" % (parts[0], bundle, parts[1]) - _match_scss_file_url_regex = re.compile("^/(\w+)/(.+?)(\.custom\.(.+))?\.(\w+)$") - def _match_scss_file_url(self, url): - m = self._match_scss_file_url_regex.match(url) + _match_file_url_regex = re.compile("^/(\w+)/(.+?)(\.custom\.(.+))?\.(\w+)$") + def _match_scss_or_js_file_url(self, url): + m = self._match_file_url_regex.match(url) if not m: return False return { 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 = $('', { - 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($('', { + 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($('