diff --git a/addons/auth_totp_portal/static/src/js/totp_frontend.js b/addons/auth_totp_portal/static/src/js/totp_frontend.js index 3449ac82d6c..7bc1067da8c 100644 --- a/addons/auth_totp_portal/static/src/js/totp_frontend.js +++ b/addons/auth_totp_portal/static/src/js/totp_frontend.js @@ -57,9 +57,7 @@ function fromField(f, record) { clipboard.on('success', function () { clipboard.destroy(); $(copyButton).tooltip('show'); - _.delay(function () { - $(copyButton).tooltip("hide"); - }, 800); + setTimeout(() => $(copyButton).tooltip("hide"), 800); }); clipboard.on('error', function (e) { clipboard.destroy(); diff --git a/addons/board/static/src/board_view.js b/addons/board/static/src/board_view.js index 431978a99b9..5968839b695 100644 --- a/addons/board/static/src/board_view.js +++ b/addons/board/static/src/board_view.js @@ -5,6 +5,7 @@ import { registry } from "@web/core/registry"; import { BoardController } from "./board_controller"; import { XMLParser } from "@web/core/utils/xml"; import { Domain } from "@web/core/domain"; +import { escape } from "@web/core/utils/strings"; export class BoardArchParser extends XMLParser { parse(arch, customViewId) { @@ -36,7 +37,7 @@ export class BoardArchParser extends XMLParser { const isFolded = Boolean( node.hasAttribute("fold") ? parseInt(node.getAttribute("fold"), 10) : 0 ); - let action = { + const action = { id: nextId++, title: node.getAttribute("string"), actionId: parseInt(node.getAttribute("name"), 10), @@ -45,10 +46,12 @@ export class BoardArchParser extends XMLParser { isFolded, }; if (node.hasAttribute("domain")) { - action.domain = new Domain(_.unescape(node.getAttribute("domain"))).toList(); + action.domain = new Domain( + _.unescape(node.getAttribute("domain")) + ).toList(); // so it can be serialized when reexporting board xml // we unescape before re-escaping, to avoid double escaping due to subsequent layout change - action.domain.toString = () => _.escape(_.unescape(node.getAttribute("domain"))); + action.domain.toString = () => escape(node.getAttribute("domain")); } archInfo.columns[currentIndex].actions.push(action); break; diff --git a/addons/im_livechat/static/src/legacy/core_models/device.js b/addons/im_livechat/static/src/legacy/core_models/device.js index 69ae1906e50..f30234f75af 100644 --- a/addons/im_livechat/static/src/legacy/core_models/device.js +++ b/addons/im_livechat/static/src/legacy/core_models/device.js @@ -4,13 +4,14 @@ import { attr, Model } from "@im_livechat/legacy/model"; import { browser } from "@web/core/browser/browser"; import { isMobileOS } from "@web/core/browser/feature_detection"; import { SIZES } from "@web/core/ui/ui_service"; +import { debounce } from "@web/core/utils/timing"; Model({ name: "Device", lifecycleHooks: { _created() { this._refresh(); - this._onResize = _.debounce(() => this._refresh(), 100); + this._onResize = debounce(() => this._refresh(), 100); }, _willDelete() { browser.removeEventListener("resize", this._onResize); diff --git a/addons/im_livechat/static/src/legacy/legacy_models/public_livechat.js b/addons/im_livechat/static/src/legacy/legacy_models/public_livechat.js index 57960c8bc41..86e96b29609 100644 --- a/addons/im_livechat/static/src/legacy/legacy_models/public_livechat.js +++ b/addons/im_livechat/static/src/legacy/legacy_models/public_livechat.js @@ -102,7 +102,7 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { }); } - if (_.isBoolean(params.data.folded)) { + if (typeof params.data.folded === "boolean") { this.messaging.publicLivechatGlobal.publicLivechat.update({ isFolded: params.data.folded, }); @@ -207,7 +207,7 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { * @returns {boolean} */ hasMessages() { - return !_.isEmpty(this.getMessages()); + return Object.keys(this.getMessages() || {}).length > 0; }, /** * Tells if someone other than current user is typing something on this @@ -216,7 +216,7 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { * @returns {boolean} */ isSomeoneTyping() { - return !_.isEmpty(this._typingPartnerIDs); + return Object.keys(this._typingPartnerIDs || {}).length > 0; }, /** * Mark the thread as read, which resets the unread counter to 0. This is @@ -269,7 +269,7 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { timeoutCallbackArguments: [partnerID], timerID: partnerID, }); - if (_.contains(this._typingPartnerIDs, partnerID)) { + if (this._typingPartnerIDs.includes(partnerID)) { return; } this._typingPartnerIDs.push(partnerID); @@ -327,7 +327,7 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { unregisterTyping(params) { const partnerID = params.partnerID; this._othersTypingTimers.unregisterTimer({ timerID: partnerID }); - if (!_.contains(this._typingPartnerIDs, partnerID)) { + if (!this._typingPartnerIDs.includes(partnerID)) { return; } this._typingPartnerIDs = this._typingPartnerIDs.filter((id) => id !== partnerID); diff --git a/addons/im_livechat/static/src/legacy/public_models/chatbot.js b/addons/im_livechat/static/src/legacy/public_models/chatbot.js index a09f05f168f..8537de4b32f 100644 --- a/addons/im_livechat/static/src/legacy/public_models/chatbot.js +++ b/addons/im_livechat/static/src/legacy/public_models/chatbot.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { attr, one, clear, increment, Model } from "@im_livechat/legacy/model"; +import { debounce } from "@web/core/utils/timing"; import { qweb } from "web.core"; import { Markup } from "web.utils"; @@ -410,7 +411,7 @@ Model({ debouncedAwaitUserInput: attr({ compute() { // debounced to let the user type several sentences, see 'Chatbot/awaitUserInput' for details - return _.debounce(this.awaitUserInput, this.awaitUserInputDebounceTime); + return debounce(this.awaitUserInput, this.awaitUserInputDebounceTime); }, }), hasRestartButton: attr({ diff --git a/addons/im_livechat/static/src/legacy/public_models/livechat_button_view.js b/addons/im_livechat/static/src/legacy/public_models/livechat_button_view.js index 7ea79b5a56e..59ba9bc8555 100644 --- a/addons/im_livechat/static/src/legacy/public_models/livechat_button_view.js +++ b/addons/im_livechat/static/src/legacy/public_models/livechat_button_view.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { attr, clear, one, Model } from "@im_livechat/legacy/model"; +import { debounce } from "@web/core/utils/timing"; import { getCookie, deleteCookie } from "web.utils.cookies"; @@ -480,7 +481,7 @@ Model({ }), openChatDebounced: attr({ compute() { - return _.debounce(this._openChat, 200, true); + return debounce(this._openChat, 200, true); }, }), publicLivechatGlobalOwner: one("PublicLivechatGlobal", { diff --git a/addons/im_livechat/static/src/legacy/public_models/public_livechat_global.js b/addons/im_livechat/static/src/legacy/public_models/public_livechat_global.js index b2998e09970..fc726e7ccc2 100644 --- a/addons/im_livechat/static/src/legacy/public_models/public_livechat_global.js +++ b/addons/im_livechat/static/src/legacy/public_models/public_livechat_global.js @@ -17,7 +17,7 @@ Model({ if (pageHistory) { urlHistory = JSON.parse(pageHistory) || []; } - if (!_.contains(urlHistory, page)) { + if (!urlHistory.includes(page)) { urlHistory.push(page); while (urlHistory.length > this.HISTORY_LIMIT) { urlHistory.shift(); diff --git a/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.js b/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.js index 17b542431a5..02ada9f4aa5 100644 --- a/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.js +++ b/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import * as mailUtils from "@mail/js/utils"; +import { debounce } from "@web/core/utils/timing"; import core from "web.core"; import time from "web.time"; @@ -324,7 +325,7 @@ const PublicLivechatView = Widget.extend({ * @param {string} options.model * @param {integer} options.id */ - _redirect: _.debounce( + _redirect: debounce( function (options) { if ("channelID" in options) { this.trigger("redirect_to_channel", options.channelID); diff --git a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js index b86a05dee04..967efb9f3c8 100644 --- a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js +++ b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js @@ -6,6 +6,7 @@ import Widget from "web.Widget"; import { unaccent } from "web.utils"; import { setCookie } from "web.utils.cookies"; +import { debounce } from "@web/core/utils/timing"; /** * This is the widget that represent windows of livechat in the frontend. @@ -35,7 +36,7 @@ const PublicLivechatWindow = Widget.extend({ this._super(parent); this.messaging = messaging; - this._debouncedOnScroll = _.debounce(this._onScroll.bind(this), 100); + this._debouncedOnScroll = debounce(this._onScroll.bind(this), 100); }, /** * @override @@ -175,7 +176,7 @@ const PublicLivechatWindow = Widget.extend({ * Otherwise, fold/unfold the window if set/unset. */ toggleFold(folded) { - if (!_.isBoolean(folded)) { + if (typeof folded !== "boolean") { folded = !this.messaging.publicLivechatGlobal.publicLivechat.isFolded; } this.messaging.publicLivechatGlobal.publicLivechat.update({ isFolded: folded }); @@ -327,7 +328,7 @@ const PublicLivechatWindow = Widget.extend({ ev.stopPropagation(); // to prevent jquery's blockUI to cancel event // ENTER key (avoid requiring jquery ui for external livechat) if (ev.which === 13) { - const content = _.str.trim(this.$input.val()); + const content = String(this.$input.val() || "").trim(); const messageData = { content, attachment_ids: [], diff --git a/addons/l10n_fr_pos_cert/static/src/js/pos.js b/addons/l10n_fr_pos_cert/static/src/js/pos.js index 1edc8148419..f3bc8e575f2 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/pos.js +++ b/addons/l10n_fr_pos_cert/static/src/js/pos.js @@ -17,7 +17,7 @@ patch(PosGlobalState.prototype, "l10n_fr_pos_cert.PosGlobalState", { }); return false; } - return _.contains(french_countries, this.company.country.code); + return french_countries.includes(this.company.country.code); }, disallowLineQuantityChange() { const result = this._super(...arguments); diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index 8e94c665351..fa51a663a9e 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -40,11 +40,13 @@ function parseAndTransform(htmlString, transformFunction) { * @return {string} */ function _parseAndTransform(nodes, transformFunction) { - return _.map(nodes, function (node) { - return transformFunction(node, function () { - return _parseAndTransform(node.childNodes, transformFunction); - }); - }).join(""); + return Array.from($(nodes)) + .map((node) => { + return transformFunction(node, function () { + return _parseAndTransform(node.childNodes, transformFunction); + }); + }) + .join(""); } /** @@ -85,16 +87,19 @@ function linkify(text, attrs) { if (attrs.target === "_blank") { attrs.rel = "noreferrer noopener"; } - attrs = _.map(attrs, function (value, key) { - return key + '="' + _.escape(value) + '"'; - }).join(" "); + attrs = Object.keys(attrs || {}) + .map((key) => { + const value = attrs[key]; + return `${key}="${escape(value)}"`; + }) + .join(" "); let curIndex = 0; let result = ""; let match; while ((match = urlRegexp.exec(text)) !== null) { result += _escapeEntities(text.slice(curIndex, match.index)); const url = match[0]; - const href = !/^https?:\/\//i.test(url) ? "http://" + _.escape(url) : _.escape(url); + const href = !/^https?:\/\//i.test(url) ? "http://" + encodeURI(url) : encodeURI(url); result += "' + _escapeEntities(url) + ""; curIndex = match.index + match[0].length; } @@ -175,11 +180,11 @@ function parseEmail(text) { if (text) { var result = text.match(/"?(.*?)"? <(.*@.*)>/); if (result) { - return [_.str.trim(result[1]), _.str.trim(result[2])]; + return [(result[1] || "").trim(), (result[2] || "").trim()]; } result = text.match(/(.*@.*)/); if (result) { - return [_.str.trim(result[1]), _.str.trim(result[1])]; + return [String(result[1] || "").trim(), String(result[1] || "").trim()]; } return [text, false]; } diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js index 7fdef784444..79984b8380e 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js @@ -115,7 +115,7 @@ patch(MockServer.prototype, "mail/models/mail_activity", { const records = this.getRecords(res_model, domain); const activityTypes = this.getRecords("mail.activity.type", []); - const activityIds = _.pluck(records, "activity_ids").flat(); + const activityIds = records.map((x) => x.activity_ids).flat(); const groupedActivities = {}; const resIdToDeadline = {}; @@ -157,7 +157,7 @@ patch(MockServer.prototype, "mail/models/mail_activity", { count: group.__count, state: state, o_closest_deadline: group.date_deadline, - ids: _.pluck(activites, "id"), + ids: activites.map((x) => x.id), }; }); @@ -177,9 +177,12 @@ patch(MockServer.prototype, "mail/models/mail_activity", { } return [type.id, type.display_name, mailTemplates]; }), - activity_res_ids: _.sortBy(_.pluck(records, "id"), function (id) { - return moment(resIdToDeadline[id]); - }), + activity_res_ids: _.sortBy( + records.map((x) => x.id), + function (id) { + return moment(resIdToDeadline[id]); + } + ), grouped_activities: groupedActivities, }; }, diff --git a/addons/mail/static/tests/mail_utils_tests.js b/addons/mail/static/tests/mail_utils_tests.js index 059abd627ea..1ba95a72ca3 100644 --- a/addons/mail/static/tests/mail_utils_tests.js +++ b/addons/mail/static/tests/mail_utils_tests.js @@ -18,6 +18,7 @@ QUnit.test("add_link utility function", function (assert) { "https://tenor.com/view/chỗgiặt-dog-smile-gif-13860250": true, "http://www.boîtenoire.be": true, }; + for (const [content, willLinkify] of Object.entries(testInputs)) { const output = utils.parseAndTransform(content, utils.addLink); if (willLinkify) { diff --git a/addons/mass_mailing/static/src/js/mailing_portal.js b/addons/mass_mailing/static/src/js/mailing_portal.js index 7e1d612e563..417cc0a2131 100644 --- a/addons/mass_mailing/static/src/js/mailing_portal.js +++ b/addons/mass_mailing/static/src/js/mailing_portal.js @@ -4,7 +4,7 @@ import ajax from "web.ajax"; import core from "web.core"; import "web.dom_ready"; - import { sprintf } from "@web/core/utils/strings"; + import { sprintf, escape } from "@web/core/utils/strings"; var _t = core._t; @@ -48,7 +48,7 @@ if ($('.o_unsubscribe_form').length) { if (unsubscribed_list){ $('#subscription_info').html(sprintf( _t("You have been successfully unsubscribed from %s."), - _.escape(unsubscribed_list) + escape(unsubscribed_list) )); } else{ diff --git a/addons/mass_mailing/static/src/js/mass_mailing_html_field.js b/addons/mass_mailing/static/src/js/mass_mailing_html_field.js index 1bf755c1e55..913a70dc639 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_html_field.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_html_field.js @@ -262,7 +262,7 @@ export class MassMailingHtmlField extends HtmlField { device.isMobile ? $themes.filter(theme => !$(theme).data("hideFromMobile")) : $themes; - this._themeParams = _.map(displayableThemes, (theme) => { + this._themeParams = Array.from(displayableThemes).map((theme) => { const $theme = $(theme); const name = $theme.data("name"); const classname = "o_" + name + "_theme"; diff --git a/addons/payment/static/src/js/checkout_form.js b/addons/payment/static/src/js/checkout_form.js index a078e628b7e..ca2d6b96d5a 100644 --- a/addons/payment/static/src/js/checkout_form.js +++ b/addons/payment/static/src/js/checkout_form.js @@ -3,6 +3,7 @@ import publicWidget from "web.public.widget"; import paymentFormMixin from "payment.payment_form_mixin"; + import { debounce } from "@web/core/utils/timing"; publicWidget.registry.PaymentCheckoutForm = publicWidget.Widget.extend(paymentFormMixin, { selector: 'form[name="o_payment_checkout"]', @@ -19,7 +20,7 @@ */ init: function () { const preventDoubleClick = handlerMethod => { - return _.debounce(handlerMethod, 500, true); + return debounce(handlerMethod, 500, true); }; this._super(...arguments); // Prevent double-clicks and browser glitches on all inputs diff --git a/addons/payment/static/src/js/manage_form.js b/addons/payment/static/src/js/manage_form.js index 0eb04900d6d..c0a6f56b93c 100644 --- a/addons/payment/static/src/js/manage_form.js +++ b/addons/payment/static/src/js/manage_form.js @@ -5,6 +5,7 @@ import Dialog from "web.Dialog"; import paymentFormMixin from "payment.payment_form_mixin"; +import { debounce } from "@web/core/utils/timing"; const _t = core._t; @@ -24,7 +25,7 @@ */ init: function () { const preventDoubleClick = handlerMethod => { - return _.debounce(handlerMethod, 500, true); + return debounce(handlerMethod, 500, true); }; this._super(...arguments); // Prevent double-clicks and browser glitches on all inputs diff --git a/addons/payment/static/src/js/payment_form_mixin.js b/addons/payment/static/src/js/payment_form_mixin.js index 8bafa44180d..d2ff5f1c66e 100644 --- a/addons/payment/static/src/js/payment_form_mixin.js +++ b/addons/payment/static/src/js/payment_form_mixin.js @@ -5,6 +5,26 @@ const _t = core._t; + /** + * Replace HTML character with ASCII character + * + * @param {string} str string + * @returns {string} ASCII string + */ + function escapeHTML(str) { + const htmlCaracters = { + "&": "&", + "<": "<", + ">": ">", + "'": "'", + '"': """, + }; + for (const [key, value] of Object.entries(htmlCaracters)) { + str = String(str).replace(new RegExp(key, "g"), value); + } + return str; + } + export default { /** @@ -76,7 +96,7 @@ return new Dialog(null, { title: _.str.sprintf(_t("Error: %s"), title), size: 'medium', - $content: `

${_.str.escapeHTML(description) || ''}

`, + $content: `

${escapeHTML(description) || ''}

`, buttons: [{text: _t("Ok"), close: true}] }).open(); } else { // Show error in inline form @@ -84,12 +104,12 @@ // Build the html for the error let errorHtml = `
- ${_.str.escapeHTML(title)}`; + ${escapeHTML(title)}`; if (description !== '') { - errorHtml += `
${_.str.escapeHTML(description)}`; + errorHtml += `
${escapeHTML(description)}`; } if (error !== '') { - errorHtml += `
${_.str.escapeHTML(error)}`; + errorHtml += `
${escapeHTML(error)}`; } errorHtml += '
'; diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index ef50ddd0468..64d3a32bfff 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -230,7 +230,7 @@ export const PosDB = core.Class.extend({ }, add_packagings: function (product_packagings) { var self = this; - _.map(product_packagings, function (product_packaging) { + Object.values(product_packagings || {}).map((product_packaging) => { const products = Object.values(self.product_by_id); if (products.find(({ id }) => id === product_packaging.product_id[0])) { self.product_packaging_by_barcode[product_packaging.barcode] = product_packaging; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 1bd84a9026d..b2a4a6e70a9 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1011,7 +1011,7 @@ export class PosGlobalState extends PosModel { // Keep the order ids that are about to be sent to the // backend. In between create_from_ui and the success callback // new orders may have been added to it. - var order_ids_to_sync = _.pluck(orders, "id"); + var order_ids_to_sync = orders.map((o) => o.id); for (const order of orders) { order.to_invoice = options.to_invoice || false; @@ -1590,10 +1590,9 @@ export class Product extends PosModel { let draftPackLotLines, weight, description, packLotLinesToEdit; if (this.attribute_line_ids.some((id) => id in this.pos.attributes_by_ptal_id)) { - const attributes = _.map( - this.attribute_line_ids, - (id) => this.pos.attributes_by_ptal_id[id] - ).filter((attr) => attr !== undefined); + const attributes = this.attribute_line_ids + .map((id) => this.pos.attributes_by_ptal_id[id]) + .filter((attr) => attr !== undefined); const { confirmed, payload } = await this.pos.env.services.popup.add( ProductConfiguratorPopup, { @@ -2172,15 +2171,7 @@ export class Orderline extends PosModel { price_subtotal_incl: this.get_price_with_tax(), discount: this.get_discount(), product_id: this.get_product().id, - tax_ids: [ - [ - 6, - false, - _.map(this.get_applicable_taxes(), function (tax) { - return tax.id; - }), - ], - ], + tax_ids: [[6, false, this.get_applicable_taxes().map((tax) => tax.id)]], id: this.id, pack_lot_ids: pack_lot_ids, description: this.description, @@ -2441,7 +2432,7 @@ export class Orderline extends PosModel { if (line_taxes.length && line_taxes[0].price_include) { new_included_taxes = new_included_taxes.concat(line_taxes); } - if (tax.price_include && !_.contains(line_taxes, tax)) { + if (tax.price_include && !line_taxes.includes(tax)) { mapped_included_taxes.push(tax); } }); @@ -2521,7 +2512,7 @@ export class Packlotline extends PosModel { } set_lot_name(name) { - this.lot_name = _.str.trim(name) || null; + this.lot_name = String(name || "").trim() || null; } get_lot_name() { @@ -2836,11 +2827,9 @@ export class Order extends PosModel { return orderLines.push([0, 0, item.export_as_JSON()]); }); paymentLines = []; - this.paymentlines.forEach( - _.bind(function (item) { - return paymentLines.push([0, 0, item.export_as_JSON()]); - }, this) - ); + this.paymentlines.forEach((item) => { + return paymentLines.push([0, 0, item.export_as_JSON()]); + }); var json = { name: this.get_name(), amount_paid: this.get_total_paid() - this.get_change(), diff --git a/addons/portal/static/src/js/portal_composer.js b/addons/portal/static/src/js/portal_composer.js index 70276392bb5..de02036b661 100644 --- a/addons/portal/static/src/js/portal_composer.js +++ b/addons/portal/static/src/js/portal_composer.js @@ -147,8 +147,8 @@ var PortalComposer = publicWidget.Widget.extend({ _prepareMessageData: function () { return Object.assign(this.options || {}, { 'message': this.$('textarea[name="message"]').val(), - 'attachment_ids': _.pluck(this.attachments, 'id'), - 'attachment_tokens': _.pluck(this.attachments, 'access_token'), + attachment_ids: this.attachments.map((a) => a.id), + attachment_tokens: this.attachments.map((a) => a.access_token), }); }, /** diff --git a/addons/portal_rating/static/src/js/portal_chatter.js b/addons/portal_rating/static/src/js/portal_chatter.js index 801d0b0b1fa..326ff99f6d9 100644 --- a/addons/portal_rating/static/src/js/portal_chatter.js +++ b/addons/portal_rating/static/src/js/portal_chatter.js @@ -32,7 +32,7 @@ PortalChatter.include({ init: function (parent, options) { this._super.apply(this, arguments); // options - if (!_.contains(this.options, 'display_rating')) { + if (!Object.keys(this.options).includes("display_rating")) { this.options = _.defaults(this.options, { 'display_rating': false, 'rating_default_value': 0.0, diff --git a/addons/pos_loyalty/static/src/js/Loyalty.js b/addons/pos_loyalty/static/src/js/Loyalty.js index d0f854aefda..39543497e9d 100644 --- a/addons/pos_loyalty/static/src/js/Loyalty.js +++ b/addons/pos_loyalty/static/src/js/Loyalty.js @@ -377,7 +377,7 @@ patch(Order.prototype, "pos_loyalty.Order", { }, wait_for_push_order() { return ( - !_.isEmpty(this.couponPointChanges) || + Object.keys(this.couponPointChanges || {}).length > 0 || this._has_gift_card_product() || this._get_reward_lines().length || this._super(...arguments) @@ -931,12 +931,11 @@ patch(Order.prototype, "pos_loyalty.Order", { continue; } let price_to_use = line.get_price_with_tax(); - if (program.program_type === 'gift_card') { + if (program.program_type === "gift_card") { price_to_use = line.price; } const pointsPerUnit = round_precision( - (rule.reward_point_amount * price_to_use) / - line.get_quantity(), + (rule.reward_point_amount * price_to_use) / line.get_quantity(), 0.01 ); if (pointsPerUnit > 0) { diff --git a/addons/pos_loyalty/static/src/js/PaymentScreen.js b/addons/pos_loyalty/static/src/js/PaymentScreen.js index 4bd3fa5590c..f6b5b671c08 100644 --- a/addons/pos_loyalty/static/src/js/PaymentScreen.js +++ b/addons/pos_loyalty/static/src/js/PaymentScreen.js @@ -33,7 +33,7 @@ patch(PaymentScreen.prototype, "pos_loyalty.PaymentScreen", { return; } // No need to do an rpc if no existing coupon is being used. - if (!_.isEmpty(pointChanges) || newCodes.length) { + if (Object.keys(pointChanges || {}).length > 0 || newCodes.length) { try { const { successful, payload } = await this.orm.call( "pos.order", @@ -121,7 +121,7 @@ patch(PaymentScreen.prototype, "pos_loyalty.PaymentScreen", { return true; }) ); - if (!_.isEmpty(couponData)) { + if (Object.keys(couponData || []).length > 0) { const payload = await this.orm.call("pos.order", "confirm_coupon_programs", [ server_ids, couponData, diff --git a/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js b/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js index 99dbf4d8e72..c6930d426f2 100644 --- a/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js +++ b/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js @@ -15,6 +15,7 @@ import { Table } from "./table"; import { usePos } from "@point_of_sale/app/pos_hook"; import { useService } from "@web/core/utils/hooks"; import { Component, onPatched, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl"; +import { sprintf } from "@web/core/utils/strings"; export class FloorScreen extends Component { static components = { EditableTable, EditBar, Table }; @@ -98,7 +99,7 @@ export class FloorScreen extends Component { _onDeselectTable() { this.state.selectedTableIds = []; } - async _createTableHelper(copyTable, duplicateFloor=false) { + async _createTableHelper(copyTable, duplicateFloor = false) { const existingTable = this.activeFloor.tables; let newTable; if (copyTable) { @@ -226,9 +227,13 @@ export class FloorScreen extends Component { this.activeFloor.table_ids.push(newTable.id); return newTable; } - _getNewTableName() { + _getNewTableName() { let firstNum = 1; - const tablesNameNumber = this.activeTables.map(table => +table.name).sort(function(a, b){return a-b}); + const tablesNameNumber = this.activeTables + .map((table) => +table.name) + .sort(function (a, b) { + return a - b; + }); for (let i = 0; i < tablesNameNumber.length; i++) { if (tablesNameNumber[i] == firstNum) { @@ -262,7 +267,9 @@ export class FloorScreen extends Component { //This will lead in receiving information on orders from a table that we do not have. //If we do not have a table in the PoS but still receive information about it, we reload the tables //from the server. - const result = await this.orm.call("pos.session", "get_pos_ui_restaurant_floor", [[odoo.pos_session_id]]); + const result = await this.orm.call("pos.session", "get_pos_ui_restaurant_floor", [ + [odoo.pos_session_id], + ]); if (this.env.pos.config.is_table_management) { this.env.pos.floors = result; this.env.pos.loadRestaurantFloor(); @@ -279,8 +286,10 @@ export class FloorScreen extends Component { table_obj.order_count = table.orders + unsynced_orders; } } - get activeFloor() { - return this.state.selectedFloorId ? this.env.pos.floors_by_id[this.state.selectedFloorId] : null; + get activeFloor() { + return this.state.selectedFloorId + ? this.env.pos.floors_by_id[this.state.selectedFloorId] + : null; } get activeTables() { return this.activeFloor ? this.activeFloor.tables : null; @@ -368,7 +377,11 @@ export class FloorScreen extends Component { if (!confirmed) { return; } - const floor = await this.orm.call("restaurant.floor", "create_from_ui", [newName, "#ACADAD",this.env.pos.config.id]); + const floor = await this.orm.call("restaurant.floor", "create_from_ui", [ + newName, + "#ACADAD", + this.env.pos.config.id, + ]); this.env.pos.floors_by_id[floor.id] = floor; this.env.pos.floors.push(floor); this.selectFloor(floor); @@ -386,10 +399,14 @@ export class FloorScreen extends Component { const floor = this.activeFloor; const tables = this.activeFloor.tables; const newFloorName = floor.name + " (copy)"; - const newFloor = await this.orm.call("restaurant.floor", "create_from_ui", [newFloorName, floor.background_color, this.env.pos.config.id]); + const newFloor = await this.orm.call("restaurant.floor", "create_from_ui", [ + newFloorName, + floor.background_color, + this.env.pos.config.id, + ]); this.env.pos.floors_by_id[newFloor.id] = newFloor; this.env.pos.floors.push(newFloor); - this.selectFloor(newFloor) + this.selectFloor(newFloor); for (const table of tables) { await this._createTableHelper(table, true); } @@ -458,8 +475,7 @@ export class FloorScreen extends Component { selectedTable.seats = newSeatsNum; await this._save(selectedTable); } - }) - + }); } async changeToCircle() { await this.changeShape("round"); @@ -474,7 +490,7 @@ export class FloorScreen extends Component { this.selectedTables.forEach(async (selectedTable) => { selectedTable.shape = form; await this._save(selectedTable); - }) + }); this.render(); } async setTableColor(color) { @@ -482,7 +498,7 @@ export class FloorScreen extends Component { selectedTables.forEach(async (selectedTable) => { selectedTable.color = color; await this._save(selectedTable); - }) + }); this.render(); this.state.isColorPicker = false; } @@ -500,8 +516,8 @@ export class FloorScreen extends Component { async deleteFloorOrTable() { if (this.selectedTables.length == 0) { const { confirmed } = await this.popup.add(ConfirmPopup, { - title: _.str.sprintf("Removing floor %s", this.activeFloor.name), - body: _.str.sprintf( + title: `Removing floor ${this.activeFloor.name}`, + body: sprintf( this.env._t("Removing a floor cannot be undone. Do you still wanna remove %s?"), this.activeFloor.name ), @@ -511,7 +527,8 @@ export class FloorScreen extends Component { } const originalSelectedFloorId = this.activeFloor.id; await this.orm.call("restaurant.floor", "deactivate_floor", [ - originalSelectedFloorId, this.env.pos.pos_session.id + originalSelectedFloorId, + this.env.pos.pos_session.id, ]); const floor = this.env.pos.floors_by_id[originalSelectedFloorId]; const orderList = [...this.env.pos.get_order_list()]; @@ -524,7 +541,9 @@ export class FloorScreen extends Component { delete this.env.pos.tables_by_id[tableId]; }); delete this.env.pos.floors_by_id[originalSelectedFloorId]; - this.env.pos.floors = this.env.pos.floors.filter((floor) => floor.id != originalSelectedFloorId); + this.env.pos.floors = this.env.pos.floors.filter( + (floor) => floor.id != originalSelectedFloorId + ); this.env.pos.TICKET_SCREEN_STATE.syncedOrders.cache = {}; if (this.env.pos.floors.length > 0) { this.selectFloor(this.env.pos.floors[0]); @@ -558,17 +577,17 @@ export class FloorScreen extends Component { await this.orm.call("restaurant.table", "create_from_ui", [ { active: false, id: id }, ]); - this.activeFloor.tables = this.activeTables.filter( - (table) => table.id !== id - ); + this.activeFloor.tables = this.activeTables.filter((table) => table.id !== id); delete this.env.pos.tables_by_id[id]; } else { await this.popup.add(ErrorPopup, { title: this.env._t("Delete Error"), - body: this.env._t("You cannot delete a table with orders still in draft for this table."), + body: this.env._t( + "You cannot delete a table with orders still in draft for this table." + ), }); } - }) + }); // Value of an object can change inside async function call. // Which means that in this code block, the value of `state.selectedTableId` // before the await call can be different after the finishing the await call. @@ -577,7 +596,7 @@ export class FloorScreen extends Component { // else is selected during the rpc call. const equalsCheck = (a, b) => { return JSON.stringify(a) === JSON.stringify(b); - } + }; if (equalsCheck(this.state.selectedTableIds, originalSelectedTableIds)) { this.state.selectedTableIds = []; } diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js index 58dcc8401b3..750e6bd63ba 100644 --- a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js @@ -5,7 +5,7 @@ import { Order } from "@point_of_sale/js/models"; import { SplitOrderline } from "./SplitOrderline"; import { registry } from "@web/core/registry"; import { usePos } from "@point_of_sale/app/pos_hook"; -import { Component, useState, onMounted } from "@odoo/owl"; +import { Component, useState, onMounted } from "@odoo/owl"; export class SplitBillScreen extends Component { static template = "SplitBillScreen"; @@ -46,7 +46,7 @@ export class SplitBillScreen extends Component { this.pos.showScreen("ProductScreen"); } proceed() { - if (_.isEmpty(this.splitlines)) { + if (Object.keys(this.splitlines || {})?.length === 0) { // Splitlines is empty return; } diff --git a/addons/purchase/static/src/js/purchase_portal_sidebar.js b/addons/purchase/static/src/js/purchase_portal_sidebar.js index 03f15ab5eeb..a5f1033367e 100644 --- a/addons/purchase/static/src/js/purchase_portal_sidebar.js +++ b/addons/purchase/static/src/js/purchase_portal_sidebar.js @@ -118,22 +118,19 @@ publicWidget.registry.PurchasePortalSidebar = PortalSidebar.extend({ _extractText: function ($node) { var self = this; var rawText = []; - $node - .contents() - .toArray() - .forEach((el) => { - var current = $(el); - if ($.trim(current.text())) { - var tagName = current.prop("tagName"); - if ( - _.isUndefined(tagName) || - (!_.isUndefined(tagName) && - _.contains(self.authorizedTextTag, tagName.toLowerCase())) - ) { - rawText.push($.trim(current.text())); - } + Array.from($node.contents()).forEach((el) => { + var current = $(el); + if ($.trim(current.text())) { + var tagName = current.prop("tagName"); + if ( + typeof tagName === "undefined" || + (typeof tagName !== "undefined" && + self.authorizedTextTag.includes(tagName.toLowerCase())) + ) { + rawText.push($.trim(current.text())); } - }); + } + }); return rawText.join(" "); }, }); diff --git a/addons/sale/static/src/js/sale_portal_sidebar.js b/addons/sale/static/src/js/sale_portal_sidebar.js index 8a55538f71b..7b5872da523 100644 --- a/addons/sale/static/src/js/sale_portal_sidebar.js +++ b/addons/sale/static/src/js/sale_portal_sidebar.js @@ -105,7 +105,11 @@ publicWidget.registry.SalePortalSidebar = PortalSidebar.extend({ var current = $(el); if ($.trim(current.text())) { var tagName = current.prop("tagName"); - if (_.isUndefined(tagName) || (!_.isUndefined(tagName) && _.contains(self.authorizedTextTag, tagName.toLowerCase()))) { + if ( + typeof tagName === "undefined" || + (typeof tagName !== "undefined" && + self.authorizedTextTag.includes(tagName.toLowerCase())) + ) { rawText.push($.trim(current.text())); } } diff --git a/addons/sale/static/src/js/variant_mixin.js b/addons/sale/static/src/js/variant_mixin.js index 8e481e7bee5..f3097e548da 100644 --- a/addons/sale/static/src/js/variant_mixin.js +++ b/addons/sale/static/src/js/variant_mixin.js @@ -5,6 +5,8 @@ import core from "web.core"; import utils from "web.utils"; import ajax from "web.ajax"; import { sprintf } from "@web/core/utils/strings"; +import { memoize } from "@web/core/utils/functions"; + var _t = core._t; var VariantMixin = { @@ -39,7 +41,7 @@ var VariantMixin = { if (!$parent.data('uniqueId')) { $parent.data('uniqueId', _.uniqueId()); } - this._throttledGetCombinationInfo($parent.data('uniqueId'))(ev); + this._throttledGetCombinationInfo(this, $parent.data('uniqueId'))(ev); }, /** * @see onChangeVariant @@ -523,7 +525,7 @@ var VariantMixin = { $default_price.text(self._priceToStr(combination.list_price)); var isCombinationPossible = true; - if (!_.isUndefined(combination.is_combination_possible)) { + if (typeof combination.is_combination_possible !== "undefined") { isCombinationPossible = combination.is_combination_possible; } this._toggleDisable($parent, isCombinationPossible); @@ -628,12 +630,10 @@ var VariantMixin = { * @param {string} uniqueId * @returns {function} */ - _throttledGetCombinationInfo: _.memoize(function (uniqueId) { + _throttledGetCombinationInfo: memoize(function (self, uniqueId) { var dropMisordered = new concurrency.DropMisordered(); - var _getCombinationInfo = _.throttle(this._getCombinationInfo.bind(this), 500); - return function (ev, params) { - return dropMisordered.add(_getCombinationInfo(ev, params)); - }; + var _getCombinationInfo = _.throttle(self._getCombinationInfo.bind(self), 500); + return (ev, params) => dropMisordered.add(_getCombinationInfo(ev, params)); }), /** * Toggles the disabled class depending on the $parent element diff --git a/addons/sale_product_configurator/static/src/js/sale_product_field.js b/addons/sale_product_configurator/static/src/js/sale_product_field.js index 1408efc59ae..4e96d2b1dd5 100644 --- a/addons/sale_product_configurator/static/src/js/sale_product_field.js +++ b/addons/sale_product_configurator/static/src/js/sale_product_field.js @@ -210,7 +210,7 @@ patch(SaleOrderLineProductField.prototype, 'sale_product_configurator', { var noVariantAttributeValues = mainProduct.no_variant_attribute_values; var noVariantCommands = [{ operation: "DELETE_ALL" }]; if (noVariantAttributeValues && noVariantAttributeValues.length !== 0) { - var resIds = _.map(noVariantAttributeValues, function (noVariantValue) { + var resIds = noVariantAttributeValues.map((noVariantValue) => { return { id: parseInt(noVariantValue.value) }; }); diff --git a/addons/survey/static/src/js/survey_session_manage.js b/addons/survey/static/src/js/survey_session_manage.js index e248c525667..3ff31e8a6b2 100644 --- a/addons/survey/static/src/js/survey_session_manage.js +++ b/addons/survey/static/src/js/survey_session_manage.js @@ -116,9 +116,7 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend(SurveyPre clipboard.on('success', function () { clipboard.destroy(); $clipboardBtn.popover('show'); - _.delay(function () { - $clipboardBtn.popover('hide'); - }, 800); + setTimeout(() => $clipboardBtn.popover('hide'), 800); }); clipboard.on('error', function (e) { diff --git a/addons/web/static/src/core/utils/numbers.js b/addons/web/static/src/core/utils/numbers.js index 54b0405492a..8fd0ac71e82 100644 --- a/addons/web/static/src/core/utils/numbers.js +++ b/addons/web/static/src/core/utils/numbers.js @@ -16,15 +16,16 @@ export function clamp(num, min, max) { * A function to create flexibly-numbered lists of integers, handy for each and map loops. * step defaults to 1. * Returns a list of integers from start (inclusive) to stop (exclusive), incremented (or decremented) by step. - * @param {number} min default 0 - * @param {number} max + * @param {number} start default 0 + * @param {number} stop * @param {number} step default 1 * @returns {number[]} */ export function range(start, stop, step = 1) { const array = []; - for (let i = start; i < stop; i += step) { - array.push(i); + const nsteps = Math.floor((stop - start) / step); + for (let i = 0; i < nsteps; i++) { + array.push(start + step * i); } return array; } diff --git a/addons/web/static/src/legacy/js/core/context.js b/addons/web/static/src/legacy/js/core/context.js index 03c2d024d99..e9c59f60813 100644 --- a/addons/web/static/src/legacy/js/core/context.js +++ b/addons/web/static/src/legacy/js/core/context.js @@ -9,7 +9,7 @@ var Context = Class.extend({ this.__contexts = []; this.__eval_context = null; var self = this; - Object.values(arguments).forEach((x) => self.add(x)); + Object.values(arguments || {}).forEach((x) => self.add(x)); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/legacy/js/core/dialog.js b/addons/web/static/src/legacy/js/core/dialog.js index ec803e51a25..3ba2ceefca1 100644 --- a/addons/web/static/src/legacy/js/core/dialog.js +++ b/addons/web/static/src/legacy/js/core/dialog.js @@ -137,7 +137,7 @@ var Dialog = Widget.extend({ self.$footer = self.$modal.find(".modal-footer"); self.set_buttons(self.buttons); } - self.$modal.on('hidden.bs.modal', _.bind(self.destroy, self)); + self.$modal.on('hidden.bs.modal', self.destroy.bind(self)); }); }, /** @@ -312,7 +312,7 @@ var Dialog = Widget.extend({ _setButtonsTo($target, buttons) { var self = this; $target.empty(); - buttons.forEach(buttonData => { + buttons.forEach((buttonData) => { var $button = dom.renderButton({ attrs: { class: buttonData.classes || (buttons.length > 1 ? 'btn-secondary' : 'btn-primary'), diff --git a/addons/web/static/src/legacy/js/core/domain.js b/addons/web/static/src/legacy/js/core/domain.js index 504144fb2cd..66daf5e9807 100644 --- a/addons/web/static/src/legacy/js/core/domain.js +++ b/addons/web/static/src/legacy/js/core/domain.js @@ -35,7 +35,7 @@ var Domain = collections.Tree.extend({ */ init: function (domain, evalContext) { this._super.apply(this, arguments); - if (Array.isArray(domain) || _.isString(domain)) { + if (Array.isArray(domain) || typeof domain === "string") { this._parse(this.normalizeArray(_.clone(this.stringToArray(domain, evalContext)))); } else { this._data = !!domain; @@ -87,10 +87,10 @@ var Domain = collections.Tree.extend({ switch (this._data[1]) { case "=": case "==": - return _.isEqual(fieldValue, this._data[2]); + return JSON.stringify(fieldValue) === JSON.stringify(this._data[2]); case "!=": case "<>": - return !_.isEqual(fieldValue, this._data[2]); + return JSON.stringify(fieldValue) !== JSON.stringify(this._data[2]); case "<": return (fieldValue < this._data[2]); case ">": @@ -156,9 +156,10 @@ var Domain = collections.Tree.extend({ return []; } else { var arr = [this._data]; - return arr.concat.apply(arr, _.map(this._children, function (child) { - return child.toArray(); - })); + return arr.concat.apply( + arr, + this._children.map((child) => child.toArray()) + ); } }, /** @@ -239,7 +240,7 @@ var Domain = collections.Tree.extend({ * @returns {string} */ arrayToString: function (domain) { - if (_.isString(domain)) return domain; + if (typeof domain === "string") return domain; function jsToPy(p) { switch (p) { @@ -267,7 +268,7 @@ var Domain = collections.Tree.extend({ * @returns {Array} */ stringToArray: function (domain, evalContext) { - if (!_.isString(domain)) return _.clone(domain); + if (typeof domain !== "string") return _.clone(domain); return pyUtils.eval("domain", domain ? domain.replace(/%%/g, '%') : "[]", evalContext); }, /** diff --git a/addons/web/static/src/legacy/js/core/misc.js b/addons/web/static/src/legacy/js/core/misc.js index 95b6a71373b..4d24a24ec81 100644 --- a/addons/web/static/src/legacy/js/core/misc.js +++ b/addons/web/static/src/legacy/js/core/misc.js @@ -86,7 +86,9 @@ function blockUI() { } function unblockUI() { - _.invoke(throbbers, 'destroy'); + throbbers.forEach((throbber) => { + throbber.destroy(); + }); throbbers = []; $(document.body).removeClass('o_ui_blocked'); unblockAccessKeys(); diff --git a/addons/web/static/src/legacy/js/core/mixins.js b/addons/web/static/src/legacy/js/core/mixins.js index ad0302e6c32..bbb4d367ca4 100644 --- a/addons/web/static/src/legacy/js/core/mixins.js +++ b/addons/web/static/src/legacy/js/core/mixins.js @@ -277,7 +277,7 @@ var EventDispatcherMixin = Object.assign({}, ParentedMixin, { }; }, _delegateCustomEvents: function () { - if (_.isEmpty(this.custom_events)) { return; } + if (Object.keys(this.custom_events || {}).length === 0) { return; } for (var key in this.custom_events) { if (!this.custom_events.hasOwnProperty(key)) { continue; } diff --git a/addons/web/static/src/legacy/js/core/popover.js b/addons/web/static/src/legacy/js/core/popover.js index 92ea2039c44..a0df16ee345 100644 --- a/addons/web/static/src/legacy/js/core/popover.js +++ b/addons/web/static/src/legacy/js/core/popover.js @@ -1,5 +1,6 @@ /** @odoo-module alias=web.Popover **/ + import { debounce } from "@web/core/utils/timing"; import { LegacyComponent } from "@web/legacy/legacy_component"; const { status, onWillUnmount, useEffect, useRef, useState } = owl; @@ -29,7 +30,7 @@ this._onResizeWindow = this._onResizeWindow.bind(this); this._onScrollDocument = _.throttle(this._onScrollDocument, 50); - this._onResizeWindow = _.debounce(this._onResizeWindow, 250); + this._onResizeWindow = debounce(this._onResizeWindow, 250); /** * Those events are only necessary if the popover is currently open, diff --git a/addons/web/static/src/legacy/js/core/py_utils.js b/addons/web/static/src/legacy/js/core/py_utils.js index b84a953e492..ca61fd7ed35 100644 --- a/addons/web/static/src/legacy/js/core/py_utils.js +++ b/addons/web/static/src/legacy/js/core/py_utils.js @@ -109,8 +109,8 @@ function eval_contexts(contexts, evaluation_context) { return _(contexts).reduce(function (result_context, ctx) { // __eval_context evaluations can lead to some of `contexts`'s // values being null, skip them as well as empty contexts - if (_.isEmpty(ctx)) { return result_context; } - if (_.isString(ctx)) { + if (Object.keys(ctx || {}).length === 0) { return result_context; } + if (typeof ctx === "string") { // wrap raw strings in context ctx = { __ref: 'context', __debug: ctx }; } @@ -144,7 +144,7 @@ function eval_domains(domains, evaluation_context) { (domains[0][0] === "|" || domains[0][0] === "!") ); _(domains).each(function (domain) { - if (_.isString(domain)) { + if (typeof domain === "string") { // wrap raw strings in domain domain = { __ref: 'domain', __debug: domain }; } @@ -190,7 +190,7 @@ function eval_groupbys(contexts, evaluation_context) { evaluation_context = Object.assign(pycontext(), evaluation_context || {}); var result_group = []; _(contexts).each(function (ctx) { - if (_.isString(ctx)) { + if (typeof ctx === "string") { // wrap raw strings in context ctx = { __ref: 'context', __debug: ctx }; } diff --git a/addons/web/static/src/legacy/js/core/rpc.js b/addons/web/static/src/legacy/js/core/rpc.js index 68fe98369ab..a36d325a2a3 100644 --- a/addons/web/static/src/legacy/js/core/rpc.js +++ b/addons/web/static/src/legacy/js/core/rpc.js @@ -116,9 +116,9 @@ const rpc = { * @returns {string} */ _serializeSort: function (orderBy) { - return _.map(orderBy, function (order) { - return order.name + (order.asc !== false ? ' ASC' : ' DESC'); - }).join(', '); + return orderBy + .map((order) => order.name + (order.asc !== false ? " ASC" : " DESC")) + .join(", "); }, }; diff --git a/addons/web/static/src/legacy/js/core/session.js b/addons/web/static/src/legacy/js/core/session.js index 85f13a8de92..c7cf10291b2 100644 --- a/addons/web/static/src/legacy/js/core/session.js +++ b/addons/web/static/src/legacy/js/core/session.js @@ -47,7 +47,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { // must be able to customize server var window_origin = location.protocol + "//" + location.host; origin = origin ? origin.replace( /\/+$/, '') : window_origin; - if (!_.isUndefined(this.origin) && this.origin !== origin) + if (typeof this.origin !== "undefined" && this.origin !== origin) throw new Error('Session already bound to ' + this.origin); else this.origin = origin; @@ -256,7 +256,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { } // TODO: remove - if (! _.isString(url)) { + if (typeof url !== "string") { Object.assign(options, url); url = url.url; } diff --git a/addons/web/static/src/legacy/js/core/utils.js b/addons/web/static/src/legacy/js/core/utils.js index 5ca513bf99c..0759eb8d07e 100644 --- a/addons/web/static/src/legacy/js/core/utils.js +++ b/addons/web/static/src/legacy/js/core/utils.js @@ -949,7 +949,7 @@ const utils = { */ toBoolElse: function (str, elseValues, trueValues, falseValues) { var ret = _.str.toBool(str, trueValues, falseValues); - if (_.isUndefined(ret)) { + if (typeof ret === "undefined") { return elseValues; } return ret; diff --git a/addons/web/static/src/legacy/js/core/widget.js b/addons/web/static/src/legacy/js/core/widget.js index d0249a34c54..8306349450b 100644 --- a/addons/web/static/src/legacy/js/core/widget.js +++ b/addons/web/static/src/legacy/js/core/widget.js @@ -218,7 +218,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * @param {boolean} [display] use true to show the widget or false to hide it */ do_toggle: function (display) { - if (_.isBoolean(display)) { + if (typeof display === "boolean") { display ? this.do_show() : this.do_hide(); } else if (this.$el) { this.$el.hasClass('o_hidden') ? this.do_show() : this.do_hide(); @@ -283,9 +283,9 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { * @returns {Promise} */ replace: function (target) { - return this._widgetRenderAndInsert(_.bind(function (t) { + return this._widgetRenderAndInsert((t) => { this.$el.replaceAll(t); - }, this), target); + }, target); }, /** * Re-sets the widget's root element (el/$el/$el). @@ -337,7 +337,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { */ _delegateEvents: function () { var events = this.events; - if (_.isEmpty(events)) { return; } + if (Object.keys(events || {}).length === 0) { return; } for(var key in events) { if (!events.hasOwnProperty(key)) { continue; } @@ -372,7 +372,7 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, { attrs['class'] = this.className; } var $el = $(document.createElement(this.tagName)); - if (!_.isEmpty(attrs)) { + if (Object.keys(attrs || {}).length > 0) { $el.attr(attrs); } return $el; diff --git a/addons/web/static/src/legacy/js/fields/basic_fields.js b/addons/web/static/src/legacy/js/fields/basic_fields.js index b34f63be877..a4ff266bb8d 100644 --- a/addons/web/static/src/legacy/js/fields/basic_fields.js +++ b/addons/web/static/src/legacy/js/fields/basic_fields.js @@ -16,6 +16,7 @@ import session from "web.session"; import field_utils from "web.field_utils"; import utils from "web.utils"; import { sprintf } from "@web/core/utils/strings"; +import { debounce } from "@web/core/utils/timing"; var _t = core._t; var _lt = core._lt; @@ -238,7 +239,7 @@ var DebouncedField = AbstractField.extend({ this._isDirty = false; if (this.mode === 'edit') { if (this.DEBOUNCE) { - this._doDebouncedAction = _.debounce(this._doAction, this.DEBOUNCE); + this._doDebouncedAction = debounce(this._doAction, this.DEBOUNCE); } else { this._doDebouncedAction = this._doAction; } diff --git a/addons/web/static/src/legacy/js/fields/field_utils.js b/addons/web/static/src/legacy/js/fields/field_utils.js index 0c6ef19bb93..32ce9b2f131 100644 --- a/addons/web/static/src/legacy/js/fields/field_utils.js +++ b/addons/web/static/src/legacy/js/fields/field_utils.js @@ -14,7 +14,7 @@ * conversions. */ -import { sprintf } from "@web/core/utils/strings"; +import { escape, escapeRegExp, sprintf } from "@web/core/utils/strings"; import core from "web.core"; import dom from "web.dom"; import session from "web.session"; @@ -87,10 +87,10 @@ function formatBoolean(value, field, options) { function formatChar(value, field, options) { value = typeof value === 'string' ? value : ''; if (options && options.isPassword) { - return _.str.repeat('*', value ? value.length : 0); + return "*".repeat(value ? value.length : 0); } if (options && options.escape) { - value = _.escape(value); + value = escape(value); } return value; } @@ -241,7 +241,7 @@ function formatFloatTime(value, field, options) { function formatInteger(value, field, options) { options = options || {}; if (options.isPassword) { - return _.str.repeat('*', String(value).length); + return "*".repeat(String(value).length); } if (!value && value !== 0) { // previously, it returned 'false'. I don't know why. But for the Pivot @@ -282,8 +282,8 @@ function formatMany2one(value, field, options) { value = value.data.display_name || ''; } } - if (options && options.escape) { - value = _.escape(value); + if (options?.escape) { + value = escape(value); } return value; } @@ -373,7 +373,7 @@ function formatMonetary(value, field, options) { if (options.forceString) { return val.join(' '); } - return utils.Markup(val.map((v) => _.escape(v)).join(NBSP)); + return utils.Markup(val.map((v) => escape(v)).join(NBSP)); } /** * Returns a string representing the given value (multiplied by 100) @@ -408,8 +408,8 @@ function formatSelection(value, field, options) { return ''; } value = val[1]; - if (options && options.escape) { - value = _.escape(value); + if (options?.escape) { + value = escape(value); } return value; } @@ -575,8 +575,9 @@ function parseDateTime(value, field, options) { * @returns {float|NaN} the number value contained in the string representation */ function parseNumber(value) { + if (core._t.database.parameters.thousands_sep) { - var escapedSep = _.str.escapeRegExp(core._t.database.parameters.thousands_sep); + var escapedSep = escapeRegExp(core._t.database.parameters.thousands_sep); value = value.replace(new RegExp(escapedSep, 'g'), ''); } if (core._t.database.parameters.decimal_point) { @@ -735,7 +736,7 @@ function parseMany2one(value) { display_name: value[1], }; } - if (Number.isFinite(value) || _.isString(value)) { + if (Number.isFinite(value) || typeof value === "string") { return { id: parseInt(value, 10), }; @@ -753,7 +754,7 @@ export default { float: formatFloat, float_factor: formatFloatFactor, float_time: formatFloatTime, - html: _.identity, // todo + html: (value) => value, // todo integer: formatInteger, many2many: formatX2Many, many2one: formatMany2one, @@ -767,25 +768,25 @@ export default { json: formatJson, }, parse: { - binary: _.identity, - boolean: _.identity, // todo - char: _.identity, // todo + binary: (value) => value, + boolean: (value) => value, // todo + char: (value) => value, // todo date: parseDate, // todo datetime: parseDateTime, // todo float: parseFloat, float_factor: parseFloatFactor, float_time: parseFloatTime, - html: _.identity, // todo + html: (value) => value, // todo integer: parseInteger, - many2many: _.identity, // todo + many2many: (value) => value, // todo many2one: parseMany2one, many2one_reference: parseInteger, monetary: parseMonetary, - one2many: _.identity, + one2many: (value) => value, percentage: parsePercentage, reference: parseMany2one, - selection: _.identity, // todo - text: _.identity, // todo - json: _.identity, // todo + selection: (value) => value, // todo + text: (value) => value, // todo + json: (value) => value, // todo }, }; diff --git a/addons/web/static/src/legacy/js/fields/relational_fields.js b/addons/web/static/src/legacy/js/fields/relational_fields.js index 0c7fa920777..9d11189800f 100644 --- a/addons/web/static/src/legacy/js/fields/relational_fields.js +++ b/addons/web/static/src/legacy/js/fields/relational_fields.js @@ -582,7 +582,7 @@ var FieldMany2One = AbstractField.extend({ * @returns {string} escaped html of value lines */ _renderValueLines: function (needFirstLine) { - const escapedValue = _.escape((this.m2o_value || "").trim()); + const escapedValue = escape((this.m2o_value || "").trim()); const lines = escapedValue.split('\n'); if (!needFirstLine) { lines.shift(); @@ -1050,7 +1050,7 @@ var FieldMany2ManyTags = AbstractField.extend({ * @returns {Promise} */ _addTag: function (data) { - if (!_.contains(this.value.res_ids, data.id)) { + if (!this.value.res_ids.includes(data.id)) { return this._setValue({ operation: 'ADD_M2M', ids: data @@ -1076,7 +1076,7 @@ var FieldMany2ManyTags = AbstractField.extend({ * @returns {Object} */ _getRenderTagsContext: function () { - var elements = this.value ? _.pluck(this.value.data, 'data') : []; + var elements = this.value ? this.value.data.map((d) => d.data) : []; return { colorField: this.colorField, elements: elements, @@ -1249,7 +1249,7 @@ const M2MAvatarMixin = { html: true, placement: 'auto', content: () => { - const elements = this.value ? _.pluck(this.value.data, 'data') : []; + const elements = this.value ? this.value.data.map((d) => d.data) : []; return qweb.render('Many2ManyTagAvatarPopover', { elements: elements.slice(this.visibleAvatarCount - 1), }); diff --git a/addons/web/static/src/legacy/js/public/public_root.js b/addons/web/static/src/legacy/js/public/public_root.js index ccbef75e350..5f77cbe6e58 100644 --- a/addons/web/static/src/legacy/js/public/public_root.js +++ b/addons/web/static/src/legacy/js/public/public_root.js @@ -251,7 +251,7 @@ export const PublicRoot = publicWidget.RootWidget.extend({ args = args.concat(ev.target); var route = args[0]; - if (_.str.startsWith(route, '/web/dataset/call_kw/')) { + if (String(route).startsWith("/web/dataset/call_kw/")) { var params = args[1]; var options = args[2]; var noContextKeys; diff --git a/addons/web/static/src/legacy/js/public/public_widget.js b/addons/web/static/src/legacy/js/public/public_widget.js index 88b7943a1a2..4aefaa8793e 100644 --- a/addons/web/static/src/legacy/js/public/public_widget.js +++ b/addons/web/static/src/legacy/js/public/public_widget.js @@ -212,14 +212,14 @@ var PublicWidget = Widget.extend({ } // If the method has no meaningful options, use the default Widget // system - var isAsync = _.contains(methodOptions, 'async'); + var isAsync = methodOptions.includes('async'); if (!isAsync) { events[event] = method; return; } method = self.proxy(methodOptions[methodOptions.length - 1]); - if (_.str.startsWith(event, 'click')) { + if (String(event).startsWith("click")) { // Protect click handler to be called multiple times by // mistake by the user and add a visual disabling effect // for buttons. diff --git a/addons/web/static/src/legacy/js/services/config.js b/addons/web/static/src/legacy/js/services/config.js index d07ba6c9d2e..9d7c3d1bd52 100644 --- a/addons/web/static/src/legacy/js/services/config.js +++ b/addons/web/static/src/legacy/js/services/config.js @@ -114,7 +114,10 @@ function _updateSizeProps() { } } -_.invoke(medias, 'addListener', _updateSizeProps); +medias.forEach((media) => { + media.addListener(_updateSizeProps); +}); + _updateSizeProps(); export default config; diff --git a/addons/web/static/src/legacy/js/views/basic/basic_model.js b/addons/web/static/src/legacy/js/views/basic/basic_model.js index f7d197bcb15..5210fb17693 100644 --- a/addons/web/static/src/legacy/js/views/basic/basic_model.js +++ b/addons/web/static/src/legacy/js/views/basic/basic_model.js @@ -366,7 +366,7 @@ var BasicModel = AbstractModel.extend({ return this._rpc({ model: modelName, method: 'unlink', - args: [_.pluck(records, 'res_id')], + args: [records.map(r => r.res_id)], context: context, }) .then(function () { @@ -908,7 +908,7 @@ var BasicModel = AbstractModel.extend({ // We only need to load the datapoint in the first case. if (field.value && field.value.length) { if (_.isObject(field.value[0])) { - dpParams.res_ids = _.pluck(field.value, 'id'); + dpParams.res_ids = field.value.map((v) => v.id); dataPoint = self._makeDataPoint(dpParams); field.value.forEach((data) => { var recordDP = self._makeDataPoint({ @@ -1298,7 +1298,7 @@ var BasicModel = AbstractModel.extend({ args: [resIDs], }) .then(function (action) { - if (!_.isEmpty(action)) { + if (Object.keys(action || {}).length > 0) { return new Promise(function (resolve, reject) { self.do_action(action, { on_close: function (result) { @@ -1339,7 +1339,7 @@ var BasicModel = AbstractModel.extend({ }) .then(function (action) { // optionally clear the DataManager's cache - if (!_.isEmpty(action)) { + if (Object.keys(action || {}).length > 0) { return new Promise(function (resolve, reject) { self.do_action(action, { on_close: function () { @@ -2013,7 +2013,7 @@ var BasicModel = AbstractModel.extend({ def = this._rpc({ model: list.model, method: 'read', - args: [_.pluck(data, 'id'), fieldNames], + args: [data.map((d) => d.id), fieldNames], context: Object.assign({}, record.context, field.context, list.getContext()), }).then(function (records) { records.forEach((record) => { @@ -2895,7 +2895,7 @@ var BasicModel = AbstractModel.extend({ */ _fetchSpecialRelation: function (record, fieldName) { var field = record.fields[fieldName]; - if (!_.contains(["many2one", "many2many", "one2many"], field.type)) { + if (!["many2one", "many2many", "one2many"].includes(field.type)) { return Promise.resolve(); } @@ -3318,7 +3318,7 @@ var BasicModel = AbstractModel.extend({ this._sortList(list); if (type === 'many2many' || list._forceM2MLink) { var relRecordCreated = relRecordAdded.filter(rec => typeof rec.res_id === 'string'); - var realIDs = _.difference(list.res_ids, _.pluck(relRecordCreated, 'res_id')); + var realIDs = _.difference(list.res_ids, relRecordCreated.map(r => r.res_id)); // deliberately generate a single 'replace' command instead // of a 'delete' and a 'link' commands with the exact diff // because 1) performance-wise it doesn't change anything @@ -3333,7 +3333,7 @@ var BasicModel = AbstractModel.extend({ // updated (it may happen with editable lists) relRecordUpdated.forEach((relRecord) => { var changes = self._generateChanges(relRecord, options); - if (!_.isEmpty(changes)) { + if (Object.keys(changes || {}).length > 0) { var command = x2ManyCommands.update(relRecord.res_id, changes); commands[fieldName].push(command); } @@ -3348,18 +3348,18 @@ var BasicModel = AbstractModel.extend({ var didChange = false; var changes, command, relRecord; for (var i = 0; i < list.res_ids.length; i++) { - if (_.contains(keptIds, list.res_ids[i])) { + if (keptIds.includes(list.res_ids[i])) { // this is an id that already existed relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]}); changes = relRecord ? this._generateChanges(relRecord, options) : {}; - if (!_.isEmpty(changes)) { + if (Object.keys(changes || {}).length > 0) { command = x2ManyCommands.update(relRecord.res_id, changes); didChange = true; } else { command = x2ManyCommands.link_to(list.res_ids[i]); } commands[fieldName].push(command); - } else if (_.contains(addedIds, list.res_ids[i])) { + } else if (addedIds.includes(list.res_ids[i])) { // this is a new id (maybe existing in DB, but new in JS) relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]}); if (!relRecord) { @@ -3443,7 +3443,7 @@ var BasicModel = AbstractModel.extend({ if (options.full || !(options.fieldName || options.additionalContext)) { var context_to_add = options.sanitize_default_values ? _.omit(element.context, function (val, key) { - return _.str.startsWith(key, 'default_'); + return String(key).startsWith('default_'); }) : element.context; context.add(context_to_add); @@ -4643,7 +4643,7 @@ var BasicModel = AbstractModel.extend({ }); r._changes[fieldName] = rec.id; many2ones[fieldName] = true; - } else if (_.contains(['one2many', 'many2many'], fieldType)) { + } else if (['one2many', 'many2many'].includes(fieldType)) { var x2mCommands = value[2][fieldName]; defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands)); } else { @@ -4799,7 +4799,7 @@ var BasicModel = AbstractModel.extend({ groups.forEach((group) => { var aggregateValues = {}; for (const [key, value] of Object.entries(group)) { - if (_.contains(fields, key) && key !== groupByField && + if (fields.includes(key) && key !== groupByField && AGGREGATABLE_TYPES.includes(list.fields[key].type)) { aggregateValues[key] = value; } @@ -4870,7 +4870,7 @@ var BasicModel = AbstractModel.extend({ // readGroup but are not there anymore. // Note that these groups are put after existing groups so // the order is not conserved. A sort *might* be useful. - var emptyGroupsIDs = _.difference(_.pluck(previousGroups, 'id'), list.data); + var emptyGroupsIDs = _.difference(previousGroups.map((group) => group.id), list.data); emptyGroupsIDs.forEach((groupID) => { list.data.push(groupID); var emptyGroup = self.localData[groupID]; @@ -4884,9 +4884,9 @@ var BasicModel = AbstractModel.extend({ if (!options.onlyGroups) { // generate the res_ids of the main list, being the concatenation // of the fetched res_ids in each group - list.res_ids = _.flatten(_.map(groups, function (group) { + list.res_ids = _.map(groups, function (group) { return group ? group.res_ids : []; - })); + }).flat(); } return list; }).then(function () { @@ -4918,7 +4918,7 @@ var BasicModel = AbstractModel.extend({ // order field first to apply the order on all pages if (list.res_ids.length > list.limit && list.orderedBy.length) { if (!list.orderedResIDs) { - var fieldNames = _.pluck(list.orderedBy, 'name'); + var fieldNames = list.orderedBy.map((list) => list.name); def = this._readMissingFields(list, list.res_ids.filter(id => Number.isFinite(id)), fieldNames, options.withoutRecordData); } def.then(function () { @@ -5077,7 +5077,7 @@ var BasicModel = AbstractModel.extend({ * @returns {boolean} false if the value was already the given one */ _saveSpecialDataCache: function (record, fieldName, value) { - if (_.isEqual(record._specialDataCache[fieldName], value)) { + if (JSON.stringify(record._specialDataCache[fieldName]) === JSON.stringify(value)) { return false; } record._specialDataCache[fieldName] = value; @@ -5121,7 +5121,7 @@ var BasicModel = AbstractModel.extend({ return prom.then(function (result) { delete list.__data; list.count = result.length; - var ids = _.pluck(result.records, 'id'); + var ids = result.records.map((r) => r.id); var data = _.map(result.records, function (record) { var dataPoint = self._makeDataPoint({ context: list.context, @@ -5291,9 +5291,9 @@ var BasicModel = AbstractModel.extend({ var self = this; if (element.parentID) { var parent = this.localData[element.parentID]; - parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) { + parent.res_ids = _.map(parent.data, function (dataPointID) { return self.localData[dataPointID].res_ids; - })); + }).flat(); this._updateParentResIDs(parent); } }, @@ -5395,7 +5395,7 @@ var BasicModel = AbstractModel.extend({ if (!field) { continue; } - if (_.contains(['one2many', 'many2one', 'many2many'], field.type)) { + if (['one2many', 'many2one', 'many2many'].includes(field.type)) { var hasChange = element._changes && fieldName in element._changes; var value = hasChange ? element._changes[fieldName] : element.data[fieldName]; var relationalElement = this.localData[value]; diff --git a/addons/web/static/src/legacy/js/views/standalone_field_manager_mixin.js b/addons/web/static/src/legacy/js/views/standalone_field_manager_mixin.js index b2f71e46912..cef759851c4 100644 --- a/addons/web/static/src/legacy/js/views/standalone_field_manager_mixin.js +++ b/addons/web/static/src/legacy/js/views/standalone_field_manager_mixin.js @@ -42,7 +42,7 @@ var StandaloneFieldManagerMixin = Object.assign({}, FieldManagerMixin, { var result = FieldManagerMixin._confirmChange.apply(this, arguments); var record = this.model.get(id); for (const [fieldName, widget] of Object.entries(this.registeredWidgets[id])) { - if (_.contains(fields, fieldName)) { + if (fields.includes(fieldName)) { widget.reset(record, event); } } diff --git a/addons/web/static/src/legacy/js/views/view_utils.js b/addons/web/static/src/legacy/js/views/view_utils.js index b1a3b734330..d1bcdc38320 100644 --- a/addons/web/static/src/legacy/js/views/view_utils.js +++ b/addons/web/static/src/legacy/js/views/view_utils.js @@ -62,12 +62,12 @@ var viewUtils = { } var dateTypes = ['date', 'datetime']; if (!list.fields[groupByField].readonly && - _.contains(dateTypes, list.fields[groupByField].type)) { + dateTypes.includes(list.fields[groupByField].type)) { return list.fieldsInfo && list.fieldsInfo[list.viewType][groupByField] && list.fieldsInfo[list.viewType][groupByField].allowGroupRangeValue; } var availableTypes = ['char', 'boolean', 'many2one', 'selection']; - return _.contains(availableTypes, list.fields[groupByField].type); + return availableTypes.includes(list.fields[groupByField].type); }, /** * @param {string} arch view arch diff --git a/addons/web/static/src/legacy/js/widgets/domain_selector.js b/addons/web/static/src/legacy/js/widgets/domain_selector.js index 538992fde31..6b5feab4e32 100644 --- a/addons/web/static/src/legacy/js/widgets/domain_selector.js +++ b/addons/web/static/src/legacy/js/widgets/domain_selector.js @@ -694,7 +694,7 @@ var DomainLeaf = DomainNode.extend({ // Set list of operators according to field type var selectedField = this.fieldSelector.getSelectedField() || {}; this.operators = this._getOperatorsFromType(selectedField.type); - if (_.contains(["child_of", "parent_of", "like", "not like", "=like", "=ilike"], this.operator)) { + if (["child_of", "parent_of", "like", "not like", "=like", "=ilike"].includes(this.operator)) { // In case user entered manually or from demo data this.operators[this.operator] = operator_mapping[this.operator]; } else if (!this.operators[this.operator]) { @@ -726,7 +726,7 @@ var DomainLeaf = DomainNode.extend({ } // TODO the value could be a m2o input, etc... - if (_.contains(["date", "datetime"], selectedField.type)) { + if (["date", "datetime"].includes(selectedField.type)) { this.valueWidget = new (selectedField.type === "datetime" ? datepicker.DateTimeWidget : datepicker.DateWidget)(this); wDefs.push(this.valueWidget.appendTo("
").then((function () { this.valueWidget.$el.addClass("o_domain_leaf_value_input"); @@ -821,10 +821,10 @@ var DomainLeaf = DomainNode.extend({ _changeOperator: function (operator, silent) { this.operator = operator; - if (_.contains(["set", "not set"], this.operator)) { + if (["set", "not set"].includes(this.operator)) { this.operator = this.operator === "not set" ? "=" : "!="; this.value = false; - } else if (_.contains(["in", "not in"], this.operator)) { + } else if (["in", "not in"].includes(this.operator)) { this.value = Array.isArray(this.value) ? this.value : this.value ? ("" + this.value).split(",") : []; } else { if (Array.isArray(this.value)) { @@ -857,22 +857,22 @@ var DomainLeaf = DomainNode.extend({ } if (selectedField.type === "boolean") { - if (!_.isBoolean(this.value)) { // Convert boolean-like value to boolean + if (typeof this.value !== "boolean") { // Convert boolean-like value to boolean this.value = !!parseFloat(this.value); } } else if (selectedField.type === "selection") { if (!_.some(selectedField.selection, (function (option) { return option[0] === this.value; }).bind(this))) { this.value = selectedField.selection[0][0]; } - } else if (_.contains(["date", "datetime"], selectedField.type)) { - if (couldNotParse || _.isBoolean(this.value)) { + } else if (["date", "datetime"].includes(selectedField.type)) { + if (couldNotParse || typeof this.value === "boolean") { this.value = field_utils.parse[selectedField.type](field_utils.format[selectedField.type](moment())).toJSON(); // toJSON to get date with server format } else { this.value = this.value.toJSON(); // toJSON to get date with server format } } else { // Never display "true" or "false" strings from boolean value - if (_.isBoolean(this.value)) { + if (typeof this.value === "boolean") { this.value = ""; } else if (_.isObject(this.value) && !Array.isArray(this.value)) { // Can be object if parsed to x2x representation this.value = this.value.id || value || ""; @@ -975,7 +975,7 @@ var DomainLeaf = DomainNode.extend({ // TODO The two following functions should be in an independant widget on_add_tag: function (e) { if (e.type === "keyup" && e.which !== $.ui.keyCode.ENTER) return; - if (!_.contains(["not in", "in"], this.operator)) return; + if (!["not in", "in"].includes(this.operator)) return; var values = Array.isArray(this.value) ? this.value.slice() : []; diff --git a/addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js b/addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js index 85c0a77a170..5019229fd41 100644 --- a/addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js +++ b/addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js @@ -158,7 +158,7 @@ var ModelFieldSelectorPopover = Widget.extend({ * @returns {Promise} resolved once the re-rendering is finished */ setChain: function (chain) { - if (_.isEqual(chain, this.chain)) { + if (JSON.stringify(chain) === JSON.stringify(this.chain)) { return Promise.resolve(); } @@ -284,7 +284,7 @@ var ModelFieldSelectorPopover = Widget.extend({ * @param {Object} field - the field to add to the chain node */ _goToNextPage: function (field) { - if (!_.isEqual(this._getLastPageField(field.name), field)) return; + if (JSON.stringify(this._getLastPageField(field.name)) !== JSON.stringify(field)) return; this._validate(true); this._addChainNode(field.name); @@ -489,7 +489,7 @@ var ModelFieldSelectorPopover = Widget.extend({ * @param {Object} field - the field to select */ _selectField: function (field) { - if (!_.isEqual(this._getLastPageField(field.name), field)) return; + if (JSON.stringify(this._getLastPageField(field.name)) !== JSON.stringify(field)) return; this._validate(true); this._addChainNode(field.name); diff --git a/addons/web/static/src/legacy/js/widgets/name_and_signature.js b/addons/web/static/src/legacy/js/widgets/name_and_signature.js index a7a97ea6088..ce3a14a4e36 100644 --- a/addons/web/static/src/legacy/js/widgets/name_and_signature.js +++ b/addons/web/static/src/legacy/js/widgets/name_and_signature.js @@ -4,6 +4,7 @@ import core from "web.core"; import config from "web.config"; import utils from "web.utils"; import Widget from "web.Widget"; +import { debounce } from "@web/core/utils/timing"; /** @@ -147,7 +148,7 @@ var NameAndSignature = Widget.extend({ } // Resize the signature area if it is resized - $(window).on('resize.o_web_sign_name_and_signature', _.debounce(function () { + $(window).on('resize.o_web_sign_name_and_signature', debounce(function () { if (self.isDestroyed()) { // May happen since this is debounced return; diff --git a/addons/web/static/tests/core/utils/numbers_test.js b/addons/web/static/tests/core/utils/numbers_test.js new file mode 100644 index 00000000000..6c43f4b680f --- /dev/null +++ b/addons/web/static/tests/core/utils/numbers_test.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import { range } from "@web/core/utils/numbers"; + +QUnit.module("utils", () => { + QUnit.module("Numbers", () => { + QUnit.test("test range function from core/utils/numbers.js", (assert) => { + assert.deepEqual(range(0, 10), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + assert.deepEqual(range(0, -10, -1), [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]); + assert.deepEqual(range(0, 35, 5), [0, 5, 10, 15, 20, 25, 30]); + assert.deepEqual(range(-10, 6, 2), [-10, -8, -6, -4, -2, 0, 2, 4]); + assert.deepEqual(range(4, -4, -1), [4, 3, 2, 1, 0, -1, -2, -3]); + }); + }); +}); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 32a160d98af..25401329923 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -544,7 +544,8 @@ export class MockServer { item.includes("readonly") ))) && (!["1", "True"].includes(node.getAttribute("readonly")) || - !_.isEmpty(evaluateExpr(node.getAttribute("attrs") || "{}"))) + Object.keys(evaluateExpr(node.getAttribute("attrs") || "{}") || {}).length > + 0) ); } default: @@ -1275,7 +1276,7 @@ export class MockServer { group.__domain = [[fieldName, "=", value]].concat(group.__domain); } } - if (_.isEmpty(group.__range)) { + if (Object.keys(group.__range || {}).length === 0) { delete group.__range; } // compute count key to match dumb server logic... diff --git a/addons/web/static/tests/legacy/control_panel/filter_menu_tests.js b/addons/web/static/tests/legacy/control_panel/filter_menu_tests.js index 1b674d7ad6f..e6a2a267cac 100644 --- a/addons/web/static/tests/legacy/control_panel/filter_menu_tests.js +++ b/addons/web/static/tests/legacy/control_panel/filter_menu_tests.js @@ -5,6 +5,7 @@ import testUtils from "web.test_utils"; import * as cpHelpers from "@web/../tests/search/helpers"; + const { createControlPanel, mock } = testUtils; const { patchDate } = mock; diff --git a/addons/web/static/tests/legacy/core/time_tests.js b/addons/web/static/tests/legacy/core/time_tests.js index c04afb3c430..e4b5bb6ed75 100644 --- a/addons/web/static/tests/legacy/core/time_tests.js +++ b/addons/web/static/tests/legacy/core/time_tests.js @@ -40,7 +40,7 @@ QUnit.module('core', {}, function () { try { window.Date = function (v) { - if (_.isUndefined(v)) { + if (typeof v === "undefined") { v = '2013-10-31 12:34:56'; } return new wDate(v); @@ -73,7 +73,7 @@ QUnit.module('core', {}, function () { try { window.Date = function (v) { - if (_.isUndefined(v)) { + if (typeof v === "undefined") { v = '2013-10-31 12:34:56'; } return new wDate(v); diff --git a/addons/web/static/tests/legacy/core/widget_tests.js b/addons/web/static/tests/legacy/core/widget_tests.js index ff72083602b..b14f608d987 100644 --- a/addons/web/static/tests/legacy/core/widget_tests.js +++ b/addons/web/static/tests/legacy/core/widget_tests.js @@ -100,7 +100,7 @@ QUnit.module('core', {}, function () { assert.strictEqual(widget.el.nodeName, 'DIV', "should have generated the default element"); assert.strictEqual(widget.el.attributes.length, 0, "should not have generated any attribute"); - assert.ok(_.isEmpty(widget.$el.html(), "should not have generated any content")); + assert.ok(Object.keys(widget.$el.html() || {}).length === 0, "should not have generated any content"); widget.destroy(); }); diff --git a/addons/web/static/tests/legacy/helpers/mock_server.js b/addons/web/static/tests/legacy/helpers/mock_server.js index b5cbe83299a..e33c5045455 100644 --- a/addons/web/static/tests/legacy/helpers/mock_server.js +++ b/addons/web/static/tests/legacy/helpers/mock_server.js @@ -216,7 +216,7 @@ var MockServer = Class.extend({ if ('default' in model.fields[fieldName]) { const def = model.fields[fieldName].default; record[fieldName] = typeof def === 'function' ? def.call(this) : def; - } else if (_.contains(['one2many', 'many2many'], model.fields[fieldName].type)) { + } else if (["one2many", "many2many"].includes(model.fields[fieldName].type)) { record[fieldName] = []; } else { record[fieldName] = false; @@ -1254,7 +1254,7 @@ var MockServer = Class.extend({ ids = [ids]; } var records = this.data[model].records; - var names = _.map(ids, function (id) { + var names = ids.map(id => { return id ? [id, _.findWhere(records, {id: id}).display_name] : [null, ""]; }); return names; @@ -1299,7 +1299,7 @@ var MockServer = Class.extend({ if (str.length) { records = records.filter(record => record.display_name.indexOf(str) !== -1); } - var result = _.map(records, function (record) { + var result = records.map(record => { return [record.id, record.display_name]; }); return result.slice(0, limit); @@ -1471,8 +1471,8 @@ var MockServer = Class.extend({ }); // filter out non existing fields - aggregatedFields = aggregatedFields.filter(name => { - return name in self.data[model].fields && !(_.contains(groupByFieldNames,name)); + aggregatedFields = aggregatedFields.filter((name) => { + return name in self.data[model].fields && !groupByFieldNames.includes(name); }); function aggregateFields(group, records) { @@ -1487,7 +1487,7 @@ var MockServer = Class.extend({ } } if (type === 'many2one') { - var ids = _.pluck(records, aggregatedFields[i]); + var ids = records.map((record) => aggregatedFields[i]); group[aggregatedFields[i]] = _.uniq(ids).length || null; } } @@ -1657,7 +1657,7 @@ var MockServer = Class.extend({ group.__domain = [[fieldName, "=", value]].concat(group.__domain); } } - if (_.isEmpty(group.__range)) { + if (Object.keys(group.__range || {}).length === 0) { delete group.__range; } // compute count key to match dumb server logic... @@ -1869,7 +1869,9 @@ var MockServer = Class.extend({ if (!Array.isArray(ids)) { ids = [ids]; } - this.data[model].records = this.data[model].records.filter(record => !_.contains(ids, record.id)); + this.data[model].records = this.data[model].records.filter( + (record) => !ids.includes(record.id) + ); // update value of relationnal fields pointing to the deleted records Object.values(this.data).forEach((d) => { @@ -1995,7 +1997,10 @@ var MockServer = Class.extend({ case '/web/dataset/resequence': return this._mockResequence(args); } - if (route.indexOf('/web/image') >= 0 || _.contains(['.png', '.jpg'], route.substr(route.length - 4))) { + if ( + route.indexOf("/web/image") >= 0 || + [".png", ".jpg"].includes(route.substr(route.length - 4)) + ) { return; } switch (args.method) { @@ -2204,7 +2209,7 @@ var MockServer = Class.extend({ if (!field) { throw Error(`Mock: Can't write value "${JSON.stringify(value)}" on field "${field_changed}" on record "${model},${id}" (field is undefined)`); } - if (_.contains(['one2many', 'many2many'], field.type)) { + if (["one2many", "many2many"].includes(field.type)) { var ids = _.clone(record[field_changed]) || []; if ( @@ -2234,7 +2239,7 @@ var MockServer = Class.extend({ } else if (command[0] === 3) { // FORGET ids = _.without(ids, command[1]); } else if (command[0] === 4) { // LINK_TO - if (!_.contains(ids, command[1])) { + if (!ids.includes(command[1])) { ids.push(command[1]); } } else if (command[0] === 5) { // DELETE ALL diff --git a/addons/web/static/tests/legacy/qweb_tests.js b/addons/web/static/tests/legacy/qweb_tests.js index 044eb613b32..7c3c5cd768d 100644 --- a/addons/web/static/tests/legacy/qweb_tests.js +++ b/addons/web/static/tests/legacy/qweb_tests.js @@ -4,6 +4,7 @@ import {Markup} from "web.utils"; var qwebPath = '/web/static/lib/qweb/'; import {hushConsole} from "@web/../tests/helpers/utils"; +import { escape } from "@web/core/utils/strings"; function trim(s) { return s.replace(/(^\s+|\s+$)/g, ''); @@ -114,13 +115,13 @@ QUnit.module('QWeb', { QUnit.test('escape', assert => { // not strictly about qweb... assert.expect(8); - assert.equal(_.escape('a'), 'a'); - assert.equal(_.escape(''), '<a>'); + assert.equal(escape('a'), 'a'); + assert.equal(escape(''), '<a>'); assert.equal(_.escape({[_.escapeMethod]() { return 'a'; }}), 'a'); assert.equal(_.escape({[_.escapeMethod]() { return ''; }}), ''); - assert.equal(_.escape(Markup('a')), 'a'); + assert.equal(escape(Markup('a')), 'a'); assert.equal(_.escape(Markup('')), ''); - assert.equal(_.escape(Markup`a`), 'a'); + assert.equal(escape(Markup`a`), 'a'); assert.equal(_.escape(Markup``), ''); }); QUnit.module('t-out', {}, () => { diff --git a/addons/web/static/tests/legacy/views/basic_model_tests.js b/addons/web/static/tests/legacy/views/basic_model_tests.js index 41608d6be1b..0cbfd98d8d8 100644 --- a/addons/web/static/tests/legacy/views/basic_model_tests.js +++ b/addons/web/static/tests/legacy/views/basic_model_tests.js @@ -1374,7 +1374,7 @@ await model.load(params).then(function (resultID) { var record = model.get(resultID); - assert.ok(_.isEqual(record.data.category.res_ids, [12, 14]), + assert.ok(JSON.stringify(record.data.category.res_ids) === JSON.stringify([12, 14]), "category field should have correct default value"); }); diff --git a/addons/web/static/tests/qunit.js b/addons/web/static/tests/qunit.js index 28d76587167..4a461b047cf 100644 --- a/addons/web/static/tests/qunit.js +++ b/addons/web/static/tests/qunit.js @@ -8,6 +8,7 @@ import { fullAnnotatedTraceback, } from "@web/core/errors/error_utils"; import { registry } from "@web/core/registry"; +import { escape } from "@web/core/utils/strings"; function setQUnitDebugMode() { owl.whenReady(() => document.body.classList.add("debug")); // make the test visible to the naked eye @@ -263,15 +264,15 @@ export function setupQUnit() { modulesAlert.classList.toggle("alert-info"); let error = "Some modules couldn't be started: