diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index 6847791545c..48d2e62917f 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -53,6 +53,9 @@ up a management by affair. 'web.qunit_suite_tests': [ 'hr_timesheet/static/tests/**/*', ], + 'project.webclient': [ + 'hr_timesheet/static/src/components/**/*', + ], }, 'license': 'LGPL-3', } diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index c231378b70b..ea9728d7c6f 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': [ @@ -88,25 +89,134 @@ 'project/static/tests/tours/**/*', ], 'project.webclient': [ - ('include', 'web.assets_backend'), + ('include', 'web._assets_helpers'), + ('include', 'web._assets_backend_helpers'), - # Remove Longpolling bus and packages needed this bus - ('remove', 'mail/static/src/services/messaging_service.js'), + 'web/static/src/scss/pre_variables.scss', + 'web/static/lib/bootstrap/scss/_variables.scss', - ('remove', 'mail/static/src/components/dialog_manager/dialog_manager.js'), - ('remove', 'mail/static/src/components/chat_window_manager/chat_window_manager.js'), + ('include', 'web._assets_bootstrap'), + + 'base/static/src/css/modules.css', + + 'web/static/src/core/utils/transitions.scss', + 'web/static/src/core/**/*', + 'web/static/src/search/**/*', + 'web/static/src/webclient/icons.scss', # variables required in list_controller.scss + 'web/static/src/views/*.js', + 'web/static/src/views/*.xml', + 'web/static/src/views/*.scss', + 'web/static/src/views/fields/**/*', + 'web/static/src/views/form/**/*', + 'web/static/src/views/kanban/**/*', + 'web/static/src/views/list/**/*', + 'web/static/src/views/view_button/**/*', + 'web/static/src/views/view_dialogs/**/*', + 'web/static/src/views/widgets/**/*', + 'web/static/src/webclient/**/*', + ('remove', 'web/static/src/webclient/navbar/navbar.scss'), # already in assets_common + ('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded + ('remove', 'web/static/src/views/form/button_box/*.scss'), + + # remove the report code and whitelist only what's needed + ('remove', 'web/static/src/webclient/actions/reports/**/*'), + 'web/static/src/webclient/actions/reports/*.js', + 'web/static/src/webclient/actions/reports/*.xml', + + 'web/static/src/env.js', + + 'web/static/lib/jquery.scrollTo/jquery.scrollTo.js', + 'web/static/lib/py.js/lib/py.js', + 'web/static/lib/py.js/lib/py_extras.js', + 'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js', + + 'web/static/src/legacy/scss/fields.scss', + 'web/static/src/legacy/scss/views.scss', + 'web/static/src/legacy/scss/form_view.scss', + 'web/static/src/legacy/scss/list_view.scss', + 'web/static/src/legacy/scss/kanban_dashboard.scss', + 'web/static/src/legacy/scss/kanban_examples_dialog.scss', + 'web/static/src/legacy/scss/kanban_column_progressbar.scss', + 'web/static/src/legacy/scss/kanban_view.scss', + + 'base/static/src/scss/res_partner.scss', + + # Form style should be computed before + 'web/static/src/views/form/button_box/*.scss', + + 'web/static/src/legacy/action_adapters.js', + 'web/static/src/legacy/debug_manager.js', + 'web/static/src/legacy/legacy_service_provider.js', + 'web/static/src/legacy/legacy_client_actions.js', + 'web/static/src/legacy/legacy_dialog.js', + 'web/static/src/legacy/legacy_load_views.js', + 'web/static/src/legacy/legacy_promise_error_handler.js', + 'web/static/src/legacy/legacy_rpc_error_handler.js', + 'web/static/src/legacy/root_widget.js', + 'web/static/src/legacy/legacy_setup.js', + 'web/static/src/legacy/root_widget.js', + 'web/static/src/legacy/backend_utils.js', + 'web/static/src/legacy/utils.js', + 'web/static/src/legacy/web_client.js', + 'web/static/src/legacy/js/_deprecated/data.js', + 'web/static/src/legacy/js/chrome/*', + 'web/static/src/legacy/js/components/*', + 'web/static/src/legacy/js/control_panel/*', + 'web/static/src/legacy/js/core/domain.js', + 'web/static/src/legacy/js/core/mvc.js', + 'web/static/src/legacy/js/core/py_utils.js', + 'web/static/src/legacy/js/core/context.js', + 'web/static/src/legacy/js/core/misc.js', + 'web/static/src/legacy/js/fields/abstract_field.js', + 'web/static/src/legacy/js/fields/abstract_field_owl.js', + 'web/static/src/legacy/js/_deprecated/basic_fields.js', + 'web/static/src/legacy/js/fields/basic_fields.js', + 'web/static/src/legacy/js/fields/basic_fields_owl.js', + 'web/static/src/legacy/js/fields/field_utils.js', + 'web/static/src/legacy/js/fields/relational_fields.js', + 'web/static/src/legacy/js/fields/special_fields.js', + 'web/static/src/legacy/js/fields/field_registry.js', + 'web/static/src/legacy/js/fields/field_registry_owl.js', + 'web/static/src/legacy/js/fields/field_utils.js', + 'web/static/src/legacy/js/fields/field_wrapper.js', + 'web/static/src/legacy/js/views/sample_server.js', + 'web/static/src/legacy/js/views/abstract_model.js', + 'web/static/src/legacy/js/views/basic/basic_model.js', + 'web/static/src/legacy/js/views/action_model.js', + 'web/static/src/legacy/js/views/view_utils.js', + 'web/static/src/legacy/js/services/data_manager.js', + 'web/static/src/legacy/js/services/session.js', + 'web/static/src/legacy/js/tools/tools.js', + 'web/static/src/legacy/js/views/**/*', + 'web/static/src/legacy/js/widgets/data_export.js', + 'web/static/src/legacy/js/widgets/date_picker.js', + 'web/static/src/legacy/js/widgets/domain_selector_dialog.js', + 'web/static/src/legacy/js/widgets/domain_selector.js', + 'web/static/src/legacy/js/widgets/model_field_selector.js', + 'web/static/src/legacy/js/widgets/model_field_selector_popover.js', + 'web/static/src/legacy/js/env.js', + 'web/static/src/legacy/js/model.js', + 'web/static/src/legacy/js/owl_compatibility.js', + + 'web_editor/static/src/components/**/*', + 'web_editor/static/src/scss/web_editor.common.scss', + 'web_editor/static/src/scss/web_editor.backend.scss', + + 'web_editor/static/src/js/wysiwyg/dialog.js', + 'web_editor/static/src/js/frontend/loader.js', + 'web_editor/static/src/js/backend/**/*', + 'web_editor/static/src/xml/backend.xml', + + 'mail/static/src/scss/variables/*.scss', + 'mail/static/src/widgets/**/*.scss', + + 'project/static/src/components/project_task_name_with_subtask_count_char_field/*', + 'project/static/src/views/project_task_form/*.scss', - '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 @@ + + + + +
+ + + +
+ Avatar +
+
+
+ + +
+ + + + +
+
+
+
+
+
+ +

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 @@ + + + +
+ +
+ avatar +
+ +
+ +
+
+ + +
+
+
+
+

+
+ +
+ +
+
+
+
+
+
+ +
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/list/controller.js b/addons/project/static/src/project_sharing/views/list/controller.js deleted file mode 100644 index f6dbac12087..00000000000 --- a/addons/project/static/src/project_sharing/views/list/controller.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @odoo-module **/ - -import ListController from 'web.ListController'; -import BasicController from 'web.BasicController'; - -export default ListController.extend({ - _getActionMenuItems: BasicController.prototype._getActionMenuItems, -}); diff --git a/addons/project/static/src/project_sharing/views/list/list_renderer.js b/addons/project/static/src/project_sharing/views/list/list_renderer.js new file mode 100644 index 00000000000..0731a1ff419 --- /dev/null +++ b/addons/project/static/src/project_sharing/views/list/list_renderer.js @@ -0,0 +1,41 @@ +/** @odoo-module */ + +import { ListRenderer } from "@web/views/list/list_renderer"; +import { evalDomain } from "@web/views/utils"; + +const { onWillUpdateProps } = owl; + +export class ProjectSharingListRenderer extends ListRenderer { + setup() { + super.setup(...arguments); + this.setColumns(this.allColumns); + onWillUpdateProps((nextProps) => { + this.setColumns(nextProps.archInfo.columns); + }); + } + + setColumns(columns) { + if (this.props.list.records.length) { + const allColumns = []; + const firstRecord = this.props.list.records[0]; + for (const column of columns) { + if ( + column.modifiers.column_invisible && + column.modifiers.column_invisible instanceof Array + ) { + const result = evalDomain(column.modifiers.column_invisible, firstRecord.evalContext); + if (result) { + continue; + } + } + allColumns.push(column); + } + this.allColumns = allColumns; + } else { + this.allColumns = columns; + } + this.state.columns = this.allColumns.filter( + (col) => !col.optional || this.optionalActiveFields[col.name] + ); + } +} diff --git a/addons/project/static/src/project_sharing/views/list/list_view.js b/addons/project/static/src/project_sharing/views/list/list_view.js new file mode 100644 index 00000000000..c9cc969346a --- /dev/null +++ b/addons/project/static/src/project_sharing/views/list/list_view.js @@ -0,0 +1,15 @@ +/** @odoo-module */ + +import { listView } from "@web/views/list/list_view"; + +import { ProjectSharingListRenderer } from "./list_renderer"; + +const props = listView.props; +listView.props = function (genericProps, view) { + const result = props(genericProps, view); + return { + ...result, + allowSelectors: false, + }; +}; +listView.Renderer = ProjectSharingListRenderer; diff --git a/addons/project/static/src/project_sharing/views/list/view.js b/addons/project/static/src/project_sharing/views/list/view.js deleted file mode 100644 index 843f7861303..00000000000 --- a/addons/project/static/src/project_sharing/views/list/view.js +++ /dev/null @@ -1,46 +0,0 @@ -/** @odoo-module **/ - -import ListView from 'web.ListView'; -import Controller from './controller'; - -export default ListView.extend({ - config: Object.assign({}, ListView.prototype.config, { - Controller, - }), - - init: function (viewInfo, params) { - return this._super(viewInfo, {...params, hasSelectors: false}); - }, - getRenderer (parent, state) { - const columnInvisibleFields = {}; - for (const child of this.arch.children) { - if (child.attrs && child.attrs.modifiers && child.attrs.modifiers.column_invisible) { - columnInvisibleFields[child.attrs.name] = child.attrs.modifiers.column_invisible; - } - } - this.rendererParams.columnInvisibleFields = Object.entries(columnInvisibleFields).reduce((acc, entry) => { - const [fieldName, domains] = entry; - if (domains instanceof Array && state.data.length) { - const record = state.data[0]; - const data = Object.entries(record.data).reduce((acc, entry) => { - const [fieldName, value] = entry; - const field = record.fields[fieldName]; - if (field.type === 'one2many' || field.type === 'many2many') { - if (value instanceof Object && value.type === 'list') { - acc[fieldName] = value.id; - } - } - return acc; - }, record.data); - if (Object.keys(data).length) { - // We assume the field in this domain is a related to the project shared. - acc[fieldName] = this.model._evalModifiers({ ...record, data }, { column_invisible: domains }).column_invisible; - } - } else { - acc[fieldName] = domains; - } - return acc; - }, {}); - return this._super(parent, state); - }, -}); 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 7c5bf52c715..00000000000 --- a/addons/project/static/src/project_sharing/views/registry.js +++ /dev/null @@ -1,19 +0,0 @@ -/** @odoo-module **/ - -import ViewRegistry from 'web.view_registry'; -import { registry } from '@web/core/registry'; -// FIXME: make sure the form and list views are define before we remove them below -import "@web/views/form/form_view"; -import "@web/views/list/list_view"; - -import FormView from './form/view'; -import ListView from './list/view'; - -// FIXME: remove new views to force the use of the overriden legacy views when -// project sharing form and list are converted to new views. -registry.category("views").remove("form"); -registry.category("views").remove("list"); - -ViewRegistry - .add('form', FormView) - .add('list', ListView); 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 @@