[FW][MERGE] knowledge: improve chatter usage and implementation after first user testing
Improvements to the chatter in Knowledge after the first merge. Fix the issue where the chatter would not render properly if a tracked file was modified (this sends a message in the chatter and it was not properly reloaded). Prevent the full reload of the view in cases where only the chatter needs to be reloaded. In Knowledge, if the param `keepChanges` is set to `true`, only the chatter will be updated. Update the style of the chatter button to add more contrast when the chatter is toggled on. Added a minimal size for the width of the chatter in side mode so that thread messages don't become too small. Add `_mail_post_access = 'read'` to the model in order to allow someone with read access to post chatter messages. This is useful when a private article is shared with read only access rights. Add an explicit message along with a tracked field change in case the change is caused by a user which does not have 'write' access on the modified article. Task-2858428 closes odoo/odoo#93088 Forward-port-of: odoo/odoo#91969 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -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("<p>%s</p>" % message_body)
|
||||
return changes, tracking_value_ids
|
||||
|
||||
def _send_invite_mail(self, partners):
|
||||
# TDE NOTE: try to cleanup and batchize
|
||||
self.ensure_one()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,17 +69,18 @@
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex flex-shrink-0 align-items-center pr-2">
|
||||
<a type="button" role="button" class="btn btn-secondary btn-create text-capitalize mr-1" data-hotkey="c">
|
||||
<a type="button" role="button" class="btn btn-light btn-create text-capitalize mr-1" data-hotkey="c">
|
||||
<i class="fa fa-plus-circle"/> Create
|
||||
</a>
|
||||
<div class="btn-group" role="toolbar" aria-label="options">
|
||||
<div class="o-dropdown dropdown o-dropdown--no-caret">
|
||||
<a type="button" role="button" class="btn btn-secondary btn-share dropdown-toggle mr-1"
|
||||
<a type="button" role="button" class="btn btn-light btn-share dropdown-toggle mr-1"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-hotkey="s"
|
||||
attrs="{'invisible': [('id', '=', False)]}">
|
||||
<i class="fa fa-share-alt"/> Share
|
||||
</a>
|
||||
<a type="button" role="button" class="btn btn-secondary btn-chatter mr-1" data-hotkey="d">
|
||||
<a type="button" role="button" class="btn btn-light btn-chatter mr-1" data-hotkey="d"
|
||||
attrs="{'invisible': [('id', '=', False)]}">
|
||||
<i class="rounded-circle text-center fa fa-comments" title="Open chatter"/>
|
||||
</a>
|
||||
<div class="o_knowledge_share_panel dropdown-menu" role="menu">
|
||||
@@ -96,7 +97,7 @@
|
||||
</div>
|
||||
<div class="btn-group" role="toolbar" aria-label="options">
|
||||
<div class="o-dropdown dropdown o-dropdown--no-caret">
|
||||
<a type="button" role="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown"
|
||||
<a type="button" role="button" class="btn btn-light dropdown-toggle" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" data-hotkey="p">
|
||||
<i class="fa fa-ellipsis-v"/>
|
||||
</a>
|
||||
@@ -203,13 +204,7 @@
|
||||
</div>
|
||||
<!-- Chatter -->
|
||||
<div class="o_knowledge_chatter col-12 col-lg-4 position-relative d-none d-print-none p-0">
|
||||
<div class="o_scroll_view_lg">
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" groups="base.group_user"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_scroll_view_lg o_knowledge_chatter_container"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -519,7 +519,7 @@ registerModel({
|
||||
default_model: this.composer.activeThread.model,
|
||||
default_partner_ids: this.composer.recipients.map(partner => partner.id),
|
||||
default_res_id: this.composer.activeThread.id,
|
||||
mail_post_autofollow: true,
|
||||
mail_post_autofollow: this.composer.activeThread.hasWriteAccess,
|
||||
};
|
||||
|
||||
const action = {
|
||||
@@ -579,7 +579,7 @@ registerModel({
|
||||
subtype_xmlid: composer.isLog ? 'mail.mt_note' : 'mail.mt_comment',
|
||||
});
|
||||
if (!composer.isLog) {
|
||||
params.context = { mail_post_autofollow: true };
|
||||
params.context = { mail_post_autofollow: this.composer.activeThread.hasWriteAccess };
|
||||
}
|
||||
}
|
||||
if (this.threadView && this.threadView.replyingToMessageView && this.threadView.thread !== this.messaging.inbox) {
|
||||
|
||||
Reference in New Issue
Block a user