[IMP] mail, web: adapt chatter container in new form

task-2871070

Part-of: odoo/odoo#78221
This commit is contained in:
Sébastien Theys
2022-07-22 16:21:45 +02:00
committed by Géry Debongnie
parent bc0a0cead6
commit 28200c5ad5
24 changed files with 372 additions and 149 deletions
+1
View File
@@ -198,6 +198,7 @@ For more specific needs, you may also assign custom-defined actions
'mail/static/src/components/*/*.scss',
('include', 'mail.assets_messaging'),
'mail/static/src/services/*.js',
'mail/static/src/views/**/*.js',
'mail/static/src/webclient/commands/*.js',
'mail/static/src/widgets/*/*.js',
'mail/static/src/widgets/*/*.scss',
@@ -36,7 +36,7 @@ export class ChatterContainer extends Component {
this.chatterId = getChatterNextTemporaryId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._willUpdateProps(nextProps));
onWillDestroy(this._onWillDestroy);
onWillDestroy(() => this._onWillDestroy());
}
_willUpdateProps(nextProps) {
@@ -61,10 +61,10 @@ export class ChatterContainer extends Component {
*/
async _insertFromProps(props) {
const messaging = await this.env.services.messaging.get();
if (this.__owl__.status === 5 /* destroyed */) {
if (owl.status(this) === "destroyed") {
return;
}
const values = { id: this.chatterId, ...props };
const values = { id: this.chatterId, ...props, className: undefined };
if (values.threadId === undefined) {
values.threadId = clear();
}
@@ -93,6 +93,10 @@ export class ChatterContainer extends Component {
Object.assign(ChatterContainer, {
components: { Chatter: getMessagingComponent('Chatter') },
props: {
className: {
type: String,
optional: true,
},
hasActivities: {
type: Boolean,
optional: true,
@@ -142,6 +146,10 @@ Object.assign(ChatterContainer, {
optional: true,
},
threadModel: String,
webRecord: {
type: Object,
optional: true,
}
},
template: 'mail.ChatterContainer',
});
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mail.ChatterContainer" owl="1">
<div class="o_ChatterContainer flex-grow-1 d-flex w-100" t-att-class="{ 'o-isInFormSheetBg mx-auto': props.isInFormSheetBg }" t-attf-class="{{ className }}" data-command-category="mail" t-ref="root">
<div class="o_ChatterContainer flex-grow-1 d-flex w-100" t-att-class="{ 'o-isInFormSheetBg mx-auto': props.isInFormSheetBg }" t-attf-class="{{ props.className }}" data-command-category="mail" t-ref="root">
<t t-if="chatter">
<Chatter record="chatter"/>
</t>
@@ -57,7 +57,7 @@ export class WebClientViewAttachmentViewContainer extends Component {
*/
async _insertFromProps(props) {
const messaging = await this.env.services.messaging.get();
if (this.__owl__.status === 5 /* destroyed */) {
if (owl.status(this) === "destroyed") {
this._deleteRecord();
return;
}
+9 -8
View File
@@ -160,6 +160,10 @@ registerModel({
* @param {string[]} [fieldNames]
*/
reloadParentView({ fieldNames } = {}) {
if (this.webRecord) {
this.webRecord.model.load({ resId: this.threadId });
return;
}
if (this.component) {
const options = { keepChanges: true };
if (fieldNames) {
@@ -301,8 +305,10 @@ registerModel({
*/
_onThreadIsLoadingAttachmentsChanged() {
if (!this.thread || !this.thread.isLoadingAttachments) {
this._stopAttachmentsLoading();
this.update({ isShowingAttachmentsLoading: false });
this.update({
attachmentsLoaderTimer: clear(),
isShowingAttachmentsLoading: false,
});
return;
}
if (this.isPreparingAttachmentsLoading || this.isShowingAttachmentsLoading) {
@@ -316,12 +322,6 @@ registerModel({
_prepareAttachmentsLoading() {
this.update({ attachmentsLoaderTimer: insertAndReplace() });
},
/**
* @private
*/
_stopAttachmentsLoading() {
this.update({ attachmentsLoaderTimer: clear() });
},
},
fields: {
activityBoxView: one('ActivityBoxView', {
@@ -490,6 +490,7 @@ registerModel({
readonly: true,
required: true,
}),
webRecord: attr(),
},
onChanges: [
new OnChange({
+4
View File
@@ -951,6 +951,10 @@ registerModel({
*/
async setMainAttachment(attachment) {
this.update({ mainAttachment: replace(attachment) });
if (this.model === 'account.move.line') {
// account.move.line is not actually a thread in python
return;
}
await this.messaging.rpc({
model: 'ir.attachment',
method: 'register_as_main_attachment',
@@ -298,6 +298,7 @@ registerModel({
for (const threadView of this.threadViews) {
threadView.addComponentHint('messages-loaded', { fetchedMessages });
}
this.messaging.messagingBus.trigger('o-thread-loaded-messages', { thread: this.thread });
},
},
fields: {
@@ -5,6 +5,10 @@ import { attr, one } from '@mail/model/model_field';
import { format } from 'web.field_utils';
import { session } from '@web/session';
import { registry } from '@web/core/registry';
import { deserializeDateTime } from '@web/core/l10n/dates';
const formatters = registry.category("formatters");
registerModel({
name: 'TrackingValueItem',
@@ -41,10 +45,7 @@ registerModel({
}
return format.date(this.value);
case 'datetime':
if (this.value) {
return format.datetime(moment.utc(this.value));
}
return format.datetime(this.value);
return formatters.get("datetime")(deserializeDateTime(this.value), { timezone: true });
case 'float':
return format.float(this.value);
case 'integer':
@@ -39,7 +39,17 @@ registerModel({
* @private
*/
_onChangeThreadAttachmentsInWebClientView() {
if (!this.thread.mainAttachment && this.thread.attachmentsInWebClientView.length > 0) {
if (this.thread.model === 'account.move.line') {
// account.move.line is not actually a thread in python
this.setMainAttachmentFromIndex(0);
return;
}
if (
this.thread.areAttachmentsLoaded &&
!this.thread.isLoadingAttachments &&
!this.thread.mainAttachment &&
this.thread.attachmentsInWebClientView.length > 0
) {
this.setMainAttachmentFromIndex(0);
}
},
@@ -57,7 +67,7 @@ registerModel({
},
onChanges: [
new OnChange({
dependencies: ['thread.attachmentsInWebClientView'],
dependencies: ['thread.areAttachmentsLoaded', 'thread.attachmentsInWebClientView', 'thread.isLoadingAttachments'],
methodName: '_onChangeThreadAttachmentsInWebClientView',
}),
],
@@ -0,0 +1,144 @@
/** @odoo-module */
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { SIZES } from "@web/core/ui/ui_service";
import { patch } from "@web/core/utils/patch";
import { append, createElement, setAttributes } from "@web/core/utils/xml";
import { FormCompiler } from "@web/views/form/form_compiler";
function compileChatter(node, params) {
let hasActivities = false;
let hasFollowers = false;
let hasMessageList = false;
let hasParentReloadOnAttachmentsChanged;
let hasParentReloadOnFollowersUpdate = false;
let hasParentReloadOnMessagePosted = false;
let isAttachmentBoxVisibleInitially = false;
for (const childNode of node.children) {
const options = evaluateExpr(childNode.getAttribute("options") || "{}");
switch (childNode.getAttribute('name')) {
case 'activity_ids':
hasActivities = true;
break;
case 'message_follower_ids':
hasFollowers = true;
hasParentReloadOnFollowersUpdate = Boolean(options['post_refresh']);
isAttachmentBoxVisibleInitially = isAttachmentBoxVisibleInitially || Boolean(options['open_attachments']);
break;
case 'message_ids':
hasMessageList = true;
hasParentReloadOnAttachmentsChanged = options['post_refresh'] === 'always';
hasParentReloadOnMessagePosted = Boolean(options['post_refresh']);
isAttachmentBoxVisibleInitially = isAttachmentBoxVisibleInitially || Boolean(options['open_attachments']);
break;
}
}
const chatterContainerXml = createElement("ChatterContainer");
setAttributes(chatterContainerXml, {
"hasActivities": hasActivities,
"hasFollowers": hasFollowers,
"hasMessageList": hasMessageList,
"hasParentReloadOnAttachmentsChanged": hasParentReloadOnAttachmentsChanged,
"hasParentReloadOnFollowersUpdate": hasParentReloadOnFollowersUpdate,
"hasParentReloadOnMessagePosted": hasParentReloadOnMessagePosted,
"isAttachmentBoxVisibleInitially": isAttachmentBoxVisibleInitially,
"threadId": "props.record.resId or undefined",
"threadModel": "props.record.resModel",
"webRecord": "props.record",
});
const chatterContainerHookXml = createElement("div");
chatterContainerHookXml.classList.add("o_FormRenderer_chatterContainer");
append(chatterContainerHookXml, chatterContainerXml);
return chatterContainerHookXml;
}
function compileAttachmentPreview(node, params) {
const webClientViewAttachmentViewContainerHookXml = createElement("div");
webClientViewAttachmentViewContainerHookXml.classList.add('o_attachment_preview');
const webClientViewAttachmentViewContainerXml = createElement("WebClientViewAttachmentViewContainer");
setAttributes(webClientViewAttachmentViewContainerXml, {
"threadId": "props.record.resId or undefined",
"threadModel": "props.record.resModel",
});
append(webClientViewAttachmentViewContainerHookXml, webClientViewAttachmentViewContainerXml);
return webClientViewAttachmentViewContainerHookXml;
}
registry.category("form_compilers").add("chatter_compiler", {
selector: "div.oe_chatter",
fn: compileChatter,
});
registry.category("form_compilers").add("attachment_preview_compiler", {
selector: "div.o_attachment_preview",
fn: compileAttachmentPreview,
});
patch(FormCompiler.prototype, 'mail', {
compile() {
// TODO no chatter if in dialog?
const res = this._super(...arguments);
const chatterContainerHookXml = res.querySelector('.o_FormRenderer_chatterContainer');
if (!chatterContainerHookXml) {
return res; // no chatter, keep the result as it is
}
const chatterContainerXml = chatterContainerHookXml.querySelector('ChatterContainer');
if (chatterContainerHookXml.parentNode.classList.contains('o_form_sheet')) {
setAttributes(chatterContainerXml, {
"hasExternalBorder": 'true',
"hasMessageListScrollAdjust": 'false',
});
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
}
const webClientViewAttachmentViewHookXml = res.querySelector('.o_attachment_preview');
// TODO hasAttachmentViewer should also depend on the groups= and/or invisible modifier on o_attachment_preview (see invoice form)
if (webClientViewAttachmentViewHookXml) {
// in sheet bg (attachment viewer present)
setAttributes(webClientViewAttachmentViewHookXml, {
't-if': `hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`,
});
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
sheetBgChatterContainerHookXml.classList.add('o-isInFormSheetBg');
setAttributes(sheetBgChatterContainerHookXml, {
't-if': `hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`,
});
append(formSheetBgXml, sheetBgChatterContainerHookXml);
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector('ChatterContainer');
setAttributes(sheetBgChatterContainerXml, {
"isInFormSheetBg": "true",
"hasExternalBorder": "true",
"hasMessageListScrollAdjust": "false",
});
}
// after sheet bg (standard position, either aside or below)
if (webClientViewAttachmentViewHookXml) {
setAttributes(chatterContainerHookXml, {
't-if': `!(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL})`,
't-attf-class': `{{ uiService.size >= ${SIZES.XXL} and !(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}) ? "o-aside" : "" }}`,
});
setAttributes(chatterContainerXml, {
"isInFormSheetBg": "hasAttachmentViewer()",
"hasExternalBorder": `!(uiService.size >= ${SIZES.XXL} and !(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}))`,
"hasMessageListScrollAdjust": `uiService.size >= ${SIZES.XXL} and !(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL})`,
});
} else {
setAttributes(chatterContainerXml, {
"isInFormSheetBg": "false",
"hasExternalBorder": `uiService.size < ${SIZES.XXL}`,
"hasMessageListScrollAdjust": `uiService.size >= ${SIZES.XXL}`,
});
setAttributes(chatterContainerHookXml, {
't-attf-class': `{{ uiService.size >= ${SIZES.XXL} ? "o-aside" : "" }}`,
});
}
append(parentXml, chatterContainerHookXml);
return res;
},
});
@@ -0,0 +1,48 @@
/** @odoo-module */
import { useModels } from '@mail/component_hooks/use_models';
import { ChatterContainer } from "@mail/components/chatter_container/chatter_container";
import { WebClientViewAttachmentViewContainer } from '@mail/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container';
import { patch } from "@web/core/utils/patch";
import { FormRenderer } from "@web/views/form/form_renderer";
patch(FormRenderer.prototype, 'mail', {
setup() {
this._super();
if (this.env.services.messaging) {
useModels();
}
},
//--------------------------------------------------------------------------
// Mail Methods
//--------------------------------------------------------------------------
/**
* @returns {Messaging|undefined}
*/
getMessaging() {
return this.env.services.messaging && this.env.services.messaging.modelManager.messaging;
},
/**
* @returns {boolean}
*/
hasAttachmentViewer() {
if (!this.getMessaging() || !this.props.record.resId) {
return false;
}
const thread = this.getMessaging().models['Thread'].insert({
id: this.props.record.resId,
model: this.props.record.resModel,
});
return (
thread.attachmentsInWebClientView.length > 0
);
},
});
Object.assign(FormRenderer.components, {
ChatterContainer,
WebClientViewAttachmentViewContainer,
});
+4 -16
View File
@@ -15,10 +15,6 @@ import { doAction, getActionManagerServerData } from "@web/../tests/webclient/he
import core from 'web.core';
import legacyViewRegistry from "web.view_registry";
import FormView from 'web.FormView';
import ListView from 'web.ListView';
const { App, EventBus } = owl;
const { afterNextRender } = App;
const modelDefinitionsPromise = new Promise(resolve => {
@@ -464,15 +460,13 @@ function getOpenFormView(afterEvent, openView) {
},
}));
const waitMessages = func => afterNextRender(() => afterEvent({
eventName: 'o-thread-view-hint-processed',
eventName: 'o-thread-loaded-messages',
func,
message: "should wait until chatter loaded its messages",
predicate: ({ hint, threadViewer }) => {
predicate: ({ thread }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer &&
threadViewer.thread.model === action.res_model &&
threadViewer.thread.id === action.res_id
thread.model === action.res_model &&
thread.id === action.res_id
);
},
}));
@@ -530,12 +524,6 @@ function getOpenFormView(afterEvent, openView) {
* @returns {Object}
*/
async function start(param0 = {}) {
if (!param0.useWowlListForm) {
registry.category("views").remove("list"); // remove new list from registry
registry.category("views").remove("form"); // remove new form from registry
legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry
legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry
}
// patch _.debounce and _.throttle to be fast and synchronous.
patchWithCleanup(_, {
debounce: func => func,
@@ -4,8 +4,8 @@ import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { WEBCLIENT_LOAD_ROUTES } from '@mail/../tests/helpers/webclient_setup';
import testUtils from 'web.test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
import { ViewAdapter } from "@web/legacy/action_adapters";
import { clickEdit, patchWithCleanup, selectDropdownItem } from '@web/../tests/helpers/utils';
import { ListController } from "@web/views/list/list_controller";
QUnit.module('mail', {}, function () {
QUnit.module('Chatter');
@@ -37,7 +37,7 @@ QUnit.test('list activity widget with no activity', async function (assert) {
assert.containsOnce(document.body, '.o_mail_activity .o_activity_color_default');
assert.strictEqual(document.querySelector('.o_activity_summary').innerText, '');
assert.verifySteps(['/web/dataset/search_read']);
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
QUnit.test('list activity widget with activities', async function (assert) {
@@ -90,7 +90,7 @@ QUnit.test('list activity widget with activities', async function (assert) {
assert.containsOnce(secondRow, '.o_mail_activity .o_activity_color_planned.fa-clock-o');
assert.strictEqual(secondRow.querySelector('.o_activity_summary').innerText, 'Type 2');
assert.verifySteps(['/web/dataset/search_read']);
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
QUnit.test('list activity widget with exception', async function (assert) {
@@ -130,7 +130,7 @@ QUnit.test('list activity widget with exception', async function (assert) {
assert.containsOnce(document.body, '.o_activity_color_today.text-warning.fa-warning');
assert.strictEqual(document.querySelector('.o_activity_summary').innerText, 'Warning');
assert.verifySteps(['/web/dataset/search_read']);
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
QUnit.test('list activity widget: open dropdown', async function (assert) {
@@ -190,7 +190,7 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
serverData: { views },
});
patchWithCleanup(ViewAdapter.prototype, {
patchWithCleanup(ListController.prototype, {
setup() {
this._super();
const selectRecord = this.props.selectRecord;
@@ -223,8 +223,8 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
assert.strictEqual(document.querySelector('.o_activity_summary').innerText, 'Meet FP');
assert.verifySteps([
'/web/dataset/search_read',
'select_record [2,{\"mode\":\"readonly\"}]',
'web_search_read',
'select_record [2,{"activeIds":[2]}]',
'open dropdown',
'activity_format',
'action_feedback',
@@ -283,9 +283,9 @@ QUnit.test('list activity exception widget with activity', async function (asser
});
assert.containsN(document.body, '.o_data_row', 2, "should have two records");
assert.doesNotHaveClass(document.querySelector('.o_data_row .o_activity_exception_cell div'), 'fa-warning',
assert.doesNotHaveClass(document.querySelector('.o_data_row .o_activity_exception_cell div div'), 'fa-warning',
"there is no any exception activity on record");
assert.hasClass(document.querySelectorAll('.o_data_row .o_activity_exception_cell div')[1], 'fa-warning',
assert.hasClass(document.querySelectorAll('.o_data_row .o_activity_exception_cell div div')[1], 'fa-warning',
"there is an exception on a record");
});
@@ -479,7 +479,7 @@ QUnit.test('many2many_tags_email widget can load more than 40 records', async fu
const views = {
'mail.message,false,form': '<form><field name="partner_ids" widget="many2many_tags"/></form>',
};
var { click, openView } = await start({
var { openView } = await start({
serverData: { views },
});
await openView({
@@ -490,13 +490,12 @@ QUnit.test('many2many_tags_email widget can load more than 40 records', async fu
assert.strictEqual(document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, 100);
await click('.o_form_button_edit');
await clickEdit(document.body);
assert.hasClass(document.querySelector('.o_legacy_form_view'), 'o_form_editable');
assert.containsOnce(document.body, '.o_form_editable');
// add a record to the relation
await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
await testUtils.fields.many2one.clickHighlightedItem('partner_ids');
await selectDropdownItem(document.body, 'partner_ids', "Public user");
assert.strictEqual(document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, 101);
});
@@ -867,7 +867,7 @@ QUnit.test('Attachments that have been unlinked from server should be visually u
views: [[false, 'form']],
},
{
currentId: resPartnerId1,
resId: resPartnerId1,
resIds: [resPartnerId1, resPartnerId2],
},
);
@@ -5,7 +5,7 @@ import {
startServer,
} from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { editInput, editSelect, selectDropdownItem, patchWithCleanup, patchTimeZone } from "@web/../tests/helpers/utils";
import session from 'web.session';
import testUtils from 'web.test_utils';
@@ -34,16 +34,17 @@ QUnit.module('tracking_value_tests.js', {
</form>`,
};
this.start = async ({ res_id }) => {
const { openView, ...remainder } = await start({
const { openFormView, ...remainder } = await start({
serverData: { views },
});
await openView(
await openFormView(
{
res_model: 'mail.test.track.all',
res_id,
views: [[false, 'form']],
},
{ mode: 'edit' }
{
props: { mode: 'edit' },
},
);
return remainder;
};
@@ -63,7 +64,7 @@ QUnit.test('basic rendering of tracking value (float type)', async function (ass
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 12.30 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('input[name=float_field]'), 45.67);
await editInput(document.body, 'div[name=float_field] input', 45.67);
await click('.o_form_button_save');
assert.containsOnce(
document.body,
@@ -114,7 +115,7 @@ QUnit.test('rendering of tracked field of type float: from non-0 to 0', async fu
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('input[name=float_field]'), 0);
await editInput(document.body, 'div[name=float_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -130,7 +131,7 @@ QUnit.test('rendering of tracked field of type float: from 0 to non-0', async fu
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('input[name=float_field]'), 1);
await editInput(document.body, 'div[name=float_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -146,7 +147,7 @@ QUnit.test('rendering of tracked field of type integer: from non-0 to 0', async
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('input[name=integer_field]'), 0);
await editInput(document.body, 'div[name=integer_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -162,7 +163,7 @@ QUnit.test('rendering of tracked field of type integer: from 0 to non-0', async
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('input[name=integer_field]'), 1);
await editInput(document.body, 'div[name=integer_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -178,7 +179,7 @@ QUnit.test('rendering of tracked field of type monetary: from non-0 to 0', async
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editSelect(document.querySelector('div[name=monetary_field] > input'), 0);
await editInput(document.body, 'div[name=monetary_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -194,7 +195,7 @@ QUnit.test('rendering of tracked field of type monetary: from 0 to non-0', async
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editSelect(document.querySelector('div[name=monetary_field] > input'), 1);
await editInput(document.body, 'div[name=monetary_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -242,7 +243,7 @@ QUnit.test('rendering of tracked field of type char: from a string to empty stri
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: 'Marc' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('input[name=char_field]'), '');
await editInput(document.body, 'div[name=char_field] input', '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -258,7 +259,7 @@ QUnit.test('rendering of tracked field of type char: from empty string to a stri
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: '' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('input[name=char_field]'), 'Marc');
await editInput(document.body, 'div[name=char_field] input', 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -274,7 +275,7 @@ QUnit.test('rendering of tracked field of type date: from no date to a set date'
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: false });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('.o_datepicker[name=date_field] .o_datepicker_input'), '12/14/2018', ['change']);
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '12/14/2018', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -290,7 +291,7 @@ QUnit.test('rendering of tracked field of type date: from a set date to no date'
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: '2018-12-14' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('.o_datepicker[name=date_field] .o_datepicker_input'), '', ['change']);
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -300,14 +301,18 @@ QUnit.test('rendering of tracked field of type date: from a set date to no date'
});
QUnit.test('rendering of tracked field of type datetime: from no date and time to a set date and time', async function (assert) {
assert.expect(1);
assert.expect(2);
patchTimeZone(180);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: false });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('.o_datepicker[name=datetime_field] .o_datepicker_input'), '12/14/2018 13:42:28', ['change']);
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '12/14/2018 13:42:28', ['change']);
await click('.o_form_button_save');
const savedRecord = pyEnv.getData()["mail.test.track.all"].records.find(({id}) => id === mailTestTrackAllId1);
assert.strictEqual(savedRecord.datetime_field, '2018-12-14 10:42:28');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"Datetime:None12/14/2018 13:42:28",
@@ -318,15 +323,17 @@ QUnit.test('rendering of tracked field of type datetime: from no date and time t
QUnit.test('rendering of tracked field of type datetime: from a set date and time to no date and time', async function (assert) {
assert.expect(1);
patchTimeZone(180)
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: '2018-12-14 13:42:28 ' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('.o_datepicker[name=datetime_field] .o_datepicker_input'), '', ['change']);
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"Datetime:12/14/2018 13:42:28None",
"Datetime:12/14/2018 16:42:28None",
"should display the correct content of tracked field of type datetime: from a set date and time to no date and time (Datetime: 12/14/2018 13:42:28 -> None)"
);
});
@@ -338,7 +345,7 @@ QUnit.test('rendering of tracked field of type text: from some text to empty', a
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: 'Marc' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('textarea[name=text_field]'), '');
await editInput(document.body, 'div[name=text_field] textarea', '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -354,7 +361,7 @@ QUnit.test('rendering of tracked field of type text: from empty to some text', a
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: '' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editInput(document.querySelector('textarea[name=text_field]'), 'Marc');
await editInput(document.body, 'div[name=text_field] textarea', 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -370,7 +377,7 @@ QUnit.test('rendering of tracked field of type selection: from a selection to no
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ selection_field: 'first' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editSelect(document.querySelector('select[name=selection_field]'), '');
await editSelect(document.body, 'div[name=selection_field] select', false);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -386,7 +393,7 @@ QUnit.test('rendering of tracked field of type selection: from no selection to a
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editSelect(document.querySelector('select[name=selection_field]'), '"first"');
await editSelect(document.body, 'div[name=selection_field] select', '"first"');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -403,7 +410,7 @@ QUnit.test('rendering of tracked field of type many2one: from having a related r
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ many2one_field_id: resPartnerId1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('.o_field_many2one_selection input'), '', ['keyup']);
await editInput(document.body, ".o_field_many2one_selection input", '')
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
@@ -420,8 +427,7 @@ QUnit.test('rendering of tracked field of type many2one: from no related record
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.many2one.clickOpenDropdown('many2one_field_id');
await testUtils.fields.many2one.clickItem('many2one_field_id', 'Marc');
await selectDropdownItem(document.body, "many2one_field_id", "Marc")
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
+10
View File
@@ -148,3 +148,13 @@ export function getTag(node, lower = false) {
const tag = (node && node.nodeName) || "";
return lower ? tag.toLowerCase() : tag;
}
/**
* @param {Node} node
* @param {Object} attributes
*/
export function setAttributes(node, attributes) {
for (const [name, value] of Object.entries(attributes)) {
node.setAttribute(name, value);
}
}
@@ -215,6 +215,7 @@ function mapStaticListDatapoint(staticList) {
data: staticList.records.map(mapRecordDatapoint),
groupedBy: [],
orderedBy: staticList.orderBy,
count: staticList.count,
};
}
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { SIZES } from "@web/core/ui/ui_service";
import {
append,
combineAttributes,
@@ -187,7 +188,7 @@ export class FormCompiler extends ViewCompiler {
compileForm(el, params) {
const form = createElement("div", {
"t-att-class": "props.class",
"t-attf-class": `{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}}`,
"t-attf-class": `{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ uiService.size < ${SIZES.XXL} ? "flex-column" : "flex-nowrap h-100" }}`,
});
const sheetNode = el.querySelector("sheet");
@@ -2,6 +2,9 @@
import { Notebook } from "@web/core/notebook/notebook";
import { Field } from "@web/views/fields/field";
import { browser } from "@web/core/browser/browser";
import { useService } from "@web/core/utils/hooks";
import { useDebounced } from "@web/core/utils/timing";
import { ButtonBox } from "@web/views/form/button_box/button_box";
import { InnerGroup, OuterGroup } from "@web/views/form/form_group/form_group";
import { ViewButton } from "@web/views/view_button/view_button";
@@ -13,7 +16,7 @@ import { FormCompiler } from "./form_compiler";
import { FormLabel } from "./form_label";
import { StatusBarButtons } from "./status_bar_buttons/status_bar_buttons";
const { Component, useSubEnv, useRef, useState, xml } = owl;
const { Component, onMounted, onWillUnmount, useSubEnv, useRef, useState, xml } = owl;
export class FormRenderer extends Component {
setup() {
@@ -31,6 +34,10 @@ export class FormRenderer extends Component {
useBounceButton(useRef("compiled_view_root"), () => {
return !record.isInEdition;
});
this.uiService = useService('ui');
this.onResize = useDebounced(this.render, 200);
onMounted(() => browser.addEventListener('resize', this.onResize));
onWillUnmount(() => browser.removeEventListener('resize', this.onResize));
}
evalDomainFromRecord(record, expr) {
@@ -772,7 +772,10 @@ export class ListRenderer extends Component {
}
}
async onCellClicked(record, column) {
async onCellClicked(record, column, ev) {
if (ev.target.special_click) {
return;
}
const recordAfterResequence = async () => {
const recordIndex = this.props.list.records.indexOf(record);
await this.resequencePromise;
@@ -182,7 +182,7 @@
</td>
<t t-foreach="state.columns" t-as="column" t-key="column.id">
<t t-if="column.type === 'field'">
<td t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)" class="o_data_cell" t-att-name="column.name" t-att-class="getCellClass(column, record)" t-att-data-tooltip="getCellTitle(column, record)" data-tooltip-delay="1000" t-on-click="() => this.onCellClicked(record, column)" tabindex="-1">
<td t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)" class="o_data_cell" t-att-name="column.name" t-att-class="getCellClass(column, record)" t-att-data-tooltip="getCellTitle(column, record)" data-tooltip-delay="1000" t-on-click="(ev) => this.onCellClicked(record, column, ev)" tabindex="-1">
<t t-if="!evalModifier(column.modifiers.invisible, record)">
<t t-if="canUseFormatter(column, record)" t-esc="getFormattedValue(column, record)"/>
<Field t-else="" name="column.name" record="record" type="column.widget" fieldInfo="props.archInfo.fieldNodes[column.name]" setDirty="(isDirty) => this.setDirty(isDirty)"/>
+1
View File
@@ -725,6 +725,7 @@ export async function dragAndDrop(from, to, position) {
export async function clickDropdown(target, fieldName) {
const dropdownInput = target.querySelector(`[name='${fieldName}'] .dropdown input`);
dropdownInput.focus();
await nextTick();
await click(dropdownInput);
}
@@ -32,7 +32,7 @@ QUnit.module("Form Compiler", () => {
const arch = /*xml*/ `<form><div>lol</div></form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}}" class="o_form_nosheet" t-ref="compiled_view_root">
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div>lol</div>
</div>
</t>`;
@@ -44,7 +44,7 @@ QUnit.module("Form Compiler", () => {
const arch = /*xml*/ `<form><div class="someClass">lol<field name="display_name"/></div></form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}}" class="o_form_nosheet" t-ref="compiled_view_root">
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div class="someClass">
lol
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']"/>
@@ -128,7 +128,7 @@ QUnit.module("Form Compiler", () => {
const expected = /*xml*/ `
<t>
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}}" class="o_form_nosheet" t-ref="compiled_view_root">
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div class="o_form_statusbar"><StatusBarButtons readonly="!props.record.isInEdition"/></div>
<div>someDiv</div>
</div>
@@ -150,7 +150,7 @@ QUnit.module("Form Compiler", () => {
const expected = /*xml*/ `
<t>
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}}" t-ref="compiled_view_root">
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }}" t-ref="compiled_view_root">
<div class="o_form_sheet_bg">
<div class="o_form_statusbar"><StatusBarButtons readonly="!props.record.isInEdition"/></div>
<div>someDiv</div>
@@ -1,77 +1,66 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { ViewAdapter } from '@web/legacy/action_adapters';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils";
import FormView from "web.FormView";
import FormView from 'web.FormView';
QUnit.module("website_livechat", {}, function () {
QUnit.module("messaging_notification_handler_tests.js");
QUnit.module('website_livechat', {}, function () {
QUnit.module('messaging_notification_handler_tests.js');
QUnit.test(
"should open chat window on send chat request to website visitor",
async function (assert) {
assert.expect(3);
QUnit.test('should open chat window on send chat request to website visitor', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const websiteVisitorId1 = pyEnv['website.visitor'].create({
display_name: "Visitor #11",
});
const views = {
'website.visitor,false,form':
`<form>
const pyEnv = await startServer();
const websiteVisitorId1 = pyEnv["website.visitor"].create({
display_name: "Visitor #11",
});
const views = {
"website.visitor,false,form": `<form>
<header>
<button name="action_send_chat_request" string="Send chat request" class="btn btn-primary" type="button"/>
</header>
<field name="name"/>
</form>`,
};
const { openView } = await start({
serverData: { views },
View: FormView,
});
await openView({
res_model: 'website.visitor',
res_id: websiteVisitorId1,
views: [[false, 'form']],
});
patchWithCleanup(ViewAdapter.prototype, {
_trigger_up: async function({ name, data }) {
if (name === 'execute_action') {
this.env.services.rpc({
route: '/web/dataset/call_button',
params: {
args: [data.env.resIDs],
kwargs: { context: data.env.context },
method: data.action_data.name,
model: data.env.model,
}
};
const { openView, env } = await start({
serverData: { views },
View: FormView,
});
await openView({
res_model: "website.visitor",
res_id: websiteVisitorId1,
views: [[false, "form"]],
});
// Simulate a click on "Send chat request"
// This is a bit of a hack as it doesn't require the button at all to work.
await afterNextRender(async () => {
await env.services.rpc("/web/dataset/call_button", {
args: [websiteVisitorId1],
kwargs: { context: env.context },
method: "action_send_chat_request",
model: "website.visitor",
});
}
},
});
});
await afterNextRender(() =>
document.querySelector('button[name="action_send_chat_request"]').click()
);
assert.containsOnce(
document.body,
'.o_ChatWindow',
"should have a chat window open after sending chat request to website visitor"
);
assert.hasClass(
document.querySelector('.o_ChatWindow'),
'o-focused',
"chat window of livechat should be focused on open"
);
assert.strictEqual(
document.querySelector('.o_ChatWindowHeader_name').textContent,
"Visitor #11",
"chat window of livechat should have name of visitor in the name"
assert.containsOnce(
document.body,
".o_ChatWindow",
"should have a chat window open after sending chat request to website visitor"
);
assert.hasClass(
document.querySelector(".o_ChatWindow"),
"o-focused",
"chat window of livechat should be focused on open"
);
assert.strictEqual(
document.querySelector(".o_ChatWindowHeader_name").textContent,
"Visitor #11",
"chat window of livechat should have name of visitor in the name"
);
}
);
});
});