[IMP] point_of_sale: improve UX of product configurator popup
- Improved the UX of the product configurator. - Display of attribute name in orderline name. Previously, the user had to click on the checkbox or radio to select the option, clicking on the label didn't work. The attribute name was not displayed in the orderline name, only the values. Now the input has been replaced by a button, making it easier to use on touch-screen devices. The attribute name is displayed in the orderline name in the format `Size: M, Fabric: Leather`. closes odoo/odoo#147744 Taskids: 3638192 & 3633555 Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
committed by
Loukas Wets (lowe)
parent
9beebd4141
commit
a261bd5bcd
+11
-1
@@ -38,6 +38,11 @@ export class BaseProductAttribute extends Component {
|
||||
extra,
|
||||
};
|
||||
}
|
||||
|
||||
getFormatPriceExtra(val) {
|
||||
const sign = val < 0 ? "- " : "+ ";
|
||||
return sign + this.env.utils.formatCurrency(Math.abs(val));
|
||||
}
|
||||
}
|
||||
|
||||
export class RadioProductAttribute extends BaseProductAttribute {
|
||||
@@ -57,6 +62,10 @@ export class RadioProductAttribute extends BaseProductAttribute {
|
||||
}
|
||||
}
|
||||
|
||||
export class PillsProductAttribute extends BaseProductAttribute {
|
||||
static template = "point_of_sale.PillsProductAttribute";
|
||||
}
|
||||
|
||||
export class SelectProductAttribute extends BaseProductAttribute {
|
||||
static template = "point_of_sale.SelectProductAttribute";
|
||||
}
|
||||
@@ -71,7 +80,7 @@ export class MultiProductAttribute extends BaseProductAttribute {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
attribute_value_ids: [],
|
||||
attribute_value_ids: {},
|
||||
custom_value: "",
|
||||
});
|
||||
|
||||
@@ -91,6 +100,7 @@ export class ProductConfiguratorPopup extends AbstractAwaitablePopup {
|
||||
static template = "point_of_sale.ProductConfiguratorPopup";
|
||||
static components = {
|
||||
RadioProductAttribute,
|
||||
PillsProductAttribute,
|
||||
SelectProductAttribute,
|
||||
ColorProductAttribute,
|
||||
MultiProductAttribute,
|
||||
|
||||
+74
-28
@@ -3,21 +3,46 @@
|
||||
|
||||
<t t-name="point_of_sale.RadioProductAttribute">
|
||||
<div class="configurator_radio" t-ref="root">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<t t-foreach="values" t-as="value" t-key="value.id">
|
||||
<div class="attribute-name-cell form-check">
|
||||
<input class="form-check-input" type="radio" t-model="state.attribute_value_ids" t-att-name="attribute.id"
|
||||
<input class="form-check-input radio-check" type="radio" t-model="state.attribute_value_ids" t-att-name="attribute.id"
|
||||
t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
|
||||
<label class="form-check-label" t-att-name="value.name" t-attf-for="{{ attribute.id }}_{{ value.id }}">
|
||||
<span t-esc="value.name"/>
|
||||
</label>
|
||||
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-3">
|
||||
<span t-esc="value.name"/>
|
||||
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
|
||||
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
|
||||
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
|
||||
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="value.id == state.attribute_value_ids && value.is_custom" class="custom-value-cell">
|
||||
<div t-if="value.id == state.attribute_value_ids and value.is_custom" class="custom-value-cell">
|
||||
<input class="custom_value form-control form-control-lg mt-2" type="text" t-model="state.custom_value"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="point_of_sale.PillsProductAttribute">
|
||||
<div class="configurator_radio" t-ref="root">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<t t-foreach="values" t-as="value" t-key="value.id">
|
||||
<div class="attribute-name-cell">
|
||||
<input class="form-check-input d-none" type="radio" t-model="state.attribute_value_ids" t-att-name="attribute.id"
|
||||
t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
|
||||
<label
|
||||
t-attf-class="btn d-flex {{ value.id == state.attribute_value_ids ? 'btn-primary' : 'btn-secondary' }}"
|
||||
t-att-name="value.name"
|
||||
t-attf-for="{{ attribute.id }}_{{ value.id }}">
|
||||
<span t-esc="value.name"/>
|
||||
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
|
||||
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
|
||||
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div t-if="value.id == state.attribute_value_ids and value.is_custom" class="custom-value-cell">
|
||||
<input class="custom_value form-control form-control-lg mt-2" type="text" t-model="state.custom_value"/>
|
||||
</div>
|
||||
</t>
|
||||
@@ -29,12 +54,12 @@
|
||||
<div>
|
||||
<t t-set="is_custom" t-value="false"/>
|
||||
|
||||
<select class="configurator_select form-select form-select-lg" t-model="state.attribute_value_ids">
|
||||
<select class="configurator_select form-select form-select-md" t-model="state.attribute_value_ids">
|
||||
<option t-foreach="values" t-as="value" t-key="value.id" t-att-value="value.id">
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom && value.id == state.attribute_value_ids)"/>
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom and value.id == state.attribute_value_ids)"/>
|
||||
<t t-esc="value.name"/>
|
||||
<t t-if="value.price_extra">
|
||||
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
|
||||
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
|
||||
</t>
|
||||
</option>
|
||||
</select>
|
||||
@@ -49,13 +74,20 @@
|
||||
|
||||
<ul class="color_attribute_list d-flex gap-3">
|
||||
<li t-foreach="values" t-as="value" t-key="value.id" class="color_attribute_list_item">
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom && value.id == state.attribute_value_ids)"/>
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom and value.id == state.attribute_value_ids)"/>
|
||||
<t t-set="img_style" t-value="value.image ? 'background:url(/web/image/product.template.attribute.value/' + value.id + '/image); background-size:cover;' : ''"/>
|
||||
<t t-set="color_style" t-value="value.is_custom ? '' : 'background-color: ' + value.html_color" />
|
||||
<label t-attf-class="configurator_color rounded border {{ value.id == state.attribute_value_ids ? 'active border-3 border-primary' : 'border-3 border-secondary' }}"
|
||||
t-attf-style="#{img_style or color_style}" t-att-data-color="value.name">
|
||||
<input class="m-4 opacity-0" type="radio" t-model="state.attribute_value_ids" t-att-value="value.id" t-att-name="attribute.id"/>
|
||||
</label>
|
||||
<span class="d-flex flex-row justify-content-center align-items-center">
|
||||
<label t-attf-class="configurator_color rounded-circle border {{ value.id == state.attribute_value_ids ? 'active border-3 border-primary' : 'border-3 border-secondary' }}"
|
||||
t-attf-style="{{ img_style or color_style }}" t-att-data-color="value.name">
|
||||
<input class="m-2 opacity-0" type="radio" t-model="state.attribute_value_ids" t-att-value="value.id" t-att-name="attribute.id"/>
|
||||
</label>
|
||||
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
|
||||
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
|
||||
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -64,15 +96,25 @@
|
||||
</t>
|
||||
|
||||
<t t-name="point_of_sale.MultiProductAttribute">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div t-foreach="values" t-as="value" t-key="value.id" class="form-check">
|
||||
<label class="form-check-label" t-attf-for="multi-{{value.id}}" t-esc="value.name" />
|
||||
<input class="form-check-input" type="checkbox" t-attf-name="multi-{{value.id}}" t-model="state.attribute_value_ids[value.id]" />
|
||||
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-3">
|
||||
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
|
||||
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<div t-foreach="values" t-as="value" t-key="value.id">
|
||||
<input
|
||||
class="form-check-input d-none"
|
||||
type="checkbox"
|
||||
t-attf-id="multi-{{value.id}}"
|
||||
t-attf-name="multi-{{value.id}}"
|
||||
t-model="state.attribute_value_ids[value.id]"/>
|
||||
<label
|
||||
t-attf-class="form-check-label btn d-flex {{ state.attribute_value_ids[value.id] === true ? 'btn-primary' : 'btn-secondary' }}"
|
||||
t-attf-name="multi-{{value.id}}"
|
||||
t-attf-for="multi-{{value.id}}">
|
||||
<span t-esc="value.name"/>
|
||||
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-2">
|
||||
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
|
||||
<t t-esc="getFormatPriceExtra(value.price_extra)"/>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
@@ -85,9 +127,13 @@
|
||||
</div>
|
||||
|
||||
<main class="body modal-body product_configurator_attributes m-0 text-start">
|
||||
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute mb-3">
|
||||
<div class="attribute_name mb-2 fw-bolder" t-esc="attribute.name"/>
|
||||
<RadioProductAttribute t-if="attribute.display_type === 'radio' or attribute.display_type === 'pills'" attribute="attribute"/>
|
||||
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute mb-4">
|
||||
<div class="attribute_name mb-2 fw-bolder d-flex w-100 align-items-center gap-2">
|
||||
<span t-esc="attribute.name" />
|
||||
<hr class="hr flex-grow-1" />
|
||||
</div>
|
||||
<RadioProductAttribute t-if="attribute.display_type === 'radio'" attribute="attribute"/>
|
||||
<PillsProductAttribute t-elif="attribute.display_type === 'pills'" attribute="attribute"/>
|
||||
<SelectProductAttribute t-elif="attribute.display_type === 'select'" attribute="attribute"/>
|
||||
<ColorProductAttribute t-elif="attribute.display_type === 'color'" attribute="attribute"/>
|
||||
<MultiProductAttribute t-elif="attribute.display_type === 'multi'" attribute="attribute"/>
|
||||
|
||||
@@ -4,7 +4,7 @@ export function pickRadio(name) {
|
||||
return [
|
||||
{
|
||||
content: `picking radio attribute with name ${name}`,
|
||||
trigger: `.product-configurator-popup .attribute-name-cell label[name='${name}']`,
|
||||
trigger: `.product-configurator-popup div.attribute-name-cell:contains('${name}') input`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+2
-1
@@ -147,6 +147,7 @@ export class AttributeSelection extends Component {
|
||||
|
||||
getfPriceExtra(value) {
|
||||
const priceExtra = this._getPriceExtra(value);
|
||||
return this.selfOrder.formatMonetary(priceExtra);
|
||||
const sign = priceExtra < 0 ? "- " : "+ ";
|
||||
return sign + this.selfOrder.formatMonetary(Math.abs(priceExtra));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
<div class="name position-relative d-flex flex-column justify-content-center align-items-center flex-grow-1 w-100 p-4 text-center">
|
||||
<span t-out="value.name"/>
|
||||
<span t-if="shouldShowPriceExtra(value)">
|
||||
+ <t t-out="getfPriceExtra(value)" />
|
||||
<t t-esc="getfPriceExtra(value)"/>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user