From c4079780beac3c3420452a10e76b7526c824c99b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2020 11:52:54 -0400 Subject: [PATCH] REFACTOR: Remove `_.merge` --- .../discourse-common/addon/lib/object.js | 37 +++++++++++++++++++ .../discourse/app/controllers/topic.js | 3 +- .../javascripts/discourse/app/lib/search.js | 5 ++- .../discourse/app/lib/utilities.js | 3 +- .../discourse/app/mixins/upload.js | 5 ++- .../discourse/app/models/nav-item.js | 5 ++- .../discourse/app/models/post-stream.js | 5 ++- .../javascripts/discourse/app/models/topic.js | 3 +- .../discourse/app/routes/invites-show.js | 3 +- .../discourse/app/routes/password-reset.js | 3 +- .../discourse/app/widgets/topic-timeline.js | 3 +- .../discourse/app/widgets/widget.js | 3 +- .../pretty-text/addon/pretty-text.js | 3 +- lib/pretty_text.rb | 1 + test/javascripts/lib/pretty-text-test.js | 3 +- test/javascripts/models/post-test.js | 3 +- 16 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/discourse-common/addon/lib/object.js diff --git a/app/assets/javascripts/discourse-common/addon/lib/object.js b/app/assets/javascripts/discourse-common/addon/lib/object.js new file mode 100644 index 00000000000..c9f0e40ffa3 --- /dev/null +++ b/app/assets/javascripts/discourse-common/addon/lib/object.js @@ -0,0 +1,37 @@ +// a fairly simple deep merge based on: https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6 +export function merge(...objects) { + const isObject = obj => obj && typeof obj === "object"; + + function deepMergeInner(target, source) { + Object.keys(source).forEach(key => { + const targetValue = target[key]; + const sourceValue = source[key]; + + if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { + target[key] = targetValue.concat(sourceValue); + } else if (isObject(targetValue) && isObject(sourceValue)) { + target[key] = deepMergeInner( + Object.assign({}, targetValue), + sourceValue + ); + } else { + target[key] = sourceValue; + } + }); + + return target; + } + + if (objects.some(object => object && !isObject(object))) { + throw new Error('deepMerge: all values should be of type "object"'); + } + + const target = objects.shift(); + let source; + + while ((source = objects.shift())) { + deepMergeInner(target, source || {}); + } + + return target; +} diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index 5aaeb4aa19c..be9b7e9598b 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -26,6 +26,7 @@ import { escapeExpression } from "discourse/lib/utilities"; import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark"; import { inject as service } from "@ember/service"; import bootbox from "bootbox"; +import { merge } from "discourse-common/lib/object"; let customPostMessageCallbacks = {}; @@ -249,7 +250,7 @@ export default Controller.extend(bufferedProperty("model"), { this.set("loadingPostIds", true); return ajax(url, { - data: _.merge( + data: merge( { post_number: post.get("post_number") }, postStream.get("streamFilters") ) diff --git a/app/assets/javascripts/discourse/app/lib/search.js b/app/assets/javascripts/discourse/app/lib/search.js index 02b7843d8de..c91677bff41 100644 --- a/app/assets/javascripts/discourse/app/lib/search.js +++ b/app/assets/javascripts/discourse/app/lib/search.js @@ -13,6 +13,7 @@ import User from "discourse/models/user"; import Post from "discourse/models/post"; import Topic from "discourse/models/topic"; import { escapeExpression } from "discourse/lib/utilities"; +import { merge } from "discourse-common/lib/object"; export function translateResults(results, opts) { opts = opts || {}; @@ -206,7 +207,7 @@ export function applySearchAutocomplete( }; $input.autocomplete( - _.merge( + merge( { template: findRawTemplate("category-tag-autocomplete"), key: "#", @@ -226,7 +227,7 @@ export function applySearchAutocomplete( if (siteSettings.enable_mentions) { $input.autocomplete( - _.merge( + merge( { template: findRawTemplate("user-selector-autocomplete"), key: "@", diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index 965d7092fa9..946e12f3a5f 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -4,6 +4,7 @@ import toMarkdown from "discourse/lib/to-markdown"; import Handlebars from "handlebars"; import { default as getURL, getURLWithCDN } from "discourse-common/lib/get-url"; import { helperContext } from "discourse-common/lib/helpers"; +import { merge } from "discourse-common/lib/object"; let _defaultHomepage; @@ -85,7 +86,7 @@ export function avatarImg(options, customGetURL) { export function tinyAvatar(avatarTemplate, options) { return avatarImg( - _.merge({ avatarTemplate: avatarTemplate, size: "tiny" }, options) + merge({ avatarTemplate: avatarTemplate, size: "tiny" }, options) ); } diff --git a/app/assets/javascripts/discourse/app/mixins/upload.js b/app/assets/javascripts/discourse/app/mixins/upload.js index 69b36bf836e..2846ce38953 100644 --- a/app/assets/javascripts/discourse/app/mixins/upload.js +++ b/app/assets/javascripts/discourse/app/mixins/upload.js @@ -8,6 +8,7 @@ import getUrl from "discourse-common/lib/get-url"; import { on } from "@ember/object/evented"; import Mixin from "@ember/object/mixin"; import bootbox from "bootbox"; +import { merge } from "discourse-common/lib/object"; export default Mixin.create({ uploading: false, @@ -56,7 +57,7 @@ export default Mixin.create({ }); $upload.fileupload( - _.merge( + merge( { url: this.calculateUploadUrl(), dataType: "json", @@ -82,7 +83,7 @@ export default Mixin.create({ }); $upload.on("fileuploadsubmit", (e, data) => { - const opts = _.merge( + const opts = merge( { bypassNewUserRestriction: true, user: this.currentUser, diff --git a/app/assets/javascripts/discourse/app/models/nav-item.js b/app/assets/javascripts/discourse/app/models/nav-item.js index 6597ae955ba..8c8bb5b9592 100644 --- a/app/assets/javascripts/discourse/app/models/nav-item.js +++ b/app/assets/javascripts/discourse/app/models/nav-item.js @@ -9,6 +9,7 @@ import deprecated from "discourse-common/lib/deprecated"; import Site from "discourse/models/site"; import User from "discourse/models/user"; import { getOwner } from "discourse-common/lib/get-owner"; +import { merge } from "discourse-common/lib/object"; const NavItem = EmberObject.extend({ @discourseComputed("name") @@ -180,7 +181,7 @@ NavItem.reopenClass({ args.noSubcategories = true; } NavItem.extraArgsCallbacks.forEach(cb => - _.merge(args, cb.call(this, filterType, opts)) + merge(args, cb.call(this, filterType, opts)) ); let store = getOwner(this).lookup("service:store"); @@ -222,7 +223,7 @@ NavItem.reopenClass({ }; const extraItems = NavItem.extraNavItemDescriptors - .map(descriptor => ExtraNavItem.create(_.merge({}, context, descriptor))) + .map(descriptor => ExtraNavItem.create(merge({}, context, descriptor))) .filter(item => { if (!item.customFilter) return true; return item.customFilter(category, args); diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js index 4a9a2458854..f2036ff77b2 100644 --- a/app/assets/javascripts/discourse/app/models/post-stream.js +++ b/app/assets/javascripts/discourse/app/models/post-stream.js @@ -10,6 +10,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { loadTopicView } from "discourse/models/topic"; import { Promise } from "rsvp"; import User from "discourse/models/user"; +import { merge } from "discourse-common/lib/object"; export default RestModel.extend({ _identityMap: null, @@ -274,7 +275,7 @@ export default RestModel.extend({ this.set("loadingFilter", true); this.set("loadingNearPost", opts.nearPost); - opts = _.merge(opts, this.streamFilters); + opts = merge(opts, this.streamFilters); // Request a topicView return loadTopicView(topic, opts) @@ -943,7 +944,7 @@ export default RestModel.extend({ include_suggested: includeSuggested }; - data = _.merge(data, this.streamFilters); + data = merge(data, this.streamFilters); const store = this.store; return ajax(url, { data }).then(result => { diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index 5cbaf1449fb..56d1cce2046 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -23,9 +23,10 @@ import { Promise } from "rsvp"; import Site from "discourse/models/site"; import User from "discourse/models/user"; import bootbox from "bootbox"; +import { merge } from "discourse-common/lib/object"; export function loadTopicView(topic, args) { - const data = _.merge({}, args); + const data = merge({}, args); const url = `${getURL("/t/")}${topic.id}`; const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json"; diff --git a/app/assets/javascripts/discourse/app/routes/invites-show.js b/app/assets/javascripts/discourse/app/routes/invites-show.js index c236a9a5db4..ceee8c0030f 100644 --- a/app/assets/javascripts/discourse/app/routes/invites-show.js +++ b/app/assets/javascripts/discourse/app/routes/invites-show.js @@ -1,6 +1,7 @@ import I18n from "I18n"; import DiscourseRoute from "discourse/routes/discourse"; import PreloadStore from "discourse/lib/preload-store"; +import { merge } from "discourse-common/lib/object"; export default DiscourseRoute.extend({ titleToken() { @@ -10,7 +11,7 @@ export default DiscourseRoute.extend({ model(params) { if (PreloadStore.get("invite_info")) { return PreloadStore.getAndRemove("invite_info").then(json => - _.merge(params, json) + merge(params, json) ); } else { return {}; diff --git a/app/assets/javascripts/discourse/app/routes/password-reset.js b/app/assets/javascripts/discourse/app/routes/password-reset.js index f637715f50d..225e17170c8 100644 --- a/app/assets/javascripts/discourse/app/routes/password-reset.js +++ b/app/assets/javascripts/discourse/app/routes/password-reset.js @@ -3,6 +3,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import PreloadStore from "discourse/lib/preload-store"; import { ajax } from "discourse/lib/ajax"; import { userPath } from "discourse/lib/url"; +import { merge } from "discourse-common/lib/object"; export default DiscourseRoute.extend({ titleToken() { @@ -12,7 +13,7 @@ export default DiscourseRoute.extend({ model(params) { if (PreloadStore.get("password_reset")) { return PreloadStore.getAndRemove("password_reset").then(json => - _.merge(params, json) + merge(params, json) ); } }, diff --git a/app/assets/javascripts/discourse/app/widgets/topic-timeline.js b/app/assets/javascripts/discourse/app/widgets/topic-timeline.js index 32f9aa2617c..088e8a7a181 100644 --- a/app/assets/javascripts/discourse/app/widgets/topic-timeline.js +++ b/app/assets/javascripts/discourse/app/widgets/topic-timeline.js @@ -8,6 +8,7 @@ import { iconNode } from "discourse-common/lib/icon-library"; import RawHtml from "discourse/widgets/raw-html"; import renderTags from "discourse/lib/render-tags"; import renderTopicFeaturedLink from "discourse/lib/render-topic-featured-link"; +import { merge } from "discourse-common/lib/object"; const SCROLLER_HEIGHT = 50; const LAST_READ_HEIGHT = 20; @@ -240,7 +241,7 @@ createWidget("timeline-scrollarea", { this.attach("timeline-padding", { height: before }), this.attach( "timeline-scroller", - _.merge(position, { + merge(position, { showDockedButton: !attrs.mobileView && hasBackPosition && !showButton, fullScreen: attrs.fullScreen }) diff --git a/app/assets/javascripts/discourse/app/widgets/widget.js b/app/assets/javascripts/discourse/app/widgets/widget.js index 4a2f77022fd..e5af7a68a7c 100644 --- a/app/assets/javascripts/discourse/app/widgets/widget.js +++ b/app/assets/javascripts/discourse/app/widgets/widget.js @@ -22,6 +22,7 @@ import DecoratorHelper from "discourse/widgets/decorator-helper"; import { Promise } from "rsvp"; import { isProduction } from "discourse-common/config/environment"; import { get } from "@ember/object"; +import { merge } from "discourse-common/lib/object"; const _registry = {}; @@ -185,7 +186,7 @@ export default class Widget { // Sometimes we pass state down from the parent if (this.mergeState) { - this.state = _.merge(this.state, this.mergeState); + this.state = merge(this.state, this.mergeState); } if (prev) { diff --git a/app/assets/javascripts/pretty-text/addon/pretty-text.js b/app/assets/javascripts/pretty-text/addon/pretty-text.js index d24fb4af6f0..8114a4471cb 100644 --- a/app/assets/javascripts/pretty-text/addon/pretty-text.js +++ b/app/assets/javascripts/pretty-text/addon/pretty-text.js @@ -2,6 +2,7 @@ import { cook as cookIt, setup as setupIt } from "pretty-text/engines/discourse-markdown-it"; +import { merge } from "discourse-common/lib/object"; export function registerOption() { // TODO next major version deprecate this @@ -48,7 +49,7 @@ export function buildOptions(state) { }; if (state.features) { - features = _.merge(features, state.features); + features = merge(features, state.features); } const options = { diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 03f4dcb4c30..46e3bc58c7e 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -87,6 +87,7 @@ module PrettyText root_path = "#{Rails.root}/app/assets/javascripts/" apply_es6_file(ctx, root_path, "discourse-common/addon/lib/get-url") + apply_es6_file(ctx, root_path, "discourse-common/addon/lib/object") apply_es6_file(ctx, root_path, "discourse/app/lib/to-markdown") apply_es6_file(ctx, root_path, "discourse/app/lib/utilities") diff --git a/test/javascripts/lib/pretty-text-test.js b/test/javascripts/lib/pretty-text-test.js index 9620b0d27d3..71fbdbe7243 100644 --- a/test/javascripts/lib/pretty-text-test.js +++ b/test/javascripts/lib/pretty-text-test.js @@ -8,6 +8,7 @@ import { } from "pretty-text/inline-oneboxer"; import { extractDataAttribute } from "pretty-text/engines/discourse-markdown-it"; import { registerEmoji } from "pretty-text/emoji"; +import { merge } from "discourse-common/lib/object"; QUnit.module("lib:pretty-text"); @@ -38,7 +39,7 @@ QUnit.assert.cooked = function(input, expected, message) { }; QUnit.assert.cookedOptions = function(input, opts, expected, message) { - const merged = _.merge({}, rawOpts, opts); + const merged = merge({}, rawOpts, opts); const actual = new PrettyText(buildOptions(merged)).cook(input); this.pushResult({ result: actual === expected, diff --git a/test/javascripts/models/post-test.js b/test/javascripts/models/post-test.js index 63905f69edb..1f62cafbe54 100644 --- a/test/javascripts/models/post-test.js +++ b/test/javascripts/models/post-test.js @@ -1,11 +1,12 @@ import Post from "discourse/models/post"; import User from "discourse/models/user"; +import { merge } from "discourse-common/lib/object"; QUnit.module("model: Post"); var buildPost = function(args) { return Post.create( - _.merge( + merge( { id: 1, can_delete: true,