[IMP] web_widget_one2many_product_picker: Better usability

pull/1802/head
Alexandre D. Díaz 2021-02-11 16:36:04 +01:00
parent c8f718cc3a
commit aa4ef9b0ed
12 changed files with 283 additions and 59 deletions

View File

@ -180,6 +180,11 @@ Other example for 'purchase.order.line' fields:
Usage
=====
Parts of the widget:
~~~~~~~~~~~~~~~~~~~~
.. image:: https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker_anat.png
Preview:
~~~~~~~~

View File

@ -1,3 +1,8 @@
Parts of the widget:
~~~~~~~~~~~~~~~~~~~~
.. image:: ../static/img/product_picker_anat.png
Preview:
~~~~~~~~

View File

@ -380,15 +380,16 @@ ul.auto-toc {
</ul>
</li>
<li><a class="reference internal" href="#usage" id="id6">Usage</a><ul>
<li><a class="reference internal" href="#preview" id="id7">Preview:</a></li>
<li><a class="reference internal" href="#parts-of-the-widget" id="id7">Parts of the widget:</a></li>
<li><a class="reference internal" href="#preview" id="id8">Preview:</a></li>
</ul>
</li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id8">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id9">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id10">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id11">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id12">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id13">Maintainers</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id9">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id10">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id11">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id12">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id13">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id14">Maintainers</a></li>
</ul>
</li>
</ul>
@ -564,22 +565,28 @@ options=&quot;{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id6">Usage</a></h1>
<div class="section" id="parts-of-the-widget">
<h2><a class="toc-backref" href="#id7">Parts of the widget:</a></h2>
<blockquote>
<img alt="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker_anat.png" src="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker_anat.png" />
</blockquote>
</div>
<div class="section" id="preview">
<h2><a class="toc-backref" href="#id7">Preview:</a></h2>
<h2><a class="toc-backref" href="#id8">Preview:</a></h2>
<blockquote>
<img alt="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker.gif" src="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker.gif" />
</blockquote>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id8">Known issues / Roadmap</a></h1>
<h1><a class="toc-backref" href="#id9">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Translations in the xml options attribute of the field that use the widget cant be exported automatically to be translated</li>
<li>The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id9">Bug Tracker</a></h1>
<h1><a class="toc-backref" href="#id10">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
@ -587,15 +594,15 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id10">Credits</a></h1>
<h1><a class="toc-backref" href="#id11">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id11">Authors</a></h2>
<h2><a class="toc-backref" href="#id12">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id12">Contributors</a></h2>
<h2><a class="toc-backref" href="#id13">Contributors</a></h2>
<ul>
<li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
<blockquote>
@ -609,7 +616,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id13">Maintainers</a></h2>
<h2><a class="toc-backref" href="#id14">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -89,9 +89,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
model: this.basicFieldParams.model,
mainRecordData: this.getParent().getParent().state,
});
if (this.id) {
this.basicFieldParams.model.save(this.id, {savePoint: true});
}
// if (this.id) {
// this.basicFieldParams.model.save(this.id, {savePoint: true});
// }
var def2 = this.formView.getController(this).then(function (controller) {
self.controller = controller;
self.$el.empty();

View File

@ -126,6 +126,10 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
state: this._getRecordState(),
})
);
if (this._disabled) {
this._disableQuickCreate();
}
},
/**
@ -138,7 +142,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
// Ensures that the record won't be created twice
this._disabled = true;
this.$el.addClass("o_disabled");
this.$("input:not(:disabled)")
this.$("input:not(:disabled),button:not(:disabled)")
.addClass("o_temporarily_disabled")
.attr("disabled", "disabled");
},
@ -151,7 +155,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
// Allows to create again
this._disabled = false;
this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled")
this.$("input.o_temporarily_disabled,button.o_temporarily_disabled")
.removeClass("o_temporarily_disabled")
.attr("disabled", false);
},
@ -234,15 +238,14 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @returns {Deferred}
*/
_add: function () {
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
if (this._disabled) {
// Don't do anything if we are already creating a record
return $.Deferred();
}
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var self = this;
this._disableQuickCreate();
return this.saveRecord(this.handle, {
@ -251,39 +254,70 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
savePoint: true,
viewType: "form",
}).then(function () {
self._enableQuickCreate();
var record = self.model.get(self.handle);
self.trigger_up("create_quick_record", {
id: record.id,
self.trigger_up("restore_flip_card", {
success_callback: function () {
self.trigger_up("create_quick_record", {
id: record.id,
});
self.model.unsetDirty(self.handle);
//self._updateButtons();
self._enableQuickCreate();
},
block: true,
});
self.model.unsetDirty(self.handle);
self._updateButtons();
self.trigger_up("restore_flip_card");
});
},
_remove: function () {
this.trigger_up("restore_flip_card");
if (this._disabled) {
// Don't do anything if we are already creating a record
return $.Deferred();
}
this._disableQuickCreate();
this.trigger_up("restore_flip_card", {block: true});
var record = this.model.get(this.handle);
this.trigger_up("list_record_remove", {
id: this.renderer.state.id,
id: record.id,
});
},
_change: function () {
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
return $.Deferred();
}
this._disableQuickCreate();
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var record = this.model.get(this.handle);
this.trigger_up("update_quick_record", {
id: record.id,
this.trigger_up("restore_flip_card", {
success_callback: function () {
self.trigger_up("update_quick_record", {
id: record.id,
});
self.model.unsetDirty(self.handle);
//self._updateButtons();
self._enableQuickCreate();
},
block: true,
});
this.trigger_up("restore_flip_card");
this.model.unsetDirty(this.handle);
this._updateButtons();
},
_discard: function () {
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
return $.Deferred();
}
this._disableQuickCreate();
var record = this.model.get(this.handle);
this.model.discardChanges(this.handle, {
rollback: true,
@ -295,11 +329,13 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
this.update({}, {reload: false});
this.trigger_up("restore_flip_card");
this._updateButtons();
this._enableQuickCreate();
} else {
this.update({}, {reload: false}).then(function () {
self.model.unsetDirty(self.handle);
self.trigger_up("restore_flip_card");
self._updateButtons();
self._enableQuickCreate();
});
}
},

View File

@ -13,6 +13,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
var tools = require("web_widget_one2many_product_picker.tools");
var ProductPickerQuickModifPriceForm = require(
"web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm");
var FieldManagerMixin = require('web.FieldManagerMixin');
var qweb = core.qweb;
var _t = core._t;
@ -42,6 +43,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
front: [],
back: [],
};
this._lazyUpdateRecord = _.debounce(this._updateRecord.bind(this), 450);
},
/**
@ -105,6 +108,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
if (state) {
this._setState(state);
}
this.$card.removeClass("blocked");
// Avoid recreate active record
if (this.$card.hasClass("active")) {
this._processDynamicFields();
@ -177,6 +181,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
this.fields = this.getParent().state.fields;
this.fieldsInfo = this.getParent().state.fieldsInfo.form;
this.state = viewState;
if (recordSearch) {
this.recordSearch = recordSearch;
}
@ -193,6 +198,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
// Using directly the 'model record' instead of the state because
// the state it's a parsed version of this record that doesn't
// contains the '_virtual' attribute.
var model = this.options.basicFieldParams.model;
var record = model.get(this.state.id);
return {
record_search: this.recordSearch,
user_context: this.getSession() && this.getSession().user_context || {},
@ -204,6 +211,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
monetary: this._getMonetaryFieldValue.bind(this),
show_discount: this.options.showDiscount,
is_virtual: this.is_virtual,
modified: record && record.context.product_picker_modified,
active_model: '',
};
},
@ -497,6 +505,93 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
);
},
/**
* @private
* @returns {Promise}
*/
_saveRecord: function () {
var self = this;
var model = this.options.basicFieldParams.model;
var record = model.get(this.state.id);
return model.save(record.id, {
savePoint: true,
}).then(function () {
var record = model.get(self.state.id);
self.trigger_up("create_quick_record", {
id: record.id,
});
model.unsetDirty(self.state.id);
});
},
/**
* @private
*/
_updateRecord: function (changes) {
var model = this.options.basicFieldParams.model;
var record = model.get(this.state.id);
this.trigger_up("update_quick_record", {
id: record.id,
});
model.unsetDirty(this.state.id);
},
/**
* @private
* @returns {Promise}
*/
_addProduct: function () {
var self = this;
var changes = {};
if (this.state.data[this.options.fieldMap.product_uom_qty] === 0) {
changes[this.options.fieldMap.product_uom_qty] = 1;
}
var model = this.options.basicFieldParams.model;
this.$card.addClass("blocked");
return model.notifyChanges(this.state.id, changes).then(function () {
self._saveRecord();
});
},
/**
* @private
* @param {Number} amount
* @returns {Promise}
*/
_incProductQty: function (amount) {
var self = this;
this.state.data[this.options.fieldMap.product_uom_qty] += amount;
var model = this.options.basicFieldParams.model;
var record = model.get(this.state.id);
var state_data = record.data;
state_data[this.options.fieldMap.product_uom_qty] += amount;
var changes = _.pick(state_data, this.options.fieldMap.product_uom_qty);
return model.notifyChanges(record.id, changes).then(function () {
self._processDynamicFields();
self._lazyUpdateRecord();
});
},
/**
* @private
*/
_doInteractAnim: function (target, currentTarget) {
var $target = $(target);
var $currentTarget = $(currentTarget);
var $img = $currentTarget.find(".oe_flip_card_front img");
$target.addClass('o_catch_attention');
$target.on('animationend', function () {
$target.removeClass('o_catch_attention');
$target.off('animationend');
});
$img.addClass('oe_product_picker_catch_attention');
$img.on('animationend', function () {
$img.removeClass('oe_product_picker_catch_attention');
$img.off('animationend');
});
},
/**
* @private
*/
@ -531,11 +626,31 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @param {ClickEvent} evt
*/
_onClickFlipCard: function (evt) {
// Avoid clicks on form elements
if (['INPUT', 'BUTTON', 'A'].indexOf(evt.target.tagName) !== -1) {
if (['INPUT', 'BUTTON', 'A'].indexOf(evt.target.tagName) !== -1 || this.$card.hasClass('blocked')) {
return;
}
var $target = $(evt.target);
if (!this.options.readOnlyMode) {
if (
$target.hasClass('add_product') ||
$target.parents('.add_product').length
) {
if (!this.is_adding_product) {
this.is_adding_product = true;
this._addProduct();
this._doInteractAnim(evt.target, evt.currentTarget);
}
return;
} else if (
$target.hasClass('product_qty') ||
$target.parents('.product_qty').length
) {
this._incProductQty(1);
this._doInteractAnim(evt.target, evt.currentTarget);
return;
}
}
if (!this._clickFlipCardDelayed) {
this._clickFlipCardDelayed = setTimeout(
this._onClickDelayedFlipCard.bind(this, evt),
@ -644,7 +759,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
/**
* @private
*/
_onRestoreFlipCard: function () {
_onRestoreFlipCard: function (evt) {
var self = this;
this.$card.removeClass("active");
this.$front.removeClass("d-none");
@ -660,7 +775,16 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
zIndex: "",
});
self.$card.off('transitionend');
if (evt.data.success_callback) {
evt.data.success_callback();
}
});
} else if (evt.data.success_callback) {
evt.data.success_callback();
}
if (evt.data.block) {
this.$card.addClass("blocked");
}
},

View File

@ -104,14 +104,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
*/
updateState: function (state, params) {
var self = this;
var sparams = _.extend({}, params, {noRender: true});
if (_.isEqual(this.state.data, state.data)) {
return this._super.apply(this, arguments);
return this._super(state, sparams);
}
var old_state = _.clone(this.state.data);
return this._super(
state,
_.extend({}, params, {noRender: true})
).then(function () {
return this._super(state, sparams).then(function () {
self._updateStateRecords(old_state);
});
},
@ -151,6 +149,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
var widget = this.widgets[eb];
if (
widget &&
widget.state &&
widget.state.data[this.options.field_map.product].data.id === widget_product_id
) {
found = true;
@ -194,7 +193,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
var found = false;
for (var e in this.state.data) {
var current_state = this.state.data[e];
if (current_state.id === old_state.id) {
if (current_state.id === old_state.id || (typeof current_state.data.id !== 'undefined' && current_state.data.id === old_state.data.id)) {
found = true;
break;
}
@ -203,6 +202,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
states_to_destroy.push(old_state);
}
}
this._removeRecords(states_to_destroy);
// Records to Update or Create
@ -215,12 +215,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
var search_record = false;
for (var e = this.widgets.length-1; e>=0; --e) {
var widget = this.widgets[e];
if (!widget) {
if (!widget || !widget.state) {
// Already processed widget (deleted)
continue;
}
if (widget.state.id === state.id) {
if (widget.state.id === state.id || (typeof state.data.id !== 'undefined' && widget.state.data.id === state.data.id)) {
widget.recreate(state);
exists = true;
break;
@ -236,8 +236,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
// Remove "pure virtual" records that have the same product that the new record
if (
widget.is_virtual &&
widget.state.data[this.options.field_map.product].data.id === state.data[this.options.field_map.product].data.id &&
widget.state.data[this.options.compa].data.id === state.data[this.options.field_map.product].data.id
widget.state.data[this.options.field_map.product].data.id === state.data[this.options.field_map.product].data.id
) {
to_destroy.push(widget);
delete this.widgets[e];
@ -284,7 +283,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
},
/**
* Compare search results with current lines
* Compare search results with current lines.
* Link a current state with the 'search record'.
*
* @private
* @param {Array[Object]} results

View File

@ -580,11 +580,20 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @param {CustomEvent} evt
*/
_onCreateQuickRecord: function (evt) {
var self = this;
this.parent_controller.model.setPureVirtual(evt.data.id, false);
this._setValue({operation: "ADD", id: evt.data.id});
if (this.options.auto_save) {
this.parent_controller.saveRecord(undefined, {stayInEdit: true});
if (!self.options.auto_save) {
self.parent_controller.model.updateRecordContext(evt.data.id, {
product_picker_modified: true,
});
}
this._setValue({operation: "ADD", id: evt.data.id}).then(function () {
if (self.options.auto_save) {
self.parent_controller.saveRecord(undefined, {stayInEdit: true}).then(function () {
self.renderer.updateState(self.value);
});
}
});
},
/**
@ -594,10 +603,19 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @param {CustomEevent} evt
*/
_onUpdateQuickRecord: function (evt) {
this._setValue({operation: "UPDATE", id: evt.data.id, data: evt.data.data});
if (this.options.auto_save) {
this.parent_controller.saveRecord(undefined, {stayInEdit: true});
var self = this;
if (!self.options.auto_save) {
self.parent_controller.model.updateRecordContext(evt.data.id, {
product_picker_modified: true,
});
}
this._setValue({operation: "UPDATE", id: evt.data.id, data: evt.data.data}).then(function () {
if (self.options.auto_save) {
self.parent_controller.saveRecord(undefined, {stayInEdit: true}).then(function () {
self.renderer.updateState(self.value);
});
}
});
},
/**

View File

@ -59,6 +59,10 @@
transition: top $one2many-product-picker-transition-3d-time, left $one2many-product-picker-transition-3d-time, width $one2many-product-picker-transition-3d-time, height $one2many-product-picker-transition-3d-time;
height: $one2many-product-picker-card-min-height;
&.blocked {
filter: blur(2px);
}
&.disabled {
filter: grayscale(100%);
opacity: 0.5;
@ -211,6 +215,9 @@
font-size: 0.95rem;
z-index: 0;
}
.add_product, .product_qty, .price_unit {
cursor: pointer;
}
}
}
}
@ -230,3 +237,20 @@
text-align: center;
}
}
.oe_product_picker_catch_attention {
position: relative;
animation: productPickerCatchAttention 200ms normal forwards;
}
@keyframes productPickerCatchAttention {
0% {
transform: scale(1.0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1.0);
}
}

View File

@ -72,11 +72,16 @@
<div class="oe_flip_container p-1 col-4 col-sm-4 col-md-2 col-lg-2 col-xl-1">
<div t-attf-class="oe_flip_card {{!state &amp;&amp; 'disabled' || ''}}">
<div class="oe_flip_card_inner text-center">
<div t-attf-class="oe_flip_card_front p-0 {{state &amp;&amp; !is_virtual &amp;&amp; 'border-primary' || ''}}">
<div t-attf-class="oe_flip_card_front p-0 {{(modified &amp;&amp; 'border-warning') || (state &amp;&amp; !is_virtual &amp;&amp; 'border-success') || ''}}">
<t t-if="state">
<t t-if="!is_virtual">
<div class="position-absolute m-0 text-left">
<span t-att-data-field="field_map.product_uom_qty" t-attf-data-esc="str({{field_map.product_uom_qty}}) + ' x ' + {{field_map.product_uom}}.data.display_name" class="badge badge-primary font-weight-bold rounded-0 mt-1 p-2" />
<span t-att-data-field="field_map.product_uom_qty" t-attf-data-esc="str({{field_map.product_uom_qty}}) + ' x ' + {{field_map.product_uom}}.data.display_name" t-attf-class="badge {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-1 p-2 product_qty" />
</div>
</t>
<t t-else="">
<div class="position-absolute m-0 text-left">
<span class="badge badge-primary font-weight-bold rounded-0 mt-1 p-2 add_product"><i class="fa fa-plus"></i> Add 1 <t t-esc="state.data[field_map.product_uom].data.display_name"/></span>
</div>
</t>
<div class="position-absolute m-0 text-left badge_price">

View File

@ -3,7 +3,7 @@
<t t-name="One2ManyProductPicker.QuickCreate.FormButtons">
<div class="oe_one2many_product_picker_form_buttons">
<t t-if="state == 'new'">
<button class="btn btn-primary oe_record_add">Add</button>
<button t-attf-class="btn btn-primary oe_record_add">Add</button>
</t>
<t t-elif="state == 'dirty'">
<button class="btn btn-success oe_record_change mr-2"><i class="fa fa-check" /></button>