diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index c231378b70b..a91b3c10c8e 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -79,6 +79,7 @@ ], 'web.assets_frontend': [ 'project/static/src/scss/portal_rating.scss', + 'project/static/src/scss/project_sharing_frontend.scss', 'project/static/src/js/portal_rating.js', ], 'web.qunit_suite_tests': [ @@ -95,18 +96,12 @@ ('remove', 'mail/static/src/components/dialog_manager/dialog_manager.js'), ('remove', 'mail/static/src/components/chat_window_manager/chat_window_manager.js'), + ('remove', 'mail/static/src/views/form/*'), - 'web/static/src/legacy/js/public/public_widget.js', - 'portal/static/src/js/portal_chatter.js', - 'portal/static/src/js/portal_composer.js', 'project/static/src/project_sharing/search/favorite_menu/custom_favorite_item.xml', - 'project/static/src/project_sharing/**/*.js', - 'project/static/src/scss/project_sharing/*', + 'project/static/src/project_sharing/**/*', 'web/static/src/start.js', 'web/static/src/legacy/legacy_setup.js', - - 'project/static/src/project_sharing/**/*.xml', - 'portal/static/src/xml/portal_chatter.xml', ], }, 'license': 'LGPL-3', diff --git a/addons/project/static/src/project_sharing/components/chatter.js b/addons/project/static/src/project_sharing/components/chatter.js deleted file mode 100644 index e1ad0d48a25..00000000000 --- a/addons/project/static/src/project_sharing/components/chatter.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @odoo-module **/ - -import { PortalChatter } from 'portal.chatter'; -import Composer from './composer'; - -export default PortalChatter.extend({ - messageFetch(domain) { - if (!this.options.res_id) { - return Promise.resolve(); - } - return this._super.apply(this, arguments); - }, - _chatterInit() { - if (!this.options.res_id) { - this.result = { messages: [] }; - return Promise.resolve(); - } - return this._super.apply(this, arguments); - }, - _messageFetchPrepareParams() { - const data = this._super.apply(this, arguments); - if (this.options.project_sharing_id) { - data.project_sharing_id = this.options.project_sharing_id; - } - return data; - }, - _createComposerWidget() { - return new Composer(this, this.options); - }, - update(props) { - this._setOptions(props); - this._reloadChatterContent(); - }, -}); diff --git a/addons/project/static/src/project_sharing/components/chatter.xml b/addons/project/static/src/project_sharing/components/chatter.xml deleted file mode 100644 index aca16ce257a..00000000000 --- a/addons/project/static/src/project_sharing/components/chatter.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - !!widget.options.res_id && widget.get('messages') || [] - - - diff --git a/addons/project/static/src/scss/project_sharing/chatter.scss b/addons/project/static/src/project_sharing/components/chatter/chatter.scss similarity index 89% rename from addons/project/static/src/scss/project_sharing/chatter.scss rename to addons/project/static/src/project_sharing/components/chatter/chatter.scss index 88f1d1262b4..745a2c40b54 100644 --- a/addons/project/static/src/scss/project_sharing/chatter.scss +++ b/addons/project/static/src/project_sharing/components/chatter/chatter.scss @@ -19,11 +19,12 @@ .o_portal_chatter_attachments { margin-bottom: 1rem; .o_portal_chatter_attachment { - .o_portal_chatter_attachment_delete { + > button { @include o-position-absolute($top: 0, $right: 0); opacity: 0; } - &:hover .o_portal_chatter_attachment_delete { + + &:hover > button { opacity: 1; } } @@ -55,7 +56,7 @@ .o_portal_chatter { .o_portal_chatter_header { padding-top: 1rem; - padding-bottom: 2px; + padding-bottom: 1px; } .o_portal_chatter_composer, .o_portal_chatter_messages { diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.js b/addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.js new file mode 100644 index 00000000000..ffb44832a63 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.js @@ -0,0 +1,15 @@ +/** @odoo-module */ + +const { Component } = owl; + +export class ChatterAttachmentsViewer extends Component {} + +ChatterAttachmentsViewer.template = 'project.ChatterAttachmentsViewer'; +ChatterAttachmentsViewer.props = { + attachments: Array, + canDelete: { type: Boolean, optional: true }, + delete: { type: Function, optional: true }, +}; +ChatterAttachmentsViewer.defaultProps = { + delete: async () => {}, +}; diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.xml b/addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.xml new file mode 100644 index 00000000000..691424bb4bd --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_composer.js b/addons/project/static/src/project_sharing/components/chatter/chatter_composer.js new file mode 100644 index 00000000000..f4219ea8acb --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_composer.js @@ -0,0 +1,134 @@ +/** @odoo-module */ + +import { useService } from '@web/core/utils/hooks'; +import { TextField } from '@web/views/fields/text/text_field'; +import { PortalAttachDocument } from '../portal_attach_document/portal_attach_document'; +import { ChatterAttachmentsViewer } from './chatter_attachments_viewer'; + +const { Component, useState, onWillUpdateProps } = owl; + +export class ChatterComposer extends Component { + setup() { + this.rpc = useService('rpc'); + this.state = useState({ + displayError: false, + attachments: this.props.attachments.map(file => file.state === 'done'), + message: '', + loading: false, + }); + + onWillUpdateProps(this.onWillUpdateProps); + } + + onWillUpdateProps(nextProps) { + this.clearErrors(); + this.state.message = ''; + this.state.attachments = nextProps.attachments.map(file => file.state === 'done'); + } + + get discussionUrl() { + return `${window.location.href.split('#')[0]}#discussion`; + } + + update(change) { + this.clearErrors(); + this.state.message = change; + } + + prepareMessageData() { + const attachment_ids = []; + const attachment_tokens = []; + for (const attachment of this.state.attachments) { + attachment_ids.push(attachment.id); + attachment_tokens.push(attachment.access_token); + } + return { + message: this.state.message, + attachment_ids, + attachment_tokens, + res_model: this.props.resModel, + res_id: this.props.resId, + project_sharing_id: this.props.projectSharingId, + }; + } + + async sendMessage() { + this.clearErrors(); + if (!this.state.message && !this.state.attachments.length) { + this.state.displayError = true; + return; + } + + await this.rpc( + "/mail/chatter_post", + this.prepareMessageData(), + ); + this.props.postProcessMessageSent(); + this.state.message = ""; + this.state.attachments = []; + } + + clearErrors() { + this.state.displayError = false; + } + + async beforeUploadFile() { + this.state.loading = true; + return true; + } + + onFileUpload(files) { + this.state.loading = false; + this.clearErrors(); + for (const file of files) { + file.state = 'pending'; + this.state.attachments.push(file); + } + } + + async deleteAttachment(attachment) { + this.clearErrors(); + try { + await this.rpc( + '/portal/attachment/remove', + { + attachment_id: attachment.id, + access_token: attachment.access_token, + }, + ); + } catch (err) { + console.error(err); + this.state.displayError = true; + } + this.state.attachments = this.state.attachments.filter(a => a.id !== attachment.id); + } +} + +ChatterComposer.components = { + ChatterAttachmentsViewer, + PortalAttachDocument, + TextField, +}; + +ChatterComposer.props = { + resModel: String, + projectSharingId: Number, + resId: { type: Number, optional: true }, + allowComposer: { type: Boolean, optional: true }, + displayComposer: { type: Boolean, optional: true }, + token: { type: String, optional: true }, + messageCount: { type: Number, optional: true }, + isUserPublic: { type: Boolean, optional: true }, + partnerId: { type: Number, optional: true }, + postProcessMessageSent: { type: Function, optional: true }, + attachments: { type: Array, optional: true }, +}; +ChatterComposer.defaultProps = { + allowComposer: true, + displayComposer: false, + isUserPublic: true, + token: '', + attachments: [], +}; + +ChatterComposer.template = 'project.ChatterComposer'; diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_composer.xml b/addons/project/static/src/project_sharing/components/chatter/chatter_composer.xml new file mode 100644 index 00000000000..6fda276a2a4 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_composer.xml @@ -0,0 +1,61 @@ + + + + + + + + Oops! Something went wrong. Try to reload the page and log in. + + + + + + + + + + + + Send + + + + + + + + + + + + Leave a comment + You must be logged in to post a comment. + + + + + diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_container.js b/addons/project/static/src/project_sharing/components/chatter/chatter_container.js new file mode 100644 index 00000000000..fc2e64a723d --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_container.js @@ -0,0 +1,172 @@ +/** @odoo-module */ + +import { formatDateTime, parseDateTime } from "@web/core/l10n/dates"; +import { useService } from "@web/core/utils/hooks"; +import { sprintf } from '@web/core/utils/strings'; +import { ChatterComposer } from "./chatter_composer"; +import { ChatterMessageCounter } from "./chatter_message_counter"; +import { ChatterMessages } from "./chatter_messages"; +import { ChatterPager } from "./chatter_pager"; + +const { Component, markup, onWillStart, useState, onWillUpdateProps } = owl; + +export class ChatterContainer extends Component { + setup() { + this.rpc = useService('rpc'); + this.state = useState({ + currentPage: this.props.pagerStart, + messages: [], + options: this.defaultOptions, + }); + + onWillStart(this.onWillStart); + onWillUpdateProps(this.onWillUpdateProps); + } + + get defaultOptions() { + return { + message_count: 0, + is_user_public: true, + is_user_employee: false, + is_user_published: false, + display_composer: Boolean(this.props.resId), + partner_id: null, + pager_scope: 4, + pager_step: 10, + }; + } + + get options() { + return this.state.options; + } + + set options(options) { + this.state.options = { + ...this.defaultOptions, + ...options, + display_composer: !!options.display_composer, + access_token: typeof options.display_composer === 'string' ? options.display_composer : '', + }; + } + + get composerProps() { + return { + allowComposer: Boolean(this.props.resId), + displayComposer: this.state.options.display_composer, + partnerId: this.state.options.partner_id || undefined, + token: this.state.options.access_token, + resModel: this.props.resModel, + resId: this.props.resId, + projectSharingId: this.props.projectSharingId, + postProcessMessageSent: async () => { + this.state.currentPage = 1; + await this.fetchMessages(); + }, + attachments: this.state.options.default_attachment_ids, + }; + } + + onWillStart() { + this.initChatter(this.messagesParams(this.props)); + } + + onWillUpdateProps(nextProps) { + if (nextProps.resId) { + this.initChatter(this.messagesParams(nextProps)); + } else { + this.state.messages = []; + this.options = {}; + } + } + + async onChangePage(page) { + this.state.currentPage = page; + await this.fetchMessages(); + } + + async initChatter(params) { + const chatterData = await this.rpc( + '/mail/chatter_init', + params, + ); + this.state.messages = this.preprocessMessages(chatterData.messages); + this.options = chatterData.options; + } + + async fetchMessages() { + const result = await this.rpc( + '/mail/chatter_fetch', + this.messagesParams(this.props), + ); + this.state.messages = this.preprocessMessages(result.messages); + this.state.options.message_count = result.message_count; + return result; + } + + messagesParams(props) { + const params = { + res_model: props.resModel, + res_id: props.resId, + limit: this.state.options.pager_step, + offset: (this.state.currentPage - 1) * this.state.options.pager_step, + allow_composer: Boolean(props.resId), + project_sharing_id: props.projectSharingId, + }; + if (props.token) { + params.token = props.token; + } + if (props.domain) { + params.domain = props.domain; + } + return params; + } + + preprocessMessages(messages) { + return messages.map(m => ({ + ...m, + author_avatar_url: sprintf('/web/image/mail.message/%s/author_avatar/50x50', m.id), + published_date_str: sprintf( + this.env._t('Published on %s'), + formatDateTime( + parseDateTime( + m.date, + { format: 'MM-dd-yyy HH:mm:ss' }, + ), + ) + ), + body: markup(m.body), + })); + } + + updateMessage(message_id, changes) { + Object.assign( + this.state.messages.find(m => m.id === message_id), + changes, + ); + } +} + +ChatterContainer.components = { + ChatterComposer, + ChatterMessageCounter, + ChatterMessages, + ChatterPager, +}; + +ChatterContainer.props = { + token: { type: String, optional: true }, + resModel: String, + resId: { type: Number, optional: true }, + pid: { type: String, optional: true }, + hash: { type: String, optional: true }, + pagerStart: { type: Number, optional: true }, + twoColumns: { type: Boolean, optional: true }, + projectSharingId: Number, +}; +ChatterContainer.defaultProps = { + token: '', + pid: '', + hash: '', + pagerStart: 1, +}; +ChatterContainer.template = 'project.ChatterContainer'; diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_container.xml b/addons/project/static/src/project_sharing/components/chatter/chatter_container.xml new file mode 100644 index 00000000000..35337763f90 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_container.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.js b/addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.js new file mode 100644 index 00000000000..479aa855623 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.js @@ -0,0 +1,10 @@ +/** @odoo-module */ + +const { Component } = owl; + +export class ChatterMessageCounter extends Component { } + +ChatterMessageCounter.props = { + count: Number, +}; +ChatterMessageCounter.template = 'project.ChatterMessageCounter'; diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.xml b/addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.xml new file mode 100644 index 00000000000..5451e4ccae4 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.xml @@ -0,0 +1,16 @@ + + + + + + + + comments + + + There are no comments for now. + + + + + diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_messages.js b/addons/project/static/src/project_sharing/components/chatter/chatter_messages.js new file mode 100644 index 00000000000..2c0e54824fa --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_messages.js @@ -0,0 +1,37 @@ +/** @odoo-module */ + +import { useService } from "@web/core/utils/hooks"; + +import { ChatterAttachmentsViewer } from "./chatter_attachments_viewer"; + +const { Component } = owl; + +export class ChatterMessages extends Component { + setup() { + this.rpc = useService('rpc'); + } + + /** + * Toggle the visibility of the message. + * + * @param {Object} message message to change the visibility + */ + async toggleMessageVisibility(message) { + const result = await this.rpc( + '/mail/update_is_internal', + { message_id: message.id, is_internal: !message.is_internal }, + ); + this.props.update(message.id, { is_internal: result }); + } +} + +ChatterMessages.template = 'project.ChatterMessages'; +ChatterMessages.props = { + messages: Array, + isUserEmployee: { type: Boolean, optional: true }, + update: { type: Function, optional: true }, +}; +ChatterMessages.defaultProps = { + update: (message_id, changes) => {}, +}; +ChatterMessages.components = { ChatterAttachmentsViewer }; diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_messages.xml b/addons/project/static/src/project_sharing/components/chatter/chatter_messages.xml new file mode 100644 index 00000000000..28ae6988e1d --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_messages.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + Internal Note + + + + Employees Only + + + Visible + + + + + + + + + + + + + + + + + + diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_pager.js b/addons/project/static/src/project_sharing/components/chatter/chatter_pager.js new file mode 100644 index 00000000000..edc8ffc7899 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_pager.js @@ -0,0 +1,64 @@ +/** @odoo-module */ + +const { Component, useState, onWillUpdateProps } = owl; + +export class ChatterPager extends Component { + setup() { + this.state = useState({ + disabledButtons: false, + pageCount: 1, + pageStart: 1, + pageEnd: 1, + pagePrevious: 1, + pageNext: 1, + pages: [1], + offset: 0, + }); + this.computePagerState(this.props); + + onWillUpdateProps(this.onWillUpdateProps); + } + + computePagerState(props) { + let page = props.page || 1; + let scope = props.pagerScope; + + const step = props.pagerStep; + + // Compute Pager + this.state.messageCount = Math.ceil(parseFloat(props.messageCount) / step); + + page = Math.max(1, Math.min(page, this.state.messageCount)); + + const pageStart = Math.max(page - parseInt(Math.floor(scope / 2)), 1); + this.state.pageEnd = Math.min(pageStart + scope, this.state.messageCount); + this.state.pageStart = Math.max(this.state.pageEnd - scope, 1); + + this.state.pages = Array.from( + {length: this.state.pageEnd - this.state.pageStart + 1}, + (_, i) => i + this.state.pageStart, + ); + this.state.pagePrevious = Math.max(this.state.pageStart, page - 1); + this.state.pageNext = Math.min(this.state.pageEnd, page + 1); + } + + onWillUpdateProps(nextProps) { + this.computePagerState(nextProps); + } + + async onPageChanged(page) { + this.state.disabledButtons = true; + await this.props.changePage(page); + this.state.disabledButtons = false; + } +} + +ChatterPager.props = { + pagerScope: Number, + pagerStep: Number, + page: Number, + messageCount: Number, + changePage: Function, +}; + +ChatterPager.template = 'project.ChatterPager'; diff --git a/addons/project/static/src/project_sharing/components/chatter/chatter_pager.xml b/addons/project/static/src/project_sharing/components/chatter/chatter_pager.xml new file mode 100644 index 00000000000..85b478d61de --- /dev/null +++ b/addons/project/static/src/project_sharing/components/chatter/chatter_pager.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/project/static/src/project_sharing/components/composer.js b/addons/project/static/src/project_sharing/components/composer.js deleted file mode 100644 index a7e75aad119..00000000000 --- a/addons/project/static/src/project_sharing/components/composer.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @odoo-module **/ - -import { PortalComposer } from 'portal.composer'; - -export default PortalComposer.extend({ - _prepareAttachmentData() { - const data = this._super.apply(this, arguments); - const newData = {}; - if (this.options.display_composer && typeof this.options.display_composer == 'string') { - // then we should have the access_token of the task - newData.access_token = this.options.display_composer; - } else { - newData.project_sharing_id = this.options.project_sharing_id; - } - return Object.assign(data, newData); - }, -}); diff --git a/addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.js b/addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.js new file mode 100644 index 00000000000..df29b68ef5e --- /dev/null +++ b/addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.js @@ -0,0 +1,33 @@ +/** @odoo-module */ + +import { PortalFileInput } from '../portal_file_input/portal_file_input'; + +const { Component } = owl; + +export class PortalAttachDocument extends Component {} + +PortalAttachDocument.template = 'project.PortalAttachDocument'; +PortalAttachDocument.components = { PortalFileInput }; +PortalAttachDocument.props = { + highlight: { type: Boolean, optional: true }, + onUpload: { type: Function, optional: true }, + beforeOpen: { type: Function, optional: true }, + slots: { + type: Object, + shape: { + default: Object, + }, + }, + resId: { type: Number, optional: true }, + resModel: { type: String, optional: true }, + multiUpload: { type: Boolean, optional: true }, + hidden: { type: Boolean, optional: true }, + acceptedFileExtensions: { type: String, optional: true }, + token: { type: String, optional: true }, +}; +PortalAttachDocument.defaultProps = { + acceptedFileExtensions: "*", + onUpload: () => {}, + route: "/portal/attachment/add", + beforeOpen: async () => true, +}; diff --git a/addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.xml b/addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.xml new file mode 100644 index 00000000000..c62cbb88236 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/addons/project/static/src/project_sharing/components/portal_file_input/portal_file_input.js b/addons/project/static/src/project_sharing/components/portal_file_input/portal_file_input.js new file mode 100644 index 00000000000..873f2fa1540 --- /dev/null +++ b/addons/project/static/src/project_sharing/components/portal_file_input/portal_file_input.js @@ -0,0 +1,46 @@ +/** @odoo-module */ + +import { FileInput } from '@web/core/file_input/file_input'; + +export class PortalFileInput extends FileInput { + /** + * @override + */ + get httpParams() { + const { + model: res_model, + id: res_id, + ...otherParams + } = super.httpParams; + return { + res_model, + res_id, + access_token: this.props.accessToken, + ...otherParams, + } + } + + async uploadFiles(params) { + const { ufile: files, ...otherParams } = params; + const filesData = await Promise.all( + files.map( + (file) => + super.uploadFiles({ + file, + name: file.name, + ...otherParams, + }) + ) + ); + return filesData; + } +} + +PortalFileInput.props = { + ...FileInput.props, + accessToken: { type: String, optional: true }, +}; +PortalFileInput.defaultProps = { + ...FileInput.defaultProps, + accessToken: '', +}; diff --git a/addons/project/static/src/project_sharing/views/form/controller.js b/addons/project/static/src/project_sharing/views/form/controller.js deleted file mode 100644 index b54daf2167e..00000000000 --- a/addons/project/static/src/project_sharing/views/form/controller.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @odoo-module **/ - -import FormController from 'web.FormController'; -import BasicController from 'web.BasicController'; - -export default FormController.extend({ - _getActionMenuItems: BasicController.prototype._getActionMenuItems, -}); diff --git a/addons/project/static/src/project_sharing/views/form/project_sharing_form_compiler.js b/addons/project/static/src/project_sharing/views/form/project_sharing_form_compiler.js new file mode 100644 index 00000000000..8d9109022ce --- /dev/null +++ b/addons/project/static/src/project_sharing/views/form/project_sharing_form_compiler.js @@ -0,0 +1,105 @@ +/** @odoo-module */ + +import { append, createElement, setAttributes } from "@web/core/utils/xml"; +import { registry } from "@web/core/registry"; +import { SIZES } from "@web/core/ui/ui_service"; +import { getModifier, ViewCompiler } from "@web/views/view_compiler"; +import { patch } from "@web/core/utils/patch"; +import { FormCompiler } from "@web/views/form/form_compiler"; + +/** + * Compiler the portal chatter in project sharing. + * + * @param {HTMLElement} node + * @param {Object} params + * @returns + */ +function compileChatter(node, params) { + const chatterContainerXml = createElement('ChatterContainer'); + const parentURLQuery = new URLSearchParams(window.parent.location.search); + setAttributes(chatterContainerXml, { + token: parentURLQuery.get('access_token') || '', + resModel: params.resModel, + resId: params.resId, + projectSharingId: params.projectSharingId, + }); + const chatterContainerHookXml = createElement('div'); + chatterContainerHookXml.classList.add('o_FormRenderer_chatterContainer'); + append(chatterContainerHookXml, chatterContainerXml); + return chatterContainerHookXml; +} + +export class ProjectSharingChatterCompiler extends ViewCompiler { + setup() { + this.compilers.push({ selector: "t", fn: this.compileT }); + this.compilers.push({ selector: 'div.oe_chatter', fn: this.compileChatter }); + } + + compile(node, params) { + const res = super.compile(node, params).children[0]; + const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer"); + if (chatterContainerHookXml) { + setAttributes(chatterContainerHookXml, { + "t-if": `uiService.size >= ${SIZES.XXL}`, + }); + chatterContainerHookXml.classList.add('overflow-x-hidden', 'overflow-y-auto', 'o-aside', 'h-100'); + } + return res; + } + + compileT(node, params) { + const compiledRoot = createElement("t"); + for (const child of node.childNodes) { + const invisible = getModifier(child, "invisible"); + let compiledChild = this.compileNode(child, params, false); + compiledChild = this.applyInvisible(invisible, compiledChild, { + ...params, + recordExpr: "model.root", + }); + append(compiledRoot, compiledChild); + } + return compiledRoot; + } + + compileChatter(node) { + return compileChatter(node, { + resId: 'model.root.resId or undefined', + resModel: 'model.root.resModel', + projectSharingId: 'model.root.context.active_id', + }); + } +} + +registry.category("form_compilers").add("portal_chatter_compiler", { + selector: "div.oe_chatter", + fn: (node) => + compileChatter(node, { + resId: "props.record.resId or undefined", + resModel: "props.record.resModel", + projectSharingId: "props.record.context.active_id", + }), +}); + +patch(FormCompiler.prototype, 'project_sharing_chatter', { + compile(node, params) { + const res = this._super(node, params); + const chatterContainerHookXml = res.querySelector('.o_FormRenderer_chatterContainer'); + if (!chatterContainerHookXml) { + return res; // no chatter, keep the result as it is + } + if (chatterContainerHookXml.parentNode.classList.contains('o_form_sheet')) { + return res; // if chatter is inside sheet, keep it there + } + const formSheetBgXml = res.querySelector('.o_form_sheet_bg'); + const parentXml = formSheetBgXml && formSheetBgXml.parentNode; + if (!parentXml) { + return res; // miss-config: a sheet-bg is required for the rest + } + // after sheet bg (standard position, below form) + setAttributes(chatterContainerHookXml, { + 't-if': `uiService.size < ${SIZES.XXL}`, + }); + append(parentXml, chatterContainerHookXml); + return res; + } +}); diff --git a/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js new file mode 100644 index 00000000000..652ab0b5b32 --- /dev/null +++ b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js @@ -0,0 +1,31 @@ +/** @odoo-module */ + +import { useService } from '@web/core/utils/hooks'; +import { FormController } from '@web/views/form/form_controller'; +import { useViewCompiler } from '@web/views/view_compiler'; +import { ProjectSharingChatterCompiler } from './project_sharing_form_compiler'; +import { ChatterContainer } from '../../components/chatter/chatter_container'; + +export class ProjectSharingFormController extends FormController { + setup() { + super.setup(); + this.uiService = useService('ui'); + const { arch, xmlDoc } = this.archInfo; + const template = document.createElement('t'); + const xmlDocChatter = xmlDoc.querySelector("div.oe_chatter"); + if (xmlDocChatter && xmlDocChatter.parentNode.nodeName === "form") { + template.appendChild(xmlDocChatter.cloneNode(true)); + } + const mailTemplates = useViewCompiler(ProjectSharingChatterCompiler, arch, { Mail: template }, {}); + this.mailTemplate = mailTemplates.Mail; + } + + getActionMenuItems() { + return {}; + } +} + +ProjectSharingFormController.components = { + ...FormController.components, + ChatterContainer, +} diff --git a/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.xml b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.xml new file mode 100644 index 00000000000..6db6caaad52 --- /dev/null +++ b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/addons/project/static/src/project_sharing/views/form/project_sharing_form_renderer.js b/addons/project/static/src/project_sharing/views/form/project_sharing_form_renderer.js new file mode 100644 index 00000000000..1da51d3f415 --- /dev/null +++ b/addons/project/static/src/project_sharing/views/form/project_sharing_form_renderer.js @@ -0,0 +1,10 @@ +/** @odoo-module */ + +import { ChatterContainer } from '../../components/chatter/chatter_container'; +import { FormRenderer } from '@web/views/form/form_renderer'; + +export class ProjectSharingFormRenderer extends FormRenderer { } +ProjectSharingFormRenderer.components = { + ...FormRenderer.components, + ChatterContainer, +}; diff --git a/addons/project/static/src/project_sharing/views/form/project_sharing_form_view.js b/addons/project/static/src/project_sharing/views/form/project_sharing_form_view.js new file mode 100644 index 00000000000..fd916276339 --- /dev/null +++ b/addons/project/static/src/project_sharing/views/form/project_sharing_form_view.js @@ -0,0 +1,8 @@ +/** @odoo-module */ + +import { formView } from '@web/views/form/form_view'; +import { ProjectSharingFormController } from './project_sharing_form_controller'; +import { ProjectSharingFormRenderer } from './project_sharing_form_renderer'; + +formView.Controller = ProjectSharingFormController; +formView.Renderer = ProjectSharingFormRenderer; diff --git a/addons/project/static/src/project_sharing/views/form/renderer.js b/addons/project/static/src/project_sharing/views/form/renderer.js deleted file mode 100644 index a3468ea7736..00000000000 --- a/addons/project/static/src/project_sharing/views/form/renderer.js +++ /dev/null @@ -1,52 +0,0 @@ -/** @odoo-module **/ - -import '@mail/widgets/form_renderer/form_renderer'; // ensure mail overrides are applied first - -import FormRenderer from 'web.FormRenderer'; -import Chatter from '@project/project_sharing/components/chatter'; -import { session } from '@web/session'; - -export default FormRenderer.extend({ - /** - * Creates the portal chatter instead of the standard chatter. - * - * This overridden method is expected to be called from proper lifecycle - * method of mail form renderer, when `this._chatterContainerTarget` is set. - * - * @override - */ - initChatter() { - const options = this.makePortalChatterOptions(); - this.portalChatter = new Chatter(this, options); - this.portalChatter.appendTo(this._chatterContainerTarget); - }, - /** - * Updates the options of the portal chatter instead of the props of the - * standard chatter. - * - * This overridden method is expected to be called from proper lifecycle - * method of mail form renderer. - * - * @override - */ - _updateChatterContainerComponent() { - const options = this.makePortalChatterOptions(); - this.portalChatter.update(options); - }, - makePortalChatterOptions() { - // FIXME: perhaps check if we have a parent in the window ? - // Call the parent window to get the url displayed in the browser since we are in an iframe - const query = new URLSearchParams(window.parent.location.search); - return { - token: query.get('access_token') || '', - res_model: 'project.task', - pid: '', - hash: '', - res_id: this.state.res_id, - pager_step: 10, - allow_composer: !!this.state.res_id, - two_columns: false, - project_sharing_id: session.project_id, - }; - }, -}); diff --git a/addons/project/static/src/project_sharing/views/form/view.js b/addons/project/static/src/project_sharing/views/form/view.js deleted file mode 100644 index 36f7904679b..00000000000 --- a/addons/project/static/src/project_sharing/views/form/view.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @odoo-module **/ - -import FormView from 'web.FormView'; -import Controller from './controller'; -import Renderer from './renderer'; - -export default FormView.extend({ - config: _.extend({}, FormView.prototype.config, { - Controller, - Renderer, - }), -}); diff --git a/addons/project/static/src/project_sharing/views/registry.js b/addons/project/static/src/project_sharing/views/registry.js deleted file mode 100644 index ea68de08d4e..00000000000 --- a/addons/project/static/src/project_sharing/views/registry.js +++ /dev/null @@ -1,15 +0,0 @@ -/** @odoo-module **/ - -import ViewRegistry from 'web.view_registry'; -import { registry } from '@web/core/registry'; -// FIXME: make sure the form view are define before we remove them below -import "@web/views/form/form_view"; - -import FormView from './form/view'; - -// FIXME: remove new views to force the use of the overriden legacy views when -// project sharing form is converted to new views. -registry.category("views").remove("form"); - -ViewRegistry - .add('form', FormView); diff --git a/addons/project/static/src/scss/project_sharing_frontend.scss b/addons/project/static/src/scss/project_sharing_frontend.scss new file mode 100644 index 00000000000..6c18b30def8 --- /dev/null +++ b/addons/project/static/src/scss/project_sharing_frontend.scss @@ -0,0 +1,3 @@ +.o_project_sharing_container > main { + display: flex; +} diff --git a/addons/project/static/tests/tours/project_sharing_tour.js b/addons/project/static/tests/tours/project_sharing_tour.js index c66fb7b2e2f..0a0a83d1c0d 100644 --- a/addons/project/static/tests/tours/project_sharing_tour.js +++ b/addons/project/static/tests/tours/project_sharing_tour.js @@ -49,14 +49,14 @@ const projectSharingSteps = [...tour.stepUtils.goToAppSteps("project.menu_main_p trigger: '.o_kanban_quick_create .o_kanban_edit', content: 'Go to the form view of this new task', }, { - trigger: 'div.o_statusbar_status[name="stage_id"] button[aria-checked="false"]:contains(Done)', + trigger: 'div[name="stage_id"] div.o_statusbar_status button[aria-checked="false"]:contains(Done)', content: 'Change the stage of the task.', }, { - trigger: '.o_portal_chatter_composer_input .o_portal_chatter_composer_body textarea[name="message"]', + trigger: '.o_portal_chatter_composer_input .o_portal_chatter_composer_body textarea', content: 'Write a message in the chatter of the task', run: 'text I create a new task for testing purpose.', }, { - trigger: '.o_portal_chatter_composer_input .o_portal_chatter_composer_body button[data-action="/mail/chatter_post"]', + trigger: '.o_portal_chatter_composer_input .o_portal_chatter_composer_body button[name="send_message"]', content: 'Send the message', }, { trigger: 'ol.breadcrumb > li.o_back_button > a:contains(Project Sharing)', diff --git a/addons/project/tests/test_project_sharing_ui.py b/addons/project/tests/test_project_sharing_ui.py index 16f532c4066..70de1324f99 100644 --- a/addons/project/tests/test_project_sharing_ui.py +++ b/addons/project/tests/test_project_sharing_ui.py @@ -39,7 +39,7 @@ class TestProjectSharingUi(HttpCase): def test_01_project_sharing(self): """ Test Project Sharing UI with an internal user """ - self.start_tour("/web?debug=assets", 'project_sharing_tour', login="admin") + self.start_tour("/web", 'project_sharing_tour', login="admin") def test_02_project_sharing(self): """ Test project sharing ui with a portal user. diff --git a/addons/project/views/project_sharing_templates.xml b/addons/project/views/project_sharing_templates.xml index 5182521f05d..b00bb5512ba 100644 --- a/addons/project/views/project_sharing_templates.xml +++ b/addons/project/views/project_sharing_templates.xml @@ -3,6 +3,8 @@ + + @@ -10,7 +12,7 @@ - + diff --git a/addons/project/views/project_sharing_views.xml b/addons/project/views/project_sharing_views.xml index 3318704a4c0..1d4595d0f49 100644 --- a/addons/project/views/project_sharing_views.xml +++ b/addons/project/views/project_sharing_views.xml @@ -85,7 +85,7 @@ - +
You must be logged in to post a comment.