From 4ed03d5940bb9a76b2c250f56169facf7b06b1dc Mon Sep 17 00:00:00 2001 From: abd-msyukyu-odoo Date: Wed, 25 May 2022 10:54:54 +0000 Subject: [PATCH 1/6] [IMP] knowledge: reorder functions alphabetically Small reorganisation of the code to order functions alphabetically in knowledge_controller and knowledge_renderer Task-2858428 X-original-commit: 874dc4a93af72c706ff756b29af96f7030a3423a Part-of: odoo/odoo#93088 --- .../static/src/js/knowledge_controller.js | 342 ++++++++-------- .../static/src/js/knowledge_renderers.js | 379 +++++++++--------- 2 files changed, 340 insertions(+), 381 deletions(-) diff --git a/addons/knowledge/static/src/js/knowledge_controller.js b/addons/knowledge/static/src/js/knowledge_controller.js index 3a4b31ee022..4d3132f1664 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,28 @@ const KnowledgeArticleFormController = FormController.extend({ this._super.apply(this, arguments); this.onFieldSavedListeners = new Map(); }, - + /** + * @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 +78,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 +98,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 +120,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 +136,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 +239,6 @@ const KnowledgeArticleFormController = FormController.extend({ }); dialog.open(); }, - /** * @param {Event} event */ @@ -205,7 +246,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 +268,6 @@ const KnowledgeArticleFormController = FormController.extend({ searchValue: "?", }); }, - /** * @param {Event} event */ @@ -237,69 +289,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 +341,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 +407,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 +426,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..ea7d987ca38 100644 --- a/addons/knowledge/static/src/js/knowledge_renderers.js +++ b/addons/knowledge/static/src/js/knowledge_renderers.js @@ -22,7 +22,6 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin 'click .o_knowledge_share_panel': '_preventDropdownClose', 'click .o_knowledge_more_options_panel': '_preventDropdownClose', }), - /** * @override */ @@ -30,7 +29,186 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin this._super(...arguments); this.breadcrumbs = params.breadcrumbs; }, - + /** + * @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') + }); + }, + /** + * 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') + } + }); + }, + /** + * 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); + }, + /** + * 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 +299,7 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin }); // Allow drag and drop between sections: + this.$('section[data-section="workspace"] .o_tree').nestedSortable( 'option', 'connectWith', @@ -138,202 +317,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 { From 35c8064b1aa2aa94e884f178bc8081333201ab86 Mon Sep 17 00:00:00 2001 From: abd-msyukyu-odoo Date: Fri, 20 May 2022 15:20:38 +0000 Subject: [PATCH 2/6] [FIX] knowledge: fix blank chatter when changing tracked fields How to reproduce * Open an article and edit one of the tracked fields (title, root_article_id, parent_id). * Open another article * Re-open the first article * Open the chatter Current behavior The chatter does not appear. Expected behavior The chatter should appear. Fix Instead of loading the chatter with the form view, load the chatter when the user clicks on the chatter button. Implements the use of the chatter component differently from the form_renderer in mail, to be able to update/render it independently from the rest of the view. Hide the chatter button if there is no article in the form view since there is no chatter to display in this case. Task-2858428 X-original-commit: b705ccf952cd67dad4f1480928c94cd7215b601a Part-of: odoo/odoo#93088 --- .../static/src/js/knowledge_renderers.js | 78 ++++++++++++++++++- .../static/src/scss/knowledge_views.scss | 8 -- .../views/knowledge_article_views.xml | 11 +-- 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/addons/knowledge/static/src/js/knowledge_renderers.js b/addons/knowledge/static/src/js/knowledge_renderers.js index ea7d987ca38..3c55806ff84 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', @@ -28,6 +33,30 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin 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, + }; + }, + /** + * @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 @@ -93,6 +122,17 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin 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 @@ -107,6 +147,21 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin } }); }, + /** + * 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 @@ -132,6 +187,27 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin }); 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 diff --git a/addons/knowledge/static/src/scss/knowledge_views.scss b/addons/knowledge/static/src/scss/knowledge_views.scss index f22883ad6cd..d8967a21bd2 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 { diff --git a/addons/knowledge/views/knowledge_article_views.xml b/addons/knowledge/views/knowledge_article_views.xml index 8b9d2d5aaee..4fdec0ea544 100644 --- a/addons/knowledge/views/knowledge_article_views.xml +++ b/addons/knowledge/views/knowledge_article_views.xml @@ -79,7 +79,8 @@ attrs="{'invisible': [('id', '=', False)]}"> Share - +
-
-
- - - -
-
+
From 8a24a7e63da2c84be5d6eefe23edaa6ccd2facd8 Mon Sep 17 00:00:00 2001 From: abd-msyukyu-odoo Date: Fri, 20 May 2022 15:21:51 +0000 Subject: [PATCH 3/6] [IMP] knowledge: prevent article reload on chatter reload Currently, when the chatter has to reload, it triggers a reload event for the parent view. In the case of Knowledge, we want to be able to exclusively reload the chatter because nothing changed in the article. In Knowledge, if the param `keepChanges` is used with reload, the full form view does not need to be fully reloaded. In this case, only the chatter will be updated. Task-2858428 X-original-commit: 91e268b580614da224f044061d01e69f09a5034d Part-of: odoo/odoo#93088 --- .../static/src/js/knowledge_controller.js | 13 +++++++++++++ .../static/src/js/knowledge_renderers.js | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/addons/knowledge/static/src/js/knowledge_controller.js b/addons/knowledge/static/src/js/knowledge_controller.js index 4d3132f1664..61ac267665d 100644 --- a/addons/knowledge/static/src/js/knowledge_controller.js +++ b/addons/knowledge/static/src/js/knowledge_controller.js @@ -48,6 +48,19 @@ 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 */ diff --git a/addons/knowledge/static/src/js/knowledge_renderers.js b/addons/knowledge/static/src/js/knowledge_renderers.js index 3c55806ff84..dafadca2b37 100644 --- a/addons/knowledge/static/src/js/knowledge_renderers.js +++ b/addons/knowledge/static/src/js/knowledge_renderers.js @@ -45,6 +45,21 @@ const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin 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 */ From 8a2380bf3698fb1f442e4b15cbc404f3b46d536b Mon Sep 17 00:00:00 2001 From: abd-msyukyu-odoo Date: Fri, 20 May 2022 15:22:41 +0000 Subject: [PATCH 4/6] [IMP] knowledge: restyle the chatter button and size When the chatter is in side mode (lg+ sizes), its size can become quite small (since it is proportional with the width of the viewport). Add a static minimal width so that the display stays readable. Change the border color of the buttons in the knowledge header so that when the chatter is displayed, it has a better contrast (the chatter button is a toggle on-off). Task-2858428 X-original-commit: 7d07f6f52886d1d7b86fd008fab579ffd8e71702 Part-of: odoo/odoo#93088 --- addons/knowledge/static/src/scss/knowledge_views.scss | 1 + addons/knowledge/views/knowledge_article_views.xml | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/knowledge/static/src/scss/knowledge_views.scss b/addons/knowledge/static/src/scss/knowledge_views.scss index d8967a21bd2..d463b6deea1 100644 --- a/addons/knowledge/static/src/scss/knowledge_views.scss +++ b/addons/knowledge/static/src/scss/knowledge_views.scss @@ -178,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 4fdec0ea544..ff708cbcf07 100644 --- a/addons/knowledge/views/knowledge_article_views.xml +++ b/addons/knowledge/views/knowledge_article_views.xml @@ -69,17 +69,17 @@
- + Create