From 136328eb89d07d1c70872f59c308898640569546 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 8 Aug 2022 11:24:12 +0200 Subject: [PATCH 1/6] [REF] project: convert the project sharing list in OWL This commit converts the list view used in project sharing in OWL and remove the legacy definition. task-2947516 X-original-commit: 79159c0c6b715c3acbfb370523b73a664648deb2 --- .../project_sharing/views/list/controller.js | 8 ---- .../views/list/list_renderer.js | 41 +++++++++++++++++ .../project_sharing/views/list/list_view.js | 15 ++++++ .../src/project_sharing/views/list/view.js | 46 ------------------- .../src/project_sharing/views/registry.js | 10 ++-- 5 files changed, 59 insertions(+), 61 deletions(-) delete mode 100644 addons/project/static/src/project_sharing/views/list/controller.js create mode 100644 addons/project/static/src/project_sharing/views/list/list_renderer.js create mode 100644 addons/project/static/src/project_sharing/views/list/list_view.js delete mode 100644 addons/project/static/src/project_sharing/views/list/view.js 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 index 7c5bf52c715..ea68de08d4e 100644 --- a/addons/project/static/src/project_sharing/views/registry.js +++ b/addons/project/static/src/project_sharing/views/registry.js @@ -2,18 +2,14 @@ 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 +// FIXME: make sure the form view 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. +// project sharing form is converted to new views. registry.category("views").remove("form"); -registry.category("views").remove("list"); ViewRegistry - .add('form', FormView) - .add('list', ListView); + .add('form', FormView); From 0c35d43adfbb41e6008d35d12b2dbd5ac952a3c0 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Tue, 4 Oct 2022 09:35:36 +0200 Subject: [PATCH 2/6] [FIX] web: check if record in component props is defined since it is optional Before this commit, when a component using the `useInputField` hook (for instance `TextField`) and the `record` is not given in its props, the hook will crash since it assume the `record` is always defined. However, the props definition the `record` is optional. This commit checks if `record` is defined in the component before using it to be sure to avoid any crash because it is not defined. task-2947516 X-original-commit: ebe3d00e52dd6097ce55986bc9e3bf9d79ca64cc --- addons/web/static/src/views/fields/input_field_hook.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/views/fields/input_field_hook.js b/addons/web/static/src/views/fields/input_field_hook.js index 7fe73b57540..2c7119015f6 100644 --- a/addons/web/static/src/views/fields/input_field_hook.js +++ b/addons/web/static/src/views/fields/input_field_hook.js @@ -56,7 +56,9 @@ export function useInputField(params) { try { val = params.parse(val); } catch (_e) { - component.props.record.setInvalidField(component.props.name); + if (component.props.record) { + component.props.record.setInvalidField(component.props.name); + } isInvalid = true; } } @@ -101,7 +103,7 @@ export function useInputField(params) { * If it is not such a case, we update the field with the new value. */ useEffect(() => { - const isInvalid = component.props.record.isInvalid(component.props.name); + const isInvalid = component.props.record ? component.props.record.isInvalid(component.props.name) : false; if (inputRef.el && !isDirty && !isInvalid) { inputRef.el.value = params.getValue(); lastSetValue = inputRef.el.value; @@ -133,7 +135,7 @@ export function useInputField(params) { isInvalid = true; if (urgent) { return; - } else { + } else if (component.props.record) { component.props.record.setInvalidField(component.props.name); } } From 6a4e3dc030d4b74671347cac0c4a6adca3cd67da Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Tue, 4 Oct 2022 16:48:01 +0200 Subject: [PATCH 3/6] [FIX] web: allow to change the row on textarea in TextField with a prop Before this commit, the rows attribute given on the TextField component was always 2, it is not possible to alter it without overriding the component. This commit adds a props to be able to update the rows with a prop called `rowCount` and this prop can be set via `rows` attribute set on the field. task-2947516 X-original-commit: 110915c80c2a2578563283b1744e0e88d80ac4bb --- .../src/views/fields/text/text_field.js | 21 ++++++++++++++---- .../tests/views/fields/text_field_tests.js | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/addons/web/static/src/views/fields/text/text_field.js b/addons/web/static/src/views/fields/text/text_field.js index d44e1353253..95466e97844 100644 --- a/addons/web/static/src/views/fields/text/text_field.js +++ b/addons/web/static/src/views/fields/text/text_field.js @@ -6,6 +6,7 @@ import { useInputField } from "../input_field_hook"; import { standardFieldProps } from "../standard_field_props"; import { TranslationButton } from "../translation_button"; import { useDynamicPlaceholder } from "../dynamicplaceholder_hook"; +import { parseInteger } from '../parsers'; const { Component, useEffect, onMounted, onWillUnmount, useRef } = owl; @@ -67,7 +68,7 @@ export class TextField extends Component { return 50; } get rowCount() { - return 2; + return this.props.rowCount; } resize() { @@ -105,23 +106,31 @@ TextField.template = "web.TextField"; TextField.components = { TranslationButton, }; -TextField.defaultProps = {dynamicPlaceholder: false}; +TextField.defaultProps = { + dynamicPlaceholder: false, + rowCount: 2, +}; TextField.props = { ...standardFieldProps, isTranslatable: { type: Boolean, optional: true }, placeholder: { type: String, optional: true }, dynamicPlaceholder: { type: Boolean, optional: true}, + rowCount: { type: Number, optional: true }, }; TextField.displayName = _lt("Multiline Text"); TextField.supportedTypes = ["html", "text"]; TextField.extractProps = ({ attrs, field }) => { - return { + const props = { isTranslatable: field.translate, placeholder: attrs.placeholder, dynamicPlaceholder: attrs.options.dynamic_placeholder, }; + if (attrs.rows) { + props.rowCount = parseInteger(attrs.rows); + } + return props; }; registry.category("fields").add("text", TextField); @@ -131,8 +140,12 @@ export class ListTextField extends TextField { return 0; } get rowCount() { - return 1; + return this.props.rowCount; } } +ListTextField.defaultProps = { + ...TextField.defaultProps, + rowCount: 1, +}; registry.category("fields").add("list.text", ListTextField); diff --git a/addons/web/static/tests/views/fields/text_field_tests.js b/addons/web/static/tests/views/fields/text_field_tests.js index 62ec480b07a..76459e2772e 100644 --- a/addons/web/static/tests/views/fields/text_field_tests.js +++ b/addons/web/static/tests/views/fields/text_field_tests.js @@ -240,6 +240,28 @@ QUnit.module("Fields", (hooks) => { ); }); + QUnit.test("set row on text fields", async function (assert) { + serverData.models.partner.fields.foo.type = "text"; + + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` +
+ + `, + }); + + const textarea = target.querySelector("textarea"); + assert.strictEqual( + textarea.rows, + 4, + "rowCount should be the one set on the field", + ); + }); + QUnit.test( "autoresize of text fields is done when switching to edit mode", async function (assert) { From da435cfe7713aff878d5d3a5aa191fa26c4dae23 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 10 Oct 2022 14:07:52 +0200 Subject: [PATCH 4/6] [REF] web: review FileInput to use it with different route Before this commit, the `FileInput` component can be used with another route only if the route has the same params than the default one. For instance, the portal chatter route does not have the same params name and so, the component cannot be used. This commit refactors the component to easily override the component to be able to use it in each case. task-2947516 X-original-commit: 8fdeea2e9918c8199724b177003f858716d7d5a4 --- .../static/src/core/file_input/file_input.js | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/addons/web/static/src/core/file_input/file_input.js b/addons/web/static/src/core/file_input/file_input.js index 2ebbb395222..80ddc25b6fd 100644 --- a/addons/web/static/src/core/file_input/file_input.js +++ b/addons/web/static/src/core/file_input/file_input.js @@ -34,6 +34,30 @@ export class FileInput extends Component { }); } + get httpParams() { + const { resId, resModel } = this.props; + const params = { + csrf_token: odoo.csrf_token, + ufile: [...this.fileInputRef.el.files], + }; + if (resModel) { + params.model = resModel; + } + if (resId !== undefined) { + params.id = resId; + } + return params; + } + + async uploadFiles(params) { + const fileData = await this.http.post(this.props.route, params, "text"); + const parsedFileData = JSON.parse(fileData); + if (parsedFileData.error) { + throw new Error(parsedFileData.error); + } + return parsedFileData; + } + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -46,22 +70,7 @@ export class FileInput extends Component { * - resId: the id of the resModel target instance */ async onFileInputChange() { - const { resId, resModel, route } = this.props; - const params = { - csrf_token: odoo.csrf_token, - ufile: [...this.fileInputRef.el.files], - }; - if (resModel) { - params.model = resModel; - } - if (resId !== undefined) { - params.id = resId; - } - const fileData = await this.http.post(route, params, "text"); - const parsedFileData = JSON.parse(fileData); - if (parsedFileData.error) { - throw new Error(parsedFileData.error); - } + const parsedFileData = await this.uploadFiles(this.httpParams); this.props.onUpload(parsedFileData); } From 8904dbd522ee859e0b4cf58125b6aa4cbe285d1c Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 10 Oct 2022 14:11:26 +0200 Subject: [PATCH 5/6] [REF] project: convert project sharing form view in OWL Before this commit, the form view in project sharing was always in legacy. Moreover, in that view, the chatter used is the portal one and so it is also a legacy widget. This commit converts the form view and then convert the chatter in OWL to be able to correctly compile and add the chatter in the form view. Also, some styling has been done to remove the overflow. Only the overflow on the y axis in the chatter can be scrolled on the page. task-2947516 X-original-commit: 175430f3593337ce582cbec644b9d630894c074b --- addons/project/__manifest__.py | 11 +- .../src/project_sharing/components/chatter.js | 34 ---- .../project_sharing/components/chatter.xml | 8 - .../components/chatter}/chatter.scss | 7 +- .../chatter/chatter_attachments_viewer.js | 15 ++ .../chatter/chatter_attachments_viewer.xml | 28 +++ .../components/chatter/chatter_composer.js | 134 ++++++++++++++ .../components/chatter/chatter_composer.xml | 61 +++++++ .../components/chatter/chatter_container.js | 172 ++++++++++++++++++ .../components/chatter/chatter_container.xml | 27 +++ .../chatter/chatter_message_counter.js | 10 + .../chatter/chatter_message_counter.xml | 16 ++ .../components/chatter/chatter_messages.js | 37 ++++ .../components/chatter/chatter_messages.xml | 43 +++++ .../components/chatter/chatter_pager.js | 64 +++++++ .../components/chatter/chatter_pager.xml | 21 +++ .../project_sharing/components/composer.js | 17 -- .../portal_attach_document.js | 33 ++++ .../portal_attach_document.xml | 21 +++ .../portal_file_input/portal_file_input.js | 46 +++++ .../project_sharing/views/form/controller.js | 8 - .../form/project_sharing_form_compiler.js | 105 +++++++++++ .../form/project_sharing_form_controller.js | 31 ++++ .../form/project_sharing_form_controller.xml | 12 ++ .../form/project_sharing_form_renderer.js | 10 + .../views/form/project_sharing_form_view.js | 8 + .../project_sharing/views/form/renderer.js | 52 ------ .../src/project_sharing/views/form/view.js | 12 -- .../src/project_sharing/views/registry.js | 15 -- .../src/scss/project_sharing_frontend.scss | 3 + .../tests/tours/project_sharing_tour.js | 6 +- .../project/tests/test_project_sharing_ui.py | 2 +- .../views/project_sharing_templates.xml | 4 +- .../project/views/project_sharing_views.xml | 2 +- 34 files changed, 912 insertions(+), 163 deletions(-) delete mode 100644 addons/project/static/src/project_sharing/components/chatter.js delete mode 100644 addons/project/static/src/project_sharing/components/chatter.xml rename addons/project/static/src/{scss/project_sharing => project_sharing/components/chatter}/chatter.scss (89%) create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.js create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_attachments_viewer.xml create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_composer.js create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_composer.xml create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_container.js create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_container.xml create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.js create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_message_counter.xml create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_messages.js create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_messages.xml create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_pager.js create mode 100644 addons/project/static/src/project_sharing/components/chatter/chatter_pager.xml delete mode 100644 addons/project/static/src/project_sharing/components/composer.js create mode 100644 addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.js create mode 100644 addons/project/static/src/project_sharing/components/portal_attach_document/portal_attach_document.xml create mode 100644 addons/project/static/src/project_sharing/components/portal_file_input/portal_file_input.js delete mode 100644 addons/project/static/src/project_sharing/views/form/controller.js create mode 100644 addons/project/static/src/project_sharing/views/form/project_sharing_form_compiler.js create mode 100644 addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js create mode 100644 addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.xml create mode 100644 addons/project/static/src/project_sharing/views/form/project_sharing_form_renderer.js create mode 100644 addons/project/static/src/project_sharing/views/form/project_sharing_form_view.js delete mode 100644 addons/project/static/src/project_sharing/views/form/renderer.js delete mode 100644 addons/project/static/src/project_sharing/views/form/view.js delete mode 100644 addons/project/static/src/project_sharing/views/registry.js create mode 100644 addons/project/static/src/scss/project_sharing_frontend.scss 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 @@ + + + + +
+ + + +
+ 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/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 @@