[FIX] web, *: record key is reactive in KanbanRecord
Before this commit, calls to record[fieldname][value/raw_value] in
the arch of a kanban view did not take into account updates on the Record
Datapoint. So if any data in the Record changed, the [raw_value/value]
was not modified.
In a custom KanbanRecord, you should never use the record containing the
value and raw_value. We will therefore replace this call by using the
datapoint record directly.
How to reproduce:
- Have a kanban view with a record.my_field.value and a widget allowing
you to modify the value of my_field.
- Click on the widget
Before this commit:
The value of my_field does not change
After this commit:
The value of my_field has been updated.
closes odoo/odoo#131165
Related: odoo/enterprise#45413
Signed-off-by: Jorge Pinna Puissant (jpp) <jpp@odoo.com>
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
<t t-name="im_livechat.KanbanRecordMenu" t-inherit="web.KanbanRecordMenu">
|
||||
<xpath expr="//Dropdown" position="attributes">
|
||||
<attribute name="position">'bottom-start'</attribute>
|
||||
<attribute name="menuClass" add="` rounded-0 ${record.available_operator_ids.raw_value.length > 0 ? 'o-livechat-ChannelKanban-highlighted': ''}`" separator="+"/>
|
||||
<attribute name="menuClass" add="` rounded-0 ${props.record.data.available_operator_ids.count > 0 ? 'o-livechat-ChannelKanban-highlighted': ''}`" separator="+"/>
|
||||
<attribute name="togglerClass" add="' bg-transparent px-2'" separator="+"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
@@ -21,7 +21,7 @@ export class ProductCatalogKanbanRecord extends KanbanRecord {
|
||||
useSubEnv({
|
||||
currencyId: this.props.record.context.product_catalog_currency_id,
|
||||
orderId: this.props.record.context.product_catalog_order_id,
|
||||
productId: this.record.id.raw_value,
|
||||
productId: this.props.record.resId,
|
||||
addProduct: this.addProduct.bind(this),
|
||||
removeProduct: this.removeProduct.bind(this),
|
||||
increaseQuantity: this.increaseQuantity.bind(this),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
t-att-class="{'text-bg-primary': state.quantity || props.record.productCatalogData.readOnly}">
|
||||
<t t-call="{{ templates[this.constructor.KANBAN_BOX_ATTRIBUTE] }}"
|
||||
t-call-context="this.renderingContext"/>
|
||||
<ProductCatalogSOL productId="record.id.raw_value"
|
||||
<ProductCatalogSOL productId="props.record.resId"
|
||||
quantity="state.quantity"
|
||||
price="state.price"
|
||||
readOnly="props.record.productCatalogData.readOnly"/>
|
||||
|
||||
@@ -503,19 +503,28 @@ export function useRecordObserver(callback) {
|
||||
let alive = true;
|
||||
const fct = (props) => {
|
||||
const def = new Deferred();
|
||||
let firstCall = true;
|
||||
effect(
|
||||
batched(
|
||||
async (record) => {
|
||||
if (!alive) {
|
||||
// effect doesn't clean up when the component is unmounted.
|
||||
// We must do it manually.
|
||||
return;
|
||||
}
|
||||
async (record) => {
|
||||
if (firstCall) {
|
||||
firstCall = false;
|
||||
await callback(record);
|
||||
def.resolve();
|
||||
},
|
||||
() => new Promise((resolve) => window.requestAnimationFrame(resolve))
|
||||
),
|
||||
} else {
|
||||
return batched(
|
||||
async (record) => {
|
||||
if (!alive) {
|
||||
// effect doesn't clean up when the component is unmounted.
|
||||
// We must do it manually.
|
||||
return;
|
||||
}
|
||||
await callback(record);
|
||||
def.resolve();
|
||||
},
|
||||
() => new Promise((resolve) => window.requestAnimationFrame(resolve))
|
||||
)(record);
|
||||
}
|
||||
},
|
||||
[props.record]
|
||||
);
|
||||
return def;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { evalDomain } from "@web/core/domain";
|
||||
import { ColorList } from "@web/core/colorlist/colorlist";
|
||||
import { evalDomain } from "@web/core/domain";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { registry } from "@web/core/registry";
|
||||
@@ -9,6 +9,7 @@ import { useTooltip } from "@web/core/tooltip/tooltip_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { url } from "@web/core/utils/urls";
|
||||
import { useRecordObserver } from "@web/model/relational_model/utils";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { fileTypeMagicWordMap, imageCacheKey } from "@web/views/fields/image/image_field";
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
@@ -24,7 +25,7 @@ import { KanbanCompiler } from "./kanban_compiler";
|
||||
import { KanbanCoverImageDialog } from "./kanban_cover_image_dialog";
|
||||
import { KanbanDropdownMenuWrapper } from "./kanban_dropdown_menu_wrapper";
|
||||
|
||||
import { Component, onMounted, onWillUpdateProps, useRef } from "@odoo/owl";
|
||||
import { Component, onMounted, onWillUpdateProps, useRef, useState } from "@odoo/owl";
|
||||
const { COLORS } = ColorList;
|
||||
|
||||
const formatters = registry.category("formatters");
|
||||
@@ -106,7 +107,13 @@ function getValue(record, fieldName) {
|
||||
}
|
||||
|
||||
export function getFormattedRecord(record) {
|
||||
const formattedRecord = {};
|
||||
const formattedRecord = {
|
||||
id: {
|
||||
value: record.resId,
|
||||
raw_value: record.resId,
|
||||
},
|
||||
};
|
||||
|
||||
for (const fieldName of record.fieldNames) {
|
||||
formattedRecord[fieldName] = {
|
||||
value: getValue(record, fieldName),
|
||||
@@ -195,13 +202,21 @@ export class KanbanRecord extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
this.createRecordAndWidget(this.props);
|
||||
this.dataState = useState({ record: {}, widget: {} });
|
||||
this.createWidget(this.props);
|
||||
onWillUpdateProps(this.createWidget);
|
||||
useRecordObserver((record) =>
|
||||
Object.assign(this.dataState.record, getFormattedRecord(record))
|
||||
);
|
||||
this.rootRef = useRef("root");
|
||||
onMounted(() => {
|
||||
// FIXME: this needs to be changed to an attribute on the root node...
|
||||
this.allowGlobalClick = !!this.rootRef.el.querySelector(ALLOW_GLOBAL_CLICK);
|
||||
});
|
||||
onWillUpdateProps(this.createRecordAndWidget);
|
||||
}
|
||||
|
||||
get record() {
|
||||
return this.dataState.record;
|
||||
}
|
||||
|
||||
getFormattedValue(fieldId) {
|
||||
@@ -211,20 +226,18 @@ export class KanbanRecord extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns both "record" and "widget" properties on the kanban record.
|
||||
* Assigns "widget" properties on the kanban record.
|
||||
*
|
||||
* @param {Object} props
|
||||
*/
|
||||
createRecordAndWidget(props) {
|
||||
createWidget(props) {
|
||||
const { archInfo, list } = props;
|
||||
const { activeActions } = archInfo;
|
||||
|
||||
this.record = getFormattedRecord(this.props.record);
|
||||
// Widget
|
||||
const deletable =
|
||||
activeActions.delete && (!list.groupByField || list.groupByField.type !== "many2many");
|
||||
const editable = activeActions.edit;
|
||||
this.widget = {
|
||||
this.dataState.widget = {
|
||||
deletable,
|
||||
editable,
|
||||
isHtmlEmpty,
|
||||
@@ -357,10 +370,10 @@ export class KanbanRecord extends Component {
|
||||
kanban_image: (...args) => getImageSrcFromRecordInfo(this.props.record, ...args),
|
||||
luxon,
|
||||
read_only_mode: this.props.readonly,
|
||||
record: this.record,
|
||||
record: this.dataState.record,
|
||||
selection_mode: this.props.forceGlobalClick,
|
||||
user_context: this.user.context,
|
||||
widget: this.widget,
|
||||
widget: this.dataState.widget,
|
||||
__comp__: Object.assign(Object.create(this), { this: this }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</t>
|
||||
|
||||
<t t-name="web.KanbanColorPicker" owl="1">
|
||||
<ul t-if="this.widget.editable" class="oe_kanban_colorpicker">
|
||||
<ul t-if="this.dataState.widget.editable" class="oe_kanban_colorpicker">
|
||||
<!--
|
||||
Used in KanbanRecord
|
||||
Note: `props` is only accessible through `this` as we call the compiled template with
|
||||
|
||||
@@ -9078,6 +9078,7 @@ QUnit.module("Views", (hooks) => {
|
||||
QUnit.test("resequence a record twice", async (assert) => {
|
||||
serverData.models.partner.records = [];
|
||||
|
||||
const def = makeDeferred();
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
@@ -9092,6 +9093,7 @@ QUnit.module("Views", (hooks) => {
|
||||
async mockRPC(route) {
|
||||
if (route === "/web/dataset/resequence") {
|
||||
assert.step("resequence");
|
||||
await def;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -9116,6 +9118,8 @@ QUnit.module("Views", (hooks) => {
|
||||
);
|
||||
|
||||
await dragAndDrop(".o_kanban_record:nth-child(2)", ".o_kanban_record:nth-child(3)");
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_kanban_group:first-child .o_kanban_record", 2);
|
||||
assert.deepEqual(
|
||||
@@ -9168,6 +9172,43 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(getCard(2).querySelector(".o_widget").innerText, '{"foo":"gnap"}');
|
||||
});
|
||||
|
||||
QUnit.test("kanban card: record value should be update", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
static template = owl.xml`<div><button t-on-click="onClick">CLick</button></div>`;
|
||||
onClick() {
|
||||
this.props.record.update({ foo: "yolo" });
|
||||
}
|
||||
}
|
||||
const myComponent = {
|
||||
component: MyComponent,
|
||||
};
|
||||
viewWidgetRegistry.add("test", myComponent);
|
||||
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban>
|
||||
<field name="foo"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<div class="foo" t-esc="record.foo.value"/>
|
||||
<widget name="test"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(getCard(0).querySelector(".foo").innerText, "yop");
|
||||
|
||||
await click(getCard(0).querySelector("button"));
|
||||
await nextTick();
|
||||
assert.strictEqual(getCard(0).querySelector(".foo").innerText, "yolo");
|
||||
});
|
||||
|
||||
QUnit.test("column progressbars properly work", async (assert) => {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
|
||||
Reference in New Issue
Block a user