From 608e90e998107f8b834d4edc8ea918041725c551 Mon Sep 17 00:00:00 2001 From: "Pulinckx Pierre (PIPU)" Date: Tue, 18 Apr 2023 12:36:07 +0000 Subject: [PATCH] [REF] *: Replace underscore functions by native JS Replace _.map(), _.flatten(), _.delay(), _.contains(), _.pluck(), _.isUndefined(), _.isEmpty(), _.isString(), _.isEqual(), _.isBoolean(), _.memoize(), _.invoke(), _.bind(), _.escape(), _.debounce(), _.str.sprintf(), _.str.repeat(), _.str.startswith(), _.str.trim(), _.str.escapeHTML(), _.str.escapeRegExp(), _.str.startsWith(), _.str.include() closes odoo/odoo#118012 Signed-off-by: Lucas Perais (lpe) --- .../static/src/js/totp_frontend.js | 4 +- addons/board/static/src/board_view.js | 9 ++- .../static/src/legacy/core_models/device.js | 3 +- .../legacy/legacy_models/public_livechat.js | 10 +-- .../src/legacy/public_models/chatbot.js | 3 +- .../public_models/livechat_button_view.js | 3 +- .../public_models/public_livechat_global.js | 2 +- .../public_livechat_view.js | 3 +- .../public_livechat_window.js | 7 +- addons/l10n_fr_pos_cert/static/src/js/pos.js | 2 +- addons/mail/static/src/js/utils.js | 27 ++++---- .../mock_server/models/mail_activity.js | 13 ++-- addons/mail/static/tests/mail_utils_tests.js | 1 + .../static/src/js/mailing_portal.js | 4 +- .../static/src/js/mass_mailing_html_field.js | 2 +- addons/payment/static/src/js/checkout_form.js | 3 +- addons/payment/static/src/js/manage_form.js | 3 +- .../static/src/js/payment_form_mixin.js | 28 ++++++-- addons/point_of_sale/static/src/js/db.js | 2 +- addons/point_of_sale/static/src/js/models.js | 31 +++------ .../portal/static/src/js/portal_composer.js | 4 +- .../static/src/js/portal_chatter.js | 2 +- addons/pos_loyalty/static/src/js/Loyalty.js | 7 +- .../static/src/js/PaymentScreen.js | 4 +- .../src/app/floor_screen/floor_screen.js | 65 ++++++++++++------- .../SplitBillScreen/SplitBillScreen.js | 4 +- .../static/src/js/purchase_portal_sidebar.js | 27 ++++---- .../sale/static/src/js/sale_portal_sidebar.js | 6 +- addons/sale/static/src/js/variant_mixin.js | 14 ++-- .../static/src/js/sale_product_field.js | 2 +- .../static/src/js/survey_session_manage.js | 4 +- addons/web/static/src/core/utils/numbers.js | 9 +-- .../web/static/src/legacy/js/core/context.js | 2 +- .../web/static/src/legacy/js/core/dialog.js | 4 +- .../web/static/src/legacy/js/core/domain.js | 17 ++--- addons/web/static/src/legacy/js/core/misc.js | 4 +- .../web/static/src/legacy/js/core/mixins.js | 2 +- .../web/static/src/legacy/js/core/popover.js | 3 +- .../web/static/src/legacy/js/core/py_utils.js | 8 +-- addons/web/static/src/legacy/js/core/rpc.js | 6 +- .../web/static/src/legacy/js/core/session.js | 4 +- addons/web/static/src/legacy/js/core/utils.js | 2 +- .../web/static/src/legacy/js/core/widget.js | 10 +-- .../src/legacy/js/fields/basic_fields.js | 3 +- .../src/legacy/js/fields/field_utils.js | 43 ++++++------ .../src/legacy/js/fields/relational_fields.js | 8 +-- .../src/legacy/js/public/public_root.js | 2 +- .../src/legacy/js/public/public_widget.js | 4 +- .../static/src/legacy/js/services/config.js | 5 +- .../src/legacy/js/views/basic/basic_model.js | 46 ++++++------- .../views/standalone_field_manager_mixin.js | 2 +- .../static/src/legacy/js/views/view_utils.js | 4 +- .../src/legacy/js/widgets/domain_selector.js | 18 ++--- .../widgets/model_field_selector_popover.js | 6 +- .../legacy/js/widgets/name_and_signature.js | 3 +- .../static/tests/core/utils/numbers_test.js | 15 +++++ .../web/static/tests/helpers/mock_server.js | 5 +- .../legacy/control_panel/filter_menu_tests.js | 1 + .../static/tests/legacy/core/time_tests.js | 4 +- .../static/tests/legacy/core/widget_tests.js | 2 +- .../tests/legacy/helpers/mock_server.js | 27 ++++---- addons/web/static/tests/legacy/qweb_tests.js | 9 +-- .../tests/legacy/views/basic_model_tests.js | 2 +- addons/web/static/tests/qunit.js | 7 +- .../views/fields/many2many_field_tests.js | 3 +- .../fields/many2many_tags_field_tests.js | 5 +- .../views/fields/percent_pie_field_tests.js | 21 +++--- .../static/src/js/backend/convert_inline.js | 9 ++- .../static/src/js/backend/html_field.js | 7 +- addons/web_editor/static/src/js/common/ace.js | 17 ++--- .../static/src/js/editor/snippets.editor.js | 13 ++-- .../static/src/js/editor/snippets.options.js | 15 +++-- .../web_editor/static/src/js/wysiwyg/fonts.js | 2 +- addons/web_editor/static/tests/test_utils.js | 8 +-- .../static/src/tour_service/tour_utils.js | 2 +- .../static/src/js/unsplash_beacon.js | 2 +- .../src/js/content/snippets.animation.js | 20 +++--- .../static/src/js/editor/snippets.editor.js | 6 +- .../static/src/js/editor/widget_link.js | 3 +- .../website/static/src/js/editor/wysiwyg.js | 2 +- addons/website/static/src/js/utils.js | 6 +- .../static/src/snippets/s_chart/options.js | 10 ++- .../src/snippets/s_image_gallery/options.js | 2 +- .../static/src/snippets/s_searchbar/000.js | 5 +- .../static/src/snippets/s_website_form/000.js | 7 +- .../tours/website_form_editor_frontend.js | 2 +- .../static/src/js/website_event.js | 2 +- .../static/src/js/event_exhibitor_connect.js | 3 +- .../static/src/js/event_track_reminder.js | 3 +- .../static/src/js/website_forum.js | 9 ++- .../static/src/js/website_links.js | 8 ++- .../static/src/js/website_sale.js | 9 ++- .../static/src/js/website_sale_payment.js | 14 ++-- .../src/js/website_sale_recently_viewed.js | 3 +- .../static/src/js/website_sale_comparison.js | 6 +- .../static/src/js/website_sale_wishlist.js | 6 +- .../src/js/slides_course_fullscreen_player.js | 8 +-- .../static/src/js/slides_course_quiz.js | 2 +- .../src/js/slides_course_quiz_finish.js | 12 ++-- .../static/src/js/slides_embed.js | 11 +++- .../static/src/js/slides_share.js | 6 +- .../static/src/js/slides_upload.js | 2 +- .../src/js/website.twitter.animation.js | 9 +-- 103 files changed, 483 insertions(+), 383 deletions(-) create mode 100644 addons/web/static/tests/core/utils/numbers_test.js 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:
    "; if (info.failed.length) { - const failedList = info.failed.map((mod) => "
  • " + _.escape(mod) + "
  • "); + const failedList = info.failed.map((mod) => "
  • " + escape(mod) + "
  • "); error += `
  • Failed modules:
      ${failedList.join("")}
  • `; } if (info.missing.length) { - const missingList = info.missing.map((mod) => "
  • " + _.escape(mod) + "
  • "); + const missingList = info.missing.map((mod) => "
  • " + escape(mod) + "
  • "); error += `
  • Missing dependencies:
      ${missingList.join("")}
  • `; } if (info.unloaded.length) { - const unloadedList = info.unloaded.map((mod) => "
  • " + _.escape(mod) + "
  • "); + const unloadedList = info.unloaded.map((mod) => "
  • " + escape(mod) + "
  • "); error += `
  • Non loaded modules due to missing dependencies:
      ${unloadedList.join("")}
    diff --git a/addons/web/static/tests/views/fields/many2many_field_tests.js b/addons/web/static/tests/views/fields/many2many_field_tests.js index 8b66bae6d63..19a2d109c85 100644 --- a/addons/web/static/tests/views/fields/many2many_field_tests.js +++ b/addons/web/static/tests/views/fields/many2many_field_tests.js @@ -286,7 +286,8 @@ QUnit.module("Fields", (hooks) => { }); var ids = _.sortBy([12, 15, 18].concat(createdType.id), _.identity.bind(_)); assert.ok( - _.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids), + JSON.stringify(_.sortBy(commands[0][2], _.identity.bind(_))) === + JSON.stringify(ids), "new value should be " + ids ); } diff --git a/addons/web/static/tests/views/fields/many2many_tags_field_tests.js b/addons/web/static/tests/views/fields/many2many_tags_field_tests.js index 55c29168685..ca7ab85d064 100644 --- a/addons/web/static/tests/views/fields/many2many_tags_field_tests.js +++ b/addons/web/static/tests/views/fields/many2many_tags_field_tests.js @@ -481,7 +481,10 @@ QUnit.module("Fields", (hooks) => { 6, "generated command should be REPLACE WITH" ); - assert.ok(_.isEqual(commands[0][2], [12]), "new value should be [12]"); + assert.ok( + JSON.stringify(commands[0][2]) === JSON.stringify([12]), + "new value should be [12]" + ); } }, }); diff --git a/addons/web/static/tests/views/fields/percent_pie_field_tests.js b/addons/web/static/tests/views/fields/percent_pie_field_tests.js index 91cb25074e1..1a8aa489573 100644 --- a/addons/web/static/tests/views/fields/percent_pie_field_tests.js +++ b/addons/web/static/tests/views/fields/percent_pie_field_tests.js @@ -93,19 +93,17 @@ QUnit.module("Fields", (hooks) => { "should have 10% as pie value since int_field=10" ); assert.ok( - _.str.include( + String( target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style - .transform, - "rotate(180deg)" - ), + .transform + ).includes("rotate(180deg)"), "left mask should be covering the whole left side of the pie" ); assert.ok( - _.str.include( + String( target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1] - .style.transform, - "rotate(36deg)" - ), + .style.transform + ).includes("rotate(36deg)"), "right mask should be rotated from 360*(10/100) = 36 degrees" ); @@ -162,11 +160,10 @@ QUnit.module("Fields", (hooks) => { "should have 80% as pie value since int_field=80" ); assert.ok( - _.str.include( + String( target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style - .transform, - "rotate(288deg)" - ), + .transform + ).includes("rotate(288deg)"), "left mask should be rotated from 360*(80/100) = 288 degrees" ); assert.hasClass( diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/backend/convert_inline.js index 2d11ead61c8..3b93c1dc876 100644 --- a/addons/web_editor/static/src/js/backend/convert_inline.js +++ b/addons/web_editor/static/src/js/backend/convert_inline.js @@ -391,7 +391,7 @@ function classToStyle($editable, cssRules) { style = `${key}:${value};${style}`; } }; - if (_.isEmpty(style)) { + if (Object.keys(style || {}).length === 0) { writes.push(() => { node.removeAttribute('style'); }); } else { writes.push(() => { @@ -1371,7 +1371,12 @@ function _normalizeStyle(style) { const normalizedStyle = {}; for (const styleName of style) { const value = style[styleName]; - if (value && !styleName.includes('animation') && !styleName.includes('-webkit') && _.isString(value)) { + if ( + value && + !styleName.includes("animation") && + !styleName.includes("-webkit") && + typeof value === "string" + ) { const normalizedStyleName = styleName.replace(/-(.)/g, (a, b) => b.toUpperCase()); normalizedStyle[styleName] = style[normalizedStyleName]; if (style.getPropertyPriority(styleName) === 'important') { diff --git a/addons/web_editor/static/src/js/backend/html_field.js b/addons/web_editor/static/src/js/backend/html_field.js index 0f002da5bb5..35b0379ad28 100644 --- a/addons/web_editor/static/src/js/backend/html_field.js +++ b/addons/web_editor/static/src/js/backend/html_field.js @@ -62,8 +62,9 @@ export class HtmlFieldWysiwygAdapterComponent extends ComponentAdapter { stripHistoryIds(newValue) !== stripHistoryIds(newProps.editingValue) && stripHistoryIds(lastValue) !== stripHistoryIds(newValue) ) || - !_.isEqual(lastRecordInfo, newRecordInfo) || - !_.isEqual(lastCollaborationChannel, newCollaborationChannel)) + JSON.stringify(lastRecordInfo) !== JSON.stringify(newRecordInfo) || + JSON.stringify(lastCollaborationChannel) !== JSON.stringify(newCollaborationChannel) + ) { this.widget.resetEditor(newValue, newProps.widgetArgs[0]); this.env.onWysiwygReset && this.env.onWysiwygReset(); @@ -132,7 +133,7 @@ export class HtmlField extends Component { res_model: newProps.record.resModel, res_id: newProps.record.resId, }; - if (!_.isEqual(this._lastRecordInfo, newRecordInfo)) { + if (JSON.stringify(this._lastRecordInfo) !== JSON.stringify(newRecordInfo)) { this.currentEditingValue = undefined; } this._lastRecordInfo = newRecordInfo; diff --git a/addons/web_editor/static/src/js/common/ace.js b/addons/web_editor/static/src/js/common/ace.js index ab1583929a7..c69d795de2c 100644 --- a/addons/web_editor/static/src/js/common/ace.js +++ b/addons/web_editor/static/src/js/common/ace.js @@ -8,6 +8,7 @@ import Dialog from "web.Dialog"; import Widget from "web.Widget"; import localStorage from "web.local_storage"; import { sprintf } from "@web/core/utils/strings"; +import { debounce } from "@web/core/utils/timing"; var _t = core._t; @@ -110,10 +111,10 @@ var checkSCSS = (function () { return function (scss) { var stack = []; var line = 1; - for (var i = 0 ; i < scss.length ; i++) { - if (_.contains(openings, scss[i])) { + for (var i = 0; i < scss.length; i++) { + if (openings.includes(scss[i])) { stack.push(scss[i]); - } else if (_.contains(closings, scss[i])) { + } else if (closings.includes(scss[i])) { if (stack.pop() !== mapping[scss[i]]) { return _getCheckReturn(false, line, _t("Unexpected ") + scss[i]); } @@ -254,7 +255,7 @@ var ViewEditor = Widget.extend({ var refX = 0; var resizing = false; var minWidth = 400; - var debounceStoreEditorWidth = _.debounce(storeEditorWidth, 500); + var debounceStoreEditorWidth = debounce(storeEditorWidth, 500); this._updateViewSelectDOM(); @@ -262,8 +263,8 @@ var ViewEditor = Widget.extend({ var initType; if (this.options.initialResID) { initResID = this.options.initialResID; - if (_.isString(initResID) && initResID[0] === '/') { - if (_.str.endsWith(initResID, '.scss')) { + if (typeof initResID === "string" && initResID[0] === '/') { + if (initResID.endsWith(".scss")) { initType = 'scss'; } else { initType = 'js'; @@ -739,7 +740,7 @@ var ViewEditor = Widget.extend({ } else { var onChangeSession = (function () { this.aceEditor.off('changeSession', onChangeSession); - _.delay(__showErrorLine.bind(this, line), 400); + setTimeout(__showErrorLine.bind(this, line), 400); }).bind(this); this.aceEditor.on('changeSession', onChangeSession); this._displayResource(resID, this.currentType); @@ -799,7 +800,7 @@ var ViewEditor = Widget.extend({ this.errorSession = this.aceEditor.getSession(); this.errorSessionChangeCallback = __restore.bind(this); this.errorSession.on('change', this.errorSessionChangeCallback); - this.errorSessionScrollCallback = _.debounce(__updateErrorLineDisplay.bind(this, line), 10); + this.errorSessionScrollCallback = debounce(__updateErrorLineDisplay.bind(this, line), 10); this.errorSession.on('changeScrollTop', this.errorSessionScrollCallback); __updateErrorLineDisplay.call(this, line); diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 677c76dce40..f0fdf84c8fa 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -11,9 +11,10 @@ import {ColorPaletteWidget} from "web_editor.ColorPalette"; import SmoothScrollOnDrag from "web.smooth_scroll_on_drag"; import {getCSSVariableValue} from "web_editor.utils"; import * as gridUtils from "@web_editor/js/common/grid_layout_utils"; -import { sprintf } from "@web/core/utils/strings"; +import { sprintf, escape } from "@web/core/utils/strings"; const QWeb = core.qweb; import {closestElement} from "@web_editor/js/editor/odoo-editor/src/utils/utils"; +import { debounce } from "@web/core/utils/timing"; var _t = core._t; @@ -23,7 +24,7 @@ let cacheSnippetTemplate = {}; $.extend($.expr[':'], { o_editable: function (node, i, m) { while (node) { - if (node.className && _.isString(node.className)) { + if (node.className && typeof node.className === "string") { if (node.className.indexOf('o_not_editable') !== -1) { return false; } @@ -2011,7 +2012,7 @@ var SnippetsMenu = Widget.extend({ const range = selection.getRangeAt(0); $(range.startContainer).closest('.o_default_snippet_text').removeClass('o_default_snippet_text'); }); - const refreshSnippetEditors = _.debounce(() => { + const refreshSnippetEditors = debounce(() => { for (const snippetEditor of this.snippetEditors) { this._mutex.exec(() => snippetEditor.destroy()); } @@ -2042,7 +2043,7 @@ var SnippetsMenu = Widget.extend({ this.options.wysiwyg.odooEditor.addEventListener('observerApply', () => { $(this.options.wysiwyg.odooEditor.editable).trigger('content_changed'); }); - this.options.wysiwyg.odooEditor.addEventListener('historyRevert', _.debounce(() => { + this.options.wysiwyg.odooEditor.addEventListener('historyRevert', debounce(() => { this.trigger_up('widgets_start_request', { $target: this.options.wysiwyg.$editable, editableMode: true, @@ -2878,8 +2879,8 @@ var SnippetsMenu = Widget.extend({ .addClass('oe_snippet') .each((i, el) => { const $snippet = $(el); - const name = _.escape(el.getAttribute('name')); - const thumbnailSrc = _.escape(el.dataset.oeThumbnail); + const name = escape(el.getAttribute('name')); + const thumbnailSrc = escape(el.dataset.oeThumbnail); const $sbody = $snippet.children().addClass('oe_snippet_body'); const isCustomSnippet = !!el.closest('#snippet_custom'); diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index bb6c400c5d6..7d985db529b 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -34,6 +34,7 @@ import { import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor"; import {SIZES, MEDIAS_BREAKPOINTS} from "@web/core/ui/ui_service"; import { sprintf } from "@web/core/utils/strings"; +import { debounce } from "@web/core/utils/timing"; var qweb = core.qweb; var _t = core._t; @@ -2370,7 +2371,7 @@ const RangeUserValueWidget = UnitUserValueWidget.extend({ this.containerEl.appendChild(this.outputEl); } - this._onInputChange = _.debounce(this._onInputChange, 100); + this._onInputChange = debounce(this._onInputChange, 100); }, //-------------------------------------------------------------------------- @@ -4767,8 +4768,8 @@ registry['sizing_x'] = registry.sizing.extend({ var gridE = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; var gridW = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; this.grid = { - e: [_.map(gridE, v => ('col-lg-' + v)), _.map(gridE, v => width / 12 * v), 'width'], - w: [_.map(gridW, v => ('offset-lg-' + v)), _.map(gridW, v => width / 12 * v), 'margin-left'], + e: [gridE.map(v => ('col-lg-' + v)), gridE.map(v => width / 12 * v), 'width'], + w: [gridW.map(v => ('offset-lg-' + v)), gridW.map(v => width / 12 * v), 'margin-left'], }; return this.grid; }, @@ -4847,10 +4848,10 @@ registry['sizing_grid'] = registry.sizing.extend({ const gridE = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; this.grid = { - n: [_.map(gridN, v => ('g-height-' + (rowEnd - v))), _.map(gridN, v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-start'], - s: [_.map(gridS, v => ('g-height-' + (v - rowStart))), _.map(gridS, v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-end'], - w: [_.map(gridW, v => ('g-col-lg-' + (columnEnd - v))), _.map(gridW, v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-start'], - e: [_.map(gridE, v => ('g-col-lg-' + (v - columnStart))), _.map(gridE, v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-end'], + n: [gridN.map(v => ('g-height-' + (rowEnd - v))), gridN.map(v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-start'], + s: [gridS.map(v => ('g-height-' + (v - rowStart))), gridS.map(v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-end'], + w: [gridW.map(v => ('g-col-lg-' + (columnEnd - v))), gridW.map(v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-start'], + e: [gridE.map(v => ('g-col-lg-' + (v - columnStart))), gridE.map(v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-end'], }; return this.grid; diff --git a/addons/web_editor/static/src/js/wysiwyg/fonts.js b/addons/web_editor/static/src/js/wysiwyg/fonts.js index 858f71887c9..7f8f389422d 100644 --- a/addons/web_editor/static/src/js/wysiwyg/fonts.js +++ b/addons/web_editor/static/src/js/wysiwyg/fonts.js @@ -90,7 +90,7 @@ export default { var self = this; this.fontIcons.forEach((data) => { data.cssData = self.getCssSelectors(data.parser); - data.alias = _.flatten(_.map(data.cssData, _.property('names'))); + data.alias = _.map(data.cssData, _.property('names')).flat(); }); }), }; diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js index 6d809c86d64..1d42c50103b 100644 --- a/addons/web_editor/static/tests/test_utils.js +++ b/addons/web_editor/static/tests/test_utils.js @@ -386,10 +386,10 @@ range(40, 127).forEach((keyCode) => { * @param {Number} addTests */ var testKeyboard = function ($editable, assert, keyboardTests, addTests) { - var tests = _.compact(_.pluck(keyboardTests, 'test')); - var testNumber = _.compact(_.pluck(tests, 'start')).length + - _.compact(_.pluck(tests, 'content')).length + - _.compact(_.pluck(tests, 'check')).length + + var tests = _.compact(keyboardTests.map((k) => k.test)); + var testNumber = _.compact(tests.map((test) => test.start)).length + + _.compact(tests.map((test) => test.content)).length + + _.compact(tests.map((test) => test.check)).length + (addTests | 0); assert.expect(testNumber); diff --git a/addons/web_tour/static/src/tour_service/tour_utils.js b/addons/web_tour/static/src/tour_service/tour_utils.js index 2622e36de90..d261e1a766a 100644 --- a/addons/web_tour/static/src/tour_service/tour_utils.js +++ b/addons/web_tour/static/src/tour_service/tour_utils.js @@ -50,7 +50,7 @@ export function getFirstVisibleElement($elements) { } export function getJQueryElementFromSelector(selector) { - const iframeSplit = _.isString(selector) && selector.match(/(.*\biframe[^ ]*)(.*)/); + const iframeSplit = typeof selector === "string" && selector.match(/(.*\biframe[^ ]*)(.*)/); if (iframeSplit && iframeSplit[2]) { var $iframe = $(`${iframeSplit[1]}:not(.o_ignore_in_tour)`); if ($iframe.is('[is-ready="false"]')) { diff --git a/addons/web_unsplash/static/src/js/unsplash_beacon.js b/addons/web_unsplash/static/src/js/unsplash_beacon.js index 7154b2df14a..53600aacb48 100644 --- a/addons/web_unsplash/static/src/js/unsplash_beacon.js +++ b/addons/web_unsplash/static/src/js/unsplash_beacon.js @@ -10,7 +10,7 @@ publicWidget.registry.UnsplashBeacon = publicWidget.Widget.extend({ * @override */ start: function () { - var unsplashImages = _.map(this.$('img[src*="/unsplash/"]'), function (img) { + var unsplashImages = this.$('img[src*="/unsplash/"]').map((img) => { // get image id from URL (`http://www.domain.com:1234/unsplash/xYdf5feoI/lion.jpg` -> `xYdf5feoI`) return img.src.split('/unsplash/')[1].split('/')[0]; }); diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 2a54e5f21ce..f2d51fb68ec 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -5,6 +5,8 @@ */ import { loadJS } from "@web/core/assets"; +import { escape } from "@web/core/utils/strings"; +import { debounce } from "@web/core/utils/timing"; import Class from "web.Class"; import config from "web.config"; import core from "web.core"; @@ -229,7 +231,7 @@ var AnimationEffect = Class.extend(mixins.ParentedMixin, { this.play(e); clearTimeout(pauseTimer); - pauseTimer = _.delay((function () { + pauseTimer = setTimeout((() => { this.pause(); pauseTimer = null; }).bind(this), 2000); @@ -310,7 +312,7 @@ var AnimationEffect = Class.extend(mixins.ParentedMixin, { var animationState = this._getStateCallback(elapsedTime, this._newEvent); if (!this._newEvent && animationState !== undefined - && _.isEqual(animationState, this._animationLastState)) { + && JSON.stringify(animationState) === JSON.stringify(this._animationLastState)) { return; } this._animationLastState = animationState; @@ -451,7 +453,7 @@ registry.slider = publicWidget.Widget.extend({ this._computeHeights(); // Initialize carousel and pause if in edit mode. this.$el.carousel(this.editableMode ? 'pause' : undefined); - $(window).on('resize.slider', _.debounce(() => this._computeHeights(), 250)); + $(window).on('resize.slider', debounce(() => this._computeHeights(), 250)); return this._super.apply(this, arguments); }, /** @@ -524,7 +526,7 @@ registry.Parallax = Animation.extend({ */ start: function () { this._rebuild(); - $(window).on('resize.animation_parallax', _.debounce(this._rebuild.bind(this), 500)); + $(window).on('resize.animation_parallax', debounce(this._rebuild.bind(this), 500)); this.modalEl = this.$target[0].closest('.modal'); if (this.modalEl) { $(this.modalEl).on('shown.bs.modal.animation_parallax', () => { @@ -724,7 +726,7 @@ registry.mediaVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, { // the src is saved in the 'data-src' attribute or the // 'data-oe-expression' one (the latter is used as a workaround in 10.0 // system but should obviously be reviewed in master). - var src = _.escape(this.$el.data('oe-expression') || this.$el.data('src')); + var src = escape(this.$el.data('oe-expression') || this.$el.data('src')); // Validate the src to only accept supported domains we can trust var m = src.match(/^(?:https?:)?\/\/([^/?#]+)/); if (!m) { @@ -733,7 +735,7 @@ registry.mediaVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, { } var domain = m[1].replace(/^www\./, ''); var supportedDomains = ['youtu.be', 'youtube.com', 'youtube-nocookie.com', 'instagram.com', 'vine.co', 'player.vimeo.com', 'vimeo.com', 'dailymotion.com', 'player.youku.com', 'youku.com']; - if (!_.contains(supportedDomains, domain)) { + if (!supportedDomains.includes(domain)) { // Unsupported domain, don't inject iframe return; } @@ -923,7 +925,7 @@ registry.socialShare = publicWidget.Widget.extend({ 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ') + url, 'linkedin': 'https://www.linkedin.com/sharing/share-offsite/?url=' + url, }; - if (!_.contains(Object.keys(socialNetworks), social)) { + if (!Object.keys(socialNetworks).includes(social)) { return; } var wHeight = 500; @@ -1036,7 +1038,7 @@ registry.FullScreenHeight = publicWidget.Widget.extend({ // rules may alter the full-screen-height class behavior in some // cases (blog...). this._adaptSize(); - $(window).on('resize.FullScreenHeight', _.debounce(() => this._adaptSize(), 250)); + $(window).on('resize.FullScreenHeight', debounce(() => this._adaptSize(), 250)); } return this._super(...arguments); }, @@ -1162,7 +1164,7 @@ registry.BottomFixedElement = publicWidget.Widget.extend({ */ async start() { this.$scrollingElement = $().getScrollingElement(); - this.__hideBottomFixedElements = _.debounce(() => this._hideBottomFixedElements(), 100); + this.__hideBottomFixedElements = debounce(() => this._hideBottomFixedElements(), 100); this.$scrollingElement.on('scroll.bottom_fixed_element', this.__hideBottomFixedElements); $(window).on('resize.bottom_fixed_element', this.__hideBottomFixedElements); return this._super(...arguments); diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js index efc0b7d01db..316296edfa8 100644 --- a/addons/website/static/src/js/editor/snippets.editor.js +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -72,11 +72,9 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ */ _computeSnippetTemplates: function (html) { const $html = $(html); - const fontVariables = _.map($html.find('we-fontfamilypicker[data-variable]'), el => { - return el.dataset.variable; - }); + const toFind = $html.find("we-fontfamilypicker[data-variable]").toArray(); + const fontVariables = toFind.map((el) => el.dataset.variable); FontFamilyPickerUserValueWidget.prototype.fontVariables = fontVariables; - return this._super(...arguments); }, /** diff --git a/addons/website/static/src/js/editor/widget_link.js b/addons/website/static/src/js/editor/widget_link.js index 5ef1c818360..65b9ddb1d2b 100644 --- a/addons/website/static/src/js/editor/widget_link.js +++ b/addons/website/static/src/js/editor/widget_link.js @@ -2,6 +2,7 @@ import weWidgets from "wysiwyg.widgets"; import wUtils from "website.utils"; +import { debounce } from "@web/core/utils/timing"; weWidgets.LinkTools.include({ custom_events: Object.assign({}, weWidgets.LinkTools.prototype.custom_events || {}, { @@ -14,7 +15,7 @@ weWidgets.LinkTools.include({ */ init: function () { this._super.apply(this, arguments); - this._adaptPageAnchor = _.debounce(this._adaptPageAnchor, this.LINK_DEBOUNCE); + this._adaptPageAnchor = debounce(this._adaptPageAnchor, this.LINK_DEBOUNCE); }, /** * Allows the URL input to propose existing website pages. diff --git a/addons/website/static/src/js/editor/wysiwyg.js b/addons/website/static/src/js/editor/wysiwyg.js index fffbd3e8924..d85eb04993e 100644 --- a/addons/website/static/src/js/editor/wysiwyg.js +++ b/addons/website/static/src/js/editor/wysiwyg.js @@ -16,7 +16,7 @@ import socialMediaOptions from "@website/snippets/s_social_media/options"; * @returns {Promise} */ function toggleDropdown($toggles, show) { - return Promise.all(_.map($toggles, toggle => { + return Promise.all($($toggles).toArray().map(toggle => { // We must select the element via the iframe so that the event handlers // declared on the iframe are triggered. const $toggle = toggle.ownerDocument.defaultView.$(toggle); diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js index 0ec09344e8f..778c38d83c3 100644 --- a/addons/website/static/src/js/utils.js +++ b/addons/website/static/src/js/utils.js @@ -22,7 +22,7 @@ function loadAnchors(url, body) { resolve(); } }).then(function (response) { - const anchors = _.map($(response).find('[id][data-anchor=true]'), function (el) { + const anchors = $(response).find('[id][data-anchor=true]').toArray().map((el) => { return '#' + el.id; }); // Always suggest the top and the bottom of the page as internal link @@ -123,7 +123,7 @@ function autocompleteWithPages(self, $input, options) { * @param {jQuery} [$excluded] */ function onceAllImagesLoaded($element, $excluded) { - var defs = _.map($element.find('img').addBack('img'), function (img) { + var defs = $element.find('img').addBack('img').map(img => { if (img.complete || $excluded && ($excluded.is(img) || $excluded.has(img).length)) { return; // Already loaded } @@ -175,7 +175,7 @@ function prompt(options, _qweb) { text: options }; } - if (_.isUndefined(_qweb)) { + if (typeof _qweb === "undefined") { _qweb = 'website.prompt'; } options = Object.assign({ diff --git a/addons/website/static/src/snippets/s_chart/options.js b/addons/website/static/src/snippets/s_chart/options.js index 72f1855e14e..955d211d4fa 100644 --- a/addons/website/static/src/snippets/s_chart/options.js +++ b/addons/website/static/src/snippets/s_chart/options.js @@ -170,9 +170,13 @@ options.registry.InnerChart = options.Class.extend({ } else { // Find max value from each row/column data const datasets = JSON.parse(dataset.data).datasets || []; - const dataValue = _.flatten(datasets.map(el => el.data.map(data => { - return !isNaN(parseInt(data)) ? parseInt(data) : 0; - }))); + const dataValue = datasets + .map((el) => { + return el.data.map((data) => { + return !isNaN(parseInt(data)) ? parseInt(data) : 0; + }); + }) + .flat(); // When max value is not given and min value is greater than chart // data values if (minValue >= Math.max(...dataValue)) { diff --git a/addons/website/static/src/snippets/s_image_gallery/options.js b/addons/website/static/src/snippets/s_image_gallery/options.js index ecf0fac1325..71243d3a0ea 100644 --- a/addons/website/static/src/snippets/s_image_gallery/options.js +++ b/addons/website/static/src/snippets/s_image_gallery/options.js @@ -278,7 +278,7 @@ options.registry.gallery = options.Class.extend({ */ slideshow: function () { const imageEls = this._getImages(); - const images = _.map(imageEls, img => ({ + const images = imageEls.map((img) => ({ // Use getAttribute to get the attribute value otherwise .src // returns the absolute url. src: img.getAttribute('src'), diff --git a/addons/website/static/src/snippets/s_searchbar/000.js b/addons/website/static/src/snippets/s_searchbar/000.js index 824ec0c1ad1..cadc904d725 100644 --- a/addons/website/static/src/snippets/s_searchbar/000.js +++ b/addons/website/static/src/snippets/s_searchbar/000.js @@ -5,6 +5,7 @@ import publicWidget from 'web.public.widget'; import {qweb} from 'web.core'; import {Markup} from 'web.utils'; +import { debounce } from '@web/core/utils/timing'; publicWidget.registry.searchBar = publicWidget.Widget.extend({ selector: '.o_searchbar_form', @@ -24,8 +25,8 @@ publicWidget.registry.searchBar = publicWidget.Widget.extend({ this._dp = new concurrency.DropPrevious(); - this._onInput = _.debounce(this._onInput, 400); - this._onFocusOut = _.debounce(this._onFocusOut, 100); + this._onInput = debounce(this._onInput, 400); + this._onFocusOut = debounce(this._onFocusOut, 100); }, /** * @override diff --git a/addons/website/static/src/snippets/s_website_form/000.js b/addons/website/static/src/snippets/s_website_form/000.js index 76e30162fc5..14ab98a1528 100644 --- a/addons/website/static/src/snippets/s_website_form/000.js +++ b/addons/website/static/src/snippets/s_website_form/000.js @@ -8,7 +8,8 @@ import publicWidget from "web.public.widget"; import dom from "web.dom"; import concurrency from "web.concurrency"; -import { sprintf } from "@web/core/utils/strings"; + import { sprintf } from "@web/core/utils/strings"; + import { debounce } from "@web/core/utils/timing"; var _t = core._t; var qweb = core.qweb; @@ -91,7 +92,7 @@ import { sprintf } from "@web/core/utils/strings"; } this._updateFieldsVisibility(); - this._onFieldInputDebounced = _.debounce(this._onFieldInput.bind(this), 400); + this._onFieldInputDebounced = debounce(this._onFieldInput.bind(this), 400); this.$el.on('input.s_website_form', '.s_website_form_field', this._onFieldInputDebounced); // Initialize datetimepickers @@ -459,7 +460,7 @@ import { sprintf } from "@web/core/utils/strings"; if (invalid_inputs.length || error_fields[field_name]) { $field.addClass('o_has_error'); $controls.addClass('is-invalid'); - if (_.isString(error_fields[field_name])) { + if (typeof error_fields[field_name] === "string") { $field.popover({content: error_fields[field_name], trigger: 'hover', container: 'body', placement: 'top'}); // update error message and show it. $field.data("bs.popover").config.content = error_fields[field_name]; diff --git a/addons/website/static/tests/tours/website_form_editor_frontend.js b/addons/website/static/tests/tours/website_form_editor_frontend.js index e36b12062b4..bd6f62242d0 100644 --- a/addons/website/static/tests/tours/website_form_editor_frontend.js +++ b/addons/website/static/tests/tours/website_form_editor_frontend.js @@ -175,7 +175,7 @@ registry.category("web_tour.tours").add("website_form_editor_tour_results", { $('body').append('
    '); } }; - mailDef.then(_.bind(success, this, 'mail_mail')); + mailDef.then(success.bind(this, 'mail_mail')); } }, { diff --git a/addons/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js index ce44b3e20f1..aea1cf036f8 100644 --- a/addons/website_event/static/src/js/website_event.js +++ b/addons/website_event/static/src/js/website_event.js @@ -44,7 +44,7 @@ var EventRegistrationForm = Widget.extend({ $('#registration_form select').each(function () { post[$(this).attr('name')] = $(this).val(); }); - var tickets_ordered = _.some(_.map(post, function (value, key) { return parseInt(value); })); + var tickets_ordered = _.some(Object.values(post).map((value) => parseInt(value))); if (!tickets_ordered) { $('
    ') .text(_t('Please select at least one ticket.')) diff --git a/addons/website_event_exhibitor/static/src/js/event_exhibitor_connect.js b/addons/website_event_exhibitor/static/src/js/event_exhibitor_connect.js index da0a7e40d89..201cfd1bda6 100644 --- a/addons/website_event_exhibitor/static/src/js/event_exhibitor_connect.js +++ b/addons/website_event_exhibitor/static/src/js/event_exhibitor_connect.js @@ -1,5 +1,6 @@ /** @odoo-module alias=website_event_exhibitor.event_exhibitor_connect **/ +import { debounce } from "@web/core/utils/timing"; import Dialog from "web.Dialog"; import publicWidget from "web.public.widget"; import {Markup} from "web.utils"; @@ -63,7 +64,7 @@ publicWidget.registry.eventExhibitorConnect = publicWidget.Widget.extend({ */ init: function () { this._super(...arguments); - this._onConnectClick = _.debounce(this._onConnectClick, 500, true); + this._onConnectClick = debounce(this._onConnectClick, 500, true); }, /** diff --git a/addons/website_event_track/static/src/js/event_track_reminder.js b/addons/website_event_track/static/src/js/event_track_reminder.js index c8ebf8c832f..f095cf44985 100644 --- a/addons/website_event_track/static/src/js/event_track_reminder.js +++ b/addons/website_event_track/static/src/js/event_track_reminder.js @@ -1,6 +1,7 @@ /** @odoo-module alias=website_event_track.website_event_track_reminder **/ import { sprintf } from "@web/core/utils/strings"; +import { debounce } from "@web/core/utils/timing"; import core from "web.core"; var _t = core._t; import publicWidget from "web.public.widget"; @@ -16,7 +17,7 @@ publicWidget.registry.websiteEventTrackReminder = publicWidget.Widget.extend({ */ init: function () { this._super.apply(this, arguments); - this._onReminderToggleClick = _.debounce(this._onReminderToggleClick, 500, true); + this._onReminderToggleClick = debounce(this._onReminderToggleClick, 500, true); }, //-------------------------------------------------------------------------- diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index e2447d4a416..023b81e9acb 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -8,6 +8,7 @@ import wysiwygLoader from "web_editor.loader"; import publicWidget from "web.public.widget"; import { Markup } from "web.utils"; import session from "web.session"; +import { escape } from "@web/core/utils/strings"; var qweb = core.qweb; var _t = core._t; @@ -76,9 +77,9 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ }, formatResult: function (term) { if (term.isNew) { - return 'New ' + _.escape(term.text); + return 'New ' + escape(term.text); } else { - return _.escape(term.text); + return escape(term.text); } }, ajax: { @@ -616,9 +617,7 @@ publicWidget.registry.websiteForumSpam = publicWidget.Widget.extend({ _onMarkSpamClick: function (ev) { var key = this.$('.modal .tab-pane.active').data('key'); var $inputs = this.$('.modal .tab-pane.active input.form-check-input:checked'); - var values = _.map($inputs, function (o) { - return parseInt(o.value); - }); + var values = $inputs.map((o) => parseInt(o.value)); return this._rpc({model: 'forum.post', method: 'mark_as_offensive_batch', args: [this.spamIDs, key, values], diff --git a/addons/website_links/static/src/js/website_links.js b/addons/website_links/static/src/js/website_links.js index e349a5475ee..3d0c0b8d922 100644 --- a/addons/website_links/static/src/js/website_links.js +++ b/addons/website_links/static/src/js/website_links.js @@ -34,7 +34,7 @@ var SelectBox = publicWidget.Widget.extend({ fields: ['id', 'name'], }, }).then(function (result) { - self.objects = _.map(result, function (val) { + self.objects = result.map((val) => { return {id: val.id, text: val.name}; }); })); @@ -103,7 +103,7 @@ var SelectBox = publicWidget.Widget.extend({ * @param {Object} ev */ _onChange: function (ev) { - if (!ev.added || !_.isString(ev.added.id)) { + if (!ev.added || typeof ev.added.id !== "string") { return; } this._createObject(ev.added.id); @@ -327,7 +327,9 @@ var RecentLinks = publicWidget.Widget.extend({ * @private */ removeLinks: function () { - _.invoke(this.getChildren(), 'destroy'); + this.getChildren().forEach((child) => { + child.destroy(); + }); }, /** * @private diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 6659c0dae0b..643edb77c30 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -12,6 +12,7 @@ import dom from "web.dom"; import { cartesian } from "@web/core/utils/arrays"; import { ComponentWrapper } from "web.OwlCompatibility"; import { ProductImageViewerWrapper } from "@website_sale/js/components/website_sale_image_viewer"; +import { debounce } from "@web/core/utils/timing"; publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, cartHandlerMixin, { selector: '.oe_website_sale', @@ -49,8 +50,8 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, car init: function () { this._super.apply(this, arguments); - this._changeCartQuantity = _.debounce(this._changeCartQuantity.bind(this), 500); - this._changeCountry = _.debounce(this._changeCountry.bind(this), 500); + this._changeCartQuantity = debounce(this._changeCartQuantity.bind(this), 500); + this._changeCountry = debounce(this._changeCountry.bind(this), 500); this.isWebsite = true; this.filmStripStartX = 0; @@ -181,9 +182,7 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, car */ _setUrlHash: function ($parent) { var $attributes = $parent.find('input.js_variant_change:checked, select.js_variant_change option:selected'); - var attributeIds = _.map($attributes, function (elem) { - return $(elem).data('value_id'); - }); + var attributeIds = $attributes.toArray().map((elem) => $(elem).data("value_id")); window.location.replace('#attr=' + attributeIds.join(',')); }, /** diff --git a/addons/website_sale/static/src/js/website_sale_payment.js b/addons/website_sale/static/src/js/website_sale_payment.js index 74b52ef9bd4..af1e7522016 100644 --- a/addons/website_sale/static/src/js/website_sale_payment.js +++ b/addons/website_sale/static/src/js/website_sale_payment.js @@ -1,6 +1,7 @@ /** @odoo-module **/ - import checkoutForm from "payment.checkout_form"; + import { debounce } from "@web/core/utils/timing"; +import checkoutForm from "payment.checkout_form"; import publicWidget from "web.public.widget"; const websiteSalePaymentMixin = { @@ -9,7 +10,7 @@ * @override */ init: function () { - this._onClickTCCheckbox = _.debounce(this._onClickTCCheckbox, 100, true); + this._onClickTCCheckbox = debounce(this._onClickTCCheckbox, 100, true); this._super(...arguments); }, @@ -60,9 +61,7 @@ * @return {boolean} Whether the submit button can be enabled */ _isButtonReady: function () { - const disabledReasonFound = _.contains( - this.$submitButton.data('disabled_reasons'), true - ); + const disabledReasonFound = this.$submitButton.data("disabled_reasons"); return !disabledReasonFound && this._super(); }, @@ -115,10 +114,7 @@ */ _onClickTCCheckbox: function () { this._adaptConfirmButton(); - - const disabledReasonFound = _.contains( - this.$submitButton.data('disabled_reasons'), true - ); + const disabledReasonFound = this.$submitButton.data("disabled_reasons"); this.$submitButton.prop('disabled', disabledReasonFound); }, })); diff --git a/addons/website_sale/static/src/js/website_sale_recently_viewed.js b/addons/website_sale/static/src/js/website_sale_recently_viewed.js index e68507e0846..37cddd3376a 100644 --- a/addons/website_sale/static/src/js/website_sale_recently_viewed.js +++ b/addons/website_sale/static/src/js/website_sale_recently_viewed.js @@ -1,5 +1,6 @@ /** @odoo-module **/ +import { debounce } from "@web/core/utils/timing"; import publicWidget from "web.public.widget"; import {getCookie, setCookie} from "web.utils.cookies"; @@ -15,7 +16,7 @@ publicWidget.registry.productsRecentlyViewedUpdate = publicWidget.Widget.extend( */ init: function () { this._super.apply(this, arguments); - this._onProductChange = _.debounce(this._onProductChange, this.debounceValue); + this._onProductChange = debounce(this._onProductChange, this.debounceValue); }, //-------------------------------------------------------------------------- diff --git a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js index 12443567995..0052371181a 100644 --- a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js +++ b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js @@ -68,7 +68,7 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, { self._removeFromComparelist(ev); self.guard.exec(function() { const newLink = '/shop/compare?products=' + encodeURIComponent(self.comparelist_product_ids); - window.location.href = _.isEmpty(self.comparelist_product_ids) ? '/shop' : newLink; + window.location.href = Object.keys(self.comparelist_product_ids || {}).length === 0 ? '/shop' : newLink; }); }); @@ -172,7 +172,7 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, { _addNewProductsImpl: function (product_id) { var self = this; $('.o_product_feature_panel').addClass('d-md-block'); - if (!_.contains(self.comparelist_product_ids, product_id)) { + if (!self.comparelist_product_ids.includes(product_id)) { self.comparelist_product_ids.push(product_id); if (_.has(self.product_data, product_id)){ self._updateContent(); @@ -233,7 +233,7 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, { _updateComparelistView: function () { this.$('.o_product_circle').text(this.comparelist_product_ids.length); this.$('.o_comparelist_button').removeClass('d-md-block'); - if (_.isEmpty(this.comparelist_product_ids)) { + if (Object.keys(this.comparelist_product_ids || {}).length === 0) { $('.o_product_feature_panel').removeClass('d-md-block'); } else { $('.o_product_feature_panel').addClass('d-md-block'); diff --git a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js index 51038e9b7d7..c0099db6361 100644 --- a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js +++ b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js @@ -96,7 +96,7 @@ publicWidget.registry.ProductWishlist = publicWidget.Widget.extend(VariantMixin, productReady.then(function (productId) { productId = parseInt(productId, 10); - if (productId && !_.contains(self.wishlistProductIDs, productId)) { + if (productId && !self.wishlistProductIDs.includes(productId)) { return self._rpc({ route: '/shop/wishlist/add', params: { @@ -250,7 +250,7 @@ publicWidget.registry.ProductWishlist = publicWidget.Widget.extend(VariantMixin, var $input = $(ev.target); var $parent = $input.closest('.js_product'); var $el = $parent.find("[data-action='o_wishlist']"); - if (!_.contains(this.wishlistProductIDs, parseInt($input.val(), 10))) { + if (!this.wishlistProductIDs.includes(parseInt($input.val(), 10))) { $el.prop("disabled", false).removeClass('disabled').removeAttr('disabled'); } else { $el.prop("disabled", true).addClass('disabled').attr('disabled', 'disabled'); @@ -265,7 +265,7 @@ publicWidget.registry.ProductWishlist = publicWidget.Widget.extend(VariantMixin, var productID = ev.currentTarget.value; var $el = $(ev.target).closest('.js_add_cart_variants').find("[data-action='o_wishlist']"); - if (!_.contains(this.wishlistProductIDs, parseInt(productID, 10))) { + if (!this.wishlistProductIDs.includes(parseInt(productID, 10))) { $el.prop("disabled", false).removeClass('disabled').removeAttr('disabled'); } else { $el.prop("disabled", true).addClass('disabled').attr('disabled', 'disabled'); diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js index 50ca540a39e..7ceb780e685 100644 --- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -450,9 +450,7 @@ clipboard.on('success', function () { clipboard.destroy(); $clipboardBtn.tooltip('show'); - _.delay(function () { - $clipboardBtn.tooltip("hide"); - }, 800); + setTimeout(() => $clipboardBtn.tooltip("hide"), 800); }); clipboard.on('error', function (e) { clipboard.destroy(); @@ -619,7 +617,7 @@ // technical settings for the Fullscreen to work var autoSetDone = false; if (!slideData.hasQuestion) { - if (_.contains(['infographic', 'document', 'article'], slideData.category)) { + if (['infographic', 'document', 'article'].includes(slideData.category)) { autoSetDone = true; // images, documents (local + external) and articles are marked as completed when opened } else if (slideData.category === 'video' && slideData.videoSourceType === 'google_drive') { autoSetDone = true; // google drive videos do not benefit from the YouTube integration and are marked as completed when opened @@ -669,7 +667,7 @@ } // render slide content - if (_.contains(['document', 'infographic'], slide.category)) { + if (['document', 'infographic'].includes(slide.category)) { $content.html(QWeb.render('website.slides.fullscreen.content', {widget: this})); } else if (slide.category === 'video' && slide.videoSourceType === 'youtube') { this.videoPlayer = new VideoPlayerYouTube(this, slide); diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js index 6d503a16e6c..538df35bf3a 100644 --- a/addons/website_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -280,7 +280,7 @@ $question.find('a.o_wslides_quiz_answer').each(function () { var $answer = $(this); if (!$answer.find('input[type=radio]')[0].checked && - _.contains(self.slide.sessionAnswers, $answer.data('answerId'))) { + self.slide.sessionAnswers.includes($answer.data('answerId'))) { $answer.find('input[type=radio]').prop('checked', true); } }); diff --git a/addons/website_slides/static/src/js/slides_course_quiz_finish.js b/addons/website_slides/static/src/js/slides_course_quiz_finish.js index a6384bb50bb..0c048074aeb 100644 --- a/addons/website_slides/static/src/js/slides_course_quiz_finish.js +++ b/addons/website_slides/static/src/js/slides_course_quiz_finish.js @@ -100,17 +100,17 @@ var SlideQuizFinishModal = Dialog.extend({ if (this.quiz.rankProgress.level_up) { this.$('.o_wslides_quiz_modal_title').text(_t('Level up!')); $progressBar.css('width', '100%'); - _.delay(function () { + setTimeout(() => { self.$('.o_wslides_quiz_modal_rank_lower_bound') .text(self.quiz.rankProgress.new_rank.lower_bound); self.$('.o_wslides_quiz_modal_rank_upper_bound') .text(self.quiz.rankProgress.new_rank.upper_bound || ""); // we need to use _.delay to force DOM re-rendering between 0 and new percentage - _.delay(function () { + setTimeout(() => { $progressBar.addClass('no-transition').width('0%'); }, 1); - _.delay(function () { + setTimeout(() => { $progressBar .removeClass('no-transition') .width(self.quiz.rankProgress.new_rank.progress + '%'); @@ -128,15 +128,15 @@ var SlideQuizFinishModal = Dialog.extend({ */ _animateText: function () { var self = this; - _.delay(function () { + setTimeout(function () { self.$('h4.o_wslides_quiz_modal_xp_gained').addClass('show in'); self.$('.o_wslides_quiz_modal_dismiss').removeClass('d-none'); }, 800); if (this.quiz.rankProgress.level_up) { - _.delay(function () { + setTimeout(function () { self.$('.o_wslides_quiz_modal_rank_motivational').addClass('fade'); - _.delay(function () { + setTimeout(function () { self.$('.o_wslides_quiz_modal_rank_motivational').html( self.quiz.rankProgress.last_rank ? self.quiz.rankProgress.description : diff --git a/addons/website_slides/static/src/js/slides_embed.js b/addons/website_slides/static/src/js/slides_embed.js index 0e83b41feb2..5a6e5a4fc52 100644 --- a/addons/website_slides/static/src/js/slides_embed.js +++ b/addons/website_slides/static/src/js/slides_embed.js @@ -1,4 +1,5 @@ /* global PDFSlidesViewer */ + /** * This is a minimal version of the PDFViewer widget. * It is NOT use in the website_slides module, but it is called when embedding @@ -7,6 +8,14 @@ */ $(function () { + function debounce(func, timeout = 300){ + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { func.apply(this, args); }, timeout); + }; + } + if ($('#PDFViewer') && $('#PDFViewerCanvas')) { // check if presentation only var MIN_ZOOM=1, MAX_ZOOM=10, ZOOM_INCREMENT=.5; @@ -177,7 +186,7 @@ $(function () { return false; } }); - $(window).on('resize', _.debounce(function() { + $(window).on("resize", debounce(() => { embeddedViewer.on_resize(); }, 500)); diff --git a/addons/website_slides/static/src/js/slides_share.js b/addons/website_slides/static/src/js/slides_share.js index d961accd446..cdb64ba6ca6 100644 --- a/addons/website_slides/static/src/js/slides_share.js +++ b/addons/website_slides/static/src/js/slides_share.js @@ -121,9 +121,7 @@ publicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({ clipboard.on('success', function () { clipboard.destroy(); $clipboardBtn.tooltip('show'); - _.delay(function () { - $clipboardBtn.tooltip("hide"); - }, 800); + setTimeout(() => $clipboardBtn.tooltip("hide"), 800); }); clipboard.on('error', function (e) { console.log(e); @@ -152,7 +150,7 @@ publicWidget.registry.websiteSlidesEmbedShare = publicWidget.Widget.extend({ clipboard.on('success', function () { clipboard.destroy(); $clipboardBtn.tooltip('show'); - _.delay(function () { + setTimeout(function () { $clipboardBtn.tooltip("hide"); }, 800); }); diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index d1df29820b6..247d7b5f5a2 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -147,7 +147,7 @@ var SlideUploadDialog = Dialog.extend({ _formValidateGetValues: async function (forcePublished) { var slideCategory = 'document'; // default slide_category (for article for instance) - if (_.contains(this.slide_category_data), this.get('state')) { + if (Object.keys(this.slide_category_data).includes(this.get("state"))) { slideCategory = this.get('state'); } diff --git a/addons/website_twitter/static/src/js/website.twitter.animation.js b/addons/website_twitter/static/src/js/website.twitter.animation.js index 6cc6b637502..c0d99e3a8e1 100644 --- a/addons/website_twitter/static/src/js/website.twitter.animation.js +++ b/addons/website_twitter/static/src/js/website.twitter.animation.js @@ -3,6 +3,7 @@ import core from "web.core"; import {Markup} from "web.utils"; import publicWidget from "web.public.widget"; +import { escape } from "@web/core/utils/strings"; var qweb = core.qweb; @@ -31,13 +32,13 @@ publicWidget.registry.twitter = publicWidget.Widget.extend({ return; } - if (_.isEmpty(data)) { + if (Object.keys(data || {}).length === 0) { return; } - var tweets = _.map(data, function (tweet) { + var tweets = data.map((tweet) => { // Parse tweet date - if (_.isEmpty(tweet.created_at)) { + if (Object.keys(tweet.created_at || {}).length === 0) { tweet.created_at = ''; } else { var v = tweet.created_at.split(' '); @@ -46,7 +47,7 @@ publicWidget.registry.twitter = publicWidget.Widget.extend({ } // Parse tweet text - tweet.text = Markup(_.escape(tweet.text) + tweet.text = Markup(escape(tweet.text) .replace( /[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&~\?\/.=]+/g, function (url) {