19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -0,0 +1,92 @@
import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";
import { googlePlacesSession } from "@google_address_autocomplete/google_places_session";
import { KeepLast } from "@web/core/utils/concurrency";
export class AddressForm extends Interaction {
static selector = ".oe_cart .address_autoformat";
static selectorHas = "input[name='street'][data-autocomplete-enabled='1']";
dynamicContent = {
"input[name='street']": { "t-on-input.withTarget": this.debounced(this.onStreetInput, 200) },
".js_autocomplete_result": { "t-on-click.withTarget": this.onClickAutocompleteResult },
};
setup() {
this.streetAndNumberInput = this.el.querySelector("input[name='street']");
this.cityInput = this.el.querySelector("input[name='city']");
this.zipInput = this.el.querySelector("input[name='zip']");
this.countrySelect = this.el.querySelector("select[name='country_id']");
this.stateSelect = this.el.querySelector("select[name='state_id']");
this.keepLast = new KeepLast();
}
/**
* @param {MouseEvent} ev
* @param {HTMLElement} currentTargetEl
*/
async onStreetInput(ev, inputEl) {
const inputContainerEl = inputEl.parentNode;
if (inputEl.value.length >= 5) {
this.keepLast.add(
googlePlacesSession.getAddressPropositions({
partial_address: inputEl.value,
}).then((response) => {
inputContainerEl.querySelector(".dropdown-menu")?.remove();
this.renderAt("website_sale_autocomplete.AutocompleteDropDown", {
results: response.results,
}, inputContainerEl);
})
);
} else {
inputContainerEl.querySelector(".dropdown-menu")?.remove();
}
}
/**
* @param {MouseEvent} ev
* @param {HTMLElement} currentTargetEl
*/
async onClickAutocompleteResult(ev, currentTargetEl) {
const dropdownEl = currentTargetEl.parentNode;
dropdownEl.innerText = "";
dropdownEl.classList.add("d-flex", "justify-content-center", "align-items-center");
const spinnerEl = document.createElement("div");
spinnerEl.classList.add("spinner-border", "text-warning", "text-center", "m-auto");
dropdownEl.appendChild(spinnerEl);
const address = await this.waitFor(googlePlacesSession.getAddressDetails({
address: currentTargetEl.innerText,
google_place_id: currentTargetEl.dataset.googlePlaceId,
}));
if (address.formatted_street_number) {
this.streetAndNumberInput.value = address.formatted_street_number;
}
// Text fields, empty if no value in order to avoid the user missing old data.
this.zipInput.value = address.zip || "";
this.cityInput.value = address.city || "";
// Selects based on odoo ids
if (address.country) {
this.countrySelect.value = address.country[0];
// Let the state select know that the country has changed so that it may fetch the correct states or disappear.
this.countrySelect.dispatchEvent(new Event("change", { bubbles: true }));
}
if (address.state) {
// Waits for the stateSelect to update before setting the state.
new MutationObserver((entries, observer) => {
this.stateSelect.value = address.state[0];
observer.disconnect();
}).observe(this.stateSelect, {
childList: true, // Trigger only if the options change
});
}
dropdownEl.remove();
}
}
registry
.category("public.interactions")
.add("website_sale_autocomplete.address_form", AddressForm);

View file

@ -1,112 +0,0 @@
/** @odoo-module */
import publicWidget from 'web.public.widget';
import { DropPrevious } from 'web.concurrency';
import { debounce } from "@web/core/utils/timing";
import { qweb as QWeb } from 'web.core';
publicWidget.registry.AddressForm = publicWidget.Widget.extend({
selector: '.oe_cart .checkout_autoformat:has(input[name="street"][data-autocomplete-enabled="1"])',
events: {
'input input[name="street"]': '_onChangeStreet',
'click .js_autocomplete_result': '_onClickAutocompleteResult'
},
init: function() {
this.streetAndNumberInput = document.querySelector('input[name="street"]');
this.cityInput = document.querySelector('input[name="city"]');
this.zipInput = document.querySelector('input[name="zip"]');
this.countrySelect = document.querySelector('select[name="country_id"]');
this.stateSelect = document.querySelector('select[name="state_id"]');
this.dp = new DropPrevious();
this.sessionId = this._generateUUID();
this._onChangeStreet = debounce(this._onChangeStreet, 200);
this._super.apply(this, arguments);
},
/**
* Used to generate a unique session ID for the places API.
*
* @private
*/
_generateUUID: function() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
},
_hideAutocomplete: function (inputContainer) {
const dropdown = inputContainer.querySelector('.dropdown-menu');
if (dropdown) {
dropdown.remove();
}
},
_onChangeStreet: async function (ev) {
const inputContainer = ev.currentTarget.parentNode;
if (ev.currentTarget.value.length >= 5) {
this.dp.add(
this._rpc({
route: '/autocomplete/address',
params: {
partial_address: ev.currentTarget.value,
session_id: this.sessionId || null
}
})).then((response) => {
this._hideAutocomplete(inputContainer);
inputContainer.appendChild($(QWeb.render("website_sale_autocomplete.AutocompleteDropDown", {
results: response.results
}))[0]);
if (response.session_id) {
this.sessionId = response.session_id;
}
}
);
} else {
this._hideAutocomplete(inputContainer);
}
},
_onClickAutocompleteResult: async function(ev) {
const dropDown = ev.currentTarget.parentNode;
const spinner = document.createElement('div');
dropDown.innerText = '';
dropDown.classList.add('d-flex', 'justify-content-center', 'align-items-center');
spinner.classList.add('spinner-border', 'text-warning', 'text-center', 'm-auto');
dropDown.appendChild(spinner);
const address = await this._rpc({
route: '/autocomplete/address_full',
params: {
address: ev.currentTarget.innerText,
google_place_id: ev.currentTarget.dataset.googlePlaceId,
session_id: this.sessionId || null
}
});
if (address.formatted_street_number) {
this.streetAndNumberInput.value = address.formatted_street_number;
}
// Text fields, empty if no value in order to avoid the user missing old data.
this.zipInput.value = address.zip || '';
this.cityInput.value = address.city || '';
// Selects based on odoo ids
if (address.country) {
this.countrySelect.value = address.country;
// Let the state select know that the country has changed so that it may fetch the correct states or disappear.
this.countrySelect.dispatchEvent(new Event('change', {bubbles: true}));
}
if (address.state) {
// Waits for the stateSelect to update before setting the state.
new MutationObserver((entries, observer) => {
this.stateSelect.value = address.state;
observer.disconnect();
}).observe(this.stateSelect, {
childList: true, // Trigger only if the options change
});
}
dropDown.remove();
},
});

View file

@ -4,11 +4,11 @@
<t t-name="website_sale_autocomplete.AutocompleteDropDown">
<div t-attf-class="dropdown-menu position-relative #{results.length ? 'show' : ''}">
<a class="dropdown-item js_autocomplete_result"
t-foreach="results" t-as="result"
t-foreach="results" t-as="result" t-key="result_index"
t-att-data-google-place-id="result['google_place_id']">
<t t-out="result['formatted_address']"/>
</a>
<img class="ms-auto pe-1" src="/website_sale_autocomplete/static/src/img/powered_by_google_on_white.png" alt="Powered by Google"/>
<img class="ms-auto pe-1" src="/google_address_autocomplete/static/src/img/powered_by_google_on_white.png" alt="Powered by Google"/>
</div>
</t>
</templates>

View file

@ -1,71 +1,42 @@
/** @odoo-module */
import tour from 'web_tour.tour';
import tourUtils from 'website_sale.tour_utils';
import { registry } from "@web/core/registry";
import * as tourUtils from '@website_sale/js/tours/tour_utils';
function fail (errorMessage) {
tour._consume_tour(tour.running_tour, errorMessage);
}
tour.register('autocomplete_tour', {
test: true,
registry.category("web_tour.tours").add('autocomplete_tour', {
url: '/shop', // /shop/address is redirected if no sales order
}, [{
content: "search test product",
trigger: 'form input[name="search"]',
run: "text A test product",
},{
content: 'Go to the product page',
trigger: '.dropdown-item:contains("A test product")'
}, {
content: 'Add to cart',
trigger: '#add_to_cart'
},
steps: () => [
...tourUtils.addToCart({ productName: "A test product", expectUnloadPage: true }),
tourUtils.goToCart(),
{
content: 'Go to process checkout',
trigger: 'a:contains("Process Checkout")'
}, { // Actual test
tourUtils.goToCheckout(),
{ // Actual test
content: 'Input in Street & Number field',
trigger: 'input[name="street"]',
run: 'text This is a test'
run: "edit This is a test",
}, {
content: 'Check if results have appeared',
trigger: '.js_autocomplete_result',
run: function () {}
}, {
content: 'Input again in street field',
trigger: 'input[name="street"]',
run: 'text add more'
run: "fill add more",
}, {
content: 'Click on the first result',
trigger: '.js_autocomplete_result'
}, {
content: 'Verify the autocomplete box disappeared',
trigger: 'body:not(:has(.js_autocomplete_result))'
}, { // Verify test data has been input
trigger: ".dropdown-menu .js_autocomplete_result:first:contains(result 0)",
run: "click",
},
// TODO: Make this step work in headless mode
// {
// content: "Verify the autocomplete box disappeared",
// trigger: `body:not(:has(.dropdown-menu .js_autocomplete_result))`,
// },
{ // Verify test data has been input
content: 'Check Street & number have been set',
trigger: 'input[name="street"]',
run: function () {
if (this.$anchor.val() !== '42 A fictional Street') {
fail('Street value is not correct : ' + this.$anchor.val())
}
}
trigger: "input[name=street]:value(/^42 A fictional Street$/)",
}, {
content: 'Check City is not empty anymore',
trigger: 'input[name="city"]',
run: function () {
if (this.$anchor.val() !== 'A Fictional City') {
fail('Street value is not correct : ' + this.$anchor.val())
}
}
trigger: 'input[name="city"]:value(/^A Fictional City$/)',
}, {
content: 'Check Zip code is not empty anymore',
trigger: 'input[name="zip"]',
run: function () {
if (this.$anchor.val() !== '12345') {
fail('Street value is not correct : ' + this.$anchor.val())
}
}
}]);
trigger: 'input[name="zip"]:value(/^12345$/)',
}]});