diff --git a/addons/knowledge/models/knowledge_article.py b/addons/knowledge/models/knowledge_article.py index d62d2bf59ce..c64e29605dc 100644 --- a/addons/knowledge/models/knowledge_article.py +++ b/addons/knowledge/models/knowledge_article.py @@ -20,6 +20,7 @@ class Article(models.Model): _description = "Knowledge Article" _inherit = ['mail.thread', 'mail.activity.mixin'] _order = "favorite_count, create_date desc, id desc" + _mail_post_access = 'read' active = fields.Boolean(default=True) name = fields.Char(string="Title", default=lambda self: _('New Article'), required=True, tracking=20) @@ -1412,6 +1413,15 @@ class Article(models.Model): # MAILING # ------------------------------------------------------------ + def _mail_track(self, tracked_fields, initial): + changes, tracking_value_ids = super(Article, self)._mail_track(tracked_fields, initial) + if {'parent_id', 'root_article_id'} & changes and not self.user_has_write_access: + partner_name = self.env.user.partner_id.display_name + message_body = _("Logging changes from %(partner_name)s without write access on article %(article_name)s due to hierarchy tree update", + partner_name=partner_name, article_name=self.display_name) + self._track_set_log_message("
%s
" % message_body) + return changes, tracking_value_ids + def _send_invite_mail(self, partners): # TDE NOTE: try to cleanup and batchize self.ensure_one() diff --git a/addons/knowledge/static/src/js/knowledge_controller.js b/addons/knowledge/static/src/js/knowledge_controller.js index 3a4b31ee022..61ac267665d 100644 --- a/addons/knowledge/static/src/js/knowledge_controller.js +++ b/addons/knowledge/static/src/js/knowledge_controller.js @@ -19,7 +19,6 @@ const KnowledgeArticleFormController = FormController.extend({ 'click i.o_toggle_favorite': '_onToggleFavorite', 'input .o_breadcrumb_article_name': '_adjustInputSize', }), - custom_events: Object.assign({}, FormController.prototype.custom_events, { create: '_onCreate', duplicate: '_onDuplicate', @@ -28,7 +27,6 @@ const KnowledgeArticleFormController = FormController.extend({ reload_tree: '_onReloadTree', emoji_click: '_onEmojiClick', }), - /** * Register the fact that the current @see FormController is one from * Knowledge in order not to try and search for a matching record for @@ -50,14 +48,41 @@ const KnowledgeArticleFormController = FormController.extend({ this._super.apply(this, arguments); this.onFieldSavedListeners = new Map(); }, - + /** + * If a reload is called with param 'keepChanges', only the chatter needs + * to be updated, no need for a full reload + * + * @override + */ + reload: function (params) { + if (params && params.keepChanges) { + return this.renderer.updateChatter(); + } else { + return this._super.apply(this, arguments); + } + }, + /** + * @override + */ + saveRecord: async function () { + const modifiedFields = await this._super(...arguments); + const { data } = this.model.get(this.handle); + for (const field of modifiedFields) { + if (this.onFieldSavedListeners.has(field)) { + this.onFieldSavedListeners.get(field).forEach(listener => { + listener.call(this, data[field]); + }); + } + } + return modifiedFields; + }, /** * @override * @returns {Promise} */ start: function () { return this._super.apply(this, arguments).then(() => { - this.onFieldSaved('icon', unicode => { + this._onFieldSaved('icon', unicode => { const { id } = this.getState(); this.renderer._setEmoji(id, unicode); }); @@ -66,6 +91,18 @@ const KnowledgeArticleFormController = FormController.extend({ // Listeners: + /** + * @param {Event} event + */ + _adjustInputSize: function (event) { + event.target.setAttribute('size', event.target.value.length); + }, + _onAddCover: async function() { + if (this.mode === 'readonly') { + await this._setMode('edit'); + } + this.$('.o_input_file').click(); + }, _onAddRandomIcon: function() { this.trigger_up('field_changed', { dataPointID: this.handle, @@ -74,50 +111,13 @@ const KnowledgeArticleFormController = FormController.extend({ } }); }, - - _onAddCover: async function() { - if (this.mode === 'readonly') { - await this._setMode('edit'); - } - this.$('.o_input_file').click(); - }, - - /** - * When the user clicks on a field in readonly mode, a new 'quick_edit' event - * will be triggered. To prevent the view from switching to the edit mode when - * the article is locked, we will overwrite the `_onQuickEdit` handler. This - * function will now ignore the event if the article is locked. - * @override - */ - _onQuickEdit: function () { - const { data } = this.model.get(this.handle); - if (data.is_locked) { - return; - } - this._super.apply(this, arguments); - }, - - /** - * Callback function called when the user renames the active article. - * The function will update the name of the articles in the aside block. - * @param {Event} event - */ - _onRename: async function (event) { - var name = event.currentTarget.value; - if (name.length === 0) { - name = _t('New Article'); - } - const id = await this._getId(); - await this._rename(id, name); - }, - /** * When the user clicks on the name of the article, checks if the article * name hasn't been set yet. If it hasn't, it will look for a title in the * body of the article and set it as the name of the article. * @param {Event} event */ - _onArticleBreadcrumbClick: async function (event) { + _onArticleBreadcrumbClick: async function (event) { const name = event.currentTarget.value; if (name === _t('New Article')) { const $articleTitle = this.$('.o_knowledge_editor h1'); @@ -133,14 +133,12 @@ const KnowledgeArticleFormController = FormController.extend({ } } }, - /** - * @param {Event} event + * @param {OdooEvent} event */ - _adjustInputSize: function (event) { - event.target.setAttribute('size', event.target.value.length); + _onCreate: async function (event) { + await this._create(event.data); }, - /** * @param {OdooEvent} event */ @@ -151,21 +149,78 @@ const KnowledgeArticleFormController = FormController.extend({ additional_context: { res_id } }); }, - /** - * @param {OdooEvent} event + * @param {Event} event */ - _onCreate: async function (event) { - await this._create(event.data); + _onEmojiClick: async function (event) { + const { id } = this.getState(); + const { articleId, unicode } = event.data; + if (articleId === id) { + this.trigger_up('field_changed', { + dataPointID: this.handle, + changes: { + icon: unicode + } + }); + } else { + const result = await this._rpc({ + model: 'knowledge.article', + method: 'write', + args: [[articleId], { icon: unicode }], + }); + if (result) { + this.renderer._setEmoji(articleId, unicode); + } + } + }, + /** + * @override + * @param {Event} event + */ + _onFieldChanged: async function (event) { + this._super(...arguments); + const { changes } = event.data; + for (const field of this._getFieldsToForceSave()) { + if (changes.hasOwnProperty(field)) { + await this.saveRecord(this.handle, { + reload: false, + stayInEdit: true + }); + return; + } + } + }, + /** + * @param {String} name - field name + * @param {Function} callback + */ + _onFieldSaved: function (name, callback) { + if (this.onFieldSavedListeners.has(name)) { + this.onFieldSavedListeners.get(name).push(callback); + } else { + this.onFieldSavedListeners.set(name, [callback]); + } + }, + /** + * When the user clicks on a field in readonly mode, a new 'quick_edit' event + * will be triggered. To prevent the view from switching to the edit mode when + * the article is locked, we will overwrite the `_onQuickEdit` handler. This + * function will now ignore the event if the article is locked. + * @override + */ + _onQuickEdit: function () { + const { data } = this.model.get(this.handle); + if (data.is_locked) { + return; + } + this._super.apply(this, arguments); }, - /** * @param {Event} event */ _onMove: function (event) { this._confirmMove(event.data); }, - /** * Opens the "Move To" modal * @param {OdooEvent} event @@ -197,7 +252,6 @@ const KnowledgeArticleFormController = FormController.extend({ }); dialog.open(); }, - /** * @param {Event} event */ @@ -205,7 +259,19 @@ const KnowledgeArticleFormController = FormController.extend({ // TODO JBN: Create a widget for the tree and reload it without reloading the whole view. this.reload(); }, - + /** + * Callback function called when the user renames the active article. + * The function will update the name of the articles in the aside block. + * @param {Event} event + */ + _onRename: async function (event) { + var name = event.currentTarget.value; + if (name.length === 0) { + name = _t('New Article'); + } + const id = await this._getId(); + await this._rename(id, name); + }, /** * @param {Event} event */ @@ -215,7 +281,6 @@ const KnowledgeArticleFormController = FormController.extend({ searchValue: "?", }); }, - /** * @param {Event} event */ @@ -237,69 +302,10 @@ const KnowledgeArticleFormController = FormController.extend({ this.renderer._setTreeFavoriteListener(); this.renderer._renderEmojiPicker($dom); }); - }, - - /** - * @param {Event} event - */ - _onEmojiClick: async function (event) { - const { id } = this.getState(); - const { articleId, unicode } = event.data; - if (articleId === id) { - this.trigger_up('field_changed', { - dataPointID: this.handle, - changes: { - icon: unicode - } - }); - } else { - const result = await this._rpc({ - model: 'knowledge.article', - method: 'write', - args: [[articleId], { icon: unicode }], - }); - if (result) { - this.renderer._setEmoji(articleId, unicode); - } - } }, + // API calls: - /** - * @param {Object} data - * @param {String} data.category - * @param {integer} data.target_parent_id - */ - _create: async function (data) { - const articleId = await this._rpc({ - model: 'knowledge.article', - method: 'article_create', - args: [[]], - kwargs: { - is_private: data.category === 'private', - parent_id: data.target_parent_id ? data.target_parent_id : false - }, - }); - if (!articleId) { - return; - } - this.do_action('knowledge.ir_actions_server_knowledge_home_page', { - stackPosition: 'replaceCurrentAction', - additional_context: { - res_id: articleId - } - }); - }, - - /** - * @param {integer} id - Target id - * @param {string} name - Target Name - */ - _rename: async function (id, name) { - this.$(`.o_knowledge_tree .o_article[data-article-id="${id}"] > .o_article_handle > .o_article_name`).text(name); - this.$(`.o_breadcrumb_article_name`).val(name); - }, - /** * @param {Object} data * @param {integer} data.article_id @@ -348,7 +354,48 @@ const KnowledgeArticleFormController = FormController.extend({ }); } }, - + /** + * @param {Object} data + * @param {String} data.category + * @param {integer} data.target_parent_id + */ + _create: async function (data) { + const articleId = await this._rpc({ + model: 'knowledge.article', + method: 'article_create', + args: [[]], + kwargs: { + is_private: data.category === 'private', + parent_id: data.target_parent_id ? data.target_parent_id : false + }, + }); + if (!articleId) { + return; + } + this.do_action('knowledge.ir_actions_server_knowledge_home_page', { + stackPosition: 'replaceCurrentAction', + additional_context: { + res_id: articleId + } + }); + }, + /** + * @returns {Array[String]} + */ + _getFieldsToForceSave: function () { + return ['full_width', 'icon', 'cover']; + }, + /** + * @returns {integer} + */ + _getId: async function () { + let state = this.getState(); + if (typeof state.id === 'undefined') { + await this.saveRecord(this.handle); + state = this.getState(); + } + return state.id; + }, /** * @param {Object} data * @param {integer} data.article_id @@ -373,7 +420,14 @@ const KnowledgeArticleFormController = FormController.extend({ data.onReject(); }) }, - + /** + * @param {integer} id - Target id + * @param {string} name - Target Name + */ + _rename: async function (id, name) { + this.$(`.o_knowledge_tree .o_article[data-article-id="${id}"] > .o_article_handle > .o_article_name`).text(name); + this.$(`.o_breadcrumb_article_name`).val(name); + }, /** * @param {integer} id - article id * @returns {Promise} @@ -385,71 +439,6 @@ const KnowledgeArticleFormController = FormController.extend({ args: [id] }); }, - - /** - * @returns {Array[String]} - */ - _getFieldsToForceSave: function () { - return ['full_width', 'icon', 'cover']; - }, - - /** - * @override - * @param {Event} event - */ - _onFieldChanged: async function (event) { - this._super(...arguments); - const { changes } = event.data; - for (const field of this._getFieldsToForceSave()) { - if (changes.hasOwnProperty(field)) { - await this.saveRecord(this.handle, { - reload: false, - stayInEdit: true - }); - return; - } - } - }, - - /** - * @override - */ - saveRecord: async function () { - const modifiedFields = await this._super(...arguments); - const { data } = this.model.get(this.handle); - for (const field of modifiedFields) { - if (this.onFieldSavedListeners.has(field)) { - this.onFieldSavedListeners.get(field).forEach(listener => { - listener.call(this, data[field]); - }); - } - } - return modifiedFields; - }, - - /** - * @param {String} name - field name - * @param {Function} callback - */ - onFieldSaved: function (name, callback) { - if (this.onFieldSavedListeners.has(name)) { - this.onFieldSavedListeners.get(name).push(callback); - } else { - this.onFieldSavedListeners.set(name, [callback]); - } - }, - - /** - * @returns {integer} - */ - _getId: async function () { - let state = this.getState(); - if (typeof state.id === 'undefined') { - await this.saveRecord(this.handle); - state = this.getState(); - } - return state.id; - }, }); export { diff --git a/addons/knowledge/static/src/js/knowledge_renderers.js b/addons/knowledge/static/src/js/knowledge_renderers.js index 716477e42b8..dafadca2b37 100644 --- a/addons/knowledge/static/src/js/knowledge_renderers.js +++ b/addons/knowledge/static/src/js/knowledge_renderers.js @@ -6,11 +6,16 @@ import FormRenderer from 'web.FormRenderer'; import KnowledgeTreePanelMixin from '@knowledge/js/tools/tree_panel_mixin'; import { qweb as QWeb } from 'web.core'; +import { ChatterContainer } from '@mail/components/chatter_container/chatter_container'; +import { ComponentWrapper } from 'web.OwlCompatibility'; + +class ChatterContainerWrapperComponent extends ComponentWrapper {} const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin, { className: 'o_knowledge_form_view', events: _.extend({}, FormRenderer.prototype.events, { - 'click .btn-chatter': '_onBtnChatterClick', + 'click .btn-chatter.active': '_onCloseChatter', + 'click .btn-chatter:not(.active)': '_onOpenChatter', 'click .btn-create': '_onBtnCreateClick', 'click .btn-duplicate': '_onBtnDuplicateClick', 'click .btn-move': '_onBtnMoveClick', @@ -22,15 +27,279 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin 'click .o_knowledge_share_panel': '_preventDropdownClose', 'click .o_knowledge_more_options_panel': '_preventDropdownClose', }), - /** * @override */ init: function (parent, state, params) { this._super(...arguments); this.breadcrumbs = params.breadcrumbs; + /** + * Manually set values for the chatter props as it is custom handled + * and not loaded with the view (oe_chatter is not used in Knowledge, + * the chatter is only rendered when the user clicks on the button). + */ + this.chatterFields = { + // `knowledge.article` has `mail.activity.mixin` + hasActivities: true, + hasFollowers: true, + hasMessageIds: true, + }; + }, + /** + * Called when the chatter triggers a reload on the Form view with the + * param 'keepChanges=true'. In this case, we only need to update the + * chatter. + * + * @returns {Promise} + */ + updateChatter: function () { + if (this._chatterContainerComponent && this.state.res_id) { + const props = this._makeChatterContainerProps(); + return this._chatterContainerComponent.update(props); + } + this._closeChatter(); + return Promise.resolve(); + }, + /** + * @private + */ + _closeChatter: function () { + const chatter = this.el.querySelector('.o_knowledge_chatter_container'); + while (chatter.firstChild) { + chatter.removeChild(chatter.firstChild); + } + if (this._chatterContainerComponent) { + this._chatterContainerComponent.destroy(); + this._chatterContainerComponent = undefined; + } + }, + /** + * @param {Event} event + */ + _onBreadcrumbItemClick: function (event) { + const $target = $(event.target); + this.trigger_up('breadcrumb_clicked', { + controllerID: $target.data('controller-id'), + }); + }, + /** + * Callback function called when the user clicks on the '+' sign of an article + * list item. The callback function will create a new article under the target article. + * @param {Event} event + */ + _onBtnArticleCreateClick: function (event) { + const $target = $(event.currentTarget); + const $li = $target.closest('li'); + this.trigger_up('create', { + target_parent_id: $li.data('article-id') + }); + }, + /** + * @param {Event} event + */ + _onBtnChatterClick: function (event) { + const $chatter = $('.o_knowledge_chatter'); + $chatter.toggleClass('d-none'); + $('.btn-chatter').toggleClass('active'); + }, + /** + * @param {Event} event + */ + _onBtnCreateClick: function (event) { + this.trigger_up('create', { + category: 'private' + }); + }, + /** + * @param {Event} event + */ + _onBtnDuplicateClick: function (event) { + event.preventDefault(); + this.trigger_up('duplicate', {}); + }, + /** + * @param {Event} event + */ + _onBtnMoveClick: function (event) { + event.preventDefault(); + this.trigger_up('open_move_to_modal', {}); + }, + /** + * Callback function called when the user clicks on the '+' sign of a section + * (workspace, private, shared). The callback function will create a new article + * on the root of the target section. + * @param {Event} event + */ + _onBtnSectionCreateClick: function (event) { + const $target = $(event.currentTarget); + const $section = $target.closest('.o_section'); + this.trigger_up('create', { + category: $section.data('section') + }); + }, + /** + * Update the btn-chatter appearance and hide the chatter section in the + * view. Closes the chatter. + * + * @private + */ + _onCloseChatter: function () { + this.el.querySelector('.o_knowledge_chatter').classList.add('d-none'); + this.el.querySelector('.btn-chatter').classList.remove('active'); + this._closeChatter(); + }, + /** + * Opens the selected record. + * @param {Event} event + */ + _onOpen: async function (event) { + event.stopPropagation(); + const $li = $(event.target).closest('li'); + this.do_action('knowledge.ir_actions_server_knowledge_home_page', { + stackPosition: 'replaceCurrentAction', + additional_context: { + res_id: $li.data('article-id') + } + }); + }, + /** + * Update tbe btn-chatter appearance and show the chatter section in the + * view. Open the chatter. + * + * @private + * @param {OdooEvent} ev + */ + _onOpenChatter: async function (ev) { + ev.stopPropagation(); + if (this.state.res_id) { + this.el.querySelector('.o_knowledge_chatter').classList.remove('d-none'); + this.el.querySelector('.btn-chatter').classList.add('active'); + await this._renderChatter(); + } + }, + /** + * By default, Bootstrap closes automatically the dropdown menu when the user + * clicks inside it. To avoid that behavior, we will add a new event listener + * on the dropdown menu that will prevent the click event from bubbling up and + * triggering the listener closing the dropdown menu. + * @param {Event} event + */ + _preventDropdownClose: function (event) { + event.stopPropagation(); + }, + _renderArticleEmoji: function () { + const { data } = this.state; + const $dropdown = this.$('.o_knowledge_icon > .o_article_emoji_dropdown'); + $dropdown.attr('data-article-id', this.state.res_id); + $dropdown.find('.o_article_emoji').text(data.icon || ''); + }, + /** + * Renders the breadcrumb + */ + _renderBreadcrumb: function () { + const items = this.breadcrumbs.map(payload => { + return QWeb.render('knowledge.knowledge_breadcrumb_item', { payload }); + }); + this.$('.breadcrumb').prepend(items); + }, + /** + * Render and mount the chatter of the current record. + * + * @private + * @returns {Promise} + */ + _renderChatter: async function () { + if (!this.state.res_id) { + return; + } + this._closeChatter(); + const props = this._makeChatterContainerProps(); + this._chatterContainerComponent = new ChatterContainerWrapperComponent( + this, + ChatterContainer, + props + ); + const target = this.el.querySelector('.o_knowledge_chatter_container'); + await this._chatterContainerComponent.mount(target); }, + /** + * Attaches an emoji picker widget to every emoji dropdown of the container. + * Note: The widget will be attached to the view when the user clicks on + * the dropdown menu for the first time. + * @param {JQuery} [$container] + */ + _renderEmojiPicker: function ($container) { + $container = $container || this.$el; + $container.find('.o_article_emoji_dropdown').one('click', event => { + const $dropdown = $(event.currentTarget); + const picker = new EmojiPickerWidget(this, { + article_id: $dropdown.data('article-id') + }); + picker.attachTo($dropdown); + }); + }, + /** + * Renders the permission panel + */ + _renderPermissionPanel: function () { + this.$('.btn-share').one('click', event => { + const $container = this.$('.o_knowledge_permission_panel'); + const panel = new PermissionPanelWidget(this, { + article_id: this.state.data.id, + user_permission: this.state.data.user_permission + }); + panel.attachTo($container); + }); + }, + /** + * @override + * @returns {Promise} + */ + _renderView: async function () { + const result = await this._super.apply(this, arguments); + this._renderBreadcrumb(); + await this._renderTree(this.state.res_id, '/knowledge/tree_panel'); + this._renderArticleEmoji(); + this._renderPermissionPanel(); + this._setResizeListener(); + return result; + }, + /** + * @param {integer} id - Article id + * @param {String} unicode + */ + _setEmoji: function (id, emoji) { + const emojis = this.$(`.o_article_emoji_dropdown[data-article-id="${id}"] > .o_article_emoji`); + emojis.text(emoji || ''); + }, + /** + * Enables the user to resize the aside block. + * Note: When the user grabs the resizer, a new listener will be attached + * to the document. The listener will be removed as soon as the user releases + * the resizer to free some resources. + */ + _setResizeListener: function () { + /** + * @param {PointerEvent} event + */ + const onPointerMove = _.throttle(event => { + event.preventDefault(); + this.el.style.setProperty('--default-sidebar-size', `${event.pageX}px`); + }, 100); + /** + * @param {PointerEvent} event + */ + const onPointerUp = event => { + $(document).off('pointermove', onPointerMove); + }; + const $resizer = this.$('.o_knowledge_article_form_resizer'); + $resizer.on('pointerdown', event => { + event.preventDefault(); + $(document).on('pointermove', onPointerMove); + $(document).one('pointerup', onPointerUp); + }); + }, /** * Initializes the drag-and-drop behavior of the tree listing all articles. * Once this function is called, the user will be able to move an article @@ -121,6 +390,7 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin }); // Allow drag and drop between sections: + this.$('section[data-section="workspace"] .o_tree').nestedSortable( 'option', 'connectWith', @@ -138,202 +408,6 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin 'section[data-section="workspace"] .o_tree, section[data-section="private"] .o_tree' ); }, - - /** - * Callback function called when the user clicks on the '+' sign of a section - * (workspace, private, shared). The callback function will create a new article - * on the root of the target section. - * @param {Event} event - */ - _onBtnSectionCreateClick: function (event) { - const $target = $(event.currentTarget); - const $section = $target.closest('.o_section'); - this.trigger_up('create', { - category: $section.data('section') - }); - }, - - /** - * Callback function called when the user clicks on the '+' sign of an article - * list item. The callback function will create a new article under the target article. - * @param {Event} event - */ - _onBtnArticleCreateClick: function (event) { - const $target = $(event.currentTarget); - const $li = $target.closest('li'); - this.trigger_up('create', { - target_parent_id: $li.data('article-id') - }); - }, - - /** - * Opens the selected record. - * @param {Event} event - */ - _onOpen: async function (event) { - event.stopPropagation(); - const $li = $(event.target).closest('li'); - this.do_action('knowledge.ir_actions_server_knowledge_home_page', { - stackPosition: 'replaceCurrentAction', - additional_context: { - res_id: $li.data('article-id') - } - }); - }, - - _renderArticleEmoji: function () { - const { data } = this.state; - const $dropdown = this.$('.o_knowledge_icon > .o_article_emoji_dropdown'); - $dropdown.attr('data-article-id', this.state.res_id); - $dropdown.find('.o_article_emoji').text(data.icon || ''); - }, - - /** - * @override - * @returns {Promise} - */ - _renderView: async function () { - const result = await this._super.apply(this, arguments); - this._renderBreadcrumb(); - await this._renderTree(this.state.res_id, '/knowledge/tree_panel'); - this._renderArticleEmoji(); - this._renderPermissionPanel(); - this._setResizeListener(); - return result; - }, - - /** - * Renders the breadcrumb - */ - _renderBreadcrumb: function () { - const items = this.breadcrumbs.map(payload => { - return QWeb.render('knowledge.knowledge_breadcrumb_item', { payload }); - }); - this.$('.breadcrumb').prepend(items); - }, - - /** - * Attaches an emoji picker widget to every emoji dropdown of the container. - * Note: The widget will be attached to the view when the user clicks on - * the dropdown menu for the first time. - * @param {JQuery} [$container] - */ - _renderEmojiPicker: function ($container) { - $container = $container || this.$el; - $container.find('.o_article_emoji_dropdown').one('click', event => { - const $dropdown = $(event.currentTarget); - const picker = new EmojiPickerWidget(this, { - article_id: $dropdown.data('article-id') - }); - picker.attachTo($dropdown); - }); - }, - - /** - * Renders the permission panel - */ - _renderPermissionPanel: function () { - this.$('.btn-share').one('click', event => { - const $container = this.$('.o_knowledge_permission_panel'); - const panel = new PermissionPanelWidget(this, { - article_id: this.state.data.id, - user_permission: this.state.data.user_permission - }); - panel.attachTo($container); - }); - }, - - /** - * Enables the user to resize the aside block. - * Note: When the user grabs the resizer, a new listener will be attached - * to the document. The listener will be removed as soon as the user releases - * the resizer to free some resources. - */ - _setResizeListener: function () { - /** - * @param {PointerEvent} event - */ - const onPointerMove = _.throttle(event => { - event.preventDefault(); - this.el.style.setProperty('--default-sidebar-size', `${event.pageX}px`); - }, 100); - /** - * @param {PointerEvent} event - */ - const onPointerUp = event => { - $(document).off('pointermove', onPointerMove); - }; - const $resizer = this.$('.o_knowledge_article_form_resizer'); - $resizer.on('pointerdown', event => { - event.preventDefault(); - $(document).on('pointermove', onPointerMove); - $(document).one('pointerup', onPointerUp); - }); - }, - - /** - * @param {integer} id - Article id - * @param {String} unicode - */ - _setEmoji: function (id, emoji) { - const emojis = this.$(`.o_article_emoji_dropdown[data-article-id="${id}"] > .o_article_emoji`); - emojis.text(emoji || ''); - }, - - /** - * @param {Event} event - */ - _onBreadcrumbItemClick: function (event) { - const $target = $(event.target); - this.trigger_up('breadcrumb_clicked', { - controllerID: $target.data('controller-id'), - }); - }, - - /** - * @param {Event} event - */ - _onBtnChatterClick: function (event) { - const $chatter = $('.o_knowledge_chatter'); - $chatter.toggleClass('d-none'); - $('.btn-chatter').toggleClass('active'); - }, - - /** - * @param {Event} event - */ - _onBtnCreateClick: function (event) { - this.trigger_up('create', { - category: 'private' - }); - }, - - /** - * @param {Event} event - */ - _onBtnDuplicateClick: function (event) { - event.preventDefault(); - this.trigger_up('duplicate', {}); - }, - - /** - * @param {Event} event - */ - _onBtnMoveClick: function (event) { - event.preventDefault(); - this.trigger_up('open_move_to_modal', {}); - }, - - /** - * By default, Bootstrap closes automatically the dropdown menu when the user - * clicks inside it. To avoid that behavior, we will add a new event listener - * on the dropdown menu that will prevent the click event from bubbling up and - * triggering the listener closing the dropdown menu. - * @param {Event} event - */ - _preventDropdownClose: function (event) { - event.stopPropagation(); - }, }); export { diff --git a/addons/knowledge/static/src/scss/knowledge_views.scss b/addons/knowledge/static/src/scss/knowledge_views.scss index f22883ad6cd..d463b6deea1 100644 --- a/addons/knowledge/static/src/scss/knowledge_views.scss +++ b/addons/knowledge/static/src/scss/knowledge_views.scss @@ -129,14 +129,6 @@ li.nav-item { display: none; } - .o_FormRenderer_chatterContainer { - max-width: unset; - &.o-aside { - width: auto; - padding: 1rem; - border: none; - } - } } .dropdown-menu.o_article_emoji_dropdown_panel { @@ -186,6 +178,7 @@ } .o_knowledge_chatter { border-left: 1px solid $border-color; + min-width: 400px; } } } diff --git a/addons/knowledge/views/knowledge_article_views.xml b/addons/knowledge/views/knowledge_article_views.xml index 8b9d2d5aaee..ff708cbcf07 100644 --- a/addons/knowledge/views/knowledge_article_views.xml +++ b/addons/knowledge/views/knowledge_article_views.xml @@ -69,17 +69,18 @@