summaryrefslogtreecommitdiff
path: root/addons/website/static/src/js/menu/content.js
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/website/static/src/js/menu/content.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website/static/src/js/menu/content.js')
-rw-r--r--addons/website/static/src/js/menu/content.js1129
1 files changed, 1129 insertions, 0 deletions
diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js
new file mode 100644
index 00000000..d2dff980
--- /dev/null
+++ b/addons/website/static/src/js/menu/content.js
@@ -0,0 +1,1129 @@
+odoo.define('website.contentMenu', function (require) {
+'use strict';
+
+var Class = require('web.Class');
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var time = require('web.time');
+var weWidgets = require('wysiwyg.widgets');
+var websiteNavbarData = require('website.navbar');
+var websiteRootData = require('website.root');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+var qweb = core.qweb;
+
+var PagePropertiesDialog = weWidgets.Dialog.extend({
+ template: 'website.pagesMenu.page_info',
+ xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat(
+ ['/website/static/src/xml/website.pageProperties.xml']
+ ),
+ events: _.extend({}, weWidgets.Dialog.prototype.events, {
+ 'keyup input#page_name': '_onNameChanged',
+ 'keyup input#page_url': '_onUrlChanged',
+ 'change input#create_redirect': '_onCreateRedirectChanged',
+ 'click input#visibility_password': '_onPasswordClicked',
+ 'change input#visibility_password': '_onPasswordChanged',
+ 'change select#visibility': '_onVisibilityChanged',
+ 'error.datetimepicker': '_onDateTimePickerError',
+ }),
+
+ /**
+ * @constructor
+ * @override
+ */
+ init: function (parent, page_id, options) {
+ var self = this;
+ var serverUrl = window.location.origin + '/';
+ var length_url = serverUrl.length;
+ var serverUrlTrunc = serverUrl;
+ if (length_url > 30) {
+ serverUrlTrunc = serverUrl.slice(0,14) + '..' + serverUrl.slice(-14);
+ }
+ this.serverUrl = serverUrl;
+ this.serverUrlTrunc = serverUrlTrunc;
+ this.current_page_url = window.location.pathname;
+ this.page_id = page_id;
+
+ var buttons = [
+ {text: _t("Save"), classes: 'btn-primary', click: this.save},
+ {text: _t("Discard"), classes: 'mr-auto', close: true},
+ ];
+ if (options.fromPageManagement) {
+ buttons.push({
+ text: _t("Go To Page"),
+ icon: 'fa-globe',
+ classes: 'btn-link',
+ click: function (e) {
+ window.location.href = '/' + self.page.url;
+ },
+ });
+ }
+ buttons.push({
+ text: _t("Duplicate Page"),
+ icon: 'fa-clone',
+ classes: 'btn-link',
+ click: function (e) {
+ // modal('hide') will break the rpc, so hide manually
+ this.$el.closest('.modal').addClass('d-none');
+ _clonePage.call(this, self.page_id);
+ },
+ });
+ buttons.push({
+ text: _t("Delete Page"),
+ icon: 'fa-trash',
+ classes: 'btn-link',
+ click: function (e) {
+ _deletePage.call(this, self.page_id, options.fromPageManagement);
+ },
+ });
+ this._super(parent, _.extend({}, {
+ title: _t("Page Properties"),
+ size: 'medium',
+ buttons: buttons,
+ }, options || {}));
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ var defs = [this._super.apply(this, arguments)];
+ var self = this;
+
+ defs.push(this._rpc({
+ model: 'website.page',
+ method: 'get_page_properties',
+ args: [this.page_id],
+ }).then(function (page) {
+ page.url = _.str.startsWith(page.url, '/') ? page.url.substring(1) : page.url;
+ page.hasSingleGroup = page.group_id !== undefined;
+ self.page = page;
+ }));
+
+ return Promise.all(defs);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+
+ var defs = [this._super.apply(this, arguments)];
+
+ this.$('.ask_for_redirect').addClass('d-none');
+ this.$('.redirect_type').addClass('d-none');
+ this.$('.warn_about_call').addClass('d-none');
+ if (this.page.visibility !== 'password') {
+ this.$('.show_visibility_password').addClass('d-none');
+ }
+ if (this.page.visibility !== 'restricted_group') {
+ this.$('.show_group_id').addClass('d-none');
+ }
+ this.autocompleteWithGroups(this.$('#group_id'));
+
+ defs.push(this._getPageDependencies(this.page_id)
+ .then(function (dependencies) {
+ var dep_text = [];
+ _.each(dependencies, function (value, index) {
+ if (value.length > 0) {
+ dep_text.push(value.length + ' ' + index.toLowerCase());
+ }
+ });
+ dep_text = dep_text.join(', ');
+ self.$('#dependencies_redirect').html(qweb.render('website.show_page_dependencies', { dependencies: dependencies, dep_text: dep_text }));
+ self.$('#dependencies_redirect [data-toggle="popover"]').popover({
+ container: 'body',
+ });
+ }));
+
+ defs.push(this._getSupportedMimetype()
+ .then(function (mimetypes) {
+ self.supportedMimetype = mimetypes;
+ }));
+
+ defs.push(this._getPageKeyDependencies(this.page_id)
+ .then(function (dependencies) {
+ var dep_text = [];
+ _.each(dependencies, function (value, index) {
+ if (value.length > 0) {
+ dep_text.push(value.length + ' ' + index.toLowerCase());
+ }
+ });
+ dep_text = dep_text.join(', ');
+ self.$('.warn_about_call').html(qweb.render('website.show_page_key_dependencies', {dependencies: dependencies, dep_text: dep_text}));
+ self.$('.warn_about_call [data-toggle="popover"]').popover({
+ container: 'body',
+ });
+ }));
+
+ defs.push(this._rpc({model: 'res.users',
+ method: 'has_group',
+ args: ['website.group_multi_website']})
+ .then(function (has_group) {
+ if (!has_group) {
+ self.$('#website_restriction').addClass('hidden');
+ }
+ }));
+
+ var datepickersOptions = {
+ minDate: moment({ y: 1000 }),
+ maxDate: moment().add(200, 'y'),
+ calendarWeeks: true,
+ icons : {
+ time: 'fa fa-clock-o',
+ date: 'fa fa-calendar',
+ next: 'fa fa-chevron-right',
+ previous: 'fa fa-chevron-left',
+ up: 'fa fa-chevron-up',
+ down: 'fa fa-chevron-down',
+ },
+ locale : moment.locale(),
+ format : time.getLangDatetimeFormat(),
+ widgetPositioning : {
+ horizontal: 'auto',
+ vertical: 'top',
+ },
+ widgetParent: 'body',
+ };
+ if (this.page.date_publish) {
+ datepickersOptions.defaultDate = time.str_to_datetime(this.page.date_publish);
+ }
+ this.$('#date_publish_container').datetimepicker(datepickersOptions);
+ return Promise.all(defs);
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ $('.popover').popover('hide');
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ save: function (data) {
+ var self = this;
+ var context;
+ this.trigger_up('context_get', {
+ callback: function (ctx) {
+ context = ctx;
+ },
+ });
+ var url = this.$('#page_url').val();
+
+ var $datePublish = this.$("#date_publish");
+ $datePublish.closest(".form-group").removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+ var datePublish = $datePublish.val();
+ if (datePublish !== "") {
+ datePublish = this._parse_date(datePublish);
+ if (!datePublish) {
+ $datePublish.closest(".form-group").addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ return;
+ }
+ }
+ var params = {
+ id: this.page.id,
+ name: this.$('#page_name').val(),
+ // Replace duplicate following '/' by only one '/'
+ url: url.replace(/\/{2,}/g, '/'),
+ is_menu: this.$('#is_menu').prop('checked'),
+ is_homepage: this.$('#is_homepage').prop('checked'),
+ website_published: this.$('#is_published').prop('checked'),
+ create_redirect: this.$('#create_redirect').prop('checked'),
+ redirect_type: this.$('#redirect_type').val(),
+ website_indexed: this.$('#is_indexed').prop('checked'),
+ visibility: this.$('#visibility').val(),
+ date_publish: datePublish,
+ };
+ if (this.page.hasSingleGroup && this.$('#visibility').val() === 'restricted_group') {
+ params['group_id'] = this.$('#group_id').data('group-id');
+ }
+ if (this.$('#visibility').val() === 'password') {
+ var field_pwd = $('#visibility_password');
+ if (!field_pwd.get(0).reportValidity()) {
+ return;
+ }
+ if (field_pwd.data('dirty')) {
+ params['visibility_pwd'] = field_pwd.val();
+ }
+ }
+
+ this._rpc({
+ model: 'website.page',
+ method: 'save_page_info',
+ args: [[context.website_id], params],
+ }).then(function (url) {
+ // If from page manager: reload url, if from page itself: go to
+ // (possibly) new url
+ var mo;
+ self.trigger_up('main_object_request', {
+ callback: function (value) {
+ mo = value;
+ },
+ });
+ if (mo.model === 'website.page') {
+ window.location.href = url.toLowerCase();
+ } else {
+ window.location.reload(true);
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Retrieves the page URL dependencies for the given object id.
+ *
+ * @private
+ * @param {integer} moID
+ * @returns {Promise<Array>}
+ */
+ _getPageDependencies: function (moID) {
+ return this._rpc({
+ model: 'website',
+ method: 'page_search_dependencies',
+ args: [moID],
+ });
+ },
+ /**
+ * Retrieves the page's key dependencies for the given object id.
+ *
+ * @private
+ * @param {integer} moID
+ * @returns {Promise<Array>}
+ */
+ _getPageKeyDependencies: function (moID) {
+ return this._rpc({
+ model: 'website',
+ method: 'page_search_key_dependencies',
+ args: [moID],
+ });
+ },
+ /**
+ * Retrieves supported mimtype
+ *
+ * @private
+ * @returns {Promise<Array>}
+ */
+ _getSupportedMimetype: function () {
+ return this._rpc({
+ model: 'website',
+ method: 'guess_mimetype',
+ });
+ },
+ /**
+ * Returns information about the page main object.
+ *
+ * @private
+ * @returns {Object} model and id
+ */
+ _getMainObject: function () {
+ var repr = $('html').data('main-object');
+ var m = repr.match(/(.+)\((\d+),(.*)\)/);
+ return {
+ model: m[1],
+ id: m[2] | 0,
+ };
+ },
+ /**
+ * Converts a string representing the browser datetime
+ * (exemple: Albanian: '2018-Qer-22 15.12.35.')
+ * to a string representing UTC in Odoo's datetime string format
+ * (exemple: '2018-04-22 13:12:35').
+ *
+ * The time zone of the datetime string is assumed to be the one of the
+ * browser and it will be converted to UTC (standard for Odoo).
+ *
+ * @private
+ * @param {String} value A string representing a datetime.
+ * @returns {String|false} A string representing an UTC datetime if the given value is valid, false otherwise.
+ */
+ _parse_date: function (value) {
+ var datetime = moment(value, time.getLangDatetimeFormat(), true);
+ if (datetime.isValid()) {
+ return time.datetime_to_str(datetime.toDate());
+ }
+ else {
+ return false;
+ }
+ },
+ /**
+ * Allows the given input to propose existing groups.
+ *
+ * @param {jQuery} $input
+ */
+ autocompleteWithGroups: function ($input) {
+ $input.autocomplete({
+ source: (request, response) => {
+ return this._rpc({
+ model: 'res.groups',
+ method: 'search_read',
+ args: [[['name', 'ilike', request.term]], ['display_name']],
+ kwargs: {
+ limit: 15,
+ },
+ }).then(founds => {
+ founds = founds.map(g => ({'id': g['id'], 'label': g['display_name']}));
+ response(founds);
+ });
+ },
+ change: (ev, ui) => {
+ var $target = $(ev.target);
+ if (!ui.item) {
+ $target.val("");
+ $target.removeData('group-id');
+ } else {
+ $target.data('group-id', ui.item.id);
+ }
+ },
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onUrlChanged: function () {
+ var url = this.$('input#page_url').val();
+ this.$('.ask_for_redirect').toggleClass('d-none', url === this.page.url);
+ },
+ /**
+ * @private
+ */
+ _onNameChanged: function () {
+ var name = this.$('input#page_name').val();
+ // If the file type is a supported mimetype, check if it is t-called.
+ // If so, warn user. Note: different from page_search_dependencies which
+ // check only for url and not key
+ var ext = '.' + this.page.name.split('.').pop();
+ if (ext in this.supportedMimetype && ext !== '.html') {
+ this.$('.warn_about_call').toggleClass('d-none', name === this.page.name);
+ }
+ },
+ /**
+ * @private
+ */
+ _onCreateRedirectChanged: function () {
+ var createRedirect = this.$('input#create_redirect').prop('checked');
+ this.$('.redirect_type').toggleClass('d-none', !createRedirect);
+ },
+ /**
+ * @private
+ */
+ _onVisibilityChanged: function (ev) {
+ this.$('.show_visibility_password').toggleClass('d-none', ev.target.value !== 'password');
+ this.$('.show_group_id').toggleClass('d-none', ev.target.value !== 'restricted_group');
+ this.$('#visibility_password').attr('required', ev.target.value === 'password');
+ },
+ /**
+ * Library clears the wrong date format so just ignore error
+ *
+ * @private
+ */
+ _onDateTimePickerError: function (ev) {
+ return false;
+ },
+ /**
+ * @private
+ */
+ _onPasswordClicked: function (ev) {
+ ev.target.value = '';
+ this._onPasswordChanged();
+ },
+ /**
+ * @private
+ */
+ _onPasswordChanged: function () {
+ this.$('#visibility_password').data('dirty', 1);
+ },
+});
+
+var MenuEntryDialog = weWidgets.LinkDialog.extend({
+ xmlDependencies: weWidgets.LinkDialog.prototype.xmlDependencies.concat(
+ ['/website/static/src/xml/website.contentMenu.xml']
+ ),
+
+ /**
+ * @constructor
+ */
+ init: function (parent, options, editable, data) {
+ this._super(parent, _.extend({
+ title: _t("Add a menu item"),
+ }, options || {}), editable, _.extend({
+ needLabel: true,
+ text: data.name || '',
+ isNewWindow: data.new_window,
+ }, data || {}));
+
+ this.menuType = data.menuType;
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ // Remove style related elements
+ this.$('.o_link_dialog_preview').remove();
+ this.$('input[name="is_new_window"], .link-style').closest('.form-group').remove();
+ this.$modal.find('.modal-lg').removeClass('modal-lg');
+ this.$('form.col-lg-8').removeClass('col-lg-8').addClass('col-12');
+
+ // Adapt URL label
+ this.$('label[for="o_link_dialog_label_input"]').text(_t("Menu Label"));
+
+ // Auto add '#' URL and hide the input if for mega menu
+ if (this.menuType === 'mega') {
+ var $url = this.$('input[name="url"]');
+ $url.val('#').trigger('change');
+ $url.closest('.form-group').addClass('d-none');
+ }
+
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ save: function () {
+ var $e = this.$('#o_link_dialog_label_input');
+ if (!$e.val() || !$e[0].checkValidity()) {
+ $e.closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ $e.focus();
+ return;
+ }
+ return this._super.apply(this, arguments);
+ },
+});
+
+var SelectEditMenuDialog = weWidgets.Dialog.extend({
+ template: 'website.contentMenu.dialog.select',
+ xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat(
+ ['/website/static/src/xml/website.contentMenu.xml']
+ ),
+
+ /**
+ * @constructor
+ * @override
+ */
+ init: function (parent, options) {
+ var self = this;
+ self.roots = [{id: null, name: _t("Top Menu")}];
+ $('[data-content_menu_id]').each(function () {
+ // Remove name fallback in master
+ self.roots.push({id: $(this).data('content_menu_id'), name: $(this).attr('name') || $(this).data('menu_name')});
+ });
+ this._super(parent, _.extend({}, {
+ title: _t("Select a Menu"),
+ save_text: _t("Continue")
+ }, options || {}));
+ },
+ /**
+ * @override
+ */
+ save: function () {
+ this.final_data = parseInt(this.$el.find('select').val() || null);
+ this._super.apply(this, arguments);
+ },
+});
+
+var EditMenuDialog = weWidgets.Dialog.extend({
+ template: 'website.contentMenu.dialog.edit',
+ xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat(
+ ['/website/static/src/xml/website.contentMenu.xml']
+ ),
+ events: _.extend({}, weWidgets.Dialog.prototype.events, {
+ 'click a.js_add_menu': '_onAddMenuButtonClick',
+ 'click button.js_delete_menu': '_onDeleteMenuButtonClick',
+ 'click button.js_edit_menu': '_onEditMenuButtonClick',
+ }),
+
+ /**
+ * @constructor
+ * @override
+ */
+ init: function (parent, options, rootID) {
+ this._super(parent, _.extend({}, {
+ title: _t("Edit Menu"),
+ size: 'medium',
+ }, options || {}));
+ this.rootID = rootID;
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ var defs = [this._super.apply(this, arguments)];
+ var context;
+ this.trigger_up('context_get', {
+ callback: function (ctx) {
+ context = ctx;
+ },
+ });
+ defs.push(this._rpc({
+ model: 'website.menu',
+ method: 'get_tree',
+ args: [context.website_id, this.rootID],
+ }).then(menu => {
+ this.menu = menu;
+ this.rootMenuID = menu.fields['id'];
+ this.flat = this._flatenize(menu);
+ this.toDelete = [];
+ }));
+ return Promise.all(defs);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ var r = this._super.apply(this, arguments);
+ this.$('.oe_menu_editor').nestedSortable({
+ listType: 'ul',
+ handle: 'div',
+ items: 'li',
+ maxLevels: 2,
+ toleranceElement: '> div',
+ forcePlaceholderSize: true,
+ opacity: 0.6,
+ placeholder: 'oe_menu_placeholder',
+ tolerance: 'pointer',
+ attribute: 'data-menu-id',
+ expression: '()(.+)', // nestedSortable takes the second match of an expression (*sigh*)
+ isAllowed: (placeholder, placeholderParent, currentItem) => {
+ return !placeholderParent
+ || !currentItem[0].dataset.megaMenu && !placeholderParent[0].dataset.megaMenu;
+ },
+ });
+ return r;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ save: function () {
+ var _super = this._super.bind(this);
+ var newMenus = this.$('.oe_menu_editor').nestedSortable('toArray', {startDepthCount: 0});
+ var levels = [];
+ var data = [];
+ var context;
+ this.trigger_up('context_get', {
+ callback: function (ctx) {
+ context = ctx;
+ },
+ });
+ // Resequence, re-tree and remove useless data
+ newMenus.forEach(menu => {
+ if (menu.id) {
+ levels[menu.depth] = (levels[menu.depth] || 0) + 1;
+ var menuFields = this.flat[menu.id].fields;
+ menuFields['sequence'] = levels[menu.depth];
+ menuFields['parent_id'] = menu['parent_id'] || this.rootMenuID;
+ data.push(menuFields);
+ }
+ });
+ return this._rpc({
+ model: 'website.menu',
+ method: 'save',
+ args: [
+ context.website_id,
+ {
+ 'data': data,
+ 'to_delete': this.toDelete,
+ }
+ ],
+ }).then(function () {
+ return _super();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns a mapping id -> menu item containing all the menu items in the
+ * given menu hierarchy.
+ *
+ * @private
+ * @param {Object} node
+ * @param {Object} [_dict] internal use: the mapping being built
+ * @returns {Object}
+ */
+ _flatenize: function (node, _dict) {
+ _dict = _dict || {};
+ _dict[node.fields['id']] = node;
+ node.children.forEach(child => {
+ this._flatenize(child, _dict);
+ });
+ return _dict;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the "add menu" button is clicked -> Opens the appropriate
+ * dialog to edit this new menu.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onAddMenuButtonClick: function (ev) {
+ var menuType = ev.currentTarget.dataset.type;
+ var dialog = new MenuEntryDialog(this, {}, null, {
+ menuType: menuType,
+ });
+ dialog.on('save', this, link => {
+ var newMenu = {
+ 'fields': {
+ 'id': _.uniqueId('new-'),
+ 'name': _.unescape(link.text),
+ 'url': link.url,
+ 'new_window': link.isNewWindow,
+ 'is_mega_menu': menuType === 'mega',
+ 'sequence': 0,
+ 'parent_id': false,
+ },
+ 'children': [],
+ 'is_homepage': false,
+ };
+ this.flat[newMenu.fields['id']] = newMenu;
+ this.$('.oe_menu_editor').append(
+ qweb.render('website.contentMenu.dialog.submenu', {submenu: newMenu})
+ );
+ });
+ dialog.open();
+ },
+ /**
+ * Called when the "delete menu" button is clicked -> Deletes this menu.
+ *
+ * @private
+ */
+ _onDeleteMenuButtonClick: function (ev) {
+ var $menu = $(ev.currentTarget).closest('[data-menu-id]');
+ var menuID = parseInt($menu.data('menu-id'));
+ if (menuID) {
+ this.toDelete.push(menuID);
+ }
+ $menu.remove();
+ },
+ /**
+ * Called when the "edit menu" button is clicked -> Opens the appropriate
+ * dialog to edit this menu.
+ *
+ * @private
+ */
+ _onEditMenuButtonClick: function (ev) {
+ var $menu = $(ev.currentTarget).closest('[data-menu-id]');
+ var menuID = $menu.data('menu-id');
+ var menu = this.flat[menuID];
+ if (menu) {
+ var dialog = new MenuEntryDialog(this, {}, null, _.extend({
+ menuType: menu.fields['is_mega_menu'] ? 'mega' : undefined,
+ }, menu.fields));
+ dialog.on('save', this, link => {
+ _.extend(menu.fields, {
+ 'name': _.unescape(link.text),
+ 'url': link.url,
+ 'new_window': link.isNewWindow,
+ });
+ $menu.find('.js_menu_label').first().text(menu.fields['name']);
+ });
+ dialog.open();
+ } else {
+ Dialog.alert(null, "Could not find menu entry");
+ }
+ },
+});
+
+var PageOption = Class.extend({
+ /**
+ * @constructor
+ * @param {string} name
+ * the option's name = the field's name in website.page model
+ * @param {*} value
+ * @param {function} setValueCallback
+ * a function which simulates an option's value change without
+ * asking the server to change it
+ */
+ init: function (name, value, setValueCallback) {
+ this.name = name;
+ this.value = value;
+ this.isDirty = false;
+ this.setValueCallback = setValueCallback;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Sets the new option's value thanks to the related callback.
+ *
+ * @param {*} [value]
+ * by default: consider the current value is a boolean and toggle it
+ */
+ setValue: function (value) {
+ if (value === undefined) {
+ value = !this.value;
+ }
+ this.setValueCallback.call(this, value);
+ this.value = value;
+ this.isDirty = true;
+ },
+});
+
+var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
+ xmlDependencies: ['/website/static/src/xml/website.xml'],
+ actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
+ edit_menu: '_editMenu',
+ get_page_option: '_getPageOption',
+ on_save: '_onSave',
+ page_properties: '_pageProperties',
+ toggle_page_option: '_togglePageOption',
+ }),
+ pageOptionsSetValueCallbacks: {
+ header_overlay: function (value) {
+ $('#wrapwrap').toggleClass('o_header_overlay', value);
+ },
+ header_color: function (value) {
+ $('#wrapwrap > header').removeClass(this.value)
+ .addClass(value);
+ },
+ header_visible: function (value) {
+ $('#wrapwrap > header').toggleClass('d-none o_snippet_invisible', !value);
+ },
+ footer_visible: function (value) {
+ $('#wrapwrap > footer').toggleClass('d-none o_snippet_invisible', !value);
+ },
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+ this.pageOptions = {};
+ _.each($('.o_page_option_data'), function (el) {
+ var value = el.value;
+ if (value === "True") {
+ value = true;
+ } else if (value === "False") {
+ value = false;
+ }
+ self.pageOptions[el.name] = new PageOption(
+ el.name,
+ value,
+ self.pageOptionsSetValueCallbacks[el.name]
+ );
+ });
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Actions
+ //--------------------------------------------------------------------------
+
+ /**
+ * Asks the user which menu to edit if multiple menus exist on the page.
+ * Then opens the menu edition dialog.
+ * Then executes the given callback once the edition is saved, to finally
+ * reload the page.
+ *
+ * @private
+ * @param {function} [beforeReloadCallback]
+ * @returns {Promise}
+ * Unresolved if the menu is edited and saved as the page will be
+ * reloaded.
+ * Resolved otherwise.
+ */
+ _editMenu: function (beforeReloadCallback) {
+ var self = this;
+ return new Promise(function (resolve) {
+ function resolveWhenEditMenuDialogIsCancelled(rootID) {
+ return self._openEditMenuDialog(rootID, beforeReloadCallback).then(resolve);
+ }
+ if ($('[data-content_menu_id]').length) {
+ var select = new SelectEditMenuDialog(self);
+ select.on('save', self, resolveWhenEditMenuDialogIsCancelled);
+ select.on('cancel', self, resolve);
+ select.open();
+ } else {
+ resolveWhenEditMenuDialogIsCancelled(null);
+ }
+ });
+ },
+ /**
+ *
+ * @param {*} rootID
+ * @param {function|undefied} beforeReloadCallback function that returns a promise
+ * @returns {Promise}
+ */
+ _openEditMenuDialog: function (rootID, beforeReloadCallback) {
+ var self = this;
+ return new Promise(function (resolve) {
+ var dialog = new EditMenuDialog(self, {}, rootID);
+ dialog.on('save', self, function () {
+ // Before reloading the page after menu modification, does the
+ // given action to do.
+ if (beforeReloadCallback) {
+ // Reload the page so that the menu modification are shown
+ beforeReloadCallback().then(function () {
+ window.location.reload(true);
+ });
+ } else {
+ window.location.reload(true);
+ }
+ });
+ dialog.on('cancel', self, resolve);
+ dialog.open();
+ });
+ },
+
+ /**
+ * Retrieves the value of a page option.
+ *
+ * @private
+ * @param {string} name
+ * @returns {Promise<*>}
+ */
+ _getPageOption: function (name) {
+ var option = this.pageOptions[name];
+ if (!option) {
+ return Promise.reject();
+ }
+ return Promise.resolve(option.value);
+ },
+ /**
+ * On save, simulated page options have to be server-saved.
+ *
+ * @private
+ * @returns {Promise}
+ */
+ _onSave: function () {
+ var self = this;
+ var defs = _.map(this.pageOptions, function (option, optionName) {
+ if (option.isDirty) {
+ return self._togglePageOption({
+ name: optionName,
+ value: option.value,
+ }, true, true);
+ }
+ });
+ return Promise.all(defs);
+ },
+ /**
+ * Opens the page properties dialog.
+ *
+ * @private
+ * @returns {Promise}
+ */
+ _pageProperties: function () {
+ var mo;
+ this.trigger_up('main_object_request', {
+ callback: function (value) {
+ mo = value;
+ },
+ });
+ var dialog = new PagePropertiesDialog(this, mo.id, {}).open();
+ return dialog.opened();
+ },
+ /**
+ * Toggles a page option.
+ *
+ * @private
+ * @param {Object} params
+ * @param {string} params.name
+ * @param {*} [params.value] (change value by default true -> false -> true)
+ * @param {boolean} [forceSave=false]
+ * @param {boolean} [noReload=false]
+ * @returns {Promise}
+ */
+ _togglePageOption: function (params, forceSave, noReload) {
+ // First check it is a website page
+ var mo;
+ this.trigger_up('main_object_request', {
+ callback: function (value) {
+ mo = value;
+ },
+ });
+ if (mo.model !== 'website.page') {
+ return Promise.reject();
+ }
+
+ // Check if this is a valid option
+ var option = this.pageOptions[params.name];
+ if (!option) {
+ return Promise.reject();
+ }
+
+ // Toggle the value
+ option.setValue(params.value);
+
+ // If simulate is true, it means we want the option to be toggled but
+ // not saved on the server yet
+ if (!forceSave) {
+ return Promise.resolve();
+ }
+
+ // If not, write on the server page and reload the current location
+ var vals = {};
+ vals[params.name] = option.value;
+ var prom = this._rpc({
+ model: 'website.page',
+ method: 'write',
+ args: [[mo.id], vals],
+ });
+ if (noReload) {
+ return prom;
+ }
+ return prom.then(function () {
+ window.location.reload();
+ return new Promise(function () {});
+ });
+ },
+});
+
+var PageManagement = Widget.extend({
+ xmlDependencies: ['/website/static/src/xml/website.xml'],
+ events: {
+ 'click a.js_page_properties': '_onPagePropertiesButtonClick',
+ 'click a.js_clone_page': '_onClonePageButtonClick',
+ 'click a.js_delete_page': '_onDeletePageButtonClick',
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Retrieves the page dependencies for the given object id.
+ *
+ * @private
+ * @param {integer} moID
+ * @returns {Promise<Array>}
+ */
+ _getPageDependencies: function (moID) {
+ return this._rpc({
+ model: 'website',
+ method: 'page_search_dependencies',
+ args: [moID],
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ _onPagePropertiesButtonClick: function (ev) {
+ var moID = $(ev.currentTarget).data('id');
+ var dialog = new PagePropertiesDialog(this,moID, {'fromPageManagement': true}).open();
+ return dialog;
+ },
+ _onClonePageButtonClick: function (ev) {
+ var pageId = $(ev.currentTarget).data('id');
+ _clonePage.call(this, pageId);
+ },
+ _onDeletePageButtonClick: function (ev) {
+ var pageId = $(ev.currentTarget).data('id');
+ _deletePage.call(this, pageId, true);
+ },
+});
+
+/**
+ * Deletes the page after showing a dependencies warning for the given page id.
+ *
+ * @private
+ * @param {integer} pageId - The ID of the page to be deleted
+ * @param {Boolean} fromPageManagement
+ * Is the function called by the page manager?
+ * It will affect redirect after page deletion: reload or '/'
+ */
+// TODO: This function should be integrated in a widget in the future
+function _deletePage(pageId, fromPageManagement) {
+ var self = this;
+ new Promise(function (resolve, reject) {
+ // Search the page dependencies
+ self._getPageDependencies(pageId)
+ .then(function (dependencies) {
+ // Inform the user about those dependencies and ask him confirmation
+ return new Promise(function (confirmResolve, confirmReject) {
+ Dialog.safeConfirm(self, "", {
+ title: _t("Delete Page"),
+ $content: $(qweb.render('website.delete_page', {dependencies: dependencies})),
+ confirm_callback: confirmResolve,
+ cancel_callback: resolve,
+ });
+ });
+ }).then(function () {
+ // Delete the page if the user confirmed
+ return self._rpc({
+ model: 'website.page',
+ method: 'unlink',
+ args: [pageId],
+ });
+ }).then(function () {
+ if (fromPageManagement) {
+ window.location.reload(true);
+ } else {
+ window.location.href = '/';
+ }
+ }, reject);
+ });
+}
+/**
+ * Duplicate the page after showing the wizard to enter new page name.
+ *
+ * @private
+ * @param {integer} pageId - The ID of the page to be duplicate
+ *
+ */
+function _clonePage(pageId) {
+ var self = this;
+ new Promise(function (resolve, reject) {
+ Dialog.confirm(this, undefined, {
+ title: _t("Duplicate Page"),
+ $content: $(qweb.render('website.duplicate_page_action_dialog')),
+ confirm_callback: function () {
+ var new_page_name = this.$('#page_name').val();
+ return self._rpc({
+ model: 'website.page',
+ method: 'clone_page',
+ args: [pageId, new_page_name],
+ }).then(function (path) {
+ window.location.href = path;
+ }).guardedCatch(reject);
+ },
+ cancel_callback: reject,
+ }).on('closed', null, reject);
+ });
+}
+
+websiteNavbarData.websiteNavbarRegistry.add(ContentMenu, '#content-menu');
+websiteRootData.websiteRootRegistry.add(PageManagement, '#list_website_pages');
+
+return {
+ PagePropertiesDialog: PagePropertiesDialog,
+ ContentMenu: ContentMenu,
+ EditMenuDialog: EditMenuDialog,
+ MenuEntryDialog: MenuEntryDialog,
+ SelectEditMenuDialog: SelectEditMenuDialog,
+};
+});